Aethelgard
ornith 1.0 35b · typescript

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 161,457 bytes · SHA-256 4dc3615c68f6
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Aethelgard: Resonant Chronicles</title>
<script src="https://cdn.tailwindcss.com/"></script>
<style>
/* ── css: base ── */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Press+Start+2P&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; }
body {
background: #0f0a1e;
background-image:
radial-gradient(ellipse at 20% 50%, rgba(88, 28, 135, 0.15) 0%, transparent 60%),
radial-gradient(ellipse at 80% 20%, rgba(15, 118, 110, 0.1) 0%, transparent 50%),
radial-gradient(ellipse at 50% 80%, rgba(139, 92, 246, 0.08) 0%, transparent 50%);
font-family: 'Inter', sans-serif;
color: #e2e8f0;
user-select: none;
-webkit-user-select: none;
}
#app { width: 100%; height: 100vh; }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: rgba(15, 23, 42, 0.3); }
::-webkit-scrollbar-thumb { background: rgba(139, 92, 246, 0.3); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: rgba(139, 92, 246, 0.5); }
canvas { display: block; }
.screen { transition: opacity 0.4s ease, transform 0.4s ease; }
.screen.hidden {
opacity: 0;
pointer-events: none;
transform: scale(0.98);
}
/* ── css: glassmorphism ── */
.glass-card {
background: rgba(15, 23, 42, 0.6);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.glass-bar {
background: rgba(15, 23, 42, 0.8);
backdrop-filter: blur(16px) saturate(150%);
-webkit-backdrop-filter: blur(16px) saturate(150%);
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.glass-panel {
background: rgba(15, 23, 42, 0.7);
backdrop-filter: blur(12px) saturate(140%);
-webkit-backdrop-filter: blur(12px) saturate(140%);
}
.glass-badge {
background: rgba(30, 41, 59, 0.5);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.06);
}
.glass-icon-btn {
background: rgba(30, 41, 59, 0.4);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.06);
}
.glass-icon-btn:hover {
background: rgba(30, 41, 59, 0.7);
border-color: rgba(255, 255, 255, 0.12);
}
/* ── css: element-themes ── */
.element-pyro { border-color: rgba(249, 115, 22, 0.3) !important; box-shadow: 0 0 20px rgba(249, 115, 22, 0.1), inset 0 0 20px rgba(249, 115, 22, 0.02); }
.element-hydro { border-color: rgba(6, 182, 212, 0.3) !important; box-shadow: 0 0 20px rgba(6, 182, 212, 0.1), inset 0 0 20px rgba(6, 182, 212, 0.02); }
.element-flora { border-color: rgba(34, 197, 94, 0.3) !important; box-shadow: 0 0 20px rgba(34, 197, 94, 0.1), inset 0 0 20px rgba(34, 197, 94, 0.02); }
.element-volt { border-color: rgba(250, 204, 21, 0.3) !important; box-shadow: 0 0 20px rgba(250, 204, 21, 0.1), inset 0 0 20px rgba(250, 204, 21, 0.02); }
.element-zephyr { border-color: rgba(167, 139, 250, 0.3) !important; box-shadow: 0 0 20px rgba(167, 139, 250, 0.1), inset 0 0 20px rgba(167, 139, 250, 0.02); }
.element-earth { border-color: rgba(161, 98, 7, 0.3) !important; box-shadow: 0 0 20px rgba(161, 98, 7, 0.1), inset 0 0 20px rgba(161, 98, 7, 0.02); }
.element-cosmos { border-color: rgba(168, 85, 247, 0.3) !important; box-shadow: 0 0 20px rgba(168, 85, 247, 0.1), inset 0 0 20px rgba(168, 85, 247, 0.02); }
.type-pyro { color: #f97316; }
.type-hydro { color: #06b6d4; }
.type-flora { color: #22c55e; }
.type-volt { color: #facc15; }
.type-zephyr { color: #a78bfa; }
.type-earth { color: #a16207; }
.type-cosmos { color: #a855f7; }
.bg-pyro { background: linear-gradient(135deg, rgba(249, 115, 22, 0.15), rgba(234, 88, 12, 0.05)); }
.bg-hydro { background: linear-gradient(135deg, rgba(6, 182, 212, 0.15), rgba(8, 145, 178, 0.05)); }
.bg-flora { background: linear-gradient(135deg, rgba(34, 197, 94, 0.15), rgba(22, 163, 74, 0.05)); }
.bg-volt { background: linear-gradient(135deg, rgba(250, 204, 21, 0.15), rgba(234, 179, 8, 0.05)); }
.bg-zephyr { background: linear-gradient(135deg, rgba(167, 139, 250, 0.15), rgba(139, 92, 246, 0.05)); }
.bg-earth { background: linear-gradient(135deg, rgba(161, 98, 7, 0.15), rgba(133, 77, 14, 0.05)); }
.bg-cosmos { background: linear-gradient(135deg, rgba(168, 85, 247, 0.15), rgba(147, 51, 234, 0.05)); }
.type-glow-pyro { text-shadow: 0 0 10px rgba(249, 115, 22, 0.5); }
.type-glow-hydro { text-shadow: 0 0 10px rgba(6, 182, 212, 0.5); }
.type-glow-flora { text-shadow: 0 0 10px rgba(34, 197, 94, 0.5); }
.type-glow-volt { text-shadow: 0 0 10px rgba(250, 204, 21, 0.5); }
.type-glow-zephyr { text-shadow: 0 0 10px rgba(167, 139, 250, 0.5); }
.type-glow-earth { text-shadow: 0 0 10px rgba(161, 98, 7, 0.5); }
.type-glow-cosmos { text-shadow: 0 0 10px rgba(168, 85, 247, 0.5); }
/* ── css: hud ── */
.hp-bar-container { position: relative; }
.hp-bar { background: rgba(15, 23, 42, 0.8); border-radius: 9999px; overflow: hidden; }
.hp-fill {
background: linear-gradient(90deg, #22c55e, #4ade80);
box-shadow: 0 0 10px rgba(34, 197, 94, 0.3);
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1), background 0.3s ease;
}
.hp-fill.mid {
background: linear-gradient(90deg, #eab308, #facc15);
box-shadow: 0 0 10px rgba(234, 179, 8, 0.3);
}
.hp-fill.low {
background: linear-gradient(90deg, #ef4444, #f87171);
box-shadow: 0 0 10px rgba(239, 68, 68, 0.3);
}
.xp-bar { background: rgba(15, 23, 42, 0.8); border-radius: 9999px; overflow: hidden; }
.status-badge {
display: inline-flex; align-items: center; gap: 4px;
padding: 2px 8px; border-radius: 9999px; font-size: 10px;
background: rgba(30, 41, 59, 0.6); border: 1px solid rgba(255,255,255,0.08);
}
.party-slot {
width: 56px; height: 56px; border-radius: 12px;
background: rgba(30, 41, 59, 0.5); backdrop-filter: blur(8px);
border: 2px solid rgba(255, 255, 255, 0.08);
display: flex; align-items: center; justify-content: center;
cursor: pointer; transition: all 0.2s ease; position: relative;
}
.party-slot:hover { border-color: rgba(139, 92, 246, 0.4); background: rgba(30, 41, 59, 0.7); }
.party-slot.active { border-color: rgba(6, 182, 212, 0.6); box-shadow: 0 0 12px rgba(6, 182, 212, 0.2); }
.party-slot.fainted { opacity: 0.3; border-color: rgba(239, 68, 68, 0.3); }
.dex-card {
background: rgba(15, 23, 42, 0.5); backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.06); border-radius: 16px;
padding: 12px; cursor: pointer; transition: all 0.2s ease;
display: flex; flex-direction: column; align-items: center; gap: 6px;
}
.dex-card:hover {
background: rgba(30, 41, 59, 0.7); border-color: rgba(139, 92, 246, 0.3);
transform: translateY(-2px); box-shadow: 0 8px 24px rgba(0,0,0,0.3);
}
.dex-card.locked { opacity: 0.3; cursor: default; }
.dex-card.locked:hover { transform: none; border-color: rgba(255,255,255,0.06); }
.shop-item {
background: rgba(15, 23, 42, 0.5); backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.06); border-radius: 16px;
padding: 16px; transition: all 0.2s ease; display: flex; flex-direction: column; gap: 8px;
}
.shop-item:hover {
background: rgba(30, 41, 59, 0.7); border-color: rgba(250, 204, 21, 0.3);
transform: translateY(-2px); box-shadow: 0 8px 24px rgba(0,0,0,0.3);
}
.menu-btn { position: relative; overflow: hidden; }
.menu-btn::after {
content: ''; position: absolute; inset: 0;
background: linear-gradient(135deg, transparent 40%, rgba(255,255,255,0.1) 50%, transparent 60%);
transform: translateX(-100%); transition: transform 0.5s ease;
}
.menu-btn:hover::after { transform: translateX(100%); }
.action-btn { position: relative; overflow: hidden; }
.action-btn:hover { border-color: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.05); }
.zone-btn.active {
background: rgba(139, 92, 246, 0.15);
box-shadow: 0 0 16px rgba(139, 92, 246, 0.15);
}
.prism-btn:hover { border-color: rgba(6, 182, 212, 0.4); background: rgba(6, 182, 212, 0.05); }
.dex-mini-sprite { width: 48px; height: 48px; }
/* ── css: animations ── */
@keyframes shake {
0%, 100% { transform: translateX(0); }
10% { transform: translateX(-8px) rotate(-1deg); }
20% { transform: translateX(6px) rotate(1deg); }
30% { transform: translateX(-5px); }
40% { transform: translateX(4px); }
50% { transform: translateX(-3px); }
60% { transform: translateX(2px); }
70% { transform: translateX(-1px); }
}
@keyframes flash-white {
0%, 100% { opacity: 1; }
50% { opacity: 0.2; }
}
@keyframes bounce-in {
0% { transform: scale(0.3); opacity: 0; }
50% { transform: scale(1.05); }
70% { transform: scale(0.95); }
100% { transform: scale(1); opacity: 1; }
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-8px); }
}
@keyframes pulse-glow {
0%, 100% { box-shadow: 0 0 20px rgba(139, 92, 246, 0.2); }
50% { box-shadow: 0 0 40px rgba(139, 92, 246, 0.4); }
}
@keyframes sparkle {
0%, 100% { opacity: 0; transform: scale(0) rotate(0deg); }
50% { opacity: 1; transform: scale(1) rotate(180deg); }
}
@keyframes prism-capture {
0% { transform: scale(1) rotate(0deg); filter: brightness(1); }
25% { transform: scale(1.3) rotate(90deg); filter: brightness(2); }
50% { transform: scale(0.8) rotate(180deg); filter: brightness(1.5); }
75% { transform: scale(1.2) rotate(270deg); filter: brightness(2); }
100% { transform: scale(0.1) rotate(360deg); filter: brightness(3); }
}
@keyframes evolutions-flash {
0%, 100% { opacity: 1; filter: brightness(1); }
25% { opacity: 0.5; filter: brightness(3) hue-rotate(90deg); }
50% { opacity: 1; filter: brightness(1); }
75% { opacity: 0.5; filter: brightness(3) hue-rotate(-90deg); }
}
@keyframes particle-float {
0% { transform: translateY(0) translateX(0); opacity: 1; }
100% { transform: translateY(-200px) translateX(var(--drift)); opacity: 0; }
}
.shake { animation: shake 0.5s ease-in-out; }
.flash-white { animation: flash-white 0.3s ease-in-out; }
.bounce-in { animation: bounce-in 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55); }
.float { animation: float 3s ease-in-out infinite; }
.pulse-glow { animation: pulse-glow 2s ease-in-out infinite; }
.battle-sprite {
transition: transform 0.3s ease, filter 0.3s ease;
filter: drop-shadow(0 4px 12px rgba(0,0,0,0.4));
}
.battle-sprite.hit { animation: shake 0.4s ease-in-out; }
.battle-sprite.attack-right { transform: translateX(30px); }
.battle-sprite.attack-left { transform: translateX(-30px); }
.battle-sprite.faint {
opacity: 0; transform: translateY(20px) scale(0.8);
transition: all 0.6s ease-in; filter: grayscale(1);
}
.particle {
position: absolute; pointer-events: none; border-radius: 50%;
animation: particle-float 1.5s ease-out forwards;
}
.toast {
background: rgba(15, 23, 42, 0.9); backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 12px;
padding: 12px 20px; color: #e2e8f0; font-size: 14px;
animation: bounce-in 0.3s ease-out; box-shadow: 0 8px 32px rgba(0,0,0,0.4);
}
.dialog-overlay-enter {
animation: slide-up 0.3s ease-out;
}
@keyframes slide-up {
from { transform: translateY(100%); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.map-node {
cursor: pointer; transition: all 0.2s ease;
filter: drop-shadow(0 2px 8px rgba(0,0,0,0.3));
}
.map-node:hover { transform: scale(1.15); filter: drop-shadow(0 4px 16px rgba(139, 92, 246, 0.4)); }
.map-node.current { animation: pulse-glow 2s ease-in-out infinite; }
.map-node.cleared { opacity: 0.5; cursor: default; }
.map-node.cleared:hover { transform: none; filter: none; }
.evolution-flash { animation: evolutions-flash 1.5s ease-in-out; }
.prism-spin { animation: prism-capture 2s ease-in-out forwards; }
</style>
</head>
<body>
<!-- Main Menu Screen -->
<div id="screen-menu" class="screen absolute inset-0 flex items-center justify-center z-10">
<div class="glass-card text-center p-12 rounded-3xl border border-white/10 shadow-2xl max-w-md w-full mx-4">
<h1 class="text-5xl font-bold mb-2 bg-gradient-to-r from-violet-400 via-cyan-400 to-emerald-400 bg-clip-text text-transparent" style="font-family: 'Press Start 2P', cursive;">AETHELGARD</h1>
<p class="text-slate-400 mb-8 text-sm tracking-widest uppercase">Resonant Chronicles</p>
<div class="space-y-3">
<button id="btn-new-game" class="menu-btn w-full py-3 px-6 rounded-xl bg-gradient-to-r from-violet-600 to-indigo-600 hover:from-violet-500 hover:to-indigo-500 text-white font-semibold transition-all duration-200 shadow-lg shadow-violet-500/25 border border-white/10">New Journey</button>
<button id="btn-load-game" class="menu-btn w-full py-3 px-6 rounded-xl bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-500 hover:to-teal-500 text-white font-semibold transition-all duration-200 shadow-lg shadow-emerald-500/25 border border-white/10">Continue</button>
<button id="btn-reset" class="menu-btn w-full py-3 px-6 rounded-xl bg-slate-800/50 hover:bg-slate-700/50 text-slate-400 hover:text-red-400 font-semibold transition-all duration-200 border border-white/5">Reset Progress</button>
</div>
</div>
</div>
<!-- Overworld Screen -->
<div id="screen-overworld" class="screen hidden absolute inset-0 flex flex-col z-10">
<div class="glass-bar px-6 py-3 flex items-center justify-between border-b border-white/5">
<div class="flex items-center gap-4">
<span id="player-name" class="text-cyan-400 font-bold text-sm">Conductor</span>
<span class="text-slate-500">|</span>
<span id="zone-name" class="text-emerald-400 text-sm">Verdant Canopy</span>
</div>
<div class="flex items-center gap-3">
<div class="glass-badge flex items-center gap-1.5 px-3 py-1 rounded-full">
<svg class="w-4 h-4 text-yellow-400" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"></path></svg>
<span id="shard-count" class="text-yellow-400 text-sm font-bold">0</span>
</div>
<button id="btn-dex" class="glass-icon-btn p-2 rounded-lg hover:bg-white/5 transition-colors" title="Resonants Dex">
<svg class="w-5 h-5 text-violet-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path></svg>
</button>
<button id="btn-shop" class="glass-icon-btn p-2 rounded-lg hover:bg-white/5 transition-colors" title="Shop">
<svg class="w-5 h-5 text-amber-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="21" r="1"></circle><circle cx="20" cy="21" r="1"></circle><path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path></svg>
</button>
</div>
</div>
<div id="map-container" class="flex-1 relative overflow-hidden">
<canvas id="map-canvas"></canvas>
</div>
<div id="overworld-bottom" class="glass-panel border-t border-white/5 p-4 flex items-center justify-between gap-4">
<div id="party-display" class="flex items-center gap-2 flex-1"></div>
<div id="event-text" class="text-slate-300 text-sm max-w-md flex-1 italic opacity-0 transition-opacity duration-300"></div>
</div>
<div id="zone-nav" class="glass-panel border-t border-white/5 p-3 hidden">
<div class="flex items-center justify-center gap-2 flex-wrap">
<button data-zone="0" class="zone-btn glass-badge px-4 py-2 rounded-xl text-xs font-semibold transition-all border border-emerald-500/30 text-emerald-400 hover:bg-emerald-500/10">Canopy</button>
<button data-zone="1" class="zone-btn glass-badge px-4 py-2 rounded-xl text-xs font-semibold transition-all border border-blue-500/30 text-blue-400 opacity-50 cursor-not-allowed" disabled="">Grotto</button>
<button data-zone="2" class="zone-btn glass-badge px-4 py-2 rounded-xl text-xs font-semibold transition-all border border-red-500/30 text-red-400 opacity-50 cursor-not-allowed" disabled="">Crag</button>
<button data-zone="3" class="zone-btn glass-badge px-4 py-2 rounded-xl text-xs font-semibold transition-all border border-purple-500/30 text-purple-400 opacity-50 cursor-not-allowed" disabled="">Pillar</button>
</div>
</div>
</div>
<!-- Battle Screen -->
<div id="screen-battle" class="screen hidden absolute inset-0 flex flex-col z-20">
<div id="battle-arena" class="flex-1 relative overflow-hidden">
<canvas id="battle-canvas"></canvas>
<div id="enemy-info" class="absolute top-6 right-6 glass-card p-4 rounded-2xl border border-red-500/20 min-w-[220px]">
<div class="flex items-center justify-between mb-2">
<span id="enemy-name" class="text-white font-bold text-sm"></span>
<span id="enemy-level" class="text-slate-400 text-xs"></span>
</div>
<div class="hp-bar-container">
<div class="flex justify-between text-xs mb-1">
<span id="enemy-hp-text" class="text-red-400"></span>
<span id="enemy-hp-max" class="text-slate-500"></span>
</div>
<div class="hp-bar bg-slate-800/80 rounded-full h-2.5 overflow-hidden">
<div id="enemy-hp-fill" class="hp-fill h-full rounded-full transition-all duration-500" style="width:100%"></div>
</div>
</div>
<div id="enemy-status" class="mt-2 text-xs text-slate-400"></div>
</div>
<div id="player-info" class="absolute bottom-36 left-6 glass-card p-4 rounded-2xl border border-cyan-500/20 min-w-[240px]">
<div class="flex items-center justify-between mb-2">
<span id="player-resonant-name" class="text-white font-bold text-sm"></span>
<span id="player-resonant-level" class="text-slate-400 text-xs"></span>
</div>
<div class="hp-bar-container">
<div class="flex justify-between text-xs mb-1">
<span id="player-hp-text" class="text-cyan-400"></span>
<span id="player-hp-max" class="text-slate-500"></span>
</div>
<div class="hp-bar bg-slate-800/80 rounded-full h-2.5 overflow-hidden">
<div id="player-hp-fill" class="hp-fill h-full rounded-full transition-all duration-500" style="width:100%"></div>
</div>
</div>
<div id="player-xp-bar" class="mt-2">
<div class="flex justify-between text-xs mb-1">
<span class="text-violet-400">XP</span>
<span id="player-xp-text" class="text-slate-500"></span>
</div>
<div class="xp-bar bg-slate-800/80 rounded-full h-1.5 overflow-hidden">
<div id="player-xp-fill" class="xp-fill h-full bg-gradient-to-r from-violet-500 to-indigo-500 rounded-full transition-all duration-300" style="width:0%"></div>
</div>
</div>
<div id="player-status" class="mt-1.5 text-xs text-slate-400"></div>
</div>
<div id="battle-message" class="absolute top-6 left-6 glass-card px-5 py-3 rounded-2xl border border-white/10 max-w-sm">
<p id="battle-msg-text" class="text-slate-200 text-sm"></p>
</div>
<div id="battle-particles" class="absolute inset-0 pointer-events-none overflow-hidden"></div>
</div>
<div id="battle-actions" class="glass-panel border-t border-white/5 p-4">
<div id="action-buttons" class="grid grid-cols-2 gap-3 max-w-lg mx-auto">
<button data-action="attack" class="action-btn glass-card p-3 rounded-xl text-center hover:bg-white/5 transition-all border border-white/10 group">
<span class="text-red-400 font-bold text-sm block group-hover:scale-105 transition-transform">⚔ Attack</span>
</button>
<button data-action="prism" class="action-btn glass-card p-3 rounded-xl text-center hover:bg-white/5 transition-all border border-white/10 group">
<span class="text-cyan-400 font-bold text-sm block group-hover:scale-105 transition-transform">◇ Prism</span>
</button>
<button data-action="exchange" class="action-btn glass-card p-3 rounded-xl text-center hover:bg-white/5 transition-all border border-white/10 group">
<span class="text-emerald-400 font-bold text-sm block group-hover:scale-105 transition-transform">↔ Exchange</span>
</button>
<button data-action="inventory" class="action-btn glass-card p-3 rounded-xl text-center hover:bg-white/5 transition-all border border-white/10 group">
<span class="text-amber-400 font-bold text-sm block group-hover:scale-105 transition-transform">✦ Items</span>
</button>
</div>
<div id="submenu-prism" class="hidden max-w-lg mx-auto space-y-2">
<p class="text-slate-400 text-xs mb-2">Select Prism Type:</p>
<div class="grid grid-cols-3 gap-2">
<button data-prism-type="basic" class="prism-btn glass-card p-3 rounded-xl text-center hover:bg-white/5 transition-all border border-cyan-500/20">
<span class="text-cyan-400 font-bold text-xs block">Aether Prism</span>
<span class="text-slate-500 text-[10px]">Catch Rate: 1.0×</span>
</button>
<button data-prism-type="hyper" class="prism-btn glass-card p-3 rounded-xl text-center hover:bg-white/5 transition-all border border-violet-500/20">
<span class="text-violet-400 font-bold text-xs block">Hyper Prism</span>
<span class="text-slate-500 text-[10px]">Catch Rate: 2.5×</span>
</button>
<button data-prism-type="return" class="prism-btn glass-card p-3 rounded-xl text-center hover:bg-white/5 transition-all border border-red-500/20">
<span class="text-red-400 font-bold text-xs block">Cancel</span>
</button>
</div>
</div>
<div id="submenu-exchange" class="hidden max-w-lg mx-auto space-y-2">
<p class="text-slate-400 text-xs mb-2">Select Resonant to swap:</p>
<div id="exchange-list" class="grid grid-cols-1 gap-2"></div>
</div>
<div id="submenu-inventory" class="hidden max-w-lg mx-auto space-y-2">
<p class="text-slate-400 text-xs mb-2">Select Item:</p>
<div id="inventory-list" class="grid grid-cols-1 gap-2"></div>
</div>
<button id="battle-back-btn" class="hidden mx-auto mt-3 glass-badge px-4 py-2 rounded-xl text-xs text-slate-400 hover:text-white transition-colors border border-white/5">← Back</button>
</div>
</div>
<!-- Dex Screen -->
<div id="screen-dex" class="screen hidden absolute inset-0 flex flex-col z-30 bg-slate-950/95 backdrop-blur-xl">
<div class="glass-bar px-6 py-3 flex items-center justify-between border-b border-white/5">
<h2 class="text-violet-400 font-bold text-lg" style="font-family: 'Press Start 2P', cursive; font-size:14px;">Resonants Dex</h2>
<button id="btn-dex-close" class="glass-icon-btn p-2 rounded-lg hover:bg-white/5 transition-colors">
<svg class="w-5 h-5 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2:="18"></line></svg>
</button>
</div>
<div id="dex-grid" class="flex-1 overflow-y-auto p-4 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3"></div>
</div>
<!-- Shop Screen -->
<div id="screen-shop" class="screen hidden absolute inset-0 flex flex-col z-30 bg-slate-950/95 backdrop-blur-xl">
<div class="glass-bar px-6 py-3 flex items-center justify-between border-b border-white/5">
<h2 class="text-amber-400 font-bold text-lg" style="font-family: 'Press Start 2P', cursive; font-size:14px;">Camp Shop</h2>
<div class="flex items-center gap-3">
<span id="shop-shards" class="text-yellow-400 text-sm font-bold"></span>
<button id="btn-shop-close" class="glass-icon-btn p-2 rounded-lg hover:bg-white/5 transition-colors">
<svg class="w-5 h-5 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2:="18"></line><line x1="6" y1="6" x2="18" y2:="18"></line></svg>
</button>
</div>
</div>
<div id="shop-grid" class="flex-1 overflow-y-auto p-4 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 max-w-3xl mx-auto w-full"></div>
</div>
<!-- Encounter Overlay -->
<div id="encounter-overlay" class="hidden absolute inset-0 z-40 bg-black/70 backdrop-blur-sm flex items-center justify-center">
<div class="glass-card p-8 rounded-3xl border border-white/10 text-center max-w-sm w-full mx-4 animate-bounce-in">
<p id="encounter-text" class="text-slate-200 text-lg mb-6"></p>
<button id="encounter-btn" class="px-8 py-3 rounded-xl bg-gradient-to-r from-violet-600 to-indigo-600 hover:from-violet-500 hover:to-indigo-500 text-white font-semibold transition-all shadow-lg shadow-violet-500/25">Continue</button>
</div>
</div>
<!-- Level Up Overlay -->
<div id="levelup-overlay" class="hidden absolute inset-0 z-40 bg-black/70 backdrop-blur-sm flex items-center justify-center">
<div class="glass-card p-8 rounded-3xl border border-violet-500/30 text-center max-w-sm w-full mx-4">
<h3 class="text-violet-400 font-bold text-xl mb-2" style="font-family: 'Press Start 2P', cursive; font-size:16px;">LEVEL UP!</h3>
<p id="levelup-text" class="text-slate-200 mb-4"></p>
<button id="levelup-btn" class="px-8 py-3 rounded-xl bg-gradient-to-r from-violet-600 to-indigo-600 hover:from-violet-500 hover:to-indigo-500 text-white font-semibold transition-all shadow-lg">Continue</button>
</div>
</div>
<!-- Evolution Overlay -->
<div id="evolution-overlay" class="hidden absolute inset-0 z-40 bg-black/80 backdrop-blur-sm flex items-center justify-center">
<div class="text-center max-w-sm w-full mx-4">
<h3 class="text-yellow-400 font-bold text-xl mb-4" style="font-family: 'Press Start 2P', cursive; font-size:16px;">EVOLVING!</h3>
<div id="evolution-sprite-container" class="w-48 h-48 mx-auto mb-4"></div>
<p id="evolution-text" class="text-slate-200 text-lg mb-6"></p>
<button id="evolution-btn" class="px-8 py-3 rounded-xl bg-gradient-to-r from-yellow-600 to-orange-600 hover:from-yellow-500 hover:to-orange-500 text-white font-semibold transition-all shadow-lg">Continue</button>
</div>
</div>
<!-- Capture Animation Overlay -->
<div id="capture-overlay" class="hidden absolute inset-0 z-40 bg-black/70 backdrop-blur-sm flex items-center justify-center">
<div class="text-center max-w-sm w-full mx-4">
<div id="capture-prism" class="w-32 h-32 mx-auto mb-6"></div>
<p id="capture-text" class="text-slate-200 text-lg"></p>
</div>
</div>
<!-- Dialog Overlay -->
<div id="dialog-overlay" class="hidden absolute inset-x-0 bottom-0 z-30 p-4">
<div class="glass-card p-5 rounded-2xl border border-white/10 max-w-2xl mx-auto">
<div class="flex items-start gap-3">
<div id="dialog-avatar" class="w-12 h-12 rounded-full bg-slate-700 flex-shrink-0 flex items-center justify-center text-xl"></div>
<div class="flex-1">
<p id="dialog-speaker" class="text-cyan-400 font-bold text-sm mb-1"></p>
<p id="dialog-text" class="text-slate-200 text-sm leading-relaxed"></p>
</div>
</div>
<button id="dialog-next" class="mt-3 ml-auto block text-slate-500 hover:text-white transition-colors text-xs">▼ Continue</button>
</div>
</div>
<!-- Notification Toast -->
<div id="toast-container" class="fixed top-4 right-4 z-50 space-y-2"></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./resonants":"src/resonants.ts","./types":"src/types.ts","./sounds":"src/sounds.ts","./state":"src/state.ts","./combat":"src/combat.ts","./map":"src/map.ts","./ui":"src/ui.ts"},"src/resonants.ts":{"./types":"src/types.ts"},"src/state.ts":{"./types":"src/types.ts","./resonants":"src/resonants.ts"},"src/map.ts":{"./types":"src/types.ts","./resonants":"src/resonants.ts"},"src/combat.ts":{"./types":"src/types.ts","./resonants":"src/resonants.ts"},"src/ui.ts":{"./types":"src/types.ts","./resonants":"src/resonants.ts","./map":"src/map.ts","./combat":"src/combat.ts"}};
function __require(id) {
if (__cache[id]) return __cache[id].exports;
var module = __cache[id] = { exports: {} };
var factory = __mods[id];
if (!factory) throw new Error("Module not found: " + id);
factory(module.exports, function (spec) {
var target = (__map[id] && __map[id][spec]) || spec;
return __require(target);
}, module);
return module.exports;
}
// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const resonants_1 = require("./resonants");
const types_1 = require("./types");
const sounds_1 = require("./sounds");
const state_1 = require("./state");
const combat_1 = require("./combat");
const map_1 = require("./map");
const ui = __importStar(require("./ui"));
// === GAME STATE ===
let gameState;
let currentBattle = null;
let isProcessing = false;
let pendingCallback = null;
function init() {
sounds_1.synth.init();
// Check for save
const saved = (0, state_1.getSaveData)();
if (saved) {
gameState = saved;
ui.initMenuScreen(true);
}
else {
gameState = (0, state_1.createInitialState)();
ui.initMenuScreen(false);
}
// Wire up menu buttons
document.getElementById('btn-new-game').addEventListener('click', () => {
sounds_1.synth.playSelect();
startNewGame();
});
document.getElementById('btn-load-game').addEventListener('click', () => {
sounds_1.synth.playSelect();
if (saved) {
gameState = saved;
enterOverworld();
}
});
document.getElementById('btn-reset').addEventListener('click', () => {
if (confirm('Reset all progress? This cannot be undone.')) {
(0, state_1.resetSave)();
location.reload();
}
});
// Dex button
document.getElementById('btn-dex').addEventListener('click', () => {
sounds_1.synth.playSelect();
renderDexScreen();
ui.showScreen('screen-dex');
});
// Shop button
document.getElementById('btn-shop').addEventListener('click', () => {
sounds_1.synth.playSelect();
renderShopScreen();
ui.showScreen('screen-shop');
});
// Dex close
document.getElementById('btn-dex-close').addEventListener('click', () => {
sounds_1.synth.playSelect();
ui.showScreen('screen-overworld');
});
// Shop close
document.getElementById('btn-shop-close').addEventListener('click', () => {
sounds_1.synth.playSelect();
ui.showScreen('screen-overworld');
});
// Zone navigation
document.querySelectorAll('.zone-btn').forEach(btn => {
btn.addEventListener('click', () => {
const zoneIdx = parseInt(btn.dataset.zone || '0');
if (gameState.unlockedZones[zoneIdx]) {
sounds_1.synth.playSelect();
gameState.currentZone = zoneIdx;
// Find first non-cleared node
const zone = map_1.ZONES[zoneIdx];
const nextNode = zone.nodes.findIndex(n => !n.cleared);
gameState.currentNode = nextNode >= 0 ? nextNode : 0;
ui.renderOverworld(gameState);
(0, state_1.saveGame)(gameState);
}
});
});
// Battle action buttons
document.querySelectorAll('.action-btn').forEach(btn => {
btn.addEventListener('click', () => handleBattleAction(btn.dataset.action || ''));
});
// Prism sub-menu
document.querySelectorAll('.prism-btn').forEach(btn => {
btn.addEventListener('click', () => {
const type = btn.dataset.prismType;
if (type === 'return') {
hideSubMenus();
}
else if (type) {
usePrism(type);
}
});
});
// Battle back button
document.getElementById('battle-back-btn').addEventListener('click', () => {
hideSubMenus();
});
// Map click handler
ui.initMap();
ui.setupMapClickHandler(handleNodeClick);
// Window resize
window.addEventListener('resize', () => {
ui.initMap();
if (!document.getElementById('screen-overworld')?.classList.contains('hidden')) {
ui.renderOverworld(gameState);
}
});
// Show starter selection if no party
if (gameState.party.length === 0) {
showStarterSelection();
}
else {
enterOverworld();
}
}
function startNewGame() {
gameState = (0, state_1.createInitialState)();
showStarterSelection();
}
function showStarterSelection() {
const starters = [
{ id: 'emberkit', name: 'Emberkit', element: 'pyro', desc: 'A fiery spirit with a restless heart.' },
{ id: 'droplet', name: 'Droplet', element: 'hydro', desc: 'A fluid essence of pure water resonance.' },
{ id: 'seedling', name: 'Seedling', element: 'flora', desc: 'A nascent nature spirit full of potential.' }
];
const overlay = document.getElementById('encounter-overlay');
if (!overlay)
return;
document.getElementById('encounter-text').innerHTML = `
<div class="space-y-4 text-left">
<p class="text-center text-slate-300 mb-4">Choose your first Resonant companion:</p>
${starters.map((s, i) => `
<button data-starter="${i}" class="starter-btn w-full p-4 rounded-xl glass-card border border-white/10 hover:border-violet-500/40 transition-all text-left group">
<div class="flex items-center gap-3">
<span class="text-2xl">${s.element === 'pyro' ? '🔥' : s.element === 'hydro' ? '💧' : '🌱'}</span>
<div>
<h3 class="type-${s.element} font-bold">${s.name}</h3>
<p class="text-slate-400 text-xs">${s.desc}</p>
</div>
</div>
</button>
`).join('')}
</div>`;
overlay.classList.remove('hidden');
// Wire starter buttons
document.querySelectorAll('.starter-btn').forEach(btn => {
btn.addEventListener('click', () => {
const idx = parseInt(btn.dataset.starter || '0');
const starter = starters[idx];
const resonant = (0, resonants_1.createResonantInstance)(starter.id, 1);
gameState.party.push(resonant);
gameState.dexUnlocked.push(starter.id);
sounds_1.synth.playSelect();
overlay.classList.add('hidden');
enterOverworld();
});
});
}
function enterOverworld() {
ui.showScreen('screen-overworld');
// Small delay to let layout settle, then init map with proper dimensions
requestAnimationFrame(() => {
ui.resizeMap();
ui.renderOverworld(gameState);
});
(0, state_1.saveGame)(gameState);
}
// === NODE INTERACTION ===
async function handleNodeClick(zoneIdx, nodeIdx) {
if (isProcessing)
return;
const zone = map_1.ZONES[zoneIdx];
if (!zone || !zone.nodes[nodeIdx])
return;
const node = zone.nodes[nodeIdx];
// Must be adjacent or first node
const playerNode = gameState.currentNode;
if (nodeIdx !== playerNode && !(nodeIdx === playerNode + 1) && nodeIdx !== 0) {
ui.showToast('Move to an adjacent node first!', 'info');
return;
}
isProcessing = true;
switch (node.type) {
case 'wild':
await handleWildEncounter(zoneIdx, node);
break;
case 'mystery':
await handleMysteryCache(node);
break;
case 'npc':
await handleNPCDialog(node);
break;
case 'healing':
await handleHealingSpring();
break;
case 'boss':
await handleBossBattle(zoneIdx, node);
break;
}
// Move to this node
gameState.currentNode = nodeIdx;
if (node.type !== 'boss') {
node.cleared = true;
}
// Check if zone is complete
checkZoneProgress(zoneIdx);
ui.renderOverworld(gameState);
(0, state_1.saveGame)(gameState);
isProcessing = false;
}
async function handleWildEncounter(zoneIdx, node) {
const encounter = (0, map_1.getNodeEncounter)(node.encounterId);
if (!encounter)
return;
const template = (0, resonants_1.getResonantTemplate)(encounter.resonantId);
if (!template)
return;
ui.showEncounter(`A wild ${template.name} appeared!`, async () => {
await startWildBattle(encounter.resonantId, encounter.level);
});
}
async function startWildBattle(resonantId, level) {
const ctx = {
playerParty: gameState.party,
playerBox: gameState.box,
inventory: [...gameState.inventory],
shards: gameState.shards
};
(0, combat_1.setCombatContext)(ctx);
currentBattle = (0, combat_1.startBattle)(resonantId, level, true);
currentBattle.message = `A wild ${(0, resonants_1.getResonantTemplate)(resonantId).name} appeared!`;
ui.showScreen('screen-battle');
requestAnimationFrame(() => {
ui.resizeBattle();
if (currentBattle)
ui.renderBattle(currentBattle);
});
// Update player info in battle
updateBattlePlayerInfo();
sounds_1.synth.playBoss();
}
function updateBattlePlayerInfo() {
if (!currentBattle)
return;
const playerActive = (0, state_1.getPlayerResonant)(gameState);
if (!playerActive)
return;
const template = (0, resonants_1.getResonantTemplate)(playerActive.templateId);
if (template) {
document.getElementById('player-resonant-name').textContent = template.name;
document.getElementById('player-resonant-level').textContent = `Lv.${playerActive.level}`;
const maxHp = (0, combat_1.getMaxHp)(playerActive);
const hpPercent = (playerActive.currentHp / maxHp) * 100;
const hpFill = document.getElementById('player-hp-fill');
hpFill.style.width = `${hpPercent}%`;
hpFill.className = `hp-fill h-full rounded-full transition-all duration-500 ${hpPercent > 50 ? '' : hpPercent > 20 ? 'mid' : 'low'}`;
document.getElementById('player-hp-text').textContent = `${playerActive.currentHp}/${maxHp}`;
document.getElementById('player-hp-max').textContent = `HP`;
const xpNeeded = (0, types_1.calcXpToLevel)(playerActive.level + 1);
const xpPercent = (playerActive.xp / xpNeeded) * 100;
document.getElementById('player-xp-fill').style.width = `${xpPercent}%`;
document.getElementById('player-xp-text').textContent = `${playerActive.xp}/${xpNeeded}`;
if (playerActive.status) {
document.getElementById('player-status').textContent = `Status: ${playerActive.status.toUpperCase()}`;
document.getElementById('player-status').className = 'mt-1.5 text-xs font-bold';
}
else {
document.getElementById('player-status').textContent = '';
}
}
}
async function handleBattleResult(result) {
if (!currentBattle)
return;
const battleState = (0, combat_1.getBattleState)();
(0, combat_1.clearCombatContext)();
if (result === 'win') {
// Calculate rewards
const xpReward = Math.floor(battleState.enemy.level * 12 + Math.random() * 10);
const shardReward = Math.floor(20 + Math.random() * 30);
gameState.shards += shardReward;
// Give XP to active resonant
const playerActive = (0, state_1.getPlayerResonant)(gameState);
if (playerActive) {
playerActive.xp += xpReward;
// Check for level up
while (playerActive.xp >= (0, types_1.calcXpToLevel)(playerActive.level + 1) && playerActive.level < 25) {
playerActive.xp -= (0, types_1.calcXpToLevel)(playerActive.level + 1);
playerActive.level++;
// Recalculate HP
const template = (0, resonants_1.getResonantTemplate)(playerActive.templateId);
if (template) {
const newMaxHp = (0, types_1.calcStats)(template, playerActive.level).hp;
const oldMaxHp = (0, combat_1.getMaxHp)(playerActive);
const hpRatio = playerActive.currentHp / oldMaxHp;
playerActive.currentHp = Math.floor(newMaxHp * hpRatio);
}
// Add new moves
if (template) {
const newMoves = template.moves.filter(m => m.unlockLevel === playerActive.level);
for (const move of newMoves) {
if (!playerActive.movesKnown.includes(move.name)) {
playerActive.movesKnown.push(move.name);
}
}
}
// Check evolution
const tmpl = (0, resonants_1.getResonantTemplate)(playerActive.templateId);
if (tmpl?.evolveLevel && playerActive.level >= tmpl.evolveLevel) {
await handleEvolution(playerActive);
}
}
// Add to dex
if (!gameState.dexUnlocked.includes(playerActive.templateId)) {
gameState.dexUnlocked.push(playerActive.templateId);
}
}
ui.showToast(`+${xpReward} XP, +${shardReward} Shards!`, 'success');
// Show result message then return to overworld
battleState.message = `Won! Gained ${xpReward} XP and ${shardReward} Shards.`;
ui.renderBattle(battleState);
setTimeout(() => {
ui.showScreen('screen-overworld');
ui.renderOverworld(gameState);
(0, state_1.saveGame)(gameState);
}, 2000);
}
else if (result === 'capture') {
// Add to box or party
const captured = (0, combat_1.getBattleState)().enemy;
captured.templateId = captured.templateId;
if (gameState.party.filter(r => r.currentHp > 0).length < 6) {
gameState.party.push(captured);
}
else {
gameState.box.push(captured);
}
gameState.dexUnlocked.push(captured.templateId);
ui.showToast(`Caught ${(0, resonants_1.getResonantTemplate)(captured.templateId)?.name}!`, 'success');
setTimeout(() => {
ui.showScreen('screen-overworld');
ui.renderOverworld(gameState);
(0, state_1.saveGame)(gameState);
}, 1500);
}
else {
// Fainted - check if all party fainted
const alive = (0, state_1.getPartyAliveCount)(gameState);
if (alive === 0) {
ui.showToast('All Resonants fainted! Returning to last healing spring...', 'error');
// Heal all at first healing node
gameState.party.forEach(r => {
const template = (0, resonants_1.getResonantTemplate)(r.templateId);
if (template) {
r.currentHp = (0, types_1.calcStats)(template, r.level).hp;
}
});
setTimeout(() => {
ui.showScreen('screen-overworld');
ui.renderOverworld(gameState);
(0, state_1.saveGame)(gameState);
}, 2000);
}
else {
// Auto-switch to next alive resonant
const nextAlive = gameState.party.find(r => r.currentHp > 0 && r.templateId !== (0, state_1.getPlayerResonant)(gameState)?.templateId);
if (nextAlive) {
ui.showToast(`Send out ${(0, resonants_1.getResonantTemplate)(nextAlive.templateId)?.name}!`, 'info');
// In a full implementation, we'd switch the active resonant here
}
setTimeout(() => {
ui.showScreen('screen-overworld');
ui.renderOverworld(gameState);
(0, state_1.saveGame)(gameState);
}, 1500);
}
}
currentBattle = null;
}
async function handleEvolution(resonant) {
const template = (0, resonants_1.getResonantTemplate)(resonant.templateId);
if (!template?.evolveLevel)
return;
// Find evolved form
const evolutionTarget = resonants_1.RESONANT_DB.find(r => r.evolutionFrom === resonant.templateId);
if (!evolutionTarget)
return;
ui.flashScreen('#fff', 500);
sounds_1.synth.playEvolution();
await ui.showEvolution(`<div class="evolution-flash">${ui.generateMiniSprite(evolutionTarget, evolutionTarget.baseHp > 80 ? 20 : 10)}</div>`, `${template.name} is evolving into ${evolutionTarget.name}!`);
// Transform the resonant
const oldLevel = resonant.level;
resonant.templateId = evolutionTarget.id;
resonant.movesKnown = evolutionTarget.moves.filter(m => m.unlockLevel <= oldLevel).map(m => m.name);
const newStats = (0, types_1.calcStats)(evolutionTarget, oldLevel);
resonant.currentHp = newStats.hp;
}
// === BATTLE ACTIONS ===
function handleBattleAction(action) {
if (isProcessing || !currentBattle)
return;
switch (action) {
case 'attack':
showMoveSelect();
break;
case 'prism':
showPrismMenu();
break;
case 'exchange':
showExchangeMenu();
break;
case 'inventory':
showInventoryMenu();
break;
}
}
function showMoveSelect() {
hideSubMenus();
const playerActive = (0, state_1.getPlayerResonant)(gameState);
if (!playerActive)
return;
const template = (0, resonants_1.getResonantTemplate)(playerActive.templateId);
if (!template)
return;
const container = document.getElementById('action-buttons');
container.innerHTML = '';
// Show up to 4 moves
const moves = playerActive.movesKnown.slice(0, 4);
while (moves.length < 4) {
moves.push('Tackle'); // Filler
}
moves.forEach(moveName => {
const moveDef = template.moves.find(m => m.name === moveName) ||
{ name: moveName, power: 40, accuracy: 100, element: 'cosmos' };
const btn = document.createElement('button');
btn.className = 'action-btn glass-card p-3 rounded-xl text-center hover:bg-white/5 transition-all border border-white/10 group';
btn.innerHTML = `
<span class="text-white font-bold text-sm block">${moveName}</span>
<span class="text-slate-400 text-[10px]">Pwr: ${moveDef.power} | Acc: ${moveDef.accuracy}%</span>
<span class="type-${moveDef.element} text-[10px] block">${moveDef.element}</span>
`;
btn.addEventListener('click', () => {
sounds_1.synth.playSelect();
(0, combat_1.executePlayerMove)(moveName);
const bs = (0, combat_1.getBattleState)();
if (bs)
ui.renderBattle(bs);
updateBattlePlayerInfo();
// Check battle end
setTimeout(() => checkBattleEnd(), 1000);
});
container.appendChild(btn);
});
// Add back button
const backBtn = document.createElement('button');
backBtn.id = 'battle-back-btn';
backBtn.className = 'mx-auto mt-3 glass-badge px-4 py-2 rounded-xl text-xs text-slate-400 hover:text-white transition-colors border border-white/5';
backBtn.textContent = '← Back';
backBtn.addEventListener('click', () => { hideSubMenus(); });
container.appendChild(backBtn);
document.getElementById('battle-back-btn')?.remove();
}
function showPrismMenu() {
hideSubMenus();
document.getElementById('action-buttons').classList.add('hidden');
document.getElementById('submenu-prism').classList.remove('hidden');
document.getElementById('battle-back-btn')?.classList.remove('hidden');
}
async function usePrism(type) {
hideSubMenus();
const prismType = type === 'basic' ? 'prism_basic' : 'prism_hyper';
if (!(0, state_1.removeInventoryItem)(gameState, prismType, 1)) {
ui.showToast('No prisms remaining!', 'error');
showBattleActions();
return;
}
sounds_1.synth.playSelect();
const success = (0, combat_1.attemptCapture)(type === 'basic' ? 'basic' : 'hyper');
if (success) {
await ui.showCaptureAnimation(true);
handleBattleResult('capture');
}
else {
await ui.showCaptureAnimation(false);
sounds_1.synth.playCaptureFail();
// Continue battle
const bs = (0, combat_1.getBattleState)();
if (bs) {
bs.message += ' The Resonant broke free!';
ui.renderBattle(bs);
setTimeout(() => checkEnemyTurn(), 1000);
}
}
(0, state_1.saveGame)(gameState);
}
function showExchangeMenu() {
hideSubMenus();
document.getElementById('action-buttons').classList.add('hidden');
document.getElementById('submenu-exchange').classList.remove('hidden');
document.getElementById('battle-back-btn')?.classList.remove('hidden');
const container = document.getElementById('exchange-list');
container.innerHTML = '';
gameState.party.forEach((resonant, idx) => {
if (resonant.currentHp <= 0 || resonant === (0, state_1.getPlayerResonant)(gameState))
return;
const template = (0, resonants_1.getResonantTemplate)(resonant.templateId);
if (!template)
return;
const btn = document.createElement('button');
btn.className = 'exchange-btn glass-card p-3 rounded-xl text-left hover:bg-white/5 transition-all border border-white/10 flex items-center gap-3';
btn.innerHTML = `
<span class="text-lg">${template.element === 'pyro' ? '🔥' : template.element === 'hydro' ? '💧' : template.element === 'flora' ? '🌱' : template.element === 'volt' ? '⚡' : template.element === 'zephyr' ? '🌀' : '🪨'}</span>
<div class="flex-1">
<span class="text-white font-bold text-sm">${template.name}</span>
<span class="text-slate-400 text-xs ml-2">Lv.${resonant.level}</span>
</div>
`;
btn.addEventListener('click', () => {
sounds_1.synth.playSelect();
// Swap active resonant (simplified - in full game would manage party order)
ui.showToast(`${template.name} is now leading!`, 'info');
hideSubMenus();
showBattleActions();
});
container.appendChild(btn);
});
}
function showInventoryMenu() {
hideSubMenus();
document.getElementById('action-buttons').classList.add('hidden');
document.getElementById('submenu-inventory').classList.remove('hidden');
document.getElementById('battle-back-btn')?.classList.remove('hidden');
const container = document.getElementById('inventory-list');
container.innerHTML = '';
const usableItems = gameState.inventory.filter(i => i.count > 0);
if (usableItems.length === 0) {
container.innerHTML = '<p class="text-slate-400 text-sm text-center py-4">No items available.</p>';
return;
}
usableItems.forEach(item => {
const shopItem = types_1.SHOP_ITEMS.find(s => s.type === item.type);
if (!shopItem)
return;
const btn = document.createElement('button');
btn.className = 'inventory-btn glass-card p-3 rounded-xl text-left hover:bg-white/5 transition-all border border-white/10 flex items-center gap-3';
btn.innerHTML = `
<span class="text-lg">${shopItem.icon}</span>
<div class="flex-1">
<span class="text-white font-bold text-sm">${shopItem.name}</span>
<span class="text-slate-400 text-xs ml-2">x${item.count}</span>
</div>
`;
btn.addEventListener('click', () => {
sounds_1.synth.playSelect();
useBattleItem(item.type);
});
container.appendChild(btn);
});
}
function useBattleItem(type) {
if (!(0, state_1.removeInventoryItem)(gameState, type, 1))
return;
const playerActive = (0, state_1.getPlayerResonant)(gameState);
if (!playerActive)
return;
switch (type) {
case 'vital_elixir': {
const template = (0, resonants_1.getResonantTemplate)(playerActive.templateId);
if (template) {
const maxHp = (0, types_1.calcStats)(template, playerActive.level).hp;
const healAmount = Math.floor(maxHp * 0.5);
playerActive.currentHp = Math.min(maxHp, playerActive.currentHp + healAmount);
ui.showToast(`Healed ${playerActive.templateId} for ${healAmount} HP!`, 'success');
sounds_1.synth.playHeal();
}
break;
}
case 'status_cleanser': {
if (playerActive.status) {
playerActive.status = null;
ui.showToast(`Cleared status condition!`, 'success');
sounds_1.synth.playHeal();
}
else {
ui.showToast('No status to clear.', 'info');
}
break;
}
case 'prism_basic':
case 'prism_hyper':
ui.showToast('Prisms can\'t be used in battle this way.', 'info');
(0, state_1.addInventoryItem)(gameState, type, 1); // Put it back
break;
default:
break;
}
hideSubMenus();
showBattleActions();
updateBattlePlayerInfo();
(0, state_1.saveGame)(gameState);
}
function showBattleActions() {
hideSubMenus();
document.getElementById('action-buttons').classList.remove('hidden');
document.getElementById('battle-back-btn')?.classList.add('hidden');
}
function hideSubMenus() {
document.getElementById('submenu-prism')?.classList.add('hidden');
document.getElementById('submenu-exchange')?.classList.add('hidden');
document.getElementById('submenu-inventory')?.classList.add('hidden');
document.getElementById('action-buttons')?.classList.remove('hidden');
}
function checkBattleEnd() {
const bs = (0, combat_1.getBattleState)();
if (!bs)
return;
// Check if enemy fainted
if (bs.enemy.currentHp <= 0) {
handleBattleResult('win');
return;
}
// Check if player has no alive resonants
const alive = (0, state_1.getPartyAliveCount)(gameState);
if (alive === 0) {
handleBattleResult('fled');
return;
}
// Enemy turn
checkEnemyTurn();
}
function checkEnemyTurn() {
(0, combat_1.executeEnemyTurn)();
const bs = (0, combat_1.getBattleState)();
if (bs)
ui.renderBattle(bs);
updateBattlePlayerInfo();
setTimeout(() => checkBattleEnd(), 1000);
}
// === MYSTERY & NPC HANDLERS ===
async function handleMysteryCache(node) {
const reward = (0, map_1.getMysteryCacheReward)(gameState.currentZone);
gameState.shards += reward.shards;
if (reward.item) {
(0, state_1.addInventoryItem)(gameState, reward.item, 1);
ui.showToast(`Found ${reward.shards} Shards and a ${reward.item}!`, 'success');
}
else {
ui.showToast(`Found ${reward.shards} Aether Shards!`, 'success');
}
sounds_1.synth.playLevelUp();
(0, state_1.saveGame)(gameState);
}
async function handleNPCDialog(node) {
if (node.dialogLines) {
for (const line of node.dialogLines) {
await ui.showDialog(line.speaker, line.text, line.avatar || '💬');
}
}
// Give small shard reward for NPC interaction
gameState.shards += 15;
sounds_1.synth.playSelect();
(0, state_1.saveGame)(gameState);
}
async function handleHealingSpring() {
ui.showEncounter('A serene healing spring glows with soft light.', async () => {
let healed = 0;
gameState.party.forEach(r => {
const template = (0, resonants_1.getResonantTemplate)(r.templateId);
if (template) {
const maxHp = (0, types_1.calcStats)(template, r.level).hp;
if (r.currentHp < maxHp) {
r.currentHp = maxHp;
healed++;
}
}
});
ui.showToast(`All Resonants fully restored!`, 'success');
sounds_1.synth.playHeal();
});
}
async function handleBossBattle(zoneIdx, node) {
const boss = (0, map_1.getBossData)(zoneIdx);
if (!boss)
return;
// Show boss dialog first
for (const line of boss.dialog) {
await ui.showDialog(line.speaker, line.text, line.avatar || '💬');
}
sounds_1.synth.playBoss();
// Start boss battle
await startWildBattle(boss.resonantId, boss.level);
}
function checkZoneProgress(zoneIdx) {
const zone = map_1.ZONES[zoneIdx];
const allCleared = zone.nodes.every(n => n.cleared || n.type === 'boss');
if (allCleared && zoneIdx < 3) {
gameState.unlockedZones[zoneIdx + 1] = true;
gameState.defeatedBosses.push(zoneIdx);
ui.showToast(`Zone unlocked: ${map_1.ZONES[zoneIdx + 1].name}!`, 'success');
sounds_1.synth.playLevelUp();
// Award bonus shards
const bonusShards = [100, 200, 400][zoneIdx];
gameState.shards += bonusShards;
ui.showToast(`+${bonusShards} bonus Shards!`, 'success');
}
// Check game completion
if (gameState.unlockedZones[3] && map_1.ZONES[3].nodes.every(n => n.cleared)) {
setTimeout(() => {
ui.showEncounter('🏆 Congratulations, Master Conductor! You have conquered all of Aethelgard and proven yourself the greatest Resonant handler in history!', () => { });
}, 2000);
}
}
// === DEX SCREEN ===
function renderDexScreen() {
ui.renderDex(gameState.dexUnlocked);
}
// === SHOP SCREEN ===
function renderShopScreen() {
ui.renderShop(gameState.shards);
// Wire up shop buttons
document.querySelectorAll('[data-shop-item]').forEach(btn => {
btn.addEventListener('click', () => {
const itemId = btn.dataset.shopItem;
if (!itemId)
return;
const shopItem = types_1.SHOP_ITEMS.find(s => s.id === itemId);
if (!shopItem)
return;
if (gameState.shards >= shopItem.cost) {
gameState.shards -= shopItem.cost;
(0, state_1.addInventoryItem)(gameState, shopItem.type, 1);
ui.showToast(`Purchased ${shopItem.name}!`, 'success');
sounds_1.synth.playSelect();
renderShopScreen(); // Re-render to update affordability
(0, state_1.saveGame)(gameState);
}
else {
ui.showToast('Not enough Shards!', 'error');
sounds_1.synth.playCaptureFail();
}
});
});
}
// === GAME LOOP (animation frame for animated elements) ===
let lastFrame = 0;
function gameLoop(timestamp) {
const delta = timestamp - lastFrame;
// Redraw map if in overworld (for particle animation)
if (!document.getElementById('screen-overworld')?.classList.contains('hidden')) {
ui.drawMap(gameState.currentZone, gameState.currentNode);
}
lastFrame = timestamp;
requestAnimationFrame(gameLoop);
}
// === INITIALIZATION ===
init();
requestAnimationFrame(gameLoop);
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SHOP_ITEMS = exports.TYPE_CHART = void 0;
exports.getTypeModifier = getTypeModifier;
exports.calcXpToLevel = calcXpToLevel;
exports.calcStats = calcStats;
exports.calcDamage = calcDamage;
exports.isCritical = isCritical;
exports.TYPE_CHART = {
pyro: { strong: ['flora'], weak: ['hydro'] },
hydro: { strong: ['pyro'], weak: ['flora'] },
flora: { strong: ['hydro'], weak: ['pyro'] },
volt: { strong: ['earth'], weak: ['zephyr'] },
zephyr: { strong: ['volt'], weak: ['earth'] },
earth: { strong: ['volt'], weak: ['zephyr'] },
cosmos: { strong: [], weak: [] }
};
function getTypeModifier(attackerType, defenderType) {
const chart = exports.TYPE_CHART[attackerType];
if (chart.strong.includes(defenderType))
return 1.5;
if (chart.weak.includes(defenderType))
return 0.75;
return 1.0;
}
exports.SHOP_ITEMS = [
{ id: 'prism_basic', name: 'Aether Prism', description: 'Basic catch tool. Reliable capture rate.', cost: 100, type: 'prism_basic', icon: '◇' },
{ id: 'prism_hyper', name: 'Hyper Prism', description: 'Enhanced capture matrix. 2.5× catch rate.', cost: 300, type: 'prism_hyper', icon: '◆' },
{ id: 'vital_elixir', name: 'Core Restorer', description: 'Restores 50% HP to one Resonant.', cost: 150, type: 'vital_elixir', icon: '♥' },
{ id: 'status_cleanser', name: 'Resonance Elixir', description: 'Heals all status conditions.', cost: 200, type: 'status_cleanser', icon: '✦' },
{ id: 'prism_booster', name: 'Prism Booster', description: 'Next prism throw gets 1.5× rate.', cost: 250, type: 'prism_booster', icon: '⬡' }
];
function calcXpToLevel(level) {
return Math.floor(4 * level * level + 6 * level);
}
function calcStats(template, level) {
const hp = Math.floor(((2 * template.baseHp + 10) * level) / 100) + level + 10;
const atk = Math.floor(((2 * template.baseAttack + 5) * level) / 100) + 5;
const def = Math.floor(((2 * template.baseDefense + 5) * level) / 100) + 5;
const spd = Math.floor(((2 * template.baseSpeed + 5) * level) / 100) + 5;
return { hp, attack: atk, defense: def, speed: spd };
}
function calcDamage(attackerLevel, movePower, attackerAtk, defenderDef, typeMod) {
const base = (((2 * attackerLevel / 5) + 2) * movePower * (attackerAtk / defenderDef)) / 50 + 2;
const random = 0.85 + Math.random() * 0.15;
return Math.max(1, Math.floor(base * typeMod * random));
}
function isCritical() {
return Math.random() < 0.1;
}
};
// ── module: src/resonants.ts ──
__mods["src/resonants.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RESONANT_DB = void 0;
exports.getResonantTemplate = getResonantTemplate;
exports.createResonantInstance = createResonantInstance;
const pyroMoves = [
{ name: 'Ember', power: 40, accuracy: 100, element: 'pyro', unlockLevel: 1, description: 'A small flame projectile.' },
{ name: 'Scratch', power: 50, accuracy: 100, element: 'cosmos', unlockLevel: 1, description: 'Raking claws attack.' },
{ name: 'Flame Burst', power: 70, accuracy: 95, element: 'pyro', unlockLevel: 5, description: 'Explosive fireball.' },
{ name: 'Fire Spin', power: 80, accuracy: 90, element: 'pyro', unlockLevel: 12, description: 'Trapping whirlwind of flame.' },
{ name: 'Inferno Rush', power: 110, accuracy: 85, element: 'pyro', unlockLevel: 20, description: 'Devastating charging blaze.' }
];
const hydroMoves = [
{ name: 'Aqua Jet', power: 40, accuracy: 100, element: 'hydro', unlockLevel: 1, description: 'Swift water shard strike.' },
{ name: 'Tackle', power: 50, accuracy: 100, element: 'cosmos', unlockLevel: 1, description: 'Body slam attack.' },
{ name: 'Water Pulse', power: 70, accuracy: 95, element: 'hydro', unlockLevel: 5, description: 'Rippling water wave.' },
{ name: 'Tidal Surge', power: 85, accuracy: 90, element: 'hydro', unlockLevel: 12, description: 'Massive wall of water.' },
{ name: 'Abyssal Tsunami', power: 120, accuracy: 80, element: 'hydro', unlockLevel: 20, description: 'Cataclysmic ocean wave.' }
];
const floraMoves = [
{ name: 'Vine Whip', power: 45, accuracy: 100, element: 'flora', unlockLevel: 1, description: 'Striking vine lash.' },
{ name: 'Absorb', power: 50, accuracy: 100, element: 'flora', unlockLevel: 1, description: 'Drains life from foe.' },
{ name: 'Pollen Storm', power: 70, accuracy: 95, element: 'flora', unlockLevel: 5, description: 'Dense cloud of spores.' },
{ name: 'Thorn Barrage', power: 85, accuracy: 90, element: 'flora', unlockLevel: 12, description: 'Relentless thorn volley.' },
{ name: 'World Tree Wrath', power: 120, accuracy: 80, element: 'flora', unlockLevel: 20, description: 'Primordial nature fury.' }
];
const voltMoves = [
{ name: 'Spark', power: 40, accuracy: 100, element: 'volt', unlockLevel: 1, description: 'Tiny electric discharge.' },
{ name: 'Quick Attack', power: 50, accuracy: 100, element: 'cosmos', unlockLevel: 1, description: 'Lightning-fast strike.' },
{ name: 'Thunder Shock', power: 75, accuracy: 95, element: 'volt', unlockLevel: 5, description: 'Jolting electric blast.' },
{ name: 'Chain Lightning', power: 90, accuracy: 88, element: 'volt', unlockLevel: 12, description: 'Splitting bolt of lightning.' },
{ name: 'Supernova Storm', power: 130, accuracy: 75, element: 'volt', unlockLevel: 20, description: 'Cataclysmic electric storm.' }
];
const zephyrMoves = [
{ name: 'Gust', power: 40, accuracy: 100, element: 'zephyr', unlockLevel: 1, description: 'Sharp wind blade.' },
{ name: 'Quick Attack', power: 50, accuracy: 100, element: 'cosmos', unlockLevel: 1, description: 'Swift aerial strike.' },
{ name: 'Air Cutter', power: 70, accuracy: 95, element: 'zephyr', unlockLevel: 5, description: 'Razor-sharp wind slash.' },
{ name: 'Cyclone', power: 85, accuracy: 90, element: 'zephyr', unlockLevel: 12, description: 'Devastating tornado.' },
{ name: 'Skyfall Descent', power: 115, accuracy: 82, element: 'zephyr', unlockLevel: 20, description: 'Divine wind from above.' }
];
const earthMoves = [
{ name: 'Rock Throw', power: 45, accuracy: 100, element: 'earth', unlockLevel: 1, description: 'Hurling sharp stones.' },
{ name: 'Tackle', power: 50, accuracy: 100, element: 'cosmos', unlockLevel: 1, description: 'Body slam attack.' },
{ name: 'Sand Tomb', power: 75, accuracy: 92, element: 'earth', unlockLevel: 5, description: 'Trapping sand vortex.' },
{ name: 'Earthquake', power: 90, accuracy: 88, element: 'earth', unlockLevel: 12, description: 'Shattering ground quake.' },
{ name: 'Mountain Fall', power: 125, accuracy: 78, element: 'earth', unlockLevel: 20, description: 'Colossal rock avalanche.' }
];
exports.RESONANT_DB = [
// === PYRO FAMILY ===
{
id: 'emberkit', name: 'Emberkit', element: 'pyro',
baseHp: 45, baseAttack: 38, baseDefense: 30, baseSpeed: 42,
evolutionFrom: undefined, evolveLevel: 8,
moves: pyroMoves.filter(m => m.unlockLevel <= 1),
spriteParams: { shape: 'circle', colors: ['#f97316', '#ea580c'], details: 'flame-tail' }
},
{
id: 'pyrowolf', name: 'Pyrowolf', element: 'pyro',
baseHp: 65, baseAttack: 62, baseDefense: 48, baseSpeed: 58,
evolutionFrom: 'emberkit', evolveLevel: 15,
moves: pyroMoves.filter(m => m.unlockLevel <= 8),
spriteParams: { shape: 'wolf', colors: ['#f97316', '#dc2626'], details: 'fire-mane' }
},
{
id: 'ignisaur', name: 'Ignisaur', element: 'pyro',
baseHp: 90, baseAttack: 88, baseDefense: 65, baseSpeed: 72,
evolutionFrom: 'pyrowolf',
moves: pyroMoves.filter(m => m.unlockLevel <= 15),
spriteParams: { shape: 'dinosaur', colors: ['#ef4444', '#f97316'], details: 'lava-plates' }
},
// === HYDRO FAMILY ===
{
id: 'droplet', name: 'Droplet', element: 'hydro',
baseHp: 50, baseAttack: 32, baseDefense: 40, baseSpeed: 38,
evolutionFrom: undefined, evolveLevel: 8,
moves: hydroMoves.filter(m => m.unlockLevel <= 1),
spriteParams: { shape: 'drop', colors: ['#06b6d4', '#0891b2'], details: 'water-glow' }
},
{
id: 'geoxus', name: 'Geoxus', element: 'hydro',
baseHp: 72, baseAttack: 55, baseDefense: 60, baseSpeed: 52,
evolutionFrom: 'droplet', evolveLevel: 15,
moves: hydroMoves.filter(m => m.unlockLevel <= 8),
spriteParams: { shape: 'mammal', colors: ['#06b6d4', '#3b82f6'], details: 'fin-crown' }
},
{
id: 'neptunian', name: 'Neptunian', element: 'hydro',
baseHp: 100, baseAttack: 80, baseDefense: 78, baseSpeed: 65,
evolutionFrom: 'geoxus',
moves: hydroMoves.filter(m => m.unlockLevel <= 15),
spriteParams: { shape: 'leviathan', colors: ['#0e7490', '#06b6d4'], details: 'ocean-crown' }
},
// === FLORA FAMILY ===
{
id: 'seedling', name: 'Seedling', element: 'flora',
baseHp: 55, baseAttack: 30, baseDefense: 45, baseSpeed: 35,
evolutionFrom: undefined, evolveLevel: 8,
moves: floraMoves.filter(m => m.unlockLevel <= 1),
spriteParams: { shape: 'plant', colors: ['#22c55e', '#16a34a'], details: 'sprout-leaves' }
},
{
id: 'thornback', name: 'Thornback', element: 'flora',
baseHp: 78, baseAttack: 52, baseDefense: 70, baseSpeed: 45,
evolutionFrom: 'seedling', evolveLevel: 15,
moves: floraMoves.filter(m => m.unlockLevel <= 8),
spriteParams: { shape: 'turtle', colors: ['#16a34a', '#15803d'], details: 'thorn-shell' }
},
{
id: 'bloomgoliath', name: 'Bloomgoliath', element: 'flora',
baseHp: 110, baseAttack: 75, baseDefense: 95, baseSpeed: 40,
evolutionFrom: 'thornback',
moves: floraMoves.filter(m => m.unlockLevel <= 15),
spriteParams: { shape: 'colossus', colors: ['#15803d', '#22c55e'], details: 'flower-crown' }
},
// === VOLT FAMILY ===
{
id: 'sparky', name: 'Sparky', element: 'volt',
baseHp: 42, baseAttack: 45, baseDefense: 28, baseSpeed: 55,
evolutionFrom: undefined, evolveLevel: 8,
moves: voltMoves.filter(m => m.unlockLevel <= 1),
spriteParams: { shape: 'mouse', colors: ['#facc15', '#eab308'], details: 'lightning-tail' }
},
{
id: 'boltsaber', name: 'Boltsaber', element: 'volt',
baseHp: 62, baseAttack: 72, baseDefense: 45, baseSpeed: 70,
evolutionFrom: 'sparky', evolveLevel: 15,
moves: voltMoves.filter(m => m.unlockLevel <= 8),
spriteParams: { shape: 'cat', colors: ['#eab308', '#f59e0b'], details: 'bolt-ears' }
},
{
id: 'stormlord', name: 'Stormlord', element: 'volt',
baseHp: 85, baseAttack: 95, baseDefense: 60, baseSpeed: 88,
evolutionFrom: 'boltsaber',
moves: voltMoves.filter(m => m.unlockLevel <= 15),
spriteParams: { shape: 'dragon', colors: ['#f59e0b', '#ef4444'], details: 'storm-wings' }
},
// === ZEPHYR FAMILY ===
{
id: 'gusty', name: 'Gusty', element: 'zephyr',
baseHp: 40, baseAttack: 35, baseDefense: 25, baseSpeed: 60,
evolutionFrom: undefined, evolveLevel: 8,
moves: zephyrMoves.filter(m => m.unlockLevel <= 1),
spriteParams: { shape: 'spirit', colors: ['#a78bfa', '#8b5cf6'], details: 'wind-trail' }
},
{
id: 'slipstream', name: 'Slipstream', element: 'zephyr',
baseHp: 58, baseAttack: 58, baseDefense: 40, baseSpeed: 78,
evolutionFrom: 'gusty', evolveLevel: 15,
moves: zephyrMoves.filter(m => m.unlockLevel <= 8),
spriteParams: { shape: 'bird', colors: ['#8b5cf6', '#a78bfa'], details: 'wind-wings' }
},
{
id: 'zephyrus', name: 'Zephyrus', element: 'zephyr',
baseHp: 78, baseAttack: 82, baseDefense: 55, baseSpeed: 95,
evolutionFrom: 'slipstream',
moves: zephyrMoves.filter(m => m.unlockLevel <= 15),
spriteParams: { shape: 'angel', colors: ['#c4b5fd', '#a78bfa'], details: 'halo-wind' }
},
// === EARTH FAMILY ===
{
id: 'pebble', name: 'Pebble', element: 'earth',
baseHp: 52, baseAttack: 35, baseDefense: 50, baseSpeed: 30,
evolutionFrom: undefined, evolveLevel: 8,
moves: earthMoves.filter(m => m.unlockLevel <= 1),
spriteParams: { shape: 'rock', colors: ['#a16207', '#854d0e'], details: 'crystal-vein' }
},
{
id: 'obsidian', name: 'Obsidian', element: 'earth',
baseHp: 75, baseAttack: 60, baseDefense: 75, baseSpeed: 42,
evolutionFrom: 'pebble', evolveLevel: 15,
moves: earthMoves.filter(m => m.unlockLevel <= 8),
spriteParams: { shape: 'golem', colors: ['#854d0e', '#713f12'], details: 'lava-cracks' }
},
{
id: 'terrasunder', name: 'Terrasunder', element: 'earth',
baseHp: 105, baseAttack: 85, baseDefense: 100, baseSpeed: 35,
evolutionFrom: 'obsidian',
moves: earthMoves.filter(m => m.unlockLevel <= 15),
spriteParams: { shape: 'titan', colors: ['#713f12', '#a16207'], details: 'mountain-core' }
}
];
function getResonantTemplate(id) {
return exports.RESONANT_DB.find(r => r.id === id);
}
function createResonantInstance(templateId, level = 1) {
const template = getResonantTemplate(templateId);
if (!template)
throw new Error(`Unknown resonant: ${templateId}`);
const stats = (0, types_1.calcStats)(template, level);
return {
templateId,
level,
currentHp: stats.hp,
xp: 0,
status: null,
movesKnown: template.moves.filter(m => m.unlockLevel <= level).map(m => m.name),
caught: true
};
}
const types_1 = require("./types");
};
// ── module: src/sounds.ts ──
__mods["src/sounds.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.synth = exports.SynthManager = void 0;
class SynthManager {
constructor() {
this.ctx = null;
this.enabled = true;
this.masterGain = null;
}
init() {
if (this.ctx)
return;
try {
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
this.masterGain = this.ctx.createGain();
this.masterGain.gain.value = 0.3;
this.masterGain.connect(this.ctx.destination);
}
catch {
this.enabled = false;
}
}
playTone(frequency, duration, type = 'square', volume = 0.3) {
if (!this.enabled || !this.ctx || !this.masterGain)
return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(frequency, this.ctx.currentTime);
gain.gain.setValueAtTime(volume, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + duration);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start();
osc.stop(this.ctx.currentTime + duration);
}
playNoise(duration, volume = 0.2) {
if (!this.enabled || !this.ctx || !this.masterGain)
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;
}
const noise = this.ctx.createBufferSource();
noise.buffer = buffer;
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(volume, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + duration);
noise.connect(gain);
gain.connect(this.masterGain);
noise.start();
}
playHover() {
this.playTone(800, 0.05, 'sine', 0.15);
}
playSelect() {
this.playTone(600, 0.08, 'square', 0.2);
setTimeout(() => this.playTone(900, 0.06, 'square', 0.15), 50);
}
playHit() {
this.playNoise(0.15, 0.3);
this.playTone(200, 0.1, 'sawtooth', 0.25);
}
playHitSuperEffective() {
this.playNoise(0.2, 0.35);
this.playTone(400, 0.15, 'sawtooth', 0.3);
setTimeout(() => this.playTone(600, 0.1, 'square', 0.2), 80);
setTimeout(() => this.playTone(800, 0.15, 'square', 0.15), 160);
}
playLevelUp() {
const notes = [523, 659, 784, 1047];
notes.forEach((freq, i) => {
setTimeout(() => this.playTone(freq, 0.2, 'square', 0.2), i * 120);
});
}
playCaptureSuccess() {
const notes = [440, 554, 659, 880, 1108];
notes.forEach((freq, i) => {
setTimeout(() => this.playTone(freq, 0.25, 'sine', 0.2), i * 100);
});
setTimeout(() => {
this.playTone(1318, 0.4, 'sine', 0.15);
}, 600);
}
playCaptureFail() {
this.playTone(300, 0.2, 'square', 0.2);
setTimeout(() => this.playTone(250, 0.2, 'square', 0.15), 200);
setTimeout(() => this.playTone(200, 0.3, 'sawtooth', 0.1), 400);
}
playEvolution() {
const notes = [392, 494, 587, 659, 784, 880, 988, 1175];
notes.forEach((freq, i) => {
setTimeout(() => this.playTone(freq, 0.3, 'sine', 0.2), i * 100);
});
}
playBoss() {
const notes = [220, 196, 220, 262, 220, 196];
notes.forEach((freq, i) => {
setTimeout(() => this.playTone(freq, 0.3, 'sawtooth', 0.25), i * 200);
});
}
playHeal() {
const notes = [784, 988, 1175];
notes.forEach((freq, i) => {
setTimeout(() => this.playTone(freq, 0.3, 'sine', 0.15), i * 150);
});
}
playDialog() {
this.playTone(500, 0.04, 'sine', 0.1);
}
toggle() {
this.enabled = !this.enabled;
return this.enabled;
}
}
exports.SynthManager = SynthManager;
exports.synth = new SynthManager();
};
// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createInitialState = createInitialState;
exports.getSaveData = getSaveData;
exports.saveGame = saveGame;
exports.resetSave = resetSave;
exports.hasSave = hasSave;
exports.cloneResonant = cloneResonant;
exports.addInventoryItem = addInventoryItem;
exports.removeInventoryItem = removeInventoryItem;
exports.getPlayerResonant = getPlayerResonant;
exports.getPartyAliveCount = getPartyAliveCount;
const SAVE_KEY = 'aethelgard_save_v1';
function createInitialState() {
const starterChoices = [
{ id: 'emberkit', name: 'Emberkit' },
{ id: 'droplet', name: 'Droplet' },
{ id: 'seedling', name: 'Seedling' }
];
return {
name: 'Conductor',
party: [],
box: [],
inventory: [
{ type: 'prism_basic', count: 5 },
{ type: 'vital_elixir', count: 3 },
{ type: 'status_cleanser', count: 1 }
],
shards: 200,
unlockedZones: [true, false, false, false],
currentZone: 0,
currentNode: 0,
defeatedBosses: [],
dexUnlocked: [],
_starterChoices: starterChoices
};
}
function getSaveData() {
try {
const raw = localStorage.getItem(SAVE_KEY);
if (!raw)
return null;
const data = JSON.parse(raw);
// Validate essential fields
if (data.name && Array.isArray(data.party) && Array.isArray(data.unlockedZones)) {
return data;
}
}
catch {
// Corrupt save, ignore
}
return null;
}
function saveGame(state) {
try {
const { _starterChoices, ...saveData } = state;
localStorage.setItem(SAVE_KEY, JSON.stringify(saveData));
}
catch {
// Storage full or unavailable
}
}
function resetSave() {
localStorage.removeItem(SAVE_KEY);
}
function hasSave() {
return getSaveData() !== null;
}
// Helper to clone a ResonantInstance deeply
function cloneResonant(r) {
return JSON.parse(JSON.stringify(r));
}
function addInventoryItem(state, type, count = 1) {
const existing = state.inventory.find(i => i.type === type);
if (existing) {
existing.count += count;
}
else {
state.inventory.push({ type, count });
}
}
function removeInventoryItem(state, type, count = 1) {
const item = state.inventory.find(i => i.type === type);
if (!item || item.count < count)
return false;
item.count -= count;
if (item.count <= 0) {
state.inventory = state.inventory.filter(i => i.type !== type);
}
return true;
}
function getPlayerResonant(state) {
const alive = state.party.filter(r => r.currentHp > 0);
if (alive.length === 0)
return state.party[0]; // Return first even if fainted
return alive[0];
}
function getPartyAliveCount(state) {
return state.party.filter(r => r.currentHp > 0).length;
}
};
// ── module: src/map.ts ──
__mods["src/map.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ZONES = void 0;
exports.createZones = createZones;
exports.getBossData = getBossData;
exports.getNodeEncounter = getNodeEncounter;
exports.getMysteryCacheReward = getMysteryCacheReward;
const WILD_ENCOUNTERS = {
0: [
{ resonantId: 'seedling', weight: 35, minLevel: 1, maxLevel: 4 },
{ resonantId: 'gusty', weight: 30, minLevel: 2, maxLevel: 5 },
{ resonantId: 'sparky', weight: 20, minLevel: 1, maxLevel: 3 },
{ resonantId: 'pebble', weight: 15, minLevel: 3, maxLevel: 5 }
],
1: [
{ resonantId: 'droplet', weight: 30, minLevel: 5, maxLevel: 9 },
{ resonantId: 'pebble', weight: 25, minLevel: 5, maxLevel: 9 },
{ resonantId: 'seedling', weight: 20, minLevel: 6, maxLevel: 10 },
{ resonantId: 'gusty', weight: 15, minLevel: 5, maxLevel: 8 },
{ resonantId: 'sparky', weight: 10, minLevel: 7, maxLevel: 10 }
],
2: [
{ resonantId: 'emberkit', weight: 25, minLevel: 10, maxLevel: 14 },
{ resonantId: 'sparky', weight: 25, minLevel: 10, maxLevel: 15 },
{ resonantId: 'droplet', weight: 20, minLevel: 10, maxLevel: 14 },
{ resonantId: 'obsidian', weight: 15, minLevel: 11, maxLevel: 15 },
{ resonantId: 'gusty', weight: 15, minLevel: 10, maxLevel: 13 }
],
3: [
{ resonantId: 'ignisaur', weight: 15, minLevel: 15, maxLevel: 20 },
{ resonantId: 'stormlord', weight: 15, minLevel: 16, maxLevel: 22 },
{ resonantId: 'neptunian', weight: 15, minLevel: 15, maxLevel: 20 },
{ resonantId: 'zephyrus', weight: 15, minLevel: 16, maxLevel: 22 },
{ resonantId: 'terrasunder', weight: 15, minLevel: 15, maxLevel: 20 },
{ resonantId: 'bloomgoliath', weight: 15, minLevel: 16, maxLevel: 22 }
]
};
const BOSS_DATA = {
0: {
resonantId: 'thornback', level: 7,
dialog: [
{ speaker: 'Guardian of the Canopy', text: 'Halt, young Conductor. The Verdant Canopy tests all who would pass.', avatar: '🌿' },
{ speaker: 'Thornback', text: '*rustles menacingly*', avatar: '🐢' },
{ speaker: 'Guardian of the Canopy', text: 'Defeat my champion and prove your bond with Resonants is worthy.', avatar: '🌿' }
]
},
1: {
resonantId: 'geoxus', level: 12,
dialog: [
{ speaker: 'Cave Keeper Neri', text: 'The Sunken Grotto holds ancient waters. Few emerge unchanged.', avatar: '🧜' },
{ speaker: 'Geoxus', text: '*surges with tidal power*', avatar: '🐋' },
{ speaker: 'Cave Keeper Neri', text: 'Show me you can navigate the depths, Conductor. Face my guardian.', avatar: '🧜' }
]
},
2: {
resonantId: 'boltsaber', level: 17,
dialog: [
{ speaker: 'Crag Warden Voss', text: 'The Obsidian Crags burn with raw elemental fury. Only the strong survive.', avatar: '⚡' },
{ speaker: 'Boltsaber', text: '*crackles with electric energy*', avatar: '🐱' },
{ speaker: 'Crag Warden Voss', text: 'Your Resonant sparks like a candle. Let us see if yours can weather the storm.', avatar: '⚡' }
]
},
3: {
resonantId: 'stormlord', level: 22,
dialog: [
{ speaker: 'The Apex Sentinel', text: 'You have reached the pinnacle of Aethelgard. Few Conductors ever stand before me.', avatar: '👁️' },
{ speaker: 'Stormlord', text: '*thunder rolls in the distance*', avatar: '🐉' },
{ speaker: 'The Apex Sentinel', text: 'I am the final test. Defeat my champion and prove yourself Master Conductor of Aethelgard.', avatar: '👁️' },
{ speaker: 'The Apex Sentinel', text: 'May the strongest resonance prevail.', avatar: '👁️' }
]
}
};
const NPC_DIALOGS = {
0: [
[{ speaker: 'Wandering Botanist', text: 'The flora here is unusually vibrant! Would you like to tend to your Resonants at the spring ahead?', avatar: '👩🌾' }],
[{ speaker: 'Young Conductor', text: 'I just caught my first Gusty! It\'s so fast, I can barely keep up!', avatar: '🧒' }]
],
1: [
[{ speaker: 'Deep Diver', text: 'Beware the currents in the lower grotto. Only Hydro-type Resonants can navigate safely.', avatar: '🤿' }],
[{ speaker: 'Mysterious Hermit', text: 'The ancient waters hold secrets... but also dangers beyond your level.', avatar: '🧙' }]
],
2: [
[{ speaker: 'Volcanic Smith', text: 'I forge prisms from cooled obsidian. They make excellent capture tools!', avatar: '⚒️' }],
[{ speaker: 'Exhausted Traveler', text: 'The crags are treacherous. Make sure your team is ready before pushing forward.', avatar: '🥾' }]
],
3: [
[{ speaker: 'Lost Scholar', text: 'The Apex Pillar... they say it tests the very essence of resonance between Conductor and Resonant.', avatar: '📚' }],
[{ speaker: 'Retired Champion', text: 'I once stood where you stand. I did not succeed. But perhaps you will.', avatar: '🏆' }]
]
};
function createZones() {
const zoneConfigs = [
{ name: 'The Verdant Canopy', desc: 'Ancient forest canopy alive with Flora and Zephyr energy.', levelRange: [1, 5], color: '#22c55e', nodeCount: 8 },
{ name: 'The Sunken Grotto', desc: 'Underwater caverns pulsing with Hydro and Terra resonance.', levelRange: [5, 10], color: '#06b6d4', nodeCount: 9 },
{ name: 'The Obsidian Crags', desc: 'Volcanic mountain peaks crackling with Pyro and Volt energy.', levelRange: [10, 15], color: '#ef4444', nodeCount: 9 },
{ name: 'The Apex Pillar', desc: 'The sacred summit where elemental forces converge. Final gauntlet.', levelRange: [15, 22], color: '#a855f7', nodeCount: 7 }
];
return zoneConfigs.map((config, zoneIdx) => {
const nodes = [];
// Generate trail layout - winding path
const cols = Math.min(config.nodeCount, 6);
const rows = Math.ceil(config.nodeCount / cols);
for (let i = 0; i < config.nodeCount; i++) {
const col = i % cols;
const row = Math.floor(i / cols);
const x = 120 + col * ((800 - 200) / Math.max(cols - 1, 1));
const y = 100 + row * ((500 - 150) / Math.max(rows - 1, 1)) + (col % 2 === 0 ? 30 : -20);
let type;
if (i === config.nodeCount - 1) {
type = 'boss';
}
else if (i === 0) {
type = 'healing';
}
else {
const rand = Math.random();
if (rand < 0.55)
type = 'wild';
else if (rand < 0.75)
type = 'mystery';
else if (rand < 0.92)
type = 'npc';
else
type = 'healing';
}
const node = {
id: `zone${zoneIdx}_node${i}`,
x, y, type, cleared: false
};
if (type === 'wild') {
const encounters = WILD_ENCOUNTERS[zoneIdx];
const totalWeight = encounters.reduce((s, e) => s + e.weight, 0);
let roll = Math.random() * totalWeight;
let selected = encounters[0];
for (const enc of encounters) {
roll -= enc.weight;
if (roll <= 0) {
selected = enc;
break;
}
}
node.encounterId = `${selected.resonantId}_${selected.minLevel}_${selected.maxLevel}`;
}
if (type === 'npc') {
const dialogs = NPC_DIALOGS[zoneIdx];
node.dialogLines = dialogs[Math.floor(Math.random() * dialogs.length)];
}
nodes.push(node);
}
return {
name: config.name,
description: config.desc,
levelRange: config.levelRange,
elementWeights: zoneIdx === 0 ? { flora: 35, zephyr: 30, pyro: 15, hydro: 10, volt: 5, earth: 5, cosmos: 0 } :
zoneIdx === 1 ? { hydro: 35, earth: 25, flora: 15, zephyr: 10, pyro: 10, volt: 5, cosmos: 0 } :
zoneIdx === 2 ? { pyro: 30, volt: 30, hydro: 15, earth: 10, zephyr: 10, flora: 5, cosmos: 0 } :
{ cosmos: 20, pyro: 20, volt: 20, hydro: 15, flora: 15, zephyr: 5, earth: 5 },
nodes,
bossId: BOSS_DATA[zoneIdx]?.resonantId,
color: config.color
};
});
}
exports.ZONES = createZones();
function getBossData(zoneIdx) {
return BOSS_DATA[zoneIdx];
}
function getNodeEncounter(encounterId) {
if (!encounterId)
return null;
const parts = encounterId.split('_');
if (parts.length !== 3)
return null;
return {
resonantId: parts[0],
level: parseInt(parts[1]) + Math.floor(Math.random() * (parseInt(parts[2]) - parseInt(parts[1]) + 1))
};
}
function getMysteryCacheReward(zoneIdx) {
const baseShards = [30, 60, 100, 150][zoneIdx];
const variance = Math.floor(Math.random() * 20) - 10;
const shards = baseShards + variance;
if (Math.random() < 0.3) {
const items = ['prism_basic', 'vital_elixir', 'status_cleanser'];
return { shards, item: items[Math.floor(Math.random() * items.length)] };
}
return { shards };
}
};
// ── module: src/combat.ts ──
__mods["src/combat.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.startBattle = startBattle;
exports.getPlayerActive = getPlayerActive;
exports.getStat = getStat;
exports.getMaxHp = getMaxHp;
exports.executePlayerMove = executePlayerMove;
exports.executeEnemyTurn = executeEnemyTurn;
exports.attemptCapture = attemptCapture;
exports.getBattleState = getBattleState;
exports.setCombatContext = setCombatContext;
exports.clearCombatContext = clearCombatContext;
const types_1 = require("./types");
const resonants_1 = require("./resonants");
let battleState = null;
let combatCtx = null;
let battleCallback = null;
function startBattle(enemyTemplateId, enemyLevel, isWild) {
const template = (0, resonants_1.getResonantTemplate)(enemyTemplateId);
if (!template)
throw new Error(`Unknown resonant template: ${enemyTemplateId}`);
const stats = (0, types_1.calcStats)(template, enemyLevel);
const enemy = {
templateId: enemyTemplateId,
level: enemyLevel,
currentHp: stats.hp,
xp: 0,
status: null,
movesKnown: template.moves.filter(m => m.unlockLevel <= enemyLevel).map(m => m.name),
caught: false
};
const playerActive = getPlayerActive(combatCtx.playerParty);
battleState = {
enemy,
isWild,
turnOrder: [],
currentTurn: 0,
message: `A wild ${template.name} appeared!`,
phase: 'idle'
};
// Determine initial turn order based on speed
updateTurnOrder();
return battleState;
}
function getPlayerActive(party) {
const alive = party.filter(r => r.currentHp > 0);
if (alive.length === 0)
return party[0]; // All fainted
return alive[0];
}
function updateTurnOrder() {
if (!battleState || !combatCtx)
return;
const playerActive = getPlayerActive(combatCtx.playerParty);
const pSpeed = getStat(playerActive, 'speed');
const eSpeed = getStat(battleState.enemy, 'speed');
battleState.turnOrder = pSpeed >= eSpeed ? ['player', 'enemy'] : ['enemy', 'player'];
battleState.currentTurn = 0;
battleState.phase = 'action-select';
}
function getStat(resonant, stat) {
const template = (0, resonants_1.getResonantTemplate)(resonant.templateId);
if (!template)
return 0;
const baseStats = (0, types_1.calcStats)(template, resonant.level);
switch (stat) {
case 'hp': return baseStats.hp;
case 'attack': return baseStats.attack;
case 'defense': return baseStats.defense;
case 'speed': return baseStats.speed;
}
}
function getMaxHp(resonant) {
const template = (0, resonants_1.getResonantTemplate)(resonant.templateId);
if (!template)
return 0;
return (0, types_1.calcStats)(template, resonant.level).hp;
}
function executePlayerMove(moveName) {
if (!battleState || !combatCtx)
return;
const playerActive = getPlayerActive(combatCtx.playerParty);
const template = (0, resonants_1.getResonantTemplate)(playerActive.templateId);
if (!template)
return;
const move = template.moves.find(m => m.name === moveName);
if (!move)
return;
// Check accuracy
if (Math.random() * 100 > move.accuracy) {
battleState.message = `${playerActive.templateId} used ${move.name}... but it missed!`;
endPlayerTurn();
return;
}
const enemyDef = getStat(battleState.enemy, 'defense');
let typeMod = (0, types_1.getTypeModifier)(move.element, battleState.enemy.templateId);
// Check for critical hit
const crit = (0, types_1.isCritical)();
if (crit)
typeMod *= 1.5;
let damage = (0, types_1.calcDamage)(playerActive.level, move.power, getStat(playerActive, 'attack'), enemyDef, typeMod);
battleState.enemy.currentHp = Math.max(0, battleState.enemy.currentHp - damage);
const isSuperEffective = (0, types_1.getTypeModifier)(move.element, battleState.enemy.templateId) > 1;
if (isSuperEffective) {
battleState.message = `${move.name} is super effective! ${damage} damage!`;
}
else if (crit) {
battleState.message = `Critical hit! ${move.name} dealt ${damage} damage!`;
}
else {
battleState.message = `${playerActive.templateId} used ${move.name} for ${damage} damage!`;
}
// Check if enemy fainted
if (battleState.enemy.currentHp <= 0) {
battleState.phase = 'result';
battleState.message += ` ${battleState.enemy.templateId} fainted!`;
return;
}
endPlayerTurn();
}
function endPlayerTurn() {
if (!battleState || !combatCtx)
return;
// Move to next turn
const nextIdx = battleState.currentTurn + 1;
if (nextIdx >= battleState.turnOrder.length) {
// Enemy's turn
battleState.phase = 'enemy-turn';
setTimeout(() => executeEnemyTurn(), 800);
}
else {
battleState.currentTurn = nextIdx;
const turnHolder = battleState.turnOrder[nextIdx];
if (turnHolder === 'player') {
// Player goes again (shouldn't normally happen in 1v1)
battleState.phase = 'action-select';
}
else {
battleState.phase = 'enemy-turn';
setTimeout(() => executeEnemyTurn(), 800);
}
}
}
function executeEnemyTurn() {
if (!battleState || !combatCtx)
return;
const enemy = battleState.enemy;
const playerActive = getPlayerActive(combatCtx.playerParty);
// Pick a random move the enemy knows
const availableMoves = enemy.movesKnown.map(name => (0, resonants_1.getResonantTemplate)(enemy.templateId)?.moves.find(m => m.name === name)).filter((m) => !!m);
if (availableMoves.length === 0) {
battleState.message = `${enemy.templateId} has no moves!`;
endEnemyTurn();
return;
}
const move = availableMoves[Math.floor(Math.random() * availableMoves.length)];
// Check accuracy
if (Math.random() * 100 > move.accuracy) {
battleState.message = `${enemy.templateId} used ${move.name}... but it missed!`;
endBattleResult();
return;
}
const playerDef = getStat(playerActive, 'defense');
let typeMod = (0, types_1.getTypeModifier)(move.element, playerActive.templateId);
const crit = (0, types_1.isCritical)();
if (crit)
typeMod *= 1.5;
let damage = (0, types_1.calcDamage)(enemy.level, move.power, getStat(enemy, 'attack'), playerDef, typeMod);
// Status effect chance
if (Math.random() < 0.1 && !playerActive.status) {
const statuses = ['burn', 'paralysis', 'poison'];
playerActive.status = statuses[Math.floor(Math.random() * statuses.length)];
}
playerActive.currentHp = Math.max(0, playerActive.currentHp - damage);
const isSuperEffective = (0, types_1.getTypeModifier)(move.element, playerActive.templateId) > 1;
if (isSuperEffective) {
battleState.message = `${move.name} is super effective! ${damage} damage!`;
}
else if (crit) {
battleState.message = `Critical hit! ${enemy.templateId}'s ${move.name} dealt ${damage} damage!`;
}
else {
battleState.message = `${enemy.templateId} used ${move.name} for ${damage} damage!`;
}
if (playerActive.currentHp <= 0) {
// Check if any party members left
const aliveCount = combatCtx.playerParty.filter(r => r.currentHp > 0).length;
if (aliveCount === 0) {
battleState.phase = 'result';
battleState.message += ` All your Resonants fainted! You blacked out...`;
return;
}
else {
battleState.phase = 'result';
battleState.message += ` ${playerActive.templateId} fainted!`;
return;
}
}
endBattleResult();
}
function endBattleResult() {
if (!battleState || !combatCtx)
return;
// Check if enemy fainted or player has no more alive
const enemyAlive = battleState.enemy.currentHp > 0;
const playerAlive = combatCtx.playerParty.filter(r => r.currentHp > 0).length;
if (!enemyAlive) {
battleState.phase = 'result';
// Calculate XP reward
const xpReward = Math.floor(battleState.enemy.level * 12 + Math.random() * 10);
const shardReward = Math.floor(20 + Math.random() * 30);
if (battleCallback) {
battleCallback('win');
}
return;
}
if (playerAlive === 0) {
battleState.phase = 'result';
battleState.message += ` All your Resonants fainted! You blacked out...`;
if (battleCallback) {
battleCallback('win'); // Will handle defeat in caller
}
return;
}
battleState.phase = 'action-select';
}
function endEnemyTurn() {
if (!battleState)
return;
// Check for status damage
const playerActive = getPlayerActive(combatCtx.playerParty);
if (playerActive.status === 'poison') {
const poisonDmg = Math.floor(getMaxHp(playerActive) * 0.1);
playerActive.currentHp = Math.max(0, playerActive.currentHp - poisonDmg);
battleState.message += ` ${playerActive.templateId} took ${poisonDmg} poison damage!`;
}
else if (playerActive.status === 'burn') {
const burnDmg = Math.floor(getMaxHp(playerActive) * 0.06);
playerActive.currentHp = Math.max(0, playerActive.currentHp - burnDmg);
battleState.message += ` ${playerActive.templateId} is burned! Took ${burnDmg} damage!`;
}
if (playerActive.currentHp <= 0) {
const aliveCount = combatCtx.playerParty.filter(r => r.currentHp > 0).length;
if (aliveCount === 0) {
battleState.phase = 'result';
battleState.message += ` All your Resonants fainted!`;
}
else {
// Auto-switch to next alive
const nextAlive = combatCtx.playerParty.find(r => r.currentHp > 0 && r.templateId !== playerActive.templateId);
if (nextAlive) {
battleState.message += ` Send out ${nextAlive.templateId}!`;
}
}
return;
}
battleState.phase = 'action-select';
}
function attemptCapture(prismType) {
if (!battleState || !combatCtx)
return false;
const enemy = battleState.enemy;
const hpRatio = enemy.currentHp / getMaxHp(enemy);
let catchRate;
switch (prismType) {
case 'basic':
catchRate = 0.5;
break;
case 'hyper':
catchRate = 1.25;
break;
}
// Lower HP = higher catch rate
const hpModifier = (1 - hpRatio) * 1.5 + 0.3;
const finalRate = Math.min(0.95, catchRate * hpModifier);
return Math.random() < finalRate;
}
function getBattleState() {
return battleState;
}
function setCombatContext(ctx) {
combatCtx = ctx;
}
function clearCombatContext() {
combatCtx = null;
battleState = null;
}
};
// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.showScreen = showScreen;
exports.initMenuScreen = initMenuScreen;
exports.renderOverworld = renderOverworld;
exports.initMap = initMap;
exports.resizeMap = resizeMap;
exports.drawMap = drawMap;
exports.setupMapClickHandler = setupMapClickHandler;
exports.initBattle = initBattle;
exports.resizeBattle = resizeBattle;
exports.renderBattle = renderBattle;
exports.drawBattleSprite = drawBattleSprite;
exports.generateMiniSprite = generateMiniSprite;
exports.renderDex = renderDex;
exports.renderShop = renderShop;
exports.showEncounter = showEncounter;
exports.showLevelUp = showLevelUp;
exports.showEvolution = showEvolution;
exports.showCaptureAnimation = showCaptureAnimation;
exports.showDialog = showDialog;
exports.showToast = showToast;
exports.spawnBattleParticle = spawnBattleParticle;
exports.screenShake = screenShake;
exports.flashScreen = flashScreen;
const resonants_1 = require("./resonants");
const map_1 = require("./map");
const combat_1 = require("./combat");
const types_1 = require("./types");
// === SCREEN MANAGEMENT ===
function showScreen(screenId) {
document.querySelectorAll('.screen').forEach(s => s.classList.add('hidden'));
const screen = document.getElementById(screenId);
if (screen) {
screen.classList.remove('hidden');
// Trigger reflow for transition
void screen.offsetWidth;
}
}
// === MENU SCREEN ===
function initMenuScreen(hasSave) {
const loadBtn = document.getElementById('btn-load-game');
if (!hasSave) {
loadBtn.style.opacity = '0.3';
loadBtn.style.pointerEvents = 'none';
loadBtn.textContent = 'No Save Found';
}
}
// === OVERWORLD RENDERING ===
function renderOverworld(state) {
const zone = map_1.ZONES[state.currentZone];
// Update HUD
document.getElementById('player-name').textContent = state.name;
document.getElementById('zone-name').textContent = zone.name;
document.getElementById('shard-count').textContent = state.shards.toString();
// Update party display
renderPartyDisplay(state);
// Update zone nav
updateZoneNav(state);
// Draw map
drawMap(state.currentZone, state.currentNode);
}
function renderPartyDisplay(state) {
const container = document.getElementById('party-display');
container.innerHTML = '';
state.party.forEach((resonant, idx) => {
const template = (0, resonants_1.getResonantTemplate)(resonant.templateId);
if (!template)
return;
const slot = document.createElement('div');
slot.className = `party-slot ${idx === 0 ? 'active' : ''} ${resonant.currentHp <= 0 ? 'fainted' : ''}`;
slot.innerHTML = generateMiniSprite(template, resonant.level);
slot.title = `${template.name} Lv.${resonant.level}`;
const hpPercent = (resonant.currentHp / (0, combat_1.getMaxHp)(resonant)) * 100;
const hpColor = hpPercent > 50 ? '#22c55e' : hpPercent > 20 ? '#eab308' : '#ef4444';
slot.innerHTML += `
<div class="absolute bottom-0.5 left-1 right-1 h-1 bg-slate-700 rounded-full overflow-hidden">
<div class="h-full rounded-full" style="width:${hpPercent}%;background:${hpColor}"></div>
</div>`;
container.appendChild(slot);
});
// Fill empty slots
for (let i = state.party.length; i < 3; i++) {
const slot = document.createElement('div');
slot.className = 'party-slot opacity-20';
slot.innerHTML = '<svg class="w-6 h-6 text-slate-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/></svg>';
container.appendChild(slot);
}
}
function updateZoneNav(state) {
const nav = document.getElementById('zone-nav');
if (!nav)
return;
const buttons = nav.querySelectorAll('.zone-btn');
buttons.forEach((btn, idx) => {
const unlocked = state.unlockedZones[idx];
btn.disabled = !unlocked;
btn.classList.toggle('opacity-50', !unlocked);
btn.classList.toggle('cursor-not-allowed', !unlocked);
btn.classList.toggle('active', idx === state.currentZone);
});
}
// === MAP RENDERING (Canvas) ===
let mapCtx = null;
let mapCanvas = null;
function initMap() {
mapCanvas = document.getElementById('map-canvas');
if (!mapCanvas)
return;
// Use a reasonable default size if container has no dimensions yet
const container = document.getElementById('map-container');
const w = container?.clientWidth || 800;
const h = container?.clientHeight || 500;
mapCanvas.width = Math.max(w, 400);
mapCanvas.height = Math.max(h, 300);
mapCtx = mapCanvas.getContext('2d');
}
function resizeMap() {
if (!mapCanvas)
return;
const container = document.getElementById('map-container');
if (container) {
mapCanvas.width = container.clientWidth;
mapCanvas.height = container.clientHeight;
mapCtx = mapCanvas.getContext('2d');
}
}
function drawMap(zoneIdx, currentNode) {
if (!mapCtx || !mapCanvas)
return;
const ctx = mapCtx;
const canvas = mapCanvas;
const w = canvas.width;
const h = canvas.height;
const zone = map_1.ZONES[zoneIdx];
// Clear with gradient background
const bgGrad = ctx.createLinearGradient(0, 0, 0, h);
bgGrad.addColorStop(0, '#0f172a');
bgGrad.addColorStop(1, '#1e1b4b');
mapCtx.fillStyle = bgGrad;
mapCtx.fillRect(0, 0, w, h);
// Draw trail connections
const nodes = zone.nodes;
ctx.strokeStyle = 'rgba(139, 92, 246, 0.2)';
ctx.lineWidth = 3;
ctx.setLineDash([8, 4]);
ctx.beginPath();
for (let i = 0; i < nodes.length - 1; i++) {
if (!nodes[i].cleared && !nodes[i + 1].cleared) {
ctx.moveTo(nodes[i].x, nodes[i].y);
ctx.lineTo(nodes[i + 1].x, nodes[i + 1].y);
}
}
ctx.stroke();
ctx.setLineDash([]);
// Draw nodes
nodes.forEach((node, idx) => {
drawNode(ctx, node, idx === currentNode, zoneIdx);
});
// Draw current position indicator
if (nodes[currentNode]) {
const cn = nodes[currentNode];
ctx.save();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(cn.x, cn.y, 28, 0, Math.PI * 2);
ctx.stroke();
// Pulsing ring
const pulse = (Math.sin(Date.now() / 300) + 1) / 2;
ctx.strokeStyle = `rgba(139, 92, 246, ${0.3 + pulse * 0.4})`;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(cn.x, cn.y, 28 + pulse * 8, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
}
// Zone label
ctx.save();
ctx.font = '14px Inter';
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
ctx.textAlign = 'center';
ctx.fillText(zone.name, w / 2, h - 20);
ctx.restore();
}
function drawAmbientParticles(zoneIdx) {
if (!mapCtx || !mapCanvas)
return;
const ctx = mapCtx;
const canvas = mapCanvas;
const colors = [
['#22c55e', '#16a34a'],
['#06b6d4', '#0891b2'],
['#ef4444', '#f97316'],
['#a855f7', '#8b5cf6']
];
const [c1, c2] = colors[zoneIdx];
const time = Date.now() / 1000;
for (let i = 0; i < 30; i++) {
const x = (Math.sin(time * 0.3 + i * 1.7) + 1) / 2 * canvas.width;
const y = (Math.cos(time * 0.2 + i * 2.3) + 1) / 2 * canvas.height;
const size = 1.5 + Math.sin(time + i) * 1;
const alpha = 0.15 + Math.sin(time * 0.5 + i) * 0.1;
ctx.save();
ctx.fillStyle = i % 2 === 0 ? c1 : c2;
ctx.globalAlpha = alpha;
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
function drawNode(ctx, node, isCurrent, zoneIdx) {
const { x, y, type, cleared } = node;
ctx.save();
if (cleared) {
ctx.globalAlpha = 0.4;
}
// Node glow
const glowColors = {
wild: '#22c55e', mystery: '#facc15', npc: '#a78bfa',
healing: '#06b6d4', boss: '#ef4444'
};
const glowColor = glowColors[type] || '#ffffff';
if (isCurrent && !cleared) {
ctx.shadowColor = glowColor;
ctx.shadowBlur = 20;
}
// Node body
const size = type === 'boss' ? 24 : 18;
ctx.fillStyle = cleared ? '#334155' : glowColor;
if (type === 'wild') {
// Grass tuft shape
drawGrassNode(ctx, x, y, size);
}
else if (type === 'mystery') {
// Diamond shape
ctx.beginPath();
ctx.moveTo(x, y - size);
ctx.lineTo(x + size * 0.7, y);
ctx.lineTo(x, y + size);
ctx.lineTo(x - size * 0.7, y);
ctx.closePath();
ctx.fill();
}
else if (type === 'npc') {
// Circle with person icon
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.font = `${size}px serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('👤', x, y + 1);
}
else if (type === 'healing') {
// Cross shape
drawCrossNode(ctx, x, y, size);
}
else if (type === 'boss') {
// Star shape
drawStarNode(ctx, x, y, size);
}
ctx.restore();
}
function drawGrassNode(ctx, x, y, size) {
ctx.beginPath();
ctx.moveTo(x, y + size);
ctx.quadraticCurveTo(x - size, y + size * 0.3, x - size * 0.5, y - size * 0.2);
ctx.quadraticCurveTo(x - size * 0.3, y - size * 0.8, x, y - size);
ctx.quadraticCurveTo(x + size * 0.3, y - size * 0.8, x + size * 0.5, y - size * 0.2);
ctx.quadraticCurveTo(x + size, y + size * 0.3, x, y + size);
ctx.closePath();
ctx.fill();
}
function drawCrossNode(ctx, x, y, size) {
const w = size * 0.35;
ctx.beginPath();
ctx.moveTo(x - w, y - size);
ctx.lineTo(x + w, y - size);
ctx.lineTo(x + w, y - w);
ctx.lineTo(x + size, y - w);
ctx.lineTo(x + size, y + w);
ctx.lineTo(x + w, y + w);
ctx.lineTo(x + w, y + size);
ctx.lineTo(x - w, y + size);
ctx.lineTo(x - w, y + w);
ctx.lineTo(x - size, y + w);
ctx.lineTo(x - size, y - w);
ctx.lineTo(x - w, y - w);
ctx.closePath();
ctx.fill();
}
function drawStarNode(ctx, x, y, size) {
const spikes = 5;
const outerRadius = size;
const innerRadius = size * 0.4;
ctx.beginPath();
for (let i = 0; i < spikes * 2; i++) {
const radius = i % 2 === 0 ? outerRadius : innerRadius;
const angle = (Math.PI / spikes) * i - Math.PI / 2;
const px = x + Math.cos(angle) * radius;
const py = y + Math.sin(angle) * radius;
if (i === 0)
ctx.moveTo(px, py);
else
ctx.lineTo(px, py);
}
ctx.closePath();
ctx.fill();
}
// Click handler for map nodes
function setupMapClickHandler(onNodeClick) {
const canvas = document.getElementById('map-canvas');
if (!canvas)
return;
canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// Find closest node
let closestIdx = -1;
let closestDist = Infinity;
const currentZoneIdx = 0; // Will be set by game state
map_1.ZONES[currentZoneIdx]?.nodes?.forEach((node, nodeIdx) => {
const dist = Math.sqrt((x - node.x) ** 2 + (y - node.y) ** 2);
if (dist < 30 && dist < closestDist) {
closestDist = dist;
closestIdx = nodeIdx;
}
});
if (closestIdx >= 0) {
onNodeClick(currentZoneIdx, closestIdx);
}
});
}
// === BATTLE RENDERING ===
let battleAnimFrame = null;
let battleCtx = null;
let battleCanvas = null;
function initBattle() {
battleCanvas = document.getElementById('battle-canvas');
if (!battleCanvas)
return;
const arena = document.getElementById('battle-arena');
const w = arena?.clientWidth || 800;
const h = arena?.clientHeight || 500;
battleCanvas.width = Math.max(w, 400);
battleCanvas.height = Math.max(h, 300);
battleCtx = battleCanvas.getContext('2d');
}
function resizeBattle() {
if (!battleCanvas)
return;
const arena = document.getElementById('battle-arena');
if (arena) {
battleCanvas.width = arena.clientWidth;
battleCanvas.height = arena.clientHeight;
battleCtx = battleCanvas.getContext('2d');
}
}
function renderBattle(state) {
// Update enemy info
const enemyTemplate = (0, resonants_1.getResonantTemplate)(state.enemy.templateId);
if (enemyTemplate) {
document.getElementById('enemy-name').textContent = enemyTemplate.name;
document.getElementById('enemy-level').textContent = `Lv.${state.enemy.level}`;
const maxHp = (0, combat_1.getMaxHp)(state.enemy);
const hpPercent = (state.enemy.currentHp / maxHp) * 100;
const hpFill = document.getElementById('enemy-hp-fill');
hpFill.style.width = `${hpPercent}%`;
hpFill.className = `hp-fill h-full rounded-full transition-all duration-500 ${hpPercent > 50 ? '' : hpPercent > 20 ? 'mid' : 'low'}`;
document.getElementById('enemy-hp-text').textContent = `${state.enemy.currentHp}/${maxHp}`;
document.getElementById('enemy-hp-max').textContent = `HP`;
const statusEl = document.getElementById('enemy-status');
if (state.enemy.status) {
statusEl.textContent = `Status: ${state.enemy.status.toUpperCase()}`;
statusEl.className = 'mt-2 text-xs font-bold';
}
else {
statusEl.textContent = '';
}
}
// Update player info
const playerActive = state.enemy ? null : null; // Will be set by caller
document.getElementById('battle-msg-text').textContent = state.message;
// Draw battle scene
drawBattleScene(state);
}
function drawBattleScene(state) {
if (!battleCtx || !battleCanvas)
return;
const w = battleCanvas.width;
const h = battleCanvas.height;
// Background gradient
const bgGrad = battleCtx.createLinearGradient(0, 0, 0, h);
bgGrad.addColorStop(0, '#1e1b4b');
bgGrad.addColorStop(0.5, '#0f172a');
bgGrad.addColorStop(1, '#172554');
battleCtx.fillStyle = bgGrad;
battleCtx.fillRect(0, 0, w, h);
// Ground plane
const groundY = h * 0.65;
const groundGrad = battleCtx.createLinearGradient(0, groundY, 0, h);
groundGrad.addColorStop(0, 'rgba(34, 197, 94, 0.1)');
groundGrad.addColorStop(1, 'rgba(15, 23, 42, 0.3)');
battleCtx.fillStyle = groundGrad;
battleCtx.beginPath();
battleCtx.moveTo(0, groundY);
battleCtx.quadraticCurveTo(w / 2, groundY - 30, w, groundY);
battleCtx.lineTo(w, h);
battleCtx.lineTo(0, h);
battleCtx.closePath();
battleCtx.fill();
// Draw enemy sprite (right side)
const enemyTemplate = (0, resonants_1.getResonantTemplate)(state.enemy.templateId);
if (enemyTemplate) {
drawBattleSprite(battleCtx, w * 0.7, h * 0.35, enemyTemplate, state.enemy.level, false);
}
// Draw player sprite (left side) - placeholder for now
const playerActive = document.getElementById('player-resonant-name')?.textContent;
if (playerActive) {
const pTemplate = resonants_1.RESONANT_DB.find(r => r.name === playerActive);
if (pTemplate) {
drawBattleSprite(battleCtx, w * 0.3, h * 0.55, pTemplate, 1, true);
}
}
// Animated particles in background
const time = Date.now() / 1000;
for (let i = 0; i < 15; i++) {
const px = (Math.sin(time * 0.2 + i) + 1) / 2 * w;
const py = (Math.cos(time * 0.15 + i * 1.3) + 1) / 2 * h * 0.6;
battleCtx.save();
battleCtx.fillStyle = 'rgba(139, 92, 246, 0.15)';
battleCtx.beginPath();
battleCtx.arc(px, py, 2 + Math.sin(time + i) * 1, 0, Math.PI * 2);
battleCtx.fill();
battleCtx.restore();
}
}
function drawBattleSprite(ctx, x, y, template, level, isPlayer) {
const size = 60 + level * 1.5;
ctx.save();
// Bounce animation for player sprites
if (isPlayer) {
const bounce = Math.sin(Date.now() / 500) * 5;
y += bounce;
}
const colors = template.spriteParams.colors;
// Glow effect
ctx.shadowColor = colors[0];
ctx.shadowBlur = 15;
// Main body based on shape type
switch (template.spriteParams.shape) {
case 'circle':
drawCircleSprite(ctx, x, y, size, colors);
break;
case 'drop':
drawDropSprite(ctx, x, y, size, colors);
break;
case 'plant':
drawPlantSprite(ctx, x, y, size, colors);
break;
case 'mouse':
drawMouseSprite(ctx, x, y, size, colors);
break;
case 'spirit':
drawSpiritSprite(ctx, x, y, size, colors);
break;
case 'rock':
drawRockSprite(ctx, x, y, size, colors);
break;
case 'wolf':
drawWolfSprite(ctx, x, y, size, colors);
break;
case 'mammal':
drawMammalSprite(ctx, x, y, size, colors);
break;
case 'turtle':
drawTurtleSprite(ctx, x, y, size, colors);
break;
case 'cat':
drawCatSprite(ctx, x, y, size, colors);
break;
case 'bird':
drawBirdSprite(ctx, x, y, size, colors);
break;
case 'golem':
drawGolemSprite(ctx, x, y, size, colors);
break;
case 'dinosaur':
drawDinoSprite(ctx, x, y, size, colors);
break;
case 'leviathan':
drawLeviathanSprite(ctx, x, y, size, colors);
break;
case 'colossus':
drawColossusSprite(ctx, x, y, size, colors);
break;
case 'dragon':
drawDragonSprite(ctx, x, y, size, colors);
break;
case 'angel':
drawAngelSprite(ctx, x, y, size, colors);
break;
case 'titan':
drawTitanSprite(ctx, x, y, size, colors);
break;
default:
// Fallback circle
drawCircleSprite(ctx, x, y, size, ['#888', '#666']);
}
ctx.restore();
}
function drawCircleSprite(ctx, x, y, size, colors) {
const grad = ctx.createRadialGradient(x - size * 0.2, y - size * 0.2, 0, x, y, size);
grad.addColorStop(0, colors[1]);
grad.addColorStop(1, colors[0]);
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI * 2);
ctx.fill();
// Eyes
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.ellipse(x - size * 0.25, y - size * 0.1, size * 0.12, size * 0.15, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(x + size * 0.25, y - size * 0.1, size * 0.12, size * 0.15, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#1e1b4b';
ctx.beginPath();
ctx.arc(x - size * 0.2, y - size * 0.08, size * 0.06, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(x + size * 0.3, y - size * 0.08, size * 0.06, 0, Math.PI * 2);
ctx.fill();
}
function drawDropSprite(ctx, x, y, size, colors) {
const grad = ctx.createLinearGradient(x, y - size, x, y + size);
grad.addColorStop(0, colors[1]);
grad.addColorStop(1, colors[0]);
ctx.fillStyle = grad;
ctx.beginPath();
ctx.moveTo(x, y - size);
ctx.quadraticCurveTo(x + size * 0.8, y - size * 0.2, x + size * 0.6, y + size * 0.3);
ctx.quadraticCurveTo(x + size * 0.3, y + size, x, y + size * 0.8);
ctx.quadraticCurveTo(x - size * 0.3, y + size, x - size * 0.6, y + size * 0.3);
ctx.quadraticCurveTo(x - size * 0.8, y - size * 0.2, x, y - size);
ctx.closePath();
ctx.fill();
// Highlight
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.beginPath();
ctx.ellipse(x - size * 0.15, y - size * 0.2, size * 0.15, size * 0.25, -0.3, 0, Math.PI * 2);
ctx.fill();
// Eyes
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(x - size * 0.2, y + size * 0.05, size * 0.1, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(x + size * 0.2, y + size * 0.05, size * 0.1, 0, Math.PI * 2);
ctx.fill();
}
function drawPlantSprite(ctx, x, y, size, colors) {
// Pot/body
const grad = ctx.createRadialGradient(x, y + size * 0.3, 0, x, y + size * 0.3, size * 0.7);
grad.addColorStop(0, colors[1]);
grad.addColorStop(1, colors[0]);
ctx.fillStyle = grad;
ctx.beginPath();
ctx.ellipse(x, y + size * 0.3, size * 0.6, size * 0.5, 0, 0, Math.PI * 2);
ctx.fill();
// Leaves on top
const leafAngles = [-0.8, -0.3, 0.3, 0.8];
leafAngles.forEach(angle => {
ctx.save();
ctx.translate(x, y - size * 0.2);
ctx.rotate(angle);
ctx.fillStyle = colors[1];
ctx.beginPath();
ctx.ellipse(0, -size * 0.4, size * 0.15, size * 0.35, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
});
// Eyes
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(x - size * 0.2, y + size * 0.15, size * 0.08, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(x + size * 0.2, y + size * 0.15, size * 0.08, 0, Math.PI * 2);
ctx.fill();
}
function drawMouseSprite(ctx, x, y, size, colors) {
// Body
const grad = ctx.createRadialGradient(x, y, 0, x, y, size * 0.8);
grad.addColorStop(0, colors[1]);
grad.addColorStop(1, colors[0]);
ctx.fillStyle = grad;
ctx.beginPath();
ctx.ellipse(x, y + size * 0.1, size * 0.55, size * 0.65, 0, 0, Math.PI * 2);
ctx.fill();
// Ears
ctx.fillStyle = colors[0];
ctx.beginPath();
ctx.moveTo(x - size * 0.4, y - size * 0.3);
ctx.lineTo(x - size * 0.6, y - size * 0.8);
ctx.lineTo(x - size * 0.1, y - size * 0.5);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(x + size * 0.4, y - size * 0.3);
ctx.lineTo(x + size * 0.6, y - size * 0.8);
ctx.lineTo(x + size * 0.1, y - size * 0.5);
ctx.closePath();
ctx.fill();
// Tail (lightning bolt)
ctx.strokeStyle = colors[1];
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(x + size * 0.3, y + size * 0.6);
ctx.lineTo(x + size * 0.6, y + size * 0.4);
ctx.lineTo(x + size * 0.4, y + size * 0.7);
ctx.stroke();
// Eyes
ctx.fillStyle = '#1e1b4b';
ctx.beginPath();
ctx.arc(x - size * 0.2, y, size * 0.08, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(x + size * 0.2, y, size * 0.08, 0, Math.PI * 2);
ctx.fill();
}
function drawSpiritSprite(ctx, x, y, size, colors) {
// Ghostly body
const grad = ctx.createRadialGradient(x, y - size * 0.2, 0, x, y, size);
grad.addColorStop(0, 'rgba(167, 139, 250, 0.8)');
grad.addColorStop(0.7, colors[0]);
grad.addColorStop(1, 'rgba(139, 92, 246, 0.2)');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(x, y - size * 0.1, size * 0.7, Math.PI, 0);
// Wavy bottom
for (let i = 0; i < 5; i++) {
const wx = x - size * 0.7 + (i + 0.5) * size * 0.35;
const wy = y + size * 0.5 + (i % 2 === 0 ? -size * 0.15 : size * 0.05);
ctx.quadraticCurveTo(wx, wy, x - size * 0.7 + (i + 1) * size * 0.35, y + size * 0.5);
}
ctx.closePath();
ctx.fill();
// Eyes
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.ellipse(x - size * 0.2, y - size * 0.1, size * 0.1, size * 0.13, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(x + size * 0.2, y - size * 0.1, size * 0.1, size * 0.13, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#4c1d95';
ctx.beginPath();
ctx.arc(x - size * 0.15, y - size * 0.08, size * 0.05, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(x + size * 0.25, y - size * 0.08, size * 0.05, 0, Math.PI * 2);
ctx.fill();
}
function drawRockSprite(ctx, x, y, size, colors) {
// Irregular rock shape
const grad = ctx.createRadialGradient(x - size * 0.2, y - size * 0.2, 0, x, y, size);
grad.addColorStop(0, colors[1]);
grad.addColorStop(1, colors[0]);
ctx.fillStyle = grad;
ctx.beginPath();
ctx.moveTo(x - size * 0.5, y + size * 0.3);
ctx.lineTo(x - size * 0.7, y + size * 0.1);
ctx.lineTo(x - size * 0.4, y - size * 0.6);
ctx.lineTo(x, y - size * 0.8);
ctx.lineTo(x + size * 0.5, y - size * 0.5);
ctx.lineTo(x + size * 0.7, y + size * 0.1);
ctx.lineTo(x + size * 0.3, y + size * 0.4);
ctx.closePath();
ctx.fill();
// Crystal veins
ctx.strokeStyle = 'rgba(250, 204, 21, 0.6)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(x - size * 0.1, y - size * 0.3);
ctx.lineTo(x + size * 0.1, y);
ctx.lineTo(x - size * 0.05, y + size * 0.3);
ctx.stroke();
// Eyes
ctx.fillStyle = '#facc15';
ctx.beginPath();
ctx.arc(x - size * 0.2, y - size * 0.05, size * 0.07, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(x + size * 0.2, y - size * 0.05, size * 0.07, 0, Math.PI * 2);
ctx.fill();
}
function drawWolfSprite(ctx, x, y, size, colors) {
// Body
const grad = ctx.createRadialGradient(x, y + size * 0.1, 0, x, y + size * 0.1, size * 0.8);
grad.addColorStop(0, colors[1]);
grad.addColorStop(1, colors[0]);
ctx.fillStyle = grad;
ctx.beginPath();
ctx.ellipse(x, y + size * 0.2, size * 0.65, size * 0.5, 0, 0, Math.PI * 2);
ctx.fill();
// Head
ctx.fillStyle = colors[0];
ctx.beginPath();
ctx.arc(x - size * 0.15, y - size * 0.35, size * 0.4, 0, Math.PI * 2);
ctx.fill();
// Ears (pointed, fiery)
ctx.fillStyle = colors[1];
ctx.beginPath();
ctx.moveTo(x - size * 0.4, y - size * 0.55);
ctx.lineTo(x - size * 0.25, y - size * 0.9);
ctx.lineTo(x - size * 0.05, y - size * 0.55);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(x + size * 0.1, y - size * 0.55);
ctx.lineTo(x + size * 0.25, y - size * 0.85);
ctx.lineTo(x + size * 0.35, y - size * 0.5);
ctx.closePath();
ctx.fill();
// Fire mane
const flameColors = ['#f97316', '#ef4444', '#facc15'];
for (let i = 0; i < 5; i++) {
ctx.fillStyle = flameColors[i % 3];
ctx.globalAlpha = 0.7;
const fx = x + size * 0.2 + Math.sin(i) * size * 0.15;
const fy = y - size * 0.6 + i * size * 0.1;
ctx.beginPath();
ctx.moveTo(fx, fy);
ctx.quadraticCurveTo(fx + size * 0.15, fy - size * 0.2, fx + size * 0.05, fy - size * 0.3);
ctx.quadraticCurveTo(fx - size * 0.05, fy - size * 0.15, fx, fy);
ctx.fill();
}
ctx.globalAlpha = 1;
// Eyes (glowing)
ctx.fillStyle = '#facc15';
ctx.shadowColor = '#f97316';
ctx.shadowBlur = 8;
ctx.beginPath();
ctx.ellipse(x - size * 0.25, y - size * 0.35, size * 0.08, size * 0.1, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(x + size * 0.05, y - size * 0.35, size * 0.08, size * 0.1, 0, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
}
function drawMammalSprite(ctx, x, y, size, colors) {
// Body (streamlined fish-like)
const grad = ctx.createLinearGradient(x - size * 0.6, y, x + size * 0.6, y);
grad.addColorStop(0, colors[0]);
grad.addColorStop(1, colors[1]);
ctx.fillStyle = grad;
ctx.beginPath();
ctx.moveTo(x - size * 0.7, y);
ctx.quadraticCurveTo(x - size * 0.8, y + size * 0.3, x - size * 0.4, y + size * 0.5);
ctx.quadraticCurveTo(x, y + size * 0.6, x + size * 0.5, y + size * 0.3);
ctx.lineTo(x + size * 0.8, y + size * 0.1);
// Tail
ctx.moveTo(x + size * 0.7, y + size * 0.05);
ctx.lineTo(x + size * 1.0, y - size * 0.2);
ctx.lineTo(x + size * 0.85, y);
ctx.lineTo(x + size * 1.0, y + size * 0.2);
ctx.lineTo(x + size * 0.7, y + size * 0.15);
ctx.closePath();
ctx.fill();
// Crown fin
ctx.fillStyle = colors[1];
ctx.beginPath();
ctx.moveTo(x - size * 0.2, y - size * 0.3);
ctx.quadraticCurveTo(x, y - size * 0.8, x + size * 0.2, y - size * 0.3);
ctx.fill();
// Eye
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(x - size * 0.35, y - size * 0.05, size * 0.1, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#0e7490';
ctx.beginPath();
ctx.arc(x - size * 0.32, y - size * 0.03, size * 0.05, 0, Math.PI * 2);
ctx.fill();
}
function drawTurtleSprite(ctx, x, y, size, colors) {
// Shell (hexagonal)
const grad = ctx.createRadialGradient(x, y + size * 0.1, 0, x, y + size * 0.1, size * 0.7);
grad.addColorStop(0, colors[1]);
grad.addColorStop(1, colors[0]);
ctx.fillStyle = grad;
ctx.beginPath();
for (let i = 0; i < 6; i++) {
const angle = (Math.PI / 3) * i - Math.PI / 6;
const px = x + Math.cos(angle) * size * 0.7;
const py = y + size * 0.1 + Math.sin(angle) * size * 0.6;
if (i === 0)
ctx.moveTo(px, py);
else
ctx.lineTo(px, py);
}
ctx.closePath();
ctx.fill();
// Thorn patterns on shell
ctx.strokeStyle = 'rgba(0,0,0,0.3)';
ctx.lineWidth = 2;
for (let i = 0; i < 6; i++) {
const angle = (Math.PI / 3) * i;
ctx.beginPath();
ctx.moveTo(x + Math.cos(angle) * size * 0.2, y + size * 0.1 + Math.sin(angle) * size * 0.15);
ctx.lineTo(x + Math.cos(angle) * size * 0.5, y + size * 0.1 + Math.sin(angle) * size * 0.4);
ctx.stroke();
}
// Head
ctx.fillStyle = colors[0];
ctx.beginPath();
ctx.arc(x - size * 0.6, y - size * 0.15, size * 0.25, 0, Math.PI * 2);
ctx.fill();
// Thorns on back
ctx.fillStyle = '#15803d';
for (let i = 0; i < 4; i++) {
const tx = x - size * 0.3 + i * size * 0.25;
const ty = y - size * 0.35;
ctx.beginPath();
ctx.moveTo(tx - size * 0.08, ty);
ctx.lineTo(tx, ty - size * 0.25);
ctx.lineTo(tx + size * 0.08, ty);
ctx.closePath();
ctx.fill();
}
// Eyes
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(x - size * 0.68, y - size * 0.2, size * 0.06, 0, Math.PI * 2);
ctx.fill();
}
function drawCatSprite(ctx, x, y, size, colors) {
// Body
const grad = ctx.createRadialGradient(x, y + size * 0.1, 0, x, y + size * 0.1, size * 0.7);
grad.addColorStop(0, colors[1]);
grad.addColorStop(1, colors[0]);
ctx.fillStyle = grad;
ctx.beginPath();
ctx.ellipse(x, y + size * 0.2, size * 0.5, size * 0.6, 0, 0, Math.PI * 2);
ctx.fill();
// Head
ctx.fillStyle = colors[0];
ctx.beginPath();
ctx.arc(x, y - size * 0.35, size * 0.42, 0, Math.PI * 2);
ctx.fill();
// Lightning bolt ears
ctx.fillStyle = colors[1];
// Left ear
ctx.beginPath();
ctx.moveTo(x - size * 0.35, y - size * 0.5);
ctx.lineTo(x - size * 0.2, y - size * 0.9);
ctx.lineTo(x - size * 0.05, y - size * 0.48);
ctx.closePath();
ctx.fill();
// Right ear
ctx.beginPath();
ctx.moveTo(x + size * 0.35, y - size * 0.5);
ctx.lineTo(x + size * 0.2, y - size * 0.9);
ctx.lineTo(x + size * 0.05, y - size * 0.48);
ctx.closePath();
ctx.fill();
// Eyes (slitted, electric)
ctx.fillStyle = '#ef4444';
ctx.shadowColor = '#f59e0b';
ctx.shadowBlur = 10;
ctx.beginPath();
ctx.ellipse(x - size * 0.15, y - size * 0.35, size * 0.06, size * 0.12, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(x + size * 0.15, y - size * 0.35, size * 0.06, size * 0.12, 0, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
// Tail (lightning)
ctx.strokeStyle = colors[1];
ctx.lineWidth = 4;
ctx.beginPath();
ctx.moveTo(x + size * 0.3, y + size * 0.6);
ctx.quadraticCurveTo(x + size * 0.7, y + size * 0.4, x + size * 0.5, y + size * 0.1);
ctx.stroke();
}
function drawBirdSprite(ctx, x, y, size, colors) {
// Body
const grad = ctx.createRadialGradient(x, y, 0, x, y, size * 0.6);
grad.addColorStop(0, colors[1]);
grad.addColorStop(1, colors[0]);
ctx.fillStyle = grad;
ctx.beginPath();
ctx.ellipse(x, y + size * 0.1, size * 0.45, size * 0.55, 0, 0, Math.PI * 2);
ctx.fill();
// Wings (wind effect)
const time = Date.now() / 200;
ctx.fillStyle = colors[0];
ctx.globalAlpha = 0.8;
// Left wing
ctx.beginPath();
ctx.moveTo(x - size * 0.3, y);
ctx.quadraticCurveTo(x - size * 0.9, y - size * 0.5 + Math.sin(time) * 10, x - size * 0.7, y - size * 0.8);
ctx.quadraticCurveTo(x - size * 0.4, y - size * 0.3, x - size * 0.2, y + size * 0.1);
ctx.fill();
// Right wing
ctx.beginPath();
ctx.moveTo(x + size * 0.3, y);
ctx.quadraticCurveTo(x + size * 0.9, y - size * 0.5 + Math.cos(time) * 10, x + size * 0.7, y - size * 0.8);
ctx.quadraticCurveTo(x + size * 0.4, y - size * 0.3, x + size * 0.2, y + size * 0.1);
ctx.fill();
ctx.globalAlpha = 1;
// Head
ctx.fillStyle = colors[1];
ctx.beginPath();
ctx.arc(x, y - size * 0.45, size * 0.3, 0, Math.PI * 2);
ctx.fill();
// Beak
ctx.fillStyle = '#f59e0b';
ctx.beginPath();
ctx.moveTo(x + size * 0.2, y - size * 0.4);
ctx.lineTo(x + size * 0.45, y - size * 0.3);
ctx.lineTo(x + size * 0.2, y - size * 0.2);
ctx.closePath();
ctx.fill();
// Eyes
ctx.fillStyle = '#1e1b4b';
ctx.beginPath();
ctx.arc(x - size * 0.1, y - size * 0.48, size * 0.06, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(x + size * 0.1, y - size * 0.48, size * 0.06, 0, Math.PI * 2);
ctx.fill();
}
function drawGolemSprite(ctx, x, y, size, colors) {
// Body (blocky)
const grad = ctx.createLinearGradient(x - size * 0.5, y, x + size * 0.5, y + size);
grad.addColorStop(0, colors[1]);
grad.addColorStop(1, colors[0]);
ctx.fillStyle = grad;
// Torso
roundRect(ctx, x - size * 0.45, y - size * 0.2, size * 0.9, size * 0.8, 8);
ctx.fill();
// Head
ctx.fillStyle = colors[1];
roundRect(ctx, x - size * 0.3, y - size * 0.75, size * 0.6, size * 0.45, 6);
ctx.fill();
// Arms
ctx.fillStyle = colors[0];
roundRect(ctx, x - size * 0.75, y - size * 0.1, size * 0.25, size * 0.6, 4);
ctx.fill();
roundRect(ctx, x + size * 0.5, y - size * 0.1, size * 0.25, size * 0.6, 4);
ctx.fill();
// Lava cracks
ctx.strokeStyle = '#ef4444';
ctx.lineWidth = 2;
ctx.shadowColor = '#ef4444';
ctx.shadowBlur = 8;
ctx.beginPath();
ctx.moveTo(x - size * 0.1, y - size * 0.1);
ctx.lineTo(x + size * 0.05, y + size * 0.15);
ctx.lineTo(x - size * 0.05, y + size * 0.35);
ctx.stroke();
ctx.shadowBlur = 0;
// Eyes (glowing orange)
ctx.fillStyle = '#ef4444';
ctx.shadowColor = '#ef4444';
ctx.shadowBlur = 12;
ctx.beginPath();
ctx.arc(x - size * 0.12, y - size * 0.5, size * 0.06, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(x + size * 0.12, y - size * 0.5, size * 0.06, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
}
function drawDinoSprite(ctx, x, y, size, colors) {
// Body
const grad = ctx.createRadialGradient(x, y + size * 0.1, 0, x, y + size * 0.1, size * 0.7);
grad.addColorStop(0, colors[1]);
grad.addColorStop(1, colors[0]);
ctx.fillStyle = grad;
ctx.beginPath();
ctx.ellipse(x, y + size * 0.15, size * 0.6, size * 0.5, 0, 0, Math.PI * 2);
ctx.fill();
// Neck and head
ctx.fillStyle = colors[0];
ctx.beginPath();
ctx.moveTo(x + size * 0.3, y - size * 0.1);
ctx.quadraticCurveTo(x + size * 0.5, y - size * 0.5, x + size * 0.4, y - size * 0.7);
ctx.quadraticCurveTo(x + size * 0.6, y - size * 0.8, x + size * 0.5, y - size * 0.9);
ctx.quadraticCurveTo(x + size * 0.7, y - size * 0.7, x + size * 0.55, y - size * 0.5);
ctx.lineTo(x + size * 0.35, y - size * 0.2);
ctx.closePath();
ctx.fill();
// Lava plates on back
ctx.fillStyle = '#ef4444';
for (let i = 0; i < 4; i++) {
const px = x - size * 0.3 + i * size * 0.2;
const py = y - size * 0.25 - Math.sin(i * 1.5) * size * 0.15;
ctx.beginPath();
ctx.moveTo(px - size * 0.06, py);
ctx.lineTo(px, py - size * 0.2);
ctx.lineTo(px + size * 0.06, py);
ctx.closePath();
ctx.fill();
}
// Tail
ctx.fillStyle = colors[1];
ctx.beginPath();
ctx.moveTo(x - size * 0.5, y + size * 0.1);
ctx.quadraticCurveTo(x - size * 0.9, y + size * 0.3, x - size * 0.8, y + size * 0.6);
ctx.quadraticCurveTo(x - size * 0.7, y + size * 0.4, x - size * 0.5, y + size * 0.2);
ctx.closePath();
ctx.fill();
// Eyes (fierce)
ctx.fillStyle = '#facc15';
ctx.shadowColor = '#ef4444';
ctx.shadowBlur = 8;
ctx.beginPath();
ctx.ellipse(x + size * 0.5, y - size * 0.7, size * 0.06, size * 0.08, 0, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
}
function drawLeviathanSprite(ctx, x, y, size, colors) {
// Massive body
const grad = ctx.createLinearGradient(x - size * 0.7, y, x + size * 0.7, y);
grad.addColorStop(0, colors[0]);
grad.addColorStop(1, colors[1]);
ctx.fillStyle = grad;
ctx.beginPath();
ctx.moveTo(x - size * 0.7, y + size * 0.1);
ctx.quadraticCurveTo(x - size * 0.8, y + size * 0.4, x - size * 0.5, y + size * 0.5);
ctx.quadraticCurveTo(x, y + size * 0.6, x + size * 0.6, y + size * 0.3);
// Tail fin
ctx.lineTo(x + size * 0.9, y + size * 0.1);
ctx.lineTo(x + size * 0.7, y - size * 0.1);
ctx.quadraticCurveTo(x + size * 0.3, y - size * 0.2, x - size * 0.3, y - size * 0.3);
ctx.quadraticCurveTo(x - size * 0.6, y - size * 0.2, x - size * 0.7, y + size * 0.1);
ctx.closePath();
ctx.fill();
// Crown (kingly)
ctx.fillStyle = '#facc15';
ctx.beginPath();
ctx.moveTo(x - size * 0.3, y - size * 0.25);
ctx.lineTo(x - size * 0.25, y - size * 0.55);
ctx.lineTo(x - size * 0.1, y - size * 0.35);
ctx.lineTo(x, y - size * 0.65);
ctx.lineTo(x + size * 0.1, y - size * 0.35);
ctx.lineTo(x + size * 0.25, y - size * 0.55);
ctx.lineTo(x + size * 0.3, y - size * 0.25);
ctx.closePath();
ctx.fill();
// Eyes (regal blue)
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(x - size * 0.15, y - size * 0.08, size * 0.07, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(x + size * 0.15, y - size * 0.08, size * 0.07, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#0e7490';
ctx.beginPath();
ctx.arc(x - size * 0.13, y - size * 0.06, size * 0.04, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(x + size * 0.17, y - size * 0.06, size * 0.04, 0, Math.PI * 2);
ctx.fill();
}
function drawColossusSprite(ctx, x, y, size, colors) {
// Massive body
const grad = ctx.createRadialGradient(x, y + size * 0.1, 0, x, y + size * 0.1, size * 0.8);
grad.addColorStop(0, colors[1]);
grad.addColorStop(1, colors[0]);
ctx.fillStyle = grad;
// Body (large oval)
ctx.beginPath();
ctx.ellipse(x, y + size * 0.2, size * 0.7, size * 0.65, 0, 0, Math.PI * 2);
ctx.fill();
// Arms (thick)
ctx.fillStyle = colors[0];
roundRect(ctx, x - size * 0.9, y - size * 0.1, size * 0.25, size * 0.7, 6);
ctx.fill();
roundRect(ctx, x + size * 0.65, y - size * 0.1, size * 0.25, size * 0.7, 6);
ctx.fill();
// Flower crown
const flowerColors = ['#f472b6', '#c084fc', '#fb923c'];
for (let i = 0; i < 5; i++) {
const angle = -Math.PI / 2 + (i - 2) * 0.4;
const fx = x + Math.cos(angle) * size * 0.45;
const fy = y - size * 0.4 + Math.sin(angle) * size * 0.15;
ctx.fillStyle = flowerColors[i % 3];
for (let p = 0; p < 5; p++) {
const pa = (Math.PI * 2 / 5) * p;
ctx.beginPath();
ctx.ellipse(fx + Math.cos(pa) * size * 0.06, fy + Math.sin(pa) * size * 0.06, size * 0.04, size * 0.04, pa, 0, Math.PI * 2);
ctx.fill();
}
ctx.fillStyle = '#facc15';
ctx.beginPath();
ctx.arc(fx, fy, size * 0.04, 0, Math.PI * 2);
ctx.fill();
}
// Eyes (calm green)
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(x - size * 0.2, y + size * 0.0, size * 0.08, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(x + size * 0.2, y + size * 0.0, size * 0.08, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#15803d';
ctx.beginPath();
ctx.arc(x - size * 0.18, y + size * 0.02, size * 0.04, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(x + size * 0.22, y + size * 0.02, size * 0.04, 0, Math.PI * 2);
ctx.fill();
}
function drawDragonSprite(ctx, x, y, size, colors) {
// Body
const grad = ctx.createRadialGradient(x, y + size * 0.1, 0, x, y + size * 0.1, size * 0.6);
grad.addColorStop(0, colors[1]);
grad.addColorStop(1, colors[0]);
ctx.fillStyle = grad;
ctx.beginPath();
ctx.ellipse(x, y + size * 0.15, size * 0.45, size * 0.55, 0, 0, Math.PI * 2);
ctx.fill();
// Wings (electric storm)
const wingGrad = ctx.createLinearGradient(x - size * 0.8, y - size * 0.8, x + size * 0.8, y);
wingGrad.addColorStop(0, 'rgba(239, 68, 68, 0.6)');
wingGrad.addColorStop(1, 'rgba(245, 158, 11, 0.3)');
ctx.fillStyle = wingGrad;
// Left wing
ctx.beginPath();
ctx.moveTo(x - size * 0.3, y);
ctx.quadraticCurveTo(x - size * 0.9, y - size * 0.8, x - size * 0.6, y - size * 1.0);
ctx.quadraticCurveTo(x - size * 0.3, y - size * 0.6, x - size * 0.15, y - size * 0.2);
ctx.closePath();
ctx.fill();
// Right wing
ctx.beginPath();
ctx.moveTo(x + size * 0.3, y);
ctx.quadraticCurveTo(x + size * 0.9, y - size * 0.8, x + size * 0.6, y - size * 1.0);
ctx.quadraticCurveTo(x + size * 0.3, y - size * 0.6, x + size * 0.15, y - size * 0.2);
ctx.closePath();
ctx.fill();
// Head
ctx.fillStyle = colors[1];
ctx.beginPath();
ctx.arc(x, y - size * 0.45, size * 0.32, 0, Math.PI * 2);
ctx.fill();
// Horns
ctx.fillStyle = '#ef4444';
ctx.beginPath();
ctx.moveTo(x - size * 0.2, y - size * 0.65);
ctx.lineTo(x - size * 0.3, y - size * 0.95);
ctx.lineTo(x - size * 0.05, y - size * 0.65);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(x + size * 0.2, y - size * 0.65);
ctx.lineTo(x + size * 0.3, y - size * 0.95);
ctx.lineTo(x + size * 0.05, y - size * 0.65);
ctx.closePath();
ctx.fill();
// Eyes (fierce yellow)
ctx.fillStyle = '#facc15';
ctx.shadowColor = '#ef4444';
ctx.shadowBlur = 12;
ctx.beginPath();
ctx.ellipse(x - size * 0.12, y - size * 0.48, size * 0.07, size * 0.1, -0.2, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(x + size * 0.12, y - size * 0.48, size * 0.07, size * 0.1, 0.2, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
// Tail
ctx.strokeStyle = colors[0];
ctx.lineWidth = 5;
ctx.beginPath();
ctx.moveTo(x + size * 0.3, y + size * 0.5);
ctx.quadraticCurveTo(x + size * 0.8, y + size * 0.6, x + size * 0.7, y + size * 0.9);
ctx.stroke();
}
function drawAngelSprite(ctx, x, y, size, colors) {
// Ethereal body
const grad = ctx.createRadialGradient(x, y, 0, x, y, size * 0.7);
grad.addColorStop(0, 'rgba(196, 181, 253, 0.9)');
grad.addColorStop(0.6, colors[0]);
grad.addColorStop(1, 'rgba(139, 92, 246, 0.3)');
ctx.fillStyle = grad;
// Flowing body (teardrop shape)
ctx.beginPath();
ctx.moveTo(x, y - size * 0.7);
ctx.quadraticCurveTo(x + size * 0.5, y - size * 0.2, x + size * 0.4, y + size * 0.3);
ctx.quadraticCurveTo(x + size * 0.2, y + size * 0.7, x, y + size * 0.6);
ctx.quadraticCurveTo(x - size * 0.2, y + size * 0.7, x - size * 0.4, y + size * 0.3);
ctx.quadraticCurveTo(x - size * 0.5, y - size * 0.2, x, y - size * 0.7);
ctx.closePath();
ctx.fill();
// Halo (wind ring)
ctx.strokeStyle = colors[1];
ctx.lineWidth = 3;
ctx.shadowColor = colors[1];
ctx.shadowBlur = 15;
const haloY = y - size * 0.8;
ctx.beginPath();
ctx.ellipse(x, haloY, size * 0.4, size * 0.12, 0, 0, Math.PI * 2);
ctx.stroke();
ctx.shadowBlur = 0;
// Wind wisps around body
const time = Date.now() / 300;
ctx.strokeStyle = 'rgba(167, 139, 250, 0.4)';
ctx.lineWidth = 2;
for (let i = 0; i < 4; i++) {
const wx = x + Math.sin(time + i * 1.5) * size * 0.5;
const wy = y + Math.cos(time + i * 2) * size * 0.3;
ctx.beginPath();
ctx.moveTo(wx - size * 0.1, wy);
ctx.quadraticCurveTo(wx, wy - size * 0.1, wx + size * 0.1, wy);
ctx.stroke();
}
// Eyes (serene purple)
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(x - size * 0.12, y - size * 0.15, size * 0.07, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(x + size * 0.12, y - size * 0.15, size * 0.07, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#7c3aed';
ctx.beginPath();
ctx.arc(x - size * 0.1, y - size * 0.13, size * 0.04, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(x + size * 0.14, y - size * 0.13, size * 0.04, 0, Math.PI * 2);
ctx.fill();
}
function drawTitanSprite(ctx, x, y, size, colors) {
// Colossal body
const grad = ctx.createLinearGradient(x - size * 0.6, y - size * 0.5, x + size * 0.6, y + size * 0.8);
grad.addColorStop(0, colors[1]);
grad.addColorStop(1, colors[0]);
ctx.fillStyle = grad;
// Body (massive rectangular)
roundRect(ctx, x - size * 0.55, y - size * 0.3, size * 1.1, size * 1.1, 12);
ctx.fill();
// Mountain core on chest (glowing)
const coreGrad = ctx.createRadialGradient(x, y + size * 0.05, 0, x, y + size * 0.05, size * 0.25);
coreGrad.addColorStop(0, '#f97316');
coreGrad.addColorStop(0.5, '#ef4444');
coreGrad.addColorStop(1, 'rgba(239, 68, 68, 0)');
ctx.fillStyle = coreGrad;
ctx.shadowColor = '#f97316';
ctx.shadowBlur = 25;
ctx.beginPath();
ctx.arc(x, y + size * 0.05, size * 0.2, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
// Arms (thick pillars)
ctx.fillStyle = colors[0];
roundRect(ctx, x - size * 0.85, y - size * 0.1, size * 0.25, size * 0.8, 6);
ctx.fill();
roundRect(ctx, x + size * 0.6, y - size * 0.1, size * 0.25, size * 0.8, 6);
ctx.fill();
// Head (small relative to body)
ctx.fillStyle = colors[1];
roundRect(ctx, x - size * 0.2, y - size * 0.75, size * 0.4, size * 0.35, 6);
ctx.fill();
// Eyes (molten)
ctx.fillStyle = '#f97316';
ctx.shadowColor = '#ef4444';
ctx.shadowBlur = 15;
ctx.beginPath();
ctx.arc(x - size * 0.1, y - size * 0.55, size * 0.05, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(x + size * 0.1, y - size * 0.55, size * 0.05, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
// Crack lines
ctx.strokeStyle = 'rgba(234, 88, 12, 0.5)';
ctx.lineWidth = 1.5;
for (let i = 0; i < 6; i++) {
const sx = x - size * 0.4 + Math.random() * size * 0.8;
const sy = y - size * 0.2 + Math.random() * size * 0.8;
ctx.beginPath();
ctx.moveTo(sx, sy);
ctx.lineTo(sx + (Math.random() - 0.5) * size * 0.2, sy + Math.random() * size * 0.2);
ctx.stroke();
}
}
function roundRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
// === SPRITE GENERATOR (for dex and party display) ===
function generateMiniSprite(template, level) {
const colors = template.spriteParams.colors;
const size = 36 + Math.min(level, 20);
return `<svg width="${size}" height="${size}" viewBox="0 0 100 100" class="battle-sprite float">
<defs>
<radialGradient id="grad_${template.id}" cx="40%" cy="35%">
<stop offset="0%" stop-color="${colors[1]}"/>
<stop offset="100%" stop-color="${colors[0]}"/>
</radialGradient>
</defs>
<circle cx="50" cy="55" r="35" fill="url(#grad_${template.id})" filter="drop-shadow(0 2px 6px rgba(0,0,0,0.3))"/>
<circle cx="40" cy="48" r="6" fill="#fff"/>
<circle cx="60" cy="48" r="6" fill="#fff"/>
<circle cx="41" cy="49" r="3" fill="#1e1b4b"/>
<circle cx="61" cy="49" r="3" fill="#1e1b4b"/>
<path d="M 42 65 Q 50 72 58 65" stroke="#fff" stroke-width="2.5" fill="none" opacity="0.7"/>
</svg>`;
}
// === DEX RENDERING ===
function renderDex(unlockedIds) {
const grid = document.getElementById('dex-grid');
if (!grid)
return;
grid.innerHTML = '';
resonants_1.RESONANT_DB.forEach(template => {
const unlocked = unlockedIds.includes(template.id);
const card = document.createElement('div');
card.className = `dex-card ${unlocked ? '' : 'locked'}`;
if (unlocked) {
const stats = calcStatsForDex(template, 1);
card.innerHTML = `
<div class="w-full flex justify-center">${generateMiniSprite(template, 1)}</div>
<span class="text-white font-bold text-xs truncate w-full text-center">${template.name}</span>
<span class="type-${template.element} text-[10px] uppercase tracking-wider font-semibold">${template.element}</span>
<div class="w-full space-y-0.5 mt-1">
<div class="flex justify-between text-[9px] text-slate-400"><span>HP</span><span>${stats.hp}</span></div>
<div class="flex justify-between text-[9px] text-slate-400"><span>ATK</span><span>${stats.attack}</span></div>
<div class="flex justify-between text-[9px] text-slate-400"><span>DEF</span><span>${stats.defense}</span></div>
<div class="flex justify-between text-[9px] text-slate-400"><span>SPD</span><span>${stats.speed}</span></div>
</div>`;
}
else {
card.innerHTML = `
<div class="w-full flex justify-center opacity-30">
<svg width="48" height="48" viewBox="0 0 100 100"><circle cx="50" cy="50" r="40" fill="#334155"/><text x="50" y="55" text-anchor="middle" fill="#64748b" font-size="20">?</text></svg>
</div>
<span class="text-slate-500 font-bold text-xs truncate w-full text-center">???</span>
<span class="text-slate-600 text-[10px] uppercase tracking-wider">Unknown</span>`;
}
grid.appendChild(card);
});
}
function calcStatsForDex(template, level) {
const hp = Math.floor(((2 * template.baseHp + 10) * level) / 100) + level + 10;
const atk = Math.floor(((2 * template.baseAttack + 5) * level) / 100) + 5;
const def = Math.floor(((2 * template.baseDefense + 5) * level) / 100) + 5;
const spd = Math.floor(((2 * template.baseSpeed + 5) * level) / 100) + 5;
return { hp, attack: atk, defense: def, speed: spd };
}
// === SHOP RENDERING ===
function renderShop(shards) {
const grid = document.getElementById('shop-grid');
if (!grid)
return;
document.getElementById('shop-shards').textContent = `${shards} Shards`;
grid.innerHTML = '';
types_1.SHOP_ITEMS.forEach(item => {
const div = document.createElement('div');
div.className = 'shop-item';
const canAfford = shards >= item.cost;
div.innerHTML = `
<div class="flex items-center gap-3">
<span class="text-2xl">${item.icon}</span>
<div class="flex-1">
<h3 class="text-white font-bold text-sm">${item.name}</h3>
<p class="text-slate-400 text-xs">${item.description}</p>
</div>
</div>
<button data-shop-item="${item.id}" class="w-full py-2 rounded-xl text-sm font-semibold transition-all ${canAfford ? 'bg-gradient-to-r from-amber-600 to-yellow-600 hover:from-amber-500 hover:to-yellow-500 text-white' : 'bg-slate-700 text-slate-500 cursor-not-allowed'}">
${item.cost} Shards
</button>`;
grid.appendChild(div);
});
}
// === OVERLAYS & DIALOGS ===
function showEncounter(text, onContinue) {
const overlay = document.getElementById('encounter-overlay');
if (!overlay)
return;
document.getElementById('encounter-text').textContent = text;
overlay.classList.remove('hidden');
const btn = document.getElementById('encounter-btn');
if (btn) {
btn.onclick = () => {
overlay.classList.add('hidden');
onContinue();
};
}
}
function showLevelUp(text, onContinue) {
const overlay = document.getElementById('levelup-overlay');
if (!overlay)
return;
document.getElementById('levelup-text').textContent = text;
overlay.classList.remove('hidden');
const btn = document.getElementById('levelup-btn');
if (btn) {
btn.onclick = () => {
overlay.classList.add('hidden');
onContinue();
};
}
}
function showEvolution(spriteHtml, text) {
return new Promise(resolve => {
const overlay = document.getElementById('evolution-overlay');
if (!overlay) {
resolve();
return;
}
document.getElementById('evolution-sprite-container').innerHTML = spriteHtml;
document.getElementById('evolution-text').textContent = text;
overlay.classList.remove('hidden');
const btn = document.getElementById('evolution-btn');
if (btn) {
btn.onclick = () => {
overlay.classList.add('hidden');
resolve();
};
}
});
}
function showCaptureAnimation(success) {
return new Promise(resolve => {
const overlay = document.getElementById('capture-overlay');
if (!overlay) {
resolve();
return;
}
const prismEl = document.getElementById('capture-prism');
const textEl = document.getElementById('capture-text');
if (success) {
prismEl.innerHTML = `<svg width="128" height="128" viewBox="0 0 128 128">
<defs><linearGradient id="cg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#06b6d4"/><stop offset="100%" stop-color="#8b5cf6"/>
</linearGradient></defs>
<polygon points="64,8 120,40 120,88 64,120 8,88 8,40" fill="none" stroke="url(#cg)" stroke-width="3">
<animate attributeName="opacity" values="1;0.5;1" dur="1s" repeatCount="indefinite"/>
</polygon>
<polygon points="64,24 100,48 100,80 64,104 28,80 28,48" fill="url(#cg)" opacity="0.3">
<animate attributeName="opacity" values="0.3;0.8;0.3" dur="1.5s" repeatCount="indefinite"/>
</polygon>
</svg>`;
textEl.textContent = 'Capture Successful!';
prismEl.className = 'w-32 h-32 mx-auto mb-6 prism-spin';
}
else {
prismEl.innerHTML = `<svg width="128" height="128" viewBox="0 0 128 128">
<polygon points="64,8 120,40 120,88 64,120 8,88 8,40" fill="none" stroke="#ef4444" stroke-width="3" opacity="0.5"/>
<text x="64" y="72" text-anchor="middle" fill="#ef4444" font-size="40">✕</text>
</svg>`;
textEl.textContent = 'It broke free!';
}
overlay.classList.remove('hidden');
setTimeout(() => {
overlay.classList.add('hidden');
resolve();
}, 2500);
});
}
function showDialog(speaker, text, avatar = '💬') {
return new Promise(resolve => {
const overlay = document.getElementById('dialog-overlay');
if (!overlay) {
resolve();
return;
}
document.getElementById('dialog-speaker').textContent = speaker;
document.getElementById('dialog-text').textContent = text;
document.getElementById('dialog-avatar').textContent = avatar;
overlay.classList.remove('hidden');
overlay.style.animation = 'none';
void overlay.offsetWidth;
overlay.style.animation = '';
const btn = document.getElementById('dialog-next');
if (btn) {
btn.onclick = () => {
overlay.classList.add('hidden');
resolve();
};
}
});
}
// === TOAST NOTIFICATIONS ===
function showToast(message, type = 'info') {
const container = document.getElementById('toast-container');
if (!container)
return;
const toast = document.createElement('div');
toast.className = `toast`;
const colors = { success: 'border-emerald-500/30 text-emerald-400', error: 'border-red-500/30 text-red-400', info: 'border-cyan-500/30 text-cyan-400' };
toast.style.borderLeft = `3px solid ${type === 'success' ? '#22c55e' : type === 'error' ? '#ef4444' : '#06b6d4'}`;
toast.textContent = message;
container.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateX(100%)';
toast.style.transition = 'all 0.3s ease';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// === BATTLE PARTICLES ===
function spawnBattleParticle(x, y, color, count = 8) {
const container = document.getElementById('battle-particles');
if (!container)
return;
for (let i = 0; i < count; i++) {
const particle = document.createElement('div');
particle.className = 'particle';
const size = 3 + Math.random() * 5;
const drift = (Math.random() - 0.5) * 100;
particle.style.cssText = `
width: ${size}px; height: ${size}px;
background: ${color};
left: ${x}px; top: ${y}px;
--drift: ${drift}px;
box-shadow: 0 0 6px ${color};
`;
container.appendChild(particle);
setTimeout(() => particle.remove(), 1500);
}
}
// === SCREEN SHAKE ===
function screenShake(duration = 400) {
const app = document.getElementById('app');
if (!app)
return;
app.classList.add('shake');
setTimeout(() => app.classList.remove('shake'), duration);
}
// === FLASH EFFECT ===
function flashScreen(color = '#fff', duration = 300) {
const flash = document.createElement('div');
flash.style.cssText = `
position: fixed; inset: 0; background: ${color};
opacity: 0.3; pointer-events: none; z-index: 100;
transition: opacity ${duration}ms ease-out;
`;
document.body.appendChild(flash);
requestAnimationFrame(() => {
flash.style.opacity = '0';
setTimeout(() => flash.remove(), duration);
});
}
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>