Aethelgard
Qwen 3.6 27b MTP · 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 130,876 bytes · SHA-256 bb056616e3ed
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Aethelgard: Resonant Chronicles</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Press+Start+2P&display=swap">
<style>
/* ── css: base ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100%; height: 100%;
overflow: hidden;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: linear-gradient(135deg, #0f0a1e 0%, #1a0a2e 30%, #0d1b2a 60%, #0a0a14 100%);
color: #e2e8f0;
}
body::before {
content: '';
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background:
radial-gradient(ellipse at 20% 50%, rgba(120, 40, 200, 0.08) 0%, transparent 50%),
radial-gradient(ellipse at 80% 20%, rgba(60, 100, 200, 0.06) 0%, transparent 50%),
radial-gradient(ellipse at 50% 80%, rgba(40, 180, 120, 0.05) 0%, transparent 50%);
pointer-events: none;
z-index: 0;
}
#app {
position: relative;
width: 100%; height: 100%;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
.game-screen {
display: none;
width: 100%; height: 100%;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
animation: fadeIn 0.4s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.glass-card {
background: rgba(255, 255, 255, 0.04);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
padding: 20px;
}
button {
font-family: 'Inter', sans-serif;
cursor: pointer;
border: none;
outline: none;
transition: all 0.2s ease;
}
button:hover:not(:disabled) {
transform: translateY(-1px);
filter: brightness(1.1);
}
button:active:not(:disabled) {
transform: translateY(0);
}
button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; }
/* ── css: menu ── */
.menu-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 30px;
}
.title-section {
text-align: center;
margin-bottom: 20px;
}
.game-title {
font-family: 'Press Start 2P', monospace;
font-size: clamp(24px, 5vw, 48px);
background: linear-gradient(135deg, #C77DFF, #4CC9F0, #52B788);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: none;
letter-spacing: 4px;
line-height: 1.3;
}
.subtitle {
font-size: clamp(12px, 2vw, 18px);
color: rgba(255, 255, 255, 0.5);
margin-top: 8px;
letter-spacing: 6px;
text-transform: uppercase;
}
.menu-buttons {
display: flex;
flex-direction: column;
gap: 12px;
width: min(300px, 90vw);
}
.menu-btn {
padding: 14px 28px;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
letter-spacing: 1px;
}
.menu-btn.primary {
background: linear-gradient(135deg, rgba(199, 125, 255, 0.3), rgba(76, 201, 240, 0.3));
border: 1px solid rgba(199, 125, 255, 0.4);
color: #fff;
}
.menu-btn.primary:hover {
background: linear-gradient(135deg, rgba(199, 125, 255, 0.5), rgba(76, 201, 240, 0.5));
border-color: rgba(199, 125, 255, 0.7);
}
.menu-btn.secondary {
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.7);
}
.menu-btn.secondary:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.2);
}
.menu-btn.danger {
background: rgba(239, 68, 68, 0.15);
border: 1px solid rgba(239, 68, 68, 0.3);
color: #fca5a5;
}
.menu-btn.danger:hover {
background: rgba(239, 68, 68, 0.25);
}
.version-text {
font-size: 11px;
color: rgba(255, 255, 255, 0.25);
margin-top: 30px;
}
/* ── css: map ── */
.map-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
width: min(700px, 95vw);
height: min(80vh, 700px);
}
.map-header {
text-align: center;
padding: 16px 24px;
}
.map-header h2 {
font-family: 'Press Start 2P', monospace;
font-size: clamp(10px, 2vw, 16px);
color: #C77DFF;
margin-bottom: 8px;
}
.zone-desc {
font-size: 13px;
color: rgba(255, 255, 255, 0.5);
line-height: 1.4;
}
.zone-info {
display: flex;
gap: 16px;
margin-top: 10px;
font-size: 12px;
color: rgba(255, 255, 255, 0.4);
}
.map-path {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 8px;
padding: 16px;
max-width: 100%;
overflow-y: auto;
flex: 1;
}
.map-node {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 10px 8px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
min-width: 70px;
transition: all 0.2s ease;
}
.map-node:not(.locked):hover {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(199, 125, 255, 0.3);
transform: translateY(-2px);
}
.map-node.current {
background: rgba(199, 125, 255, 0.15);
border-color: rgba(199, 125, 255, 0.4);
cursor: pointer;
animation: pulseNode 2s ease-in-out infinite;
}
@keyframes pulseNode {
0%, 100% { box-shadow: 0 0 0 0 rgba(199, 125, 255, 0.3); }
50% { box-shadow: 0 0 12px 4px rgba(199, 125, 255, 0.15); }
}
.map-node.completed {
opacity: 0.6;
}
.map-node.locked {
opacity: 0.3;
cursor: not-allowed;
}
.node-icon {
font-size: 24px;
}
.node-label {
font-size: 9px;
color: rgba(255, 255, 255, 0.6);
text-align: center;
line-height: 1.2;
}
.map-actions {
display: flex;
gap: 8px;
padding: 12px;
flex-wrap: wrap;
justify-content: center;
}
.action-btn {
padding: 8px 16px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.7);
font-size: 13px;
}
.action-btn:hover {
background: rgba(255, 255, 255, 0.12);
border-color: rgba(255, 255, 255, 0.2);
}
.back-btn {
margin-top: 16px;
}
/* ── css: battle ── */
.battle-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
width: min(700px, 95vw);
height: min(85vh, 800px);
}
.enemy-side, .player-side {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 0 10px;
gap: 12px;
}
.enemy-side { flex-direction: row-reverse; }
.resonant-card {
min-width: 180px;
max-width: 240px;
padding: 14px;
}
.resonant-info {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.resonant-info h3 {
font-size: 15px;
font-weight: 600;
}
.level-badge {
font-size: 11px;
background: rgba(255, 255, 255, 0.1);
padding: 2px 8px;
border-radius: 10px;
color: rgba(255, 255, 255, 0.6);
}
.status-badge {
font-size: 10px;
background: rgba(239, 68, 68, 0.2);
border: 1px solid rgba(239, 68, 68, 0.4);
padding: 2px 6px;
border-radius: 6px;
color: #fca5a5;
}
.hp-bar-container {
width: 100%;
height: 8px;
background: rgba(255, 255, 255, 0.08);
border-radius: 4px;
overflow: hidden;
}
.hp-bar {
height: 100%;
background: var(--hp-color, #22c55e);
border-radius: 4px;
transition: width 0.5s ease, background 0.3s ease;
}
.xp-bar-container {
width: 100%;
height: 4px;
background: rgba(255, 255, 255, 0.06);
border-radius: 2px;
overflow: hidden;
margin-top: 4px;
}
.xp-bar {
height: 100%;
background: linear-gradient(90deg, #C77DFF, #4CC9F0);
border-radius: 2px;
transition: width 0.5s ease;
}
.hp-text {
font-size: 11px;
color: rgba(255, 255, 255, 0.4);
margin-top: 4px;
text-align: right;
}
.stats-row {
display: flex;
justify-content: space-between;
font-size: 11px;
color: rgba(255, 255, 255, 0.3);
margin-top: 4px;
}
.resonant-sprite {
display: flex;
align-items: center;
justify-content: center;
min-width: 100px;
}
.enemy-sprite svg, .player-sprite svg {
filter: drop-shadow(0 0 20px rgba(199, 125, 255, 0.3));
animation: floatSprite 3s ease-in-out infinite;
}
@keyframes floatSprite {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
.battle-log {
width: 100%;
min-height: 80px;
max-height: 120px;
overflow-y: auto;
padding: 14px;
}
.battle-msg {
font-size: 13px;
line-height: 1.5;
color: rgba(255, 255, 255, 0.7);
animation: msgFadeIn 0.3s ease-out;
}
@keyframes msgFadeIn {
from { opacity: 0; transform: translateX(-10px); }
to { opacity: 1; transform: translateX(0); }
}
.battle-actions {
width: 100%;
display: flex;
flex-direction: column;
gap: 8px;
}
.action-tabs {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.tab-btn {
padding: 8px 14px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.7);
font-size: 13px;
}
.tab-btn.active {
background: rgba(199, 125, 255, 0.2);
border-color: rgba(199, 125, 255, 0.4);
color: #fff;
}
.action-panel {
display: none;
flex-wrap: wrap;
gap: 6px;
padding: 8px;
}
.action-panel.active {
display: flex;
}
.move-btn {
flex: 1 1 calc(50% - 6px);
min-width: 140px;
padding: 10px 12px;
border-radius: 10px;
background: rgba(var(--type-rgb, 199, 125, 255), 0.1);
border: 1px solid var(--type-color, rgba(199, 125, 255, 0.3));
color: #fff;
text-align: left;
}
.move-btn:hover {
background: rgba(var(--type-rgb, 199, 125, 255), 0.2);
}
.move-name {
display: block;
font-size: 13px;
font-weight: 600;
}
.move-info {
display: block;
font-size: 10px;
color: rgba(255, 255, 255, 0.4);
margin-top: 2px;
}
.swap-btn, .inv-btn, .prism-btn {
padding: 8px 14px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.7);
font-size: 12px;
text-align: left;
}
.swap-btn:hover:not(:disabled), .inv-btn:hover, .prism-btn:hover {
background: rgba(255, 255, 255, 0.12);
}
.mini-hp-bar {
width: 100%;
height: 3px;
background: rgba(255, 255, 255, 0.06);
border-radius: 2px;
overflow: hidden;
margin-top: 4px;
}
.mini-hp-bar div {
height: 100%;
background: #22c55e;
border-radius: 2px;
}
.waiting-text {
text-align: center;
color: rgba(255, 255, 255, 0.3);
font-style: italic;
padding: 16px;
}
/* Shake animation for damage */
.shake {
animation: shakeAnim 0.3s ease-in-out;
}
@keyframes shakeAnim {
0%, 100% { transform: translateX(0); }
20% { transform: translateX(-8px) rotate(-1deg); }
40% { transform: translateX(6px) rotate(1deg); }
60% { transform: translateX(-4px); }
80% { transform: translateX(2px); }
}
/* Flash effect for hits */
@keyframes flashHit {
0% { filter: brightness(1); }
50% { filter: brightness(3) saturate(0.5); }
100% { filter: brightness(1); }
}
.hit-flash {
animation: flashHit 0.2s ease-out;
}
/* ── css: screens ── */
/* Party Screen */
.party-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
width: min(700px, 95vw);
height: min(85vh, 800px);
overflow-y: auto;
padding: 20px;
}
.party-container h2 {
font-family: 'Press Start 2P', monospace;
font-size: clamp(10px, 2vw, 14px);
color: #C77DFF;
}
.party-grid {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
}
.party-card {
display: flex;
align-items: center;
gap: 16px;
padding: 14px;
}
.party-sprite svg {
filter: drop-shadow(0 0 12px rgba(199, 125, 255, 0.3));
}
.party-info {
flex: 1;
}
.party-info h3 {
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
}
.type-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 6px;
font-size: 10px;
background: var(--badge-color, rgba(255,255,255,0.1));
color: #fff;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 4px;
font-size: 11px;
color: rgba(255, 255, 255, 0.4);
margin-top: 6px;
}
.moves-list {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 8px;
}
.move-tag {
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
background: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.5);
}
.move-tag.unlocked {
background: rgba(199, 125, 255, 0.15);
color: rgba(255, 255, 255, 0.8);
}
.inventory-grid {
display: flex;
flex-wrap: wrap;
gap: 6px;
width: 100%;
}
.inv-item {
padding: 6px 12px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.06);
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
}
/* Shop Screen */
.shop-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
width: min(700px, 95vw);
height: min(85vh, 800px);
overflow-y: auto;
padding: 20px;
}
.shop-container h2 {
font-family: 'Press Start 2P', monospace;
font-size: clamp(10px, 2vw, 14px);
color: #FFD700;
}
.shards-display {
font-size: 16px;
color: #FFD700;
font-weight: 600;
}
.shop-grid {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
}
.shop-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
}
.shop-info h4 {
font-size: 14px;
margin-bottom: 4px;
}
.shop-info p {
font-size: 11px;
color: rgba(255, 255, 255, 0.4);
}
.buy-btn {
padding: 8px 16px;
border-radius: 8px;
background: linear-gradient(135deg, rgba(255, 215, 0, 0.2), rgba(255, 165, 0, 0.2));
border: 1px solid rgba(255, 215, 0, 0.3);
color: #FFD700;
font-size: 12px;
white-space: nowrap;
}
.buy-btn:hover:not(:disabled) {
background: linear-gradient(135deg, rgba(255, 215, 0, 0.3), rgba(255, 165, 0, 0.3));
}
/* Dex Screen */
.dex-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
width: min(700px, 95vw);
height: min(85vh, 800px);
overflow-y: auto;
padding: 20px;
}
.dex-container h2 {
font-family: 'Press Start 2P', monospace;
font-size: clamp(10px, 2vw, 14px);
color: #52B788;
}
.dex-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 10px;
width: 100%;
}
.dex-card {
text-align: center;
padding: 14px;
}
.dex-card.hidden {
opacity: 0.3;
}
.dex-sprite svg {
filter: drop-shadow(0 0 8px rgba(82, 183, 136, 0.3));
}
.dex-hidden {
font-family: 'Press Start 2P', monospace;
font-size: 24px;
color: rgba(255, 255, 255, 0.1);
padding: 20px;
}
.dex-card h4 {
font-size: 13px;
margin-top: 8px;
}
.dex-stats {
font-size: 10px;
color: rgba(255, 255, 255, 0.3);
margin-top: 6px;
}
/* Victory Screen */
.victory-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
text-align: center;
}
.victory-container h1 {
font-family: 'Press Start 2P', monospace;
font-size: clamp(16px, 3vw, 28px);
background: linear-gradient(135deg, #FFD700, #C77DFF, #4CC9F0);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.victory-container p {
font-size: 15px;
color: rgba(255, 255, 255, 0.6);
max-width: 400px;
line-height: 1.5;
}
.victory-stats {
text-align: left;
padding: 20px;
}
.victory-stats h3 {
font-size: 14px;
color: #C77DFF;
margin-bottom: 12px;
}
.victory-stats p {
font-size: 13px;
color: rgba(255, 255, 255, 0.5);
margin-bottom: 6px;
}
/* Notification */
.notification {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
padding: 12px 24px;
border-radius: 12px;
background: rgba(32, 32, 64, 0.9);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(199, 125, 255, 0.3);
color: #fff;
font-size: 14px;
z-index: 1000;
animation: notifSlideIn 0.3s ease-out, notifFadeOut 0.3s ease-in 2s forwards;
}
@keyframes notifSlideIn {
from { opacity: 0; transform: translateX(-50%) translateY(-20px); }
to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
@keyframes notifFadeOut {
from { opacity: 1; }
to { opacity: 0; pointer-events: none; }
}
/* Responsive adjustments */
@media (max-width: 600px) {
.enemy-side, .player-side {
flex-direction: column;
align-items: center;
}
.resonant-card {
min-width: unset;
width: 100%;
}
.move-btn {
flex: 1 1 100%;
}
}
/* ── css: particles ── */
/* Particle effects container */
.particles-container {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
pointer-events: none;
z-index: 999;
overflow: hidden;
}
.status-particle {
position: absolute;
border-radius: 50%;
animation: particleFloat 2s ease-out forwards;
opacity: 0.7;
}
@keyframes particleFloat {
0% { transform: translateY(0) scale(1); opacity: 0.7; }
100% { transform: translateY(-60px) scale(0.3); opacity: 0; }
}
/* Burn particles - orange/red */
.particle-burn { background: radial-gradient(circle, #FF6B35, transparent); width: 8px; height: 8px; }
/* Paralyze particles - yellow */
.particle-paralyze { background: radial-gradient(circle, #FFD700, transparent); width: 6px; height: 6px; }
/* Poison particles - green */
.particle-poison { background: radial-gradient(circle, #22c55e, transparent); width: 7px; height: 7px; }
/* Sleep particles - purple */
.particle-sleep { background: radial-gradient(circle, #C77DFF, transparent); width: 5px; height: 5px; }
/* Ambient floating particles in background */
.ambient-particle {
position: fixed;
border-radius: 50%;
pointer-events: none;
animation: ambientFloat linear infinite;
}
@keyframes ambientFloat {
0% { transform: translateY(100vh) rotate(0deg); opacity: 0; }
10% { opacity: 0.3; }
90% { opacity: 0.3; }
100% { transform: translateY(-20px) rotate(360deg); opacity: 0; }
}
/* Evolution animation */
@keyframes evolveFlash {
0%, 100% { filter: brightness(1); }
50% { filter: brightness(2.5) saturate(1.5); }
}
.evolve-flash {
animation: evolveFlash 0.8s ease-in-out 3;
}
/* Capture success glow */
@keyframes captureGlow {
0%, 100% { box-shadow: 0 0 20px rgba(199, 125, 255, 0.3); }
50% { box-shadow: 0 0 40px rgba(199, 125, 255, 0.6), 0 0 60px rgba(76, 201, 240, 0.3); }
}
.capture-glow {
animation: captureGlow 1s ease-in-out;
}
/* Level up text animation */
@keyframes levelUpText {
0% { transform: scale(0.5) translateY(10px); opacity: 0; }
50% { transform: scale(1.2) translateY(-5px); opacity: 1; }
100% { transform: scale(1) translateY(0); opacity: 0; }
}
.level-up-text {
position: absolute;
font-family: 'Press Start 2P', monospace;
font-size: 14px;
color: #FFD700;
text-shadow: 0 0 10px rgba(255, 215, 0, 0.5);
animation: levelUpText 1.5s ease-out forwards;
pointer-events: none;
}
/* Zone transition overlay */
.zone-transition {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
animation: fadeIn 0.5s ease-out;
}
.zone-transition h2 {
font-family: 'Press Start 2P', monospace;
font-size: clamp(12px, 3vw, 24px);
color: #C77DFF;
text-align: center;
}
/* Improved HP bar glow */
.hp-bar-container {
position: relative;
}
.hp-bar::after {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
animation: hpShimmer 2s ease-in-out infinite;
}
@keyframes hpShimmer {
0%, 100% { transform: translateX(-100%); }
50% { transform: translateX(100%); }
}
/* Boss node special styling */
.map-node[data-index]:has(.node-icon) .node-icon {
font-size: 28px;
}
.map-node.current .node-icon {
animation: bossPulse 1.5s ease-in-out infinite;
}
@keyframes bossPulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.2); }
}
/* Improved glass card hover */
.glass-card:hover {
border-color: rgba(255, 255, 255, 0.12);
}
</style>
</head>
<body>
<div id="app"></div>
<div class="notification" style="display:none"></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./engine/game":"src/engine/game.ts","./ui/renderer":"src/ui/renderer.ts"},"src/data/resonants.ts":{"../types":"src/types.ts"},"src/data/zones.ts":{"../types":"src/types.ts"},"src/state/player.ts":{"../types":"src/types.ts","../data/resonants":"src/data/resonants.ts"},"src/engine/combat.ts":{"../types":"src/types.ts","../data/resonants":"src/data/resonants.ts","../state/player":"src/state/player.ts"},"src/ui/renderer.ts":{"../types":"src/types.ts","../data/resonants":"src/data/resonants.ts"},"src/engine/game.ts":{"../types":"src/types.ts","../data/zones":"src/data/zones.ts","../data/resonants":"src/data/resonants.ts","../state/player":"src/state/player.ts","./sound":"src/engine/sound.ts","./combat":"src/engine/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 game_1 = require("./engine/game");
// Expose renderer functions for game engine to use
const Renderer = __importStar(require("./ui/renderer"));
window.__generateResonantSVG = Renderer.generateResonantSVG;
// Start the game when DOM is ready
(0, game_1.initGame)();
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
};
// ── module: src/data/resonants.ts ──
__mods["src/data/resonants.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ALL_RESONANT_IDS = exports.RESONANT_DB = void 0;
exports.getResonantById = getResonantById;
const pyroMoves = [
{ name: 'Ember Toss', power: 40, accuracy: 100, element: 'Pyro', unlockLevel: 1 },
{ name: 'Flame Burst', power: 65, accuracy: 95, element: 'Pyro', unlockLevel: 3 },
{ name: 'Inferno Wave', power: 90, accuracy: 85, element: 'Pyro', unlockLevel: 7 },
{ name: 'Solar Flare', power: 120, accuracy: 70, element: 'Pyro', unlockLevel: 13 },
];
const hydroMoves = [
{ name: 'Splash Strike', power: 40, accuracy: 100, element: 'Hydro', unlockLevel: 1 },
{ name: 'Tidal Surge', power: 65, accuracy: 95, element: 'Hydro', unlockLevel: 3 },
{ name: 'Aqua Cannon', power: 90, accuracy: 85, element: 'Hydro', unlockLevel: 7 },
{ name: 'Tsunami Blast', power: 120, accuracy: 70, element: 'Hydro', unlockLevel: 13 },
];
const floraMoves = [
{ name: 'Vine Whip', power: 40, accuracy: 100, element: 'Flora', unlockLevel: 1 },
{ name: 'Thorn Barrage', power: 65, accuracy: 95, element: 'Flora', unlockLevel: 3 },
{ name: 'Root Snare', power: 80, accuracy: 90, element: 'Flora', unlockLevel: 7 },
{ name: 'Verdant Storm', power: 120, accuracy: 70, element: 'Flora', unlockLevel: 13 },
];
const voltMoves = [
{ name: 'Spark Zap', power: 40, accuracy: 100, element: 'Volt', unlockLevel: 1 },
{ name: 'Thunder Arc', power: 65, accuracy: 95, element: 'Volt', unlockLevel: 3 },
{ name: 'Lightning Bolt', power: 90, accuracy: 85, element: 'Volt', unlockLevel: 7 },
{ name: 'Thunderstorm', power: 120, accuracy: 70, element: 'Volt', unlockLevel: 13 },
];
const zephyrMoves = [
{ name: 'Gust Breeze', power: 40, accuracy: 100, element: 'Zephyr', unlockLevel: 1 },
{ name: 'Wind Slash', power: 65, accuracy: 95, element: 'Zephyr', unlockLevel: 3 },
{ name: 'Cyclone Spin', power: 85, accuracy: 90, element: 'Zephyr', unlockLevel: 7 },
{ name: 'Tempest Fury', power: 120, accuracy: 70, element: 'Zephyr', unlockLevel: 13 },
];
const earthMoves = [
{ name: 'Rock Throw', power: 40, accuracy: 100, element: 'Earth', unlockLevel: 1 },
{ name: 'Stone Slide', power: 65, accuracy: 95, element: 'Earth', unlockLevel: 3 },
{ name: 'Quake Slam', power: 85, accuracy: 90, element: 'Earth', unlockLevel: 7 },
{ name: 'Mountain Crush', power: 120, accuracy: 70, element: 'Earth', unlockLevel: 13 },
];
const cosmosMoves = [
{ name: 'Void Pulse', power: 45, accuracy: 100, element: 'Cosmos', unlockLevel: 1 },
{ name: 'Star Beam', power: 70, accuracy: 95, element: 'Cosmos', unlockLevel: 3 },
{ name: 'Nebula Burst', power: 85, accuracy: 90, element: 'Cosmos', unlockLevel: 7 },
{ name: 'Supernova', power: 120, accuracy: 70, element: 'Cosmos', unlockLevel: 13 },
];
const physicalMoves = [
{ name: 'Tackle', power: 40, accuracy: 100, element: 'Cosmos', unlockLevel: 1 },
{ name: 'Quick Strike', power: 55, accuracy: 95, element: 'Cosmos', unlockLevel: 3 },
{ name: 'Power Slam', power: 75, accuracy: 90, element: 'Cosmos', unlockLevel: 6 },
];
exports.RESONANT_DB = {
// === PYRO LINE ===
emberkit: {
id: 'emberkit', name: 'Emberkit', type: 'Pyro',
baseHp: 45, baseAtk: 52, baseDef: 38, baseSpd: 48,
evolutionLevel: 8, evolutionId: 'pyrowolf',
moves: [pyroMoves[0], pyroMoves[1], physicalMoves[1]],
spriteParams: { bodyShape: 'round', primaryColor: '#FF6B35', secondaryColor: '#FF9F1C', accentColor: '#FFE082', glowColor: '#FF4500', features: ['small-tail-flame', 'pointy-ears'] },
},
pyrowolf: {
id: 'pyrowolf', name: 'Pyrowolf', type: 'Pyro',
baseHp: 72, baseAtk: 78, baseDef: 56, baseSpd: 68,
evolutionLevel: 15, evolutionId: 'ignisaur',
moves: [pyroMoves[0], pyroMoves[1], pyroMoves[2], physicalMoves[2]],
spriteParams: { bodyShape: 'angular', primaryColor: '#E63946', secondaryColor: '#FF6B35', accentColor: '#FFE082', glowColor: '#FF4500', features: ['wolf-snout', 'flame-manes'] },
},
ignisaur: {
id: 'ignisaur', name: 'Ignisaur', type: 'Pyro',
baseHp: 105, baseAtk: 110, baseDef: 82, baseSpd: 95,
moves: [pyroMoves[0], pyroMoves[1], pyroMoves[2], pyroMoves[3]],
spriteParams: { bodyShape: 'angular', primaryColor: '#D62828', secondaryColor: '#E63946', accentColor: '#FFB703', glowColor: '#FF4500', features: ['dragon-horns', 'flame-crown'] },
},
// === HYDRO LINE ===
droplet: {
id: 'droplet', name: 'Droplet', type: 'Hydro',
baseHp: 50, baseAtk: 42, baseDef: 48, baseSpd: 55,
evolutionLevel: 8, evolutionId: 'geoxus',
moves: [hydroMoves[0], hydroMoves[1], physicalMoves[0]],
spriteParams: { bodyShape: 'round', primaryColor: '#4CC9F0', secondaryColor: '#4361EE', accentColor: '#A8DADC', glowColor: '#00BFFF', features: ['water-droplet-body', 'big-eyes'] },
},
geoxus: {
id: 'geoxus', name: 'Geoxus', type: 'Hydro',
baseHp: 78, baseAtk: 65, baseDef: 72, baseSpd: 58,
evolutionLevel: 15, evolutionId: 'neptunian',
moves: [hydroMoves[0], hydroMoves[1], hydroMoves[2], physicalMoves[2]],
spriteParams: { bodyShape: 'elongated', primaryColor: '#3A86FF', secondaryColor: '#4CC9F0', accentColor: '#A8DADC', glowColor: '#00BFFF', features: ['serpent-body', 'water-crest'] },
},
neptunian: {
id: 'neptunian', name: 'Neptunian', type: 'Hydro',
baseHp: 115, baseAtk: 98, baseDef: 100, baseSpd: 72,
moves: [hydroMoves[0], hydroMoves[1], hydroMoves[2], hydroMoves[3]],
spriteParams: { bodyShape: 'elongated', primaryColor: '#1D3557', secondaryColor: '#457B9D', accentColor: '#A8DADC', glowColor: '#00BFFF', features: ['sea-king-crown', 'trident-tail'] },
},
// === FLORA LINE ===
seedling: {
id: 'seedling', name: 'Seedling', type: 'Flora',
baseHp: 48, baseAtk: 40, baseDef: 52, baseSpd: 35,
evolutionLevel: 8, evolutionId: 'thornback',
moves: [floraMoves[0], floraMoves[1], physicalMoves[0]],
spriteParams: { bodyShape: 'round', primaryColor: '#2D6A4F', secondaryColor: '#52B788', accentColor: '#95D5B2', glowColor: '#00FF7F', features: ['leaf-sprout', 'bulb-body'] },
},
thornback: {
id: 'thornback', name: 'Thornback', type: 'Flora',
baseHp: 80, baseAtk: 68, baseDef: 78, baseSpd: 45,
evolutionLevel: 15, evolutionId: 'bloomgoliath',
moves: [floraMoves[0], floraMoves[1], floraMoves[2], physicalMoves[2]],
spriteParams: { bodyShape: 'angular', primaryColor: '#1B4332', secondaryColor: '#2D6A4F', accentColor: '#74C69D', glowColor: '#00FF7F', features: ['thorn-armor', 'flower-back'] },
},
bloomgoliath: {
id: 'bloomgoliath', name: 'Bloomgoliath', type: 'Flora',
baseHp: 125, baseAtk: 95, baseDef: 110, baseSpd: 55,
moves: [floraMoves[0], floraMoves[1], floraMoves[2], floraMoves[3]],
spriteParams: { bodyShape: 'angular', primaryColor: '#081C15', secondaryColor: '#1B4332', accentColor: '#B7E4C7', glowColor: '#00FF7F', features: ['giant-blossom', 'root-legs'] },
},
// === VOLT LINE ===
sparky: {
id: 'sparky', name: 'Sparky', type: 'Volt',
baseHp: 42, baseAtk: 50, baseDef: 35, baseSpd: 62,
evolutionLevel: 8, evolutionId: 'boltsaber',
moves: [voltMoves[0], voltMoves[1], physicalMoves[1]],
spriteParams: { bodyShape: 'round', primaryColor: '#FFD700', secondaryColor: '#FFA500', accentColor: '#FFFACD', glowColor: '#FFFF00', features: ['lightning-tail', 'electric-cheeks'] },
},
boltsaber: {
id: 'boltsaber', name: 'Boltsaber', type: 'Volt',
baseHp: 70, baseAtk: 82, baseDef: 55, baseSpd: 80,
evolutionLevel: 15, evolutionId: 'stormlord',
moves: [voltMoves[0], voltMoves[1], voltMoves[2], physicalMoves[2]],
spriteParams: { bodyShape: 'angular', primaryColor: '#FFC300', secondaryColor: '#FFD700', accentColor: '#FFFACD', glowColor: '#FFFF00', features: ['sabertooth', 'bolt-armor'] },
},
stormlord: {
id: 'stormlord', name: 'Stormlord', type: 'Volt',
baseHp: 108, baseAtk: 115, baseDef: 78, baseSpd: 105,
moves: [voltMoves[0], voltMoves[1], voltMoves[2], voltMoves[3]],
spriteParams: { bodyShape: 'angular', primaryColor: '#E6B800', secondaryColor: '#FFC300', accentColor: '#FFFACD', glowColor: '#FFFF00', features: ['storm-crown', 'lightning-wings'] },
},
// === ZEPHYR LINE ===
gusty: {
id: 'gusty', name: 'Gusty', type: 'Zephyr',
baseHp: 44, baseAtk: 45, baseDef: 38, baseSpd: 60,
evolutionLevel: 8, evolutionId: 'slipstream',
moves: [zephyrMoves[0], zephyrMoves[1], physicalMoves[0]],
spriteParams: { bodyShape: 'elongated', primaryColor: '#A2D2FF', secondaryColor: '#BDE0FE', accentColor: '#FFFFFF', glowColor: '#87CEEB', features: ['wind-wings', 'feather-body'] },
},
slipstream: {
id: 'slipstream', name: 'Slipstream', type: 'Zephyr',
baseHp: 74, baseAtk: 72, baseDef: 58, baseSpd: 82,
evolutionLevel: 15, evolutionId: 'zephyrus',
moves: [zephyrMoves[0], zephyrMoves[1], zephyrMoves[2], physicalMoves[2]],
spriteParams: { bodyShape: 'elongated', primaryColor: '#74C0FC', secondaryColor: '#A2D2FF', accentColor: '#FFFFFF', glowColor: '#87CEEB', features: ['streamlined-body', 'wind-trails'] },
},
zephyrus: {
id: 'zephyrus', name: 'Zephyrus', type: 'Zephyr',
baseHp: 102, baseAtk: 105, baseDef: 75, baseSpd: 118,
moves: [zephyrMoves[0], zephyrMoves[1], zephyrMoves[2], zephyrMoves[3]],
spriteParams: { bodyShape: 'elongated', primaryColor: '#4A90D9', secondaryColor: '#74C0FC', accentColor: '#FFFFFF', glowColor: '#87CEEB', features: ['sky-king-halo', 'tempest-wings'] },
},
// === EARTH LINE ===
pebble: {
id: 'pebble', name: 'Pebble', type: 'Earth',
baseHp: 52, baseAtk: 38, baseDef: 55, baseSpd: 28,
evolutionLevel: 8, evolutionId: 'obsidian',
moves: [earthMoves[0], earthMoves[1], physicalMoves[0]],
spriteParams: { bodyShape: 'round', primaryColor: '#8B7355', secondaryColor: '#A0926B', accentColor: '#D4C5A9', glowColor: '#CD853F', features: ['rock-body', 'crystal-eyes'] },
},
obsidian: {
id: 'obsidian', name: 'Obsidian', type: 'Earth',
baseHp: 82, baseAtk: 70, baseDef: 85, baseSpd: 38,
evolutionLevel: 15, evolutionId: 'terrasunder',
moves: [earthMoves[0], earthMoves[1], earthMoves[2], physicalMoves[2]],
spriteParams: { bodyShape: 'angular', primaryColor: '#4A4E69', secondaryColor: '#8B7355', accentColor: '#C9ADA7', glowColor: '#CD853F', features: ['crystal-armor', 'stone-claws'] },
},
terrasunder: {
id: 'terrasunder', name: 'Terrasunder', type: 'Earth',
baseHp: 120, baseAtk: 98, baseDef: 125, baseSpd: 48,
moves: [earthMoves[0], earthMoves[1], earthMoves[2], earthMoves[3]],
spriteParams: { bodyShape: 'angular', primaryColor: '#2B2D42', secondaryColor: '#4A4E69', accentColor: '#D4A574', glowColor: '#CD853F', features: ['mountain-horns', 'earth-shield'] },
},
// === BOSS RESONANTS (Cosmos type) ===
canopy_guardian: {
id: 'canopy_guardian', name: 'Canopy Guardian', type: 'Flora',
baseHp: 130, baseAtk: 85, baseDef: 90, baseSpd: 60,
moves: [floraMoves[2], floraMoves[3], cosmosMoves[2], physicalMoves[2]],
spriteParams: { bodyShape: 'angular', primaryColor: '#1B4332', secondaryColor: '#52B788', accentColor: '#FFD700', glowColor: '#00FF7F', features: ['ancient-tree-form', 'golden-eyes'] },
},
grotto_sentinel: {
id: 'grotto_sentinel', name: 'Grotto Sentinel', type: 'Hydro',
baseHp: 145, baseAtk: 90, baseDef: 100, baseSpd: 55,
moves: [hydroMoves[2], hydroMoves[3], cosmosMoves[2], physicalMoves[2]],
spriteParams: { bodyShape: 'elongated', primaryColor: '#1D3557', secondaryColor: '#4CC9F0', accentColor: '#FFD700', glowColor: '#00BFFF', features: ['crystal-armor', 'water-crown'] },
},
obsidian_lord: {
id: 'obsidian_lord', name: 'Obsidian Lord', type: 'Pyro',
baseHp: 155, baseAtk: 105, baseDef: 95, baseSpd: 70,
moves: [pyroMoves[2], pyroMoves[3], cosmosMoves[2], physicalMoves[2]],
spriteParams: { bodyShape: 'angular', primaryColor: '#D62828', secondaryColor: '#4A4E69', accentColor: '#FFD700', glowColor: '#FF4500', features: ['lava-veins', 'obsidian-crown'] },
},
apex_conductor: {
id: 'apex_conductor', name: 'Apex Conductor', type: 'Cosmos',
baseHp: 180, baseAtk: 125, baseDef: 110, baseSpd: 95,
moves: [cosmosMoves[2], cosmosMoves[3], pyroMoves[3], hydroMoves[3]],
spriteParams: { bodyShape: 'angular', primaryColor: '#7B2D8E', secondaryColor: '#C77DFF', accentColor: '#FFFFFF', glowColor: '#BF00FF', features: ['void-crown', 'cosmic-aura'] },
},
// === WANDERING CONDUCTOR RESONANTS (special variants) ===
wild_pyrowolf: {
id: 'wild_pyrowolf', name: 'Pyrowolf', type: 'Pyro',
baseHp: 72, baseAtk: 78, baseDef: 56, baseSpd: 68,
moves: [pyroMoves[0], pyroMoves[1], pyroMoves[2]],
spriteParams: { bodyShape: 'angular', primaryColor: '#E63946', secondaryColor: '#FF6B35', accentColor: '#FFE082', glowColor: '#FF4500', features: ['wolf-snout', 'flame-manes'] },
},
};
function getResonantById(id) {
return exports.RESONANT_DB[id];
}
exports.ALL_RESONANT_IDS = Object.keys(exports.RESONANT_DB);
};
// ── module: src/data/zones.ts ──
__mods["src/data/zones.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SHOP_ITEMS = exports.CACHE_REWARDS = exports.WILD_ENCOUNTER_TABLES = exports.BOSS_IDS = exports.CONDUCTOR_ROSTERS = exports.ZONES = void 0;
exports.ZONES = [
{
id: 'verdant_canopy',
name: 'The Verdant Canopy',
description: 'A lush forest canopy where Flora and Zephyr Resonants thrive among ancient trees.',
levelRange: [1, 5],
bgColor: '#0a2e1f',
accentColor: '#52B788',
nodes: [
{ type: 'wild', label: 'Tall Grass' },
{ type: 'cache', label: 'Mystery Cache' },
{ type: 'spring', label: 'Healing Spring' },
{ type: 'wild', label: 'Dense Thicket' },
{ type: 'conductor', label: 'Conductor Elara' },
{ type: 'wild', label: 'Flower Meadow' },
{ type: 'cache', label: 'Hidden Cache' },
{ type: 'spring', label: 'Crystal Spring' },
{ type: 'wild', label: 'Ancient Grove' },
{ type: 'conductor', label: 'Conductor Finn' },
{ type: 'wild', label: 'Canopy Edge' },
{ type: 'boss', label: '🌿 Canopy Guardian' },
],
},
{
id: 'sunken_grotto',
name: 'The Sunken Grotto',
description: 'Underground caverns filled with underground rivers and crystalline formations.',
levelRange: [5, 10],
bgColor: '#0a1628',
accentColor: '#4CC9F0',
nodes: [
{ type: 'wild', label: 'Crystal Cavern' },
{ type: 'spring', label: 'Underground Spring' },
{ type: 'cache', label: 'Grotto Cache' },
{ type: 'conductor', label: 'Conductor Mira' },
{ type: 'wild', label: 'Deep Pool' },
{ type: 'wild', label: 'Stalactite Hall' },
{ type: 'cache', label: 'Crystal Cache' },
{ type: 'spring', label: 'Healing Waters' },
{ type: 'conductor', label: 'Conductor Rex' },
{ type: 'wild', label: 'Abyssal Chamber' },
{ type: 'wild', label: 'Echoing Depths' },
{ type: 'boss', label: '💧 Grotto Sentinel' },
],
},
{
id: 'obsidian_crags',
name: 'The Obsidian Crags',
description: 'Volcanic peaks where Pyro and Volt Resonants dance among lightning storms.',
levelRange: [10, 15],
bgColor: '#1a0a0a',
accentColor: '#FF6B35',
nodes: [
{ type: 'wild', label: 'Lava Fields' },
{ type: 'cache', label: 'Volcanic Cache' },
{ type: 'conductor', label: 'Conductor Zara' },
{ type: 'spring', label: 'Hot Spring' },
{ type: 'wild', label: 'Thunder Ridge' },
{ type: 'wild', label: 'Obsidian Path' },
{ type: 'cache', label: 'Magma Cache' },
{ type: 'conductor', label: 'Conductor Kai' },
{ type: 'spring', label: 'Steam Spring' },
{ type: 'wild', label: 'Storm Peak' },
{ type: 'wild', label: 'Crater Edge' },
{ type: 'boss', label: '🔥 Obsidian Lord' },
],
},
{
id: 'apex_pillar',
name: 'The Apex Pillar',
description: 'A cosmic tower reaching into the void. Only the strongest Conductors dare enter.',
levelRange: [15, 20],
bgColor: '#0d0a1f',
accentColor: '#C77DFF',
nodes: [
{ type: 'wild', label: 'Void Chamber I' },
{ type: 'spring', label: 'Cosmic Spring' },
{ type: 'conductor', label: 'Conductor Nova' },
{ type: 'wild', label: 'Star Hall' },
{ type: 'cache', label: 'Astral Cache' },
{ type: 'conductor', label: 'Conductor Orion' },
{ type: 'wild', label: 'Nebula Room' },
{ type: 'spring', label: 'Void Spring' },
{ type: 'conductor', label: 'Conductor Lyra' },
{ type: 'wild', label: 'Event Horizon' },
{ type: 'boss', label: '🌌 Apex Conductor' },
],
},
];
exports.CONDUCTOR_ROSTERS = [
// Zone 0 conductors (Elara, Finn)
[{ templateId: 'seedling', level: 3 }, { templateId: 'gusty', level: 4 }],
[{ templateId: 'thornback', level: 5 }, { templateId: 'slipstream', level: 5 }],
// Zone 1 conductors (Mira, Rex)
[{ templateId: 'droplet', level: 7 }, { templateId: 'pebble', level: 6 }],
[{ templateId: 'geoxus', level: 9 }, { templateId: 'obsidian', level: 8 }],
// Zone 2 conductors (Zara, Kai)
[{ templateId: 'emberkit', level: 11 }, { templateId: 'sparky', level: 10 }],
[{ templateId: 'pyrowolf', level: 13 }, { templateId: 'boltsaber', level: 12 }],
// Zone 3 conductors (Nova, Orion, Lyra)
[{ templateId: 'ignisaur', level: 15 }, { templateId: 'neptunian', level: 14 }],
[{ templateId: 'stormlord', level: 16 }, { templateId: 'zephyrus', level: 15 }],
[{ templateId: 'bloomgoliath', level: 17 }, { templateId: 'terrasunder', level: 16 }],
];
exports.BOSS_IDS = ['canopy_guardian', 'grotto_sentinel', 'obsidian_lord', 'apex_conductor'];
exports.WILD_ENCOUNTER_TABLES = {
0: ['seedling', 'gusty', 'emberkit', 'droplet', 'pebble'],
1: ['geoxus', 'thornback', 'slipstream', 'obsidian', 'sparky'],
2: ['pyrowolf', 'boltsaber', 'ignisaur', 'stormlord', 'zephyrus'],
3: ['neptunian', 'bloomgoliath', 'terrasunder', 'zephyrus', 'stormlord'],
};
exports.CACHE_REWARDS = [
{ type: 'shards', amount: 25 },
{ type: 'item', itemId: 'vitals_elixir', quantity: 1 },
{ type: 'item', itemId: 'status_cleanser', quantity: 1 },
{ type: 'shards', amount: 40 },
{ type: 'item', itemId: 'prism_boost', quantity: 2 },
];
exports.SHOP_ITEMS = [
{ id: 'harmonizer_prism', name: 'Harmonizer Prism', description: 'Basic capture prism. Moderate catch rate.', price: 30, itemTypeId: 'prism', catchRate: 0.45 },
{ id: 'hyper_conductive_prism', name: 'Hyper-Conductive Prism', description: 'Advanced prism with high catch rate.', price: 80, itemTypeId: 'prism', catchRate: 0.75 },
{ id: 'core_restorer', name: 'Core Restorer', description: 'Restores 50% of a Resonant\'s HP.', price: 20, itemTypeId: 'heal' },
{ id: 'resonance_elixir', name: 'Resonance Elixir', description: 'Heals all status conditions on one Resonant.', price: 35, itemTypeId: 'status' },
{ id: 'vitals_elixir', name: 'Vitals Elixir', description: 'Restores a small amount of HP in battle.', price: 15, itemTypeId: 'heal' },
{ id: 'prism_boost', name: 'Prism Booster', description: 'Doubles the next prism\'s catch rate.', price: 25, itemTypeId: 'boost' },
];
};
// ── module: src/engine/sound.ts ──
__mods["src/engine/sound.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.soundManager = exports.SoundManager = void 0;
class SoundManager {
constructor() {
this.ctx = null;
this.enabled = true;
}
init() {
if (!this.ctx) {
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
}
if (this.ctx.state === 'suspended') {
this.ctx.resume();
}
}
playTone(freq, duration, type = 'square', volume = 0.15) {
if (!this.ctx || !this.enabled)
return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, 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.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + duration);
}
playNoise(duration, volume = 0.1) {
if (!this.ctx || !this.enabled)
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 source = this.ctx.createBufferSource();
source.buffer = buffer;
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(volume, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + duration);
source.connect(gain);
gain.connect(this.ctx.destination);
source.start();
}
hover() {
this.playTone(800, 0.05, 'sine', 0.06);
}
select() {
this.playTone(523, 0.08, 'square', 0.1);
setTimeout(() => this.playTone(659, 0.08, 'square', 0.1), 60);
}
hitPhysical() {
this.playNoise(0.12, 0.15);
this.playTone(150, 0.1, 'sawtooth', 0.12);
}
hitElemental() {
this.playTone(300, 0.08, 'square', 0.12);
setTimeout(() => this.playTone(450, 0.06, 'square', 0.1), 50);
setTimeout(() => this.playNoise(0.08, 0.08), 30);
}
hitSuperEffective() {
this.playTone(400, 0.06, 'square', 0.15);
setTimeout(() => this.playTone(600, 0.06, 'square', 0.15), 40);
setTimeout(() => this.playTone(800, 0.12, 'square', 0.15), 80);
setTimeout(() => this.playNoise(0.1, 0.12), 60);
}
levelUp() {
const notes = [523, 587, 659, 784, 880, 1047];
notes.forEach((freq, i) => {
setTimeout(() => this.playTone(freq, 0.15, 'square', 0.12), i * 80);
});
}
captureSuccess() {
const notes = [392, 440, 523, 659, 784];
notes.forEach((freq, i) => {
setTimeout(() => this.playTone(freq, 0.12, 'triangle', 0.1), i * 70);
});
}
captureFail() {
this.playTone(300, 0.15, 'sawtooth', 0.1);
setTimeout(() => this.playTone(200, 0.2, 'sawtooth', 0.1), 150);
}
battleStart() {
const notes = [262, 330, 392, 523];
notes.forEach((freq, i) => {
setTimeout(() => this.playTone(freq, 0.15, 'square', 0.1), i * 100);
});
}
victory() {
const melody = [523, 659, 784, 1047, 784, 1047];
melody.forEach((freq, i) => {
setTimeout(() => this.playTone(freq, 0.2, 'square', 0.1), i * 120);
});
}
defeat() {
const notes = [392, 349, 330, 262];
notes.forEach((freq, i) => {
setTimeout(() => this.playTone(freq, 0.3, 'sawtooth', 0.1), i * 200);
});
}
evolution() {
const notes = [440, 554, 659, 880, 1108];
notes.forEach((freq, i) => {
setTimeout(() => this.playTone(freq, 0.2, 'triangle', 0.12), i * 100);
});
}
toggle() {
this.enabled = !this.enabled;
}
}
exports.SoundManager = SoundManager;
exports.soundManager = new SoundManager();
};
// ── module: src/state/player.ts ──
__mods["src/state/player.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createResonant = createResonant;
exports.xpForLevel = xpForLevel;
exports.gainXp = gainXp;
exports.checkEvolution = checkEvolution;
exports.evolveResonant = evolveResonant;
exports.getTypeAdvantage = getTypeAdvantage;
exports.calculateDamage = calculateDamage;
exports.createDefaultPlayer = createDefaultPlayer;
exports.saveGame = saveGame;
exports.loadGame = loadGame;
exports.resetGame = resetGame;
const resonants_1 = require("../data/resonants");
const SAVE_KEY = 'aethelgard_save_v1';
function createResonant(templateId, level, name) {
const tmpl = (0, resonants_1.getResonantById)(templateId);
if (!tmpl)
throw new Error(`Unknown resonant template: ${templateId}`);
const xpBase = Math.floor(level * level * 1.5 + level * 2);
return {
templateId,
name: name || tmpl.name,
level,
currentHp: calcStat(tmpl.baseHp, level),
maxHp: calcStat(tmpl.baseHp, level),
atk: calcStat(tmpl.baseAtk, level),
def: calcStat(tmpl.baseDef, level),
spd: calcStat(tmpl.baseSpd, level),
xp: 0,
xpToNext: xpBase + 20,
statusEffect: null,
};
}
function calcStat(base, level) {
return Math.floor((base * 2 + 31) * level / 100 + (level >= 5 ? level : 5));
}
function xpForLevel(level) {
return Math.floor(level * level * 1.5 + level * 2);
}
function gainXp(r, amount) {
r.xp += amount;
let leveledUp = false;
while (r.xp >= r.xpToNext) {
r.xp -= r.xpToNext;
r.level++;
const tmpl = (0, resonants_1.getResonantById)(r.templateId);
if (!tmpl)
break;
const oldMaxHp = r.maxHp;
r.maxHp = calcStat(tmpl.baseHp, r.level);
r.atk = calcStat(tmpl.baseAtk, r.level);
r.def = calcStat(tmpl.baseDef, r.level);
r.spd = calcStat(tmpl.baseSpd, r.level);
const hpGain = r.maxHp - oldMaxHp;
r.currentHp += Math.max(hpGain, 10);
if (r.currentHp > r.maxHp)
r.currentHp = r.maxHp;
r.xpToNext = xpForLevel(r.level) + 20;
leveledUp = true;
}
return { leveledUp };
}
function checkEvolution(r) {
const tmpl = (0, resonants_1.getResonantById)(r.templateId);
if (!tmpl || !tmpl.evolutionLevel)
return null;
if (r.level >= tmpl.evolutionLevel && tmpl.evolutionId) {
return tmpl.evolutionId;
}
return null;
}
function evolveResonant(r, newTemplateId) {
const tmpl = (0, resonants_1.getResonantById)(newTemplateId);
if (!tmpl)
return;
r.templateId = newTemplateId;
r.name = tmpl.name;
r.maxHp = calcStat(tmpl.baseHp, r.level);
r.atk = calcStat(tmpl.baseAtk, r.level);
r.def = calcStat(tmpl.baseDef, r.level);
r.spd = calcStat(tmpl.baseSpd, r.level);
r.currentHp = r.maxHp; // Full heal on evolution
}
function getTypeAdvantage(attackerType, defenderType) {
const chart = {
Pyro: { Flora: 1.5, Hydro: 0.67 },
Hydro: { Pyro: 1.5, Flora: 0.67 },
Flora: { Hydro: 1.5, Pyro: 0.67 },
Zephyr: { Earth: 1.5, Volt: 0.67 },
Earth: { Volt: 1.5, Zephyr: 0.67 },
Volt: { Zephyr: 1.5, Earth: 0.67 },
Cosmos: {},
};
return chart[attackerType]?.[defenderType] ?? 1.0;
}
function calculateDamage(attacker, defender, movePower, moveElement) {
const typeMod = getTypeAdvantage(moveElement, (0, resonants_1.getResonantById)(defender.templateId)?.type ?? 'Cosmos');
const randomFactor = 0.85 + Math.random() * 0.15;
let damage = (((2 * attacker.level / 5 + 2) * movePower * (attacker.atk / defender.def)) / 50 + 2) * typeMod * randomFactor;
const isCrit = Math.random() < 0.1;
if (isCrit)
damage *= 1.5;
return { damage: Math.max(1, Math.floor(damage)), isCrit, typeMod };
}
function createDefaultPlayer() {
return {
playerName: 'Conductor',
party: [createResonant('emberkit', 3)],
inventory: [
{ id: 'harmonizer_prism', name: 'Harmonizer Prism', description: 'Basic capture prism.', quantity: 5, type: 'prism', catchRate: 0.45 },
{ id: 'vitals_elixir', name: 'Vitals Elixir', description: 'Restores small HP.', quantity: 3, type: 'heal' },
],
shards: 50,
unlockedZones: 1,
currentZone: 0,
currentNodeInZone: 0,
completedBosses: [],
dexSeen: new Set(),
};
}
function saveGame(state) {
try {
const serializable = JSON.stringify({
...state,
dexSeen: Array.from(state.dexSeen),
});
localStorage.setItem(SAVE_KEY, serializable);
}
catch (e) {
console.warn('Failed to save game:', e);
}
}
function loadGame() {
try {
const data = localStorage.getItem(SAVE_KEY);
if (!data)
return null;
const parsed = JSON.parse(data);
return {
...parsed,
dexSeen: new Set(parsed.dexSeen || []),
};
}
catch (e) {
console.warn('Failed to load game:', e);
return null;
}
}
function resetGame() {
localStorage.removeItem(SAVE_KEY);
}
};
// ── module: src/engine/combat.ts ──
__mods["src/engine/combat.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CombatEngine = void 0;
const resonants_1 = require("../data/resonants");
const player_1 = require("../state/player");
class CombatEngine {
constructor(battle) {
this.messages = [];
this.animating = false;
this.battle = battle;
}
getBattle() {
return this.battle;
}
isAnimating() {
return this.animating;
}
addMessage(msg) {
this.messages.push(msg);
}
clearMessages() {
this.messages = [];
}
getMessages() {
return [...this.messages];
}
getEnemyResonant() {
return this.battle.enemy;
}
async executePlayerAttack(moveIndex, party) {
const playerR = party[this.battle.activeIndex];
if (!playerR || this.animating)
return null;
const tmpl = (0, resonants_1.getResonantById)(playerR.templateId);
if (!tmpl)
return null;
const availableMoves = tmpl.moves.filter(m => m.unlockLevel <= playerR.level);
if (moveIndex >= availableMoves.length)
return null;
const move = availableMoves[moveIndex];
this.animating = true;
this.battle.turnPhase = 'animating';
// Accuracy check
if (Math.random() * 100 > move.accuracy) {
this.addMessage(`${playerR.name}'s ${move.name} missed!`);
await this.delay(800);
this.animating = false;
this.battle.turnPhase = 'enemy';
return null;
}
const result = (0, player_1.calculateDamage)(playerR, this.battle.enemy, move.power, move.element);
const typeModText = result.typeMod > 1 ? "It's super effective!" :
result.typeMod < 1 ? "It's not very effective..." : '';
let hitMsg = `${playerR.name} used ${move.name}!`;
if (result.isCrit)
hitMsg += ' Critical hit!';
if (typeModText)
hitMsg += ` ${typeModText}`;
this.addMessage(hitMsg);
await this.delay(600);
// Apply damage
this.battle.enemy.currentHp = Math.max(0, this.battle.enemy.currentHp - result.damage);
// Status effect chance for elemental moves
if (move.element !== 'Cosmos' && Math.random() < 0.15) {
const statusTypes = ['burn', 'paralyze', 'poison'];
this.battle.enemy.statusEffect = { type: statusTypes[Math.floor(Math.random() * statusTypes.length)], turnsLeft: 3 };
this.addMessage(`${this.battle.enemy.name} got a ${this.battle.enemy.statusEffect.type}!`);
}
await this.delay(500);
// Check if enemy fainted
if (this.battle.enemy.currentHp <= 0) {
this.animating = false;
const xpGained = Math.floor(this.battle.enemy.level * 15 + 20);
const shardsGained = Math.floor(this.battle.enemy.level * 8 + 10);
return { type: 'enemyDefeated', xpGained, shardsGained };
}
// Enemy turn
this.animating = false;
this.battle.turnPhase = 'enemy';
return null;
}
async executeEnemyTurn(party) {
const playerR = party[this.battle.activeIndex];
if (!playerR || this.animating)
return null;
// Status effect damage
if (this.battle.enemy.statusEffect && Math.random() < 0.33) {
const statusDmg = Math.floor(this.battle.enemy.maxHp * 0.1);
this.battle.enemy.currentHp = Math.max(0, this.battle.enemy.currentHp - statusDmg);
this.addMessage(`${this.battle.enemy.name} is hurt by its ${this.battle.enemy.statusEffect.type}!`);
if (this.battle.enemy.currentHp <= 0) {
const xpGained = Math.floor(this.battle.enemy.level * 15 + 20);
const shardsGained = Math.floor(this.battle.enemy.level * 8 + 10);
return { type: 'enemyDefeated', xpGained, shardsGained };
}
}
this.animating = true;
this.battle.turnPhase = 'animating';
const enemyTmpl = (0, resonants_1.getResonantById)(this.battle.enemy.templateId);
if (!enemyTmpl)
return null;
const availableMoves = enemyTmpl.moves.filter(m => m.unlockLevel <= this.battle.enemy.level);
// AI: prefer super-effective moves, otherwise random
let chosenMove = availableMoves[Math.floor(Math.random() * availableMoves.length)];
for (const move of availableMoves) {
if ((0, player_1.getTypeAdvantage)(move.element, (0, resonants_1.getResonantById)(playerR.templateId)?.type ?? 'Cosmos') > 1) {
chosenMove = move;
break;
}
}
const result = (0, player_1.calculateDamage)(this.battle.enemy, playerR, chosenMove.power, chosenMove.element);
const typeModText = result.typeMod > 1 ? "It's super effective!" :
result.typeMod < 1 ? "It's not very effective..." : '';
let hitMsg = `${this.battle.enemy.name} used ${chosenMove.name}!`;
if (result.isCrit)
hitMsg += ' Critical hit!';
if (typeModText)
hitMsg += ` ${typeModText}`;
this.addMessage(hitMsg);
await this.delay(600);
playerR.currentHp = Math.max(0, playerR.currentHp - result.damage);
// Status effect chance
if (chosenMove.element !== 'Cosmos' && Math.random() < 0.12) {
const statusTypes = ['burn', 'paralyze', 'poison'];
playerR.statusEffect = { type: statusTypes[Math.floor(Math.random() * statusTypes.length)], turnsLeft: 3 };
this.addMessage(`${playerR.name} got a ${playerR.statusEffect.type}!`);
}
await this.delay(500);
if (playerR.currentHp <= 0) {
// Check if player has other conscious resonants
const aliveParty = party.filter(r => r.currentHp > 0 && r !== playerR);
if (aliveParty.length === 0 || !this.battle.isWild) {
this.animating = false;
return { type: 'playerFainted' };
}
else {
// Auto-switch to next alive resonant
for (let i = 0; i < party.length; i++) {
if (party[i].currentHp > 0 && i !== this.battle.activeIndex) {
this.battle.activeIndex = i;
this.addMessage(`Go, ${party[i].name}!`);
await this.delay(500);
break;
}
}
}
}
// Player status effect damage on their turn start (handled at beginning of player turn)
if (playerR.statusEffect && Math.random() < 0.33) {
const statusDmg = Math.floor(playerR.maxHp * 0.1);
playerR.currentHp = Math.max(0, playerR.currentHp - statusDmg);
this.addMessage(`${playerR.name} is hurt by its ${playerR.statusEffect.type}!`);
}
this.animating = false;
this.battle.turnPhase = 'player';
return null;
}
async attemptCapture(party, prismCatchRate) {
if (this.animating)
return null;
this.animating = true;
this.battle.turnPhase = 'animating';
const playerR = party[this.battle.activeIndex];
if (!playerR)
return null;
this.addMessage(`${playerR.name} threw an Aether Prism!`);
await this.delay(800);
// Capture formula: based on HP ratio, status effects, and prism tier
const hpRatio = this.battle.enemy.currentHp / this.battle.enemy.maxHp;
let catchRate = prismCatchRate * (1 - hpRatio * 0.7);
if (this.battle.enemy.statusEffect)
catchRate *= 1.5;
// Multiple shakes for dramatic effect
const maxShakes = Math.min(4, Math.floor(catchRate * 8));
this.addMessage(`The Prism is shaking...`);
await this.delay(600);
let captured = false;
if (Math.random() < catchRate) {
captured = true;
}
else {
// Show shakes then break free
for (let i = 0; i < maxShakes && !captured; i++) {
await this.delay(500);
if (i === maxShakes - 1) {
captured = Math.random() < catchRate * 0.8;
}
else if (Math.random() > 0.6) {
// Continue shaking
}
}
}
await this.delay(400);
if (captured) {
const capturedR = { ...this.battle.enemy };
capturedR.currentHp = Math.max(1, Math.floor(capturedR.maxHp * 0.3));
capturedR.statusEffect = null;
this.addMessage(`${this.battle.enemy.name} was captured!`);
await this.delay(500);
this.animating = false;
return { type: 'captured', capturedResonant: capturedR };
}
else {
this.addMessage(`${this.battle.enemy.name} broke free!`);
await this.delay(600);
this.animating = false;
this.battle.turnPhase = 'enemy';
return null; // Enemy gets a turn after failed capture
}
}
async useItem(itemType, party) {
if (this.animating)
return null;
this.animating = true;
const playerR = party[this.battle.activeIndex];
if (!playerR)
return null;
switch (itemType) {
case 'heal': {
const healAmount = Math.floor(playerR.maxHp * 0.5);
playerR.currentHp = Math.min(playerR.maxHp, playerR.currentHp + healAmount);
this.addMessage(`${playerR.name} recovered ${healAmount} HP!`);
await this.delay(600);
break;
}
case 'status': {
if (playerR.statusEffect) {
this.addMessage(`${playerR.name}'s status was cured!`);
playerR.statusEffect = null;
}
else {
this.addMessage(`${playerR.name} had no status to cure.`);
}
await this.delay(600);
break;
}
case 'boost': {
// Boost is handled in the capture flow, just consume it
this.addMessage(`Prism power boosted!`);
await this.delay(400);
break;
}
}
this.animating = false;
this.battle.turnPhase = 'enemy';
return null; // Enemy gets a turn after using item
}
async attemptFlee() {
if (!this.battle.isWild) {
this.addMessage("Can't flee from a Conductor battle!");
await this.delay(500);
return null;
}
const playerR = /* will be passed */ null;
// Flee chance based on speed difference
if (Math.random() < 0.7) {
this.addMessage('Got away safely!');
await this.delay(400);
return { type: 'fled' };
}
else {
this.addMessage("Couldn't escape!");
await this.delay(500);
return null; // Enemy gets a turn
}
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
exports.CombatEngine = CombatEngine;
};
// ── module: src/ui/renderer.ts ──
__mods["src/ui/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getTypeColor = getTypeColor;
exports.generateResonantSVG = generateResonantSVG;
exports.generateZoneIcon = generateZoneIcon;
exports.generateTypeIcon = generateTypeIcon;
const resonants_1 = require("../data/resonants");
const TYPE_COLORS = {
Pyro: '#FF6B35', Hydro: '#4CC9F0', Flora: '#2D6A4F',
Zephyr: '#A2D2FF', Earth: '#8B7355', Volt: '#FFD700', Cosmos: '#C77DFF',
};
function getTypeColor(type) {
return TYPE_COLORS[type] || '#888';
}
function generateResonantSVG(r, size = 120) {
const tmpl = (0, resonants_1.getResonantById)(r.templateId);
if (!tmpl)
return '';
const sp = tmpl.spriteParams;
const cx = size / 2;
const cy = size / 2;
let bodyPath = '';
switch (sp.bodyShape) {
case 'round':
bodyPath = `<ellipse cx="${cx}" cy="${cy + 5}" rx="${size * 0.32}" ry="${size * 0.35}" fill="${sp.primaryColor}"/>`;
break;
case 'angular':
bodyPath = `<polygon points="${cx},${cy - size * 0.38} ${cx + size * 0.35},${cy + size * 0.1} ${cx + size * 0.2},${cy + size * 0.4} ${cx - size * 0.2},${cy + size * 0.4} ${cx - size * 0.35},${cy + size * 0.1}" fill="${sp.primaryColor}"/>`;
break;
case 'elongated':
bodyPath = `<ellipse cx="${cx}" cy="${cy + 5}" rx="${size * 0.25}" ry="${size * 0.4}" fill="${sp.primaryColor}"/>`;
break;
case 'spiky':
const pts = [];
for (let i = 0; i < 8; i++) {
const angle = (i / 8) * Math.PI * 2 - Math.PI / 2;
const r1 = size * 0.35;
const r2 = size * 0.2;
pts.push(`${cx + Math.cos(angle) * r1},${cy + Math.sin(angle) * r1}`);
const midAngle = ((i + 0.5) / 8) * Math.PI * 2 - Math.PI / 2;
pts.push(`${cx + Math.cos(midAngle) * r2},${cy + Math.sin(midAngle) * r2}`);
}
bodyPath = `<polygon points="${pts.join(' ')}" fill="${sp.primaryColor}"/>`;
break;
}
// Eyes
const eyeY = cy - size * 0.1;
const eyes = `
<circle cx="${cx - size * 0.1}" cy="${eyeY}" r="${size * 0.06}" fill="white"/>
<circle cx="${cx + size * 0.1}" cy="${eyeY}" r="${size * 0.06}" fill="white"/>
<circle cx="${cx - size * 0.08}" cy="${eyeY}" r="${size * 0.035}" fill="#222"/>
<circle cx="${cx + size * 0.12}" cy="${eyeY}" r="${size * 0.035}" fill="#222"/>
`;
// Feature-specific decorations
let features = '';
if (sp.features.includes('small-tail-flame')) {
features += `<path d="M${cx + size * 0.3},${cy} Q${cx + size * 0.45},${cy - size * 0.2} ${cx + size * 0.35},${cy - size * 0.35}" stroke="${sp.accentColor}" stroke-width="3" fill="none"/>`;
}
if (sp.features.includes('pointy-ears')) {
features += `<polygon points="${cx - size * 0.2},${cy - size * 0.2} ${cx - size * 0.15},${cy - size * 0.45} ${cx - size * 0.05},${cy - size * 0.2}" fill="${sp.secondaryColor}"/>`;
features += `<polygon points="${cx + size * 0.05},${cy - size * 0.2} ${cx + size * 0.15},${cy - size * 0.45} ${cx + size * 0.2},${cy - size * 0.2}" fill="${sp.secondaryColor}"/>`;
}
if (sp.features.includes('flame-manes')) {
for (let i = -2; i <= 2; i++) {
features += `<path d="M${cx + i * size * 0.1},${cy - size * 0.35} Q${cx + i * size * 0.12},${cy - size * 0.55} ${cx + i * size * 0.08},${cy - size * 0.6}" stroke="${sp.accentColor}" stroke-width="2" fill="none"/>`;
}
}
if (sp.features.includes('wolf-snout')) {
features += `<ellipse cx="${cx}" cy="${eyeY + size * 0.15}" rx="${size * 0.12}" ry="${size * 0.08}" fill="${sp.secondaryColor}"/>`;
features += `<circle cx="${cx}" cy="${eyeY + size * 0.13}" r="${size * 0.04}" fill="#333"/>`;
}
if (sp.features.includes('dragon-horns')) {
features += `<polygon points="${cx - size * 0.25},${cy - size * 0.3} ${cx - size * 0.35},${cy - size * 0.6} ${cx - size * 0.15},${cy - size * 0.3}" fill="${sp.accentColor}"/>`;
features += `<polygon points="${cx + size * 0.25},${cy - size * 0.3} ${cx + size * 0.35},${cy - size * 0.6} ${cx + size * 0.15},${cy - size * 0.3}" fill="${sp.accentColor}"/>`;
}
if (sp.features.includes('flame-crown')) {
for (let i = -2; i <= 2; i++) {
features += `<path d="M${cx + i * size * 0.1},${cy - size * 0.38} Q${cx + i * size * 0.12},${cy - size * 0.65} ${cx + i * size * 0.07},${cy - size * 0.7}" stroke="${sp.accentColor}" stroke-width="3" fill="none"/>`;
}
}
if (sp.features.includes('water-droplet-body')) {
features += `<ellipse cx="${cx}" cy="${cy - size * 0.15}" rx="${size * 0.12}" ry="${size * 0.18}" fill="${sp.secondaryColor}" opacity="0.6"/>`;
}
if (sp.features.includes('big-eyes')) {
features += `<circle cx="${cx - size * 0.1}" cy="${eyeY}" r="${size * 0.09}" fill="white" stroke="#333" stroke-width="1"/>`;
features += `<circle cx="${cx + size * 0.1}" cy="${eyeY}" r="${size * 0.09}" fill="white" stroke="#333" stroke-width="1"/>`;
}
if (sp.features.includes('serpent-body')) {
features += `<path d="M${cx - size * 0.2},${cy + size * 0.3} Q${cx - size * 0.4},${cy + size * 0.5} ${cx - size * 0.15},${cy + size * 0.6}" stroke="${sp.secondaryColor}" stroke-width="4" fill="none"/>`;
}
if (sp.features.includes('water-crest')) {
features += `<path d="M${cx - size * 0.2},${cy - size * 0.35} Q${cx},${cy - size * 0.6} ${cx + size * 0.2},${cy - size * 0.35}" stroke="${sp.accentColor}" stroke-width="3" fill="none"/>`;
}
if (sp.features.includes('sea-king-crown')) {
features += `<path d="M${cx - size * 0.25},${cy - size * 0.4} L${cx - size * 0.15},${cy - size * 0.6} L${cx - size * 0.05},${cy - size * 0.45} L${cx + size * 0.05},${cy - size * 0.6} L${cx + size * 0.15},${cy - size * 0.45} L${cx + size * 0.25},${cy - size * 0.3}" stroke="${sp.accentColor}" stroke-width="2" fill="none"/>`;
}
if (sp.features.includes('trident-tail')) {
features += `<path d="M${cx + size * 0.15},${cy + size * 0.3} Q${cx + size * 0.4},${cy + size * 0.5} ${cx + size * 0.35},${cy + size * 0.6}" stroke="${sp.secondaryColor}" stroke-width="3" fill="none"/>`;
}
if (sp.features.includes('leaf-sprout')) {
features += `<ellipse cx="${cx}" cy="${cy - size * 0.4}" rx="${size * 0.15}" ry="${size * 0.2}" fill="${sp.accentColor}"/>`;
}
if (sp.features.includes('bulb-body')) {
features += `<circle cx="${cx}" cy="${cy + size * 0.1}" r="${size * 0.15}" fill="${sp.secondaryColor}" opacity="0.4"/>`;
}
if (sp.features.includes('thorn-armor')) {
for (let i = -2; i <= 2; i++) {
features += `<line x1="${cx + i * size * 0.1}" y1="${cy - size * 0.35}" x2="${cx + i * size * 0.12}" y2="${cy - size * 0.5}" stroke="${sp.accentColor}" stroke-width="2"/>`;
}
}
if (sp.features.includes('flower-back')) {
features += `<circle cx="${cx}" cy="${cy - size * 0.35}" r="${size * 0.12}" fill="${sp.accentColor}"/>`;
features += `<circle cx="${cx}" cy="${cy - size * 0.35}" r="${size * 0.06}" fill="${sp.secondaryColor}"/>`;
}
if (sp.features.includes('giant-blossom')) {
for (let i = 0; i < 5; i++) {
const angle = (i / 5) * Math.PI * 2 - Math.PI / 2;
features += `<ellipse cx="${cx + Math.cos(angle) * size * 0.18}" cy="${cy - size * 0.35 + Math.sin(angle) * size * 0.18}" rx="${size * 0.1}" ry="${size * 0.06}" fill="${sp.accentColor}" transform="rotate(${i * 72} ${cx + Math.cos(angle) * size * 0.18} ${cy - size * 0.35 + Math.sin(angle) * size * 0.18})"/>`;
}
}
if (sp.features.includes('root-legs')) {
features += `<path d="M${cx - size * 0.15},${cy + size * 0.4} Q${cx - size * 0.25},${cy + size * 0.6} ${cx - size * 0.3},${cy + size * 0.7}" stroke="${sp.secondaryColor}" stroke-width="3" fill="none"/>`;
features += `<path d="M${cx + size * 0.15},${cy + size * 0.4} Q${cx + size * 0.25},${cy + size * 0.6} ${cx + size * 0.3},${cy + size * 0.7}" stroke="${sp.secondaryColor}" stroke-width="3" fill="none"/>`;
}
if (sp.features.includes('lightning-tail')) {
features += `<polyline points="${cx + size * 0.25},${cy} ${cx + size * 0.4},${cy - size * 0.15} ${cx + size * 0.35},${cy - size * 0.05} ${cx + size * 0.5},${cy - size * 0.3}" stroke="${sp.accentColor}" stroke-width="2" fill="none"/>`;
}
if (sp.features.includes('electric-cheeks')) {
features += `<ellipse cx="${cx - size * 0.18}" cy="${eyeY + size * 0.1}" rx="${size * 0.06}" ry="${size * 0.04}" fill="${sp.accentColor}"/>`;
features += `<ellipse cx="${cx + size * 0.18}" cy="${eyeY + size * 0.1}" rx="${size * 0.06}" ry="${size * 0.04}" fill="${sp.accentColor}"/>`;
}
if (sp.features.includes('sabertooth')) {
features += `<path d="M${cx - size * 0.08},${eyeY + size * 0.12} L${cx - size * 0.05},${eyeY + size * 0.25}" stroke="white" stroke-width="2"/>`;
features += `<path d="M${cx + size * 0.08},${eyeY + size * 0.12} L${cx + size * 0.05},${eyeY + size * 0.25}" stroke="white" stroke-width="2"/>`;
}
if (sp.features.includes('bolt-armor')) {
features += `<path d="M${cx - size * 0.3},${cy} L${cx - size * 0.15},${cy - size * 0.1} L${cx - size * 0.2},${cy + size * 0.1}" stroke="${sp.accentColor}" stroke-width="2" fill="none"/>`;
features += `<path d="M${cx + size * 0.3},${cy} L${cx + size * 0.15},${cy - size * 0.1} L${cx + size * 0.2},${cy + size * 0.1}" stroke="${sp.accentColor}" stroke-width="2" fill="none"/>`;
}
if (sp.features.includes('storm-crown')) {
features += `<path d="M${cx - size * 0.3},${cy - size * 0.4} L${cx - size * 0.15},${cy - size * 0.65} L${cx - size * 0.02},${cy - size * 0.45} L${cx + size * 0.1},${cy - size * 0.7} L${cx + size * 0.2},${cy - size * 0.45}" stroke="${sp.accentColor}" stroke-width="2" fill="none"/>`;
}
if (sp.features.includes('lightning-wings')) {
features += `<path d="M${cx - size * 0.3},${cy} Q${cx - size * 0.6},${cy - size * 0.3} ${cx - size * 0.5},${cy + size * 0.1}" stroke="${sp.accentColor}" stroke-width="2" fill="none"/>`;
features += `<path d="M${cx + size * 0.3},${cy} Q${cx + size * 0.6},${cy - size * 0.3} ${cx + size * 0.5},${cy + size * 0.1}" stroke="${sp.accentColor}" stroke-width="2" fill="none"/>`;
}
if (sp.features.includes('wind-wings')) {
features += `<ellipse cx="${cx - size * 0.35}" cy="${cy - size * 0.1}" rx="${size * 0.15}" ry="${size * 0.25}" fill="${sp.secondaryColor}" opacity="0.6" transform="rotate(-20 ${cx - size * 0.35} ${cy})"/>`;
features += `<ellipse cx="${cx + size * 0.35}" cy="${cy - size * 0.1}" rx="${size * 0.15}" ry="${size * 0.25}" fill="${sp.secondaryColor}" opacity="0.6" transform="rotate(20 ${cx + size * 0.35} ${cy})"/>`;
}
if (sp.features.includes('feather-body')) {
features += `<path d="M${cx - size * 0.1},${cy + size * 0.1} Q${cx},${cy + size * 0.25} ${cx + size * 0.1},${cy + size * 0.1}" stroke="${sp.accentColor}" stroke-width="1" fill="none"/>`;
}
if (sp.features.includes('streamlined-body')) {
features += `<path d="M${cx - size * 0.35},${cy} Q${cx - size * 0.45},${cy + size * 0.2} ${cx - size * 0.3},${cy + size * 0.3}" stroke="${sp.secondaryColor}" stroke-width="2" fill="none"/>`;
}
if (sp.features.includes('wind-trails')) {
for (let i = 0; i < 3; i++) {
features += `<path d="M${cx - size * 0.4 - i * size * 0.1},${cy + i * size * 0.1} Q${cx - size * 0.5 - i * size * 0.1},${cy + size * 0.05 + i * size * 0.1} ${cx - size * 0.35 - i * size * 0.1},${cy + size * 0.1 + i * size * 0.1}" stroke="${sp.accentColor}" stroke-width="1" fill="none" opacity="0.5"/>`;
}
}
if (sp.features.includes('sky-king-halo')) {
features += `<ellipse cx="${cx}" cy="${cy - size * 0.45}" rx="${size * 0.2}" ry="${size * 0.06}" stroke="${sp.accentColor}" stroke-width="2" fill="none"/>`;
}
if (sp.features.includes('tempest-wings')) {
features += `<path d="M${cx - size * 0.3},${cy} Q${cx - size * 0.7},${cy - size * 0.4} ${cx - size * 0.55},${cy + size * 0.2}" stroke="${sp.accentColor}" stroke-width="2" fill="none"/>`;
features += `<path d="M${cx + size * 0.3},${cy} Q${cx + size * 0.7},${cy - size * 0.4} ${cx + size * 0.55},${cy + size * 0.2}" stroke="${sp.accentColor}" stroke-width="2" fill="none"/>`;
}
if (sp.features.includes('rock-body')) {
features += `<circle cx="${cx - size * 0.1}" cy="${cy + size * 0.15}" r="${size * 0.08}" fill="${sp.secondaryColor}" opacity="0.4"/>`;
}
if (sp.features.includes('crystal-eyes')) {
features += `<polygon points="${cx - size * 0.1},${eyeY - size * 0.06} ${cx - size * 0.07},${eyeY + size * 0.03} ${cx - size * 0.13},${eyeY + size * 0.03}" fill="${sp.accentColor}"/>`;
features += `<polygon points="${cx + size * 0.1},${eyeY - size * 0.06} ${cx + size * 0.13},${eyeY + size * 0.03} ${cx + size * 0.07},${eyeY + size * 0.03}" fill="${sp.accentColor}"/>`;
}
if (sp.features.includes('crystal-armor')) {
features += `<polygon points="${cx - size * 0.25},${cy - size * 0.1} ${cx - size * 0.15},${cy + size * 0.1} ${cx - size * 0.3},${cy + size * 0.1}" fill="${sp.accentColor}" opacity="0.6"/>`;
features += `<polygon points="${cx + size * 0.25},${cy - size * 0.1} ${cx + size * 0.15},${cy + size * 0.1} ${cx + size * 0.3},${cy + size * 0.1}" fill="${sp.accentColor}" opacity="0.6"/>`;
}
if (sp.features.includes('stone-claws')) {
features += `<path d="M${cx - size * 0.25},${cy + size * 0.2} L${cx - size * 0.35},${cy + size * 0.3}" stroke="${sp.accentColor}" stroke-width="2"/>`;
features += `<path d="M${cx + size * 0.25},${cy + size * 0.2} L${cx + size * 0.35},${cy + size * 0.3}" stroke="${sp.accentColor}" stroke-width="2"/>`;
}
if (sp.features.includes('mountain-horns')) {
features += `<polygon points="${cx - size * 0.2},${cy - size * 0.3} ${cx - size * 0.35},${cy - size * 0.65} ${cx - size * 0.1},${cy - size * 0.3}" fill="${sp.accentColor}"/>`;
features += `<polygon points="${cx + size * 0.2},${cy - size * 0.3} ${cx + size * 0.35},${cy - size * 0.65} ${cx + size * 0.1},${cy - size * 0.3}" fill="${sp.accentColor}"/>`;
}
if (sp.features.includes('earth-shield')) {
features += `<rect x="${cx - size * 0.25}" y="${cy - size * 0.15}" width="${size * 0.5}" height="${size * 0.3}" rx="4" stroke="${sp.accentColor}" stroke-width="2" fill="none"/>`;
}
if (sp.features.includes('ancient-tree-form')) {
features += `<path d="M${cx},${cy - size * 0.5} Q${cx - size * 0.3},${cy - size * 0.7} ${cx - size * 0.4},${cy - size * 0.6}" stroke="${sp.accentColor}" stroke-width="2" fill="none"/>`;
features += `<path d="M${cx},${cy - size * 0.5} Q${cx + size * 0.3},${cy - size * 0.7} ${cx + size * 0.4},${cy - size * 0.6}" stroke="${sp.accentColor}" stroke-width="2" fill="none"/>`;
}
if (sp.features.includes('golden-eyes')) {
features += `<circle cx="${cx - size * 0.1}" cy="${eyeY}" r="${size * 0.07}" fill="${sp.accentColor}"/>`;
features += `<circle cx="${cx + size * 0.1}" cy="${eyeY}" r="${size * 0.07}" fill="${sp.accentColor}"/>`;
}
if (sp.features.includes('water-crown')) {
features += `<path d="M${cx - size * 0.2},${cy - size * 0.45} Q${cx},${cy - size * 0.65} ${cx + size * 0.2},${cy - size * 0.45}" stroke="${sp.accentColor}" stroke-width="3" fill="none"/>`;
}
if (sp.features.includes('lava-veins')) {
features += `<path d="M${cx - size * 0.15},${cy} Q${cx},${cy + size * 0.1} ${cx + size * 0.15},${cy}" stroke="${sp.accentColor}" stroke-width="2" fill="none"/>`;
}
if (sp.features.includes('obsidian-crown')) {
features += `<path d="M${cx - size * 0.25},${cy - size * 0.4} L${cx - size * 0.1},${cy - size * 0.6} L${cx + size * 0.1},${cy - size * 0.6} L${cx + size * 0.25},${cy - size * 0.4}" stroke="${sp.accentColor}" stroke-width="2" fill="none"/>`;
}
if (sp.features.includes('void-crown')) {
features += `<circle cx="${cx}" cy="${cy - size * 0.5}" r="${size * 0.15}" stroke="${sp.accentColor}" stroke-width="2" fill="none"/>`;
features += `<circle cx="${cx}" cy="${cy - size * 0.5}" r="${size * 0.08}" fill="${sp.accentColor}" opacity="0.3"/>`;
}
if (sp.features.includes('cosmic-aura')) {
for (let i = 0; i < 6; i++) {
const angle = (i / 6) * Math.PI * 2;
features += `<circle cx="${cx + Math.cos(angle) * size * 0.4}" cy="${cy + Math.sin(angle) * size * 0.4}" r="${size * 0.03}" fill="${sp.accentColor}" opacity="0.5"/>`;
}
}
// Glow filter
const glow = `<filter id="glow_${r.templateId}"><feGaussianBlur stdDeviation="3" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>`;
return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
<defs>${glow}</defs>
<g filter="url(#glow_${r.templateId})" opacity="0.3">${bodyPath}</g>
${bodyPath}
${eyes}
${features}
</svg>`;
}
function generateZoneIcon(zoneIndex, size = 40) {
const icons = [
`<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="#52B788"><path d="M12 2L6 8v10h3v-6h6v6h3V8z"/></svg>`,
`<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="#4CC9F0"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm0 3c1.7 0 3 1.3 3 3s-1.3 3-3 3-3-1.3-3-3 1.3-3 3-3z"/></svg>`,
`<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="#FF6B35"><path d="M12 2l3 7h7l-5.5 4.5L18 21l-6-4.5L6 21l1.5-7.5L2 9h7z"/></svg>`,
`<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="#C77DFF"><circle cx="12" cy="12" r="8"/><circle cx="12" cy="12" r="3"/></svg>`,
];
return icons[zoneIndex] || icons[0];
}
function generateTypeIcon(type, size = 24) {
const color = getTypeColor(type);
const shapes = {
Pyro: `<path d="M12 2 Q8 8 10 12 Q6 14 8 18 L12 22 L16 18 Q18 14 14 12 Q16 8 12 2z"/>`,
Hydro: `<path d="M12 2 Q6 10 6 14 A6 6 0 0 0 18 14 Q18 10 12 2z"/>`,
Flora: `<path d="M12 2 L8 8 L4 8 L8 12 L6 18 L12 14 L18 18 L16 12 L20 8 L16 8z"/>`,
Zephyr: `<path d="M4 8 Q8 6 12 8 Q16 10 20 8 M4 12 Q8 10 12 12 Q16 14 20 12 M4 16 Q8 14 12 16"/>`,
Earth: `<path d="M12 2 L4 8 L4 18 L12 22 L20 18 L20 8z"/>`,
Volt: `<path d="M13 2 L7 12 L11 12 L10 22 L18 10 L14 10z"/>`,
Cosmos: `<circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="3"/>`,
};
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="${color}">${shapes[type] || shapes.Cosmos}</svg>`;
}
};
// ── module: src/engine/game.ts ──
__mods["src/engine/game.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initGame = initGame;
const zones_1 = require("../data/zones");
const resonants_1 = require("../data/resonants");
const player_1 = require("../state/player");
const sound_1 = require("./sound");
const combat_1 = require("./combat");
// DOM references
let screens;
let currentScreen = 'menu';
let playerState;
let battleEngine = null;
let battleParty = [];
let prismBoostActive = false;
// Animation state
let animatingBattle = false;
// Particle system
function spawnStatusParticles(statusType, targetElementId) {
const target = document.getElementById(targetElementId);
if (!target)
return;
const rect = target.getBoundingClientRect();
const particleClass = `particle-${statusType}`;
for (let i = 0; i < 6; i++) {
const particle = document.createElement('div');
particle.className = `status-particle ${particleClass}`;
particle.style.left = `${rect.left + rect.width * 0.3 + Math.random() * rect.width * 0.4}px`;
particle.style.top = `${rect.top + rect.height * 0.5 + Math.random() * 20}px`;
document.body.appendChild(particle);
setTimeout(() => particle.remove(), 2000);
}
}
// Ambient background particles
function initAmbientParticles() {
const colors = ['rgba(199,125,255,0.3)', 'rgba(76,201,240,0.2)', 'rgba(82,183,136,0.2)'];
for (let i = 0; i < 15; i++) {
const particle = document.createElement('div');
particle.className = 'ambient-particle';
const size = 2 + Math.random() * 4;
particle.style.width = `${size}px`;
particle.style.height = `${size}px`;
particle.style.left = `${Math.random() * 100}%`;
particle.style.background = colors[Math.floor(Math.random() * colors.length)];
particle.style.animationDuration = `${8 + Math.random() * 12}s`;
particle.style.animationDelay = `${Math.random() * 10}s`;
document.body.appendChild(particle);
}
}
function initGame() {
screens = {};
const app = document.getElementById('app');
// Create screen containers
const screenNames = ['menu', 'map', 'battle', 'party', 'shop', 'dex', 'victory'];
for (const name of screenNames) {
const div = document.createElement('div');
div.id = `screen-${name}`;
div.className = 'game-screen';
app.appendChild(div);
screens[name] = div;
}
// Try to load saved game
const saved = (0, player_1.loadGame)();
if (saved) {
playerState = saved;
showScreen('map');
renderMap();
}
else {
playerState = (0, player_1.createDefaultPlayer)();
showScreen('menu');
renderMenu();
}
// Init sound on first interaction
document.addEventListener('click', () => sound_1.soundManager.init(), { once: true });
// Ambient background particles
initAmbientParticles();
}
function showScreen(name) {
currentScreen = name;
for (const key in screens) {
screens[key].style.display = 'none';
}
screens[name].style.display = 'flex';
sound_1.soundManager.select();
}
// ==================== MENU SCREEN ====================
function renderMenu() {
const hasSave = !!(0, player_1.loadGame)();
screens.menu.innerHTML = `
<div class="menu-container">
<div class="title-section">
<h1 class="game-title">AETHELGARD</h1>
<p class="subtitle">Resonant Chronicles</p>
</div>
<div class="menu-buttons">
${hasSave ? `<button id="btn-continue" class="menu-btn primary">Continue</button>` : ''}
<button id="btn-newgame" class="menu-btn primary">New Game</button>
<button id="btn-dex" class="menu-btn secondary">Conductors Dex</button>
${hasSave ? `<button id="btn-reset" class="menu-btn danger">Reset Progress</button>` : ''}
</div>
<p class="version-text">v1.0 — A world vibrating with elemental energy</p>
</div>
`;
document.getElementById('btn-continue')?.addEventListener('click', () => {
const saved = (0, player_1.loadGame)();
if (saved) {
playerState = saved;
showScreen('map');
renderMap();
}
});
document.getElementById('btn-newgame').addEventListener('click', () => {
(0, player_1.resetGame)();
playerState = (0, player_1.createDefaultPlayer)();
(0, player_1.saveGame)(playerState);
showScreen('map');
renderMap();
});
document.getElementById('btn-dex')?.addEventListener('click', () => {
showScreen('dex');
renderDex();
});
document.getElementById('btn-reset')?.addEventListener('click', () => {
if (confirm('Are you sure? This will erase all progress!')) {
(0, player_1.resetGame)();
playerState = (0, player_1.createDefaultPlayer)();
renderMenu();
}
});
}
// ==================== MAP SCREEN ====================
function renderMap() {
const zone = zones_1.ZONES[playerState.currentZone];
const nodes = zone.nodes;
let nodesHTML = '';
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
const isUnlocked = i <= playerState.currentNodeInZone;
const isCurrent = i === playerState.currentNodeInZone;
const isCompleted = i < playerState.currentNodeInZone;
let icon = '🌿';
if (node.type === 'cache')
icon = '📦';
else if (node.type === 'conductor')
icon = '⚔️';
else if (node.type === 'spring')
icon = '💧';
else if (node.type === 'boss')
icon = '👑';
let nodeClass = 'map-node';
if (!isUnlocked)
nodeClass += ' locked';
if (isCurrent)
nodeClass += ' current';
if (isCompleted)
nodeClass += ' completed';
nodesHTML += `
<div class="${nodeClass}" data-index="${i}" title="${node.label}">
<span class="node-icon">${icon}</span>
<span class="node-label">${node.label}</span>
</div>
`;
}
screens.map.innerHTML = `
<div class="map-container">
<div class="map-header glass-card">
<h2>${zone.name}</h2>
<p class="zone-desc">${zone.description}</p>
<div class="zone-info">
<span>Levels ${zone.levelRange[0]}-${zone.levelRange[1]}</span>
<span>Shards: ${playerState.shards} 💎</span>
<span>Party: ${playerState.party.length}/6</span>
</div>
</div>
<div class="map-path">
${nodesHTML}
</div>
<div class="map-actions glass-card">
<button id="btn-party" class="action-btn">🎒 Party</button>
<button id="btn-shop" class="action-btn">🏪 Shop</button>
<button id="btn-dex-map" class="action-btn">📖 Dex</button>
<button id="btn-menu" class="action-btn">☰ Menu</button>
</div>
</div>
`;
// Node click handlers
document.querySelectorAll('.map-node.current').forEach(el => {
el.addEventListener('click', () => {
const idx = parseInt(el.dataset.index);
handleNodeInteraction(idx);
});
});
document.getElementById('btn-party').addEventListener('click', () => { showScreen('party'); renderParty(); });
document.getElementById('btn-shop').addEventListener('click', () => { showScreen('shop'); renderShop(); });
document.getElementById('btn-dex-map').addEventListener('click', () => { showScreen('dex'); renderDex(); });
document.getElementById('btn-menu').addEventListener('click', () => { showScreen('menu'); renderMenu(); });
}
async function handleNodeInteraction(nodeIndex) {
const zone = zones_1.ZONES[playerState.currentZone];
const node = zone.nodes[nodeIndex];
switch (node.type) {
case 'wild':
await startWildBattle(playerState.currentZone);
break;
case 'cache':
await openCache();
break;
case 'conductor':
await startConductorBattle(nodeIndex);
break;
case 'spring':
healParty();
advanceNode();
renderMap();
break;
case 'boss':
await startBossBattle(playerState.currentZone);
break;
}
}
function advanceNode() {
playerState.currentNodeInZone++;
(0, player_1.saveGame)(playerState);
}
// ==================== WILD BATTLES ====================
async function startWildBattle(zoneIndex) {
const zone = zones_1.ZONES[zoneIndex];
const encounterTable = zones_1.WILD_ENCOUNTER_TABLES[zoneIndex] || [];
const templateId = encounterTable[Math.floor(Math.random() * encounterTable.length)];
const levelRange = zone.levelRange;
const level = levelRange[0] + Math.floor(Math.random() * (levelRange[1] - levelRange[0] + 1));
const enemy = (0, player_1.createResonant)(templateId, level);
playerState.dexSeen.add(templateId);
battleParty = [...playerState.party];
startBattle(enemy, true);
}
// ==================== CONDUCTOR BATTLES ====================
async function startConductorBattle(nodeIndex) {
const zone = zones_1.ZONES[playerState.currentZone];
const conductorIdx = nodeIndex - 1; // First conductor is at index 4 in zone 0, etc.
let rosterIdx = 0;
if (playerState.currentZone === 0)
rosterIdx = nodeIndex >= 8 ? 1 : 0;
else if (playerState.currentZone === 1)
rosterIdx = nodeIndex >= 8 ? 3 : 2;
else if (playerState.currentZone === 2)
rosterIdx = nodeIndex >= 8 ? 5 : 4;
else
rosterIdx = Math.min(nodeIndex - 1, 8);
const roster = zones_1.CONDUCTOR_ROSTERS[rosterIdx] || zones_1.CONDUCTOR_ROSTERS[0];
const conductorNames = ['Elara', 'Finn', 'Mira', 'Rex', 'Zara', 'Kai', 'Nova', 'Orion', 'Lyra'];
// Pick a random resonant from the roster
const entry = roster[Math.floor(Math.random() * roster.length)];
const enemy = (0, player_1.createResonant)(entry.templateId, entry.level);
playerState.dexSeen.add(entry.templateId);
battleParty = [...playerState.party];
startBattle(enemy, false, conductorNames[rosterIdx] || 'Unknown');
}
// ==================== BOSS BATTLES ====================
async function startBossBattle(zoneIndex) {
const bossId = zones_1.BOSS_IDS[zoneIndex];
if (!bossId)
return;
let level = 8 + zoneIndex * 3;
const enemy = (0, player_1.createResonant)(bossId, level);
playerState.dexSeen.add(bossId);
battleParty = [...playerState.party];
startBattle(enemy, false, bossId.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()));
}
// ==================== BATTLE SYSTEM ====================
function startBattle(enemy, isWild, conductorName) {
const battleState = {
enemy,
activeIndex: findAliveResonant(battleParty),
turnPhase: 'player',
messageLog: [`A wild ${enemy.name} appeared!`],
isWild,
conductorName,
};
battleEngine = new combat_1.CombatEngine(battleState);
prismBoostActive = false;
showScreen('battle');
renderBattle();
sound_1.soundManager.battleStart();
}
function findAliveResonant(party) {
for (let i = 0; i < party.length; i++) {
if (party[i].currentHp > 0)
return i;
}
return 0;
}
// ==================== BATTLE RENDERING ====================
function renderBattle() {
if (!battleEngine)
return;
const bs = battleEngine.getBattle();
const enemy = bs.enemy;
const playerR = battleParty[bs.activeIndex];
if (!playerR)
return;
const tmpl = (0, resonants_1.getResonantById)(playerR.templateId);
const availableMoves = tmpl?.moves.filter(m => m.unlockLevel <= playerR.level) || [];
const enemyHpPct = Math.max(0, (enemy.currentHp / enemy.maxHp) * 100);
const playerHpPct = Math.max(0, (playerR.currentHp / playerR.maxHp) * 100);
const playerXpPct = Math.min(100, (playerR.xp / playerR.xpToNext) * 100);
const enemySvg = generateResonantSVG(enemy, 140);
const playerSvg = generateResonantSVG(playerR, 120);
// Build move buttons
let movesHTML = '';
for (let i = 0; i < availableMoves.length; i++) {
const m = availableMoves[i];
const typeColor = getTypeColor(m.element);
movesHTML += `<button class="move-btn" data-move="${i}" style="--type-color: ${typeColor}">
<span class="move-name">${m.name}</span>
<span class="move-info">${m.element} · Pwr ${m.power} · Acc ${m.accuracy}%</span>
</button>`;
}
// Build party swap buttons
let swapHTML = '';
for (let i = 0; i < battleParty.length; i++) {
if (i === bs.activeIndex)
continue;
const r = battleParty[i];
const hpPct = Math.max(0, (r.currentHp / r.maxHp) * 100);
swapHTML += `<button class="swap-btn" data-swap="${i}" ${r.currentHp <= 0 ? 'disabled' : ''}>
<span>${r.name} Lv.${r.level}</span>
<div class="mini-hp-bar"><div style="width:${hpPct}%"></div></div>
</button>`;
}
// Build inventory buttons
let invHTML = '';
for (const item of playerState.inventory) {
if (item.quantity <= 0)
continue;
const icon = item.type === 'prism' ? '💎' : item.type === 'heal' ? '❤️' : item.type === 'status' ? '✨' : '⚡';
invHTML += `<button class="inv-btn" data-item="${item.id}">
${icon} ${item.name} (×${item.quantity})
</button>`;
}
// Message log
const messages = battleEngine.getMessages();
let msgHTML = '';
for (const m of messages.slice(-3)) {
msgHTML += `<div class="battle-msg">${m}</div>`;
}
screens.battle.innerHTML = `
<div class="battle-container">
<!-- Enemy side -->
<div class="enemy-side">
<div class="resonant-card glass-card enemy-card" id="enemy-card">
<div class="resonant-info">
<h3>${enemy.name}</h3>
<span class="level-badge">Lv.${enemy.level}</span>
${enemy.statusEffect ? `<span class="status-badge">${enemy.statusEffect.type}</span>` : ''}
</div>
<div class="hp-bar-container">
<div class="hp-bar" style="width:${enemyHpPct}%; --hp-color: ${getHpColor(enemyHpPct)}"></div>
</div>
<div class="hp-text">${enemy.currentHp}/${enemy.maxHp}</div>
</div>
<div class="resonant-sprite enemy-sprite" id="enemy-sprite">${enemySvg}</div>
</div>
<!-- Player side -->
<div class="player-side">
<div class="resonant-sprite player-sprite" id="player-sprite">${playerSvg}</div>
<div class="resonant-card glass-card player-card" id="player-card">
<div class="resonant-info">
<h3>${playerR.name}</h3>
<span class="level-badge">Lv.${playerR.level}</span>
${playerR.statusEffect ? `<span class="status-badge">${playerR.statusEffect.type}</span>` : ''}
</div>
<div class="hp-bar-container">
<div class="hp-bar" style="width:${playerHpPct}%; --hp-color: ${getHpColor(playerHpPct)}"></div>
</div>
<div class="xp-bar-container">
<div class="xp-bar" style="width:${playerXpPct}%"></div>
</div>
<div class="stats-row">
<span>${playerR.currentHp}/${playerR.maxHp} HP</span>
<span>XP ${playerR.xp}/${playerR.xpToNext}</span>
</div>
</div>
</div>
<!-- Message log -->
<div class="battle-log glass-card" id="battle-log">${msgHTML || '<div class="battle-msg">What will you do?</div>'}</div>
<!-- Action buttons -->
<div class="battle-actions" id="battle-actions">
${bs.turnPhase === 'player' ? `
<div class="action-tabs">
<button class="tab-btn active" data-tab="attack">⚔️ Attack</button>
<button class="tab-btn" data-tab="prism">💎 Prism</button>
<button class="tab-btn" data-tab="swap">🔄 Swap</button>
<button class="tab-btn" data-tab="items">🎒 Items</button>
${bs.isWild ? '<button class="tab-btn" data-tab="flee">🏃 Flee</button>' : ''}
</div>
<div class="action-panel active" id="panel-attack">${movesHTML}</div>
<div class="action-panel" id="panel-prism">
${playerState.inventory.filter(i => i.type === 'prism' && i.quantity > 0).map(item => `<button class="inv-btn prism-btn" data-prism="${item.id}">${item.name} (×${item.quantity})</button>`).join('') || '<p>No prisms available. Visit the shop!</p>'}
</div>
<div class="action-panel" id="panel-swap">${swapHTML || '<p>No other Resonants to swap.</p>'}</div>
<div class="action-panel" id="panel-items">${invHTML || '<p>No items available.</p>'}</div>
` : '<div class="waiting-text">Battle in progress...</div>'}
</div>
</div>
`;
// Bind event listeners
bindBattleEvents();
}
function getHpColor(pct) {
if (pct > 50)
return '#22c55e';
if (pct > 25)
return '#eab308';
return '#ef4444';
}
function bindBattleEvents() {
// Tab switching
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
sound_1.soundManager.hover();
const tab = btn.dataset.tab;
if (!tab)
return;
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.querySelectorAll('.action-panel').forEach(p => p.classList.remove('active'));
const panel = document.getElementById(`panel-${tab}`);
if (panel)
panel.classList.add('active');
});
});
// Move buttons
document.querySelectorAll('.move-btn').forEach(btn => {
btn.addEventListener('click', async () => {
sound_1.soundManager.select();
const moveIdx = parseInt(btn.dataset.move);
await executePlayerMove(moveIdx);
});
});
// Prism buttons
document.querySelectorAll('.prism-btn').forEach(btn => {
btn.addEventListener('click', async () => {
sound_1.soundManager.select();
const itemId = btn.dataset.prism;
await executePrism(itemId);
});
});
// Swap buttons
document.querySelectorAll('.swap-btn').forEach(btn => {
btn.addEventListener('click', async () => {
sound_1.soundManager.select();
const swapIdx = parseInt(btn.dataset.swap);
await executeSwap(swapIdx);
});
});
// Item buttons
document.querySelectorAll('.inv-btn').forEach(btn => {
btn.addEventListener('click', async () => {
sound_1.soundManager.select();
const itemId = btn.dataset.item;
await executeItem(itemId);
});
});
// Flee button
document.querySelector('[data-tab="flee"]')?.addEventListener('click', async () => {
sound_1.soundManager.select();
await executeFlee();
});
}
// ==================== BATTLE ACTIONS ====================
async function executePlayerMove(moveIdx) {
if (animatingBattle)
return;
animatingBattle = true;
const bs = battleEngine.getBattle();
const playerR = battleParty[bs.activeIndex];
const tmpl = (0, resonants_1.getResonantById)(playerR.templateId);
const availableMoves = tmpl?.moves.filter(m => m.unlockLevel <= playerR.level) || [];
const move = availableMoves[moveIdx];
if (!playerR || !move) {
animatingBattle = false;
return;
}
// Status effect damage at start of turn
if (playerR.statusEffect) {
const statusDmg = Math.floor(playerR.maxHp * 0.1);
playerR.currentHp = Math.max(0, playerR.currentHp - statusDmg);
battleEngine.addMessage(`${playerR.name} is hurt by its ${playerR.statusEffect.type}!`);
if (playerR.statusEffect.turnsLeft !== undefined) {
// @ts-ignore
playerR.statusEffect.turnsLeft--;
if (playerR.statusEffect.turnsLeft <= 0)
playerR.statusEffect = null;
}
renderBattle();
await delay(500);
if (playerR.currentHp <= 0) {
animatingBattle = false;
await handlePlayerFaint();
return;
}
}
// Accuracy check
if (Math.random() * 100 > move.accuracy) {
battleEngine.addMessage(`${playerR.name}'s ${move.name} missed!`);
renderBattle();
await delay(800);
animatingBattle = false;
bs.turnPhase = 'enemy';
await executeEnemyTurn();
return;
}
// Calculate damage
const result = (0, player_1.calculateDamage)(playerR, bs.enemy, move.power, move.element);
let hitMsg = `${playerR.name} used ${move.name}!`;
if (result.isCrit)
hitMsg += ' Critical hit!';
if (result.typeMod > 1)
hitMsg += " It's super effective!";
else if (result.typeMod < 1)
hitMsg += " It's not very effective...";
battleEngine.addMessage(hitMsg);
// Play sound based on effectiveness
if (result.typeMod > 1)
sound_1.soundManager.hitSuperEffective();
else if (move.element !== 'Cosmos')
sound_1.soundManager.hitElemental();
else
sound_1.soundManager.hitPhysical();
renderBattle();
await delay(600);
// Animate hit on enemy
const enemyCard = document.getElementById('enemy-card');
if (enemyCard) {
enemyCard.classList.add('shake');
setTimeout(() => enemyCard.classList.remove('shake'), 300);
}
bs.enemy.currentHp = Math.max(0, bs.enemy.currentHp - result.damage);
// Status effect chance
if (move.element !== 'Cosmos' && !bs.enemy.statusEffect && Math.random() < 0.15) {
const statusTypes = ['burn', 'paralyze', 'poison'];
bs.enemy.statusEffect = { type: statusTypes[Math.floor(Math.random() * statusTypes.length)], turnsLeft: 3 };
battleEngine.addMessage(`${bs.enemy.name} got a ${bs.enemy.statusEffect.type}!`);
spawnStatusParticles(bs.enemy.statusEffect.type, 'enemy-card');
}
renderBattle();
await delay(500);
// Check enemy faint
if (bs.enemy.currentHp <= 0) {
animatingBattle = false;
await handleEnemyDefeated();
return;
}
// Enemy turn
bs.turnPhase = 'enemy';
renderBattle();
await delay(500);
await executeEnemyTurn();
}
async function executePrism(itemId) {
if (animatingBattle)
return;
animatingBattle = true;
const bs = battleEngine.getBattle();
const playerR = battleParty[bs.activeIndex];
if (!playerR) {
animatingBattle = false;
return;
}
// Find and consume prism
const prismItem = playerState.inventory.find(i => i.id === itemId);
if (!prismItem || prismItem.quantity <= 0) {
animatingBattle = false;
return;
}
prismItem.quantity--;
if (prismItem.quantity <= 0) {
const idx = playerState.inventory.indexOf(prismItem);
if (idx >= 0)
playerState.inventory.splice(idx, 1);
}
battleEngine.addMessage(`${playerR.name} threw an Aether Prism!`);
renderBattle();
await delay(800);
// Capture formula
const hpRatio = bs.enemy.currentHp / bs.enemy.maxHp;
let catchRate = (prismItem.catchRate || 0.45) * (1 - hpRatio * 0.7);
if (bs.enemy.statusEffect)
catchRate *= 1.5;
if (prismBoostActive) {
catchRate *= 2;
prismBoostActive = false;
}
// Dramatic shake sequence
const maxShakes = Math.min(4, Math.floor(catchRate * 6));
battleEngine.addMessage('The Prism is shaking...');
renderBattle();
for (let i = 0; i < maxShakes; i++) {
await delay(700);
const enemySprite = document.getElementById('enemy-sprite');
if (enemySprite) {
enemySprite.classList.add('shake');
setTimeout(() => enemySprite.classList.remove('shake'), 300);
}
}
const captured = Math.random() < catchRate;
await delay(400);
if (captured) {
sound_1.soundManager.captureSuccess();
battleEngine.addMessage(`${bs.enemy.name} was captured!`);
renderBattle();
await delay(800);
const capturedR = (0, player_1.createResonant)(bs.enemy.templateId, bs.enemy.level);
capturedR.currentHp = Math.max(1, Math.floor(capturedR.maxHp * 0.3));
capturedR.statusEffect = null;
if (playerState.party.length < 6) {
playerState.party.push(capturedR);
battleEngine.addMessage(`${capturedR.name} joined your party!`);
}
else {
battleEngine.addMessage(`Party is full. ${capturedR.name} was released.`);
}
(0, player_1.saveGame)(playerState);
animatingBattle = false;
await delay(500);
returnToMap();
}
else {
sound_1.soundManager.captureFail();
battleEngine.addMessage(`${bs.enemy.name} broke free!`);
renderBattle();
await delay(600);
animatingBattle = false;
bs.turnPhase = 'enemy';
await executeEnemyTurn();
}
}
async function executeSwap(swapIdx) {
if (animatingBattle)
return;
const bs = battleEngine.getBattle();
const target = battleParty[swapIdx];
if (!target || target.currentHp <= 0)
return;
sound_1.soundManager.select();
bs.activeIndex = swapIdx;
battleEngine.addMessage(`Go, ${target.name}!`);
renderBattle();
await delay(500);
// Enemy still gets a turn after swap
bs.turnPhase = 'enemy';
await executeEnemyTurn();
}
async function executeItem(itemId) {
if (animatingBattle)
return;
animatingBattle = true;
const bs = battleEngine.getBattle();
const playerR = battleParty[bs.activeIndex];
if (!playerR) {
animatingBattle = false;
return;
}
const item = playerState.inventory.find(i => i.id === itemId);
if (!item || item.quantity <= 0) {
animatingBattle = false;
return;
}
item.quantity--;
if (item.quantity <= 0) {
const idx = playerState.inventory.indexOf(item);
if (idx >= 0)
playerState.inventory.splice(idx, 1);
}
switch (item.type) {
case 'heal': {
const healAmount = Math.floor(playerR.maxHp * 0.5);
playerR.currentHp = Math.min(playerR.maxHp, playerR.currentHp + healAmount);
battleEngine.addMessage(`${playerR.name} recovered ${healAmount} HP!`);
break;
}
case 'status': {
if (playerR.statusEffect) {
battleEngine.addMessage(`${playerR.name}'s status was cured!`);
playerR.statusEffect = null;
}
else {
battleEngine.addMessage(`${playerR.name} had no status to cure.`);
}
break;
}
case 'boost': {
prismBoostActive = true;
battleEngine.addMessage('Prism power boosted! Next capture attempt will be stronger.');
animatingBattle = false;
renderBattle();
return;
}
}
sound_1.soundManager.select();
renderBattle();
await delay(600);
animatingBattle = false;
bs.turnPhase = 'enemy';
await executeEnemyTurn();
}
async function executeFlee() {
if (animatingBattle)
return;
const bs = battleEngine.getBattle();
if (!bs.isWild) {
battleEngine.addMessage("Can't flee from a Conductor battle!");
renderBattle();
await delay(500);
return;
}
if (Math.random() < 0.7) {
sound_1.soundManager.select();
battleEngine.addMessage('Got away safely!');
renderBattle();
await delay(400);
animatingBattle = false;
returnToMap();
}
else {
battleEngine.addMessage("Couldn't escape!");
renderBattle();
await delay(500);
bs.turnPhase = 'enemy';
await executeEnemyTurn();
}
}
// ==================== ENEMY TURN ====================
async function executeEnemyTurn() {
const bs = battleEngine.getBattle();
const playerR = battleParty[bs.activeIndex];
if (!playerR)
return;
// Enemy status effect damage
if (bs.enemy.statusEffect && Math.random() < 0.33) {
const statusDmg = Math.floor(bs.enemy.maxHp * 0.1);
bs.enemy.currentHp = Math.max(0, bs.enemy.currentHp - statusDmg);
battleEngine.addMessage(`${bs.enemy.name} is hurt by its ${bs.enemy.statusEffect.type}!`);
if (bs.enemy.statusEffect.turnsLeft !== undefined) {
// @ts-ignore
bs.enemy.statusEffect.turnsLeft--;
if (bs.enemy.statusEffect.turnsLeft <= 0)
bs.enemy.statusEffect = null;
}
renderBattle();
await delay(500);
if (bs.enemy.currentHp <= 0) {
animatingBattle = false;
await handleEnemyDefeated();
return;
}
}
// Enemy AI: choose move
const enemyTmpl = (0, resonants_1.getResonantById)(bs.enemy.templateId);
if (!enemyTmpl)
return;
const availableMoves = enemyTmpl.moves.filter(m => m.unlockLevel <= bs.enemy.level);
let chosenMove = availableMoves[Math.floor(Math.random() * availableMoves.length)];
// Prefer super-effective moves
for (const move of availableMoves) {
const playerType = (0, resonants_1.getResonantById)(playerR.templateId)?.type ?? 'Cosmos';
if (getTypeAdvantage(move.element, playerType) > 1) {
chosenMove = move;
break;
}
}
// Paralyzed enemy might be unable to move
if (bs.enemy.statusEffect?.type === 'paralyze' && Math.random() < 0.25) {
battleEngine.addMessage(`${bs.enemy.name} is paralyzed and can't move!`);
renderBattle();
await delay(600);
bs.turnPhase = 'player';
animatingBattle = false;
renderBattle();
return;
}
const result = (0, player_1.calculateDamage)(bs.enemy, playerR, chosenMove.power, chosenMove.element);
let hitMsg = `${bs.enemy.name} used ${chosenMove.name}!`;
if (result.isCrit)
hitMsg += ' Critical hit!';
if (result.typeMod > 1)
hitMsg += " It's super effective!";
else if (result.typeMod < 1)
hitMsg += " It's not very effective...";
battleEngine.addMessage(hitMsg);
if (result.typeMod > 1)
sound_1.soundManager.hitSuperEffective();
else if (chosenMove.element !== 'Cosmos')
sound_1.soundManager.hitElemental();
else
sound_1.soundManager.hitPhysical();
renderBattle();
await delay(600);
// Animate hit on player
const playerCard = document.getElementById('player-card');
if (playerCard) {
playerCard.classList.add('shake');
setTimeout(() => playerCard.classList.remove('shake'), 300);
}
playerR.currentHp = Math.max(0, playerR.currentHp - result.damage);
// Status effect chance on player
if (chosenMove.element !== 'Cosmos' && !playerR.statusEffect && Math.random() < 0.12) {
const statusTypes = ['burn', 'paralyze', 'poison'];
playerR.statusEffect = { type: statusTypes[Math.floor(Math.random() * statusTypes.length)], turnsLeft: 3 };
battleEngine.addMessage(`${playerR.name} got a ${playerR.statusEffect.type}!`);
spawnStatusParticles(playerR.statusEffect.type, 'player-card');
}
renderBattle();
await delay(500);
// Check player faint
if (playerR.currentHp <= 0) {
animatingBattle = false;
await handlePlayerFaint();
return;
}
bs.turnPhase = 'player';
animatingBattle = false;
renderBattle();
}
// ==================== BATTLE RESOLUTIONS ====================
async function handleEnemyDefeated() {
const bs = battleEngine.getBattle();
sound_1.soundManager.victory();
const xpGained = Math.floor(bs.enemy.level * 15 + 20);
const shardsGained = Math.floor(bs.enemy.level * 8 + 10);
// Apply XP to active resonant
const playerR = battleParty[bs.activeIndex];
if (playerR) {
const result = (0, player_1.gainXp)(playerR, xpGained);
battleEngine.addMessage(`${playerR.name} gained ${xpGained} XP!`);
// Check for level up
if (result.leveledUp) {
sound_1.soundManager.levelUp();
battleEngine.addMessage(`${playerR.name} grew to Lv.${playerR.level}!`);
renderBattle();
await delay(800);
// Check evolution
const evoId = (0, player_1.checkEvolution)(playerR);
if (evoId) {
sound_1.soundManager.evolution();
battleEngine.addMessage(`${playerR.name} is evolving!`);
renderBattle();
await delay(1000);
(0, player_1.evolveResonant)(playerR, evoId);
battleEngine.addMessage(`It evolved into ${playerR.name}!`);
renderBattle();
await delay(800);
}
}
}
// Add shards
playerState.shards += shardsGained;
battleEngine.addMessage(`You earned ${shardsGained} Aether Shards! 💎`);
renderBattle();
await delay(1000);
// Check if this was a boss battle
const zone = zones_1.ZONES[playerState.currentZone];
const currentNode = zone.nodes[playerState.currentNodeInZone];
if (currentNode.type === 'boss') {
const bossId = zones_1.BOSS_IDS[playerState.currentZone];
if (bossId && !playerState.completedBosses.includes(bossId)) {
playerState.completedBosses.push(bossId);
// Unlock next zone
if (playerState.currentZone < zones_1.ZONES.length - 1) {
playerState.unlockedZones = Math.max(playerState.unlockedZones, playerState.currentZone + 2);
battleEngine.addMessage(`🎉 ${zone.name} cleared! The path to the next zone is open!`);
}
else {
// Final boss defeated - victory!
(0, player_1.saveGame)(playerState);
animatingBattle = false;
showScreen('victory');
renderVictory();
return;
}
}
}
// Sync battle party back to player state
syncPartyToPlayer();
advanceNode();
(0, player_1.saveGame)(playerState);
animatingBattle = false;
await delay(500);
returnToMap();
}
async function handlePlayerFaint() {
sound_1.soundManager.defeat();
// Check if any resonants are still alive
const aliveResonants = battleParty.filter(r => r.currentHp > 0);
if (aliveResonants.length === 0) {
battleEngine.addMessage('All your Resonants have fainted!');
renderBattle();
await delay(1500);
// Heal party and return to map
for (const r of playerState.party) {
r.currentHp = Math.max(1, Math.floor(r.maxHp * 0.3));
r.statusEffect = null;
}
(0, player_1.saveGame)(playerState);
animatingBattle = false;
await delay(500);
returnToMap();
return;
}
// Auto-switch to next alive resonant
for (let i = 0; i < battleParty.length; i++) {
if (battleParty[i].currentHp > 0 && i !== battleEngine.getBattle().activeIndex) {
battleEngine.getBattle().activeIndex = i;
battleEngine.addMessage(`Go, ${battleParty[i].name}!`);
renderBattle();
await delay(500);
// Enemy gets a turn after switch
animatingBattle = false;
battleEngine.getBattle().turnPhase = 'enemy';
await executeEnemyTurn();
return;
}
}
}
function syncPartyToPlayer() {
for (let i = 0; i < Math.min(battleParty.length, playerState.party.length); i++) {
playerState.party[i] = battleParty[i];
}
}
function returnToMap() {
showScreen('map');
renderMap();
}
// ==================== CACHE & HEALING ====================
async function openCache() {
const reward = zones_1.CACHE_REWARDS[Math.floor(Math.random() * zones_1.CACHE_REWARDS.length)];
let msg = '';
if (reward.type === 'shards') {
const amt = reward.amount ?? 0;
playerState.shards += amt;
msg = `You found ${amt} Aether Shards! 💎`;
}
else {
const shopItem = zones_1.SHOP_ITEMS.find(s => s.id === reward.itemId);
if (shopItem) {
const qty = reward.quantity ?? 1;
const existing = playerState.inventory.find(i => i.id === shopItem.id);
if (existing) {
existing.quantity += qty;
}
else {
playerState.inventory.push({
id: shopItem.id,
name: shopItem.name,
description: shopItem.description,
quantity: qty,
type: shopItem.itemTypeId,
catchRate: shopItem.catchRate,
});
}
msg = `You found ${qty}× ${shopItem.name}!`;
}
}
showNotification(msg);
advanceNode();
(0, player_1.saveGame)(playerState);
renderMap();
}
function healParty() {
for (const r of playerState.party) {
r.currentHp = r.maxHp;
r.statusEffect = null;
}
showNotification('Your Resonants have been fully restored! 💚');
}
// ==================== PARTY SCREEN ====================
function renderParty() {
let partyHTML = '';
for (let i = 0; i < playerState.party.length; i++) {
const r = playerState.party[i];
const tmpl = (0, resonants_1.getResonantById)(r.templateId);
const hpPct = Math.max(0, (r.currentHp / r.maxHp) * 100);
const xpPct = Math.min(100, (r.xp / r.xpToNext) * 100);
const svg = generateResonantSVG(r, 80);
let movesList = '';
if (tmpl) {
for (const m of tmpl.moves) {
const unlocked = m.unlockLevel <= r.level;
movesList += `<span class="move-tag ${unlocked ? 'unlocked' : 'locked'}">${m.name} Lv.${m.unlockLevel}</span>`;
}
}
partyHTML += `
<div class="party-card glass-card">
<div class="party-sprite">${svg}</div>
<div class="party-info">
<h3>${r.name} <span class="level-badge">Lv.${r.level}</span></h3>
${tmpl ? `<span class="type-badge" style="--badge-color: ${getTypeColor(tmpl.type)}">${tmpl.type}</span>` : ''}
<div class="hp-bar-container"><div class="hp-bar" style="width:${hpPct}%; --hp-color: ${getHpColor(hpPct)}"></div></div>
<div class="xp-bar-container"><div class="xp-bar" style="width:${xpPct}%"></div></div>
<div class="stats-grid">
<span>HP: ${r.maxHp}</span><span>ATK: ${r.atk}</span>
<span>DEF: ${r.def}</span><span>SPD: ${r.spd}</span>
</div>
<div class="moves-list">${movesList}</div>
</div>
</div>
`;
}
// Inventory section
let invHTML = '';
for (const item of playerState.inventory) {
if (item.quantity <= 0)
continue;
const icon = item.type === 'prism' ? '💎' : item.type === 'heal' ? '❤️' : item.type === 'status' ? '✨' : '⚡';
invHTML += `<div class="inv-item">${icon} ${item.name} ×${item.quantity}</div>`;
}
screens.party.innerHTML = `
<div class="party-container">
<h2>Party & Inventory</h2>
<div class="party-grid">${partyHTML || '<p>No Resonants yet!</p>'}</div>
${playerState.party.length < 6 ? `<button id="btn-catch-info" class="action-btn">Capture more Resonants in the wild!</button>` : ''}
<h3 style="margin-top:20px;">Inventory</h3>
<div class="inventory-grid">${invHTML || '<p>No items.</p>'}</div>
<button id="btn-back-party" class="action-btn back-btn">← Back to Map</button>
</div>
`;
document.getElementById('btn-back-party').addEventListener('click', () => { showScreen('map'); renderMap(); });
}
// ==================== SHOP SCREEN ====================
function renderShop() {
let shopHTML = '';
for (const item of zones_1.SHOP_ITEMS) {
const canAfford = playerState.shards >= item.price;
shopHTML += `
<div class="shop-item glass-card">
<div class="shop-info">
<h4>${item.name}</h4>
<p>${item.description}</p>
</div>
<button class="buy-btn ${canAfford ? '' : 'disabled'}" data-shop-id="${item.id}" ${!canAfford ? 'disabled' : ''}>
Buy — ${item.price} 💎
</button>
</div>
`;
}
screens.shop.innerHTML = `
<div class="shop-container">
<h2>Camp Shop</h2>
<p class="shards-display">Aether Shards: ${playerState.shards} 💎</p>
<div class="shop-grid">${shopHTML}</div>
<button id="btn-back-shop" class="action-btn back-btn">← Back to Map</button>
</div>
`;
document.querySelectorAll('.buy-btn:not(.disabled)').forEach(btn => {
btn.addEventListener('click', () => {
const shopId = btn.dataset.shopId;
buyItem(shopId);
});
});
document.getElementById('btn-back-shop').addEventListener('click', () => { showScreen('map'); renderMap(); });
}
function buyItem(shopId) {
const shopItem = zones_1.SHOP_ITEMS.find(s => s.id === shopId);
if (!shopItem || playerState.shards < shopItem.price)
return;
playerState.shards -= shopItem.price;
sound_1.soundManager.select();
const existing = playerState.inventory.find(i => i.id === shopItem.id);
if (existing) {
existing.quantity++;
}
else {
playerState.inventory.push({
id: shopItem.id,
name: shopItem.name,
description: shopItem.description,
quantity: 1,
type: shopItem.itemTypeId,
catchRate: shopItem.catchRate,
});
}
(0, player_1.saveGame)(playerState);
showNotification(`Bought ${shopItem.name}!`);
renderShop();
}
// ==================== DEX SCREEN ====================
function renderDex() {
const allResonants = Object.values(resonants_1.RESONANT_DB).filter(r => !zones_1.BOSS_IDS.includes(r.id));
let dexHTML = '';
for (const r of allResonants) {
const seen = playerState.dexSeen.has(r.id);
const svg = seen ? generateResonantSVG((0, player_1.createResonant)(r.id, 1), 60) : '<div class="dex-hidden">???</div>';
dexHTML += `
<div class="dex-card glass-card ${seen ? '' : 'hidden'}">
<div class="dex-sprite">${svg}</div>
${seen ? `
<h4>${r.name}</h4>
<span class="type-badge" style="--badge-color: ${getTypeColor(r.type)}">${r.type}</span>
<div class="dex-stats">
HP:${r.baseHp} ATK:${r.baseAtk} DEF:${r.baseDef} SPD:${r.baseSpd}
</div>
` : `<h4>Unknown</h4><p>Not yet encountered</p>`}
</div>
`;
}
screens.dex.innerHTML = `
<div class="dex-container">
<h2>Conductors Dex</h2>
<p>${playerState.dexSeen.size}/${allResonants.length} Resonants discovered</p>
<div class="dex-grid">${dexHTML}</div>
<button id="btn-back-dex" class="action-btn back-btn">← Back</button>
</div>
`;
document.getElementById('btn-back-dex').addEventListener('click', () => {
const prevScreen = currentScreen === 'menu' ? 'menu' : 'map';
showScreen(prevScreen);
if (prevScreen === 'menu')
renderMenu();
else
renderMap();
});
}
// ==================== VICTORY SCREEN ====================
function renderVictory() {
screens.victory.innerHTML = `
<div class="victory-container">
<h1>🌟 Victory! 🌟</h1>
<p>You have defeated the Apex Conductor and restored harmony to Aethelgard!</p>
<div class="victory-stats glass-card">
<h3>Journey Complete</h3>
<p>Resonants Discovered: ${playerState.dexSeen.size}/18</p>
<p>Bosses Defeated: ${playerState.completedBosses.length}/4</p>
<p>Total Shards Earned: ${playerState.shards} 💎</p>
</div>
<button id="btn-newgame-victory" class="menu-btn primary">Start New Journey</button>
</div>
`;
document.getElementById('btn-newgame-victory').addEventListener('click', () => {
(0, player_1.resetGame)();
playerState = (0, player_1.createDefaultPlayer)();
showScreen('menu');
renderMenu();
});
}
// ==================== UTILITIES ====================
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function getTypeAdvantage(attackerType, defenderType) {
const chart = {
Pyro: { Flora: 1.5, Hydro: 0.67 },
Hydro: { Pyro: 1.5, Flora: 0.67 },
Flora: { Hydro: 1.5, Pyro: 0.67 },
Zephyr: { Earth: 1.5, Volt: 0.67 },
Earth: { Volt: 1.5, Zephyr: 0.67 },
Volt: { Zephyr: 1.5, Earth: 0.67 },
};
return chart[attackerType]?.[defenderType] ?? 1.0;
}
function getTypeColor(type) {
const colors = {
Pyro: '#FF6B35', Hydro: '#4CC9F0', Flora: '#2D6A4F',
Zephyr: '#A2D2FF', Earth: '#8B7355', Volt: '#FFD700', Cosmos: '#C77DFF',
};
return colors[type] || '#888';
}
function generateResonantSVG(r, size = 120) {
// Delegate to the renderer module via window reference
const fn = window.__generateResonantSVG;
if (fn)
return fn(r, size);
// Fallback simple SVG
const tmpl = (0, resonants_1.getResonantById)(r.templateId);
if (!tmpl)
return '';
const color = getTypeColor(tmpl.type);
return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}"><circle cx="${size / 2}" cy="${size / 2}" r="${size * 0.35}" fill="${color}"/><text x="${size / 2}" y="${size / 2 + 8}" text-anchor="middle" fill="white" font-size="14">${r.name.charAt(0)}</text></svg>`;
}
function showNotification(msg) {
const existing = document.querySelector('.notification');
if (existing)
existing.remove();
const notif = document.createElement('div');
notif.className = 'notification';
notif.textContent = msg;
document.body.appendChild(notif);
setTimeout(() => notif.remove(), 2500);
}
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen3.6-27b-mtp","provider":"lmstudio","persona":"typescript","sessionId":"255d2649-41c0-40ea-9323-9744fe02e2dc","tokensIn":4390755,"tokensOut":63164,"tokensTotal":4453919,"turns":83,"toolCalls":82,"failedToolCalls":9,"timestamp":"2026-07-30T15:37:23.368Z"} -->