Aethelgard
longcat 2.0 · full

Browser check: Artifact runtime error. This is a technical check, not a quality review.
Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 121,957 bytes · SHA-256 9af9ecdfc04c
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Aethelgard — Resonant Conductor</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<style>
/* ========== RESET & BASE ========== */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; overflow: hidden; font-family: 'Inter', sans-serif; background: #0a0a14; color: #e2e8f0; }
.font-pixel { font-family: 'Press Start 2P', monospace; }
/* ========== BACKGROUND ========== */
.bg-game {
background: radial-gradient(ellipse at 20% 50%, rgba(88, 28, 135, 0.25) 0%, transparent 60%),
radial-gradient(ellipse at 80% 20%, rgba(15, 118, 110, 0.2) 0%, transparent 50%),
radial-gradient(ellipse at 50% 80%, rgba(124, 58, 237, 0.15) 0%, transparent 60%),
#0a0a14;
}
.bg-stars {
position: fixed; inset: 0; pointer-events: none; z-index: 0;
}
.bg-stars::before, .bg-stars::after {
content: ''; position: absolute; inset: 0;
background-image:
radial-gradient(1px 1px at 10% 20%, rgba(255,255,255,0.6) 0%, transparent 100%),
radial-gradient(1px 1px at 30% 70%, rgba(255,255,255,0.4) 0%, transparent 100%),
radial-gradient(1.5px 1.5px at 50% 10%, rgba(255,255,255,0.5) 0%, transparent 100%),
radial-gradient(1px 1px at 70% 50%, rgba(255,255,255,0.3) 0%, transparent 100%),
radial-gradient(1.5px 1.5px at 90% 80%, rgba(255,255,255,0.5) 0%, transparent 100%),
radial-gradient(1px 1px at 15% 90%, rgba(255,255,255,0.3) 0%, transparent 100%),
radial-gradient(1px 1px at 85% 30%, rgba(255,255,255,0.4) 0%, transparent 100%),
radial-gradient(1px 1px at 45% 45%, rgba(255,255,255,0.2) 0%, transparent 100%);
}
.bg-stars::after { animation: twinkle 4s ease-in-out infinite alternate; }
@keyframes twinkle { 0% { opacity: 0.5; } 100% { opacity: 1; } }
/* ========== GLASSMORPHISM ========== */
.glass {
background: rgba(15, 15, 30, 0.7);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 16px;
}
.glass-light {
background: rgba(30, 30, 60, 0.5);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 12px;
}
/* ========== ELEMENT BORDERS & GLOW ========== */
.el-pyro { --el: rgba(255,107,53,0.7); border-color: var(--el) !important; box-shadow: 0 0 15px rgba(255,107,53,0.3), inset 0 0 15px rgba(255,107,53,0.1); }
.el-hydro { --el: rgba(0,188,212,0.7); border-color: var(--el) !important; box-shadow: 0 0 15px rgba(0,188,212,0.3), inset 0 0 15px rgba(0,188,212,0.1); }
.el-flora { --el: rgba(76,175,80,0.7); border-color: var(--el) !important; box-shadow: 0 0 15px rgba(76,175,80,0.3), inset 0 0 15px rgba(76,175,80,0.1); }
.el-zephyr { --el: rgba(129,212,250,0.7); border-color: var(--el) !important; box-shadow: 0 0 15px rgba(129,212,250,0.3), inset 0 0 15px rgba(129,212,250,0.1); }
.el-earth { --el: rgba(141,110,99,0.7); border-color: var(--el) !important; box-shadow: 0 0 15px rgba(141,110,99,0.3), inset 0 0 15px rgba(141,110,99,0.1); }
.el-volt { --el: rgba(255,235,59,0.7); border-color: var(--el) !important; box-shadow: 0 0 15px rgba(255,235,59,0.3), inset 0 0 15px rgba(255,235,59,0.1); }
.el-cosmos { --el: rgba(206,147,216,0.7); border-color: var(--el) !important; box-shadow: 0 0 15px rgba(206,147,216,0.3), inset 0 0 15px rgba(206,147,216,0.1); }
.glow-pyro { box-shadow: 0 0 20px rgba(255,107,53,0.5), 0 0 40px rgba(255,107,53,0.2); }
.glow-hydro { box-shadow: 0 0 20px rgba(0,188,212,0.5), 0 0 40px rgba(0,188,212,0.2); }
.glow-flora { box-shadow: 0 0 20px rgba(76,175,80,0.5), 0 0 40px rgba(76,175,80,0.2); }
.glow-zephyr { box-shadow: 0 0 20px rgba(129,212,250,0.5), 0 0 40px rgba(129,212,250,0.2); }
.glow-earth { box-shadow: 0 0 20px rgba(141,110,99,0.5), 0 0 40px rgba(141,110,99,0.2); }
.glow-volt { box-shadow: 0 0 20px rgba(255,235,59,0.5), 0 0 40px rgba(255,235,59,0.2); }
.glow-cosmos { box-shadow: 0 0 20px rgba(206,147,216,0.5), 0 0 40px rgba(206,147,216,0.2); }
.text-pyro { color: #FF6B35; } .text-hydro { color: #00BCD4; } .text-flora { color: #4CAF50; }
.text-zephyr { color: #81D4FA; } .text-earth { color: #A1887F; } .text-volt { color: #FFEB3B; } .text-cosmos { color: #CE93D8; }
/* ========== ANIMATIONS ========== */
@keyframes bounce-mon {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
20% { transform: translateX(-8px); }
40% { transform: translateX(8px); }
60% { transform: translateX(-5px); }
80% { transform: translateX(5px); }
}
@keyframes flash {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
@keyframes slideInLeft { from { opacity: 0; transform: translateX(-60px); } to { opacity: 1; transform: translateX(0); } }
@keyframes slideInRight { from { opacity: 0; transform: translateX(60px); } to { opacity: 1; transform: translateX(0); } }
@keyframes slideInUp { from { opacity: 0; transform: translateY(40px); } to { opacity: 1; transform: translateY(0); } }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes pulse-glow {
0%, 100% { box-shadow: 0 0 20px var(--glow, rgba(168,85,247,0.5)); }
50% { box-shadow: 0 0 40px var(--glow, rgba(168,85,247,0.8)); }
}
@keyframes bar-fill { from { width: 0%; } }
@keyframes float {
0%, 100% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-15px) rotate(5deg); }
}
@keyframes particle-rise {
from { opacity: 1; transform: translateY(0) scale(1); }
to { opacity: 0; transform: translateY(-80px) scale(0.3); }
}
@keyframes levelUp {
0% { transform: scale(1); filter: brightness(1); }
50% { transform: scale(1.3); filter: brightness(2); }
100% { transform: scale(1); filter: brightness(1); }
}
@keyframes capture-shake {
0%, 100% { transform: rotate(0deg); }
25% { transform: rotate(-15deg); }
75% { transform: rotate(15deg); }
}
@keyframes capture-glow {
0%, 100% { opacity: 0.5; transform: scale(1); }
50% { opacity: 1; transform: scale(1.1); }
}
.anim-bounce { animation: bounce-mon 2s ease-in-out infinite; }
.anim-shake { animation: shake 0.4s ease-in-out; }
.anim-flash { animation: flash 0.3s ease-in-out 2; }
.anim-slide-left { animation: slideInLeft 0.5s ease-out; }
.anim-slide-right { animation: slideInRight 0.5s ease-out; }
.anim-slide-up { animation: slideInUp 0.4s ease-out; }
.anim-fade { animation: fadeIn 0.4s ease-out; }
.anim-float { animation: float 4s ease-in-out infinite; }
.anim-pulse-glow { animation: pulse-glow 2s ease-in-out infinite; }
.anim-levelup { animation: levelUp 0.8s ease-out; }
.anim-capture { animation: capture-shake 0.6s ease-in-out infinite; }
.anim-capture-glow { animation: capture-glow 1.2s ease-in-out infinite; }
/* ========== TRANSITIONS ========== */
.screen { display: none; opacity: 0; transition: opacity 0.4s ease, transform 0.4s ease; transform: scale(0.97); }
.screen.active { display: flex; opacity: 1; transform: scale(1); }
.screen.slide-out-left { animation: slideOutLeft 0.3s forwards; }
.screen.slide-out-right { animation: slideOutRight 0.3s forwards; }
@keyframes slideOutLeft { to { opacity: 0; transform: translateX(-100px); } }
@keyframes slideOutRight { to { opacity: 0; transform: translateX(100px); } }
/* ========== BUTTONS ========== */
.btn-game {
position: relative; padding: 10px 24px; border: 1px solid rgba(255,255,255,0.15);
border-radius: 10px; font-family: 'Inter', sans-serif; font-weight: 600;
font-size: 13px; cursor: pointer; transition: all 0.2s;
background: rgba(30,30,60,0.6); color: #e2e8f0;
display: inline-flex; align-items: center; gap: 8px;
}
.btn-game:hover { background: rgba(50,50,100,0.7); border-color: rgba(255,255,255,0.3); transform: translateY(-1px); }
.btn-game:active { transform: translateY(1px); }
.btn-game:disabled { opacity: 0.4; cursor: not-allowed; }
.btn-primary { background: linear-gradient(135deg, rgba(139,92,246,0.5), rgba(168,85,247,0.3)); border-color: rgba(168,85,257,0.5); }
.btn-primary:hover { background: linear-gradient(135deg, rgba(139,92,246,0.7), rgba(168,85,247,0.5)); }
.btn-danger { background: linear-gradient(135deg, rgba(239,68,68,0.4), rgba(220,38,38,0.2)); border-color: rgba(239,68,68,0.4); }
.btn-success { background: linear-gradient(135deg, rgba(34,197,94,0.4), rgba(16,185,129,0.2)); border-color: rgba(34,197,94,0.4); }
/* ========== HP BAR ========== */
.hp-bar-track { height: 10px; border-radius: 5px; background: rgba(0,0,0,0.4); overflow: hidden; position: relative; border: 1px solid rgba(255,255,255,0.1); }
.hp-bar-fill { height: 100%; border-radius: 4px; transition: width 0.6s ease, background-color 0.3s; background: linear-gradient(90deg, #4CAF50, #8BC34A); }
.hp-bar-fill.warn { background: linear-gradient(90deg, #FDD835, #FFB300); }
.hp-bar-fill.danger { background: linear-gradient(90deg, #EF5350, #E53935); }
.xp-bar-track { height: 6px; border-radius: 3px; background: rgba(0,0,0,0.4); overflow: hidden; }
.xp-bar-fill { height: 100%; background: linear-gradient(90deg, #2196F3, #00BCD4); border-radius: 2px; transition: width 0.6s ease; }
/* ========== NODE MAP ========== */
.node {
width: 56px; height: 56px; border-radius: 50%; display: flex; align-items: center; justify-content: center;
cursor: pointer; transition: all 0.3s; border: 2px solid rgba(255,255,255,0.2);
background: rgba(20,20,45,0.8); position: relative; z-index: 2;
}
.node:hover:not(.locked):not(.completed) { transform: scale(1.15); border-color: rgba(168,85,247,0.7); }
.node.completed { border-color: #4CAF50; background: rgba(76,175,80,0.2); }
.node.locked { opacity: 0.3; cursor: not-allowed; border-color: rgba(255,255,255,0.1); }
.node.current { border-color: #A855F7; box-shadow: 0 0 20px rgba(168,85,247,0.5); transform: scale(1.1); }
.node.boss-node { width: 64px; height: 64px; }
.node svg { width: 28px; height: 28px; }
.node-connector {
position: absolute; background: rgba(168,85,247,0.3); height: 3px; z-index: 1;
transform-origin: left center;
}
.node-connector.active { background: rgba(76,175,80,0.6); }
/* ========== BATTLE BG ========== */
.battle-bg {
background: radial-gradient(ellipse at 50% 100%, rgba(168,85,247,0.15) 0%, transparent 60%),
radial-gradient(ellipse at 30% 30%, rgba(15,118,110,0.2) 0%, transparent 50%),
linear-gradient(180deg, #1a1a2e 0%, #0f0f1a 100%);
}
/* ========== PARTICLES ========== */
.particle {
position: absolute; pointer-events: none; animation: particle-rise 1.2s ease-out forwards;
border-radius: 50%;
}
/* ========== TOOLTIP ========== */
.tooltip {
position: absolute; background: rgba(10,10,25,0.95); border: 1px solid rgba(255,255,255,0.15);
border-radius: 8px; padding: 8px 12px; font-size: 12px; z-index: 100;
pointer-events: none; white-space: nowrap; backdrop-filter: blur(8px);
}
/* ========== SCROLLBAR ========== */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: rgba(0,0,0,0.2); }
::-webkit-scrollbar-thumb { background: rgba(168,85,247,0.4); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: rgba(168,85,247,0.6); }
/* ========== DEX GRID ========== */
.dex-card { transition: all 0.3s; }
.dex-card:hover { transform: translateY(-4px); }
.dex-card.undiscovered { filter: brightness(0.2); }
/* ========== NOTIFICATION ========== */
.notification { animation: slideInUp 0.4s ease, fadeIn 0.4s ease; }
/* ========== BADGES ========== */
.badge {
display: inline-flex; align-items: center; justify-content: center;
padding: 2px 8px; border-radius: 6px; font-size: 10px; font-weight: 700; letter-spacing: 0.5px;
}
.badge-pyro { background: rgba(255,107,53,0.2); color: #FF6B35; border: 1px solid rgba(255,107,53,0.4); }
.badge-hydro { background: rgba(0,188,212,0.2); color: #00BCD4; border: 1px solid rgba(0,188,212,0.4); }
.badge-flora { background: rgba(76,175,80,0.2); color: #4CAF50; border: 1px solid rgba(76,175,80,0.4); }
.badge-zephyr { background: rgba(129,212,250,0.2); color: #81D4FA; border: 1px solid rgba(129,212,250,0.4); }
.badge-earth { background: rgba(141,110,99,0.2); color: #A1887F; border: 1px solid rgba(141,110,99,0.4); }
.badge-volt { background: rgba(255,235,59,0.2); color: #FFEB3B; border: 1px solid rgba(255,235,59,0.4); }
.badge-cosmos { background: rgba(206,147,216,0.2); color: #CE93D8; border: 1px solid rgba(206,147,216,0.4); }
/* ========== STATUS ICON ========== */
.status-icon { width: 18px; height: 18px; border-radius: 4px; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; }
.status-burn { background: rgba(255,107,53,0.3); color: #FF6B35; }
.status-wet { background: rgba(0,188,212,0.3); color: #00BCD4; }
.status-spore { background: rgba(76,175,80,0.3); color: #4CAF50; }
.status-stun { background: rgba(255,235,59,0.3); color: #FFEB3B; }
</style>
</head>
<body class="bg-game min-h-screen flex items-center justify-center">
<div class="bg-stars"></div>
<!-- ==================== APP CONTAINER ==================== -->
<div id="app" class="relative w-full max-w-[1200px] h-[95vh] mx-auto flex flex-col" style="z-index:1;">
<!-- TOP BAR (visible during game) -->
<div id="topbar" class="hidden flex items-center justify-between px-5 py-3 mb-2 fade-in">
<div class="flex items-center gap-3">
<button onclick="goHome()" class="btn-game !px-3 !py-2 text-xs">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/></svg>
Camp
</button>
<span id="zone-label" class="font-pixel text-[10px] text-purple-300"></span>
</div>
<div class="flex items-center gap-4">
<div class="flex items-center gap-2 glass-light px-3 py-1.5 rounded-lg">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#A855F7" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
<span id="shards-display" class="font-pixel text-[10px] text-purple-300">0</span>
</div>
<button onclick="openShop()" class="btn-game !px-3 !py-2 text-xs">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 01-8 0"/></svg>
Shop
</button>
<button onclick="openDex()" class="btn-game !px-3 !py-2 text-xs">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 016.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z"/></svg>
Dex
</button>
<button onclick="openParty()" class="btn-game !px-3 !py-2 text-xs">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4-4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 00-3-3.87"/><path d="M16 3.13a4 4 0 010 7.75"/></svg>
Party
</button>
</div>
</div>
<!-- ==================== MAIN MENU ==================== -->
<div id="screen-menu" class="screen active flex-col items-center justify-center gap-8 absolute inset-0">
<div class="text-center">
<h1 class="font-pixel text-3xl md:text-4xl mb-3" style="color: #A855F7; text-shadow: 0 0 30px rgba(168,85,247,0.6);">AETHELGARD</h1>
<p class="font-pixel text-[10px] text-purple-300 tracking-widest">RESONANT CONDUCTOR</p>
</div>
<div class="anim-float flex justify-center my-4">
<svg width="180" height="180" viewBox="0 0 180 180">
<defs>
<radialGradient id="logo-bg" cx="50%" cy="50%" r="50%">
<stop offset="0%" style="stop-color:rgba(168,85,247,0.4)"/>
<stop offset="100%" style="stop-color:rgba(139,92,246,0)"/>
</radialGradient>
<linearGradient id="logo-outline" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#A855F7"/>
<stop offset="100%" style="stop-color:#00BCD4"/>
</linearGradient>
</defs>
<circle cx="90" cy="90" r="70" fill="url(#logo-bg)"/>
<polygon points="90,25 135,50 135,110 90,150 45,110 45,50" fill="none" stroke="url(#logo-outline)" stroke-width="2.5" opacity="0.9"/>
<polygon points="90,40 120,58 120,102 90,128 60,102 60,58" fill="rgba(168,85,247,0.15)" stroke="url(#logo-outline)" stroke-width="1.5" opacity="0.7"/>
<circle cx="90" cy="80" r="15" fill="rgba(168,85,247,0.3)" stroke="#A855F7" stroke-width="1"/>
<circle cx="90" cy="80" r="6" fill="#A855F7"/>
<line x1="90" y1="25" x2="90" y2="55" stroke="url(#logo-outline)" stroke-width="1.5" opacity="0.6"/>
<line x1="135" y1="50" x2="115" y2="65" stroke="url(#logo-outline)" stroke-width="1.5" opacity="0.6"/>
<line x1="45" y1="50" x2="65" y2="65" stroke="url(#logo-outline)" stroke-width="1.5" opacity="0.6"/>
</svg>
</div>
<div class="flex flex-col gap-3 w-64">
<button onclick="startNewGame()" class="btn-game btn-primary w-full justify-center py-3 font-pixel text-[10px]">NEW JOURNEY</button>
<button onclick="continueGame()" id="btn-continue" class="btn-game w-full justify-center py-3 font-pixel text-[10px] opacity-40" disabled>CONTINUE</button>
<button onclick="openSettings()" class="btn-game w-full justify-center py-3 font-pixel text-[10px]">OPTIONS</button>
</div>
<p class="text-xs text-gray-500 mt-4">A world of elemental resonance awaits</p>
</div>
<!-- ==================== ZONE SELECT ==================== -->
<div id="screen-zoneselect" class="screen flex-col items-center justify-center px-6 absolute inset-0">
<h2 class="font-pixel text-lg text-purple-300 mb-2 anim-slide-up">SELECT ZONE</h2>
<p class="text-sm text-gray-400 mb-6">Choose your path, Conductor.</p>
<div id="zone-grid" class="grid grid-cols-1 sm:grid-cols-2 gap-4 w-full max-w-2xl"></div>
<button onclick="showScreen('screen-menu')" class="btn-game mt-6"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 12H5M12 19l-7-7 7-7"/></svg> Back</button>
</div>
<!-- ==================== OVERWORLD MAP ==================== -->
<div id="screen-overworld" class="screen flex-col items-center px-4 py-2 absolute inset-0">
<div class="w-full max-w-3xl flex items-center justify-between mb-4">
<button onclick="showScreen('screen-zoneselect')" class="btn-game !px-3 !py-2 text-xs">← Zones</button>
<h3 id="map-title" class="font-pixel text-[11px] text-purple-300"></h3>
<div class="text-xs text-gray-400" id="map-progress"></div>
</div>
<div id="node-map" class="glass flex-1 w-full max-w-3xl relative overflow-hidden rounded-2xl border border-white/10"></div>
</div>
<!-- ==================== BATTLE SCREEN ==================== -->
<div id="screen-battle" class="screen flex-col absolute inset-0">
<div class="battle-bg w-full flex-1 flex flex-col items-center justify-between px-4 py-3 relative overflow-hidden">
<!-- Battle field -->
<div class="w-full max-w-3xl flex justify-between items-start mt-2">
<!-- Player's monster -->
<div class="anim-slide-left flex items-center gap-3" id="player-mon-side">
<div id="player-sprite-wrap" class="anim-bounce"></div>
<div class="glass-light p-3 rounded-xl min-w-[200px]">
<div class="flex items-center justify-between mb-1">
<span id="player-mon-name" class="font-semibold text-sm"></span>
<span id="player-mon-level" class="text-xs text-gray-400">Lv.1</span>
</div>
<div class="hp-bar-track mb-1">
<div id="player-mon-hp-bar" class="hp-bar-fill"></div>
</div>
<div class="flex justify-between text-[10px] text-gray-400 mb-2">
<span id="player-mon-hp-text">HP: 0/0</span>
</div>
<div class="xp-bar-track">
<div id="player-mon-xp-bar" class="xp-bar-fill"></div>
</div>
<div id="player-mon-status" class="flex gap-1 mt-1"></div>
</div>
</div>
<!-- Enemy's monster -->
<div class="anim-slide-right flex items-center gap-3" id="enemy-mon-side">
<div class="glass-light p-3 rounded-xl min-w-[200px]">
<div class="flex items-center justify-between mb-1">
<span id="enemy-mon-name" class="font-semibold text-sm"></span>
<span id="enemy-mon-level" class="text-xs text-gray-400">Lv.1</span>
</div>
<div class="hp-bar-track mb-1">
<div id="enemy-mon-hp-bar" class="hp-bar-fill"></div>
</div>
<div class="flex justify-between text-[10px] text-gray-400 mb-1">
<span id="enemy-mon-hp-text">HP: 0/0</span>
</div>
<div id="enemy-mon-status" class="flex gap-1 mt-1"></div>
</div>
<div id="enemy-sprite-wrap" class="anim-bounce"></div>
</div>
</div>
<!-- Particle layer -->
<div id="battle-particles" class="absolute inset-0 pointer-events-none z-10"></div>
<!-- Battle log / bottom UI -->
<div class="w-full max-w-3xl glass rounded-xl p-4 mb-2 relative z-20">
<div id="battle-log" class="text-sm text-gray-300 min-h-[40px] mb-3 px-2"></div>
<div id="battle-actions" class="grid grid-cols-2 md:grid-cols-4 gap-2"></div>
</div>
</div>
</div>
<!-- ==================== PARTY SCREEN ==================== -->
<div id="screen-party" class="screen flex-col items-center px-6 py-4 absolute inset-0 overflow-y-auto">
<div class="w-full max-w-3xl">
<div class="flex items-center justify-between mb-6">
<button onclick="goHome()" class="btn-game !px-3 !py-2 text-xs"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 12H5M12 19l-7-7 7-7"/></svg> Back</button>
<h2 class="font-pixel text-sm text-purple-300">YOUR PARTY</h2>
<div></div>
</div>
<div id="party-grid" class="grid grid-cols-1 md:grid-cols-3 gap-4"></div>
<div class="mt-6">
<h3 class="font-pixel text-[11px] text-purple-300 mb-3">STORAGE BOX</h3>
<div id="storage-grid" class="grid grid-cols-3 md:grid-cols-6 gap-2"></div>
</div>
</div>
</div>
<!-- ==================== SHOP ==================== -->
<div id="screen-shop" class="screen flex-col items-center px-6 py-4 absolute inset-0 overflow-y-auto">
<div class="w-full max-w-2xl">
<div class="flex items-center justify-between mb-6">
<button onclick="goHome()" class="btn-game !px-3 !py-2 text-xs"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 12H5M12 19l-7-7 7-7"/></svg> Back</button>
<h2 class="font-pixel text-sm text-purple-300">CAMP SHOP</h2>
<div class="glass-light px-3 py-1.5 rounded-lg flex items-center gap-2">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#A855F7" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
<span id="shop-shards" class="font-pixel text-[10px] text-purple-300">0</span>
</div>
</div>
<div id="shop-items" class="grid grid-cols-1 sm:grid-cols-2 gap-4"></div>
</div>
</div>
<!-- ==================== DEX ==================== -->
<div id="screen-dex" class="screen flex-col items-center px-6 py-4 absolute inset-0 overflow-y-auto">
<div class="w-full max-w-4xl">
<div class="flex items-center justify-between mb-6">
<button onclick="goHome()" class="btn-game !px-3 !py-2 text-xs"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 12H5M12 19l-7-7 7-7"/></svg> Back</button>
<h2 class="font-pixel text-sm text-purple-300">CONDUCTOR'S DEX</h2>
<div id="dex-count" class="text-xs text-gray-400 font-pixel text-[9px]"></div>
</div>
<!-- Dex detail popup -->
<div id="dex-detail" class="hidden mb-4"></div>
<div id="dex-grid" class="grid grid-cols-3 md:grid-cols-6 gap-3"></div>
</div>
</div>
<!-- ==================== SETTINGS ==================== -->
<div id="screen-settings" class="screen flex-col items-center justify-center px-6 absolute inset-0">
<div class="glass p-8 rounded-2xl max-w-md w-full">
<h2 class="font-pixel text-sm text-purple-300 mb-6 text-center">OPTIONS</h2>
<div class="space-y-4">
<div class="flex items-center justify-between">
<span class="text-sm">Sound Effects</span>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" id="toggle-sfx" checked class="sr-only peer">
<div class="w-10 h-5 bg-gray-700 peer-checked:bg-purple-600 rounded-full after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:after:translate-x-full"></div>
</label>
</div>
<div class="flex items-center justify-between">
<span class="text-sm">Music</span>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" id="toggle-music" checked class="sr-only peer">
<div class="w-10 h-5 bg-gray-700 peer-checked:bg-purple-600 rounded-full after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:after:translate-x-full"></div>
</label>
</div>
<button onclick="resetProgress()" class="btn-game btn-danger w-full justify-center mt-6 text-xs">RESET ALL PROGRESS</button>
<button onclick="showScreen('screen-menu')" class="btn-game w-full justify-center text-xs">BACK</button>
</div>
</div>
</div>
<!-- ==================== NOTIFICATION ==================== -->
<div id="notification" class="hidden fixed top-4 left-1/2 -translate-x-1/2 z-50 glass px-5 py-3 rounded-xl border border-purple-500/30 text-sm"></div>
<!-- ==================== LEVEL UP MODAL ==================== -->
<div id="levelup-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div class="glass p-8 rounded-2xl text-center max-w-sm">
<div id="levelup-content"></div>
</div>
</div>
</div><!-- #app -->
<!-- Pick starter overlay -->
<div id="screen-starter" class="screen flex-col items-center justify-center px-6 absolute inset-0 bg-game" style="z-index:5;">
<!-- Dynamically populated -->
</div>
<!-- Battle result overlay -->
<div id="battle-result-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div class="glass p-8 rounded-2xl text-center max-w-sm">
<div id="battle-result-content"></div>
</div>
</div>
<script>
// ============================================================
// AETHELGARD — RESONANT CONDUCTOR
// Single-file turn-based monster collection RPG
// ============================================================
// ========== AUDIO SYNTH MANAGER ==========
class SynthManager {
constructor() {
this.ctx = null;
this.sfxEnabled = true;
this.musicEnabled = true;
this.musicNodes = [];
this.musicInterval = null;
}
init() {
if (this.ctx) return;
try {
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
} catch (e) {
console.warn('AudioContext not available');
}
}
resume() {
if (this.ctx && this.ctx.state === 'suspended') this.ctx.resume();
}
_tone(freq, duration, type = 'square', vol = 0.08, slide = 0) {
if (!this.ctx || !this.sfxEnabled) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, this.ctx.currentTime);
if (slide) osc.frequency.linearRampToValueAtTime(freq + slide, this.ctx.currentTime + duration);
gain.gain.setValueAtTime(vol, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + duration);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(this.ctx.currentTime);
osc.stop(this.ctx.currentTime + duration);
}
_noise(duration, vol = 0.05) {
if (!this.ctx || !this.sfxEnabled) return;
const bufferSize = this.ctx.sampleRate * duration;
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / bufferSize, 2);
const source = this.ctx.createBufferSource();
source.buffer = buffer;
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(vol, this.ctx.currentTime);
source.connect(gain);
gain.connect(this.ctx.destination);
source.start();
}
hover() { this._tone(600, 0.06, 'sine', 0.04); }
select() { this._tone(800, 0.08, 'square', 0.06); this._tone(1000, 0.08, 'square', 0.04); }
hitPhysical() { this._noise(0.12, 0.08); this._tone(200, 0.1, 'sawtooth', 0.06, -100); }
hitElemental() { this._tone(400, 0.15, 'sine', 0.07, 200); this._tone(600, 0.15, 'triangle', 0.05, 100); this._noise(0.1, 0.06); }
hitSuper() { this._tone(300, 0.2, 'sine', 0.08, 400); this._tone(600, 0.2, 'triangle', 0.06, 200); this._noise(0.15, 0.08); }
miss() { this._tone(150, 0.15, 'sine', 0.04, -50); }
levelUp() {
const notes = [523, 659, 784, 1047];
notes.forEach((n, i) => setTimeout(() => this._tone(n, 0.2, 'square', 0.06), i * 100));
}
captureStart() {
this._tone(800, 0.1, 'triangle', 0.05);
setTimeout(() => this._tone(1000, 0.1, 'triangle', 0.05), 120);
setTimeout(() => this._tone(1200, 0.15, 'triangle', 0.05), 240);
}
captureSuccess() {
const notes = [523, 587, 659, 698, 784, 1047];
notes.forEach((n, i) => setTimeout(() => this._tone(n, 0.25, 'square', 0.06), i * 80));
}
captureFail() {
this._tone(600, 0.2, 'sawtooth', 0.05, -200);
setTimeout(() => this._tone(300, 0.3, 'sawtooth', 0.04, -100), 200);
}
heal() {
const notes = [440, 554, 659, 554, 659, 880];
notes.forEach((n, i) => setTimeout(() => this._tone(n, 0.18, 'sine', 0.05), i * 80));
}
error() { this._tone(200, 0.15, 'sawtooth', 0.06, -80); }
victory() {
const notes = [523, 659, 784, 1047, 784, 1047];
notes.forEach((n, i) => setTimeout(() => this._tone(n, 0.2, 'square', 0.07), i * 120));
}
runAway() { this._tone(600, 0.1, 'sine', 0.05, -300); this._tone(300, 0.15, 'sine', 0.04, -200); }
startMusic() {
if (!this.ctx || !this.musicEnabled || this.musicInterval) return;
const melody = [262, 294, 330, 392, 330, 294, 349, 392, 349, 330, 294, 262, 330, 392, 440, 392];
let note = 0;
const beat = () => {
if (!this.musicEnabled) return;
const n = melody[note % melody.length];
this._tone(n, 0.3, 'sine', 0.03);
this._tone(n / 2, 0.3, 'triangle', 0.02);
note++;
};
beat();
this.musicInterval = setInterval(beat, 450);
}
stopMusic() {
if (this.musicInterval) { clearInterval(this.musicInterval); this.musicInterval = null; }
}
}
const synth = new SynthManager();
// ========== TYPE EFFECTIVENESS ==========
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 getTypeMultiplier(attackType, defenderTypes) {
let mult = 1;
for (const dt of defenderTypes) {
if (TYPE_CHART[attackType]?.strong?.includes(dt)) mult *= 2;
if (TYPE_CHART[attackType]?.weak?.includes(dt)) mult *= 0.5;
}
return mult;
}
// Badge class helper
function elBadgeClass(el) { return `badge-${el}`; }
function elBorderClass(el) { return `el-${el}`; }
function elGlowClass(el) { return `glow-${el}`; }
function elTextClass(el) { return `text-${el}`; }
// Color palettes per element for SVG rendering
const EL_COLORS = {
pyro: { primary: '#FF6B35', secondary: '#FF3D00', accent: '#FFD54F', glow: '#FF6B35' },
hydro: { primary: '#00BCD4', secondary: '#0097A7', accent: '#B2EBF2', glow: '#00BCD4' },
flora: { primary: '#4CAF50', secondary: '#388E3C', accent: '#C8E6C9', glow: '#4CAF50' },
zephyr: { primary: '#81D4FA', secondary: '#29B6F6', accent: '#E1F5FE', glow: '#81D4FA' },
earth: { primary: '#A1887F', secondary: '#795548', accent: '#D7CCC8', glow: '#A1887F' },
volt: { primary: '#FFEB3B', secondary: '#FBC02D', accent: '#FFF9C4', glow: '#FFEB3B' },
cosmos: { primary: '#CE93D8', secondary: '#AB47BC', accent: '#F3E5F5', glow: '#CE93D8' }
};
// ============================================================
// RESONANTS DATABASE (18 creatures)
// ============================================================
// Each has: id, name, element, tier, baseStats, moves[], evolution, description
const MOVES_DB = [
// Universal moves
{ id: 'tackle', name: 'Tackle', power: 35, accuracy: 90, type: 'cosmos', category: 'physical', desc: 'A basic body blow.', effect: null, level: 1 },
{ id: 'rush', name: 'Quick Rush', power: 40, accuracy: 95, type: 'cosmos', category: 'physical', desc: 'A swift tackle attack.', effect: null, level: 2 },
{ id: 'charge', name: 'Power Charge', power: 55, accuracy: 85, type: 'cosmos', category: 'physical', desc: 'Gathers energy for a strong hit.', effect: null, level: 5 },
{ id: 'crush', name: 'Skull Crush', power: 65, accuracy: 80, type: 'cosmos', category: 'physical', desc: 'A devastating close-range slam.', effect: null, level: 10 },
{ id: 'focus', name: 'Focus Energy', power: 0, accuracy: 100, type: 'cosmos', category: 'support', desc: 'Sharply raises attack.', effect: { stat: 'attack', stages: 2 }, level: 4 },
{ id: 'shield', name: 'Iron Guard', power: 0, accuracy: 100, type: 'cosmos', category: 'support', desc: 'Raises defense.', effect: { stat: 'defense', stages: 2 }, level: 4 },
// Pyro moves
{ id: 'ember', name: 'Ember', power: 40, accuracy: 100, type: 'pyro', category: 'special', desc: 'Tongue of searing flame.', effect: { status: 'burn', chance: 0.3 }, level: 1 },
{ id: 'flame-jet', name: 'Flame Jet', power: 55, accuracy: 90, type: 'pyro', category: 'special', desc: 'A concentrated jet of fire.', effect: { status: 'burn', chance: 0.2 }, level: 5 },
{ id: 'inferno', name: 'Inferno Burst', power: 80, accuracy: 75, type: 'pyro', category: 'special', desc: 'Engulfing infernal flames.', effect: { status: 'burn', chance: 0.4 }, level: 12 },
{ id: 'pyroclasm', name: 'Pyroclasm', power: 95, accuracy: 70, type: 'pyro', category: 'special', desc: 'Volcanic devastation unleashed.', effect: { status: 'burn', chance: 0.5 }, level: 18 },
// Hydro moves
{ id: 'water-gun', name: 'Aqua Pulse', power: 40, accuracy: 100, type: 'hydro', category: 'special', desc: 'Pressurized water blast.', effect: null, level: 1 },
{ id: 'tidal-wave', name: 'Tidal Wave', power: 60, accuracy: 85, type: 'hydro', category: 'special', desc: 'Cascading tidal force.', effect: { status: 'wet', chance: 0.3 }, level: 5 },
{ id: 'riptide', name: 'Riptide Torrent', power: 75, accuracy: 80, type: 'hydro', category: 'special', desc: 'Pulls enemies under.', effect: { status: 'wet', chance: 0.4 }, level: 12 },
{ id: 'tsunami', name: 'Abyssal Surge', power: 90, accuracy: 70, type: 'hydro', category: 'special', desc: 'Oceanic devastation.', effect: { status: 'wet', chance: 0.5 }, level: 18 },
// Flora moves
{ id: 'vine-whip', name: 'Vine Whip', power: 35, accuracy: 100, type: 'flora', category: 'physical', desc: 'Lashing thorny tendrils.', effect: null, level: 1 },
{ id: 'spore', name: 'Toxic Spore', power: 30, accuracy: 95, type: 'flora', category: 'special', desc: 'Releases toxic spores.', effect: { status: 'spore', chance: 0.6 }, level: 3 },
{ id: 'thorn-storm', name: 'Thorn Storm', power: 60, accuracy: 85, type: 'flora', category: 'physical', desc: 'Scattering razor thorns.', effect: { status: 'spore', chance: 0.3 }, level: 6 },
{ id: 'giga-drain', name: 'Giga Bloom', power: 70, accuracy: 85, type: 'flora', category: 'special', desc: 'Drains life force.', effect: { heal: 0.5 }, level: 10 },
{ id: 'petal-barrage', name: 'Petal Barrage', power: 85, accuracy: 80, type: 'flora', category: 'special', desc: 'A storm of razor petals.', effect: { status: 'spore', chance: 0.4 }, level: 16 },
// Volt moves
{ id: 'spark', name: 'Spark', power: 40, accuracy: 100, type: 'volt', category: 'special', desc: 'A jolt of electricity.', effect: { status: 'stun', chance: 0.2 }, level: 1 },
{ id: 'thunderbolt', name: 'Thunderbolt', power: 60, accuracy: 85, type: 'volt', category: 'special', desc: 'Crackling electric strike.', effect: { status: 'stun', chance: 0.3 }, level: 5 },
{ id: 'plasma', name: 'Plasma Arc', power: 75, accuracy: 80, type: 'volt', category: 'special', desc: 'Arcing plasma chains.', effect: { status: 'stun', chance: 0.4 }, level: 11 },
{ id: 'megavolt', name: 'Megavolt Strike', power: 95, accuracy: 70, type: 'volt', category: 'special', desc: 'Cataclysmic lightning burst.', effect: { status: 'stun', chance: 0.5 }, level: 18 },
// Zephyr moves
{ id: 'gust', name: 'Gust', power: 30, accuracy: 100, type: 'zephyr', category: 'special', desc: 'A swift cutting breeze.', effect: null, level: 1 },
{ id: 'air-blade', name: 'Air Blade', power: 50, accuracy: 95, type: 'zephyr', category: 'special', desc: 'Compressed air slash.', effect: null, level: 4 },
{ id: 'cyclone', name: 'Cyclone Crash', power: 70, accuracy: 80, type: 'zephyr', category: 'special', desc: 'Devastating rotating winds.', effect: null, level: 10 },
{ id: 'maelstrom', name: 'Maelstrom', power: 90, accuracy: 75, type: 'zephyr', category: 'special', desc: 'Category-6 windstorm.', effect: null, level: 17 },
// Earth moves
{ id: 'pebble', name: 'Stone Fling', power: 35, accuracy: 100, type: 'earth', category: 'physical', desc: 'Hurls sharp rocks.', effect: null, level: 1 },
{ id: 'rockslide', name: 'Rockslide', power: 55, accuracy: 85, type: 'earth', category: 'physical', desc: 'Cascading boulders.', effect: null, level: 5 },
{ id: 'quake', name: 'Earthquake', power: 80, accuracy: 80, type: 'earth', category: 'physical', desc: 'The ground splits apart.', effect: null, level: 12 },
{ id: 'terrain', name: 'Tectonic Fury', power: 95, accuracy: 70, type: 'earth', category: 'physical', desc: 'Continental-scale power.', effect: null, level: 18 },
// Cosmos moves
{ id: 'void-pulse', name: 'Void Pulse', power: 45, accuracy: 95, type: 'cosmos', category: 'special', desc: 'Dispels nothingness.', effect: null, level: 1 },
{ id: 'starfall', name: 'Starfall', power: 60, accuracy: 90, type: 'cosmos', category: 'special', desc: 'Calls down a star shard.', effect: null, level: 6 },
{ id: 'nova', name: 'Cosmic Nova', power: 80, accuracy: 80, type: 'cosmos', category: 'special', desc: 'Stellar explosion.', effect: null, level: 14 }
];
const MOVE_LOOKUP = {};
MOVES_DB.forEach(m => { MOVE_LOOKUP[m.id] = m; });
const RESONANTS_DB = [
// ===== PYRO LINE =====
{
id: 'emberkit', name: 'Emberkit', element: 'pyro', tier: 1, level: 1,
baseStats: { hp: 35, attack: 22, defense: 18, speed: 28 },
moves: ['tackle', 'ember', 'flame-jet', 'inferno'],
evolvesTo: 'pyrowolf', evolveLevel: 8,
desc: 'A playful spark-beast that fuels the warmth of young Conductors. Its ears flicker like embers.',
hosts: ['pyro']
},
{
id: 'pyrowolf', name: 'Pyrowolf', element: 'pyro', tier: 2, level: 8,
baseStats: { hp: 58, attack: 42, defense: 32, speed: 45 },
moves: ['rush', 'flame-jet', 'inferno', 'pyroclasm'],
evolvesTo: 'ignisaur', evolveLevel: 15,
desc: 'A noble fire-pelt whose howl ignites the air. Commands packs in volcanic highlands.',
hosts: ['pyro']
},
{
id: 'ignisaur', name: 'Ignisaur', element: 'pyro', tier: 3, level: 15,
baseStats: { hp: 85, attack: 65, defense: 50, speed: 55 },
moves: ['crush', 'inferno', 'pyroclasm', 'charge'],
evolvesTo: null, evolveLevel: null,
desc: 'An ancient torch-beast whose molten crown dims the sun. Legends say it forged mountain ranges.',
hosts: ['pyro']
},
// ===== HYDRO LINE =====
{
id: 'droplet', name: 'Droplet', element: 'hydro', tier: 1, level: 1,
baseStats: { hp: 40, attack: 18, defense: 25, speed: 22 },
moves: ['tackle', 'water-gun', 'tidal-wave', 'riptide'],
evolvesTo: 'geoxus', evolveLevel: 8,
desc: 'A bubbly sprite of condensed dew. Leaves puddles wherever it hops.',
hosts: ['hydro']
},
{
id: 'geoxus', name: 'Geoxus', element: 'hydro', tier: 2, level: 8,
baseStats: { hp: 62, attack: 38, defense: 48, speed: 35 },
moves: ['rush', 'tidal-wave', 'riptide', 'tsunami'],
evolvesTo: 'neptunian', evolveLevel: 15,
desc: 'Shell-armored guardian of grotto depths. Its roar triggers underground springs.',
hosts: ['hydro']
},
{
id: 'neptunian', name: 'Neptunian', element: 'hydro', tier: 3, level: 15,
baseStats: { hp: 90, attack: 55, defense: 68, speed: 45 },
moves: ['crush', 'riptide', 'tsunami', 'charge'],
evolvesTo: null, evolveLevel: null,
desc: 'Sovereign of the abyssal tides. Its pearl crown commands ocean currents for miles.',
hosts: ['hydro']
},
// ===== FLORA LINE =====
{
id: 'seedling', name: 'Seedling', element: 'flora', tier: 1, level: 1,
baseStats: { hp: 38, attack: 20, defense: 22, speed: 25 },
moves: ['tackle', 'vine-whip', 'spore', 'thorn-storm'],
evolvesTo: 'thornback', evolveLevel: 8,
desc: 'A tender sapling-sprite. Nurtured sunlight drips from its leafy mane.',
hosts: ['flora']
},
{
id: 'thornback', name: 'Thornback', element: 'flora', tier: 2, level: 8,
baseStats: { hp: 60, attack: 40, defense: 42, speed: 38 },
moves: ['rush', 'vine-whip', 'giga-drain', 'petal-barrage'],
evolvesTo: 'bloomgoliath', evolveLevel: 15,
desc: 'A thorn-armored beast that entangles foes in creeping vegetation.',
hosts: ['flora']
},
{
id: 'bloomgoliath', name: 'Bloomgoliath', element: 'flora', tier: 3, level: 15,
baseStats: { hp: 85, attack: 58, defense: 60, speed: 40 },
moves: ['crush', 'giga-drain', 'petal-barrage', 'charge'],
evolvesTo: null, evolveLevel: null,
desc: 'A living forest on four legs. Its blooms bring spring and its roots split stone.',
hosts: ['flora']
},
// ===== VOLT LINE =====
{
id: 'sparky', name: 'Sparky', element: 'volt', tier: 1, level: 1,
baseStats: { hp: 32, attack: 28, defense: 15, speed: 35 },
moves: ['tackle', 'spark', 'thunderbolt', 'plasma'],
evolvesTo: 'boltsaber', evolveLevel: 8,
desc: 'A jittery orb of static. Fur stands perpetually on end.',
hosts: ['volt']
},
{
id: 'boltsaber', name: 'Boltsaber', element: 'volt', tier: 2, level: 8,
baseStats: { hp: 52, attack: 50, defense: 28, speed: 58 },
moves: ['rush', 'thunderbolt', 'plasma', 'megavolt'],
evolvesTo: 'stormlord', evolveLevel: 15,
desc: 'A sleek fang-beast crackling with electron arcs. Hunts with surgical electric strikes.',
hosts: ['volt']
},
{
id: 'stormlord', name: 'Stormlord', element: 'volt', tier: 3, level: 15,
baseStats: { hp: 72, attack: 72, defense: 42, speed: 75 },
moves: ['crush', 'plasma', 'megavolt', 'charge'],
evolvesTo: null, evolveLevel: null,
desc: 'Thunder incarnate. Its wingbeats generate cyclonic lightning storms.',
hosts: ['volt']
},
// ===== ZEPHYR LINE =====
{
id: 'gusty', name: 'Gusty', element: 'zephyr', tier: 1, level: 1,
baseStats: { hp: 34, attack: 20, defense: 18, speed: 32 },
moves: ['tackle', 'gust', 'air-blade', 'cyclone'],
evolvesTo: 'slipstream', evolveLevel: 8,
desc: 'A gentle puff spirit that rides cool breezes. Loves pinwheels.',
hosts: ['zephyr']
},
{
id: 'slipstream', name: 'Slipstream', element: 'zephyr', tier: 2, level: 8,
baseStats: { hp: 55, attack: 36, defense: 30, speed: 52 },
moves: ['rush', 'air-blade', 'cyclone', 'maelstrom'],
evolvesTo: 'zephyrus', evolveLevel: 15,
desc: 'A swift wind-dancer who leaves afterimages in brush strokes.',
hosts: ['zephyr']
},
{
id: 'zephyrus', name: 'Zephyrus', element: 'zephyr', tier: 3, level: 15,
baseStats: { hp: 78, attack: 55, defense: 48, speed: 80 },
moves: ['crush', 'cyclone', 'maelstrom', 'charge'],
evolvesTo: null, evolveLevel: null,
desc: 'Lord of the eternal gale. Its wingspan measures the horizon.',
hosts: ['zephyr']
},
// ===== EARTH LINE =====
{
id: 'pebble', name: 'Pebble', element: 'earth', tier: 1, level: 1,
baseStats: { hp: 42, attack: 22, defense: 28, speed: 18 },
moves: ['tackle', 'pebble', 'rockslide', 'quake'],
evolvesTo: 'obsidian', evolveLevel: 8,
desc: 'A tiny boulder given life. Roll-tucks into protective shapes.',
hosts: ['earth']
},
{
id: 'obsidian', name: 'Obsidian', element: 'earth', tier: 2, level: 8,
baseStats: { hp: 68, attack: 38, defense: 52, speed: 30 },
moves: ['rush', 'rockslide', 'quake', 'terrain'],
evolvesTo: 'terrasunder', evolveLevel: 15,
desc: 'A dark crystalline golem. Jade veins pulse with tectonic beats.',
hosts: ['earth']
},
{
id: 'terrasunder', name: 'Terrasunder', element: 'earth', tier: 3, level: 15,
baseStats: { hp: 95, attack: 60, defense: 75, speed: 38 },
moves: ['crush', 'quake', 'terrain', 'charge'],
evolvesTo: null, evolveLevel: null,
desc: "The mountain's wrath made flesh. Its footsteps reshape geography.",
hosts: ['earth']
}
];
const RESONANTS_LOOKUP = {};
RESONANTS_DB.forEach(r => { RESONANTS_LOOKUP[r.id] = r; });
// ============================================================
// ZONES & NODE MAPS
// ============================================================
const ZONES = [
{
id: 'verdant', name: 'The Verdant Canopy', lvlRange: [1, 5],
desc: 'Towering bioluminescent trees shrouded in mist. Flora and Zephyr resonants thrive here.',
bgGradient: ['#1a3a1a', '#0a1a0a'],
elements: [['flora'], ['flora', 'zephyr'], ['zephyr']],
nodes: [
{ type: 'heal', label: 'Healing Spring', icon: '💧' },
{ type: 'wild', label: 'Tall Grass', icon: '🌿', levels: [1,3], elements: ['flora'] },
{ type: 'wild', label: 'Mossy Hollow', icon: '🍄', levels: [2,4], elements: ['flora'] },
{ type: 'cache', label: 'Mystery Cache', icon: '📦', reward: 'prisms' },
{ type: 'trainer', label: 'Sage Willow', icon: '🧙', levels: [3,5], elements: ['flora'], dialogue: '"The canopy remembers every footfall. Are you worthy of its knowledge?"' },
{ type: 'wild', label: 'Vine Bridge', icon: '🌿', levels: [3,5], elements: ['flora', 'zephyr'] },
{ type: 'trainer', label: 'Forest Watcher', icon: '🪓', levels: [4,6], elements: ['flora'], dialogue: '"The trees whisper of your approach, stranger. Let us see if you are more than noise."' },
{ type: 'heal', label: 'Healing Spring', icon: '💷' },
{ type: 'boss', label: 'Gatekeeper Verdan', icon: '🌳', level: 6, element: 'flora', dialogue: '"I am Verdan, Rootheart of the Canopy. You seek passage to the deeps? Then face the wrath of ten thousand seasons!"', rewardShards: 100, bossName: 'Verdan, Rootheart' },
]
},
{
id: 'sunken', name: 'The Sunken Grotto', lvlRange: [5, 10],
desc: 'Vast underwater caverns lit by crystal formations. Hydro and Earth resonants dwell here.',
bgGradient: ['#0a2a4a', '#050f1a'],
elements: [['hydro'], ['hydro', 'earth'], ['earth']],
nodes: [
{ type: 'heal', label: 'Healing Spring', icon: '💧' },
{ type: 'wild', label: 'Stalactite Cave', icon: '💎', levels: [5,7], elements: ['earth'] },
{ type: 'wild', label: 'River Bend', icon: '🌊', levels: [6,8], elements: ['hydro'] },
{ type: 'cache', label: 'Sunken Chest', icon: '📦', reward: 'hyper' },
{ type: 'trainer', label: 'Diver Mechis', icon: '🤿', levels: [7,9], elements: ['hydro'], dialogue: '"The grotto doesn\'t forgive hesitation. Neither do I!"' },
{ type: 'wild', label: 'Crystal Pool', icon: '🔮', levels: [7,10], elements: ['hydro', 'earth'] },
{ type: 'trainer', label: 'Cavern Sentinel', icon: '🛡️', levels: [8,10], elements: ['earth'], dialogue: '"My resonants have been forged in the crushing deep. Will yours endure?"' },
{ type: 'heal', label: 'Healing Spring', icon: '💧' },
{ type: 'boss', label: 'Gatekeeper Aquanis', icon: '🐉', level: 11, element: 'hydro', dialogue: '"I am Aquanis, Tide Empress of the Grotto. Your journey ends in drowning waters!"', rewardShards: 200, bossName: 'Aquanis, Tide Empress' },
]
},
{
id: 'obsidian', name: 'The Obsidian Crags', lvlRange: [10, 15],
desc: 'Searing volcanic ridges of black glass. Pyro and Volt resonants dominate the landscape.',
bgGradient: ['#3a0a0a', '#1a0505'],
elements: [['pyro'], ['pyro','volt'], ['volt']],
nodes: [
{ type: 'heal', label: 'Healing Spring', icon: '💧' },
{ type: 'wild', label: 'Lava Flow', icon: '🌋', levels: [10,12], elements: ['pyro'] },
{ type: 'wild', label: 'Storm Perch', icon: '⛈️', levels: [11,13], elements: ['volt'] },
{ type: 'cache', label: 'Molten Cache', icon: '🔥', reward: 'elixir' },
{ type: 'trainer', label: 'Pyromancer Izu', icon: '🔥', levels: [12,14], elements: ['pyro'], dialogue: '"The Crags will test your soul. Only the burning survive!"' },
{ type: 'wild', label: 'Thunder Ridge', icon: '⚡', levels: [12,14], elements: ['volt', 'pyro'] },
{ type: 'trainer', name: 'Voltmaster Kyra', icon: '⚡', levels: [13,15], elements: ['volt'], dialogue: '"Feel that charge in the air? It is my resonants gathering. Prepare for the storm!"' },
{ type: 'heal', label: 'Healing Spring', icon: '💧' },
{ type: 'boss', label: 'Gatekeeper Pyrax', icon: '🐲', level: 16, element: 'pyro', dialogue: '"I am Pyrax, Magma Sovereign. The volcano is my throne and your pyre!"', rewardShards: 400, bossName: 'Pyrax, Magma Sovereign' },
]
},
{
id: 'apex', name: 'The Apex Pillar', lvlRange: [15, 25],
desc: 'A colossal spire piercing the heavens. The ultimate test for any Conductor.',
bgGradient: ['#2a0a4a', '#0a0520'],
elements: [['cosmos'], ['pyro','hydro','flora'], ['volt','earth','zephyr']],
nodes: [
{ type: 'heal', label: 'Healing Spring', icon: '💧' },
{ type: 'wild', label: 'Astral Spire', icon: '✨', levels: [15,18], elements: ['cosmos'] },
{ type: 'wild', label: 'Prism Sanctum', icon: '💎', levels: [16,19], elements: ['cosmos'] },
{ type: 'cache', label: 'Celestial Cache', icon: '🌟', reward: 'max' },
{ type: 'trainer', label: 'Elder Stellan', icon: '🔭', levels: [17,20], elements: ['cosmos'], dialogue: '"You come seeking the peak. Few return unchanged."' },
{ type: 'wild', label: 'Storm Apex', icon: '🌪️', levels: [18,22], elements: ['volt'] },
{ type: 'trainer', label: 'Voidwalker Ere', icon: '🌑', levels: [18,24], elements: ['cosmos'], dialogue: '"The void hears me. Do you wish to be heard... or silenced?"' },
{ type: 'heal', label: 'Healing Spring', icon: '💧' },
{ type: 'boss', label: 'Primordial Nexus', icon: '👑', level: 25, element: 'cosmos', dialogue: '"I am the Nexus, the Core Resonance. All elements obey me. You, Conductor, will submit!"', rewardShards: 1000, bossName: 'The Primordial Nexus', isFinal: true },
]
}
];
// ============================================================
// SVG SPRITE RENDERER (premium vector-style)
// ============================================================
function renderResonantSVG(mon, size = 140, flip = false) {
const idb = RESONANTS_LOOKUP[mon.baseId] || mon;
const el = idb.element;
const tier = idb.tier;
const colors = EL_COLORS[el];
const s = size;
const cx = s / 2, cy = s / 2;
let svg = `<svg width="${s}" height="${s}" viewBox="0 0 ${s} ${s}" xmlns="http://www.w3.org/2000/svg">`;
svg += `<defs>
<radialGradient id="g${mon.uid}" cx="45%" cy="40%" r="55%">
<stop offset="0%" stop-color="${colors.primary}" stop-opacity="0.9"/>
<stop offset="100%" stop-color="${colors.secondary}" stop-opacity="0.7"/>
</radialGradient>
<filter id="f${mon.uid}"><feGaussianBlur stdDeviation="1.5"/></filter>
<radialGradient id="g2${mon.uid}" cx="50%" cy="35%" r="50%">
<stop offset="0%" stop-color="${colors.accent}" stop-opacity="0.3"/>
<stop offset="100%" stop-color="${colors.glow}" stop-opacity="0"/>
</radialGradient>
</defs>`;
// Element aura
const auraR = tier === 3 ? 54 : tier === 2 ? 48 : 44;
svg += `<circle cx="${cx}" cy="${cy}" r="${auraR}" fill="url(#g2${mon.uid})" filter="url(#f${mon.uid})"/>`;
// Direction indicator
const dx = flip ? -1 : 1;
// Body shape - varies by element tier and type
const bodyW = tier === 3 ? 36 : tier === 2 ? 30 : 26;
const bodyH = tier === 3 ? 40 : tier === 2 ? 34 : 28;
if (el === 'pyro') {
// Flame-like angular body
svg += `<path d="M${cx},${cy - bodyH} ${cx + bodyW},${cy - bodyH*0.3} ${cx + bodyW*0.7},${cy} ${cx + bodyW},${cy + bodyH*0.7} ${cx},${cy + bodyH} ${cx - bodyW},${cy + bodyH*0.7} ${cx - bodyW*0.7},${cy} ${cx - bodyW},${cy - bodyH*0.3} Z" fill="url(#g${mon.uid})" stroke="${colors.accent}" stroke-width="1"/>`;
// Flame top spikes
svg += `<polygon points="${cx - 8},${cy - bodyH + 5} ${cx},${cy - bodyH - 12} ${cx + 8},${cy - bodyH + 5}" fill="${colors.accent}" opacity="0.8"/>`;
if (tier >= 2) {
svg += `<polygon points="${cx - 16},${cy - bodyH*0.6} ${cx - 6},${cy - bodyH*0.9 - 5} ${cx - 2},${cy - bodyH*0.6}" fill="${colors.secondary}" opacity="0.6"/>`;
svg += `<polygon points="${cx + 16},${cy - bodyH*0.6} ${cx + 6},${cy - bodyH*0.9 - 5} ${cx + 2},${cy - bodyH*0.6}" fill="${colors.secondary}" opacity="0.6"/>`;
}
if (tier === 3) {
svg += `<ellipse cx="${cx}" cy="${cy + bodyH + 3}" rx="18" ry="6" fill="${colors.primary}" opacity="0.4"/>`;
}
} else if (el === 'hydro') {
// Droplet/rounded body
svg += `<ellipse cx="${cx}" cy="${cy + 4}" rx="${bodyW}" ry="${bodyH}" fill="url(#g${mon.uid})" stroke="${colors.accent}" stroke-width="1"/>`;
svg += `<path d="M${cx},${cy - bodyH - 5} ${cx + 12},${cy - bodyH + 8} ${cx - 12},${cy - bodyH + 8} Z" fill="${colors.accent}" opacity="0.7"/>`;
// Wave patterns
svg += `<path d="M${cx - 18},${cy - 4} Q${cx - 10},${cy - 10} ${cx},${cy - 4} Q${cx + 10},${cy + 2} ${cx + 18},${cy - 4}" fill="none" stroke="${colors.accent}" stroke-width="1.5" opacity="0.5"/>`;
if (tier >= 2) {
svg += `<path d="M${cx - 14},${cy + 8} Q${cx - 7},${cy + 3} ${cx},${cy + 8} Q${cx + 7},${cy + 13} ${cx + 14},${cy + 8}" fill="none" stroke="${colors.accent}" stroke-width="1" opacity="0.4"/>`;
}
// Shell/back features
if (tier >= 2) {
svg += `<ellipse cx="${cx}" cy="${cy - 8}" rx="${bodyW*0.5}" ry="${bodyH*0.35}" fill="${colors.secondary}" opacity="0.3"/>`;
}
} else if (el === 'flora') {
// Leaf/plant body
svg += `<ellipse cx="${cx}" cy="${cy + 2}" rx="${bodyW}" ry="${bodyH*0.9}" fill="url(#g${mon.uid})" stroke="${colors.accent}" stroke-width="1"/>`;
// Leaves on top
svg += `<ellipse cx="${cx}" cy="${cy - bodyH}" rx="${bodyW*0.4}" ry="12" fill="${colors.primary}" opacity="0.9" transform="rotate(-10, ${cx}, ${cy - bodyH})"/>`;
if (tier >= 2) {
svg += `<ellipse cx="${cx - 14}" cy="${cy - bodyH + 8}" rx="10" ry="6" fill="${colors.secondary}" opacity="0.7" transform="rotate(-30, ${cx - 14}, ${cy - bodyH + 8})"/>`;
svg += `<ellipse cx="${cx + 14}" cy="${cy - bodyH + 8}" rx="10" ry="6" fill="${colors.secondary}" opacity="0.7" transform="rotate(30, ${cx + 14}, ${cy - bodyH + 8})"/>`;
}
if (tier === 3) {
svg += `<path d="M${cx - 8},${cy + bodyH*0.5} L${cx - 14},${cy + bodyH} L${cx - 4},${cy + bodyH} Z" fill="${colors.accent}" opacity="0.5"/>`;
svg += `<path d="M${cx + 8},${cy + bodyH*0.5} L${cx + 14},${cy + bodyH} L${cx + 4},${cy + bodyH} Z" fill="${colors.accent}" opacity="0.5"/>`;
}
// Vine accents
svg += `<path d="M${cx - bodyW},${cy} Q${cx - bodyW - 6},${cy - 10} ${cx - bodyW + 4},${cy - 16}" fill="none" stroke="${colors.accent}" stroke-width="1.5" opacity="0.6"/>`;
} else if (el === 'volt') {
// Angular/jagged body
svg += `<polygon points="${cx},${cy - bodyH} ${cx + bodyW},${cy - bodyH*0.4} ${cx + bodyW*1.1},${cy + 4} ${cx + bodyW*0.6},${cy + bodyH} ${cx - bodyW*0.6},${cy + bodyH} ${cx - bodyW*1.1},${cy + 4} ${cx - bodyW},${cy - bodyH*0.4}" fill="url(#g${mon.uid})" stroke="${colors.accent}" stroke-width="1"/>`;
// Lightning bolt accents
svg += `<polyline points="${cx},${cy - bodyH - 4} ${cx - 6},${cy - bodyH - 12} ${cx - 1},${cy - bodyH - 12} ${cx - 7},${cy - bodyH - 20}" fill="none" stroke="${colors.accent}" stroke-width="2" opacity="0.7"/>`;
if (tier >= 2) {
svg += `<polyline points="${cx + 10},${cy - bodyH*0.4} ${cx + 14},${cy - bodyH*0.4 - 8} ${cx + 8},${cy - bodyH*0.4 - 8} ${cx + 13},${cy - bodyH*0.4 - 16}" fill="none" stroke="${colors.accent}" stroke-width="1.5" opacity="0.5"/>`;
}
if (tier === 3) {
svg += `<circle cx="${cx}" cy="${cy + bodyH + 5}" r="8" fill="${colors.primary}" opacity="0.5"/>`;
svg += `<circle cx="${cx - 20}" cy="${cy + bodyH + 3}" r="4" fill="${colors.accent}" opacity="0.4"/>`;
svg += `<circle cx="${cx + 20}" cy="${cy + bodyH + 3}" r="4" fill="${colors.accent}" opacity="0.4"/>`;
}
} else if (el === 'zephyr') {
// Swirl/cloud body
svg += `<ellipse cx="${cx}" cy="${cy}" rx="${bodyW*1.1}" ry="${bodyH*0.85}" fill="url(#g${mon.uid})" stroke="${colors.accent}" stroke-width="1"/>`;
// Swirl lines
svg += `<path d="M${cx - 20},${cy - 4} Q${cx - 10},${cy - 18} ${cx + 4},${cy - 10} Q${cx + 16},${cy - 2} ${cx + 22},${cy - 10}" fill="none" stroke="${colors.accent}" stroke-width="1.5" opacity="0.6"/>`;
if (tier >= 2) {
svg += `<path d="M${cx - 14},${cy + 6} Q${cx - 5},${cy + 14} ${cx + 6},${cy + 8} Q${cx + 14},${cy + 4} ${cx + 20},${cy + 10}" fill="none" stroke="${colors.accent}" stroke-width="1" opacity="0.5"/>`;
}
// Wing-like accents
svg += `<path d="M${cx - bodyW},${cy - 6} Q${cx - bodyW - 14},${cy - 16} ${cx - bodyW - 8},${cy + 4}" fill="${colors.primary}" opacity="0.4"/>`;
svg += `<path d="M${cx + bodyW},${cy - 6} Q${cx + bodyW + 14},${cy - 16} ${cx + bodyW + 8},${cy + 4}" fill="${colors.primary}" opacity="0.4"/>`;
if (tier === 3) {
svg += `<ellipse cx="${cx - bodyW - 16}" cy="${cy}" rx="8" ry="12" fill="${colors.accent}" opacity="0.3"/>`;
svg += `<ellipse cx="${cx + bodyW + 16}" cy="${cy}" rx="8" ry="12" fill="${colors.accent}" opacity="0.3"/>`;
}
} else if (el === 'earth') {
// Boulder/hexagonal body
const r = bodyW;
let pts = [];
for (let i = 0; i < 6; i++) {
const angle = (i * 60 - 90) * Math.PI / 180;
pts.push([cx + r * Math.cos(angle), cy + bodyH * 0.6 * Math.sin(angle) + 4]);
}
svg += `<polygon points="${pts.map(p => p.join(',')).join(' ')}" fill="url(#g${mon.uid})" stroke="${colors.accent}" stroke-width="1.5"/>`;
// Crystal facets
svg += `<line x1="${pts[0][0]}" y1="${pts[0][1]}" x2="${cx}" y2="${cy + 4}" stroke="${colors.accent}" stroke-width="0.8" opacity="0.4"/>`;
svg += `<line x1="${pts[1][0]}" y1="${pts[1][1]}" x2="${cx}" y2="${cy + 4}" stroke="${colors.accent}" stroke-width="0.8" opacity="0.4"/>`;
svg += `<line x1="${pts[2][0]}" y1="${pts[2][1]}" x2="${cx}" y2="${cy + 4}" stroke="${colors.accent}" stroke-width="0.8" opacity="0.4"/>`;
if (tier >= 2) {
svg += `<line x1="${pts[4][0]}" y1="${pts[4][1]}" x2="${cx}" y2="${cy + 4}" stroke="${colors.accent}" stroke-width="1" opacity="0.3"/>`;
svg += `<line x1="${pts[5][0]}" y1="${pts[5][1]}" x2="${cx}" y2="${cy + 4}" stroke="${colors.accent}" stroke-width="1" opacity="0.3"/>`;
}
if (tier === 3) {
svg += `<ellipse cx="${cx}" cy="${cy + bodyH + 6}" rx="22" ry="5" fill="${colors.secondary}" opacity="0.4"/>`;
svg += `<polygon points="${cx - 4},${cy - bodyH - 5} ${cx + 2},${cy - bodyH - 14} ${cx + 8},${cy - bodyH - 5}" fill="${colors.accent}" opacity="0.6"/>`;
}
} else {
// cosmos - star/nebula
const starR = bodyW * 0.7;
let starPts = [];
for (let i = 0; i < 5; i++) {
const a = (i * 72 - 90) * Math.PI / 180;
starPts.push([cx + starR * Math.cos(a), cy + starR * Math.sin(a) + 4]);
const a2 = ((i * 72 + 36) - 90) * Math.PI / 180;
starPts.push([cx + starR * 0.45 * Math.cos(a2), cy + starR * 0.45 * Math.sin(a2) + 4]);
}
svg += `<polygon points="${starPts.map(p => p.join(',')).join(' ')}" fill="url(#g${mon.uid})" stroke="${colors.accent}" stroke-width="1"/>`;
// Orbit rings
svg += `<ellipse cx="${cx}" cy="${cy + 4}" rx="${bodyW * 1.2}" ry="${bodyH * 0.5}" fill="none" stroke="${colors.accent}" stroke-width="0.8" opacity="0.5" transform="rotate(-15, ${cx}, ${cy + 4})"/>`;
if (tier >= 2) {
svg += `<ellipse cx="${cx}" cy="${cy + 4}" rx="${bodyW * 1.4}" ry="${bodyH * 0.6}" fill="none" stroke="${colors.primary}" stroke-width="0.5" opacity="0.3" transform="rotate(20, ${cx}, ${cy + 4})"/>`;
}
}
// Shared features
const eyeY = cy + (tier === 3 ? 2 : 0);
const eyeDX = tier === 3 ? 12 : 10;
// Eyes
if (el !== 'pyro' || tier < 2) {
// Normal eyes
svg += `<circle cx="${cx - eyeDX}" cy="${eyeY}" r="4" fill="white" opacity="0.9"/>`;
svg += `<circle cx="${cx + eyeDX}" cy="${eyeY}" r="4" fill="white" opacity="0.9"/>`;
svg += `<circle cx="${cx - eyeDX}" cy="${eyeY}" r="2" fill="#1a1a2e"/>`;
svg += `<circle cx="${cx + eyeDX}" cy="${eyeY}" r="2" fill="#1a1a2e"/>`;
} else {
// Pyro tier 2+ - cat eyes
svg += `<path d="M${cx - eyeDX - 4},${eyeY - 1} L${cx - eyeDX},${eyeY - 5} L${cx - eyeDX + 4},${eyeY - 1} Z" fill="white" opacity="0.9"/>`;
svg += `<path d="M${cx + eyeDX - 4},${eyeY - 1} L${cx + eyeDX},${eyeY - 5} L${cx + eyeDX + 4},${eyeY - 1} Z" fill="white" opacity="0.9"/>`;
svg += `<circle cx="${cx - eyeDX}" cy="${eyeY + 1}" r="2" fill="#FF6B35"/>`;
svg += `<circle cx="${cx + eyeDX}" cy="${eyeY + 1}" r="2" fill="#FF6B35"/>`;
}
// Mouth
if (el === 'flora') {
svg += `<path d="M${cx - 5},${cy + bodyH*0.5} Q${cx},${cy + bodyH*0.5 + 5} ${cx + 5},${cy + bodyH*0.5}" fill="none" stroke="${colors.accent}" stroke-width="1.5"/>`;
} else if (el === 'volt' || el === 'pyro') {
svg += `<path d="M${cx - 6},${cy + bodyH*0.55} L${cx},${cy + bodyH*0.55 + 4} L${cx + 6},${cy + bodyH*0.55}" fill="none" stroke="${colors.accent}" stroke-width="1.5"/>`;
} else if (el === 'earth') {
svg += `<line x1="${cx - 5}" y1="${cy + bodyH*0.5}" x2="${cx + 5}" y2="${cy + bodyH*0.5}" stroke="${colors.accent}" stroke-width="1.5"/>`;
} else if (el === 'zephyr') {
svg += `<ellipse cx="${cx}" cy="${cy + bodyH*0.5 + 2}" rx="4" ry="2" fill="${colors.accent}" opacity="0.6"/>`;
} else {
svg += `<path d="M${cx - 4},${cy + bodyH*0.5} Q${cx},${cy + bodyH*0.5 + 8} ${cx + 4},${cy + bodyH*0.5}" fill="none" stroke="${colors.accent}" stroke-width="1.2"/>`;
}
// Unique extra feature per element for identification
if (el === 'pyro' && tier >= 2) {
// Smoke from head
svg += `<circle cx="${cx - 6}" cy="${cy - bodyH - 6}" r="3" fill="${colors.accent}" opacity="0.3"/>`;
svg += `<circle cx="${cx + 8}" cy="${cy - bodyH - 9}" r="2" fill="${colors.accent}" opacity="0.2"/>`;
}
if (el === 'hydro') {
// Water drop
svg += `<path d="M${cx + 16},${cy - bodyH*0.3} L${cx + 20},${cy - bodyH*0.3 + 6} L${cx + 12},${cy - bodyH*0.3 + 6} Z" fill="${colors.accent}" opacity="0.5"/>`;
}
if (el === 'earth') {
// Embedded gem
svg += `<polygon points="${cx - 4},${cy - 4} ${cx},${cy - 8} ${cx + 4},${cy - 4} ${cx},${cy}" fill="${colors.accent}" opacity="0.7"/>`;
}
svg += `</svg>`;
return svg;
}
// Simplified node icons
function getNodeIcon(node) {
if (node.icon && !node.icon.includes(' ')) {
// Emoji icons for nodes
return node.icon;
}
// Fallback SVG
return `<svg width="24" height="24" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8" fill="rgba(255,255,255,0.3)"/></svg>`;
}
// ============================================================
// GAME STATE & SAVE SYSTEM
// ============================================================
const SAVE_KEY = 'aethelgard_save_v1';
let gameState = {
playerName: 'Conductor',
activeParty: [], // array of monster instances (max 3)
storageBox: [], // additional monsters
inventory: {
prism: 5,
hyperPrism: 1,
restorer: 2,
elixir: 1
},
shards: 50,
unlockedZones: ['verdant'],
currentZone: null,
currentNodeIndex: 0,
completedNodes: {}, // { zoneId: [nodeIndices] }
defeatedBosses: [], // [zoneId]
dexDiscovered: [], // [resonantId]
starterChosen: false,
settings: { sfx: true, music: true }
};
let uidCounter = 1;
function nextUid() { return 'm' + (uidCounter++); }
// Create a monster instance from base data
function createMonster(baseId, level) {
const base = RESONANTS_LOOKUP[baseId];
if (!base) return null;
const lvl = level || base.level;
const statMult = 1 + (lvl - 1) * 0.08;
return {
uid: nextUid(),
baseId: baseId,
name: base.name,
element: base.element,
tier: base.tier,
level: lvl,
xp: 0,
xpToNext: xpForLevel(lvl + 1),
currentHP: Math.floor(base.baseStats.hp * statMult),
maxHP: Math.floor(base.baseStats.hp * statMult),
attack: Math.floor(base.baseStats.attack * statMult),
defense: Math.floor(base.baseStats.defense * statMult),
speed: Math.floor(base.baseStats.speed * statMult),
status: null, // { type: 'burn'|'wet'|'spore'|'stun', turns: n }
moves: base.moves,
statBoosts: { attack: 0, defense: 0, speed: 0 }
};
}
function xpForLevel(level) {
return Math.floor(20 * Math.pow(level, 1.4));
}
function calcMaxHP(mon) {
const base = RESONANTS_LOOKUP[mon.baseId];
const statMult = 1 + (mon.level - 1) * 0.08;
return Math.floor(base.baseStats.hp * statMult);
}
function calcStat(mon, stat) {
const base = RESONANTS_LOOKUP[mon.baseId];
const statMult = 1 + (mon.level - 1) * 0.08;
return Math.floor(base.baseStats[stat] * statMult);
}
function saveGame() {
try {
const saveData = {
...gameState,
uidCounter,
savedAt: Date.now()
};
localStorage.setItem(SAVE_KEY, JSON.stringify(saveData));
} catch (e) {
console.warn('Save failed:', e);
}
}
function loadGame() {
try {
const raw = localStorage.getItem(SAVE_KEY);
if (!raw) return false;
const data = JSON.parse(raw);
gameState = { ...gameState, ...data };
uidCounter = data.uidCounter || 1;
return true;
} catch (e) {
console.warn('Load failed:', e);
return false;
}
}
function hasSave() {
return !!localStorage.getItem(SAVE_KEY);
}
function resetProgress() {
if (!confirm('Are you sure? This will erase ALL progress permanently.')) return;
localStorage.removeItem(SAVE_KEY);
location.reload();
}
// ============================================================
// SCREEN MANAGEMENT
// ============================================================
let currentScreen = 'screen-menu';
function showScreen(id) {
const prev = document.getElementById(currentScreen);
const next = document.getElementById(id);
if (prev && prev !== next) {
prev.classList.remove('active');
prev.style.display = 'none';
}
if (next) {
next.style.display = 'flex';
// Force reflow
void next.offsetWidth;
next.classList.add('active');
}
currentScreen = id;
// Show/hide topbar
const topbar = document.getElementById('topbar');
if (id === 'screen-menu' || id === 'screen-settings' || id === 'screen-starter') {
topbar.classList.add('hidden');
} else {
topbar.classList.remove('hidden');
}
// Screen-specific init
if (id === 'screen-zoneselect') renderZoneSelect();
if (id === 'screen-overworld') renderOverworld();
if (id === 'screen-party') renderParty();
if (id === 'screen-shop') renderShop();
if (id === 'screen-dex') renderDex();
if (id === 'screen-settings') renderSettings();
updateTopbar();
}
function goHome() {
if (gameState.currentZone) {
showScreen('screen-overworld');
} else {
showScreen('screen-zoneselect');
}
}
function updateTopbar() {
document.getElementById('shards-display').textContent = gameState.shards;
const zl = document.getElementById('zone-label');
if (gameState.currentZone) {
const zone = ZONES.find(z => z.id === gameState.currentZone);
zl.textContent = zone ? zone.name : '';
} else {
zl.textContent = '';
}
}
// ============================================================
// NOTIFICATIONS
// ============================================================
function notify(msg, duration = 2500) {
const el = document.getElementById('notification');
el.textContent = msg;
el.classList.remove('hidden');
el.classList.add('notification');
clearTimeout(el._timeout);
el._timeout = setTimeout(() => {
el.classList.add('hidden');
el.classList.remove('notification');
}, duration);
}
// ============================================================
// STARTER SELECTION
// ============================================================
function showStarterSelection() {
const starters = ['emberkit', 'droplet', 'seedling', 'sparky', 'gusty', 'pebble'];
const container = document.getElementById('screen-starter');
let html = `<h2 class="font-pixel text-lg text-purple-300 mb-2 anim-slide-up">CHOOSE YOUR FIRST RESONANT</h2>`;
html += `<p class="text-sm text-gray-400 mb-6">This companion will begin your journey.</p>`;
html += `<div class="grid grid-cols-2 md:grid-cols-3 gap-4 max-w-2xl">`;
starters.forEach(id => {
const r = RESONANTS_LOOKUP[id];
const colors = EL_COLORS[r.element];
html += `<div onclick="selectStarter('${id}')" class="glass p-4 rounded-xl cursor-pointer hover:scale-105 transition-transform ${elBorderClass(r.element)}" style="border-width:2px;">
<div class="flex justify-center mb-3">${renderResonantSVG(createMonster(id, 1), 80)}</div>
<p class="font-semibold text-sm text-center ${elTextClass(r.element)}">${r.name}</p>
<p class="text-[10px] text-gray-400 text-center mt-1">${r.element.toUpperCase()}</p>
<p class="text-[10px] text-gray-500 text-center mt-1 leading-relaxed">${r.desc}</p>
</div>`;
});
html += `</div>`;
container.innerHTML = html;
showScreen('screen-starter');
}
function selectStarter(id) {
synth.init();
synth.resume();
synth.select();
const mon = createMonster(id, 1);
gameState.activeParty = [mon];
gameState.starterChosen = true;
gameState.dexDiscovered.push(id);
saveGame();
showScreen('screen-zoneselect');
notify(`${mon.name} joins your party!`);
}
// ============================================================
// ZONE SELECT
// ============================================================
function renderZoneSelect() {
const grid = document.getElementById('zone-grid');
let html = '';
ZONES.forEach((zone, i) => {
const unlocked = gameState.unlockedZones.includes(zone.id);
const completed = gameState.defeatedBosses.includes(zone.id);
const colors = zone.elements[0][0];
html += `<div class="glass p-5 rounded-xl ${unlocked ? elBorderClass(colors) : 'opacity-40'} ${unlocked ? 'cursor-pointer hover:scale-[1.02] transition-transform' : ''}" style="border-width:2px;"
${unlocked ? `onclick="enterZone('${zone.id}')"` : ''}>
<div class="flex items-center justify-between mb-2">
<h3 class="font-pixel text-[10px] ${elTextClass(colors)}">${zone.name}</h3>
${completed ? '<span class="text-green-400 text-xs">✓</span>' : ''}
${!unlocked ? '<span class="text-gray-500 text-xs">🔒</span>' : ''}
</div>
<p class="text-xs text-gray-400 mb-2">${zone.desc}</p>
<div class="flex gap-2">
<span class="badge badge-cosmos text-[9px]">Lv. ${zone.lvlRange[0]}-${zone.lvlRange[1]}</span>
${zone.elements[0].map(e => `<span class="badge ${elBadgeClass(e)} text-[9px]">${e}</span>`).join('')}
</div>
</div>`;
});
grid.innerHTML = html;
}
function enterZone(zoneId) {
synth.select();
gameState.currentZone = zoneId;
gameState.currentNodeIndex = 0;
if (!gameState.completedNodes[zoneId]) gameState.completedNodes[zoneId] = [];
saveGame();
showScreen('screen-overworld');
}
// ============================================================
// OVERWORLD NODE MAP
// ============================================================
function renderOverworld() {
const zone = ZONES.find(z => z.id === gameState.currentZone);
if (!zone) return;
document.getElementById('map-title').textContent = zone.name;
const completed = gameState.completedNodes[zone.id] || [];
const totalNodes = zone.nodes.length;
const completedCount = completed.length;
document.getElementById('map-progress').textContent = `${completedCount}/${totalNodes}`;
const mapEl = document.getElementById('node-map');
// Set background
mapEl.style.background = `radial-gradient(ellipse at 50% 50%, ${zone.bgGradient[0]}44, ${zone.bgGradient[1]})`;
// Layout nodes in a winding path
let html = '<div class="absolute inset-0 flex items-center justify-center">';
html += '<div class="relative w-full h-full">';
const nodes = zone.nodes;
const mapW = 100; // percentage
const mapH = 100;
// Calculate positions in a snake pattern
const positions = [];
const cols = Math.min(nodes.length, 3);
const rows = Math.ceil(nodes.length / cols);
nodes.forEach((node, i) => {
const col = i % cols;
const row = Math.floor(i / cols);
const xPct = cols === 1 ? 50 : 15 + (col / (cols - 1)) * 70;
// Snake: alternate row direction
const actualCol = row % 2 === 0 ? col : (cols - 1 - col);
const xPctSnake = cols === 1 ? 50 : 15 + (actualCol / (cols - 1)) * 70;
const yPct = 12 + (row / Math.max(rows - 1, 1)) * 76;
positions.push({ x: xPctSnake, y: yPct });
});
// Draw connectors
for (let i = 0; i < positions.length - 1; i++) {
const p1 = positions[i];
const p2 = positions[i + 1];
const isActive = completed.includes(i);
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const len = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx) * 180 / Math.PI;
html += `<div class="node-connector ${isActive ? 'active' : ''}" style="left:${p1.x}%;top:${p1.y}%;width:${len}%;transform:rotate(${angle}deg);"></div>`;
}
// Draw nodes
nodes.forEach((node, i) => {
const pos = positions[i];
const isCompleted = completed.includes(i);
const isCurrent = i === gameState.currentNodeIndex;
const isLocked = i > 0 && !completed.includes(i - 1) && !isCompleted;
const isBoss = node.type === 'boss';
let nodeClass = 'node';
if (isCompleted) nodeClass += ' completed';
if (isLocked) nodeClass += ' locked';
if (isCurrent && !isCompleted) nodeClass += ' current';
if (isBoss) nodeClass += ' boss-node';
const elColor = node.element || (node.elements && node.elements[0]) || 'cosmos';
const borderClass = isBoss ? elBorderClass(elColor) : '';
html += `<div class="${nodeClass} ${borderClass}" style="left:${pos.x}%;top:${pos.y}%;transform:translate(-50%,-50%);border-width:2px;"
${!isLocked && !isCompleted ? `onclick="interactNode(${i})"` : ''}
title="${node.label}">
${getNodeIcon(node)}
</div>`;
// Label
html += `<div class="absolute text-[9px] text-gray-400 text-center pointer-events-none" style="left:${pos.x}%;top:${pos.y + 8}%;transform:translateX(-50%);white-space:nowrap;">${node.label}</div>`;
});
html += '</div></div>';
mapEl.innerHTML = html;
}
function interactNode(index) {
synth.select();
const zone = ZONES.find(z => z.id === gameState.currentZone);
const node = zone.nodes[index];
gameState.currentNodeIndex = index;
switch (node.type) {
case 'wild': startWildBattle(zone, node); break;
case 'trainer': startTrainerBattle(zone, node); break;
case 'boss': startBossBattle(zone, node); break;
case 'heal': healAtSpring(); break;
case 'cache': openCache(node); break;
}
}
function healAtSpring() {
synth.heal();
gameState.activeParty.forEach(mon => {
mon.currentHP = mon.maxHP;
mon.status = null;
mon.statBoosts = { attack: 0, defense: 0, speed: 0 };
});
completeCurrentNode();
notify('Your party has been fully restored!');
saveGame();
}
function openCache(node) {
const rewards = {
prism: { type: 'item', item: 'prism', amount: 3, msg: 'Found 3 Harmonizer Prisms!' },
hyper: { type: 'item', item: 'hyperPrism', amount: 2, msg: 'Found 2 Hyper-Conductive Prisms!' },
elixir: { type: 'item', item: 'elixir', amount: 2, msg: 'Found 2 Resonance Elixirs!' },
max: { type: 'shards', amount: 150, msg: 'Found 150 Aether Shards!' }
};
const reward = rewards[node.reward] || rewards.prism;
if (reward.type === 'item') {
gameState.inventory[reward.item] = (gameState.inventory[reward.item] || 0) + reward.amount;
} else {
gameState.shards += reward.amount;
}
synth.captureSuccess();
completeCurrentNode();
notify(reward.msg);
saveGame();
}
function completeCurrentNode() {
const zoneId = gameState.currentZone;
const idx = gameState.currentNodeIndex;
if (!gameState.completedNodes[zoneId]) gameState.completedNodes[zoneId] = [];
if (!gameState.completedNodes[zoneId].includes(idx)) {
gameState.completedNodes[zoneId].push(idx);
}
// Advance to next node
const zone = ZONES.find(z => z.id === zoneId);
if (idx + 1 < zone.nodes.length) {
gameState.currentNodeIndex = idx + 1;
}
saveGame();
renderOverworld();
}
// ============================================================
// COMBAT SYSTEM
// ============================================================
let battle = null;
function initBattle(enemyMon, context) {
battle = {
playerMon: gameState.activeParty[0],
enemyMon: enemyMon,
isWild: context.isWild,
isBoss: context.isBoss || false,
log: [],
phase: 'select', // select, anim, enemyTurn, result
context: context,
turn: 0
};
if (context.isWild) {
battle.log.push(`A wild ${enemyMon.name} emerged!`);
} else {
battle.log.push(`${context.trainerName || 'A Conductor'} challenges you!`);
if (context.dialogue) battle.log.push(context.dialogue);
battle.log.push(`${enemyMon.name} appears!`);
}
renderBattle();
}
function addLog(msg) {
if (!battle) return;
battle.log.push(msg);
if (battle.log.length > 8) battle.log.shift();
updateBattleLog();
}
function updateBattleLog() {
const el = document.getElementById('battle-log');
if (!el || !battle) return;
el.innerHTML = battle.log.map(l => `<p class="mb-1">${l}</p>`).join('');
el.scrollTop = el.scrollHeight;
}
function renderBattle() {
if (!battle) return;
const pm = battle.playerMon;
const em = battle.enemyMon;
// Player side
document.getElementById('player-sprite-wrap').innerHTML = renderResonantSVG(pm, 120, false);
document.getElementById('player-mon-name').textContent = pm.name;
document.getElementById('player-mon-name').className = `font-semibold text-sm ${elTextClass(pm.element)}`;
document.getElementById('player-mon-level').textContent = `Lv.${pm.level}`;
updateHPBars();
// Status
const pStatus = document.getElementById('player-mon-status');
pStatus.innerHTML = pm.status ? `<span class="status-icon status-${pm.status.type}">${pm.status.type.toUpperCase()}</span>` : '';
// Enemy side
document.getElementById('enemy-sprite-wrap').innerHTML = renderResonantSVG(em, 120, true);
document.getElementById('enemy-mon-name').textContent = em.name;
document.getElementById('enemy-mon-name').className = `font-semibold text-sm ${elTextClass(em.element)}`;
document.getElementById('enemy-mon-level').textContent = `Lv.${em.level}`;
const eStatus = document.getElementById('enemy-mon-status');
eStatus.innerHTML = em.status ? `<span class="status-icon status-${em.status.type}">${em.status.type.toUpperCase()}</span>` : '';
updateBattleLog();
renderBattleActions();
}
function updateHPBars() {
if (!battle) return;
const pm = battle.playerMon;
const em = battle.enemyMon;
const pHpPct = Math.max(0, pm.currentHP / pm.maxHP * 100);
const pBar = document.getElementById('player-mon-hp-bar');
pBar.style.width = pHpPct + '%';
pBar.className = 'hp-bar-fill ' + (pHpPct < 25 ? 'danger' : pHpPct < 50 ? 'warn' : '');
document.getElementById('player-mon-hp-text').textContent = `HP: ${Math.max(0,pm.currentHP)}/${pm.maxHP}`;
const pXpPct = pm.xp / pm.xpToNext * 100;
document.getElementById('player-mon-xp-bar').style.width = Math.min(100, pXpPct) + '%';
const eHpPct = Math.max(0, em.currentHP / em.maxHP * 100);
const eBar = document.getElementById('enemy-mon-hp-bar');
eBar.style.width = eHpPct + '%';
eBar.className = 'hp-bar-fill ' + (eHpPct < 25 ? 'danger' : eHpPct < 50 ? 'warn' : '');
document.getElementById('enemy-mon-hp-text').textContent = `HP: ${Math.max(0,em.currentHP)}/${em.maxHP}`;
}
function renderBattleActions() {
const container = document.getElementById('battle-actions');
if (!battle || battle.phase !== 'select') return;
const pm = battle.playerMon;
const moves = pm.moves.map(mid => MOVE_LOOKUP[mid]).filter(Boolean);
// Main actions
let html = `
<button class="btn-game btn-primary col-span-2 md:col-span-1" onclick="showMoveSelect()">⚔️ Attack</button>
<button class="btn-game btn-success" onclick="showPrisms()" ${battle.isWild ? '' : 'disabled'}>🔮 Prisms</button>
<button class="btn-game" onclick="showExchange()" ${gameState.activeParty.length < 2 ? 'disabled' : ''}>🔄 Switch</button>
<button class="btn-game" onclick="showInventory()">🎒 Items</button>
<button class="btn-game" onclick="tryRunAway()" ${battle.isBoss ? 'disabled' : ''}>🏃 Flee</button>
<button class="btn-game col-span-2 md:col-span-1" onclick="battleResult('forfeit')">⏭️ Forfeit</button>
`;
container.innerHTML = html;
}
function showMoveSelect() {
synth.select();
const container = document.getElementById('battle-actions');
const pm = battle.playerMon;
const moves = pm.moves.map(mid => MOVE_LOOKUP[mid]).filter(Boolean);
let html = '';
moves.forEach((move, i) => {
const elClass = elTextClass(move.type);
html += `<button class="btn-game flex-col !items-start !p-3" onclick="executeMove('${move.id}')">
<span class="font-semibold text-xs ${elClass}">${move.name}</span>
<span class="text-[10px] text-gray-400 mt-0.5">${move.type.toUpperCase()} ${move.power > 0 ? '• Pwr ' + move.power : ''}${move.accuracy < 100 ? ' • Acc ' + move.accuracy : ''}</span>
</button>`;
});
html += `<button class="btn-game !col-span-full !md:col-span-4" onclick="renderBattleActions()">← Back</button>`;
container.innerHTML = html;
}
function showPrisms() {
synth.select();
const container = document.getElementById('battle-actions');
const prismCount = gameState.inventory.prism || 0;
const hyperCount = gameState.inventory.hyperPrism || 0;
let html = `
<button class="btn-game flex-col !items-start !p-3" onclick="usePrism('prism')" ${prismCount < 1 ? 'disabled' : ''}>
<span class="font-semibold text-xs">🔮 Harmonizer Prism</span>
<span class="text-[10px] text-gray-400 mt-0.5">Basic catch (x${prismCount})</span>
</button>
<button class="btn-game flex-col !items-start !p-3" onclick="usePrism('hyperPrism')" ${hyperCount < 1 ? 'disabled' : ''}>
<span class="font-semibold text-xs">💎 Hyper-Conductive Prism</span>
<span class="text-[10px] text-gray-400 mt-0.5">High catch rate (x${hyperCount})</span>
</button>
<button class="btn-game !col-span-2 md:col-span-2 mt-2" onclick="renderBattleActions()">← Back</button>
`;
container.innerHTML = html;
}
function showExchange() {
synth.select();
const container = document.getElementById('battle-actions');
let html = '';
gameState.activeParty.forEach((mon, i) => {
const isCurrent = i === 0;
html += `<button class="btn-game flex-col !items-start !p-3" ${isCurrent ? 'disabled' : `onclick="swapMonster(${i})"`}>
<span class="font-semibold text-xs ${elTextClass(mon.element)}">${mon.name}</span>
<span class="text-[10px] text-gray-400 mt-0.5">Lv.${mon.level} • HP ${mon.currentHP}/${mon.maxHP}${isCurrent ? ' (active)' : ''}</span>
</button>`;
});
html += `<button class="btn-game !col-span-2 md:col-span-2 mt-2" onclick="renderBattleActions()">← Back</button>`;
container.innerHTML = html;
}
function showInventory() {
synth.select();
const container = document.getElementById('battle-actions');
const items = [
{ key: 'restorer', name: 'Core Restorer', desc: 'Restore 50% HP', icon: '💚' },
{ key: 'elixir', name: 'Resonance Elixir', desc: 'Cure status', icon: '✨' }
];
let html = '';
items.forEach(item => {
const count = gameState.inventory[item.key] || 0;
html += `<button class="btn-game flex-col !items-start !p-3" onclick="useItem('${item.key}')" ${count < 1 ? 'disabled' : ''}>
<span class="font-semibold text-xs">${item.icon} ${item.name}</span>
<span class="text-[10px] text-gray-400 mt-0.5">${item.desc} (x${count})</span>
</button>`;
});
html += `<button class="btn-game !col-span-2 md:col-span-2 mt-2" onclick="renderBattleActions()">← Back</button>`;
container.innerHTML = html;
}
// ============================================================
// COMBAT EXECUTION
// ============================================================
function executeMove(moveId) {
if (!battle || battle.phase !== 'select') return;
synth.select();
const move = MOVE_LOOKUP[moveId];
const playerMon = battle.playerMon;
const enemyMon = battle.enemyMon;
// Check accuracy
const accRoll = Math.random() * 100;
if (accRoll > move.accuracy) {
synth.miss();
addLog(`${playerMon.name} used ${move.name}... but it missed!`);
battle.phase = 'anim';
renderBattleUIAfterAction();
setTimeout(() => enemyTurn(), 1200);
return;
}
if (move.power === 0) {
// Support move
executeSupportMove(playerMon, move);
battle.phase = 'anim';
renderBattleUIAfterAction();
setTimeout(() => enemyTurn(), 1200);
return;
}
// Calculate damage
const isCrit = Math.random() < 0.10;
const atkStat = playerMon.attack * (1 + playerMon.statBoosts.attack * 0.5);
const defStat = enemyMon.defense * (1 + enemyMon.statBoosts.defense * 0.5);
const typeMod = getTypeMultiplier(move.type, [enemyMon.element]);
const randFactor = 0.85 + Math.random() * 0.15;
let damage = Math.floor((((2 * playerMon.level / 5 + 2) * move.power * (atkStat / defStat)) / 50 + 2) * typeMod * randFactor);
if (isCrit) damage = Math.floor(damage * 1.5);
damage = Math.max(1, damage);
// STUN check pre-attack
if (playerMon.status && playerMon.status.type === 'stun' && Math.random() < 0.3) {
addLog(`${playerMon.name} is stunned and can't move!`);
playerMon.status.turns--;
if (playerMon.status.turns <= 0) playerMon.status = null;
battle.phase = 'anim';
renderBattleUIAfterAction();
setTimeout(() => enemyTurn(), 1200);
return;
}
// Apply damage
enemyMon.currentHP = Math.max(0, enemyMon.currentHP - damage);
// Sound & animation
if (typeMod > 1) {
synth.hitSuper();
addLog(`${playerMon.name} used ${move.name}! Super effective! ${damage} damage!`);
spawnParticles('enemy', 'super');
shakeScreen();
} else if (typeMod < 1) {
synth.hitPhysical();
addLog(`${playerMon.name} used ${move.name}. Not very effective... ${damage} damage.`);
spawnParticles('enemy', 'weak');
} else {
synth.hitElemental();
addLog(`${playerMon.name} used ${move.name}! ${damage} damage!`);
spawnParticles('enemy', 'hit');
}
if (isCrit) addLog('Critical hit!');
// Status effect
if (move.effect && move.effect.status && Math.random() < move.effect.chance) {
if (!enemyMon.status) {
enemyMon.status = { type: move.effect.status, turns: 3 };
addLog(`${enemyMon.name} is afflicted with ${move.effect.status}!`);
}
}
// Heal from drain
if (move.effect && move.effect.heal) {
const healAmt = Math.floor(damage * move.effect.heal);
playerMon.currentHP = Math.min(playerMon.maxHP, playerMon.currentHP + healAmt);
addLog(`${playerMon.name} recovered ${healAmt} HP!`);
}
battle.phase = 'anim';
renderBattleUIAfterAction();
// Check if enemy fainted
if (enemyMon.currentHp <= 0 || enemyMon.currentHP <= 0) {
setTimeout(() => handleEnemyFainted(), 1000);
return;
}
setTimeout(() => enemyTurn(), 1200);
}
function executeSupportMove(mon, move) {
if (move.effect && move.effect.stat) {
const boost = move.effect.stages || 1;
mon.statBoosts[move.effect.stat] = Math.min(6, mon.statBoosts[move.effect.stat] + boost);
addLog(`${mon.name}'s ${move.effect.stat} rose sharply!`);
synth.heal();
}
}
function enemyTurn() {
if (!battle) return;
const enemyMon = battle.enemyMon;
const playerMon = battle.playerMon;
// Check stun
if (enemyMon.status && enemyMon.status.type === 'stun' && Math.random() < 0.3) {
addLog(`${enemyMon.name} is stunned!`);
enemyMon.status.turns--;
if (enemyMon.status.turns <= 0) enemyMon.status = null;
endEnemyTurn();
return;
}
// Status damage
if (enemyMon.status && enemyMon.status.type === 'burn') {
const burnDmg = Math.floor(enemyMon.maxHP * 0.06);
enemyMon.currentHP = Math.max(0, enemyMon.currentHP - burnDmg);
addLog(`${enemyMon.name} takes ${burnDmg} burn damage!`);
if (enemyMon.currentHP <= 0) {
setTimeout(() => handleEnemyFainted(), 800);
return;
}
}
if (enemyMon.status && enemyMon.status.type === 'spore') {
const sporeDmg = Math.floor(enemyMon.maxHP * 0.05);
enemyMon.currentHP = Math.max(0, enemyMon.currentHP - sporeDmg);
addLog(`${enemyMon.name} takes ${sporeDmg} spore damage!`);
if (enemyMon.currentHP <= 0) {
setTimeout(() => handleEnemyFainted(), 800);
return;
}
}
// Pick enemy move (random from available)
const availableMoves = enemyMon.moves.map(mid => MOVE_LOOKUP[mid]).filter(m => m && m.power > 0);
if (availableMoves.length === 0) {
addLog(`${enemyMon.name} has no moves!`);
endEnemyTurn();
return;
}
const move = availableMoves[Math.floor(Math.random() * availableMoves.length)];
// Accuracy check
const accRoll = Math.random() * 100;
if (accRoll > move.accuracy) {
synth.miss();
addLog(`${enemyMon.name} used ${move.name}... but it missed!`);
endEnemyTurn();
return;
}
const isCrit = Math.random() < 0.10;
const atkStat = enemyMon.attack * (1 + enemyMon.statBoosts.attack * 0.5);
const defStat = playerMon.defense * (1 + playerMon.statBoosts.defense * 0.5);
const typeMod = getTypeMultiplier(move.type, [playerMon.element]);
const randFactor = 0.85 + Math.random() * 0.15;
let damage = Math.floor((((2 * enemyMon.level / 5 + 2) * move.power * (atkStat / defStat)) / 50 + 2) * typeMod * randFactor);
if (isCrit) damage = Math.floor(damage * 1.5);
damage = Math.max(1, damage);
playerMon.currentHP = Math.max(0, playerMon.currentHP - damage);
if (typeMod > 1) {
synth.hitSuper();
addLog(`${enemyMon.name} used ${move.name}! Super effective! ${damage} damage!`);
spawnParticles('player', 'super');
shakeScreen();
} else if (typeMod < 1) {
synth.hitPhysical();
addLog(`${enemyMon.name} used ${move.name}. Not very effective... ${damage} damage.`);
spawnParticles('player', 'weak');
} else {
synth.hitElemental();
addLog(`${enemyMon.name} used ${move.name}! ${damage} damage!`);
spawnParticles('player', 'hit');
}
if (isCrit) addLog('Critical hit!');
// Status from enemy move
if (move.effect && move.effect.status && Math.random() < move.effect.chance) {
if (!playerMon.status) {
playerMon.status = { type: move.effect.status, turns: 3 };
addLog(`${playerMon.name} is afflicted with ${move.effect.status}!`);
}
}
renderBattle();
if (playerMon.currentHP <= 0) {
setTimeout(() => handlePlayerFainted(), 1000);
return;
}
endEnemyTurn();
}
function endEnemyTurn() {
// Decrement status turns
const em = battle.enemyMon;
if (em.status) {
em.status.turns--;
if (em.status.turns <= 0) {
addLog(`${em.name} recovered from ${em.status.type}!`);
em.status = null;
}
}
battle.phase = 'select';
battle.turn++;
renderBattle();
}
function renderBattleUIAfterAction() {
updateHPBars();
updateBattleLog();
// Re-render status
const pm = battle.playerMon;
const em = battle.enemyMon;
document.getElementById('player-mon-status').innerHTML = pm.status ? `<span class="status-icon status-${pm.status.type}">${pm.status.type.toUpperCase()}</span>` : '';
document.getElementById('enemy-mon-status').innerHTML = em.status ? `<span class="status-icon status-${em.status.type}">${em.status.type.toUpperCase()}</span>` : '';
}
function swapMonster(index) {
if (!battle || battle.phase !== 'select') return;
const mon = gameState.activeParty[index];
if (!mon || mon.currentHP <= 0) {
synth.error();
notify('That Resonant is fainted!');
return;
}
synth.select();
addLog(`Go, ${mon.name}!`);
gameState.activeParty[0] = gameState.activeParty.splice(index, 1, gameState.activeParty[0])[0];
battle.playerMon = gameState.activeParty[0];
renderBattle();
setTimeout(() => enemyTurn(), 800);
}
function tryRunAway() {
if (!battle || !battle.isWild) return;
synth.runAway();
addLog('Got away safely!');
battle.phase = 'anim';
setTimeout(() => endBattle('flee'), 1000);
}
// ============================================================
// CAPTURE SYSTEM
// ============================================================
function usePrism(prismType) {
if (!battle || !battle.isWild) return;
const prismCount = gameState.inventory[prismType] || 0;
if (prismCount < 1) {
synth.error();
notify('No prisms left!');
return;
}
gameState.inventory[prismType]--;
synth.captureStart();
const enemy = battle.enemyMon;
const hpFactor = 1 - (enemy.currentHP / enemy.maxHP);
const statusBonus = enemy.status ? 0.2 : 0;
const prismBonus = prismType === 'hyperPrism' ? 0.3 : 0;
let catchRate = 0.15 + hpFactor * 0.5 + statusBonus + prismBonus;
catchRate = Math.min(0.95, catchRate);
battle.phase = 'anim';
addLog(`Thrown a ${prismType === 'hyperPrism' ? 'Hyper-Conductive' : 'Harmonizer'} Prism...`);
// Shake animation
const enemyWrap = document.getElementById('enemy-sprite-wrap');
enemyWrap.classList.add('anim-capture');
setTimeout(() => {
enemyWrap.classList.remove('anim-capture');
const roll = Math.random();
if (roll < catchRate) {
synth.captureSuccess();
addLog(`✨ ${enemy.name} was caught!`);
// Discover in dex
if (!gameState.dexDiscovered.includes(enemy.baseId)) {
gameState.dexDiscovered.push(enemy.baseId);
}
// Add to party or storage
const newMon = createMonster(enemy.baseId, enemy.level);
if (gameState.activeParty.length < 3) {
gameState.activeParty.push(newMon);
} else {
gameState.storageBox.push(newMon);
}
saveGame();
setTimeout(() => endBattle('capture'), 1500);
} else {
synth.captureFail();
addLog(`${enemy.name} broke free!`);
renderBattle();
setTimeout(() => enemyTurn(), 800);
}
}, 2000);
renderBattle();
}
// ============================================================
// ITEMS IN BATTLE
// ============================================================
function useItem(itemKey) {
if (!battle || battle.phase !== 'select') return;
const count = gameState.inventory[itemKey] || 0;
if (count < 1) { synth.error(); return; }
gameState.inventory[itemKey]--;
const mon = battle.playerMon;
if (itemKey === 'restorer') {
const healAmt = Math.floor(mon.maxHP * 0.5);
mon.currentHP = Math.min(mon.maxHP, mon.currentHP + healAmt);
synth.heal();
addLog(`Used Core Restorer. Restored ${healAmt} HP!`);
} else if (itemKey === 'elixir') {
mon.status = null;
synth.heal();
addLog(`Used Resonance Elixir. Status cleared!`);
}
battle.phase = 'anim';
renderBattle();
setTimeout(() => enemyTurn(), 1000);
}
// ============================================================
// BATTLE RESULTS & LEVELING
// ============================================================
function handleEnemyFainted() {
if (!battle) return;
const enemy = battle.enemyMon;
const player = battle.playerMon;
// XP reward
const xpGain = Math.floor((enemy.level * 15) * (battle.isBoss ? 2.5 : battle.isWild ? 1 : 1.8));
player.xp += xpGain;
let shardReward = 0;
if (battle.isWild) shardReward = Math.floor(5 + enemy.level * 3);
else if (battle.context.isTrainer) shardReward = Math.floor(15 + enemy.level * 8);
else if (battle.isBoss) shardReward = battle.context.rewardShards || 100;
gameState.shards += shardReward;
addLog(`${enemy.name} was defeated! Gained ${xpGain} XP!${shardReward > 0 ? ' +' + shardReward + ' shards' : ''}`);
// Level up check
while (player.xp >= player.xpToNext && player.level < 50) {
player.level++;
player.xp -= player.xpToNext;
player.xpToNext = xpForLevel(player.level + 1);
// Recalc stats
const base = RESONANTS_LOOKUP[player.baseId];
const statMult = 1 + (player.level - 1) * 0.08;
player.maxHP = Math.floor(base.baseStats.hp * statMult);
player.currentHP = Math.min(player.maxHP, player.currentHP + Math.floor(base.baseStats.hp * 0.08));
player.attack = Math.floor(base.baseStats.attack * statMult);
player.defense = Math.floor(base.baseStats.defense * statMult);
player.speed = Math.floor(base.baseStats.speed * statMult);
addLog(`${player.name} grew to Lv.${player.level}!`);
synth.levelUp();
// Evolution check
if (base.evolvesTo && player.level >= base.evolveLevel && !player.evolved) {
player.evolved = true;
evolveInBattle(player);
}
}
// Discover enemy in dex
if (!gameState.dexDiscovered.includes(enemy.baseId)) {
gameState.dexDiscovered.push(enemy.baseId);
}
saveGame();
updateHPBars();
// Handle trainers with multiple mons or boss progression
if (battle.isBoss) {
const zoneId = gameState.currentZone;
if (!gameState.defeatedBosses.includes(zoneId)) {
gameState.defeatedBosses.push(zoneId);
}
// Unlock next zone
const zoneIdx = ZONES.findIndex(z => z.id === zoneId);
if (zoneIdx < ZONES.length - 1) {
const nextZone = ZONES[zoneIdx + 1];
if (!gameState.unlockedZones.includes(nextZone.id)) {
gameState.unlockedZones.push(nextZone.id);
}
}
completeCurrentNode();
saveGame();
setTimeout(() => endBattle('victory_boss'), 2000);
} else {
completeCurrentNode();
saveGame();
setTimeout(() => endBattle('victory'), 1500);
}
}
function evolveInBattle(mon) {
const base = RESONANTS_LOOKUP[mon.baseId];
if (!base || !base.evolvesTo) return;
const evoBase = RESONANTS_LOOKUP[base.evolvesTo];
if (!evoBase) return;
addLog(`✨ ${mon.name} is evolving!`);
mon.baseId = evoBase.id;
mon.name = evoBase.name;
mon.element = evoBase.element;
mon.tier = evoBase.tier;
mon.moves = [...evoBase.moves];
// Recalc stats with evolution base
const statMult = 1 + (mon.level - 1) * 0.08;
mon.maxHP = Math.floor(evoBase.baseStats.hp * statMult);
mon.currentHP = mon.maxHP;
mon.attack = Math.floor(evoBase.baseStats.attack * statMult);
mon.defense = Math.floor(evoBase.baseStats.defense * statMult);
mon.speed = Math.floor(evoBase.baseStats.speed * statMult);
if (!gameState.dexDiscovered.includes(evoBase.id)) {
gameState.dexDiscovered.push(evoBase.id);
}
showLevelUp(mon, true);
}
function handlePlayerFainted() {
if (!battle) return;
const mon = battle.playerMon;
addLog(`${mon.name} fainted!`);
// Check if any conscious mon available
const conscious = gameState.activeParty.find(m => m.currentHP > 0 && m.uid !== mon.uid);
if (conscious) {
addLog(`Switch to another Resonant!`);
battle.phase = 'select';
renderBattleActions();
// Force switch
setTimeout(() => {
showExchange();
}, 500);
} else {
addLog('All your Resonants have fallen...');
setTimeout(() => endBattle('defeat'), 1500);
}
}
function endBattle(result) {
const modal = document.getElementById('battle-result-modal');
const content = document.getElementById('battle-result-content');
let html = '';
if (result === 'victory') {
html = `<div class="text-2xl mb-3">🏆</div><h2 class="font-pixel text-sm text-green-400 mb-3">VICTORY!</h2>
<p class="text-sm text-gray-300 mb-4">You defeated the opponent!</p>`;
} else if (result === 'victory_boss') {
html = `<div class="text-2xl mb-3">👑</div><h2 class="font-pixel text-sm text-yellow-400 mb-3">GATEWAY OPENED!</h2>
<p class="text-sm text-gray-300 mb-4">The zone gatekeeper has fallen. A new path is revealed!</p>`;
synth.victory();
} else if (result === 'capture') {
html = `<div class="text-2xl mb-3">🔮</div><h2 class="font-pixel text-sm text-purple-400 mb-3">CAPTURED!</h2>
<p class="text-sm text-gray-300 mb-4">A new Resonant joins your party!</p>`;
} else if (result === 'flee') {
html = `<div class="text-2xl mb-3">🏃</div><h2 class="font-pixel text-sm text-blue-400 mb-3">ESCAPED!</h2>
<p class="text-sm text-gray-300 mb-4">You retreated safely.</p>`;
} else if (result === 'defeat') {
html = `<div class="text-2xl mb-3">💀</div><h2 class="font-pixel text-sm text-red-400 mb-3">DEFEATED</h2>
<p class="text-sm text-gray-300 mb-4">Your party was knocked out. You return to base.</p>`;
// Heal after defeat
gameState.activeParty.forEach(m => {
m.currentHP = m.maxHP;
m.status = null;
});
gameState.currentZone = null;
} else {
html = `<div class="text-2xl mb-3">⏹️</div><h2 class="font-pixel text-sm text-gray-400 mb-3">FORFEIT</h2>`;
}
html += `<button onclick="closeBattleResult()" class="btn-game btn-primary w-full justify-center mt-2">Continue</button>`;
content.innerHTML = html;
modal.classList.remove('hidden');
}
function closeBattleResult() {
document.getElementById('battle-result-modal').classList.add('hidden');
battle = null;
if (gameState.currentZone) {
showScreen('screen-overworld');
} else {
showScreen('screen-zoneselect');
}
}
function showLevelUp(mon, isEvolution) {
const modal = document.getElementById('levelup-modal');
const content = document.getElementById('levelup-content');
content.innerHTML = isEvolution
? `<div class="anim-levelup text-3xl mb-3">✨</div><h2 class="font-pixel text-sm text-yellow-400 mb-2">EVOLUTION!</h2>
<p class="text-sm text-gray-300 mb-1">${mon.name} evolved!</p>
<p class="text-xs text-gray-400 mb-4">Now Lv.${mon.level} ${RESONANTS_LOOKUP[mon.baseId]?.name || ''}</p>
<button onclick="closeLevelUp()" class="btn-game btn-primary w-full justify-center">Amazing!</button>`
: `<div class="anim-levelup text-2xl mb-3">⬆️</div><h2 class="font-pixel text-sm text-purple-400 mb-2">LEVEL UP!</h2>
<p class="text-sm text-gray-300 mb-4">${mon.name} reached Lv.${mon.level}!</p>
<button onclick="closeLevelUp()" class="btn-game btn-primary w-full justify-center">Great!</button>`;
modal.classList.remove('hidden');
}
function closeLevelUp() {
document.getElementById('levelup-modal').classList.add('hidden');
}
// ============================================================
// BATTLE STARTERS
// ============================================================
function startWildBattle(zone, node) {
const elPool = node.elements;
const chosenEl = elPool[Math.floor(Math.random() * elPool.length)];
// Find a monster of this element appropriate for level range
const candidates = RESONANTS_DB.filter(r => r.element === chosenEl && r.tier === 1);
if (candidates.length === 0) {
const allEl = RESONANTS_DB.filter(r => r.element === chosenEl);
candidates.push(...allEl);
}
if (candidates.length === 0) {
// Fallback: any monster
candidates.push(...RESONANTS_DB.filter(r => r.tier === 1));
}
const base = candidates[Math.floor(Math.random() * candidates.length)];
const lvlRange = node.levels || zone.lvlRange;
const lvl = lvlRange[0] + Math.floor(Math.random() * (lvlRange[1] - lvlRange[0] + 1));
const enemy = createMonster(base.id, lvl);
showScreen('screen-battle');
setTimeout(() => initBattle(enemy, { isWild: true, zone, node }), 400);
}
function startTrainerBattle(zone, node) {
const elPool = node.elements;
const trainerName = node.name;
const dialogue = node.dialogue;
const lvlRange = node.levels || zone.lvlRange;
const lvl = lvlRange[0] + Math.floor(Math.random() * (lvlRange[1] - lvlRange[0] + 1));
const chosenEl = elPool[Math.floor(Math.random() * elPool.length)];
const candidates = RESONANTS_DB.filter(r => r.element === chosenEl && r.tier <= 2);
if (candidates.length === 0) candidates.push(...RESONANTS_DB.filter(r => r.tier <= 2));
const base = candidates[Math.floor(Math.random() * candidates.length)];
const enemy = createMonster(base.id, lvl);
showScreen('screen-battle');
setTimeout(() => initBattle(enemy, { isWild: false, isTrainer: true, trainerName, dialogue, zone, node }), 400);
}
function startBossBattle(zone, node) {
const bossName = node.bossName || node.name;
const dialogue = node.dialogue;
const lvl = node.level || zone.lvlRange[1];
const el = node.element || 'cosmos';
// Find a tier 2-3 boss monster
const candidates = RESONANTS_DB.filter(r => r.element === el && r.tier >= 2);
const base = candidates.length > 0 ? candidates[Math.floor(Math.random() * candidates.length)]
: RESONANTS_DB.filter(r => r.tier >= 2)[0];
const enemy = createMonster(base.id, lvl);
showScreen('screen-battle');
setTimeout(() => initBattle(enemy, {
isWild: false, isBoss: true, bossName, dialogue,
rewardShards: node.rewardShards || 100,
zone, node
}), 400);
}
// ============================================================
// VISUAL EFFECTS
// ============================================================
function spawnParticles(side, type) {
const layer = document.getElementById('battle-particles');
if (!layer) return;
const colors = {
super: ['#FF6B35', '#FFEB3B', '#A855F7'],
hit: ['#00BCD4', '#81D4FA', '#ffffff'],
weak: ['#6B7280', '#9CA3AF'],
heal: ['#4CAF50', '#8BC34A', '#C8E6C9']
};
const cols = colors[type] || colors.hit;
const count = type === 'super' ? 20 : (type === 'heal' ? 12 : 14);
const targetEl = document.getElementById(side === 'enemy' ? 'enemy-sprite-wrap' : 'player-sprite-wrap');
const rect = targetEl.getBoundingClientRect();
const layerRect = layer.getBoundingClientRect();
const cx = rect.left + rect.width / 2 - layerRect.left;
const cy = rect.top + rect.height / 2 - layerRect.top;
for (let i = 0; i < count; i++) {
const p = document.createElement('div');
p.className = 'particle';
const size = 3 + Math.random() * 8;
const angle = Math.random() * Math.PI * 2;
const dist = 20 + Math.random() * 60;
const color = cols[Math.floor(Math.random() * cols.length)];
p.style.cssText = `
width:${size}px;height:${size}px;background:${color};
left:${cx}px;top:${cy}px;
box-shadow:0 0 6px ${color};
`;
// Use CSS variable for direction
p.style.setProperty('--dx', Math.cos(angle) * dist + 'px');
p.style.setProperty('--dy', Math.sin(angle) * dist + 'px');
layer.appendChild(p);
setTimeout(() => p.remove(), 1200);
}
}
function shakeScreen() {
const battleEl = document.getElementById('screen-battle');
if (!battleEl) return;
battleEl.classList.remove('anim-shake');
void battleEl.offsetWidth;
battleEl.classList.add('anim-shake');
setTimeout(() => battleEl.classList.remove('anim-shake'), 400);
}
// ============================================================
// SHOP
// ============================================================
const SHOP_ITEMS = [
{ id: 'prism', name: 'Harmonizer Prism', desc: 'Basic capture prism.', price: 30, icon: '🔮' },
{ id: 'hyperPrism', name: 'Hyper-Conductive Prism', desc: 'High catch rate prism.', price: 80, icon: '💎' },
{ id: 'restorer', name: 'Core Restorer', desc: 'Restores 50% HP.', price: 50, icon: '💚' },
{ id: 'elixir', name: 'Resonance Elixir', desc: 'Cures all status conditions.', price: 60, icon: '✨' },
{ id: 'harmonyBoost', name: 'Harmonic Tuner', desc: 'Permanently boosts a Resonants Speed by 5.', price: 200, icon: '🎯' }
];
function renderShop() {
document.getElementById('shop-shards').textContent = gameState.shards;
const grid = document.getElementById('shop-items');
let html = '';
SHOP_ITEMS.forEach(item => {
const owned = gameState.inventory[item.id] || 0;
html += `<div class="glass p-4 rounded-xl flex flex-col gap-3">
<div class="flex items-center gap-3">
<span class="text-2xl">${item.icon}</span>
<div>
<p class="font-semibold text-sm">${item.name}</p>
<p class="text-[10px] text-gray-400">${item.desc}</p>
${owned > 0 ? `<p class="text-[10px] text-purple-300">Owned: ${owned}</p>` : ''}
</div>
</div>
<button class="btn-game btn-primary justify-center ${gameState.shards < item.price ? 'opacity-50' : ''}"
onclick="buyItem('${item.id}')" ${gameState.shards < item.price ? 'disabled' : ''}>
🪙 ${item.price} Shards
</button>
</div>`;
});
grid.innerHTML = html;
}
function buyItem(itemId) {
const item = SHOP_ITEMS.find(i => i.id === itemId);
if (!item || gameState.shards < item.price) return;
synth.select();
gameState.shards -= item.price;
if (itemId === 'harmonyBoost') {
// Apply to first conscious mon
const mon = gameState.activeParty[0];
if (mon) {
mon.speed += 5;
notify(`${mon.name}'s Speed increased!`);
}
} else {
gameState.inventory[itemId] = (gameState.inventory[itemId] || 0) + 1;
notify(`Purchased ${item.name}!`);
}
saveGame();
renderShop();
updateTopbar();
}
function openShop() { synth.select(); showScreen('screen-shop'); }
// ============================================================
// PARTY SCREEN
// ============================================================
function renderParty() {
const partyGrid = document.getElementById('party-grid');
let html = '';
for (let i = 0; i < 3; i++) {
const mon = gameState.activeParty[i];
if (mon) {
html += `<div class="glass p-4 rounded-xl ${elBorderClass(mon.element)}" style="border-width:2px;">
<div class="flex justify-center mb-3">${renderResonantSVG(mon, 100)}</div>
<p class="font-semibold text-sm ${elTextClass(mon.element)} text-center">${mon.name}</p>
<p class="text-center text-[10px] text-gray-400">Lv.${mon.level} ${mon.element.toUpperCase()}</p>
<div class="mt-2">
<div class="hp-bar-track mb-2"><div class="hp-bar-fill" style="width:${Math.max(0,mon.currentHP/mon.maxHP*100)}%;"></div></div>
<div class="xp-bar-track mb-2"><div class="xp-bar-fill" style="width:${Math.min(100,mon.xp/mon.xpToNext*100)}%;"></div></div>
<div class="grid grid-cols-2 gap-1 text-[9px] text-gray-400">
<span>HP: ${mon.currentHP}/${mon.maxHP}</span>
<span>ATK: ${mon.attack}</span>
<span>DEF: ${mon.defense}</span>
<span>SPD: ${mon.speed}</span>
</div>
</div>
<div class="mt-2 flex flex-wrap gap-1">
${mon.moves.slice(0, 4).map(mId => {
const mv = MOVE_LOOKUP[mId];
return mv ? `<span class="badge ${elBadgeClass(mv.type)} text-[8px]">${mv.name}</span>` : '';
}).join('')}
</div>
${i > 0 ? `<button class="btn-game !mt-3 w-full justify-center text-xs" onclick="swapPartySlots(0, ${i})">Move to Front</button>` : '<span class="badge badge-cosmos text-[8px] mt-3 inline-block">LEAD</span>'}
</div>`;
} else {
html += `<div class="glass p-4 rounded-xl border border-dashed border-white/10 flex items-center justify-center min-h-[250px]">
<span class="text-gray-500 text-xs">Empty Slot</span>
</div>`;
}
}
partyGrid.innerHTML = html;
// Storage box
const storageGrid = document.getElementById('storage-grid');
let shtml = '';
if (gameState.storageBox.length === 0) {
shtml = '<p class="text-gray-500 text-xs col-span-full text-center py-4">Storage is empty</p>';
} else {
gameState.storageBox.forEach((mon, i) => {
shtml += `<div class="glass-light p-2 rounded-lg cursor-pointer hover:border-purple-400/50 transition-colors" onclick="swapToParty(${i})">
<div class="flex justify-center mb-1">${renderResonantSVG(mon, 50)}</div>
<p class="text-[10px] text-center ${elTextClass(mon.element)}">${mon.name}</p>
<p class="text-[9px] text-center text-gray-500">Lv.${mon.level}</p>
</div>`;
});
}
storageGrid.innerHTML = shtml;
}
function swapPartySlots(a, b) {
synth.select();
[gameState.activeParty[a], gameState.activeParty[b]] = [gameState.activeParty[b], gameState.activeParty[a]];
saveGame();
renderParty();
}
function swapToParty(storageIndex) {
synth.select();
if (gameState.activeParty.length >= 3) {
notify('Party is full! Remove a Resonant first.');
return;
}
const mon = gameState.storageBox.splice(storageIndex, 1)[0];
gameState.activeParty.push(mon);
saveGame();
renderParty();
notify(`${mon.name} joined the party!`);
}
function openParty() { synth.select(); showScreen('screen-party'); }
// ============================================================
// CONDUCTOR'S DEX
// ============================================================
function renderDex() {
const grid = document.getElementById('dex-grid');
const discovered = gameState.dexDiscovered;
document.getElementById('dex-count').textContent = `${discovered.length}/18`;
let html = '';
RESONANTS_DB.forEach(r => {
const isFound = discovered.includes(r.id);
html += `<div class="dex-card glass-light p-3 rounded-lg cursor-pointer ${isFound ? '' : 'undiscovered'} ${isFound ? 'hover:border-purple-400/50' : ''}"
${isFound ? `onclick="showDexDetail('${r.id}')"` : ''}>
<div class="flex justify-center mb-2">${isFound ? renderResonantSVG(createMonster(r.id, r.level), 60) : '<svg width="60" height="60" viewBox="0 0 60 60"><circle cx="30" cy="30" r="20" fill="rgba(100,100,100,0.2)" stroke="rgba(255,255,255,0.1)" stroke-dasharray="4"/><text x="30" y="35" text-anchor="middle" fill="rgba(255,255,255,0.2)" font-size="16">?</text></svg>'}</div>
<p class="${isFound ? elTextClass(r.element) : 'text-gray-600'} text-[10px] text-center font-semibold">${isFound ? r.name : '???'}</p>
${isFound ? `<p class="text-[9px] text-center text-gray-400">#${String(RESONANTS_DB.indexOf(r)+1).padStart(2,'0')}</p>` : ''}
</div>`;
});
grid.innerHTML = html;
}
function showDexDetail(id) {
synth.select();
const r = RESONANTS_LOOKUP[id];
const detail = document.getElementById('dex-detail');
detail.classList.remove('hidden');
detail.innerHTML = `<div class="glass p-5 rounded-xl ${elBorderClass(r.element)}" style="border-width:2px;">
<div class="flex items-start gap-5">
<div>${renderResonantSVG(createMonster(id, r.level), 100)}</div>
<div class="flex-1">
<h3 class="font-pixel text-[11px] ${elTextClass(r.element)} mb-1">${r.name}</h3>
<div class="flex gap-2 mb-2">
<span class="badge ${elBadgeClass(r.element)}">${r.element.toUpperCase()}</span>
<span class="badge badge-cosmos">Tier ${r.tier}</span>
<span class="badge badge-cosmos">Lv.${r.level}+</span>
</div>
<p class="text-xs text-gray-300 mb-3 leading-relaxed">${r.desc}</p>
<div class="grid grid-cols-2 gap-x-4 gap-y-1 text-[10px] mb-3">
<span class="text-gray-400">HP: <span class="text-white">${r.baseStats.hp}</span></span>
<span class="text-gray-400">ATK: <span class="text-white">${r.baseStats.attack}</span></span>
<span class="text-gray-400">DEF: <span class="text-white">${r.baseStats.defense}</span></span>
<span class="text-gray-400">SPD: <span class="text-white">${r.baseStats.speed}</span></span>
</div>
<div class="flex flex-wrap gap-1">
${r.moves.map(mId => { const mv = MOVE_LOOKUP[mId]; return mv ? `<span class="badge ${elBadgeClass(mv.type)} text-[8px]">${mv.name} ${mv.power > 0 ? '(' + mv.power + ')' : ''}</span>` : ''; }).join('')}
</div>
${r.evolvesTo ? `<p class="text-[10px] text-purple-300 mt-2">Evolves into ${RESONANTS_LOOKUP[r.evolvesTo]?.name || '???'} at Lv.${r.evolveLevel}</p>` : '<p class="text-[10px] text-yellow-300 mt-2">Final Evolution</p>'}
</div>
</div>
</div>`;
detail.scrollIntoView({ behavior: 'smooth' });
}
function openDex() { synth.select(); showScreen('screen-dex'); }
// ============================================================
// SETTINGS
// ============================================================
function renderSettings() {
document.getElementById('toggle-sfx').checked = gameState.settings.sfx;
document.getElementById('toggle-music').checked = gameState.settings.music;
}
function openSettings() {
synth.select();
showScreen('screen-settings');
}
// ============================================================
// HOVER SOUND HANDLERS
// ============================================================
document.addEventListener('mouseover', (e) => {
const btn = e.target.closest('.btn-game, .node, .dex-card');
if (btn) synth.hover();
});
document.addEventListener('change', (e) => {
if (e.target.id === 'toggle-sfx') {
gameState.settings.sfx = e.target.checked;
synth.sfxEnabled = e.target.checked;
saveGame();
}
if (e.target.id === 'toggle-music') {
gameState.settings.music = e.target.checked;
synth.musicEnabled = e.target.checked;
if (e.target.checked) synth.startMusic();
else synth.stopMusic();
saveGame();
}
});
// ============================================================
// INITIALIZATION
// ============================================================
function startNewGame() {
synth.init();
synth.resume();
synth.select();
if (hasSave() && !confirm('Starting a new game will erase your current progress. Continue?')) return;
// Reset
gameState = {
playerName: 'Conductor',
activeParty: [],
storageBox: [],
inventory: { prism: 5, hyperPrism: 1, restorer: 2, elixir: 1 },
shards: 50,
unlockedZones: ['verdant'],
currentZone: null,
currentNodeIndex: 0,
completedNodes: {},
defeatedBosses: [],
dexDiscovered: [],
starterChosen: false,
settings: { sfx: true, music: true }
};
uidCounter = 1;
saveGame();
showStarterSelection();
}
function continueGame() {
synth.init();
synth.resume();
synth.select();
if (loadGame()) {
synth.sfxEnabled = gameState.settings.sfx !== false;
synth.musicEnabled = gameState.settings.music !== false;
showScreen('screen-zoneselect');
if (gameState.settings.music) synth.startMusic();
} else {
notify('No save data found!');
}
}
// Keyboard shortcut for audio init
document.addEventListener('click', () => {
synth.init();
synth.resume();
if (gameState.settings.music) synth.startMusic();
}, { once: true });
// Boot
window.addEventListener('DOMContentLoaded', () => {
// Apply settings from save
if (hasSave()) {
loadGame();
document.getElementById('btn-continue').disabled = false;
document.getElementById('btn-continue').classList.remove('opacity-40');
synth.sfxEnabled = gameState.settings.sfx !== false;
synth.musicEnabled = gameState.settings.music !== false;
}
showScreen('screen-menu');
});
// Auto-save on unload
window.addEventListener('beforeunload', saveGame);
// Prevent context menu to feel more game-like
document.addEventListener('contextmenu', e => {
if (e.target.closest('#app')) e.preventDefault();
});
</script>
</body>
</html>
<!-- agent-meta {"model":"meituan/longcat-2.0","provider":"openrouter","persona":"full","sessionId":"6b4880b1-f4e6-45dc-b8c3-9d860357c113","tokensIn":1353553,"tokensOut":49723,"tokensTotal":1403276,"turns":22,"toolCalls":23,"failedToolCalls":1,"timestamp":"2026-07-20T20:05:08.271Z"} -->