Creature Collection
Qwen 3.6 35b a3b · composer

Browser check: Artifact runtime error. This is a technical check, not a quality review.
Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 123,776 bytes · SHA-256 a1601cbf7253
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Aetherials: The Fractured Realm</title>
<style>
/* ── css: palette ── */
:root {
--bg-dark: #0a0a1a;
--bg-card: rgba(15, 15, 35, 0.85);
--glass: rgba(255, 255, 255, 0.08);
--glass-border: rgba(255, 255, 255, 0.15);
--text: #e8e8f0;
--text-dim: #8888aa;
--accent: #6ee7b7;
--accent2: #818cf8;
--danger: #f87171;
--warn: #fbbf24;
--type-virel: #4ade80;
--type-krell: #a78bfa;
--type-thorne: #fb923c;
--type-dusk: #8b5cf6;
--type-flux: #06b6d4;
--type-prism: #fde68a;
--type-aegis: #60a5fa;
--hp-green: #4ade80;
--hp-yellow: #fbbf24;
--hp-red: #f87171;
}
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: var(--bg-dark); color: var(--text); font-family: 'Segoe UI', system-ui, sans-serif; }
#app { width: 100%; height: 100%; position: relative; }
canvas { display: block; width: 100%; height: 100%; }
/* Title Screen */
#title-screen { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; background: linear-gradient(135deg, #0a0a2e 0%, #1a0a3e 50%, #0a1a2e 100%); z-index: 100; }
#title-screen h1 { font-size: 3.5em; background: linear-gradient(90deg, #6ee7b7, #818cf8, #fde68a); -webkit-background-clip: text; -webkit-text-fill-color: transparent; text-shadow: none; margin-bottom: 0.2em; letter-spacing: 0.05em; }
#title-screen .subtitle { color: var(--text-dim); font-size: 1.1em; margin-bottom: 2em; font-style: italic; }
.title-btn { padding: 14px 40px; margin: 8px; font-size: 1.1em; border: 1px solid var(--glass-border); border-radius: 12px; background: var(--glass); color: var(--text); cursor: pointer; backdrop-filter: blur(10px); transition: all 0.3s; }
.title-btn:hover { background: rgba(255,255,255,0.15); transform: scale(1.05); border-color: var(--accent); }
.title-btn:active { transform: scale(0.98); }
/* Overworld HUD */
#overworld-hud { position: absolute; top: 0; left: 0; right: 0; padding: 10px 15px; display: flex; justify-content: space-between; align-items: flex-start; pointer-events: none; z-index: 10; }
#overworld-hud > * { pointer-events: auto; }
.hud-zone { background: var(--glass); backdrop-filter: blur(8px); border: 1px solid var(--glass-border); border-radius: 10px; padding: 8px 16px; font-size: 0.9em; }
.hud-party { display: flex; gap: 6px; }
.party-dot { width: 28px; height: 28px; border-radius: 50%; border: 2px solid var(--glass-border); display: flex; align-items: center; justify-content: center; font-size: 0.7em; font-weight: bold; }
/* Touch Controls */
#touch-controls { position: absolute; bottom: 15px; left: 15px; z-index: 20; display: none; }
.touch-btn { width: 50px; height: 50px; border-radius: 12px; border: 1px solid var(--glass-border); background: var(--glass); backdrop-filter: blur(4px); color: var(--text); font-size: 1.3em; display: flex; align-items: center; justify-content: center; cursor: pointer; user-select: none; -webkit-user-select: none; }
.touch-btn:active { background: rgba(255,255,255,0.2); }
.dpad { display: grid; grid-template-columns: repeat(3, 50px); grid-template-rows: repeat(3, 50px); gap: 4px; }
.dpad .empty { visibility: hidden; }
#touch-actions { position: absolute; bottom: 15px; right: 15px; display: none; gap: 8px; z-index: 20; }
.action-btn { width: 56px; height: 56px; border-radius: 50%; border: 2px solid var(--glass-border); background: var(--glass); backdrop-filter: blur(4px); color: var(--text); font-size: 0.8em; font-weight: bold; display: flex; align-items: center; justify-content: center; cursor: pointer; user-select: none; }
.action-btn:active { background: rgba(255,255,255,0.2); }
/* Battle Scene */
#battle-scene { position: absolute; inset: 0; display: none; z-index: 50; }
#battle-canvas { width: 100%; height: 100%; }
#battle-ui { position: absolute; bottom: 0; left: 0; right: 0; background: var(--bg-card); border-top: 1px solid var(--glass-border); backdrop-filter: blur(12px); padding: 12px 16px; min-height: 200px; }
#battle-text { font-size: 1em; margin-bottom: 8px; min-height: 1.4em; line-height: 1.4; }
#battle-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.battle-btn { padding: 8px 16px; border: 1px solid var(--glass-border); border-radius: 8px; background: var(--glass); color: var(--text); cursor: pointer; font-size: 0.9em; transition: all 0.2s; }
.battle-btn:hover { background: rgba(255,255,255,0.15); }
.battle-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.battle-btn.ability-btn { min-width: 100px; }
.battle-btn.ability-btn.type-virel { border-color: var(--type-virel); color: var(--type-virel); }
.battle-btn.ability-btn.type-krell { border-color: var(--type-krell); color: var(--type-krell); }
.battle-btn.ability-btn.type-thorne { border-color: var(--type-thorne); color: var(--type-thorne); }
.battle-btn.ability-btn.type-dusk { border-color: var(--type-dusk); color: var(--type-dusk); }
.battle-btn.ability-btn.type-flux { border-color: var(--type-flux); color: var(--type-flux); }
.battle-btn.ability-btn.type-prism { border-color: var(--type-prism); color: var(--type-prism); }
.battle-btn.ability-btn.type-aegis { border-color: var(--type-aegis); color: var(--type-aegis); }
/* Battle creature display */
#battle-enemies { position: absolute; top: 20px; right: 40px; display: flex; gap: 30px; }
.battle-creature-display { background: var(--glass); backdrop-filter: blur(8px); border: 1px solid var(--glass-border); border-radius: 12px; padding: 10px 14px; min-width: 160px; }
.battle-creature-display .name { font-size: 0.85em; font-weight: bold; margin-bottom: 4px; }
.battle-creature-display .level { font-size: 0.7em; color: var(--text-dim); }
.hp-bar-container { width: 100%; height: 8px; background: rgba(0,0,0,0.4); border-radius: 4px; overflow: hidden; margin-top: 4px; }
.hp-bar { height: 100%; border-radius: 4px; transition: width 0.5s ease; }
.hp-bar.green { background: var(--hp-green); }
.hp-bar.yellow { background: var(--hp-yellow); }
.hp-bar.red { background: var(--hp-red); }
.hp-text { font-size: 0.7em; color: var(--text-dim); margin-top: 2px; }
.status-icons { display: flex; gap: 3px; margin-top: 3px; }
.status-icon { font-size: 0.65em; padding: 1px 4px; border-radius: 3px; background: rgba(255,255,255,0.1); }
/* Floating damage numbers */
.damage-float { position: absolute; font-weight: bold; font-size: 1.4em; pointer-events: none; animation: floatUp 1s ease-out forwards; z-index: 60; }
.damage-float.crit { font-size: 1.8em; }
@keyframes floatUp { 0% { opacity: 1; transform: translateY(0) scale(1); } 100% { opacity: 0; transform: translateY(-60px) scale(0.7); } }
/* Modals */
.modal-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 80; backdrop-filter: blur(4px); }
.modal { background: var(--bg-card); border: 1px solid var(--glass-border); border-radius: 16px; padding: 24px; max-width: 500px; width: 90%; max-height: 80vh; overflow-y: auto; backdrop-filter: blur(16px); }
.modal h2 { font-size: 1.4em; margin-bottom: 12px; background: linear-gradient(90deg, var(--accent), var(--accent2)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.modal-close { position: absolute; top: 12px; right: 16px; background: none; border: none; color: var(--text-dim); font-size: 1.5em; cursor: pointer; }
.modal-close:hover { color: var(--text); }
/* Encyclopedia */
.encyc-entry { display: flex; gap: 12px; padding: 10px; border-bottom: 1px solid var(--glass-border); align-items: center; }
.encyc-entry .icon { width: 40px; height: 40px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 1.2em; }
.encyc-entry .info { flex: 1; }
.encyc-entry .info .name { font-weight: bold; font-size: 0.95em; }
.encyc-entry .info .detail { font-size: 0.75em; color: var(--text-dim); }
/* Story vignette */
#story-vignette { position: absolute; inset: 0; display: none; z-index: 90; flex-direction: column; align-items: center; justify-content: center; padding: 40px; }
#story-vignette .vignette-text { font-size: 1.3em; line-height: 1.6; text-align: center; max-width: 600px; opacity: 0; animation: vignetteFade 2s ease forwards; }
#story-vignette .vignette-skip { margin-top: 30px; color: var(--text-dim); font-size: 0.85em; opacity: 0; animation: vignetteFade 1s ease 3s forwards; }
@keyframes vignetteFade { 0% { opacity: 0; } 100% { opacity: 1; } }
/* Screen shake */
.shake { animation: screenShake 0.3s ease; }
@keyframes screenShake { 0%, 100% { transform: translate(0); } 20% { transform: translate(-4px, 2px); } 40% { transform: translate(4px, -2px); } 60% { transform: translate(-2px, 4px); } 80% { transform: translate(2px, -4px); } }
/* Capture mini-game */
#capture-game { position: absolute; inset: 0; display: none; z-index: 70; background: rgba(0,0,0,0.7); flex-direction: column; align-items: center; justify-content: center; }
.capture-ring { width: 120px; height: 120px; border-radius: 50%; border: 4px solid var(--glass-border); position: relative; display: flex; align-items: center; justify-content: center; }
.capture-indicator { position: absolute; width: 100%; height: 100%; border-radius: 50%; border: 4px solid transparent; border-top-color: var(--accent); animation: captureSpin 1s linear infinite; }
.capture-zone { position: absolute; width: 100%; height: 100%; border-radius: 50%; border: 4px solid transparent; }
@keyframes captureSpin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.capture-btn { padding: 16px 48px; font-size: 1.2em; border-radius: 12px; border: 2px solid var(--accent); background: var(--glass); color: var(--accent); cursor: pointer; margin-top: 20px; }
.capture-btn:hover { background: rgba(110, 231, 183, 0.2); }
/* Shrine overlay */
#shrine-overlay { position: absolute; inset: 0; display: none; z-index: 75; background: rgba(0,0,0,0.5); flex-direction: column; align-items: center; justify-content: center; }
.shrine-panel { background: var(--bg-card); border: 1px solid var(--glass-border); border-radius: 16px; padding: 24px; text-align: center; backdrop-filter: blur(16px); }
.shrine-panel h2 { font-size: 1.6em; margin-bottom: 16px; color: var(--warn); }
.shrine-btn { padding: 10px 24px; margin: 6px; border: 1px solid var(--glass-border); border-radius: 8px; background: var(--glass); color: var(--text); cursor: pointer; font-size: 0.95em; }
.shrine-btn:hover { background: rgba(255,255,255,0.15); }
/* Pause menu */
#pause-menu { position: absolute; inset: 0; display: none; z-index: 85; background: rgba(0,0,0,0.7); flex-direction: column; align-items: center; justify-content: center; }
#pause-menu .modal { text-align: center; }
#pause-menu .pause-btn { padding: 12px 32px; margin: 8px; border: 1px solid var(--glass-border); border-radius: 10px; background: var(--glass); color: var(--text); cursor: pointer; font-size: 1em; }
#pause-menu .pause-btn:hover { background: rgba(255,255,255,0.15); }
/* Inventory */
.inv-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; border-bottom: 1px solid var(--glass-border); }
.inv-item .item-name { font-weight: bold; font-size: 0.9em; }
.inv-item .item-count { color: var(--text-dim); font-size: 0.8em; }
.inv-item .item-btn { padding: 4px 12px; border-radius: 6px; border: 1px solid var(--glass-border); background: var(--glass); color: var(--text); cursor: pointer; font-size: 0.8em; }
.inv-item .item-btn:hover { background: rgba(255,255,255,0.15); }
/* Tab navigation */
.tab-bar { display: flex; gap: 4px; margin-bottom: 12px; border-bottom: 1px solid var(--glass-border); padding-bottom: 8px; }
.tab-btn { padding: 6px 14px; border: none; border-radius: 6px; background: var(--glass); color: var(--text-dim); cursor: pointer; font-size: 0.85em; }
.tab-btn.active { background: rgba(255,255,255,0.15); color: var(--text); }
/* Scrollbar */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--glass-border); border-radius: 3px; }
/* Responsive */
@media (max-width: 600px) {
#title-screen h1 { font-size: 2.2em; }
.modal { padding: 16px; width: 95%; }
#battle-ui { min-height: 180px; padding: 8px 10px; }
.battle-btn { padding: 6px 10px; font-size: 0.8em; }
}
/* ── css: animations ── */
@keyframes pulse { 0%, 100% { opacity: 0.6; } 50% { opacity: 1; } }
@keyframes glow { 0%, 100% { box-shadow: 0 0 8px rgba(110, 231, 183, 0.3); } 50% { box-shadow: 0 0 20px rgba(110, 231, 183, 0.6); } }
@keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }
@keyframes enemyLunge { 0% { transform: scale(1); } 30% { transform: scale(1.1) translateX(-10px); } 100% { transform: scale(1); } }
@keyframes enemyHit { 0% { transform: translateX(0); filter: brightness(1); } 25% { transform: translateX(8px); filter: brightness(2); } 50% { transform: translateX(-4px); filter: brightness(1.5); } 100% { transform: translateX(0); filter: brightness(1); } }
@keyframes playerLunge { 0% { transform: scale(1); } 30% { transform: scale(1.1) translateX(10px); } 100% { transform: scale(1); } }
@keyframes playerHit { 0% { transform: translateX(0); filter: brightness(1); } 25% { transform: translateX(-8px); filter: brightness(2); } 50% { transform: translateX(4px); filter: brightness(1.5); } 100% { transform: translateX(0); filter: brightness(1); } }
@keyframes captureFlash { 0% { opacity: 0; transform: scale(0.5); } 50% { opacity: 1; transform: scale(1.2); } 100% { opacity: 0; transform: scale(1.5); } }
@keyframes particleFade { 0% { opacity: 1; transform: translateY(0); } 100% { opacity: 0; transform: translateY(-30px); } }
.pulse { animation: pulse 2s ease infinite; }
.glow { animation: glow 3s ease infinite; }
/* ── css: capture_anim ── */
@keyframes captureSpin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
@keyframes vignetteFade { 0% { opacity: 0; transform: translateY(10px); } 100% { opacity: 1; transform: translateY(0); } }
@keyframes screenShake { 0%, 100% { transform: translate(0) rotate(0); } 10% { transform: translate(-4px, 2px) rotate(-0.5deg); } 20% { transform: translate(4px, -2px) rotate(0.5deg); } 30% { transform: translate(-2px, 4px) rotate(-0.3deg); } 40% { transform: translate(2px, -4px) rotate(0.3deg); } 50% { transform: translate(-3px, 1px) rotate(-0.2deg); } 60% { transform: translate(3px, -1px) rotate(0.2deg); } 70% { transform: translate(-1px, 3px) rotate(-0.1deg); } 80% { transform: translate(1px, -3px) rotate(0.1deg); } 90% { transform: translate(0, 0); } }
.shake { animation: screenShake 0.3s ease; }
.capture-indicator { animation: captureSpin 1s linear infinite; transform-origin: center; }
.vignette-text { animation: vignetteFade 2s ease forwards; }
.vignette-skip { animation: vignetteFade 1s ease 3s forwards; }
#title-screen { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; background: linear-gradient(135deg, #0a0a2e 0%, #1a0a3e 50%, #0a1a2e 100%); z-index: 100; }
#title-screen h1 { font-size: 3.5em; background: linear-gradient(90deg, #6ee7b7, #818cf8, #fde68a); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-bottom: 0.2em; letter-spacing: 0.05em; }
#title-screen .subtitle { color: #8888aa; font-size: 1.1em; margin-bottom: 2em; font-style: italic; }
.title-btn { padding: 14px 40px; margin: 8px; font-size: 1.1em; border: 1px solid rgba(255,255,255,0.15); border-radius: 12px; background: rgba(255,255,255,0.08); color: #e8e8f0; cursor: pointer; backdrop-filter: blur(10px); transition: all 0.3s; }
.title-btn:hover { background: rgba(255,255,255,0.15); transform: scale(1.05); border-color: #6ee7b7; }
.title-btn:active { transform: scale(0.98); }
#overworld-hud { position: absolute; top: 0; left: 0; right: 0; padding: 10px 15px; display: flex; justify-content: space-between; align-items: flex-start; pointer-events: none; z-index: 10; }
#overworld-hud > * { pointer-events: auto; }
.hud-zone { background: rgba(15,15,35,0.85); backdrop-filter: blur(8px); border: 1px solid rgba(255,255,255,0.15); border-radius: 10px; padding: 8px 16px; font-size: 0.9em; }
.hud-party { display: flex; gap: 6px; }
.party-dot { width: 28px; height: 28px; border-radius: 50%; border: 2px solid rgba(255,255,255,0.15); display: flex; align-items: center; justify-content: center; font-size: 0.7em; font-weight: bold; }
.battle-btn { padding: 8px 16px; border: 1px solid rgba(255,255,255,0.15); border-radius: 8px; background: rgba(255,255,255,0.08); color: #e8e8f0; cursor: pointer; font-size: 0.9em; transition: all 0.2s; }
.battle-btn:hover { background: rgba(255,255,255,0.15); }
.battle-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.battle-btn.ability-btn { min-width: 100px; }
.battle-btn.ability-btn.type-virel { border-color: #4ade80; color: #4ade80; }
.battle-btn.ability-btn.type-krell { border-color: #a78bfa; color: #a78bfa; }
.battle-btn.ability-btn.type-thorne { border-color: #fb923c; color: #fb923c; }
.battle-btn.ability-btn.type-dusk { border-color: #8b5cf6; color: #8b5cf6; }
.battle-btn.ability-btn.type-flux { border-color: #06b6d4; color: #06b6d4; }
.battle-btn.ability-btn.type-prism { border-color: #fde68a; color: #fde68a; }
.battle-btn.ability-btn.type-aegis { border-color: #60a5fa; color: #60a5fa; }
.modal-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 80; backdrop-filter: blur(4px); }
.modal { background: rgba(15,15,35,0.95); border: 1px solid rgba(255,255,255,0.15); border-radius: 16px; padding: 24px; max-width: 500px; width: 90%; max-height: 80vh; overflow-y: auto; backdrop-filter: blur(16px); }
.modal h2 { font-size: 1.4em; margin-bottom: 12px; background: linear-gradient(90deg, #6ee7b7, #818cf8); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.pause-btn { padding: 12px 32px; margin: 8px; border: 1px solid rgba(255,255,255,0.15); border-radius: 10px; background: rgba(255,255,255,0.08); color: #e8e8f0; cursor: pointer; font-size: 1em; }
.pause-btn:hover { background: rgba(255,255,255,0.15); }
.shrine-btn { padding: 10px 24px; margin: 6px; border: 1px solid rgba(255,255,255,0.15); border-radius: 8px; background: rgba(255,255,255,0.08); color: #e8e8f0; cursor: pointer; font-size: 0.95em; }
.shrine-btn:hover { background: rgba(255,255,255,0.15); }
.inv-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; border-bottom: 1px solid rgba(255,255,255,0.1); }
.inv-item .item-name { font-weight: bold; font-size: 0.9em; }
.inv-item .item-count { color: #8888aa; font-size: 0.8em; }
.inv-item .item-btn { padding: 4px 12px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.15); background: rgba(255,255,255,0.08); color: #e8e8f0; cursor: pointer; font-size: 0.8em; }
.inv-item .item-btn:hover { background: rgba(255,255,255,0.15); }
.encyc-entry { display: flex; gap: 12px; padding: 10px; border-bottom: 1px solid rgba(255,255,255,0.1); align-items: center; }
.encyc-entry .icon { width: 40px; height: 40px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 1.2em; }
.encyc-entry .info { flex: 1; }
.encyc-entry .info .name { font-weight: bold; font-size: 0.95em; }
.encyc-entry .info .detail { font-size: 0.75em; color: #8888aa; }
.capture-ring { width: 120px; height: 120px; border-radius: 50%; border: 4px solid rgba(255,255,255,0.15); position: relative; display: flex; align-items: center; justify-content: center; }
.capture-zone { position: absolute; width: 100%; height: 100%; border-radius: 50%; border: 4px solid transparent; }
.capture-btn { padding: 16px 48px; font-size: 1.2em; border-radius: 12px; border: 2px solid #6ee7b7; background: rgba(255,255,255,0.08); color: #6ee7b7; cursor: pointer; margin-top: 20px; }
.capture-btn:hover { background: rgba(110,231,183,0.2); }
.dpad { display: grid; grid-template-columns: repeat(3, 50px); grid-template-rows: repeat(3, 50px); gap: 4px; }
.dpad .empty { visibility: hidden; }
.touch-btn { width: 50px; height: 50px; border-radius: 12px; border: 1px solid rgba(255,255,255,0.15); background: rgba(255,255,255,0.08); color: #e8e8f0; font-size: 1.3em; display: flex; align-items: center; justify-content: center; cursor: pointer; user-select: none; -webkit-user-select: none; }
.touch-btn:active { background: rgba(255,255,255,0.2); }
.action-btn { width: 56px; height: 56px; border-radius: 50%; border: 2px solid rgba(255,255,255,0.15); background: rgba(255,255,255,0.08); color: #e8e8f0; font-size: 0.8em; font-weight: bold; display: flex; align-items: center; justify-content: center; cursor: pointer; user-select: none; }
.action-btn:active { background: rgba(255,255,255,0.2); }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 3px; }
@media (max-width: 600px) {
#title-screen h1 { font-size: 2.2em; }
.modal { padding: 16px; width: 95%; }
#battle-ui { min-height: 180px; padding: 8px 10px; }
.battle-btn { padding: 6px 10px; font-size: 0.8em; }
}
</style>
</head>
<body>
<!-- Title Screen -->
<div id="title-screen">
<canvas id="title-canvas" style="position:absolute;inset:0;z-index:0;"></canvas>
<div style="position:relative;z-index:1;text-align:center;">
<h1>AETHERIALS</h1>
<div class="subtitle">The Fractured Realm</div>
<button id="new-binding" class="title-btn">New Binding</button>
<button id="continue-btn" class="title-btn" style="display:none;">Continue</button>
</div>
</div>
<!-- Overworld -->
<div id="overworld-hud" style="display:none;">
<div class="hud-zone" id="hud-zone-name">Verdant Hollow</div>
<div class="hud-party" id="hud-party"></div>
<div style="display:flex;gap:4px;">
<button class="battle-btn" onclick="showPauseMenu()" style="padding:4px 10px;font-size:0.8em;">⏸</button>
<button class="battle-btn" onclick="showEncyclopedia()" style="padding:4px 10px;font-size:0.8em;">📖</button>
<button class="battle-btn" onclick="toggleMute()" style="padding:4px 10px;font-size:0.8em;" id="mute-btn">🔊</button>
</div>
</div>
<canvas id="overworld-canvas" style="display:none;"></canvas>
<!-- Touch Controls -->
<div id="touch-controls" style="display:none;">
<div class="dpad">
<div class="empty"></div>
<button class="touch-btn" id="btn-up">▲</button>
<div class="empty"></div>
<button class="touch-btn" id="btn-left">◀</button>
<div class="empty"></div>
<button class="touch-btn" id="btn-right">▶</button>
<div class="empty"></div>
<button class="touch-btn" id="btn-down">▼</button>
<div class="empty"></div>
</div>
</div>
<div id="touch-actions" style="display:none;">
<button class="action-btn" onclick="showPauseMenu()">⏸</button>
<button class="action-btn" onclick="showEncyclopedia()">📖</button>
</div>
<!-- Battle Scene -->
<div id="battle-scene" style="display:none;">
<canvas id="battle-canvas" style="width:100%;height:100%;"></canvas>
<div id="battle-ui">
<div id="battle-text" style="font-size:1em;margin-bottom:8px;min-height:1.4em;line-height:1.4;color:#e8e8f0;"></div>
<div id="battle-actions" style="display:flex;gap:8px;flex-wrap:wrap;"></div>
</div>
</div>
<!-- Capture Mini-game -->
<div id="capture-game" style="display:none;position:absolute;inset:0;z-index:70;background:rgba(0,0,0,0.7);flex-direction:column;align-items:center;justify-content:center;">
<div style="color:#e8e8f0;font-size:1.2em;margin-bottom:20px;">Tap when the ring aligns!</div>
<div class="capture-ring">
<div class="capture-indicator" id="capture-indicator"></div>
<div class="capture-zone" id="capture-zone" style="border-color:transparent;"></div>
<div style="font-size:2em;" id="capture-creature-emoji">🐾</div>
</div>
<button class="capture-btn" id="capture-tap-btn" onclick="tapCapture()">TAP!</button>
<div id="capture-result" style="margin-top:16px;font-size:1em;color:#e8e8f0;"></div>
</div>
<!-- Shrine Overlay -->
<div id="shrine-overlay" style="display:none;position:absolute;inset:0;z-index:75;background:rgba(0,0,0,0.5);flex-direction:column;align-items:center;justify-content:center;">
<div class="shrine-panel" id="shrine-panel" style="text-align:center;">
<h2>⛩️ Aether Shrine</h2>
<p style="color:var(--text-dim);margin-bottom:16px;">The shrine hums with Aetherium energy.</p>
<button class="shrine-btn" onclick="shrineHeal()">💚 Heal Party</button>
<button class="shrine-btn" onclick="shrineSave()">💾 Save</button>
<button class="shrine-btn" onclick="shrineShop()">🛒 Shop</button>
<button class="shrine-btn" onclick="shrineClose()">🚪 Leave</button>
</div>
</div>
<!-- Story Vignette -->
<div id="story-vignette" style="display:none;position:absolute;inset:0;z-index:90;flex-direction:column;align-items:center;justify-content:center;padding:40px;">
<div class="vignette-text" id="vignette-text" style="font-size:1.3em;line-height:1.6;text-align:center;max-width:600px;opacity:0;animation:vignetteFade 2s ease forwards;"></div>
<div class="vignette-skip" style="margin-top:30px;color:var(--text-dim);font-size:0.85em;opacity:0;animation:vignetteFade 1s ease 3s forwards;">Tap to skip</div>
</div>
<!-- Pause Menu -->
<div id="pause-menu" style="display:none;position:absolute;inset:0;z-index:85;background:rgba(0,0,0,0.7);flex-direction:column;align-items:center;justify-content:center;">
<div class="modal" style="text-align:center;">
<h2>⏸️ Paused</h2>
<button class="pause-btn" onclick="hideModal();game.screen='overworld';showScreen('overworld');">▶️ Resume</button>
<button class="pause-btn" onclick="showEncyclopedia();">📖 Codex</button>
<button class="pause-btn" onclick="showPartyScreen();">👥 Party</button>
<button class="pause-btn" onclick="showInventoryScreen();">🎒 Inventory</button>
<button class="pause-btn" onclick="saveGame(1);hideModal();">💾 Save</button>
<button class="pause-btn" onclick="toggleMute();showPauseMenu();">🔊 Audio</button>
<button class="pause-btn" onclick="hideModal();showScreen('title');">🏠 Title</button>
</div>
</div>
<script>
'use strict';
// ── const: TYPES ──
// ===== WORLD TYPE SYSTEM =====
const TYPES = ['virel','krell','thorne','dusk','flux','prism','aegis'];
// ── const: TYPE_COLORS ──
const TYPE_COLORS = { virel:'#4ade80', krell:'#a78bfa', thorne:'#fb923c', dusk:'#8b5cf6', flux:'#06b6d4', prism:'#fde68a', aegis:'#60a5fa' };
// ── const: TYPE_NAMES ──
const TYPE_NAMES = { virel:'Virel', krell:'Krell', thorne:'Thorne', dusk:'Dusk', flux:'Flux', prism:'Prism', aegis:'Aegis' };
// ── const: TYPE_EMOJI ──
const TYPE_EMOJI = { virel:'🌿', krell:'🪨', thorne:'🌵', dusk:'🌑', flux:'⚡', prism:'💎', aegis:'🛡️' };
// ── const: TYPE_CHART ──
// Type effectiveness chart
// virel > krell > thorne > virel
// dusk > virel & thorne
// flux > dusk & krell
// prism > all slightly
// aegis: resists all, weak to prism
const TYPE_CHART = {
virel: { krell: 1.5, thorne: 0.5, virel: 1.0, dusk: 1.0, flux: 1.0, prism: 0.8, aegis: 1.0 },
krell: { thorne: 1.5, virel: 0.5, krell: 1.0, dusk: 1.0, flux: 0.5, prism: 0.8, aegis: 1.0 },
thorne: { virel: 1.5, krell: 0.5, thorne: 1.0, dusk: 0.5, flux: 1.0, prism: 0.8, aegis: 1.0 },
dusk: { virel: 1.5, thorne: 1.5, dusk: 1.0, krell: 1.0, flux: 0.5, prism: 0.8, aegis: 1.0 },
flux: { dusk: 1.5, krell: 1.5, flux: 1.0, virel: 1.0, thorne: 1.0, prism: 0.8, aegis: 1.0 },
prism: { virel: 1.3, krell: 1.3, thorne: 1.3, dusk: 1.3, flux: 1.3, prism: 1.0, aegis: 1.3 },
aegis: { virel: 0.7, krell: 0.7, thorne: 0.7, dusk: 0.7, flux: 0.7, prism: 1.5, aegis: 1.0 }
};
// ── const: CREATURES ──
// ===== CREATURE DATABASE =====
const CREATURES = {
emberlit: {
name: 'Emberlit', emoji: '🔥', affinity: 'virel',
baseStats: { hp: 45, atk: 38, def: 22, spd: 35 },
lore: 'A small flame spirit born from the first warmth of the Aetherium.',
growth: { hp: 8, atk: 7, def: 4, spd: 5 },
abilities: ['scorch', 'ember_blast', 'flame_wisp', 'blaze_fury'],
evolve: { to: 'pyraxis', atLevel: 20, catalyst: 'ember_crystal' },
captureRate: 180, encounterLevel: [1, 4]
},
stonewisp: {
name: 'Stonewisp', emoji: '🪨', affinity: 'krell',
baseStats: { hp: 50, atk: 30, def: 35, spd: 20 },
lore: 'A patient rock sprite that shapes itself over centuries of quiet thought.',
growth: { hp: 9, atk: 5, def: 6, spd: 3 },
abilities: ['rock_throw', 'stone_wall', 'quarry_strike', 'earth_shield'],
evolve: { to: 'lithos', atLevel: 20, catalyst: 'stone_fragment' },
captureRate: 170, encounterLevel: [2, 5]
},
thornling: {
name: 'Thornling', emoji: '🌵', affinity: 'thorne',
baseStats: { hp: 42, atk: 40, def: 20, spd: 38 },
lore: 'A restless vine creature that lashes out when its territory is threatened.',
growth: { hp: 7, atk: 8, def: 3, spd: 6 },
abilities: ['vine_whip', 'thorn_barrage', 'suffocate', 'bloom_strike'],
evolve: { to: 'briarthorn', atLevel: 20, catalyst: 'thorn_seed' },
captureRate: 160, encounterLevel: [3, 6]
},
pyraxis: {
name: 'Pyraxis', emoji: '🔥', affinity: 'virel',
baseStats: { hp: 65, atk: 55, def: 35, spd: 48 },
lore: 'The fully awakened flame spirit, its body burns with the memory of a thousand suns.',
growth: { hp: 10, atk: 9, def: 5, spd: 6 },
abilities: ['scorch', 'ember_blast', 'flame_wisp', 'blaze_fury'],
evolve: null, captureRate: 60, encounterLevel: [25, 35]
},
lithos: {
name: 'Lithos', emoji: '🏔️', affinity: 'krell',
baseStats: { hp: 80, atk: 48, def: 55, spd: 30 },
lore: 'A living mountain that walks the Aetherium\'s ridges with ancient purpose.',
growth: { hp: 12, atk: 7, def: 8, spd: 4 },
abilities: ['rock_throw', 'stone_wall', 'quarry_strike', 'earth_shield'],
evolve: null, captureRate: 55, encounterLevel: [25, 35]
},
briarthorn: {
name: 'Briarthorn', emoji: '🌹', affinity: 'thorne',
baseStats: { hp: 60, atk: 62, def: 32, spd: 52 },
lore: 'A magnificent but deadly thorned bloom that feeds on the Aetherium\'s energy.',
growth: { hp: 9, atk: 10, def: 4, spd: 7 },
abilities: ['vine_whip', 'thorn_barrage', 'suffocate', 'bloom_strike'],
evolve: null, captureRate: 50, encounterLevel: [25, 35]
},
dusk_moth: {
name: 'Dusk Moth', emoji: '🦋', affinity: 'dusk',
baseStats: { hp: 40, atk: 35, def: 18, spd: 42 },
lore: 'A shadow-winged moth that feeds on the fading light between zones.',
growth: { hp: 7, atk: 6, def: 3, spd: 6 },
abilities: ['shadow_wing', 'void_dust', 'nightmare', 'gloom_burst'],
evolve: { to: 'umbrafly', atLevel: 22, catalyst: 'shadow_wing' },
captureRate: 150, encounterLevel: [5, 10]
},
umbrafly: {
name: 'Umbrafly', emoji: '🌑', affinity: 'dusk',
baseStats: { hp: 58, atk: 52, def: 28, spd: 58 },
lore: 'The evolved shadow-fly whose wings can swallow light itself.',
growth: { hp: 9, atk: 8, def: 4, spd: 7 },
abilities: ['shadow_wing', 'void_dust', 'nightmare', 'gloom_burst'],
evolve: null, captureRate: 55, encounterLevel: [28, 38]
},
flux_spark: {
name: 'Flux Spark', emoji: '⚡', affinity: 'flux',
baseStats: { hp: 38, atk: 42, def: 15, spd: 45 },
lore: 'A crackling energy sprite that dances between the Aetherium\'s ley lines.',
growth: { hp: 6, atk: 7, def: 3, spd: 6 },
abilities: ['spark_bolt', 'overload', 'chain_lightning', 'surge'],
evolve: { to: 'voltex', atLevel: 22, catalyst: 'lightning_core' },
captureRate: 140, encounterLevel: [6, 12]
},
voltex: {
name: 'Voltex', emoji: '🌩️', affinity: 'flux',
baseStats: { hp: 55, atk: 60, def: 25, spd: 62 },
lore: 'A living storm that commands the Aetherium\'s electrical currents.',
growth: { hp: 8, atk: 9, def: 4, spd: 8 },
abilities: ['spark_bolt', 'overload', 'chain_lightning', 'surge'],
evolve: null, captureRate: 50, encounterLevel: [30, 40]
},
prism_fae: {
name: 'Prism Fae', emoji: '💎', affinity: 'prism',
baseStats: { hp: 44, atk: 36, def: 24, spd: 40 },
lore: 'A crystalline fairy that refracts the Aetherium\'s light into living color.',
growth: { hp: 7, atk: 6, def: 4, spd: 5 },
abilities: ['prism_ray', 'refraction', 'luster_burst', 'prismatic_heal'],
evolve: { to: 'crystalla', atLevel: 24, catalyst: 'prism_shard' },
captureRate: 130, encounterLevel: [8, 15]
},
crystalla: {
name: 'Crystalla', emoji: '✨', affinity: 'prism',
baseStats: { hp: 62, atk: 52, def: 38, spd: 55 },
lore: 'The apex crystal fairy whose body becomes a living prism of pure Aetherium.',
growth: { hp: 9, atk: 8, def: 5, spd: 7 },
abilities: ['prism_ray', 'refraction', 'luster_burst', 'prismatic_heal'],
evolve: null, captureRate: 45, encounterLevel: [32, 42]
},
aegis_knight: {
name: 'Aegis Knight', emoji: '🛡️', affinity: 'aegis',
baseStats: { hp: 55, atk: 32, def: 42, spd: 25 },
lore: 'An ancient guardian construct that patrols the boundaries of the Aetherium.',
growth: { hp: 9, atk: 5, def: 6, spd: 3 },
abilities: ['shield_bash', 'iron_wall', 'guardian_strike', 'fortify'],
evolve: { to: 'sentinel', atLevel: 24, catalyst: 'aegis_core' },
captureRate: 120, encounterLevel: [10, 18]
},
sentinel: {
name: 'Sentinel', emoji: '⚔️', affinity: 'aegis',
baseStats: { hp: 78, atk: 48, def: 62, spd: 38 },
lore: 'The ultimate guardian, its shield can deflect even the Aetherium\'s raw power.',
growth: { hp: 11, atk: 7, def: 8, spd: 5 },
abilities: ['shield_bash', 'iron_wall', 'guardian_strike', 'fortify'],
evolve: null, captureRate: 40, encounterLevel: [35, 45]
},
aether_worm: {
name: 'Aether Worm', emoji: '🐛', affinity: 'virel',
baseStats: { hp: 48, atk: 28, def: 30, spd: 22 },
lore: 'A burrowing creature that feeds on the Aetherium\'s energy deposits.',
growth: { hp: 8, atk: 5, def: 5, spd: 3 },
abilities: ['burrow', 'energy_drain', 'aether_bite', 'dig_down'],
evolve: { to: 'aether_drake', atLevel: 26, catalyst: 'aether_heart' },
captureRate: 160, encounterLevel: [10, 20]
},
aether_drake: {
name: 'Aether Drake', emoji: '🐉', affinity: 'virel',
baseStats: { hp: 72, atk: 50, def: 45, spd: 35 },
lore: 'A majestic drake born from the Aetherium\'s deepest energy currents.',
growth: { hp: 10, atk: 8, def: 6, spd: 5 },
abilities: ['burrow', 'energy_drain', 'aether_bite', 'dig_down'],
evolve: null, captureRate: 40, encounterLevel: [38, 48]
},
glow_moss: {
name: 'Glow Moss', emoji: '🌱', affinity: 'virel',
baseStats: { hp: 52, atk: 25, def: 28, spd: 18 },
lore: 'A sentient moss that glows with the Aetherium\'s gentle warmth.',
growth: { hp: 8, atk: 4, def: 4, spd: 2 },
abilities: ['spore_cloud', 'regenerate', 'moss_bond', 'photosynthesis'],
evolve: { to: 'moss_giant', atLevel: 26, catalyst: 'glow_cap' },
captureRate: 150, encounterLevel: [12, 22]
},
moss_giant: {
name: 'Moss Giant', emoji: '🌳', affinity: 'virel',
baseStats: { hp: 85, atk: 42, def: 48, spd: 28 },
lore: 'A towering forest spirit whose roots reach the Aetherium\'s core.',
growth: { hp: 11, atk: 6, def: 7, spd: 4 },
abilities: ['spore_cloud', 'regenerate', 'moss_bond', 'photosynthesis'],
evolve: null, captureRate: 40, encounterLevel: [40, 50]
},
void_fox: {
name: 'Void Fox', emoji: '🦊', affinity: 'dusk',
baseStats: { hp: 42, atk: 44, def: 16, spd: 48 },
lore: 'A sly shadow-fox that slips between the Aetherium\'s zones unnoticed.',
growth: { hp: 7, atk: 7, def: 3, spd: 6 },
abilities: ['shadow_pounce', 'void_step', 'dark_bite', 'phantom_fade'],
evolve: { to: 'nether_wolf', atLevel: 28, catalyst: 'void_fang' },
captureRate: 130, encounterLevel: [14, 24]
},
nether_wolf: {
name: 'Nether Wolf', emoji: '🐺', affinity: 'dusk',
baseStats: { hp: 62, atk: 62, def: 28, spd: 62 },
lore: 'A pack hunter of the shadows, its howl echoes through the Aetherium.',
growth: { hp: 9, atk: 9, def: 4, spd: 8 },
abilities: ['shadow_pounce', 'void_step', 'dark_bite', 'phantom_fade'],
evolve: null, captureRate: 35, encounterLevel: [42, 50]
},
crystal_slug: {
name: 'Crystal Slug', emoji: '🐌', affinity: 'krell',
baseStats: { hp: 46, atk: 22, def: 38, spd: 12 },
lore: 'A slow-moving slug that secretes crystalline armor from Aetherium minerals.',
growth: { hp: 8, atk: 3, def: 5, spd: 2 },
abilities: ['crystal_shell', 'acid_trail', 'mineral_bite', 'harden'],
evolve: { to: 'gem_shield', atLevel: 28, catalyst: 'crystal_gel' },
captureRate: 140, encounterLevel: [15, 25]
},
gem_shield: {
name: 'Gem Shield', emoji: '💠', affinity: 'krell',
baseStats: { hp: 70, atk: 38, def: 58, spd: 22 },
lore: 'A living gem that hardens its shell with layers of Aetherium crystals.',
growth: { hp: 10, atk: 5, def: 7, spd: 3 },
abilities: ['crystal_shell', 'acid_trail', 'mineral_bite', 'harden'],
evolve: null, captureRate: 35, encounterLevel: [44, 50]
},
storm_jelly: {
name: 'Storm Jelly', emoji: '🪼', affinity: 'flux',
baseStats: { hp: 36, atk: 38, def: 14, spd: 44 },
lore: 'A translucent jellyfish that conducts Aetherium electricity through its tentacles.',
growth: { hp: 6, atk: 6, def: 3, spd: 6 },
abilities: ['electro_shock', 'tentacle_whip', 'static_burst', 'jelly_wrap'],
evolve: { to: 'tempest_ray', atLevel: 30, catalyst: 'storm_heart' },
captureRate: 120, encounterLevel: [18, 28]
},
tempest_ray: {
name: 'Tempest Ray', emoji: '🐟', affinity: 'flux',
baseStats: { hp: 58, atk: 56, def: 24, spd: 58 },
lore: 'A floating ray that channels storms across the Aetherium\'s skies.',
growth: { hp: 8, atk: 8, def: 4, spd: 7 },
abilities: ['electro_shock', 'tentacle_whip', 'static_burst', 'jelly_wrap'],
evolve: null, captureRate: 30, encounterLevel: [45, 50]
},
aura_bloom: {
name: 'Aura Bloom', emoji: '🌸', affinity: 'prism',
baseStats: { hp: 48, atk: 30, def: 26, spd: 32 },
lore: 'A flower that radiates healing light from the Aetherium\'s purest crystals.',
growth: { hp: 8, atk: 5, def: 4, spd: 4 },
abilities: ['aura_heal', 'petal_storm', 'bloom_burst', 'light_bond'],
evolve: { to: 'luminaria', atLevel: 30, catalyst: 'aura_petals' },
captureRate: 110, encounterLevel: [20, 30]
},
luminaria: {
name: 'Luminaria', emoji: '🌟', affinity: 'prism',
baseStats: { hp: 68, atk: 46, def: 40, spd: 48 },
lore: 'The radiant bloom whose light can mend even the deepest Aetherium wounds.',
growth: { hp: 10, atk: 7, def: 5, spd: 6 },
abilities: ['aura_heal', 'petal_storm', 'bloom_burst', 'light_bond'],
evolve: null, captureRate: 30, encounterLevel: [46, 50]
},
guardian_ward: {
name: 'Guardian Ward', emoji: '🗿', affinity: 'aegis',
baseStats: { hp: 60, atk: 35, def: 45, spd: 20 },
lore: 'A stone guardian animated by the Aetherium\'s protective will.',
growth: { hp: 10, atk: 5, def: 6, spd: 3 },
abilities: ['ward_strike', 'stone_guard', 'aegis_wall', 'protective_roar'],
evolve: { to: 'ward_lord', atLevel: 32, catalyst: 'ward_stone' },
captureRate: 100, encounterLevel: [22, 35]
},
ward_lord: {
name: 'Ward Lord', emoji: '👑', affinity: 'aegis',
baseStats: { hp: 88, atk: 52, def: 65, spd: 32 },
lore: 'The sovereign of guardians, its authority commands the Aetherium\'s defenses.',
growth: { hp: 12, atk: 7, def: 8, spd: 5 },
abilities: ['ward_strike', 'stone_guard', 'aegis_wall', 'protective_roar'],
evolve: null, captureRate: 25, encounterLevel: [48, 50]
}
};
// ── const: ABILITIES ──
// ===== ABILITY DATABASE (40+ abilities) =====
const ABILITIES = {
// Virel abilities
scorch: { name: 'Scorch', type: 'virel', power: 40, accuracy: 100, pp: 20, effect: 'burn', effectChance: 0.3 },
ember_blast: { name: 'Ember Blast', type: 'virel', power: 65, accuracy: 95, pp: 15, effect: 'burn', effectChance: 0.25 },
flame_wisp: { name: 'Flame Wisp', type: 'virel', power: 30, accuracy: 100, pp: 25, effect: 'heal_self', effectChance: 0.4 },
blaze_fury: { name: 'Blaze Fury', type: 'virel', power: 90, accuracy: 85, pp: 10, effect: 'recoil', effectChance: 0.3 },
energy_drain: { name: 'Energy Drain', type: 'virel', power: 50, accuracy: 95, pp: 15, effect: 'steal_hp', effectChance: 0.5 },
aether_bite: { name: 'Aether Bite', type: 'virel', power: 55, accuracy: 90, pp: 18, effect: 'energy_boost', effectChance: 0.2 },
photosynthesis: { name: 'Photosynthesis', type: 'virel', power: 0, accuracy: 100, pp: 10, effect: 'self_heal', effectChance: 0.6 },
moss_bond: { name: 'Moss Bond', type: 'virel', power: 35, accuracy: 100, pp: 15, effect: 'bind', effectChance: 0.35 },
// Krell abilities
rock_throw: { name: 'Rock Throw', type: 'krell', power: 45, accuracy: 100, pp: 20, effect: 'frost', effectChance: 0.15 },
stone_wall: { name: 'Stone Wall', type: 'krell', power: 0, accuracy: 100, pp: 15, effect: 'barrier', effectChance: 0.7 },
quarry_strike: { name: 'Quarry Strike', type: 'krell', power: 80, accuracy: 85, pp: 10, effect: 'def_down', effectChance: 0.3 },
earth_shield: { name: 'Earth Shield', type: 'krell', power: 0, accuracy: 100, pp: 15, effect: 'def_up', effectChance: 0.5 },
crystal_shell: { name: 'Crystal Shell', type: 'krell', power: 0, accuracy: 100, pp: 12, effect: 'barrier', effectChance: 0.6 },
acid_trail: { name: 'Acid Trail', type: 'krell', power: 40, accuracy: 95, pp: 15, effect: 'def_down', effectChance: 0.4 },
mineral_bite: { name: 'Mineral Bite', type: 'krell', power: 50, accuracy: 90, pp: 18, effect: 'steal_hp', effectChance: 0.3 },
harden: { name: 'Harden', type: 'krell', power: 0, accuracy: 100, pp: 20, effect: 'def_up', effectChance: 0.5 },
// Thorne abilities
vine_whip: { name: 'Vine Whip', type: 'thorne', power: 40, accuracy: 100, pp: 20, effect: 'bind', effectChance: 0.3 },
thorn_barrage: { name: 'Thorn Barrage', type: 'thorne', power: 30, accuracy: 95, pp: 15, effect: 'multi_hit', effectChance: 0.3 },
suffocate: { name: 'Suffocate', type: 'thorne', power: 60, accuracy: 85, pp: 12, effect: 'frost', effectChance: 0.35 },
bloom_strike: { name: 'Bloom Strike', type: 'thorne', power: 75, accuracy: 90, pp: 10, effect: 'heal_self', effectChance: 0.3 },
burrow: { name: 'Burrow', type: 'thorne', power: 0, accuracy: 100, pp: 10, effect: 'evasion_up', effectChance: 0.7 },
dig_down: { name: 'Dig Down', type: 'thorne', power: 70, accuracy: 90, pp: 10, effect: 'multi_hit', effectChance: 0.3 },
// Dusk abilities
shadow_wing: { name: 'Shadow Wing', type: 'dusk', power: 50, accuracy: 95, pp: 15, effect: 'spd_down', effectChance: 0.3 },
void_dust: { name: 'Void Dust', type: 'dusk', power: 45, accuracy: 90, pp: 18, effect: 'shock', effectChance: 0.25 },
nightmare: { name: 'Nightmare', type: 'dusk', power: 70, accuracy: 80, pp: 10, effect: 'shock', effectChance: 0.4 },
gloom_burst: { name: 'Gloom Burst', type: 'dusk', power: 85, accuracy: 75, pp: 8, effect: 'all_down', effectChance: 0.2 },
shadow_pounce: { name: 'Shadow Pounce', type: 'dusk', power: 55, accuracy: 100, pp: 15, effect: 'priority', effectChance: 0 },
void_step: { name: 'Void Step', type: 'dusk', power: 0, accuracy: 100, pp: 10, effect: 'spd_up', effectChance: 0.6 },
dark_bite: { name: 'Dark Bite', type: 'dusk', power: 60, accuracy: 90, pp: 15, effect: 'steal_hp', effectChance: 0.3 },
phantom_fade: { name: 'Phantom Fade', type: 'dusk', power: 0, accuracy: 100, pp: 12, effect: 'evasion_up', effectChance: 0.7 },
// Flux abilities
spark_bolt: { name: 'Spark Bolt', type: 'flux', power: 50, accuracy: 100, pp: 20, effect: 'shock', effectChance: 0.3 },
overload: { name: 'Overload', type: 'flux', power: 80, accuracy: 80, pp: 10, effect: 'burn', effectChance: 0.4 },
chain_lightning: { name: 'Chain Lightning', type: 'flux', power: 35, accuracy: 95, pp: 10, effect: 'multi_hit', effectChance: 0.3 },
surge: { name: 'Surge', type: 'flux', power: 0, accuracy: 100, pp: 15, effect: 'spd_up', effectChance: 0.5 },
electro_shock: { name: 'Electro Shock', type: 'flux', power: 45, accuracy: 95, pp: 18, effect: 'shock', effectChance: 0.35 },
tentacle_whip: { name: 'Tentacle Whip', type: 'flux', power: 40, accuracy: 100, pp: 20, effect: 'bind', effectChance: 0.25 },
static_burst: { name: 'Static Burst', type: 'flux', power: 65, accuracy: 85, pp: 12, effect: 'shock', effectChance: 0.4 },
jelly_wrap: { name: 'Jelly Wrap', type: 'flux', power: 55, accuracy: 90, pp: 14, effect: 'bind', effectChance: 0.45 },
// Prism abilities
prism_ray: { name: 'Prism Ray', type: 'prism', power: 55, accuracy: 95, pp: 15, effect: 'burn', effectChance: 0.2 },
refraction: { name: 'Refraction', type: 'prism', power: 0, accuracy: 100, pp: 12, effect: 'evasion_up', effectChance: 0.6 },
luster_burst: { name: 'Luster Burst', type: 'prism', power: 75, accuracy: 85, pp: 10, effect: 'all_up', effectChance: 0.3 },
prismatic_heal: { name: 'Prismatic Heal', type: 'prism', power: 0, accuracy: 100, pp: 15, effect: 'heal_self', effectChance: 0.7 },
aura_heal: { name: 'Aura Heal', type: 'prism', power: 0, accuracy: 100, pp: 20, effect: 'regen', effectChance: 0.6 },
petal_storm: { name: 'Petal Storm', type: 'prism', power: 45, accuracy: 95, pp: 18, effect: 'multi_hit', effectChance: 0.3 },
bloom_burst: { name: 'Bloom Burst', type: 'prism', power: 70, accuracy: 85, pp: 10, effect: 'heal_self', effectChance: 0.35 },
light_bond: { name: 'Light Bond', type: 'prism', power: 0, accuracy: 100, pp: 12, effect: 'barrier', effectChance: 0.5 },
// Aegis abilities
shield_bash: { name: 'Shield Bash', type: 'aegis', power: 50, accuracy: 95, pp: 18, effect: 'def_down', effectChance: 0.3 },
iron_wall: { name: 'Iron Wall', type: 'aegis', power: 0, accuracy: 100, pp: 15, effect: 'barrier', effectChance: 0.65 },
guardian_strike: { name: 'Guardian Strike', type: 'aegis', power: 70, accuracy: 85, pp: 10, effect: 'def_up', effectChance: 0.3 },
fortify: { name: 'Fortify', type: 'aegis', power: 0, accuracy: 100, pp: 15, effect: 'def_up', effectChance: 0.55 },
ward_strike: { name: 'Ward Strike', type: 'aegis', power: 55, accuracy: 90, pp: 15, effect: 'def_down', effectChance: 0.35 },
stone_guard: { name: 'Stone Guard', type: 'aegis', power: 0, accuracy: 100, pp: 12, effect: 'barrier', effectChance: 0.6 },
aegis_wall: { name: 'Aegis Wall', type: 'aegis', power: 0, accuracy: 100, pp: 10, effect: 'all_up', effectChance: 0.25 },
protective_roar: { name: 'Protective Roar', type: 'aegis', power: 0, accuracy: 100, pp: 12, effect: 'def_up', effectChance: 0.5 }
};
// ── const: ITEMS ──
// ===== ITEMS =====
const ITEMS = {
// Collection items
aether_ball: { name: 'Aether Ball', type: 'capture', tier: 1, rate: 1, price: 100, desc: 'Basic collection sphere.' },
void_orb: { name: 'Void Orb', type: 'capture', tier: 2, rate: 2, price: 300, desc: 'Enhanced collection orb.' },
prism_crystal: { name: 'Prism Crystal', type: 'capture', tier: 3, rate: 4, price: 800, desc: 'Powerful collection crystal.' },
// Restoration
aether_draught: { name: 'Aether Draught', type: 'restore', heal: 30, price: 50, desc: 'Restores 30 HP.' },
great_aether: { name: 'Great Aether', type: 'restore', heal: 80, price: 150, desc: 'Restores 80 HP.' },
full_restoration: { name: 'Full Restoration', type: 'restore', full: true, price: 500, desc: 'Fully restores HP.' },
// Catalysts for transformation
ember_crystal: { name: 'Ember Crystal', type: 'catalyst', desc: 'Evolve Emberlit → Pyraxis.' },
stone_fragment: { name: 'Stone Fragment', type: 'catalyst', desc: 'Evolve Stonewisp → Lithos.' },
thorn_seed: { name: 'Thorn Seed', type: 'catalyst', desc: 'Evolve Thornling → Briarthorn.' },
shadow_wing: { name: 'Shadow Wing', type: 'catalyst', desc: 'Evolve Dusk Moth → Umbrafly.' },
lightning_core: { name: 'Lightning Core', type: 'catalyst', desc: 'Evolve Flux Spark → Voltex.' },
prism_shard: { name: 'Prism Shard', type: 'catalyst', desc: 'Evolve Prism Fae → Crystalla.' },
aegis_core: { name: 'Aegis Core', type: 'catalyst', desc: 'Evolve Aegis Knight → Sentinel.' },
aether_heart: { name: 'Aether Heart', type: 'catalyst', desc: 'Evolve Aether Worm → Aether Drake.' },
glow_cap: { name: 'Glow Cap', type: 'catalyst', desc: 'Evolve Glow Moss → Moss Giant.' },
void_fang: { name: 'Void Fang', type: 'catalyst', desc: 'Evolve Void Fox → Nether Wolf.' },
crystal_gel: { name: 'Crystal Gel', type: 'catalyst', desc: 'Evolve Crystal Slug → Gem Shield.' },
storm_heart: { name: 'Storm Heart', type: 'catalyst', desc: 'Evolve Storm Jelly → Tempest Ray.' },
aura_petals: { name: 'Aura Petals', type: 'catalyst', desc: 'Evolve Aura Bloom → Luminaria.' },
ward_stone: { name: 'Ward Stone', type: 'catalyst', desc: 'Evolve Guardian Ward → Ward Lord.' },
// Field tools
aether_compass: { name: 'Aether Compass', type: 'field', price: 200, desc: 'Reveals wild encounters on the overworld.' },
energy_drink: { name: 'Energy Drink', type: 'field', price: 75, desc: 'Boosts SPD by 1 level for 3 turns.' }
};
// ── const: ZONES ──
// ===== ZONES =====
const ZONES = [
{
id: 0, name: 'Verdant Hollow', name_display: 'Verdant Hollow',
gradient: 'linear-gradient(180deg, #1a3a1a 0%, #0a2a0a 100%)',
particles: { color: '#4ade80', count: 30, size: 3, speed: 0.5 },
encounterRate: 0.15,
wildCreatures: ['emberlit', 'stonewisp', 'thornling'],
encounterLevels: [1, 5],
shrine: true,
guardian: { name: 'Verdant Warden', type: 'virel', level: 8, emoji: '🌳', abilities: ['photosynthesis', 'moss_bond', 'energy_drain', 'regenerate'] },
story: 'The first hollow opens before you — a realm of living green where Aetherium energy seeps from the earth itself.',
storyColor: '#4ade80',
unlocked: true
},
{
id: 1, name: 'Krell Depths', name_display: 'Krell Depths',
gradient: 'linear-gradient(180deg, #2a1a3a 0%, #1a0a2a 100%)',
particles: { color: '#a78bfa', count: 25, size: 4, speed: 0.3 },
encounterRate: 0.2,
wildCreatures: ['stonewisp', 'crystal_slug', 'guardian_ward'],
encounterLevels: [4, 10],
shrine: true,
guardian: { name: 'Deep Stone Guardian', type: 'krell', level: 15, emoji: '🗿', abilities: ['earth_shield', 'quarry_strike', 'stone_wall', 'harden'] },
story: 'Beneath the surface lies the Krell Depths — ancient stone corridors humming with crystalline resonance.',
storyColor: '#a78bfa',
unlocked: false
},
{
id: 2, name: 'Thornwood Expanse', name_display: 'Thornwood Expanse',
gradient: 'linear-gradient(180deg, #3a2a1a 0%, #2a1a0a 100%)',
particles: { color: '#fb923c', count: 20, size: 3, speed: 0.6 },
encounterRate: 0.25,
wildCreatures: ['thornling', 'dusk_moth', 'glow_moss'],
encounterLevels: [8, 16],
shrine: true,
guardian: { name: 'Thornwood Sovereign', type: 'thorne', level: 22, emoji: '🌹', abilities: ['suffocate', 'bloom_strike', 'vine_whip', 'thorn_barrage'] },
story: 'The Thornwood stretches endlessly — a forest of living thorns where the Aetherium bleeds amber light.',
storyColor: '#fb923c',
unlocked: false
},
{
id: 3, name: 'Duskreach Abyss', name_display: 'Duskreach Abyss',
gradient: 'linear-gradient(180deg, #1a0a2a 0%, #0a001a 100%)',
particles: { color: '#8b5cf6', count: 35, size: 2, speed: 0.8 },
encounterRate: 0.3,
wildCreatures: ['dusk_moth', 'void_fox', 'flux_spark'],
encounterLevels: [14, 24],
shrine: true,
guardian: { name: 'Abyssal Watcher', type: 'dusk', level: 30, emoji: '🌑', abilities: ['nightmare', 'gloom_burst', 'void_dust', 'phantom_fade'] },
story: 'The Duskreach — where light dies and the Aetherium\'s shadow-self takes form.',
storyColor: '#8b5cf6',
unlocked: false
},
{
id: 4, name: 'Fluxstorm Peaks', name_display: 'Fluxstorm Peaks',
gradient: 'linear-gradient(180deg, #0a2a3a 0%, #001a2a 100%)',
particles: { color: '#06b6d4', count: 40, size: 3, speed: 1.0 },
encounterRate: 0.35,
wildCreatures: ['flux_spark', 'storm_jelly', 'prism_fae'],
encounterLevels: [20, 32],
shrine: true,
guardian: { name: 'Stormheart Titan', type: 'flux', level: 38, emoji: '🌩️', abilities: ['overload', 'chain_lightning', 'surge', 'static_burst'] },
story: 'The Fluxstorm Peaks — mountains of living electricity where the Aetherium\'s raw power crashes like thunder.',
storyColor: '#06b6d4',
unlocked: false
},
{
id: 5, name: 'Prism Core', name_display: 'Prism Core',
gradient: 'linear-gradient(180deg, #2a2a1a 0%, #1a1a0a 100%)',
particles: { color: '#fde68a', count: 50, size: 4, speed: 0.7 },
encounterRate: 0.4,
wildCreatures: ['prism_fae', 'aegis_knight', 'aether_worm'],
encounterLevels: [28, 42],
shrine: true,
guardian: { name: 'The Final Anchor', type: 'prism', level: 45, emoji: '💎', abilities: ['luster_burst', 'prismatic_heal', 'prism_ray', 'refraction'] },
story: 'The Prism Core — the heart of the Aetherium. Here, all energies converge. The final Anchor awaits.',
storyColor: '#fde68a',
unlocked: false
}
];
// ── const: ANCHORS ──
// ===== BOSS ANCHORS (4 zone anchors) =====
const ANCHORS = [
{ name: 'Verdant Anchor', zone: 0, level: 10, type: 'virel', emoji: '🌿', bonus: 'atk', bonusVal: 5 },
{ name: 'Krell Anchor', zone: 1, level: 18, type: 'krell', emoji: '🪨', bonus: 'def', bonusVal: 5 },
{ name: 'Thornwood Anchor', zone: 2, level: 28, type: 'thorne', emoji: '🌵', bonus: 'spd', bonusVal: 5 },
{ name: 'Duskreach Anchor', zone: 3, level: 38, type: 'dusk', emoji: '🌑', bonus: 'hp', bonusVal: 10 }
];
// ── const: STATUS_EFFECTS ──
// ===== STATUS EFFECTS =====
const STATUS_EFFECTS = {
burn: { name: 'Burn', emoji: '🔥', damage: 0.05, desc: 'Takes damage each turn.' },
frost: { name: 'Frost', emoji: '❄️', spdMult: 0.5, desc: 'Speed halved.' },
shock: { name: 'Shock', emoji: '⚡', skipChance: 0.3, desc: 'May skip turn.' },
barrier: { name: 'Barrier', emoji: '🛡️', hits: 1, desc: 'One-hit deflect.' },
regen: { name: 'Regen', emoji: '💚', healPerTurn: 0.03, desc: 'Heals each turn.' },
bind: { name: 'Bind', emoji: '🌿', spdMult: 0.7, desc: 'Speed reduced.' },
burn_self: { name: 'Burn', emoji: '🔥', damage: 0.05, desc: 'Takes damage each turn.' },
def_down: { name: 'DEF Down', emoji: '📉', defMult: 0.75, desc: 'Defense reduced.' },
spd_down: { name: 'SPD Down', emoji: '📉', spdMult: 0.75, desc: 'Speed reduced.' },
all_down: { name: 'Stats Down', emoji: '📉', allMult: 0.8, desc: 'All stats reduced.' }
};
// ── let: game ──
// ===== GAME STATE =====
let game = {
screen: 'title', // title, overworld, battle, capture, shrine, vignette, modal, pause
zone: 0,
playerX: 5, playerY: 5,
party: [], // up to 6 creatures
inventory: {}, // item_id -> count
collected: [], // creature_ids collected
anchorsDefeated: [], // anchor indices
storyStage: 0,
totalBattles: 0,
totalCaptures: 0,
shrineSave: null,
audioMuted: false,
postGame: false,
tutorialPhase: 0,
moveCount: 0,
hasMoved: false,
firstBattle: false,
firstCapture: false
};
// ── function: saveGame ──
// ===== SAVE / LOAD =====
function saveGame(slot) {
const data = {
zone: game.zone, playerX: game.playerX, playerY: game.playerY,
party: game.party.map(c => ({ ...c, currentHp: c.currentHp })),
inventory: { ...game.inventory },
collected: [...game.collected],
anchorsDefeated: [...game.anchorsDefeated],
storyStage: game.storyStage,
shrineSave: game.shrineSave
};
localStorage.setItem('aetherials_save_' + slot, JSON.stringify(data));
}
// ── function: loadGame ──
function loadGame(slot) {
const raw = localStorage.getItem('aetherials_save_' + slot);
if (!raw) return null;
const data = JSON.parse(raw);
game.zone = data.zone;
game.playerX = data.playerX;
game.playerY = data.playerY;
game.party = data.party;
game.inventory = data.inventory;
game.collected = data.collected;
game.anchorsDefeated = data.anchorsDefeated;
game.storyStage = data.storyStage;
game.shrineSave = data.shrineSave;
game.postGame = game.anchorsDefeated.length >= 4;
return true;
}
// ── function: hasSave ──
function hasSave(slot) {
return localStorage.getItem('aetherials_save_' + slot) !== null;
}
// ── function: getStat ──
// ===== STAT CALCULATIONS =====
function getStat(creature, stat) {
const base = creature.baseStats[stat];
const growth = creature.growth[stat];
const bonus = creature.affinityBonus ? creature.affinityBonus[stat] || 0 : 0;
return Math.floor(base + (growth * (creature.level - 1)) + bonus);
}
// ── function: getEffectiveStat ──
function getEffectiveStat(creature, stat, multiplier) {
return Math.floor(getStat(creature, stat) * (multiplier || 1));
}
// ── function: getMaxHp ──
function getMaxHp(creature) {
return getStat(creature, 'hp');
}
// ── function: calcDamage ──
// ===== DAMAGE FORMULA =====
function calcDamage(atkCreature, defCreature, ability) {
const atkStat = getEffectiveStat(atkCreature, 'atk', 1);
const defStat = getEffectiveStat(defCreature, 'def', 1);
const level = atkCreature.level;
let multiplier = 1;
const typeChart = TYPE_CHART[ability.type];
if (typeChart && typeChart[defCreature.affinity]) {
multiplier = typeChart[defCreature.affinity];
}
// STAB
if (ability.type === atkCreature.affinity) {
multiplier *= 1.25;
}
let base = (0.4 * level * atkStat / defStat) * multiplier;
let random = 0.85 + Math.random() * 0.3;
let damage = Math.floor(base * random);
damage = Math.max(1, damage);
// Critical hit (6% chance)
let crit = false;
if (Math.random() < 0.06) {
damage = Math.floor(damage * 1.5);
crit = true;
}
return { damage, multiplier, crit };
}
// ── function: getTypeEffectiveness ──
// ===== TYPE EFFECTIVENESS DISPLAY =====
function getTypeEffectiveness(abilityType, targetType) {
const chart = TYPE_CHART[abilityType];
if (!chart) return { text: 'Normal', color: '#aaa', mult: 1 };
const mult = chart[targetType] || 1;
if (mult > 1.2) return { text: 'Super Effective!', color: '#4ade80', mult };
if (mult < 0.8) return { text: 'Not Very Effective', color: '#f87171', mult };
return { text: 'Normal', color: '#aaa', mult };
}
// ── function: addItem ──
// ===== INVENTORY HELPERS =====
function addItem(itemId, count) {
game.inventory[itemId] = (game.inventory[itemId] || 0) + count;
}
// ── function: removeItem ──
function removeItem(itemId, count) {
game.inventory[itemId] = (game.inventory[itemId] || 0) - count;
if (game.inventory[itemId] <= 0) delete game.inventory[itemId];
}
// ── function: hasItem ──
function hasItem(itemId, count) {
return (game.inventory[itemId] || 0) >= count;
}
// ── function: createCreature ──
function createCreature(templateId, level) {
const template = CREATURES[templateId];
if (!template) return null;
const creature = {
id: templateId,
name: template.name,
emoji: template.emoji,
affinity: template.affinity,
level: level,
currentHp: Math.floor(template.baseStats.hp + template.growth.hp * (level - 1)),
baseStats: { ...template.baseStats },
growth: { ...template.growth },
abilities: [...template.abilities],
evolved: false,
exp: 0,
expToNext: level * 20,
statusEffects: [],
affinityBonus: null
};
return creature;
}
// ── function: createBossCreature ──
function createBossCreature(templateId, level) {
const c = createCreature(templateId, level);
c.currentHp = getMaxHp(c);
return c;
}
// ── function: checkLevelUp ──
// ===== LEVEL UP =====
function checkLevelUp(creature) {
while (creature.exp >= creature.expToNext && creature.level < 50) {
creature.level++;
creature.exp -= creature.expToNext;
creature.expToNext = creature.level * 20;
// Recalculate HP
const maxHp = getMaxHp(creature);
creature.currentHp = Math.min(creature.currentHp + 10, maxHp);
}
return creature.level >= 50 ? false : true;
}
// ── function: calcExp ──
// ===== EXP CALCULATION =====
function calcExp(creature, defeatedLevel, isBoss) {
const base = Math.floor(creature.level * 10);
const levelDiff = Math.abs(defeatedLevel - creature.level);
const diffBonus = Math.floor(levelDiff * 2);
const bossMult = isBoss ? 2 : 1;
return Math.floor((base + diffBonus) * bossMult);
}
// ── function: generateWildEncounter ──
// ===== WILD ENCOUNTER =====
function generateWildEncounter() {
const zone = ZONES[game.zone];
const levelRange = game.postGame
? [zone.encounterLevels[0] + 10, zone.encounterLevels[1] + 10]
: zone.encounterLevels;
const creatureId = zone.wildCreatures[Math.floor(Math.random() * zone.wildCreatures.length)];
const level = levelRange[0] + Math.floor(Math.random() * (levelRange[1] - levelRange[0] + 1));
return { creature: createCreature(creatureId, level), isBoss: false };
}
// ── function: generateBossEncounter ──
function generateBossEncounter(anchorIndex) {
const anchor = ANCHORS[anchorIndex];
const templateId = ['emberlit', 'stonewisp', 'thornling', 'dusk_moth'][anchorIndex];
return { creature: createBossCreature(templateId, anchor.level), isBoss: true, anchor };
}
// ── let: battle ──
// ===== BATTLE INITIALIZATION =====
let battle = {
playerParty: [],
enemies: [],
turnOrder: [],
currentTurnIndex: 0,
phase: 'select', // select, animating, capture, victory, defeat
selectedAbility: null,
selectedTarget: null,
animating: false,
floatingNumbers: [],
screenShake: false,
particleBursts: [],
captured: null,
capturedCreature: null
};
// ── function: calcTurnOrder ──
// ===== BATTLE TURN ORDER =====
function calcTurnOrder() {
const all = [...battle.playerParty.map((c, i) => ({ type: 'player', creature: c, index: i })),
...battle.enemies.map((c, i) => ({ type: 'enemy', creature: c, index: i }))];
all.sort((a, b) => {
const spdA = getEffectiveStat(a.creature, 'spd', 1);
const spdB = getEffectiveStat(b.creature, 'spd', 1);
return spdB - spdA;
});
return all;
}
// ── let: audioCtx ──
// ===== AUDIO SYSTEM =====
let audioCtx = null;
// ── let: audioNodes ──
let audioNodes = [];
// ── function: initAudio ──
// ===== AUDIO =====
function initAudio() {
if (audioCtx) return;
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
// ── function: playTone ──
function playTone(freq, duration, type, volume) {
if (!audioCtx || game.audioMuted) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = type || 'sine';
osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
gain.gain.setValueAtTime(volume || 0.1, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + duration);
}
// ── function: playDamageSound ──
function playDamageSound() { playTone(200, 0.15, 'sawtooth', 0.08); setTimeout(() => playTone(150, 0.1, 'sawtooth', 0.06), 50); }
// ── function: playCritSound ──
function playCritSound() { playTone(400, 0.1, 'square', 0.1); setTimeout(() => playTone(600, 0.15, 'square', 0.1), 50); setTimeout(() => playTone(800, 0.2, 'square', 0.08), 100); }
// ── function: playSuperEffectiveSound ──
function playSuperEffectiveSound() { playTone(300, 0.2, 'sine', 0.1); setTimeout(() => playTone(500, 0.2, 'sine', 0.1), 100); setTimeout(() => playTone(700, 0.3, 'sine', 0.08), 200); }
// ── function: playCaptureSound ──
function playCaptureSound() { playTone(440, 0.15, 'sine', 0.08); setTimeout(() => playTone(550, 0.15, 'sine', 0.08), 150); setTimeout(() => playTone(660, 0.2, 'sine', 0.08), 300); setTimeout(() => playTone(880, 0.3, 'sine', 0.06), 500); }
// ── function: playVictorySound ──
function playVictorySound() { [440, 550, 660, 880].forEach((f, i) => { setTimeout(() => playTone(f, 0.3, 'sine', 0.08), i * 150); }); }
// ── function: playDefeatSound ──
function playDefeatSound() { [440, 350, 280, 200].forEach((f, i) => { setTimeout(() => playTone(f, 0.4, 'sawtooth', 0.06), i * 200); }); }
// ── function: playAmbientPad ──
function playAmbientPad() {
if (!audioCtx || game.audioMuted) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(80, audioCtx.currentTime);
gain.gain.setValueAtTime(0.02, audioCtx.currentTime);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
audioNodes.push(osc);
}
// ── function: playZoneAmbient ──
function playZoneAmbient(zoneId) {
if (game.audioMuted) return;
const freqs = [[120, 180], [100, 150], [140, 200], [90, 130], [160, 220], [200, 280]];
const f = freqs[zoneId] || freqs[0];
playTone(f[0], 2, 'sine', 0.03);
playTone(f[1], 2, 'sine', 0.02);
}
// ── function: toggleMute ──
function toggleMute() { game.audioMuted = !game.audioMuted; }
// ── let: particles ──
let particles = [];
// ── function: spawnParticle ──
function spawnParticle(x, y, color, size, vx, vy, life) {
particles.push({ x, y, color, size, vx: vx || (Math.random() - 0.5) * 2, vy: vy || -Math.random() * 2, life: life || 60, maxLife: life || 60 });
}
// ── function: updateParticles ──
function updateParticles() {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx; p.y += p.vy; p.life--;
if (p.life <= 0) particles.splice(i, 1);
}
}
// ── function: drawParticles ──
function drawParticles(ctx) {
for (const p of particles) {
const alpha = p.life / p.maxLife;
ctx.globalAlpha = alpha;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
}
// ── let: floatingNumbers ──
let floatingNumbers = [];
// ── function: spawnFloatingNumber ──
function spawnFloatingNumber(x, y, text, color, isCrit) {
floatingNumbers.push({ x, y, text, color, isCrit: isCrit || false, life: 60, maxLife: 60 });
}
// ── function: updateFloatingNumbers ──
function updateFloatingNumbers() {
for (let i = floatingNumbers.length - 1; i >= 0; i--) {
const n = floatingNumbers[i];
n.y -= 1; n.life--;
if (n.life <= 0) floatingNumbers.splice(i, 1);
}
}
// ── function: drawFloatingNumbers ──
function drawFloatingNumbers(ctx) {
for (const n of floatingNumbers) {
const alpha = n.life / n.maxLife;
ctx.globalAlpha = alpha;
ctx.font = n.isCrit ? 'bold 24px sans-serif' : 'bold 18px sans-serif';
ctx.fillStyle = n.color;
ctx.strokeStyle = 'rgba(0,0,0,0.5)';
ctx.lineWidth = 2;
ctx.strokeText(n.text, n.x, n.y);
ctx.fillText(n.text, n.x, n.y);
}
ctx.globalAlpha = 1;
}
// ── let: battleAnim ──
// ===== BATTLE ANIMATIONS =====
let battleAnim = { active: false, type: '', target: null, progress: 0, duration: 30, callback: null };
// ── function: triggerBattleAnim ──
function triggerBattleAnim(type, target, duration, callback) {
battleAnim = { active: true, type, target, progress: 0, duration: duration || 30, callback };
}
// ── function: updateBattleAnim ──
function updateBattleAnim() {
if (!battleAnim.active) return;
battleAnim.progress++;
if (battleAnim.progress >= battleAnim.duration) {
battleAnim.active = false;
if (battleAnim.callback) battleAnim.callback();
}
}
// ── const: TILE_SIZE ──
// ===== OVERWORLD RENDERING =====
const TILE_SIZE = 48;
// ── const: MAP_W ──
const MAP_W = 20;
// ── const: MAP_H ──
const MAP_H = 20;
// ── function: generateMap ──
function generateMap(zoneId) {
const map = [];
for (let y = 0; y < MAP_H; y++) {
map[y] = [];
for (let x = 0; x < MAP_W; x++) {
if (x === 0 || x === MAP_W - 1 || y === 0 || y === MAP_H - 1) {
map[y][x] = 1;
} else {
const r = Math.random();
if (r < 0.08) map[y][x] = 2;
else if (r < 0.12) map[y][x] = 3;
else if (r < 0.15) map[y][x] = 4;
else map[y][x] = 0;
}
}
}
map[9][9] = 0; map[9][10] = 0; map[10][9] = 0; map[10][10] = 0;
map[10][10] = 3;
map[3][17] = 5;
for (let x = 10; x < 17; x++) map[3][x] = 0;
for (let y = 10; y < 3; y++) map[y][10] = 0;
return map;
}
// ── let: overworldMap ──
let overworldMap = generateMap(0);
// ── let: overworldCanvas ──
let overworldCanvas = null;
// ── let: overworldCtx ──
let overworldCtx = null;
// ── let: zoneParticles ──
let zoneParticles = [];
// ── function: initOverworldCanvas ──
function initOverworldCanvas() {
overworldCanvas = document.getElementById('overworld-canvas');
if (overworldCanvas) {
const dpr = window.devicePixelRatio || 1;
overworldCanvas.width = overworldCanvas.clientWidth * dpr;
overworldCanvas.height = overworldCanvas.clientHeight * dpr;
overworldCtx = overworldCanvas.getContext('2d');
overworldCtx.scale(dpr, dpr);
}
}
// ── function: resizeOverworldCanvas ──
function resizeOverworldCanvas() {
if (!overworldCanvas) return;
const dpr = window.devicePixelRatio || 1;
overworldCanvas.width = overworldCanvas.clientWidth * dpr;
overworldCanvas.height = overworldCanvas.clientHeight * dpr;
if (overworldCtx) overworldCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
// ── function: initZoneParticles ──
function initZoneParticles(zoneId) {
zoneParticles = [];
const zone = ZONES[zoneId];
for (let i = 0; i < zone.particles.count; i++) {
zoneParticles.push({
x: Math.random() * (overworldCanvas?.clientWidth || 800),
y: Math.random() * (overworldCanvas?.clientHeight || 600),
vx: (Math.random() - 0.5) * zone.particles.speed,
vy: -Math.random() * zone.particles.speed * 0.5,
size: zone.particles.size * (0.5 + Math.random()),
alpha: 0.3 + Math.random() * 0.5,
color: zone.particles.color,
pulse: Math.random() * Math.PI * 2
});
}
}
// ── function: updateZoneParticles ──
function updateZoneParticles() {
for (const p of zoneParticles) {
p.x += p.vx; p.y += p.vy; p.pulse += 0.02;
if (p.y < -10) { p.y = (overworldCanvas?.clientHeight || 600) + 10; p.x = Math.random() * (overworldCanvas?.clientWidth || 800); }
if (p.x < -10) p.x = (overworldCanvas?.clientWidth || 800) + 10;
if (p.x > (overworldCanvas?.clientWidth || 800) + 10) p.x = -10;
}
}
// ── function: drawZoneParticles ──
function drawZoneParticles(ctx) {
for (const p of zoneParticles) {
const alpha = p.alpha * (0.7 + 0.3 * Math.sin(p.pulse));
ctx.globalAlpha = alpha;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
}
// ── function: drawOverworld ──
function drawOverworld() {
if (!overworldCtx || !overworldCanvas) return;
const w = overworldCanvas.clientWidth;
const h = overworldCanvas.clientHeight;
const zone = ZONES[game.zone];
const grad = overworldCtx.createLinearGradient(0, 0, 0, h);
grad.addColorStop(0, '#1a1a2e');
grad.addColorStop(1, '#0a0a1a');
overworldCtx.fillStyle = grad;
overworldCtx.fillRect(0, 0, w, h);
const camX = game.playerX * TILE_SIZE - w / 2 + TILE_SIZE / 2;
const camY = game.playerY * TILE_SIZE - h / 2 + TILE_SIZE / 2;
for (let y = 0; y < MAP_H; y++) {
for (let x = 0; x < MAP_W; x++) {
const sx = x * TILE_SIZE - camX;
const sy = y * TILE_SIZE - camY;
if (sx < -TILE_SIZE || sx > w + TILE_SIZE || sy < -TILE_SIZE || sy > h + TILE_SIZE) continue;
const tile = overworldMap[y][x];
if (tile === 0) {
overworldCtx.fillStyle = 'rgba(255,255,255,0.03)';
overworldCtx.fillRect(sx, sy, TILE_SIZE, TILE_SIZE);
} else if (tile === 1) {
overworldCtx.fillStyle = 'rgba(255,255,255,0.08)';
overworldCtx.fillRect(sx, sy, TILE_SIZE, TILE_SIZE);
overworldCtx.strokeStyle = 'rgba(255,255,255,0.15)';
overworldCtx.lineWidth = 1;
overworldCtx.strokeRect(sx, sy, TILE_SIZE, TILE_SIZE);
} else if (tile === 2) {
overworldCtx.fillStyle = 'rgba(255,255,255,0.12)';
overworldCtx.fillRect(sx, sy, TILE_SIZE, TILE_SIZE);
overworldCtx.fillStyle = 'rgba(255,255,255,0.2)';
overworldCtx.fillRect(sx + 4, sy + 4, TILE_SIZE - 8, TILE_SIZE - 8);
} else if (tile === 3) {
overworldCtx.fillStyle = 'rgba(251, 191, 36, 0.3)';
overworldCtx.fillRect(sx, sy, TILE_SIZE, TILE_SIZE);
overworldCtx.fillStyle = '#fbbf24';
overworldCtx.font = '24px sans-serif';
overworldCtx.textAlign = 'center';
overworldCtx.textBaseline = 'middle';
overworldCtx.fillText('⛩️', sx + TILE_SIZE / 2, sy + TILE_SIZE / 2);
} else if (tile === 4) {
overworldCtx.fillStyle = 'rgba(96, 165, 250, 0.3)';
overworldCtx.fillRect(sx, sy, TILE_SIZE, TILE_SIZE);
} else if (tile === 5) {
overworldCtx.fillStyle = 'rgba(251, 191, 36, 0.4)';
overworldCtx.fillRect(sx, sy, TILE_SIZE, TILE_SIZE);
overworldCtx.fillStyle = '#fbbf24';
overworldCtx.font = '24px sans-serif';
overworldCtx.textAlign = 'center';
overworldCtx.textBaseline = 'middle';
overworldCtx.fillText('👑', sx + TILE_SIZE / 2, sy + TILE_SIZE / 2);
}
}
}
const playerSx = game.playerX * TILE_SIZE - camX;
const playerSy = game.playerY * TILE_SIZE - camY;
overworldCtx.fillStyle = '#6ee7b7';
overworldCtx.beginPath();
overworldCtx.arc(playerSx + TILE_SIZE / 2, playerSy + TILE_SIZE / 2, 12, 0, Math.PI * 2);
overworldCtx.fill();
overworldCtx.fillStyle = '#fff';
overworldCtx.font = '16px sans-serif';
overworldCtx.textAlign = 'center';
overworldCtx.textBaseline = 'middle';
overworldCtx.fillText('🧙', playerSx + TILE_SIZE / 2, playerSy + TILE_SIZE / 2);
drawZoneParticles(overworldCtx);
drawParticles(overworldCtx);
overworldCtx.fillStyle = 'rgba(0,0,0,0.3)';
overworldCtx.fillRect(0, 0, w, 40);
overworldCtx.fillStyle = '#e8e8f0';
overworldCtx.font = '14px sans-serif';
overworldCtx.textAlign = 'left';
overworldCtx.fillText('📍 ' + zone.name, 15, 25);
}
// ── let: battleCanvas ──
// ===== BATTLE RENDERING =====
let battleCanvas = null;
// ── let: battleCtx ──
let battleCtx = null;
// ── function: initBattleCanvas ──
function initBattleCanvas() {
battleCanvas = document.getElementById('battle-canvas');
if (battleCanvas) {
const dpr = window.devicePixelRatio || 1;
battleCanvas.width = battleCanvas.clientWidth * dpr;
battleCanvas.height = battleCanvas.clientHeight * dpr;
battleCtx = battleCanvas.getContext('2d');
battleCtx.scale(dpr, dpr);
}
}
// ── function: resizeBattleCanvas ──
function resizeBattleCanvas() {
if (!battleCanvas) return;
const dpr = window.devicePixelRatio || 1;
battleCanvas.width = battleCanvas.clientWidth * dpr;
battleCanvas.height = battleCanvas.clientHeight * dpr;
if (battleCtx) battleCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
// ── function: drawBattleScene ──
function drawBattleScene() {
if (!battleCtx || !battleCanvas) return;
const w = battleCanvas.clientWidth;
const h = battleCanvas.clientHeight;
const grad = battleCtx.createLinearGradient(0, 0, 0, h);
grad.addColorStop(0, '#1a1a2e');
grad.addColorStop(0.5, '#16213e');
grad.addColorStop(1, '#0f3460');
battleCtx.fillStyle = grad;
battleCtx.fillRect(0, 0, w, h);
battleCtx.strokeStyle = 'rgba(255,255,255,0.03)';
battleCtx.lineWidth = 1;
for (let x = 0; x < w; x += 40) {
battleCtx.beginPath(); battleCtx.moveTo(x, 0); battleCtx.lineTo(x, h); battleCtx.stroke();
}
for (let y = 0; y < h; y += 40) {
battleCtx.beginPath(); battleCtx.moveTo(0, y); battleCtx.lineTo(w, y); battleCtx.stroke();
}
const enemyStartX = w * 0.55;
const enemyY = h * 0.25;
battle.enemies.forEach((enemy, i) => {
const ex = enemyStartX + i * 120;
const ey = enemyY;
const hpPercent = enemy.currentHp / getMaxHp(enemy);
battleCtx.fillStyle = 'rgba(255,255,255,0.05)';
battleCtx.beginPath();
battleCtx.arc(ex, ey, 35, 0, Math.PI * 2);
battleCtx.fill();
battleCtx.fillStyle = '#e8e8f0';
battleCtx.font = '32px sans-serif';
battleCtx.textAlign = 'center';
battleCtx.textBaseline = 'middle';
battleCtx.fillText(enemy.emoji, ex, ey);
battleCtx.fillStyle = '#e8e8f0';
battleCtx.font = 'bold 12px sans-serif';
battleCtx.textAlign = 'left';
battleCtx.fillText(enemy.name, ex - 30, ey + 50);
battleCtx.fillStyle = '#8888aa';
battleCtx.font = '10px sans-serif';
battleCtx.fillText('Lv.' + enemy.level, ex - 30, ey + 65);
const barW = 60;
const barH = 6;
const barX = ex - 30;
const barY = ey + 75;
battleCtx.fillStyle = 'rgba(0,0,0,0.4)';
battleCtx.fillRect(barX, barY, barW, barH);
const hpColor = hpPercent > 0.5 ? '#4ade80' : hpPercent > 0.2 ? '#fbbf24' : '#f87171';
battleCtx.fillStyle = hpColor;
battleCtx.fillRect(barX, barY, barW * hpPercent, barH);
battleCtx.strokeStyle = 'rgba(255,255,255,0.2)';
battleCtx.lineWidth = 1;
battleCtx.strokeRect(barX, barY, barW, barH);
let statusX = ex - 30;
let statusY = ey + 88;
enemy.statusEffects.forEach(se => {
battleCtx.font = '10px sans-serif';
battleCtx.textAlign = 'left';
battleCtx.fillText(STATUS_EFFECTS[se] ? STATUS_EFFECTS[se].emoji : '?', statusX, statusY);
statusX += 14;
});
});
const activeCreature = battle.activePlayerCreature;
if (activeCreature) {
const px = w * 0.2;
const py = h * 0.65;
const hpPercent = activeCreature.currentHp / getMaxHp(activeCreature);
battleCtx.fillStyle = 'rgba(110, 231, 183, 0.1)';
battleCtx.beginPath();
battleCtx.arc(px, py, 45, 0, Math.PI * 2);
battleCtx.fill();
battleCtx.fillStyle = '#e8e8f0';
battleCtx.font = '40px sans-serif';
battleCtx.textAlign = 'center';
battleCtx.textBaseline = 'middle';
battleCtx.fillText(activeCreature.emoji, px, py);
battleCtx.fillStyle = '#e8e8f0';
battleCtx.font = 'bold 13px sans-serif';
battleCtx.textAlign = 'left';
battleCtx.fillText(activeCreature.name, px + 55, py - 30);
battleCtx.fillStyle = '#8888aa';
battleCtx.font = '11px sans-serif';
battleCtx.fillText('Lv.' + activeCreature.level, px + 55, py - 15);
const barW = 100;
const barH = 8;
const barX = px + 55;
const barY = py;
battleCtx.fillStyle = 'rgba(0,0,0,0.4)';
battleCtx.fillRect(barX, barY, barW, barH);
const hpColor = hpPercent > 0.5 ? '#4ade80' : hpPercent > 0.2 ? '#fbbf24' : '#f87171';
battleCtx.fillStyle = hpColor;
battleCtx.fillRect(barX, barY, barW * hpPercent, barH);
battleCtx.strokeStyle = 'rgba(255,255,255,0.2)';
battleCtx.lineWidth = 1;
battleCtx.strokeRect(barX, barY, barW, barH);
battleCtx.fillStyle = '#8888aa';
battleCtx.font = '10px sans-serif';
battleCtx.textAlign = 'left';
battleCtx.fillText(activeCreature.currentHp + '/' + getMaxHp(activeCreature), barX, barY + 14);
battleCtx.fillStyle = TYPE_COLORS[activeCreature.affinity];
battleCtx.font = '10px sans-serif';
battleCtx.textAlign = 'left';
battleCtx.fillText(TYPE_EMOJI[activeCreature.affinity] + ' ' + TYPE_NAMES[activeCreature.affinity], barX, barY + 26);
let statusX = px + 55;
let statusY = py + 36;
activeCreature.statusEffects.forEach(se => {
battleCtx.font = '10px sans-serif';
battleCtx.fillText(STATUS_EFFECTS[se] ? STATUS_EFFECTS[se].emoji : '?', statusX, statusY);
statusX += 14;
});
}
drawParticles(battleCtx);
drawFloatingNumbers(battleCtx);
}
// ── function: showScreen ──
// ===== UI HELPERS =====
function showScreen(screenId) {
document.querySelectorAll('#title-screen, #overworld-hud, #battle-scene, #capture-game, #shrine-overlay, #story-vignette, #pause-menu').forEach(el => {
if (el) el.style.display = 'none';
});
document.querySelectorAll('.modal-overlay').forEach(el => el.remove());
switch (screenId) {
case 'title':
document.getElementById('title-screen').style.display = 'flex';
break;
case 'overworld':
document.getElementById('overworld-hud').style.display = 'flex';
document.getElementById('overworld-canvas').style.display = 'block';
break;
case 'battle':
document.getElementById('battle-scene').style.display = 'block';
break;
case 'capture':
document.getElementById('capture-game').style.display = 'flex';
break;
case 'shrine':
document.getElementById('shrine-overlay').style.display = 'flex';
break;
case 'vignette':
document.getElementById('story-vignette').style.display = 'flex';
break;
case 'pause':
document.getElementById('pause-menu').style.display = 'flex';
break;
}
game.screen = screenId;
}
// ── function: showModal ──
function showModal(content) {
hideModal();
const overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.innerHTML = '<div class="modal" style="position:relative;">' + content + '</div>';
document.getElementById('app').appendChild(overlay);
game.modalOverlay = overlay;
}
// ── function: hideModal ──
function hideModal() {
if (game.modalOverlay) {
game.modalOverlay.remove();
game.modalOverlay = null;
}
}
// ── function: shakeScreen ──
function shakeScreen() {
const app = document.getElementById('app');
app.classList.add('shake');
setTimeout(() => app.classList.remove('shake'), 300);
}
// ── function: renderBattleUI ──
// ===== BATTLE UI RENDERING =====
function renderBattleUI() {
const activeCreature = battle.activePlayerCreature;
if (!activeCreature) return;
const textEl = document.getElementById('battle-text');
const actionsEl = document.getElementById('battle-actions');
if (battle.phase === 'select') {
textEl.textContent = 'What will ' + activeCreature.name + ' do?';
textEl.style.color = '#e8e8f0';
let html = '';
activeCreature.abilities.forEach((abilityId, i) => {
const ability = ABILITIES[abilityId];
if (!ability) return;
const ppLeft = (activeCreature.pp && activeCreature.pp[i]) || ability.pp;
const typeClass = 'type-' + ability.type;
html += '<button class="battle-btn ability-btn ' + typeClass + '" onclick="useAbility(' + i + ')" ' + (ppLeft <= 0 ? 'disabled' : '') + '>' +
TYPE_EMOJI[ability.type] + ' ' + ability.name + ' <span style="font-size:0.7em;color:#8888aa">PP:' + ppLeft + '</span></button>';
});
html += '<button class="battle-btn" onclick="showSwitchMenu()">🔄 Switch</button>';
html += '<button class="battle-btn" onclick="showItemMenu()">🎒 Item</button>';
if (!battle.isBossBattle) {
const hasBall = hasItem('aether_ball', 1) || hasItem('void_orb', 1) || hasItem('prism_crystal', 1);
html += '<button class="battle-btn" onclick="startCapture()" ' + (!hasBall ? 'disabled' : '') + '>⭕ Capture</button>';
}
html += '<button class="battle-btn" onclick="tryRun()">🏃 Run</button>';
actionsEl.innerHTML = html;
} else if (battle.phase === 'animating') {
textEl.textContent = battle.animText || '...';
textEl.style.color = '#fbbf24';
actionsEl.innerHTML = '';
} else if (battle.phase === 'victory') {
textEl.textContent = 'Victory!';
textEl.style.color = '#4ade80';
actionsEl.innerHTML = '<button class="battle-btn" onclick="endBattle()">Continue</button>';
} else if (battle.phase === 'defeat') {
textEl.textContent = 'Your party has fallen...';
textEl.style.color = '#f87171';
actionsEl.innerHTML = '<button class="battle-btn" onclick="retreatFromBattle()">Retreat</button>';
}
}
// ── function: startBattle ──
// ===== BATTLE LOGIC =====
function startBattle(encounter, isBossBattle) {
initAudio();
battle = {
playerParty: game.party.map(c => ({ ...c, currentHp: c.currentHp, pp: c.abilities.map(a => ABILITIES[a] ? ABILITIES[a].pp : 0) })),
enemies: [encounter.creature],
turnOrder: [],
currentTurnIndex: 0,
phase: 'select',
selectedAbility: null,
selectedTarget: null,
animating: false,
floatingNumbers: [],
screenShake: false,
particleBursts: [],
captured: null,
capturedCreature: null,
isBossBattle: isBossBattle || false,
activePlayerCreature: null,
animText: '',
encounter: encounter
};
updateActiveCreature();
battle.turnOrder = calcTurnOrder();
battle.currentTurnIndex = 0;
showScreen('battle');
initBattleCanvas();
resizeBattleCanvas();
renderBattleUI();
game.totalBattles++;
}
// ── function: updateActiveCreature ──
function updateActiveCreature() {
const alive = battle.playerParty.filter(c => c.currentHp > 0);
if (alive.length === 0) {
battle.phase = 'defeat';
return;
}
if (!battle.activePlayerCreature || battle.activePlayerCreature.currentHp <= 0) {
battle.activePlayerCreature = alive[0];
}
}
// ── function: useAbility ──
function useAbility(abilityIndex) {
if (battle.phase !== 'select' || battle.animating) return;
const creature = battle.activePlayerCreature;
if (!creature || creature.currentHp <= 0) return;
const abilityId = creature.abilities[abilityIndex];
const ability = ABILITIES[abilityId];
if (!ability) return;
const ppIndex = creature.abilities.indexOf(abilityId);
const currentPp = (creature.pp && creature.pp[ppIndex]) || ability.pp;
if (currentPp <= 0) return;
battle.phase = 'animating';
battle.selectedAbility = ability;
battle.selectedAbilityIndex = abilityIndex;
if (!creature.pp) creature.pp = creature.abilities.map(() => ability.pp);
if (creature.pp[ppIndex] > 0) creature.pp[ppIndex]--;
let target, targetIndex;
if (battle.enemies.length === 1) {
target = battle.enemies[0];
targetIndex = 0;
} else {
let minHp = Infinity;
battle.enemies.forEach((e, i) => {
if (e.currentHp > 0 && e.currentHp < minHp) {
minHp = e.currentHp;
targetIndex = i;
}
});
target = battle.enemies[targetIndex];
}
executeMove(creature, ability, target, targetIndex, 'player');
}
// ── function: executeMove ──
function executeMove(creature, ability, target, targetIndex, side) {
const isPlayer = side === 'player';
battle.animText = creature.name + ' used ' + ability.name + '!';
renderBattleUI();
if (isPlayer) {
triggerBattleAnim('lunge_player', targetIndex, 15, () => {
resolveHit(creature, ability, target, targetIndex, side);
});
} else {
triggerBattleAnim('lunge_enemy', targetIndex, 15, () => {
resolveHit(creature, ability, target, targetIndex, side);
});
}
}
// ── function: resolveHit ──
function resolveHit(creature, ability, target, targetIndex, side) {
const isPlayer = side === 'player';
if (Math.random() * 100 > ability.accuracy) {
battle.animText = creature.name + "'s attack missed!";
renderBattleUI();
setTimeout(() => nextTurn(), 800);
return;
}
const barrierIndex = target.statusEffects.indexOf('barrier');
if (barrierIndex >= 0) {
target.statusEffects.splice(barrierIndex, 1);
battle.animText = target.name + "'s barrier absorbed the attack!";
renderBattleUI();
spawnFloatingNumber(
isPlayer ? window.innerWidth * 0.7 : window.innerWidth * 0.3,
window.innerHeight * 0.3,
'Blocked!', '#60a5fa', false
);
shakeScreen();
playTone(300, 0.2, 'sine', 0.08);
setTimeout(() => nextTurn(), 800);
return;
}
if (ability.power > 0) {
const result = calcDamage(creature, target, ability);
target.currentHp = Math.max(0, target.currentHp - result.damage);
const fx = isPlayer ? window.innerWidth * 0.7 : window.innerWidth * 0.3;
const fy = window.innerHeight * 0.3;
if (result.crit) {
playCritSound();
spawnFloatingNumber(fx, fy, result.damage + '!', '#fde68a', true);
} else {
playDamageSound();
spawnFloatingNumber(fx, fy, result.damage + '!', '#e8e8f0', false);
}
const eff = getTypeEffectiveness(ability.type, target.affinity);
if (eff.mult > 1.2) {
playSuperEffectiveSound();
battle.animText += '\nIt\'s super effective!';
shakeScreen();
for (let i = 0; i < 20; i++) {
spawnParticle(fx, fy, TYPE_COLORS[ability.type], 3 + Math.random() * 3, (Math.random() - 0.5) * 8, (Math.random() - 0.5) * 8, 30);
}
} else if (eff.mult < 0.8) {
battle.animText += '\nIt\'s not very effective...';
}
shakeScreen();
triggerBattleAnim('hit_enemy', targetIndex, 10, () => {
checkFaint(target, targetIndex, side);
});
} else {
applyAbilityEffect(creature, ability, target, targetIndex, side);
}
renderBattleUI();
}
// ── function: applyAbilityEffect ──
function applyAbilityEffect(creature, ability, target, targetIndex, side) {
const isPlayer = side === 'player';
switch (ability.effect) {
case 'heal_self':
case 'self_heal': {
const healAmount = Math.floor(getMaxHp(creature) * 0.3);
creature.currentHp = Math.min(getMaxHp(creature), creature.currentHp + healAmount);
battle.animText = creature.name + ' restored ' + healAmount + ' HP!';
spawnFloatingNumber(window.innerWidth * 0.2, window.innerHeight * 0.6, '+' + healAmount, '#4ade80', false);
break;
}
case 'steal_hp': {
const stealAmount = Math.floor(getMaxHp(creature) * 0.2);
creature.currentHp = Math.min(getMaxHp(creature), creature.currentHp + stealAmount);
target.currentHp = Math.max(0, target.currentHp - stealAmount);
battle.animText = creature.name + ' drained ' + stealAmount + ' HP!';
spawnFloatingNumber(window.innerWidth * 0.2, window.innerHeight * 0.6, '+' + stealAmount, '#4ade80', false);
checkFaint(target, targetIndex, side);
break;
}
case 'barrier': {
target.statusEffects.push('barrier');
battle.animText = target.name + ' created a barrier!';
break;
}
case 'def_up': {
if (!creature.statusEffects) creature.statusEffects = [];
creature.statusEffects.push('def_up');
battle.animText = creature.name + "'s defense rose!";
break;
}
case 'spd_up': {
if (!creature.statusEffects) creature.statusEffects = [];
creature.statusEffects.push('spd_up');
battle.animText = creature.name + "'s speed increased!";
break;
}
case 'evasion_up': {
if (!creature.statusEffects) creature.statusEffects = [];
creature.statusEffects.push('evasion_up');
battle.animText = creature.name + ' became harder to hit!';
break;
}
case 'all_up': {
if (!creature.statusEffects) creature.statusEffects = [];
creature.statusEffects.push('all_up');
battle.animText = creature.name + "'s stats rose!";
break;
}
case 'regen': {
target.statusEffects.push('regen');
battle.animText = target.name + ' began regenerating!';
break;
}
case 'bind': {
target.statusEffects.push('bind');
battle.animText = target.name + ' was bound!';
break;
}
default:
battle.animText = creature.name + ' used ' + ability.name + '!';
}
setTimeout(() => nextTurn(), 800);
}
// ── function: checkFaint ──
function checkFaint(creature, index, side) {
if (creature.currentHp <= 0) {
if (side === 'player') {
battle.animText = creature.name + ' fainted!';
checkEnemyFaint(index);
} else {
battle.animText = creature.name + ' fainted!';
checkPlayerFaint(index);
}
} else {
setTimeout(() => nextTurn(), 600);
}
}
// ── function: checkEnemyFaint ──
function checkEnemyFaint(index) {
if (battle.enemies[index] && battle.enemies[index].currentHp <= 0) {
battle.enemies.splice(index, 1);
if (battle.enemies.length === 0) {
setTimeout(() => handleVictory(), 800);
return;
}
}
setTimeout(() => nextTurn(), 600);
}
// ── function: checkPlayerFaint ──
function checkPlayerFaint(index) {
updateActiveCreature();
if (battle.playerParty.filter(c => c.currentHp > 0).length === 0) {
battle.phase = 'defeat';
renderBattleUI();
}
}
// ── function: nextTurn ──
function nextTurn() {
battle.animating = false;
battleAnim.active = false;
processStatusEffects();
if (battle.phase === 'victory' || battle.phase === 'defeat') return;
const currentCreature = battle.turnOrder[battle.currentTurnIndex];
if (!currentCreature) {
battle.currentTurnIndex = 0;
nextTurn();
return;
}
if (currentCreature.creature.currentHp <= 0) {
battle.currentTurnIndex = (battle.currentTurnIndex + 1) % battle.turnOrder.length;
nextTurn();
return;
}
if (currentCreature.creature.statusEffects && currentCreature.creature.statusEffects.includes('shock')) {
if (Math.random() < 0.3) {
battle.animText = currentCreature.creature.name + ' is shocked and can\'t move!';
renderBattleUI();
setTimeout(() => {
battle.currentTurnIndex = (battle.currentTurnIndex + 1) % battle.turnOrder.length;
nextTurn();
}, 800);
return;
}
}
battle.activePlayerCreature = currentCreature.creature;
if (currentCreature.type === 'player') {
battle.phase = 'select';
renderBattleUI();
} else {
battle.phase = 'animating';
renderBattleUI();
setTimeout(() => enemyTurn(currentCreature), 600);
}
battle.currentTurnIndex = (battle.currentTurnIndex + 1) % battle.turnOrder.length;
}
// ── function: processStatusEffects ──
function processStatusEffects() {
battle.playerParty.forEach(c => {
if (c.currentHp <= 0) return;
c.statusEffects.forEach(se => {
const effect = STATUS_EFFECTS[se];
if (!effect) return;
if (effect.damage) {
const dmg = Math.floor(getMaxHp(c) * effect.damage);
c.currentHp = Math.max(0, c.currentHp - dmg);
}
if (effect.healPerTurn) {
c.currentHp = Math.min(getMaxHp(c), c.currentHp + Math.floor(getMaxHp(c) * effect.healPerTurn));
}
});
});
battle.enemies.forEach(e => {
if (e.currentHp <= 0) return;
e.statusEffects.forEach(se => {
const effect = STATUS_EFFECTS[se];
if (!effect || !effect.damage) return;
const dmg = Math.floor(getMaxHp(e) * effect.damage);
e.currentHp = Math.max(0, e.currentHp - dmg);
});
});
battle.enemies = battle.enemies.filter(e => e.currentHp > 0);
updateActiveCreature();
}
// ── function: enemyTurn ──
function enemyTurn(enemyEntry) {
const enemy = enemyEntry.creature;
const abilityId = enemy.abilities[Math.floor(Math.random() * enemy.abilities.length)];
const ability = ABILITIES[abilityId];
if (!ability) { nextTurn(); return; }
const alivePlayers = battle.playerParty.filter(c => c.currentHp > 0);
if (alivePlayers.length === 0) { nextTurn(); return; }
const target = alivePlayers[Math.floor(Math.random() * alivePlayers.length)];
const targetIndex = battle.playerParty.indexOf(target);
if (target.statusEffects.includes('evasion_up') && Math.random() < 0.4) {
battle.animText = target.name + ' evaded the attack!';
renderBattleUI();
setTimeout(() => nextTurn(), 800);
return;
}
executeMove(enemy, ability, target, targetIndex, 'enemy');
}
// ── function: handleVictory ──
function handleVictory() {
battle.phase = 'victory';
battle.playerParty.forEach(c => {
if (c.currentHp <= 0) return;
const exp = calcExp(c, battle.encounter.creature.level, battle.isBossBattle);
c.exp += exp;
checkLevelUp(c);
});
battle.playerParty.forEach(c => {
const template = CREATURES[c.id];
if (template && template.evolve && c.level >= template.evolve.atLevel) {
if (hasItem(template.evolve.catalyst, 1)) {
c.id = template.evolve.to;
c.name = CREATURES[c.id].name;
c.emoji = CREATURES[c.id].emoji;
c.affinity = CREATURES[c.id].affinity;
c.baseStats = { ...CREATURES[c.id].baseStats };
c.growth = { ...CREATURES[c.id].growth };
c.abilities = [...CREATURES[c.id].abilities];
c.evolved = true;
c.currentHp = getMaxHp(c);
removeItem(template.evolve.catalyst, 1);
battle.animText = c.name + ' transformed!';
}
}
});
if (battle.isBossBattle && battle.encounter.anchor) {
const anchorIdx = ANCHORS.indexOf(battle.encounter.anchor);
if (anchorIdx >= 0 && !game.anchorsDefeated.includes(anchorIdx)) {
game.anchorsDefeated.push(anchorIdx);
const anchor = battle.encounter.anchor;
battle.playerParty.forEach(c => {
if (c.affinityBonus) {
c.affinityBonus[anchor.bonus] = (c.affinityBonus[anchor.bonus] || 0) + anchor.bonusVal;
} else {
c.affinityBonus = { [anchor.bonus]: anchor.bonusVal };
}
});
if (anchorIdx < 3) ZONES[anchorIdx + 1].unlocked = true;
if (game.anchorsDefeated.length >= 4) game.postGame = true;
saveGame(1);
}
}
if (!battle.isBossBattle && Math.random() < 0.15) {
addItem('aether_draught', 1);
}
renderBattleUI();
playVictorySound();
}
// ── function: endBattle ──
function endBattle() {
showScreen('overworld');
resizeOverworldCanvas();
initZoneParticles(game.zone);
playZoneAmbient(game.zone);
}
// ── function: retreatFromBattle ──
function retreatFromBattle() {
showScreen('overworld');
resizeOverworldCanvas();
initZoneParticles(game.zone);
playZoneAmbient(game.zone);
}
// ── function: tryRun ──
function tryRun() {
if (battle.isBossBattle) {
battle.animText = "Can't run from a boss!";
renderBattleUI();
setTimeout(() => { battle.phase = 'select'; renderBattleUI(); }, 800);
return;
}
if (Math.random() < 0.6) {
battle.animText = 'Got away safely!';
renderBattleUI();
setTimeout(() => endBattle(), 800);
} else {
battle.animText = "Couldn't escape!";
renderBattleUI();
setTimeout(() => { battle.phase = 'select'; renderBattleUI(); }, 800);
}
}
// ── let: captureState ──
// ===== CAPTURE SYSTEM =====
let captureState = { active: false, ringRotation: 0, targetAngle: 0, zoneWidth: 0, zoneStart: 0, captured: false, creature: null, timer: 0, maxTime: 180 };
// ── function: startCapture ──
function startCapture() {
if (battle.phase !== 'select') return;
const targetEnemy = battle.enemies[0];
if (!targetEnemy || targetEnemy.currentHp <= 0) return;
captureState = {
active: true, ringRotation: 0,
targetAngle: Math.random() * Math.PI * 2,
zoneWidth: 0.3 + Math.random() * 0.4,
zoneStart: Math.random() * Math.PI * 2,
captured: false, creature: targetEnemy,
timer: 0, maxTime: 180
};
showScreen('capture');
const emojiEl = document.getElementById('capture-creature-emoji');
if (emojiEl) emojiEl.textContent = targetEnemy.emoji;
}
// ── function: tapCapture ──
function tapCapture() {
if (!captureState.active) return;
const diff = Math.abs(captureState.ringRotation - captureState.targetAngle);
const normalizedDiff = Math.min(diff, Math.PI * 2 - diff);
if (normalizedDiff < captureState.zoneWidth / 2) {
captureState.captured = true;
captureState.active = false;
const hpPercent = captureState.creature.currentHp / getMaxHp(captureState.creature);
const captureRate = CREATURES[captureState.creature.id]?.captureRate || 150;
let chance = (captureRate / 300) * (1 - hpPercent * 0.5);
if (hasItem('prism_crystal', 1)) chance *= 4;
else if (hasItem('void_orb', 1)) chance *= 2;
if (Math.random() < chance) playCaptureSound();
else playTone(200, 0.3, 'sine', 0.08);
showCaptureResult();
} else {
playTone(150, 0.1, 'sine', 0.06);
const zoneEl = document.getElementById('capture-zone');
if (zoneEl) {
zoneEl.style.borderColor = '#f87171';
setTimeout(() => { zoneEl.style.borderColor = 'transparent'; }, 200);
}
}
}
// ── function: showCaptureResult ──
function showCaptureResult() {
const resultEl = document.getElementById('capture-result');
if (!resultEl) return;
if (captureState.captured) {
const newCreature = createCreature(captureState.creature.id, captureState.creature.level);
if (game.party.length < 6) {
game.party.push(newCreature);
resultEl.innerHTML = '<div style="color:#4ade80;font-size:1.2em;margin-bottom:10px;">✨ ' + newCreature.name + ' captured!</div><button class="capture-btn" onclick="hideCaptureGame()">OK</button>';
game.collected.push(captureState.creature.id);
game.totalCaptures++;
} else {
resultEl.innerHTML = '<div style="color:#fbbf24;font-size:1.2em;margin-bottom:10px;">Party is full! ' + newCreature.name + ' added to storage.</div><button class="capture-btn" onclick="hideCaptureGame()">OK</button>';
game.collected.push(captureState.creature.id);
game.totalCaptures++;
}
} else {
resultEl.innerHTML = '<div style="color:#f87171;font-size:1.2em;margin-bottom:10px;">💔 ' + captureState.creature.name + ' broke free!</div><button class="capture-btn" onclick="hideCaptureGame()">OK</button>';
}
}
// ── function: hideCaptureGame ──
function hideCaptureGame() {
document.getElementById('capture-game').style.display = 'none';
captureState.active = false;
if (captureState.captured) {
const idx = battle.enemies.indexOf(captureState.creature);
if (idx >= 0) battle.enemies.splice(idx, 1);
if (battle.enemies.length === 0) handleVictory();
else { battle.phase = 'select'; showScreen('battle'); renderBattleUI(); }
} else {
battle.phase = 'select';
showScreen('battle');
renderBattleUI();
}
}
// ── function: openShrine ──
// ===== SHRINE SYSTEM =====
function openShrine() {
showScreen('shrine');
const panel = document.getElementById('shrine-panel');
if (panel) {
panel.innerHTML = '<h2>⛩️ Aether Shrine</h2><p style="color:var(--text-dim);margin-bottom:16px;">The shrine hums with Aetherium energy.</p>' +
'<button class="shrine-btn" onclick="shrineHeal()">💚 Heal Party</button>' +
'<button class="shrine-btn" onclick="shrineSave()">💾 Save</button>' +
'<button class="shrine-btn" onclick="shrineShop()">🛒 Shop</button>' +
'<button class="shrine-btn" onclick="shrineClose()">🚪 Leave</button>';
}
}
// ── function: shrineHeal ──
function shrineHeal() {
game.party.forEach(c => { c.currentHp = getMaxHp(c); c.statusEffects = []; });
openShrine();
}
// ── function: shrineSave ──
function shrineSave() {
saveGame(1);
openShrine();
}
// ── function: shrineShop ──
function shrineShop() {
hideModal();
showModal('<h2>🛒 Shrine Shop</h2><div style="max-height:300px;overflow-y:auto;">' +
'<div class="inv-item"><span class="item-name">Aether Draught (30HP)</span><span class="item-count">50g</span><button class="item-btn" onclick="buyItem(\'aether_draught\',50)">Buy</button></div>' +
'<div class="inv-item"><span class="item-name">Great Aether (80HP)</span><span class="item-count">150g</span><button class="item-btn" onclick="buyItem(\'great_aether\',150)">Buy</button></div>' +
'<div class="inv-item"><span class="item-name">Full Restoration</span><span class="item-count">500g</span><button class="item-btn" onclick="buyItem(\'full_restoration\',500)">Buy</button></div>' +
'<div class="inv-item"><span class="item-name">Aether Ball</span><span class="item-count">100g</span><button class="item-btn" onclick="buyItem(\'aether_ball\',100)">Buy</button></div>' +
'<div class="inv-item"><span class="item-name">Void Orb</span><span class="item-count">300g</span><button class="item-btn" onclick="buyItem(\'void_orb\',300)">Buy</button></div>' +
'<div class="inv-item"><span class="item-name">Prism Crystal</span><span class="item-count">800g</span><button class="item-btn" onclick="buyItem(\'prism_crystal\',800)">Buy</button></div>' +
'</div><button class="battle-btn" style="margin-top:12px;" onclick="hideModal()">Close</button>');
}
// ── function: buyItem ──
function buyItem(itemId, price) {
addItem(itemId, 1);
playTone(600, 0.1, 'sine', 0.08);
}
// ── function: shrineClose ──
function shrineClose() {
showScreen('overworld');
resizeOverworldCanvas();
initZoneParticles(game.zone);
}
// ── function: showSwitchMenu ──
// ===== SWITCH / ITEM MENUS =====
function showSwitchMenu() {
let html = '<h2>🔄 Switch Creature</h2><div style="max-height:250px;overflow-y:auto;">';
game.party.forEach((c, i) => {
const hpPct = c.currentHp / getMaxHp(c);
const color = hpPct > 0.5 ? '#4ade80' : hpPct > 0.2 ? '#fbbf24' : '#f87171';
const isCurrent = battle.activePlayerCreature && battle.activePlayerCreature.id === c.id && battle.activePlayerCreature.currentHp > 0;
html += '<div class="inv-item" style="' + (isCurrent ? 'opacity:0.5;' : '') + '">' +
'<span class="item-name">' + c.emoji + ' ' + c.name + ' Lv.' + c.level + ' <span style="color:' + color + '">' + c.currentHp + '/' + getMaxHp(c) + '</span></span>' +
(isCurrent ? '<span style="color:var(--text-dim)">Active</span>' : '<button class="item-btn" onclick="switchCreature(' + i + ')">Switch</button>') +
'</div>';
});
html += '</div><button class="battle-btn" style="margin-top:12px;" onclick="hideModal()">Cancel</button>';
showModal(html);
}
// ── function: switchCreature ──
function switchCreature(index) {
const target = game.party[index];
if (target.currentHp <= 0) { hideModal(); return; }
battle.activePlayerCreature = target;
hideModal();
battle.phase = 'select';
renderBattleUI();
playTone(440, 0.1, 'sine', 0.08);
}
// ── function: showItemMenu ──
function showItemMenu() {
let html = '<h2>🎒 Use Item</h2><div style="max-height:250px;overflow-y:auto;">';
['aether_draught', 'great_aether', 'full_restoration'].forEach(itemId => {
if ((game.inventory[itemId] || 0) > 0) {
const item = ITEMS[itemId];
html += '<div class="inv-item"><span class="item-name">' + item.name + ' (' + game.inventory[itemId] + ')</span><button class="item-btn" onclick="useItem(\'' + itemId + '\')">Use</button></div>';
}
});
html += '</div><button class="battle-btn" style="margin-top:12px;" onclick="hideModal()">Cancel</button>';
showModal(html);
}
// ── function: useItem ──
function useItem(itemId) {
if (!hasItem(itemId, 1)) return;
const item = ITEMS[itemId];
if (item.type === 'restore') {
if (item.full) {
game.party.forEach(c => { c.currentHp = getMaxHp(c); c.statusEffects = []; });
battle.activePlayerCreature = game.party.find(c => c.currentHp > 0) || battle.activePlayerCreature;
} else {
battle.activePlayerCreature.currentHp = Math.min(getMaxHp(battle.activePlayerCreature), battle.activePlayerCreature.currentHp + item.heal);
}
removeItem(itemId, 1);
playTone(500, 0.15, 'sine', 0.08);
}
hideModal();
battle.phase = 'select';
renderBattleUI();
}
// ── function: showEncyclopedia ──
// ===== ENCYCLOPEDIA / PARTY / INVENTORY =====
function showEncyclopedia() {
let html = '<h2>📖 Aetherial Codex</h2><div style="max-height:350px;overflow-y:auto;">';
game.collected.forEach(cid => {
const c = CREATURES[cid];
if (!c) return;
html += '<div class="encyc-entry"><div class="icon" style="background:' + TYPE_COLORS[c.affinity] + '22;">' + c.emoji + '</div>' +
'<div class="info"><div class="name" style="color:' + TYPE_COLORS[c.affinity] + '">' + c.name + '</div>' +
'<div class="detail">' + TYPE_NAMES[c.affinity] + ' | ' + c.lore + '</div></div></div>';
});
Object.keys(CREATURES).forEach(cid => {
if (game.collected.includes(cid)) return;
const c = CREATURES[cid];
html += '<div class="encyc-entry" style="opacity:0.4;"><div class="icon" style="background:' + TYPE_COLORS[c.affinity] + '22;">❓</div>' +
'<div class="info"><div class="name">???</div><div class="detail">Not yet discovered</div></div></div>';
});
html += '</div><button class="battle-btn" style="margin-top:12px;" onclick="hideModal()">Close</button>';
showModal(html);
}
// ── function: showPartyScreen ──
function showPartyScreen() {
let html = '<h2>👥 Party</h2><div style="max-height:300px;overflow-y:auto;">';
game.party.forEach(c => {
const hpPct = c.currentHp / getMaxHp(c);
const color = hpPct > 0.5 ? '#4ade80' : hpPct > 0.2 ? '#fbbf24' : '#f87171';
html += '<div class="encyc-entry"><div class="icon" style="background:' + TYPE_COLORS[c.affinity] + '22;">' + c.emoji + '</div>' +
'<div class="info"><div class="name" style="color:' + TYPE_COLORS[c.affinity] + '">' + c.name + ' Lv.' + c.level + '</div>' +
'<div class="detail">HP: <span style="color:' + color + '">' + c.currentHp + '/' + getMaxHp(c) + '</span> | ATK:' + getStat(c, 'atk') + ' DEF:' + getStat(c, 'def') + ' SPD:' + getStat(c, 'spd') + '</div>' +
'<div class="detail">Abilities: ' + c.abilities.map(a => ABILITIES[a] ? ABILITIES[a].name : '?').join(', ') + '</div></div></div>';
});
html += '</div><button class="battle-btn" style="margin-top:12px;" onclick="hideModal()">Close</button>';
showModal(html);
}
// ── function: showInventoryScreen ──
function showInventoryScreen() {
let html = '<h2>🎒 Inventory</h2><div style="max-height:300px;overflow-y:auto;">';
Object.keys(game.inventory).forEach(itemId => {
const item = ITEMS[itemId];
if (!item) return;
html += '<div class="inv-item"><span class="item-name">' + item.name + ' (' + game.inventory[itemId] + ')</span><span class="item-count">' + item.desc + '</span></div>';
});
if (Object.keys(game.inventory).length === 0) html += '<p style="color:var(--text-dim);text-align:center;padding:20px;">Empty</p>';
html += '</div><button class="battle-btn" style="margin-top:12px;" onclick="hideModal()">Close</button>';
showModal(html);
}
// ── function: showStoryVignette ──
// ===== STORY VIGNETTE =====
function showStoryVignette(text, color, duration, callback) {
const vignette = document.getElementById('story-vignette');
const textEl = document.getElementById('vignette-text');
vignette.style.background = 'radial-gradient(ellipse at center, ' + color + '22 0%, rgba(0,0,0,0.9) 70%)';
textEl.textContent = text;
textEl.style.color = color;
showScreen('vignette');
const timer = setTimeout(() => { if (callback) callback(); }, duration || 4000);
vignette.onclick = () => { clearTimeout(timer); if (callback) callback(); };
}
// ── function: showPauseMenu ──
// ===== PAUSE MENU =====
function showPauseMenu() {
showModal('<h2>⏸️ Paused</h2>' +
'<button class="pause-btn" onclick="hideModal();game.screen=\'overworld\';showScreen(\'overworld\');">▶️ Resume</button>' +
'<button class="pause-btn" onclick="showEncyclopedia();">📖 Codex (' + game.collected.length + '/27)</button>' +
'<button class="pause-btn" onclick="showPartyScreen();">👥 Party</button>' +
'<button class="pause-btn" onclick="showInventoryScreen();">🎒 Inventory</button>' +
'<button class="pause-btn" onclick="saveGame(1);hideModal();">💾 Save</button>' +
'<button class="pause-btn" onclick="toggleMute();showPauseMenu();">' + (game.audioMuted ? '🔊' : '🔇') + ' Audio</button>' +
'<button class="pause-btn" onclick="hideModal();showScreen(\'title\');">🏠 Title</button>');
}
// ── const: keys ──
// ===== OVERWORLD MOVEMENT =====
const keys = {};
// ── let: moveCooldown ──
let moveCooldown = 0;
// ── function: handleOverworldMovement ──
function handleOverworldMovement() {
if (game.screen !== 'overworld') return;
if (moveCooldown > 0) { moveCooldown--; return; }
let dx = 0, dy = 0;
if (keys['ArrowUp'] || keys['w'] || keys['W'] || keys['TouchUp']) dy = -1;
if (keys['ArrowDown'] || keys['s'] || keys['S'] || keys['TouchDown']) dy = 1;
if (keys['ArrowLeft'] || keys['a'] || keys['A'] || keys['TouchLeft']) dx = -1;
if (keys['ArrowRight'] || keys['d'] || keys['D'] || keys['TouchRight']) dx = 1;
if (dx !== 0 || dy !== 0) {
const newX = game.playerX + dx;
const newY = game.playerY + dy;
if (newX >= 0 && newX < MAP_W && newY >= 0 && newY < MAP_H) {
const tile = overworldMap[newY][newX];
if (tile !== 1 && tile !== 2 && tile !== 4) {
game.playerX = newX;
game.playerY = newY;
game.moveCount++;
moveCooldown = 8;
const zone = ZONES[game.zone];
const encounterChance = game.postGame ? zone.encounterRate * 1.5 : zone.encounterRate;
if (Math.random() < encounterChance) {
startBattle(generateWildEncounter(), false);
}
if (tile === 3) setTimeout(() => openShrine(), 200);
if (tile === 5) {
const anchorIdx = game.zone;
if (anchorIdx < ANCHORS.length && !game.anchorsDefeated.includes(anchorIdx)) {
const bossEncounter = generateBossEncounter(anchorIdx);
startBattle(bossEncounter.creature, true);
}
}
if (game.zone < ZONES.length - 1 && game.playerX === MAP_W - 3 && game.playerY === 3) {
if (ZONES[game.zone + 1].unlocked) {
transitionToZone(game.zone + 1);
}
}
}
}
}
}
// ── function: transitionToZone ──
function transitionToZone(zoneId) {
game.zone = zoneId;
overworldMap = generateMap(zoneId);
game.playerX = 5;
game.playerY = 10;
const zone = ZONES[zoneId];
game.storyStage = zoneId;
showStoryVignette(zone.story, zone.storyColor, 3500, () => {
showScreen('overworld');
resizeOverworldCanvas();
initZoneParticles(zoneId);
playZoneAmbient(zoneId);
});
}
// ── function: showTutorial ──
// ===== TUTORIAL =====
function showTutorial() {
const phases = [
{ text: 'Welcome to the Aetherium, Traveler. The realm\'s energy is fracturing.', color: '#6ee7b7' },
{ text: 'Use WASD or Arrow Keys to move. Explore the zones to find Aetherials.', color: '#818cf8' },
{ text: 'Wild Aetherials roam the zones. Battle them and capture with Aether Balls!', color: '#fb923c' },
{ text: 'Defeat the zone Guardians to unlock new realms. Good luck!', color: '#fde68a' }
];
let phase = 0;
function nextPhase() {
if (phase < phases.length) {
showStoryVignette(phases[phase].text, phases[phase].color, 3000, () => { phase++; nextPhase(); });
} else {
game.party = [createCreature('emberlit', 5), createCreature('stonewisp', 5), createCreature('thornling', 5)];
addItem('aether_ball', 5);
addItem('aether_draught', 3);
game.tutorialPhase = 1;
showScreen('overworld');
resizeOverworldCanvas();
initZoneParticles(0);
playZoneAmbient(0);
}
}
nextPhase();
}
// ── function: initTitleScreen ──
// ===== TITLE SCREEN =====
function initTitleScreen() {
const newBtn = document.getElementById('new-binding');
if (newBtn) newBtn.onclick = startNewGame;
const continueBtn = document.getElementById('continue-btn');
if (hasSave(1)) {
continueBtn.style.display = 'block';
continueBtn.onclick = () => {
loadGame(1);
game.screen = 'overworld';
showScreen('overworld');
resizeOverworldCanvas();
initZoneParticles(game.zone);
playZoneAmbient(game.zone);
};
} else {
continueBtn.style.display = 'none';
}
}
// ── let: titleParticles ──
// ===== TITLE PARTICLES =====
let titleParticles = [];
// ── function: initTitleParticles ──
function initTitleParticles() {
titleParticles = [];
for (let i = 0; i < 50; i++) {
titleParticles.push({
x: Math.random() * (window.innerWidth || 800),
y: Math.random() * (window.innerHeight || 600),
vx: (Math.random() - 0.5) * 0.5,
vy: -Math.random() * 0.5,
size: 1 + Math.random() * 3,
alpha: 0.2 + Math.random() * 0.5,
color: ['#6ee7b7', '#818cf8', '#fde68a', '#06b6d4'][Math.floor(Math.random() * 4)],
pulse: Math.random() * Math.PI * 2
});
}
}
// ── function: drawTitleParticles ──
function drawTitleParticles() {
const canvas = document.getElementById('title-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const w = canvas.width;
const h = canvas.height;
ctx.clearRect(0, 0, w, h);
for (const p of titleParticles) {
p.x += p.vx; p.y += p.vy; p.pulse += 0.02;
if (p.y < -10) { p.y = h + 10; p.x = Math.random() * w; }
if (p.x < -10) p.x = w + 10;
if (p.x > w + 10) p.x = -10;
const alpha = p.alpha * (0.7 + 0.3 * Math.sin(p.pulse));
ctx.globalAlpha = alpha;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
}
// ── function: startNewGame ──
// ===== NEW GAME =====
function startNewGame() {
initAudio();
game = {
screen: 'overworld', zone: 0, playerX: 5, playerY: 5,
party: [], inventory: {}, collected: [], anchorsDefeated: [],
storyStage: 0, totalBattles: 0, totalCaptures: 0,
shrineSave: null, audioMuted: false, postGame: false,
tutorialPhase: 0, moveCount: 0, hasMoved: false,
firstBattle: false, firstCapture: false
};
overworldMap = generateMap(0);
ZONES.forEach((z, i) => { z.unlocked = i === 0; });
showScreen('overworld');
resizeOverworldCanvas();
initZoneParticles(0);
playZoneAmbient(0);
setTimeout(() => showTutorial(), 500);
}
// ── function: setupKeyboard ──
// ===== KEYBOARD INPUT =====
function setupKeyboard() {
document.addEventListener('keydown', (e) => {
keys[e.key] = true;
if (game.screen === 'overworld' && (e.key === 'Escape' || e.key === 'p')) showPauseMenu();
if (game.screen === 'title' && e.key === 'Enter') startNewGame();
});
document.addEventListener('keyup', (e) => { keys[e.key] = false; });
}
// ── function: setupTouchControls ──
// ===== TOUCH CONTROLS =====
function setupTouchControls() {
const isMobile = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
if (isMobile) {
document.getElementById('touch-controls').style.display = 'block';
document.getElementById('touch-actions').style.display = 'flex';
['up', 'down', 'left', 'right'].forEach(dir => {
const btn = document.getElementById('btn-' + dir);
if (btn) {
btn.addEventListener('touchstart', (e) => { e.preventDefault(); keys['Touch' + dir.charAt(0).toUpperCase() + dir.slice(1)] = true; });
btn.addEventListener('touchend', (e) => { e.preventDefault(); keys['Touch' + dir.charAt(0).toUpperCase() + dir.slice(1)] = false; });
btn.addEventListener('mousedown', (e) => { keys['Touch' + dir.charAt(0).toUpperCase() + dir.slice(1)] = true; });
btn.addEventListener('mouseup', (e) => { keys['Touch' + dir.charAt(0).toUpperCase() + dir.slice(1)] = false; });
}
});
document.getElementById('touch-actions').querySelectorAll('.action-btn').forEach(btn => {
btn.addEventListener('touchstart', (e) => { e.preventDefault(); btn.click(); });
});
}
}
// ── function: updateHUD ──
// ===== HUD UPDATE =====
function updateHUD() {
const zoneName = document.getElementById('hud-zone-name');
if (zoneName && game.screen === 'overworld') zoneName.textContent = ZONES[game.zone].name;
const partyEl = document.getElementById('hud-party');
if (partyEl && game.screen === 'overworld') {
let html = '';
game.party.forEach((c, i) => {
const hpPct = c.currentHp / getMaxHp(c);
const color = hpPct > 0.5 ? '#4ade80' : hpPct > 0.2 ? '#fbbf24' : '#f87171';
html += '<div class="party-dot" style="background:' + color + '33;border-color:' + color + ';">' + c.emoji + '</div>';
});
partyEl.innerHTML = html;
}
const muteBtn = document.getElementById('mute-btn');
if (muteBtn) muteBtn.textContent = game.audioMuted ? '🔇' : '🔊';
}
// ── let: lastTime ──
// ===== MAIN GAME LOOP =====
let lastTime = 0;
// ── let: frameCount ──
let frameCount = 0;
// ── function: gameLoop ──
function gameLoop(timestamp) {
const dt = timestamp - lastTime;
lastTime = timestamp;
frameCount++;
if (game.screen === 'overworld') {
handleOverworldMovement();
updateZoneParticles();
updateParticles();
updateFloatingNumbers();
drawOverworld();
updateHUD();
} else if (game.screen === 'battle') {
updateBattleAnim();
updateParticles();
updateFloatingNumbers();
drawBattleScene();
updateHUD();
// Update capture ring
if (captureState.active) {
captureState.ringRotation = (captureState.ringRotation + 0.05) % (Math.PI * 2);
captureState.timer++;
const indicator = document.getElementById('capture-indicator');
if (indicator) indicator.style.transform = 'rotate(' + (captureState.ringRotation * 180 / Math.PI) + 'deg)';
if (captureState.timer >= captureState.maxTime) {
const hpPercent = captureState.creature.currentHp / getMaxHp(captureState.creature);
const captureRate = CREATURES[captureState.creature.id]?.captureRate || 150;
let chance = (captureRate / 300) * (1 - hpPercent * 0.5);
if (hasItem('prism_crystal', 1)) chance *= 4;
else if (hasItem('void_orb', 1)) chance *= 2;
captureState.captured = Math.random() < chance;
captureState.active = false;
showCaptureResult();
}
}
} else if (game.screen === 'title') {
drawTitleParticles();
}
requestAnimationFrame(gameLoop);
}
// ── main ──
// ===== MAIN INITIALIZATION =====
function init() {
initTitleScreen();
initTitleParticles();
setupKeyboard();
setupTouchControls();
updateHUD();
requestAnimationFrame(gameLoop);
}
window.addEventListener('load', init);
window.addEventListener('resize', () => {
if (game.screen === 'overworld') resizeOverworldCanvas();
if (game.screen === 'battle') resizeBattleCanvas();
if (game.screen === 'title') initTitleParticles();
});
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.6-35b-a3b","provider":"lmstudio","persona":"composer","sessionId":"25fb5ad4-77ab-4acb-8d64-23401b45b246","tokensIn":3505178,"tokensOut":72528,"tokensTotal":3577706,"turns":52,"toolCalls":51,"failedToolCalls":3,"timestamp":"2026-07-20T23:18:01.308Z"} -->