Creature Collection
Gemini 3.5 Flash · composer

Browser check: Artifact runtime error. This is a technical check, not a quality review.
Reviews & generation details
Reviews
Human review · Jul 17, 2026 · rubric 1
Historical path-based review. Association with these exact bytes is unverified.
- Visual design
- 3 / 5
- UX / feel
- 3 / 5
- Works
- 3 / 5
- Prompt spirit
- 4 / 5
- Delight
- 3 / 5
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 120,002 bytes · SHA-256 2c3e80828aee
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Aethera: Chronicles of the Resonated</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;600;800&family=Playfair+Display:ital,wght@0,700;1,400&display=swap">
<style>
/* ── css: layout_and_title ── */
:root {
--bg-primary: #0a0a0f;
--panel-glass: rgba(18, 18, 27, 0.85);
--border-glass: rgba(255, 255, 255, 0.12);
--text-main: #f0f0ff;
--text-muted: #a0a0c0;
--accent-cyan: #00f3ff;
--accent-gold: #ffbe3b;
--accent-pink: #ff3377;
--card-light: rgba(255, 255, 255, 0.04);
--success-color: #39e99a;
--font-main: 'Plus Jakarta Sans', sans-serif;
--font-serif: 'Playfair Display', Georgia, serif;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-primary);
color: var(--text-main);
font-family: var(--font-main);
overflow: hidden;
user-select: none;
}
#game-container {
position: relative;
width: 1280px;
height: 720px;
margin: 0 auto;
border: 1px solid var(--border-glass);
background: radial-gradient(circle at center, #1b1c2b 0%, #030409 100%);
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 10px 40px rgba(0,0,0,0.8);
}
.screen {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
transition: opacity 0.3s ease, transform 0.3s ease;
}
.screen.hidden {
opacity: 0;
transform: scale(0.98);
pointer-events: none;
}
/* Icons and generic Buttons */
.icon-btn {
position: absolute;
top: 15px;
right: 15px;
background: var(--panel-glass);
border: 1px solid var(--border-glass);
color: var(--text-main);
border-radius: 50%;
width: 44px;
height: 44px;
font-size: 20px;
cursor: pointer;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.icon-btn:hover {
border-color: var(--accent-cyan);
background: rgba(0, 243, 255, 0.15);
}
.btn {
font-family: var(--font-main);
font-weight: 600;
border-radius: 6px;
border: 1px solid var(--border-glass);
cursor: pointer;
padding: 12px 24px;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
letter-spacing: 0.5px;
font-size: 14px;
outline: none;
}
.btn-primary {
background: linear-gradient(135deg, #1d90f4, #5732dc);
color: #fff;
border: none;
box-shadow: 0 4px 15px rgba(29, 144, 244, 0.3);
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(29, 144, 244, 0.45);
}
.btn-secondary {
background: rgba(255, 255, 255, 0.06);
color: var(--text-main);
}
.btn-secondary:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.12);
}
.btn-accent {
background: linear-gradient(135deg, #dc3265, #ff3377);
color: #fff;
border: none;
}
.btn-accent:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(255, 51, 119, 0.4);
}
.btn-glass {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(8px);
color: var(--accent-cyan);
border-borderColor: rgba(0,243,255,0.2);
}
.btn-glass:hover {
background: rgba(0, 243, 255, 0.1);
border-color: var(--accent-cyan);
}
.btn-sm {
padding: 6px 14px;
font-size: 12px;
}
.btn-full {
width: 100%;
}
.btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* Luminous Title Screen */
#title-screen {
justify-content: center;
align-items: center;
gap: 40px;
}
.logo-container {
text-align: center;
}
.subtitle-engraved {
font-size: 13px;
letter-spacing: 6px;
color: var(--accent-gold);
text-shadow: 0 2px 4px rgba(0,0,0,0.5);
font-weight: 800;
margin-bottom: 8px;
}
.sparkle-title {
font-family: var(--font-serif);
font-size: 110px;
font-weight: 700;
letter-spacing: -2px;
background: linear-gradient(to right, #00f3ff, #ff3377, #ffbe3b, #00f3ff);
background-size: 300% auto;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: shimmer 8s linear infinite;
filter: drop-shadow(0 15px 30px rgba(0,0,0,0.9));
}
@keyframes shimmer {
0% { background-position: 0% 50%; }
100% { background-position: 300% 50%; }
}
.title-action-panel {
display: flex;
gap: 20px;
background: var(--panel-glass);
padding: 30px 40px;
border-radius: 12px;
border: 1px solid var(--border-glass);
box-shadow: 0 12px 30px rgba(0,0,0,0.5);
}
.title-footer {
position: absolute;
bottom: 25px;
font-size: 11px;
color: var(--text-muted);
letter-spacing: 2px;
}
/* ── css: overworld ── */
/* Overworld & Main Adventure Styling */
#adventure-screen {
justify-content: flex-start;
align-items: center;
}
.hud {
width: 100%;
height: 64px;
background: rgba(10, 10, 15, 0.85);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--border-glass);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 40px;
z-index: 10;
}
.hud-left {
display: flex;
flex-direction: column;
}
.hud-header {
font-size: 10px;
letter-spacing: 2px;
color: var(--accent-gold);
font-weight: 800;
}
.zone-label-text {
font-size: 18px;
font-weight: 800;
color: var(--text-main);
text-shadow: 0 2px 4px rgba(0,0,0,0.5);
}
.anchor-display {
background: rgba(255, 51, 119, 0.12);
border: 1px solid rgba(255, 51, 119, 0.3);
color: #ff5599;
padding: 6px 14px;
border-radius: 4px;
font-size: 12px;
font-weight: 700;
letter-spacing: 1px;
}
.canvas-wrapper {
position: relative;
width: 800px;
height: 480px;
margin-top: 40px;
border-radius: 8px;
overflow: hidden;
border: 2px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 20px 50px rgba(0,0,0,0.6);
}
#overworld-canvas {
display: block;
background: #0d0f1a;
cursor: pointer;
}
/* On-Screen Field Arrow Pad for touch/accessibility */
#field-arrow-pad {
position: absolute;
bottom: 20px;
right: 20px;
display: flex;
flex-direction: column;
gap: 6px;
align-items: center;
pointer-events: none;
}
#field-arrow-pad .pad-row {
display: flex;
gap: 6px;
}
.pad-btn {
pointer-events: auto;
width: 44px;
height: 44px;
border-radius: 8px;
background: rgba(18, 18, 27, 0.85);
border: 1px solid var(--border-glass);
color: white;
font-size: 16px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.1s;
}
.pad-btn:active {
background: var(--accent-cyan);
color: black;
}
.tips-footer {
margin-top: 20px;
font-size: 12px;
color: var(--text-muted);
}
.tips-footer b {
color: var(--accent-cyan);
}
/* ── css: modals_and_tutorial ── */
/* Glassmorphism Modals & Overlay Layouts */
.modal-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(4, 4, 8, 0.7);
backdrop-filter: blur(8px);
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.25s ease;
}
.modal-overlay.hidden {
opacity: 0;
pointer-events: none;
}
.glass-modal {
background: var(--panel-glass);
border: 1px solid var(--border-glass);
width: 520px;
border-radius: 12px;
padding: 40px;
box-shadow: 0 20px 40px rgba(0,0,0,0.8);
display: flex;
flex-direction: column;
gap: 25px;
}
.ritual-header {
font-family: var(--font-serif);
font-size: 26px;
font-weight: 700;
color: var(--accent-gold);
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
padding-bottom: 10px;
}
.intro-text {
font-size: 14px;
line-height: 1.6;
color: var(--text-muted);
max-height: 250px;
overflow-y: auto;
}
/* Starter Grid inside Tutorial */
.starter-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
margin-top: 15px;
}
.starter-card {
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--border-glass);
border-radius: 8px;
padding: 15px;
cursor: pointer;
text-align: center;
transition: all 0.25s;
}
.starter-card:hover {
transform: translateY(-4px);
border-color: var(--accent-cyan);
background: rgba(0, 243, 255, 0.06);
}
.starter-card.selected {
border-color: var(--accent-gold);
background: rgba(255, 190, 59, 0.1);
box-shadow: 0 4px 15px rgba(255, 190, 59, 0.2);
}
.starter-icon {
font-size: 36px;
margin-bottom: 10px;
animation: idle-float 3s ease-in-out infinite alternate;
}
.starter-name {
font-size: 14px;
font-weight: 700;
color: var(--text-main);
margin-bottom: 4px;
}
.starter-affinity {
font-size: 11px;
color: var(--accent-cyan);
text-transform: uppercase;
font-weight: 800;
letter-spacing: 1px;
}
/* ── css: combat_arena_css ── */
/* Combat Screen & Arena Layouts */
#combat-screen {
background: black;
overflow: hidden;
}
.combat-background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle at 50% 80%, #151a30 0%, #030409 100%);
z-index: 1;
}
.combat-fx-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 3;
pointer-events: none;
}
.combat-arena {
flex: 1;
z-index: 2;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 40px 10%);
height: calc(100% - 190px);
}
.arena-row {
display: flex;
width: 100%;
position: relative;
}
.opponents-row {
justify-content: flex-end;
gap: 20px;
align-items: flex-start;
margin-top: 20px;
}
.allies-row {
justify-content: flex-start;
align-items: flex-end;
margin-bottom: 20px;
}
/* Battle Pod (Creature node in fighting arena) */
.battle-pod {
background: rgba(18, 19, 31, 0.7);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
backdrop-filter: blur(8px);
width: 250px;
padding: 15px;
display: flex;
flex-direction: column;
gap: 8px;
position: relative;
transition: all 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28);
box-shadow: 0 10px 25px rgba(0,0,0,0.4);
}
.battle-pod.targetable {
border-color: var(--accent-cyan);
cursor: pointer;
box-shadow: 0 0 15px rgba(0, 243, 255, 0.4);
}
.battle-pod.targetable:hover {
transform: scale(1.05);
}
.pod-creature-panel {
display: flex;
align-items: center;
gap: 12px;
}
.pod-avatar {
font-size: 38px;
transition: transform 0.2s;
}
.pod-details {
display: flex;
flex-direction: column;
flex: 1;
}
.pod-name {
font-weight: 700;
font-size: 14px;
}
.pod-lvl {
font-size: 11px;
color: var(--accent-gold);
font-weight: 800;
}
.pod-type-badge {
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
background: rgba(255,255,255,0.08);
width: fit-content;
margin-top: 4px;
text-transform: uppercase;
}
/* HP & Status Elements */
.pod-stats {
display: flex;
flex-direction: column;
gap: 4px;
}
.hp-bar-outer {
width: 100%;
height: 8px;
background: rgba(255,255,255,0.1);
border-radius: 4px;
overflow: hidden;
}
.hp-bar-inner {
height: 100%;
background: linear-gradient(to right, #10b981, #059669);
width: 100%;
transition: width 0.4s ease-out;
}
.hp-bar-inner.medium {
background: linear-gradient(to right, #f59e0b, #d97706);
}
.hp-bar-inner.low {
background: linear-gradient(to right, #ef4444, #dc2626);
}
.hp-text {
font-size: 11px;
text-align: right;
color: var(--text-muted);
}
/* Status effect Badges */
.status-tray {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.status-tag {
font-size: 9px;
font-weight: 800;
padding: 1px 4px;
border-radius: 3px;
letter-spacing: 0.5px;
color: #fff;
text-transform: uppercase;
}
.status-tag.burn { background: #ef4444; }
.status-tag.frost { background: #3b82f6; }
.status-tag.shock { background: #eab308; }
.status-tag.barrier { background: #a855f7; }
.status-tag.regen { background: #10b981; }
/* Arena Animations */
@keyframes idle-float {
0% { transform: translateY(0); }
100% { transform: translateY(-6px); }
}
.allies-row .battle-pod {
animation: idle-float 4s ease-in-out infinite alternate;
}
.opponents-row .battle-pod {
animation: idle-float 4s ease-in-out infinite alternate-reverse;
}
/* ── css: combat_hud_controls ── */
/* Combat HUD & Action Deck Panel */
.combat-hud {
height: 190px;
background: rgba(10, 10, 15, 0.95);
border-top: 1px solid var(--border-glass);
display: flex;
flex-direction: column;
z-index: 10;
}
.combat-status-log {
padding: 12px 30px;
font-size: 13px;
color: var(--accent-gold);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
font-weight: 600;
letter-spacing: 0.5px;
min-height: 44px;
}
.combat-control-deck {
flex: 1;
display: flex;
padding: 15px 30px;
position: relative;
}
.combat-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 15px;
width: 100%;
}
.action-card {
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--border-glass);
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
color: var(--text-main);
cursor: pointer;
transition: all 0.2s;
}
.action-card:hover {
background: rgba(255, 255, 255, 0.08);
border-color: var(--accent-cyan);
transform: translateY(-2px);
}
.action-card .action-icon {
font-size: 24px;
}
.action-card .action-label {
font-weight: 700;
font-size: 13px;
}
/* Combat Context Panels */
.combat-context-panel {
position: absolute;
top: 15px;
left: 30px;
right: 30px;
bottom: 15px;
background: #0a0a0f;
border-radius: 8px;
display: flex;
gap: 15px;
align-items: center;
}
.combat-context-panel.hidden {
display: none !important;
}
.context-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
flex: 1;
overflow-y: auto;
padding-right: 5px;
height: 100%;
}
.context-grid-scroll {
display: flex;
gap: 12px;
flex: 1;
overflow-x: auto;
align-items: center;
height: 100%;
}
/* Custom button style inside inner combat actions */
.combat-act-btn {
background: rgba(255,255,255,0.03);
border: 1px solid var(--border-glass);
border-radius: 6px;
padding: 10px;
color: var(--text-main);
text-align: left;
cursor: pointer;
display: flex;
flex-direction: column;
gap: 4px;
height: 100%;
justify-content: space-between;
transition: all 0.15s;
}
.combat-act-btn:hover:not(:disabled) {
border-color: var(--accent-cyan);
background: rgba(0, 243, 255, 0.05);
}
.combat-act-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.btn-title-row {
display: flex;
justify-content: space-between;
width: 100%;
font-weight: 700;
font-size: 13px;
}
.btn-type-sub {
font-size: 10px;
color: var(--text-muted);
}
.btn-meta {
display: flex;
justify-content: space-between;
font-size: 11px;
}
.btn-pp {
color: var(--accent-gold);
}
/* ── css: shrine_and_shop ── */
/* Sanctuary (Shrine) and Shop */
#shrine-screen {
background: radial-gradient(circle at center, #1b0e2b 0%, #030109 100%);
justify-content: center;
align-items: center;
}
.text-center {
text-align: center;
}
.glass-container {
background: var(--panel-glass);
border: 1px solid var(--border-glass);
width: 600px;
border-radius: 12px;
padding: 40px;
box-shadow: 0 15px 40px rgba(0,0,0,0.8);
display: flex;
flex-direction: column;
gap: 20px;
position: relative;
overflow: hidden;
}
.shrine-aura {
position: absolute;
top: -50px;
left: calc(50% - 100px);
width: 200px;
height: 200px;
background: radial-gradient(circle, rgba(168, 85, 247, 0.25) 0%, rgba(0,0,0,0) 70%);
filter: blur(10px);
pointer-events: none;
animation: idle-float 6s ease-in-out infinite alternate;
}
.shrine-title {
font-family: var(--font-serif);
font-size: 32px;
color: var(--accent-gold);
text-shadow: 0 0 10px rgba(255, 190, 59, 0.4);
}
.shrine-desc {
font-size: 13px;
color: var(--text-muted);
line-height: 1.5;
}
.shrine-actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
/* Shop panel drawer layout */
.shop-drawer-panel {
border-top: 1px solid var(--border-glass);
padding-top: 20px;
margin-top: 15px;
display: flex;
flex-direction: column;
gap: 15px;
}
.flux-meter {
font-size: 13px;
font-weight: 700;
color: var(--accent-cyan);
}
.shop-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
max-height: 180px;
overflow-y: auto;
}
.shop-card {
background: rgba(255,255,255,0.03);
border: 1px solid var(--border-glass);
border-radius: 6px;
padding: 10px;
text-align: left;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
transition: all 0.15s;
}
.shop-card:hover {
background: rgba(0,243,255,0.06);
border-color: var(--accent-cyan);
}
.shop-card-info {
display: flex;
flex-direction: column;
}
.shop-item-name {
font-size: 12px;
font-weight: 700;
}
.shop-item-cost {
font-size: 11px;
color: var(--accent-gold);
}
/* ── css: codex_and_tabs ── */
/* Pause Menu / Codex & Reservoir layout */
.glass-codex-container {
background: var(--panel-glass);
border: 1px solid var(--border-glass);
width: 1040px;
height: 600px;
border-radius: 12px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 25px 50px rgba(0,0,0,0.9);
}
.codex-tabs {
display: flex;
background: rgba(10, 10, 15, 0.9);
border-bottom: 1px solid var(--border-glass);
}
.tab-btn {
background: none;
border: none;
outline: none;
color: var(--text-muted);
padding: 16px 24px;
font-family: var(--font-main);
font-weight: 700;
font-size: 13px;
cursor: pointer;
letter-spacing: 1px;
transition: all 0.2s;
border-bottom: 2px solid transparent;
}
.tab-btn:hover {
color: var(--text-main);
}
.tab-btn.active {
color: var(--accent-cyan);
border-color: var(--accent-cyan);
background: rgba(0,243,255,0.04);
}
.tab-btn-close {
margin-left: auto;
color: var(--accent-pink);
}
.tab-btn-close:hover {
background: rgba(255, 51, 119, 0.1);
}
.codex-content {
flex: 1;
padding: 30px;
overflow: hidden;
position: relative;
}
.tab-panel {
display: none;
width: 100%;
height: 100%;
}
.tab-panel.active {
display: block;
}
/* Team Tab Styling */
.team-layout {
display: grid;
grid-template-columns: 280px 1fr;
gap: 30px;
height: 100%;
}
.team-list {
display: flex;
flex-direction: column;
gap: 12px;
overflow-y: auto;
padding-right: 5px;
}
.team-card {
background: rgba(255,255,255,0.03);
border: 1px solid var(--border-glass);
border-radius: 8px;
padding: 12px;
cursor: pointer;
display: flex;
align-items: center;
gap: 12px;
transition: all 0.15s;
}
.team-card:hover {
background: rgba(255,255,255,0.06);
border-color: var(--accent-cyan);
}
.team-card.selected {
background: rgba(0,243,255,0.06);
border-color: var(--accent-cyan);
box-shadow: 0 4px 12px rgba(0,243,255,0.15);
}
.team-card-icon {
font-size: 28px;
}
.team-card-info {
display: flex;
flex-direction: column;
}
.team-card-name {
font-weight: 700;
font-size: 13px;
}
.team-card-level {
font-size: 11px;
color: var(--accent-gold);
}
/* Detail view */
.team-detail-pane {
background: rgba(0, 0, 0, 0.2);
border: 1px solid var(--border-glass);
border-radius: 8px;
padding: 25px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 20px;
}
.detail-header {
display: flex;
gap: 20px;
align-items: center;
border-bottom: 1px solid rgba(255,255,255,0.06);
padding-bottom: 15px;
}
.detail-avatar-large {
font-size: 64px;
}
.detail-meta-text {
display: flex;
flex-direction: column;
gap: 4px;
}
.detail-meta-title {
font-family: var(--font-serif);
font-size: 24px;
font-weight: 700;
}
.detail-pills {
display: flex;
gap: 6px;
}
.detail-pill-type {
font-size: 10px;
font-weight: 800;
padding: 2px 8px;
border-radius: 4px;
background: rgba(255,255,255,0.1);
text-transform: uppercase;
}
.detail-grid-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 15px;
}
.detail-stat-box {
background: rgba(255,255,255,0.02);
border: 1px solid var(--border-glass);
padding: 10px;
border-radius: 6px;
text-align: center;
}
.detail-stat-lbl {
font-size: 10px;
color: var(--text-muted);
text-transform: uppercase;
margin-bottom: 4px;
}
.detail-stat-val {
font-size: 16px;
font-weight: 700;
color: var(--text-main);
}
.detail-lore-blurb {
font-size: 12px;
line-height: 1.5;
color: var(--text-muted);
font-style: italic;
background: rgba(255,255,255,0.01);
padding: 10px;
border-radius: 6px;
border-left: 2px solid var(--accent-gold);
}
/* Abilities grid inside detail pane */
.detail-abilities {
display: flex;
flex-direction: column;
gap: 8px;
}
.detail-abilities-title {
font-size: 12px;
font-weight: 800;
color: var(--accent-cyan);
letter-spacing: 1px;
}
.detail-ab-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.detail-ab-card {
background: rgba(255,255,255,0.03);
border: 1px solid var(--border-glass);
padding: 10px;
border-radius: 6px;
font-size: 12px;
}
.neutral-placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-muted);
text-align: center;
font-size: 14px;
}
/* ── css: inventory_and_glossary ── */
/* Inventory and Glossary Styles */
.inv-split {
display: grid;
grid-template-columns: 1fr 280px;
gap: 25px;
height: calc(100% - 40px);
}
.inv-list-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
overflow-y: auto;
align-content: flex-start;
}
.inv-card {
background: rgba(255,255,255,0.03);
border: 1px solid var(--border-glass);
border-radius: 8px;
padding: 12px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.15s;
}
.inv-card:hover {
background: rgba(255,255,255,0.06);
border-color: var(--accent-cyan);
}
.inv-label-group {
display: flex;
align-items: center;
gap: 8px;
}
.inv-icon {
font-size: 20px;
}
.inv-name {
font-size: 13px;
font-weight: 700;
}
.inv-qty {
font-size: 11px;
background: rgba(255, 190, 59, 0.12);
color: var(--accent-gold);
padding: 2px 6px;
border-radius: 4px;
}
.inv-desc-pane {
background: rgba(0,0,0,0.25);
border: 1px solid var(--border-glass);
border-radius: 8px;
padding: 20px;
display: flex;
flex-direction: column;
gap: 15px;
}
.inv-desc-head {
font-size: 16px;
font-weight: 700;
color: var(--accent-cyan);
}
.inv-desc-text {
font-size: 12px;
color: var(--text-muted);
line-height: 1.5;
}
/* Glossary panel layout */
.glossary-grid {
display: grid;
grid-template-columns: repeat(9, 1fr);
gap: 12px;
overflow-y: auto;
height: 100%;
}
.gloss-card {
aspect-ratio: 1;
background: rgba(255,255,255,0.02);
border: 3px solid rgba(255,255,255,0.04);
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
cursor: help;
transition: all 0.15s;
}
.gloss-card.unlocked {
background: rgba(255,255,255,0.05);
border-color: var(--accent-cyan);
}
.gloss-card.unlocked:hover {
transform: scale(1.05);
}
.gloss-card.locked {
opacity: 0.4;
}
.gloss-idx {
position: absolute;
top: 4px;
left: 6px;
font-size: 9px;
color: var(--text-muted);
font-weight: 800;
}
.gloss-avatar {
font-size: 28px;
}
.gloss-name {
font-size: 9px;
text-align: center;
overflow: hidden;
text-transform: uppercase;
font-weight: 800;
width: 90%;
margin-top: 4px;
}
/* Rhythm Harmonization Mini Game */
.resonance-ring-container {
position: relative;
width: 200px;
height: 200px;
margin: 20px auto;
display: flex;
align-items: center;
justify-content: center;
}
.resonant-circle-target {
position: absolute;
width: 140px;
height: 140px;
border-radius: 50%;
border: 4px dashed var(--accent-cyan);
box-shadow: 0 0 10px rgba(0, 243, 255, 0.2);
}
.resonant-circle-pulse {
position: absolute;
width: 10px;
height: 10px;
border-radius: 50%;
border: 3px solid var(--accent-pink);
transform: scale(1);
}
.pulse-percentage {
font-size: 32px;
font-weight: 800;
color: var(--text-main);
text-shadow: 0 2px 4px rgba(0,0,0,0.5);
z-index: 2;
}
.btn-full-pulse {
animation: pulse-border 1.5s infinite alternate;
}
@keyframes pulse-border {
0% { transform: scale(1); box-shadow: 0 0 10px rgba(29, 144, 244, 0.3); }
100% { transform: scale(1.02); box-shadow: 0 0 25px rgba(29, 144, 244, 0.6); }
}
/* Narrative Vignette layered overlays */
.vignette-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle, rgba(13,10,31,0.9) 0%, rgba(3,2,10,0.95) 100%);
display: flex;
align-items: center;
justify-content: center;
z-index: 200;
transition: opacity 0.5s ease;
}
.vignette-layer.hidden {
opacity: 0;
pointer-events: none;
}
.vignette-dialog-bubble {
max-width: 600px;
background: var(--panel-glass);
border: 1px solid var(--border-glass);
padding: 40px;
border-radius: 12px;
display: flex;
flex-direction: column;
gap: 20px;
box-shadow: 0 10px 40px rgba(0,0,0,0.9);
}
.vignette-title {
font-weight: 800;
color: var(--accent-gold);
letter-spacing: 4px;
font-size: 13px;
}
.vignette-body {
font-family: var(--font-serif);
font-size: 20px;
line-height: 1.6;
color: var(--text-main);
}
</style>
</head>
<body>
<div id="game-container">
<!-- AUDIO / SOUND TOGGLE IN CORNER -->
<button id="mute-btn" class="icon-btn" title="Toggle Music">🔊</button>
<!-- TITLE SCREEN -->
<div id="title-screen" class="screen active">
<div class="logo-container">
<div class="subtitle-engraved">CHRONICLES OF THE RESONATED</div>
<h1 class="sparkle-title">AETHERA</h1>
</div>
<div class="title-action-panel">
<button id="new-game-btn" class="btn btn-primary">Begin New Resonance</button>
<button id="continue-btn" class="btn btn-secondary" disabled="">Resume Continuum</button>
</div>
<div class="title-footer">Crafted in the Silent Spire — Version 1.0</div>
</div>
<!-- INTRO INTRODUCTORY TUTORIAL MODAL OVERLAY -->
<div id="intro-modal" class="modal-overlay hidden">
<div class="glass-modal">
<h2 id="intro-title" class="ritual-header">Awakening Sequence</h2>
<div id="intro-body" class="intro-text"></div>
<div id="starter-choice-container" class="starter-grid hidden"></div>
<div class="modal-actions">
<button id="intro-next-btn" class="btn btn-primary">Process</button>
</div>
</div>
</div>
<!-- MAIN ADVENTURE HUD & OVERWORLD SPLIT -->
<div id="adventure-screen" class="screen hidden">
<div class="hud">
<div class="hud-left">
<div class="hud-header">🛡️ CURRENT REGION</div>
<div id="hud-zone-name" class="zone-label-text">Verdant Ascent</div>
</div>
<div class="hud-center">
<div id="anchors-count" class="anchor-display">Anchors Unchained: 0/4</div>
</div>
<div class="hud-right">
<button id="menu-btn" class="btn btn-sm btn-glass">📂 Codex & Reservoir</button>
</div>
</div>
<!-- CANVAS VIEWPORT -->
<div class="canvas-wrapper">
<canvas id="overworld-canvas" width="800" height="480"></canvas>
<div id="field-arrow-pad">
<button id="pad-up" class="pad-btn">▲</button>
<div class="pad-row">
<button id="pad-left" class="pad-btn">◀</button>
<button id="pad-down" class="pad-btn">▼</button>
<button id="pad-right" class="pad-btn">▶</button>
</div>
</div>
</div>
<div class="tips-footer">
<span>Move with <b>WASD / Arrow Keys</b> or Tap/Click Canvas/Pad. Find wild battles, resting shrines, or the Zone Guardian.</span>
</div>
</div>
<!-- COMBAT INTERFACE -->
<div id="combat-screen" class="screen hidden">
<div class="combat-background" id="combat-bg-grad"></div>
<!-- Particle canvas for damage pops & sparks -->
<canvas id="combat-particle-canvas" class="combat-fx-canvas" width="800" height="480"></canvas>
<div class="combat-arena">
<!-- Enemy Party Row (1 to 3 Targets) -->
<div id="combat-opponents" class="arena-row opponents-row"></div>
<!-- Player Active Ally Node -->
<div id="combat-allies" class="arena-row allies-row">
<!-- Main Battling Creature Display -->
<div class="battle-pod" id="active-ally-pod"></div>
</div>
</div>
<!-- COMBAT FOOTER / CONTROLS -->
<div class="combat-hud">
<div class="combat-status-log" id="combat-log">A feral presence blocks your path!</div>
<div class="combat-control-deck">
<!-- Main Controls Grid -->
<div id="combat-main-actions" class="combat-grid">
<button id="act-attack-btn" class="action-card">
<span class="action-icon">⚔️</span>
<span class="action-label">Abilities</span>
</button>
<button id="act-switch-btn" class="action-card">
<span class="action-icon">🔄</span>
<span class="action-label">Switch Ally</span>
</button>
<button id="act-collect-btn" class="action-card">
<span class="action-icon">💎</span>
<span class="action-label">Harmonize</span>
</button>
<button id="act-inventory-btn" class="action-card">
<span class="action-icon">🎒</span>
<span class="action-label">Sustain</span>
</button>
</div>
<!-- Dynamic Context Panels -->
<div id="combat-abilities-panel" class="combat-context-panel hidden">
<div class="context-grid" id="abilities-grid"></div>
<button id="combat-abilities-back" class="btn btn-sm btn-secondary btn-full">⬅ Back</button>
</div>
<div id="combat-allies-panel" class="combat-context-panel hidden">
<div class="context-grid-scroll" id="switch-allies-grid"></div>
<button id="combat-switch-back" class="btn btn-sm btn-secondary btn-full">⬅ Back</button>
</div>
<div id="combat-items-panel" class="combat-context-panel hidden">
<div class="context-grid" id="combat-items-grid"></div>
<button id="combat-items-back" class="btn btn-sm btn-secondary btn-full">⬅ Back</button>
</div>
</div>
</div>
</div>
<!-- SHRINE / SANCTUARY INTERFACE -->
<div id="shrine-screen" class="screen hidden">
<div class="glass-container text-center">
<div class="shrine-aura"></div>
<h1 class="shrine-title">⛩️ Sanctuary Anchor</h1>
<p class="shrine-desc">The localized field humming here cures essence fatigue and stores your cosmic progression index.</p>
<div class="shrine-actions">
<button id="shrine-heal" class="btn btn-primary">💖 Restore Celestial Essences</button>
<button id="shrine-save" class="btn btn-secondary">💾 Sync Cosmic Progression INDEX</button>
<button id="shrine-shop" class="btn btn-secondary">🏪 Exchange Flux (Shop)</button>
<button id="shrine-exit" class="btn btn-accent btn-full">🛰️ Step Back to Cosmos</button>
</div>
<!-- SHOP INNER DRAWER -->
<div id="shop-drawer" class="shop-drawer-panel hidden">
<h3 class="ritual-header">Sanctuary Merchant Flux Exchange</h3>
<div class="flux-meter">Flux Wallet: <span id="shop-flux-count">0</span> Aura-units</div>
<div class="shop-grid" id="shop-items-grid"></div>
</div>
</div>
</div>
<!-- CODES / RESERVOIR DIALOGUE OVERLAY (MENU) -->
<div id="menu-overlay" class="modal-overlay hidden">
<div class="glass-codex-container">
<div class="codex-tabs">
<button class="tab-btn active" data-tab="tab-team">👥 RESONA-TEAM</button>
<button class="tab-btn" data-tab="tab-inventory">🎒 CHRONO-PACK</button>
<button class="tab-btn" data-tab="tab-encyclopedia">📖 GLOSSARY (27)</button>
<button id="codex-close" class="tab-btn tab-btn-close">❌ EXIT</button>
</div>
<div class="codex-content">
<!-- TEAM TAB -->
<div id="tab-team" class="tab-panel active">
<div class="team-layout">
<div class="team-list" id="menu-team-list"></div>
<div class="team-detail-pane" id="menu-team-detail">
<p class="neutral-placeholder">Select a Resonated creature to examine cosmic alignment, traits, and prompt transformation catalyst.</p>
</div>
</div>
</div>
<!-- INVENTORY TAB -->
<div id="tab-inventory" class="tab-panel">
<div class="inv-header">🎒 Chrono-Pack Vault</div>
<div class="inv-split">
<div id="menu-inv-list" class="inv-list-grid"></div>
<div id="menu-inv-desc" class="inv-desc-pane">
<p class="neutral-placeholder">Hover/Click custom tools to read function, use transformation catalysts or restockers.</p>
</div>
</div>
</div>
<!-- ENCYCLOPEDIA TAB -->
<div id="tab-encyclopedia" class="tab-panel">
<div class="glossary-grid" id="menu-encyclopedia-grid"></div>
</div>
</div>
</div>
</div>
<!-- MINI-GAME HARMONIZE OVERLAY -->
<div id="harmonize-modal" class="modal-overlay hidden">
<div class="glass-modal text-center">
<h2 class="ritual-header">Rhythm Resonance Key</h2>
<p id="harmonize-prompt" class="small-italic">Tap inside the circle when the expanding pulse exactly overlaps the outer ring!</p>
<div class="resonance-ring-container">
<div class="resonant-circle-target"></div>
<div class="resonant-circle-pulse" id="resonant-pulse"></div>
<div class="pulse-percentage" id="pulse-percent-disp">0%</div>
</div>
<button id="harmonize-tap-btn" class="btn btn-primary btn-full-pulse">💥 HARMONIZE!</button>
</div>
</div>
<!-- FLOATING VIGNETTE DIALOG -->
<div id="narrative-vignette" class="vignette-layer hidden">
<div class="vignette-dialog-bubble">
<div class="vignette-title" id="narr-vignette-title">CONVERGENCE EVENT</div>
<div class="vignette-body" id="narr-vignette-body">...</div>
<button id="narr-vignette-btn" class="btn btn-sm btn-accent">Synthesize</button>
</div>
</div>
</div>
<script>
'use strict';
// ── class: SynthEngine ──
// WEBAUDIO BACKEND INVENTED pad synthesizer
class SynthEngine {
constructor() {
this.ctx = null;
this.muted = false;
this.primaryOsc = null;
this.secondaryOsc = null;
this.gainNode = null;
}
init() {
if (this.ctx) return;
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
if (!AudioContextClass) return;
this.ctx = new AudioContextClass();
this.gainNode = this.ctx.createGain();
this.gainNode.gain.setValueAtTime(0.04, this.ctx.currentTime);
this.gainNode.connect(this.ctx.destination);
}
toggle() {
this.muted = !this.muted;
if (this.gainNode) {
this.gainNode.gain.setValueAtTime(this.muted ? 0 : 0.04, this.ctx.currentTime);
}
return this.muted;
}
playAmbientLoop() {
this.init();
if (!this.ctx || this.primaryOsc) return;
try {
this.primaryOsc = this.ctx.createOscillator();
this.primaryOsc.type = 'triangle';
this.primaryOsc.frequency.setValueAtTime(110, this.ctx.currentTime); // A2
this.secondaryOsc = this.ctx.createOscillator();
this.secondaryOsc.type = 'sine';
this.secondaryOsc.frequency.setValueAtTime(165, this.ctx.currentTime); // E3
this.primaryOsc.connect(this.gainNode);
this.secondaryOsc.connect(this.gainNode);
this.primaryOsc.start();
this.secondaryOsc.start();
// Slow dynamic pitch modulation (LFO-like sweep)
let t = 0;
setInterval(() => {
if (this.ctx && !this.muted) {
t += 0.05;
const shift = Math.sin(t) * 1.5;
this.primaryOsc.frequency.setValueAtTime(110 + shift, this.ctx.currentTime);
this.secondaryOsc.frequency.setValueAtTime(165 - shift, this.ctx.currentTime);
}
}, 100);
} catch (e) {
console.warn("Audio Context blocked or failed during boot: ", e);
}
}
triggerBeep(freq, type = 'sine', duration = 0.1) {
this.init();
if (!this.ctx || this.muted) return;
try {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, this.ctx.currentTime);
gain.gain.setValueAtTime(0.03, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + duration);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + duration);
} catch(e) {}
}
triggerSuperEffectiveNoise() {
this.triggerBeep(440, 'triangle', 0.1);
setTimeout(() => this.triggerBeep(880, 'sine', 0.15), 80);
}
triggerCritNoise() {
this.triggerBeep(220, 'sawtooth', 0.1);
setTimeout(() => this.triggerBeep(660, 'sawtooth', 0.1), 50);
setTimeout(() => this.triggerBeep(1320, 'sine', 0.2), 100);
}
}
// ── const: AudioController ──
const AudioController = new SynthEngine();
// ── const: AFFINITIES ──
// SEVEN ORIGINAL COSMIC TYPES (AFFINITIES)
// Chart Rules:
// Pyre (A) > Bloom (B) > Hydro (C) > Pyre (A)
// Golem (D) > Pyre (A) & Hydro (C)
// Zephyr (E) > Golem (D) & Bloom (B)
// Nova (F) beats all others slightly (+30% bonus vs others)
// Epoch (G) resists everything (takes 0.5x) but is weak to Nova (F) (takes 2.0x)
const AFFINITIES = {
PYRE: 'pyre', // A
BLOOM: 'bloom', // B
HYDRO: 'hydro', // C
GOLEM: 'golem', // D
ZEPHYR: 'zephyr', // E
NOVA: 'nova', // F
EPOCH: 'epoch' // G
};
// ── const: TYPE_COLORS ──
const TYPE_COLORS = {
pyre: '#ef4444',
bloom: '#10b981',
hydro: '#06b6d4',
golem: '#b45309',
zephyr: '#a855f7',
nova: '#f59e0b',
epoch: '#ec4899'
};
// ── function: getDamageMultiplier ──
function getDamageMultiplier(attType, defType) {
// Epoch resists everything except Nova
if (defType === AFFINITIES.EPOCH) {
if (attType === AFFINITIES.NOVA) return 2.0;
return 0.5;
}
// Nova beats all other standard types slightly
if (attType === AFFINITIES.NOVA) {
return 1.3;
}
// Base Cycle
if (attType === AFFINITIES.PYRE && defType === AFFINITIES.BLOOM) return 2.0;
if (attType === AFFINITIES.BLOOM && defType === AFFINITIES.HYDRO) return 2.0;
if (attType === AFFINITIES.HYDRO && defType === AFFINITIES.PYRE) return 2.0;
// D > A & C
if (attType === AFFINITIES.GOLEM && (defType === AFFINITIES.PYRE || defType === AFFINITIES.HYDRO)) return 2.0;
// E > D & B
if (attType === AFFINITIES.ZEPHYR && (defType === AFFINITIES.GOLEM || defType === AFFINITIES.BLOOM)) return 2.0;
// If match or general resist cycle back:
if (defType === AFFINITIES.PYRE && attType === AFFINITIES.BLOOM) return 0.5;
if (defType === AFFINITIES.BLOOM && attType === AFFINITIES.PYRE) return 2.0;
return 1.0;
}
// ── const: ZONE_DATABASES ──
// OVERWORLD DATABASE & CONFIGURATION
// 6 Distinct zones, customized with unique templates, background graphics on canvas, particle styles.
const ZONE_DATABASES = [
{
idx: 0,
name: "Verdant Ascent",
color: "#0f3a20",
gradient: ["#021c0e", "#103f26"],
particleColor: "#3bf283",
encounterRate: 0.12,
storyNode: "The spatial weave begins to unravel in the lower climbs. Secure the first Anchor.",
guardianName: "Arch-Weaver Vael",
guardianAvatar: "🕷️",
unlocked: true
},
{
idx: 1,
name: "Submerged Siphon",
color: "#083344",
gradient: ["#02151e", "#0e3a4d"],
particleColor: "#0ef1f9",
encounterRate: 0.14,
storyNode: "A liquid turbine keeps the lower chambers aligned. Quiet the guardian vortex.",
guardianName: "Krakenoid Sovereign",
guardianAvatar: "🐙",
unlocked: false
},
{
idx: 2,
name: "Foundry Spire",
color: "#450a0a",
gradient: ["#210404", "#520c0c"],
particleColor: "#f9620e",
encounterRate: 0.15,
storyNode: "Searing heat maintains structural integrity. Transcend the molten crucible.",
guardianName: "Automaton Solderer",
guardianAvatar: "🤖",
unlocked: false
},
{
idx: 3,
name: "Basalt Monolith",
color: "#271b12",
gradient: ["#140d08", "#332215"],
particleColor: "#eab308",
encounterRate: 0.16,
storyNode: "High-density matrix blockades. Forge onwards past the crushing pressure.",
guardianName: "Goliath Monolith",
guardianAvatar: "🗿",
unlocked: false
},
{
idx: 4,
name: "Windswept Spire",
color: "#2e1065",
gradient: ["#170535", "#3c1285"],
particleColor: "#a855f7",
encounterRate: 0.18,
storyNode: "Violent jetstreams swirl near the outer exosphere. Tame the gale.",
guardianName: "Vortex Sphinx",
guardianAvatar: "🧝",
unlocked: false
},
{
idx: 5,
name: "Singularity Core",
color: "#030712",
gradient: ["#000000", "#111827"],
particleColor: "#ec4899",
encounterRate: 0.20,
storyNode: "The final localized anchor holding Aethera together is spinning out of control.",
guardianName: "Singularity Overlord",
guardianAvatar: "👁️",
unlocked: false
}
];
// ── const: COMBAT_ABILITIES ──
// 43 UNIQUE ABILITIES FOR COMBAT
// Format: name, power, accuracy, affinity, cost, effect (burn, frost, shock, barrier, regen, heal, multiHit, priority, atkBoost, spdBoost, critBoost)
const COMBAT_ABILITIES = {
// PYRE Type
SearingSpike: { name: "Searing Spike", power: 45, accuracy: 0.95, affinity: 'pyre', cost: 10, effect: 'burn' },
BlazeBurst: { name: "Blaze Burst", power: 65, accuracy: 0.90, affinity: 'pyre', cost: 15, effect: 'burn' },
CinderSpark: { name: "Cinder Spark", power: 30, accuracy: 1.00, affinity: 'pyre', cost: 5, effect: 'priority' },
PyreVolley: { name: "Pyre Volley", power: 25, accuracy: 0.85, affinity: 'pyre', cost: 12, effect: 'multiHit' },
InfernoRift: { name: "Inferno Rift", power: 90, accuracy: 0.80, affinity: 'pyre', cost: 20, effect: 'atkBoost' },
FlameVeil: { name: "Flame Veil", power: 0, accuracy: 1.00, affinity: 'pyre', cost: 8, effect: 'barrier' },
// BLOOM Type
IvyLash: { name: "Ivy Lash", power: 45, accuracy: 0.95, affinity: 'bloom', cost: 10, effect: 'spdBoost' },
LeafTracer: { name: "Leaf Tracer", power: 30, accuracy: 1.00, affinity: 'bloom', cost: 6, effect: 'priority' },
FloraRenew: { name: "Flora Renew", power: 0, accuracy: 1.00, affinity: 'bloom', cost: 12, effect: 'regen' },
ThornEruption: { name: "Thorn Eruption", power: 75, accuracy: 0.85, affinity: 'bloom', cost: 18, effect: 'critBoost' },
ForestHeal: { name: "Forest Heal", power: 40, accuracy: 1.00, affinity: 'bloom', cost: 15, effect: 'heal' },
RootBnd: { name: "Root Bind", power: 55, accuracy: 0.90, affinity: 'bloom', cost: 10, effect: 'frost' },
// HYDRO Type
AquaJet: { name: "Aqua Jet", power: 35, accuracy: 1.00, affinity: 'hydro', cost: 5, effect: 'priority' },
VortexSpout: { name: "Vortex Spout", power: 60, accuracy: 0.95, affinity: 'hydro', cost: 12, effect: 'frost' },
VaporShield: { name: "Vapor Shield", power: 0, accuracy: 1.00, affinity: 'hydro', cost: 8, effect: 'barrier' },
TidalCrash: { name: "Tidal Crash", power: 80, accuracy: 0.85, affinity: 'hydro', cost: 18, effect: 'spdBoost' },
RadianSip: { name: "Radian Sip", power: 45, accuracy: 0.95, affinity: 'hydro', cost: 12, effect: 'heal' },
MistGlow: { name: "Mist Glow", power: 0, accuracy: 1.00, affinity: 'hydro', cost: 10, effect: 'regen' },
// GOLEM Type
StoneCrush: { name: "Stone Crush", power: 50, accuracy: 0.95, affinity: 'golem', cost: 10, effect: 'none' },
DustStorm: { name: "Dust Storm", power: 35, accuracy: 0.90, affinity: 'golem', cost: 8, effect: 'frost' },
ClayShield: { name: "Clay Shield", power: 0, accuracy: 1.00, affinity: 'golem', cost: 12, effect: 'barrier' },
MeteorTome: { name: "Meteor Tome", power: 85, accuracy: 0.80, affinity: 'golem', cost: 20, effect: 'atkBoost' },
FossilClaw: { name: "Fossil Claw", power: 60, accuracy: 0.90, affinity: 'golem', cost: 12, effect: 'critBoost' },
RockTumbler: { name: "Rock Tumbler", power: 30, accuracy: 0.85, affinity: 'golem', cost: 15, effect: 'multiHit' },
// ZEPHYR Type
GaleGusta: { name: "Gale Gusta", power: 45, accuracy: 0.95, affinity: 'zephyr', cost: 10, effect: 'spdBoost' },
ZephyrClip: { name: "Zephyr Clip", power: 30, accuracy: 1.00, affinity: 'zephyr', cost: 6, effect: 'priority' },
TornadoVane: { name: "Tornado Vane", power: 75, accuracy: 0.85, affinity: 'zephyr', cost: 16, effect: 'shock' },
SonicCloud: { name: "Sonic Cloud", power: 0, accuracy: 1.00, affinity: 'zephyr', cost: 8, effect: 'barrier' },
FeatherLick: { name: "Feather Lick", power: 40, accuracy: 0.95, affinity: 'zephyr', cost: 10, effect: 'regen' },
AetherDrift: { name: "Aether Drift", power: 95, accuracy: 0.80, affinity: 'zephyr', cost: 22, effect: 'atkBoost' },
// NOVA Type
NovaPulse: { name: "Nova Pulse", power: 55, accuracy: 0.95, affinity: 'nova', cost: 12, effect: 'critBoost' },
AuraBlast: { name: "Aura Blast", power: 75, accuracy: 0.90, affinity: 'nova', cost: 18, effect: 'shock' },
GlitterBeam: { name: "Glitter Beam", power: 35, accuracy: 1.00, affinity: 'nova', cost: 8, effect: 'priority' },
CosmicSweep: { name: "Cosmic Sweep", power: 25, accuracy: 0.85, affinity: 'nova', cost: 14, effect: 'multiHit' },
Supernova: { name: "Supernova", power: 100, accuracy: 0.75, affinity: 'nova', cost: 25, effect: 'burn' },
StarlightRefrain: { name: "Starlight Refrain", power: 50, accuracy: 1.00, affinity: 'nova', cost: 15, effect: 'heal' },
// EPOCH Type
StasisLock: { name: "Stasis Lock", power: 50, accuracy: 0.95, affinity: 'epoch', cost: 12, effect: 'frost' },
TemporalStrike: { name: "Temporal Strike", power: 70, accuracy: 0.90, affinity: 'epoch', cost: 16, effect: 'shock' },
ContinuumWarp: { name: "Continuum Warp", power: 0, accuracy: 1.00, affinity: 'epoch', cost: 15, effect: 'regen' },
ParadoxScythe: { name: "Paradox Scythe", power: 90, accuracy: 0.85, affinity: 'epoch', cost: 24, effect: 'critBoost' },
EpochArmor: { name: "Epoch Armor", power: 0, accuracy: 1.00, affinity: 'epoch', cost: 10, effect: 'barrier' },
ChronosEcho: { name: "Chronos Echo", power: 30, accuracy: 0.85, affinity: 'epoch', cost: 15, effect: 'multiHit' },
AeonHeal: { name: "Aeon Heal", power: 45, accuracy: 1.00, affinity: 'epoch', cost: 18, effect: 'heal' }
};
// ── const: CREATURES_DB ──
// CREATURES DATABASE
// 18 Base Forms + 9 Catalyst Transformed Forms = 27 total.
// Each has: key, name, avatar (emoji), affinity, stats, desc, learnset (level up abilities list), catalyst req.
const CREATURES_DB = [
// Base Creatures (Indices 1 to 18)
{
id: 1, name: "Pyroshell", avatar: "🐢", affinity: 'pyre', baseStats: { hp: 55, atk: 52, def: 65, spd: 40 },
lore: "Its shell maintains a continuous ember. Very sturdy but heavy.",
learnset: [
{ lvl: 1, move: 'SearingSpike' },
{ lvl: 5, move: 'CinderSpark' },
{ lvl: 12, move: 'FlameVeil' },
{ lvl: 25, move: 'InfernoRift' }
],
transformsTo: 19, transformLvl: 18, catalyst: "Cinder Dynamo"
},
{
id: 2, name: "Sporepup", avatar: "🐕", affinity: 'bloom', baseStats: { hp: 48, atk: 56, def: 42, spd: 60 },
lore: "Sniffs out ancient soil deposits to fuel the fungal spores on its back.",
learnset: [
{ lvl: 1, move: 'IvyLash' },
{ lvl: 4, move: 'LeafTracer' },
{ lvl: 10, move: 'ForestHeal' },
{ lvl: 22, move: 'ThornEruption' }
],
transformsTo: 20, transformLvl: 18, catalyst: "Spore Nucleus"
},
{
id: 3, name: "Finlet", avatar: "🐬", affinity: 'hydro', baseStats: { hp: 50, atk: 48, def: 46, spd: 66 },
lore: "Emits sub-aquatic frequencies that soothe nearby bio-nodes.",
learnset: [
{ lvl: 1, move: 'AquaJet' },
{ lvl: 6, move: 'VortexSpout' },
{ lvl: 11, move: 'MistGlow' },
{ lvl: 24, move: 'TidalCrash' }
],
transformsTo: 21, transformLvl: 18, catalyst: "Vapor Membrane"
},
{
id: 4, name: "CrystaGrub", avatar: "🐛", affinity: 'golem', baseStats: { hp: 60, atk: 45, def: 58, spd: 35 },
lore: "Feeds on dense geo-stratas to coat itself in flawless crystalline platelets.",
learnset: [
{ lvl: 1, move: 'StoneCrush' },
{ lvl: 8, move: 'DustStorm' },
{ lvl: 15, move: 'ClayShield' },
{ lvl: 26, move: 'MeteorTome' }
],
transformsTo: 22, transformLvl: 18, catalyst: "Geo Prism"
},
{
id: 5, name: "ZephyrBeak", avatar: "🐦", affinity: 'zephyr', baseStats: { hp: 45, atk: 54, def: 38, spd: 72 },
lore: "An avian vector that glides effortlessly through micro-currents.",
learnset: [
{ lvl: 1, move: 'ZephyrClip' },
{ lvl: 5, move: 'GaleGusta' },
{ lvl: 14, move: 'TornadoVane' },
{ lvl: 28, move: 'AetherDrift' }
],
transformsTo: 23, transformLvl: 20, catalyst: "Wind Core"
},
{
id: 6, name: "Starlet", avatar: "⭐", affinity: 'nova', baseStats: { hp: 42, atk: 58, def: 40, spd: 68 },
lore: "A fragment of fallen stellar light condensed into biological sentience.",
learnset: [
{ lvl: 1, move: 'GlitterBeam' },
{ lvl: 7, move: 'NovaPulse' },
{ lvl: 16, move: 'CosmicSweep' },
{ lvl: 30, move: 'Supernova' }
],
transformsTo: 24, transformLvl: 22, catalyst: "Cosmic Spark"
},
{
id: 7, name: "RuneLizard", avatar: "🦎", affinity: 'epoch', baseStats: { hp: 64, atk: 50, def: 52, spd: 48 },
lore: "Ancient inscriptions glow along its neck, shifting local timelines by milliseconds.",
learnset: [
{ lvl: 1, move: 'StasisLock' },
{ lvl: 8, move: 'ContinuumWarp' },
{ lvl: 18, move: 'TemporalStrike' },
{ lvl: 28, move: 'ParadoxScythe' }
],
transformsTo: 25, transformLvl: 24, catalyst: "Epoch Scroll"
},
{
id: 8, name: "Emberscar", avatar: "🦊", affinity: 'pyre', baseStats: { hp: 46, atk: 62, def: 40, spd: 64 },
lore: "Its sleek fur channels excess kinetic energy into small, controlled thermal flares.",
learnset: [
{ lvl: 1, move: 'CinderSpark' },
{ lvl: 6, move: 'PyreVolley' },
{ lvl: 15, move: 'BlazeBurst' }
],
transformsTo: 26, transformLvl: 20, catalyst: "Cinder Dynamo"
},
{
id: 9, name: "VineCoyote", avatar: "🐺", affinity: 'bloom', baseStats: { hp: 52, atk: 58, def: 48, spd: 55 },
lore: "Its vine-woven coat traps surrounding moisture, which it feeds back to forest seedlings.",
learnset: [
{ lvl: 1, move: 'IvyLash' },
{ lvl: 7, move: 'FloraRenew' },
{ lvl: 16, move: 'RootBnd' }
],
transformsTo: 27, transformLvl: 21, catalyst: "Spore Nucleus"
},
{
id: 10, name: "CoralCrab", avatar: "🦀", affinity: 'hydro', baseStats: { hp: 58, atk: 50, def: 60, spd: 42 },
lore: "The coral colony on its limb acts as a defensive sponge.",
learnset: [
{ lvl: 1, move: 'RadianSip' },
{ lvl: 6, move: 'VaporShield' },
{ lvl: 14, move: 'VortexSpout' }
],
transformsTo: null
},
{
id: 11, name: "Gravelpelt", avatar: "🦡", affinity: 'golem', baseStats: { hp: 65, atk: 55, def: 55, spd: 38 },
lore: "A sturdy dweller of cavern networks, constantly tunneling with razor-claws.",
learnset: [
{ lvl: 1, move: 'StoneCrush' },
{ lvl: 8, move: 'FossilClaw' },
{ lvl: 16, move: 'RockTumbler' }
],
transformsTo: null
},
{
id: 12, name: "AirDolphin", avatar: "🐳", affinity: 'zephyr', baseStats: { hp: 70, atk: 48, def: 52, spd: 58 },
lore: "Soars through condensation clouds, converting damp mists into propulsion drafts.",
learnset: [
{ lvl: 1, move: 'GaleGusta' },
{ lvl: 7, move: 'SonicCloud' },
{ lvl: 15, move: 'FeatherLick' }
],
transformsTo: null
},
{
id: 13, name: "OrbWisp", avatar: "🔮", affinity: 'nova', baseStats: { hp: 40, atk: 65, def: 35, spd: 70 },
lore: "Highly unstable radioactive core that pulses in response to cosmic activity.",
learnset: [
{ lvl: 1, move: 'GlitterBeam' },
{ lvl: 8, move: 'AuraBlast' },
{ lvl: 17, move: 'NovaPulse' }
],
transformsTo: null
},
{
id: 14, name: "RiftGazer", avatar: "👻", affinity: 'epoch', baseStats: { hp: 50, atk: 52, def: 52, spd: 62 },
lore: "Possesses secondary eyes that observe alternative parallel realities simultaneously.",
learnset: [
{ lvl: 1, move: 'StasisLock' },
{ lvl: 7, move: 'EpochArmor' },
{ lvl: 16, move: 'ChronosEcho' }
],
transformsTo: null
},
{ id: 15, name: "KelpPony", avatar: "🐎", affinity: 'hydro', baseStats: { hp: 54, atk: 50, def: 44, spd: 62 }, lore: "A swift shore-runner that coats its hooves in high-tension aqua shields.", learnset: [{ lvl: 1, move: 'AquaJet' }], transformsTo: null },
{ id: 16, name: "GeodeSlug", avatar: "🐌", affinity: 'golem', baseStats: { hp: 72, atk: 40, def: 64, spd: 28 }, lore: "A slow sediment eater that leaves a mineral trail usable as focus fuel.", learnset: [{ lvl: 1, move: 'StoneCrush' }], transformsTo: null },
{ id: 17, name: "FlutterGlow", avatar: "🦋", affinity: 'nova', baseStats: { hp: 44, atk: 56, def: 38, spd: 68 }, lore: "Attracted to magnetic field disturbances. Glows like hot magnesium when threatened.", learnset: [{ lvl: 1, move: 'GlitterBeam' }], transformsTo: null },
{ id: 18, name: "AeonRex", avatar: "🦖", affinity: 'epoch', baseStats: { hp: 78, atk: 60, def: 58, spd: 40 }, lore: "An apex predator that bypasses kinetic barrier fields using temporal jaws.", learnset: [{ lvl: 1, move: 'StasisLock' }, { lvl: 12, move: 'TemporalStrike' }], transformsTo: null },
// Transformed Forms (9 Total: Indices 19 to 27)
{
id: 19, name: "Pyroshell-Hyperion", avatar: "🌋", affinity: 'pyre', baseStats: { hp: 85, atk: 82, def: 95, spd: 55 },
lore: "Superheated internal pressure has infused its shell with volatile magma engines.",
learnset: [{ lvl: 1, move: 'BlazeBurst' }, { lvl: 20, move: 'FlameVeil' }, { lvl: 35, move: 'InfernoRift' }], transformsTo: null
},
{
id: 20, name: "Sporepup-Mycelium", avatar: "🦁", affinity: 'bloom', baseStats: { hp: 78, atk: 86, def: 72, spd: 80 },
lore: "Fully bonded with the host fungus, discharging toxic spore clusters on impact.",
learnset: [{ lvl: 1, move: 'ForestHeal' }, { lvl: 20, move: 'ThornEruption' }, { lvl: 35, move: 'RootBnd' }], transformsTo: null
},
{
id: 21, name: "Finlet-Vaporwing", avatar: "🐉", affinity: 'hydro', baseStats: { hp: 75, atk: 78, def: 76, spd: 96 },
lore: "Propels itself through dense atmosphere on wings of super-heated moisture.",
learnset: [{ lvl: 1, move: 'VortexSpout' }, { lvl: 20, move: 'TidalCrash' }, { lvl: 35, move: 'RadianSip' }], transformsTo: null
},
{
id: 22, name: "CrystaGrub-Titan", avatar: "💎", affinity: 'golem', baseStats: { hp: 90, atk: 75, def: 92, spd: 45 },
lore: "Its diamondoid carapace blocks direct kinetic impacts with sheer crystalline weight.",
learnset: [{ lvl: 1, move: 'ClayShield' }, { lvl: 20, move: 'MeteorTome' }, { lvl: 35, move: 'FossilClaw' }], transformsTo: null
},
{
id: 23, name: "ZephyrBeak-Tornado", avatar: "🦅", affinity: 'zephyr', baseStats: { hp: 75, atk: 84, def: 68, spd: 102 },
lore: "An absolute master of thermal corridors. Forces vacuums at will.",
learnset: [{ lvl: 1, move: 'GaleGusta' }, { lvl: 22, move: 'AetherDrift' }, { lvl: 36, move: 'TornadoVane' }], transformsTo: null
},
{
id: 24, name: "Starlet-Omega", avatar: "🌟", affinity: 'nova', baseStats: { hp: 72, atk: 92, def: 65, spd: 94 },
lore: "A fully collapsed biological star, emitting blinding waves of lethal spectrums.",
learnset: [{ lvl: 1, move: 'AuraBlast' }, { lvl: 24, move: 'Supernova' }, { lvl: 38, move: 'StarlightRefrain' }], transformsTo: null
},
{
id: 25, name: "RuneLizard-Epochal", avatar: "🐊", affinity: 'epoch', baseStats: { hp: 94, atk: 80, def: 82, spd: 68 },
lore: "Its aura freezes surrounding time entirely. Moves with terrifying calculations.",
learnset: [{ lvl: 1, move: 'ContinuumWarp' }, { lvl: 25, move: 'ParadoxScythe' }, { lvl: 38, move: 'AeonHeal' }], transformsTo: null
},
{
id: 26, name: "Emberscar-Chimera", avatar: "🐅", affinity: 'pyre', baseStats: { hp: 76, atk: 88, def: 65, spd: 88 },
lore: "Its claws hum with thermal kinetic energy, leaving trails of plasma wherever it runs.",
learnset: [{ lvl: 1, move: 'BlazeBurst' }, { lvl: 22, move: 'InfernoRift' }], transformsTo: null
},
{
id: 27, name: "VineCoyote-Sylvan", avatar: "🐺", affinity: 'bloom', baseStats: { hp: 82, atk: 84, def: 74, spd: 82 },
lore: "The deep wooden limbs store memory clusters of Aethera before the great fracturing.",
learnset: [{ lvl: 1, move: 'RootBnd' }, { lvl: 22, move: 'ThornEruption' }], transformsTo: null
}
];
// ── const: INVENTORY_DB ──
// GAME INVENTORY DATABASE ITEMS
const INVENTORY_DB = {
// Capture items
'harm_1': { name: "Harmonizer Core", dName: "Harmonist Core", type: 'capture', rate: 1.0, cost: 100, desc: "Standard resonance tuner. 1.0x grab rate." },
'harm_2': { name: "Aetherial Harness", dName: "Aether Harness", type: 'capture', rate: 1.5, cost: 250, desc: "Reinforced dimensional frame. 1.5x grab rate." },
'harm_3': { name: "Cosmic Lattice", dName: "Cosmic Lattice", type: 'capture', rate: 2.2, cost: 600, desc: "Hyper-dimensional mesh. 2.2x grab rate." },
// Restoratives
'heal_1': { name: "Re-Flux Tonic", dName: "Re-Flux Tonic", type: 'heal', hp: 30, cost: 50, desc: "Repairs superficial matrix damage. Restores 30 HP." },
'heal_2': { name: "Essence Restorer", dName: "Essence Restorer", type: 'heal', hp: 100, cost: 150, desc: "Synthesizes vital matter. Restores 100 HP." },
'heal_full': { name: "Cosmic Elixir", dName: "Cosmic Elixir", type: 'heal', hp: 999, cost: 400, desc: "Repairs entire matrix. Fully heals a creature." },
// Catalysts for transformations
'cat_pyre': { name: "Cinder Dynamo", dName: "Cinder Dynamo", type: 'catalyst', cost: 300, desc: "Required to catalyze Pyre evolutions above Level 18." },
'cat_bloom': { name: "Spore Nucleus", dName: "Spore Nucleus", type: 'catalyst', cost: 300, desc: "Required to catalyze Bloom evolutions above Level 18." },
'cat_hydro': { name: "Vapor Membrane", dName: "Vapor Membrane", type: 'catalyst', cost: 300, desc: "Required to catalyze Hydro evolutions above Level 18." },
'cat_golem': { name: "Geo Prism", dName: "Geo Prism", type: 'catalyst', cost: 300, desc: "Required to catalyze Golem evolutions above Level 18." },
'cat_zephyr': { name: "Wind Core", dName: "Wind Core", type: 'catalyst', cost: 300, desc: "Required to catalyze Zephyr evolutions above Level 20." },
'cat_nova': { name: "Cosmic Spark", dName: "Cosmic Spark", type: 'catalyst', cost: 350, desc: "Required to catalyze Nova evolutions above Level 22." },
'cat_epoch': { name: "Epoch Scroll", dName: "Epoch Scroll", type: 'catalyst', cost: 350, desc: "Required to catalyze Epoch evolutions above Level 24." }
};
// ── const: MAP_COLS ──
// OVERWORLD MAP TILES AND PLAYER STATE ARCHITECTURE
const MAP_COLS = 20;
// ── const: MAP_ROWS ──
const MAP_ROWS = 12;
// ── const: TILE_SIZE ──
const TILE_SIZE = 40;
// ── const: BASE_MAP_TEMPLATE ──
// Grid tile types:
// 0 = Emptiness (Path)
// 1 = High resonance grass (Wild encounters)
// 2 = Monolith/Wall
// 3 = Rest Sanctuary (Shrine)
// 4 = Zone anchor gate (Boss/Guardian)
// 5 = Zone transit gate (Travel to next zone!)
const BASE_MAP_TEMPLATE = [
[2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,2,2],
[2,1,1,1,0,0,0,0,1,1,1,1,0,0,0,2,2,0,2,2],
[2,1,0,0,0,2,2,0,0,1,1,0,0,2,0,0,0,0,0,2],
[2,1,0,2,2,2,2,2,0,0,0,0,2,2,2,2,1,1,0,2],
[2,0,0,0,0,0,0,2,2,0,2,2,2,0,0,2,1,1,0,2],
[2,0,2,2,2,0,0,0,0,0,0,0,0,0,0,2,0,0,0,2],
[2,0,2,3,2,0,2,2,2,2,4,2,2,2,0,2,0,2,0,2],
[2,0,2,0,2,0,2,1,1,1,1,1,1,2,0,2,0,2,0,2],
[2,1,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,2,5,2],
[2,1,1,0,2,2,2,2,2,2,2,2,2,2,2,1,1,2,0,2],
[2,1,1,0,0,0,0,0,1,1,1,1,0,0,0,0,1,1,0,2],
[2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2]
];
// ── class: OverworldEngine ──
class OverworldEngine {
constructor(canvasId) {
this.canvas = document.getElementById(canvasId);
this.ctx = this.canvas.getContext('2d');
this.particles = [];
}
// Draw region based on active custom zone gradients & particles
render(playerPos, currentZone, anchorDefeated) {
const zoneData = ZONE_DATABASES[currentZone];
// Gradient Render
const grad = this.ctx.createRadialGradient(400, 240, 50, 400, 240, 450);
grad.addColorStop(0, zoneData.gradient[1]);
grad.addColorStop(1, zoneData.gradient[0]);
this.ctx.fillStyle = grad;
this.ctx.fillRect(0, 0, 800, 480);
// Draw ambient particles
this.updateParticles(zoneData.particleColor);
// Draw the tilemap
for (let r = 0; r < MAP_ROWS; r++) {
for (let c = 0; c < MAP_COLS; c++) {
const tile = BASE_MAP_TEMPLATE[r][c];
const x = c * TILE_SIZE;
const y = r * TILE_SIZE;
if (tile === 2) {
// Monolith wall
this.ctx.fillStyle = "rgba(255, 255, 255, 0.05)";
this.ctx.fillRect(x + 2, y + 2, TILE_SIZE - 4, TILE_SIZE - 4);
this.ctx.strokeStyle = "rgba(255, 255, 255, 0.15)";
this.ctx.strokeRect(x + 2, y + 2, TILE_SIZE - 4, TILE_SIZE - 4);
} else if (tile === 1) {
// High resonance grass
this.ctx.fillStyle = "rgba(0, 243, 255, 0.08)";
this.ctx.fillRect(x + 4, y + 4, TILE_SIZE - 8, TILE_SIZE - 8);
this.ctx.fillStyle = "rgba(0, 243, 255, 0.15)";
this.ctx.fillRect(x + 10, y + 10, TILE_SIZE - 20, TILE_SIZE - 20);
} else if (tile === 3) {
// Shrine
this.ctx.fillStyle = "#ec4899";
this.ctx.beginPath();
this.ctx.arc(x + 20, y + 20, 10, 0, Math.PI * 2);
this.ctx.fill();
this.ctx.strokeStyle = "#fff";
this.ctx.lineWidth = 2;
this.ctx.stroke();
this.ctx.fillStyle = "#fff";
this.ctx.font = "12px sans-serif";
this.ctx.fillText("⛩️", x + 10, y + 24);
} else if (tile === 4) {
// Guardian Node
this.ctx.fillStyle = anchorDefeated ? "rgba(16, 185, 129, 0.2)" : "rgba(239, 68, 68, 0.2)";
this.ctx.fillRect(x + 1, y + 1, TILE_SIZE - 2, TILE_SIZE - 2);
this.ctx.strokeStyle = anchorDefeated ? "#10b981" : "#ef4444";
this.ctx.strokeRect(x + 1, y + 1, TILE_SIZE - 2, TILE_SIZE - 2);
this.ctx.fillStyle = "#fff";
this.ctx.font = "14px sans-serif";
this.ctx.fillText(anchorDefeated ? "✅" : "⚠️", x + 12, y + 26);
} else if (tile === 5) {
// Zone travel gate
this.ctx.fillStyle = "rgba(255, 190, 59, 0.1)";
this.ctx.fillRect(x + 1, y + 1, TILE_SIZE - 2, TILE_SIZE - 2);
this.ctx.strokeStyle = "#ffbe3b";
this.ctx.strokeRect(x + 2, y + 2, TILE_SIZE - 4, TILE_SIZE - 4);
this.ctx.fillStyle = "#ffbe3b";
this.ctx.font = "12px Arial";
this.ctx.fillText("🌀", x + 11, y + 25);
}
}
}
// Render Player
const px = playerPos.x * TILE_SIZE;
const py = playerPos.y * TILE_SIZE;
this.ctx.fillStyle = "#00f3ff";
this.ctx.shadowColor = "#00f3ff";
this.ctx.shadowBlur = 10;
this.ctx.beginPath();
this.ctx.arc(px + 20, py + 20, 14, 0, Math.PI * 2);
this.ctx.fill();
this.ctx.shadowBlur = 0; // Reset
// Small interior dot for player orientation
this.ctx.fillStyle = "#ffffff";
this.ctx.beginPath();
this.ctx.arc(px + 20, py + 20, 5, 0, Math.PI * 2);
this.ctx.fill();
}
updateParticles(color) {
if (this.particles.length < 35) {
this.particles.push({
x: Math.random() * 800,
y: Math.random() * 480,
r: Math.random() * 2 + 1,
vy: Math.random() * -0.5 - 0.2,
alpha: Math.random() * 0.4 + 0.2
});
}
this.particles.forEach((p, idx) => {
p.y += p.vy;
if (p.y < 0) {
p.y = 480;
p.x = Math.random() * 800;
}
this.ctx.fillStyle = color;
this.ctx.globalAlpha = p.alpha;
this.ctx.beginPath();
this.ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
this.ctx.fill();
});
this.ctx.globalAlpha = 1.0;
}
}
// ── class: GameState ──
// DETAILED STATE MANAGEMENT SYSTEM (AUTO & MANUAL STORAGE INDEX SAVING)
class GameState {
constructor() {
this.playerPos = { x: 5, y: 5 };
this.currentZone = 0;
this.fluxWallet = 150; // Starting Currency
this.team = []; // Up to 6 active creatures
this.inventory = {
'harm_1': 4,
'harm_2': 1,
'heal_1': 3,
'cat_pyre': 1
};
this.unlockedEncylopedia = {}; // ID indexed booleans
this.anchorsDefeated = [false, false, false, false, false, false]; // Zone indexed
this.anchorsTargetCount = 4; // Final Boss victory requires 4 zone anchors beaten
this.tutorialCompleted = false;
this.finalGameCompleted = false;
}
addCreatureToTeam(modelId, level = 5) {
if (this.team.length >= 6) return false;
const dbData = CREATURES_DB.find(c => c.id === modelId);
if (!dbData) return false;
const stats = this.calcStatsForLvl(dbData.baseStats, level);
const newCreature = {
id: Date.now() + Math.random(),
modelId: modelId,
name: dbData.name,
avatar: dbData.avatar,
affinity: dbData.affinity,
level: level,
xp: 0,
maxXp: level * 80,
hp: stats.hp,
maxHp: stats.hp,
atk: stats.atk,
def: stats.def,
spd: stats.spd,
status: null, // 'burn', 'frost', 'shock', 'barrier', 'regen'
activeAbilities: []
};
// Populate active moves from learnset
dbData.learnset.forEach(m => {
if (level >= m.lvl && newCreature.activeAbilities.length < 4) {
newCreature.activeAbilities.push({
moveKey: m.move,
pp: COMBAT_ABILITIES[m.move].cost * 2,
maxPp: COMBAT_ABILITIES[m.move].cost * 2
});
}
});
this.team.push(newCreature);
this.unlockedEncylopedia[modelId] = true;
return true;
}
calcStatsForLvl(base, lvl) {
return {
hp: Math.floor(base.hp + (base.hp * 0.11 * lvl)),
atk: Math.floor(base.atk + (base.atk * 0.09 * lvl)),
def: Math.floor(base.def + (base.def * 0.08 * lvl)),
spd: Math.floor(base.spd + (base.spd * 0.09 * lvl))
};
}
levelUpCreature(creature) {
if (creature.level >= 50) return;
creature.level++;
creature.xp = 0;
creature.maxXp = creature.level * 80;
const dbData = CREATURES_DB.find(c => c.id === creature.modelId);
const stats = this.calcStatsForLvl(dbData.baseStats, creature.level);
// Scale current HP proportionally
const ratio = creature.hp / creature.maxHp;
creature.maxHp = stats.hp;
creature.hp = Math.max(1, Math.floor(stats.hp * ratio));
creature.atk = stats.atk;
creature.def = stats.def;
creature.spd = stats.spd;
// Check newly unlocked learnset capabilities
dbData.learnset.forEach(m => {
if (creature.level === m.lvl) {
// Replace or Append logic
if (creature.activeAbilities.length < 4) {
creature.activeAbilities.push({
moveKey: m.move,
pp: COMBAT_ABILITIES[m.move].cost * 2,
maxPp: COMBAT_ABILITIES[m.move].cost * 2
});
} else {
// Replace first slot
creature.activeAbilities[0] = {
moveKey: m.move,
pp: COMBAT_ABILITIES[m.move].cost * 2,
maxPp: COMBAT_ABILITIES[m.move].cost * 2
};
}
}
});
}
triggerCatalystTransformation(creature, itemKey) {
const dbData = CREATURES_DB.find(c => c.id === creature.modelId);
if (!dbData || !dbData.transformsTo) return { success: false, msg: "This creature cannot be catalyzed further." };
if (creature.level < dbData.transformLvl) return { success: false, msg: `Requires cosmic frequency level of ${dbData.transformLvl} or higher.` };
const requiredCatalyst = dbData.catalyst;
const itemData = INVENTORY_DB[itemKey];
if (!itemData || itemData.name !== requiredCatalyst) {
return { success: false, msg: `Requires transformation catalyst: ${requiredCatalyst}` };
}
// Deduct catalyst
this.inventory[itemKey]--;
if (this.inventory[itemKey] <= 0) delete this.inventory[itemKey];
// Evolve creature in-place!
const targetFormId = dbData.transformsTo;
const evolvedDb = CREATURES_DB.find(c => c.id === targetFormId);
creature.modelId = targetFormId;
creature.name = evolvedDb.name;
creature.avatar = evolvedDb.avatar;
// Recalculate stats
const stats = this.calcStatsForLvl(evolvedDb.baseStats, creature.level);
creature.maxHp = stats.hp;
creature.hp = stats.hp;
creature.atk = stats.atk;
creature.def = stats.def;
creature.spd = stats.spd;
this.unlockedEncylopedia[targetFormId] = true;
return { success: true, msg: `${creature.name} has transformed successfully with ${itemData.name}!` };
}
serialize() {
return JSON.stringify({
playerPos: this.playerPos,
currentZone: this.currentZone,
fluxWallet: this.fluxWallet,
team: this.team,
inventory: this.inventory,
unlockedEncylopedia: this.unlockedEncylopedia,
anchorsDefeated: this.anchorsDefeated,
tutorialCompleted: this.tutorialCompleted,
finalGameCompleted: this.finalGameCompleted
});
}
deserialize(dataString) {
try {
const parsed = JSON.parse(dataString);
this.playerPos = parsed.playerPos;
this.currentZone = parsed.currentZone;
this.fluxWallet = parsed.fluxWallet;
this.team = parsed.team;
this.inventory = parsed.inventory;
this.unlockedEncylopedia = parsed.unlockedEncylopedia;
this.anchorsDefeated = parsed.anchorsDefeated;
this.tutorialCompleted = parsed.tutorialCompleted;
this.finalGameCompleted = parsed.finalGameCompleted;
return true;
} catch (e) {
console.error("Error restoring stream indexes. Resetting.", e);
return false;
}
}
}
// ── const: CurrentState ──
const CurrentState = new GameState();
// ── class: CombatEngine ──
// BATTLE SCENE COORDINATOR & COMBAT RULES
class CombatEngine {
constructor() {
this.isActive = false;
this.allies = [];
this.opponents = [];
this.activeAllyIdx = 0;
this.currentLog = "";
this.combatType = "wild"; // 'wild' or 'guardian'
this.selectedAction = null; // Stored target selection phase data
this.resolveTurnCallback = null;
this.floatingTexts = []; // [{x, y, text, color, life}]
}
initWildBattle(zoneIdx, playerTeam) {
this.isActive = true;
this.combatType = "wild";
// Clone party to isolate battle temporary HP changes and status
this.allies = playerTeam;
this.activeAllyIdx = this.getFirstAbleAllyIdx();
// Spawn 1 to 3 wild opponents scaled close to player average level
const avgLvl = Math.max(3, Math.round(playerTeam.reduce((acc, c) => acc + c.level, 0) / playerTeam.length));
const randomCount = Math.random() < 0.65 ? 1 : (Math.random() < 0.85 ? 2 : 3);
this.opponents = [];
const zoneAvail = this.getZoneSpawns(zoneIdx);
for (let i = 0; i < randomCount; i++) {
const template = zoneAvail[Math.floor(Math.random() * zoneAvail.length)];
const spawnedLvl = Math.max(1, avgLvl + Math.floor(Math.random() * 3) - 1);
const enemyStats = CurrentState.calcStatsForLvl(template.baseStats, spawnedLvl);
const wildEnemy = {
modelId: template.id,
name: "Wild " + template.name,
avatar: template.avatar,
affinity: template.affinity,
level: spawnedLvl,
hp: enemyStats.hp,
maxHp: enemyStats.hp,
atk: enemyStats.atk,
def: enemyStats.def,
spd: enemyStats.spd,
status: null,
activeAbilities: template.learnset.map(m => m.move)
};
this.opponents.push(wildEnemy);
CurrentState.unlockedEncylopedia[template.id] = true;
}
this.currentLog = `A primal presence emerges! Team size: ${this.opponents.length}`;
}
initGuardianBattle(zoneIdx, playerTeam) {
this.isActive = true;
this.combatType = "guardian";
this.allies = playerTeam;
this.activeAllyIdx = this.getFirstAbleAllyIdx();
const zone = ZONE_DATABASES[zoneIdx];
// Scale guardian to be a true challenge!
const avgLvl = Math.max(8, Math.round(playerTeam.reduce((acc, c) => acc + c.level, 0) / playerTeam.length)) + 5;
// Guardian behaves like a massive, highly optimized high-tier Golem/Epoch/Nova boss
const bossModelId = 18; // AeonRex template
const bossStats = CurrentState.calcStatsForLvl({ hp: 120, atk: 75, def: 85, spd: 45 }, avgLvl);
const guardian = {
modelId: bossModelId,
name: zone.guardianName,
avatar: zone.guardianAvatar,
affinity: 'epoch',
level: avgLvl,
hp: bossStats.hp,
maxHp: bossStats.hp,
atk: bossStats.atk,
def: bossStats.def,
spd: bossStats.spd,
status: null,
activeAbilities: ['StasisLock', 'TemporalStrike', 'EpochArmor', 'CosmicSweep']
};
this.opponents = [guardian];
this.currentLog = `GUARDIAN ALERT: ${zone.guardianName} blocks the spatial threshold!`;
}
getZoneSpawns(zoneIdx) {
// Return creatures associated with this zone
if (zoneIdx === 0) return CREATURES_DB.filter(c => c.affinity === 'pyre' || c.affinity === 'bloom');
if (zoneIdx === 1) return CREATURES_DB.filter(c => c.affinity === 'hydro' || c.affinity === 'zephyr');
if (zoneIdx === 2) return CREATURES_DB.filter(c => c.affinity === 'pyre' || c.affinity === 'golem');
if (zoneIdx === 3) return CREATURES_DB.filter(c => c.affinity === 'golem' || c.affinity === 'epoch');
if (zoneIdx === 4) return CREATURES_DB.filter(c => c.affinity === 'zephyr' || c.affinity === 'nova');
return CREATURES_DB.filter(c => c.affinity === 'nova' || c.affinity === 'epoch');
}
getFirstAbleAllyIdx() {
for (let i = 0; i < this.allies.length; i++) {
if (this.allies[i].hp > 0) return i;
}
return 0;
}
getActiveAlly() {
return this.allies[this.activeAllyIdx];
}
// CORE DAMAGE FORMULA IMPLEMENTATION
// (0.4 × Level × ATK / DEF × Multiplier) + small random, floored sensibly. Critical hits apply. STAB = +25%
calculateCombatDamage(attacker, defender, ability) {
const isCrit = Math.random() < 0.12;
const mult = getDamageMultiplier(ability.affinity, defender.affinity);
// Base math
let damage = (0.4 * attacker.level * (attacker.atk / Math.max(1, defender.def)) * mult);
// Apply STAB (Same Type Attack Bonus)
if (attacker.affinity === ability.affinity) {
damage *= 1.25;
}
if (isCrit) {
damage *= 1.5;
}
// Variance & absolute floor
damage += Math.random() * 4;
damage = Math.floor(damage);
if (damage <= 0) damage = 1;
return { amount: damage, crit: isCrit, scale: mult };
}
executeAbilityTurn(abilityKey, targetIdx) {
const ally = this.getActiveAlly();
const target = this.opponents[targetIdx];
const ability = COMBAT_ABILITIES[abilityKey];
if (!ally || !target || !ability) return;
// Expend PP
const abRef = ally.activeAbilities.find(a => a.moveKey === abilityKey);
if (abRef) abRef.pp = Math.max(0, abRef.pp - 1);
// Speed comparison sequence logic (SPD Order decides turn order)
const allyFirst = ally.spd >= target.spd;
let turnLog = "";
if (allyFirst) {
turnLog += this.processAttackStrike(ally, target, ability, "ally");
if (target.hp > 0) {
turnLog += "<br>" + this.processOpponentReply(target, ally);
}
} else {
turnLog += this.processOpponentReply(target, ally);
if (ally.hp > 0) {
turnLog += "<br>" + this.processAttackStrike(ally, target, ability, "ally");
}
}
// Process turn-end status tick (Burn damage, Regen effects)
turnLog += this.processGlobalStatusTicks();
this.currentLog = turnLog;
this.triggerRenderCallback();
}
processAttackStrike(attacker, defender, ability, sourceSide) {
if (attacker.hp <= 0) return "";
// Status: Shock skips attack entirely
if (attacker.status === 'shock' && Math.random() < 0.35) {
return `❌ ${attacker.name}'s quantum state is shocked! Action bypassed.`;
}
// Check Accuracy
if (Math.random() > ability.accuracy) {
return `💨 ${attacker.name} used ${ability.name}... and missed!`;
}
// Check Barrier
if (defender.status === 'barrier') {
defender.status = null;
return `🛡️ ${attacker.name}'s ${ability.name} was entirely deflected by ${defender.name}'s Barrier!`;
}
const res = this.calculateCombatDamage(attacker, defender, ability);
defender.hp = Math.max(0, defender.hp - res.amount);
let logs = `💥 ${attacker.name} used <b>${ability.name}</b> on ${defender.name} dealing <b>${res.amount}</b> matrix damage.`;
if (res.crit) {
logs += ` <b style="color:var(--accent-gold);">[CRITICAL COUPLING!]</b>`;
AudioController.triggerCritNoise();
}
if (res.scale > 1.1) {
logs += ` <b style="color:var(--accent-cyan);">[Resonance Cascade!]</b>`;
AudioController.triggerSuperEffectiveNoise();
} else {
AudioController.triggerBeep(330, 'sine', 0.1);
}
// Apply secondary status effect chances
if (ability.effect && ability.effect !== 'none' && defender.hp > 0) {
if (Math.random() < 0.50) {
if (['burn', 'frost', 'shock', 'barrier', 'regen'].includes(ability.effect)) {
if (ability.effect === 'barrier' || ability.effect === 'regen') {
attacker.status = ability.effect;
logs += ` <b style="color:var(--success-color);">[Self-infused ${ability.effect.toUpperCase()}]</b>`;
} else {
defender.status = ability.effect;
logs += ` <b>[Target afflicted with ${ability.effect.toUpperCase()}]</b>`;
}
} else if (ability.effect === 'heal') {
const healPct = Math.floor(attacker.maxHp * 0.35);
attacker.hp = Math.min(attacker.maxHp, attacker.hp + healPct);
logs += ` <b>[Recovered ${healPct} HP]</b>`;
} else if (ability.effect === 'atkBoost') {
attacker.atk = Math.floor(attacker.atk * 1.25);
logs += ` <b>[ATK amplified +25%]</b>`;
} else if (ability.effect === 'spdBoost') {
attacker.spd = Math.floor(attacker.spd * 1.25);
logs += ` <b>[SPD amplified +25%]</b>`;
}
}
}
return logs;
}
processOpponentReply(opponent, ally) {
if (opponent.hp <= 0) return "";
// Dynamic mini AI chooses random available move
const moves = opponent.activeAbilities;
const moveKey = moves[Math.floor(Math.random() * moves.length)];
const ability = COMBAT_ABILITIES[moveKey] || COMBAT_ABILITIES.SearingSpike;
return this.processAttackStrike(opponent, ally, ability, "opponent");
}
processGlobalStatusTicks() {
let tickLog = "";
// Process status for active ally
const ally = this.getActiveAlly();
if (ally && ally.hp > 0) {
if (ally.status === 'burn') {
const burnDmg = Math.floor(ally.maxHp * 0.1);
ally.hp = Math.max(1, ally.hp - burnDmg);
tickLog += `<br>🔥 ${ally.name} taking burn decay (-${burnDmg} HP).`;
} else if (ally.status === 'regen') {
const regenVal = Math.floor(ally.maxHp * 0.12);
ally.hp = Math.min(ally.maxHp, ally.hp + regenVal);
tickLog += `<br>🌿 ${ally.name} revitalized by Regen capsule (+${regenVal} HP).`;
}
}
// Process status for active opponent targets
this.opponents.forEach(enemy => {
if (enemy.hp > 0) {
if (enemy.status === 'burn') {
const decay = Math.floor(enemy.maxHp * 0.1);
enemy.hp = Math.max(0, enemy.hp - decay);
tickLog += `<br>🔥 ${enemy.name} decays from Burn flux (-${decay} HP).`;
} else if (enemy.status === 'regen') {
const boost = Math.floor(enemy.maxHp * 0.12);
enemy.hp = Math.min(enemy.maxHp, enemy.hp + boost);
tickLog += `<br>🌿 ${enemy.name} synthesized HP (+${boost} HP).`;
}
}
});
return tickLog;
}
switchActiveAlly(targetIdx) {
if (this.allies[targetIdx].hp <= 0) return;
const oldName = this.getActiveAlly().name;
this.activeAllyIdx = targetIdx;
const newAlly = this.getActiveAlly();
this.currentLog = `🔄 Recalled ${oldName}. Synchronized Active Node to <b>${newAlly.name}</b>!`;
// Opponent gets free counter response instantly!
const activeEnemy = this.opponents.find(e => e.hp > 0);
if (activeEnemy) {
this.currentLog += "<br>" + this.processOpponentReply(activeEnemy, newAlly);
}
this.triggerRenderCallback();
}
setUpdateCallback(cb) {
this.resolveTurnCallback = cb;
}
triggerRenderCallback() {
if (this.resolveTurnCallback) this.resolveTurnCallback();
}
}
// ── const: ActiveCombat ──
const ActiveCombat = new CombatEngine();
// ── class: HarmonizeCoreEngine ──
// RHYTHM INTEGRATION INTERFACE (MINI-GAME IMPLEMENTATION)
class HarmonizeCoreEngine {
constructor() {
this.active = false;
this.scale = 1.0;
this.increasing = true;
this.successThreshold = 0.85; // Percent match target range
this.pulseInterval = null;
this.renderNode = null;
this.displayPercentNode = null;
this.targetCreatureIdx = 0;
this.captureDeviceKey = 'harm_1';
this.onCompleteCallback = null;
}
start(itemKey, oppIdx, onComplete) {
this.active = true;
this.scale = 0.1;
this.increasing = true;
this.captureDeviceKey = itemKey;
this.targetCreatureIdx = oppIdx;
this.onCompleteCallback = onComplete;
this.renderNode = document.getElementById("resonant-pulse");
this.displayPercentNode = document.getElementById("pulse-percent-disp");
document.getElementById("harmonize-modal").classList.remove("hidden");
if (this.pulseInterval) clearInterval(this.pulseInterval);
this.pulseInterval = setInterval(() => {
this.updatePulse();
}, 15);
}
updatePulse() {
if (!this.active) return;
if (this.increasing) {
this.scale += 0.015;
if (this.scale >= 1.5) {
this.scale = 1.5;
this.increasing = false;
}
} else {
this.scale -= 0.015;
if (this.scale <= 0.1) {
this.scale = 0.1;
this.increasing = true;
}
}
// Convert scale factors layout representation
// Target is exactly dashed ring around scale = 1.0
const accuracy = Math.max(0, 100 - Math.abs(1.0 - this.scale) * 100);
this.displayPercentNode.textContent = `${Math.floor(accuracy)}%`;
if (this.renderNode) {
this.renderNode.style.width = `${this.scale * 140}px`;
this.renderNode.style.height = `${this.scale * 140}px`;
}
}
triggerTap() {
if (!this.active) return;
clearInterval(this.pulseInterval);
this.active = false;
document.getElementById("harmonize-modal").classList.add("hidden");
const accuracy = Math.max(0, 1.0 - Math.abs(1.0 - this.scale));
// Evaluate if grab targets sync sequence succeeds
if (this.onCompleteCallback) {
this.onCompleteCallback(accuracy);
}
}
}
// ── const: ActiveHarmonizer ──
const ActiveHarmonizer = new HarmonizeCoreEngine();
// ── const: UIRenderer ──
// STYLISH RENDERERS AND DATA SYNCHRONIZERS
const UIRenderer = {
// Overworld UI Refresh
updateHUD() {
const zoneName = document.getElementById("hud-zone-name");
const zone = ZONE_DATABASES[CurrentState.currentZone];
if (zoneName && zone) {
zoneName.textContent = `${zone.idx + 1}. ${zone.name}`;
zoneName.style.color = zone.particleColor;
}
const anchorsCount = document.getElementById("anchors-count");
if (anchorsCount) {
const activeUnchained = CurrentState.anchorsDefeated.filter(x => x).length;
anchorsCount.textContent = `Anchors Unchained: ${activeUnchained}/4`;
if (activeUnchained >= 4) {
anchorsCount.style.borderColor = "var(--success-color)";
anchorsCount.style.color = "var(--success-color)";
}
}
},
// Refresh entire Pause Codex and state details
refreshCodexMenu() {
const listContainer = document.getElementById("menu-team-list");
if (!listContainer) return;
listContainer.innerHTML = "";
CurrentState.team.forEach((ally, idx) => {
const card = document.createElement("div");
card.className = "team-card";
card.innerHTML = `
<div class="team-card-icon">${ally.avatar}</div>
<div class="team-card-info">
<div class="team-card-name">${ally.name}</div>
<div class="team-card-level">Level ${ally.level}</div>
</div>
`;
card.onclick = () => UIRenderer.showTeamMemberDetail(idx);
listContainer.appendChild(card);
});
this.refreshInventoryTab();
this.refreshGlossaryGrid();
},
showTeamMemberDetail(idx) {
const ally = CurrentState.team[idx];
const pane = document.getElementById("menu-team-detail");
if (!pane || !ally) return;
// Check transformation catalyst availability
const origDb = CREATURES_DB.find(c => c.id === ally.modelId);
let transformButtonHtml = "";
if (origDb && origDb.transformsTo) {
const hasReqItem = Object.keys(CurrentState.inventory).find(k => INVENTORY_DB[k].name === origDb.catalyst);
transformButtonHtml = `
<div style="border-top: 1px solid var(--border-glass); padding-top: 15px; margin-top: 5px;">
<h4 style="font-size:12px; margin-bottom: 6px; color: var(--accent-gold);">MUTATION PATHWAY DETECTED</h4>
<p style="font-size:11px; color: var(--text-muted); margin-bottom: 10px;">Requires leveling depth of ${origDb.transformLvl} + Catalyst [${origDb.catalyst}].</p>
<button class="btn btn-sm btn-primary" onclick="triggerPartyEvolution(${idx}, '${hasReqItem || ''}')" ${(!hasReqItem || ally.level < origDb.transformLvl) ? 'disabled' : ''}>
💥 Inject ${origDb.catalyst} Trigger
</button>
</div>
`;
}
pane.innerHTML = `
<div class="detail-header">
<div class="detail-avatar-large">${ally.avatar}</div>
<div class="detail-meta-text">
<div class="detail-meta-title">${ally.name}</div>
<div class="detail-pills">
<span class="detail-pill-type" style="background:${TYPE_COLORS[ally.affinity]}">${ally.affinity}</span>
<span class="detail-pill-type" style="color:var(--accent-gold)">Level ${ally.level}</span>
</div>
</div>
</div>
<div class="detail-grid-stats">
<div class="detail-stat-box">
<div class="detail-stat-lbl">HP Matrix</div>
<div class="detail-stat-val">${ally.hp}/${ally.maxHp}</div>
</div>
<div class="detail-stat-box">
<div class="detail-stat-lbl">Attack</div>
<div class="detail-stat-val">${ally.atk}</div>
</div>
<div class="detail-stat-box">
<div class="detail-stat-lbl">Defense</div>
<div class="detail-stat-val">${ally.def}</div>
</div>
<div class="detail-stat-box">
<div class="detail-stat-lbl">Speed</div>
<div class="detail-stat-val">${ally.spd}</div>
</div>
</div>
<div class="detail-lore-blurb">
${origDb ? origDb.lore : 'Deep quantum energy flows perfectly within its physical boundaries.'}
</div>
<div class="detail-abilities">
<div class="detail-abilities-title">COSMIC ACTIONS EQUIP LIST</div>
<div class="detail-ab-grid">
${ally.activeAbilities.map(ab => {
const inf = COMBAT_ABILITIES[ab.moveKey];
return `
<div class="detail-ab-card">
<b>⚡ ${inf.name}</b> (${inf.affinity})
<div style="font-size:10px; color:var(--text-muted); margin-top: 3px;">Energy: ${ab.pp}/${ab.maxPp} | Pwr: ${inf.power}</div>
</div>
`;
}).join('')}
</div>
</div>
${transformButtonHtml}
`;
},
refreshInventoryTab() {
const list = document.getElementById("menu-inv-list");
if (!list) return;
list.innerHTML = "";
Object.keys(CurrentState.inventory).forEach(itemKey => {
const qty = CurrentState.inventory[itemKey];
const detail = INVENTORY_DB[itemKey];
if (!detail || qty <= 0) return;
const card = document.createElement("div");
card.className = "inv-card";
card.innerHTML = `
<div class="inv-label-group">
<span class="inv-icon">📦</span>
<span class="inv-name">${detail.dName}</span>
</div>
<span class="inv-qty">x${qty}</span>
`;
card.onclick = () => {
const descNode = document.getElementById("menu-inv-desc");
let actionBtn = "";
// Allow out of battle healing from inventory tab
if (detail.type === 'heal') {
actionBtn = `
<button class="btn btn-sm btn-primary" onclick="triggerOutOfBattleHeal('${itemKey}')" style="margin-top:10px; width:100%;">
Trigger restore on team party
</button>
`;
}
descNode.innerHTML = `
<div class="inv-desc-head">${detail.name}</div>
<div class="inv-desc-text">${detail.desc}</div>
${actionBtn}
`;
};
list.appendChild(card);
});
},
refreshGlossaryGrid() {
const grid = document.getElementById("menu-encyclopedia-grid");
if (!grid) return;
grid.innerHTML = "";
// 27 Total indexed database entries
CREATURES_DB.forEach(c => {
const isUnlocked = CurrentState.unlockedEncylopedia[c.id];
const card = document.createElement("div");
card.className = `gloss-card ${isUnlocked ? 'unlocked' : 'locked'}`;
card.innerHTML = `
<span class="gloss-idx">#${String(c.id).padStart(2, '0')}</span>
<span class="gloss-avatar">${isUnlocked ? c.avatar : '❓'}</span>
<span class="gloss-name">${isUnlocked ? c.name : 'Unknown Alignment'}</span>
`;
grid.appendChild(card);
});
},
refreshCombatScreen() {
const displayLog = document.getElementById("combat-log");
if (displayLog) {
displayLog.innerHTML = ActiveCombat.currentLog;
}
// Load active ally details card
const activeAllyPod = document.getElementById("active-ally-pod");
const ally = ActiveCombat.getActiveAlly();
if (activeAllyPod && ally) {
const activeAbilitiesStr = ally.activeAbilities.map(ab => ab.moveKey).join(",");
const hpPct = Math.max(0, (ally.hp / ally.maxHp) * 100);
let hpCls = "";
if (hpPct < 25) hpCls = "low";
else if (hpPct < 55) hpCls = "medium";
activeAllyPod.innerHTML = `
<div class="pod-creature-panel">
<div class="pod-avatar">${ally.avatar}</div>
<div class="pod-details">
<div class="pod-name">${ally.name}</div>
<div class="pod-lvl">LVL ${ally.level}</div>
<div class="pod-type-badge" style="border:1px solid ${TYPE_COLORS[ally.affinity]}">${ally.affinity}</div>
</div>
</div>
<div class="pod-stats">
<div class="hp-bar-outer">
<div class="hp-bar-inner ${hpCls}" style="width: ${hpPct}%"></div>
</div>
<div class="hp-text">${ally.hp} / ${ally.maxHp} HP</div>
</div>
<div class="status-tray" id="combat-ally-status-tray">
${ally.status ? `<span class="status-tag ${ally.status}">${ally.status}</span>` : ''}
</div>
`;
}
// Load Active Opponent list row
const targetRow = document.getElementById("combat-opponents");
if (targetRow) {
targetRow.innerHTML = "";
ActiveCombat.opponents.forEach((enemy, idx) => {
if (enemy.hp <= 0) return; // Hide defeated opponents completely
const enemyPct = Math.max(0, (enemy.hp / enemy.maxHp) * 100);
let hpCls = "";
if (enemyPct < 25) hpCls = "low";
else if (enemyPct < 55) hpCls = "medium";
const pod = document.createElement("div");
pod.className = "battle-pod";
// Add targetable styles depending on action select phase
if (ActiveCombat.selectedAction) {
pod.classList.add("targetable");
pod.onclick = () => executeTriggeredCombatMove(idx);
}
pod.innerHTML = `
<div class="pod-creature-panel">
<div class="pod-avatar">${enemy.avatar}</div>
<div class="pod-details">
<div class="pod-name">${enemy.name}</div>
<div class="pod-lvl">LVL ${enemy.level}</div>
<div class="pod-type-badge" style="border:1px solid ${TYPE_COLORS[enemy.affinity]}">${enemy.affinity}</div>
</div>
</div>
<div class="pod-stats">
<div class="hp-bar-outer">
<div class="hp-bar-inner ${hpCls}" style="width: ${enemyPct}%"></div>
</div>
<div class="hp-text">${enemy.hp} / ${enemy.maxHp} HP</div>
</div>
<div class="status-tray">
${enemy.status ? `<span class="status-tag ${enemy.status}">${enemy.status}</span>` : ''}
</div>
`;
targetRow.appendChild(pod);
});
}
// Refresh Dynamic panels in active combat deck
this.refreshCombatAbilityPanel();
},
refreshCombatAbilityPanel() {
const grid = document.getElementById("abilities-grid");
if (!grid) return;
grid.innerHTML = "";
const ally = ActiveCombat.getActiveAlly();
if (!ally) return;
ally.activeAbilities.forEach(ab => {
const details = COMBAT_ABILITIES[ab.moveKey];
if (!details) return;
const btn = document.createElement("button");
btn.className = "combat-act-btn";
btn.disabled = ab.pp <= 0;
btn.innerHTML = `
<div class="btn-title-row">
<span>${details.name}</span>
<span class="btn-type-sub" style="color:${TYPE_COLORS[details.affinity]}">${details.affinity}</span>
</div>
<div class="btn-meta">
<span>Pwr: ${details.power}</span>
<span class="btn-pp">PP: ${ab.pp}/${ab.maxPp}</span>
</div>
`;
btn.onclick = () => triggerSelectCombatTarget(ab.moveKey);
grid.appendChild(btn);
});
}
};
// ── const: OverworldController ──
// INTERACTIVE FIELD MOVEMENT, ENCOUNTERS, AND STORY VIGNETTES LOGIC
const OverworldController = {
playerX: 5,
playerY: 5,
canvasInstance: null,
init(overworldEngineInstance) {
this.canvasInstance = overworldEngineInstance;
this.refresh();
},
refresh() {
this.playerX = CurrentState.playerPos.x;
this.playerY = CurrentState.playerPos.y;
this.canvasInstance.render(CurrentState.playerPos, CurrentState.currentZone, CurrentState.anchorsDefeated[CurrentState.currentZone]);
UIRenderer.updateHUD();
},
tryMove(dx, dy) {
const targetX = this.playerX + dx;
const targetY = this.playerY + dy;
// Bounds checking
if (targetX < 0 || targetX >= MAP_COLS || targetY < 0 || targetY >= MAP_ROWS) return;
// Solid Wall Collision check
const tile = BASE_MAP_TEMPLATE[targetY][targetX];
if (tile === 2) {
AudioController.triggerBeep(180, 'triangle', 0.15); // Bonk Sound
return;
}
// Success movement
this.playerX = targetX;
this.playerY = targetY;
CurrentState.playerPos.x = targetX;
CurrentState.playerPos.y = targetY;
AudioController.triggerBeep(520, 'sine', 0.05);
// Re-render
this.refresh();
// Check interaction behaviors
this.evaluateCurrentTile(tile);
},
evaluateCurrentTile(tile) {
// 1 = High resonance grass (Chance of wild battle)
if (tile === 1) {
const zoneData = ZONE_DATABASES[CurrentState.currentZone];
if (Math.random() < zoneData.encounterRate) {
triggerBattleTransition("wild");
}
}
// 3 = Rest Sanctuary (Shrine)
else if (tile === 3) {
triggerShrineTransition();
}
// 4 = Zone Guardian (Boss)
else if (tile === 4) {
// Trigger boss dialog/battle if not already defeated
if (!CurrentState.anchorsDefeated[CurrentState.currentZone]) {
triggerNarrativeVignette(
"THRESHOLD CHALLENGE",
`Guardian ${ZONE_DATABASES[CurrentState.currentZone].guardianName} hums at high frequency. Defeat it to release this zone's dimensional anchor!`,
() => triggerBattleTransition("guardian")
);
} else {
triggerNarrativeVignette(
"ANCHOR HARMONIZED",
`You have already stabilized the spatial structure of this alignment. Move to the orange warp zone!`,
null
);
}
}
// 5 = Zone transport gate warp
else if (tile === 5) {
// Can only travel to next zone if the previous zone's guardian was defeated!
if (CurrentState.anchorsDefeated[CurrentState.currentZone]) {
if (CurrentState.currentZone < 5) {
CurrentState.currentZone++;
CurrentState.playerPos = { x: 5, y: 5 }; // Restart positions
ZONE_DATABASES[CurrentState.currentZone].unlocked = true;
this.refresh();
triggerNarrativeVignette(
"ZONE SHIFT SUCCESS",
`Successfully ascended to Region ${CurrentState.currentZone + 1}: ${ZONE_DATABASES[CurrentState.currentZone].name}. ${ZONE_DATABASES[CurrentState.currentZone].storyNode}`,
null
);
} else {
// POST-GAME FREEROAM / VICTORY CONDITIONS
triggerNarrativeVignette(
"COSMOS INTEGRITY COUPLING",
"Perfect resonance accomplished! All regional anchors unchained! Post-game free roam unlocked (Harder high level wild spawns active). Enjoy exploring Aethera!",
null
);
}
} else {
triggerNarrativeVignette(
"ACCESS DECLINED",
`The transmission barrier is locked. Deconstruct Guardian ${ZONE_DATABASES[CurrentState.currentZone].guardianName} (#4 anchor) first to cross.`,
null
);
}
}
}
};
// ── function: triggerBattleTransition ──
// SYSTEM GLUE ACTIONS, WIRING TRANSITIONS, EVOLUTION & SHOP TRIGGER EVENTS
// Global state transition triggers
function triggerBattleTransition(type) {
// Save position, switch active viewport screen
document.getElementById("adventure-screen").classList.add("hidden");
document.getElementById("combat-screen").classList.remove("hidden");
// Reset context panels state-wise
hideAllCombatContextPanels();
if (type === "wild") {
ActiveCombat.initWildBattle(CurrentState.currentZone, CurrentState.team);
} else {
ActiveCombat.initGuardianBattle(CurrentState.currentZone, CurrentState.team);
}
// Bind active combat engine updates to DOM updates
ActiveCombat.setUpdateCallback(() => {
UIRenderer.refreshCombatScreen();
checkCombatDecisiveEnd();
});
UIRenderer.refreshCombatScreen();
AudioController.triggerBeep(660, 'sawtooth', 0.25);
}
// ── function: hideAllCombatContextPanels ──
function hideAllCombatContextPanels() {
document.getElementById("combat-abilities-panel").classList.add("hidden");
document.getElementById("combat-allies-panel").classList.add("hidden");
document.getElementById("combat-items-panel").classList.add("hidden");
document.getElementById("combat-main-actions").classList.remove("hidden");
ActiveCombat.selectedAction = null;
}
// ── function: triggerSelectCombatTarget ──
// BATTLE TURN EXECUTION CONTROL
function triggerSelectCombatTarget(abilityKey) {
// Store dynamic choice and alert player to select a target in the opponent row
ActiveCombat.selectedAction = { abilityKey: abilityKey };
ActiveCombat.currentLog = `⚡ Select target enemy battle pod to release energy flow!`;
UIRenderer.refreshCombatScreen();
}
// ── function: executeTriggeredCombatMove ──
function executeTriggeredCombatMove(enemyIdx) {
if (!ActiveCombat.selectedAction) return;
const abKey = ActiveCombat.selectedAction.abilityKey;
ActiveCombat.executeAbilityTurn(abKey, enemyIdx);
// Return to main options selection cards deck
hideAllCombatContextPanels();
}
// ── function: checkCombatDecisiveEnd ──
function checkCombatDecisiveEnd() {
// 1. Did all active allies crash?
const allAlliesDown = CurrentState.team.every(a => a.hp <= 0);
if (allAlliesDown) {
ActiveCombat.currentLog = `🚨 All active essences collapsed. Auto-teleporting to last shrine baseline...`;
UIRenderer.refreshCombatScreen();
setTimeout(() => {
exitCombatToField(false);
}, 2800);
return;
}
// 2. Did all opponents crash?
const allEnemiesDown = ActiveCombat.opponents.every(e => e.hp <= 0);
if (allEnemiesDown) {
const totalExpReward = Math.max(30, Math.floor(100 * (ActiveCombat.opponents[0].level / ActiveCombat.getActiveAlly().level)));
const fluxWon = Math.floor(25 * ActiveCombat.opponents.length + Math.random() * 20);
CurrentState.fluxWallet += fluxWon;
ActiveCombat.currentLog = `🏆 VICTORY! Acquired +${fluxWon} flux cores. Distributed +${totalExpReward} cosmic experience.`;
// Distribute level EXP bounds
CurrentState.team.forEach(ally => {
if (ally.hp > 0) {
ally.xp += totalExpReward;
if (ally.xp >= ally.maxXp) {
CurrentState.levelUpCreature(ally);
ActiveCombat.currentLog += `<br>⭐ ${ally.name} reached Depth Level: ${ally.level}!`;
AudioController.triggerCritNoise();
}
}
});
UIRenderer.refreshCombatScreen();
// Check if this was a Guardian victory
if (ActiveCombat.combatType === "guardian") {
CurrentState.anchorsDefeated[CurrentState.currentZone] = true;
}
setTimeout(() => {
exitCombatToField(true);
}, 3000);
}
}
// ── function: exitCombatToField ──
function exitCombatToField(victorySucess) {
document.getElementById("combat-screen").classList.add("hidden");
document.getElementById("adventure-screen").classList.remove("hidden");
if (!victorySucess) {
// Standard penalty - teleport back to safety start point
CurrentState.playerPos = { x: 5, y: 5 };
// Refill party health partially to avoid loop issues
CurrentState.team.forEach(ally => {
ally.hp = Math.max(ally.hp, Math.floor(ally.maxHp * 0.40));
});
}
OverworldController.refresh();
}
// ── function: triggerShrineTransition ──
// REST SANCTUARY & SHOP INTEGRATION
function triggerShrineTransition() {
document.getElementById("adventure-screen").classList.add("hidden");
document.getElementById("shrine-screen").classList.remove("hidden");
document.getElementById("shop-flux-count").textContent = CurrentState.fluxWallet;
document.getElementById("shop-drawer").classList.add("hidden");
}
// ── function: exitShrineToField ──
function exitShrineToField() {
document.getElementById("shrine-screen").classList.add("hidden");
document.getElementById("adventure-screen").classList.remove("remove");
document.getElementById("adventure-screen").classList.remove("hidden");
OverworldController.refresh();
}
// ── function: triggerSanctuaryHealAll ──
function triggerSanctuaryHealAll() {
CurrentState.team.forEach(ally => {
ally.hp = ally.maxHp;
ally.status = null;
ally.activeAbilities.forEach(ab => ab.pp = ab.maxPp);
});
AudioController.triggerCritNoise();
alert("✨ Celestial alignment completed! All creatures restored to peak state.");
}
// ── function: triggerShopExchange ──
function triggerShopExchange(itemKey) {
const item = INVENTORY_DB[itemKey];
if (!item) return;
if (CurrentState.fluxWallet >= item.cost) {
CurrentState.fluxWallet -= item.cost;
CurrentState.inventory[itemKey] = (CurrentState.inventory[itemKey] || 0) + 1;
document.getElementById("shop-flux-count").textContent = CurrentState.fluxWallet;
AudioController.triggerBeep(880, 'sine', 0.15);
alert(`Acquired 1x ${item.name}! Packed into Chrono-pack.`);
} else {
AudioController.triggerBeep(150, 'sawtooth', 0.2);
alert("Insufficient flux cores for currency exchange.");
}
}
// ── function: triggerItemCombatUse ──
// BATTLE ITEM INVENTORY ACTIONS
function triggerItemCombatUse(itemKey) {
const item = INVENTORY_DB[itemKey];
if (!item) return;
// Defer to capture rhythm mini-game or restore healing directly
if (item.type === 'capture') {
if (ActiveCombat.combatType === "guardian") {
ActiveCombat.currentLog = "⚠️ You cannot capture a Sentinel Anchor guardian!";
UIRenderer.refreshCombatScreen();
return;
}
// Spend item immediately
CurrentState.inventory[itemKey]--;
if (CurrentState.inventory[itemKey] <= 0) delete CurrentState.inventory[itemKey];
hideAllCombatContextPanels();
// Start Rhythm Harmonizer Ring sequence!
ActiveHarmonizer.start(itemKey, 0, (accuracy) => {
const target = ActiveCombat.opponents[0];
const remainingPct = target.hp / target.maxHp;
// Captured math success probability scales
const successChance = (1.0 - remainingPct) * item.rate * accuracy;
if (Math.random() < successChance || successChance > 0.8) {
// Success alignment capture!
const successAdd = CurrentState.addCreatureToTeam(target.modelId, target.level);
if (successAdd) {
ActiveCombat.currentLog = `🌌 HARMONIZED SUCCESSFULLY! ${target.name} joined your cosmic reservoir party.`;
} else {
ActiveCombat.currentLog = `🌌 HARMONIZED! (Reservoir storage packed, limited only to active team of 6).`;
}
UIRenderer.refreshCombatScreen();
setTimeout(() => exitCombatToField(true), 2800);
} else {
ActiveCombat.currentLog = `❌ Resonance failed to synchronize. ${target.name} escaped the capture field.`;
UIRenderer.refreshCombatScreen();
// Opponent replies instantly!
setTimeout(() => {
ActiveCombat.currentLog += "<br>" + ActiveCombat.processOpponentReply(target, ActiveCombat.getActiveAlly());
UIRenderer.refreshCombatScreen();
}, 1200);
}
});
} else if (item.type === 'heal') {
// Fast restore active ally HP
CurrentState.inventory[itemKey]--;
if (CurrentState.inventory[itemKey] <= 0) delete CurrentState.inventory[itemKey];
const activeAlly = ActiveCombat.getActiveAlly();
activeAlly.hp = Math.min(activeAlly.maxHp, activeAlly.hp + item.hp);
ActiveCombat.currentLog = `🧪 Used ${item.name}. Regenerated HP to active Node!`;
hideAllCombatContextPanels();
UIRenderer.refreshCombatScreen();
}
}
// ── function: triggerPartyEvolution ──
// PARTY MUTATIONS (EVOLUTIONS)
function triggerPartyEvolution(idx, itemKey) {
const ally = CurrentState.team[idx];
const res = CurrentState.triggerCatalystTransformation(ally, itemKey);
alert(res.msg);
UIRenderer.refreshCodexMenu();
UIRenderer.showTeamMemberDetail(idx);
}
// ── function: triggerOutOfBattleHeal ──
// OUT OF BATTLE RESTORE
function triggerOutOfBattleHeal(itemKey) {
const item = INVENTORY_DB[itemKey];
if (!item || item.type !== 'heal') return;
const targetAlly = CurrentState.team.find(a => a.hp < a.maxHp);
if (!targetAlly) {
alert("Total party is already at maximum structural physical density!");
return;
}
CurrentState.inventory[itemKey]--;
if (CurrentState.inventory[itemKey] <= 0) delete CurrentState.inventory[itemKey];
targetAlly.hp = Math.min(targetAlly.maxHp, targetAlly.hp + item.hp);
AudioController.triggerBeep(990, 'sine', 0.2);
UIRenderer.refreshCodexMenu();
alert(`Applied healing capsule successfully onto ${targetAlly.name}! HP restored.`);
}
// ── function: triggerNarrativeVignette ──
// FLOATING STORY DIALOG DISPLAY SYSTEM
function triggerNarrativeVignette(title, text, cb) {
const overlay = document.getElementById("narrative-vignette");
const titleNode = document.getElementById("narr-vignette-title");
const bodyNode = document.getElementById("narr-vignette-body");
const btn = document.getElementById("narr-vignette-btn");
if (overlay && titleNode && bodyNode) {
titleNode.textContent = title;
bodyNode.textContent = text;
overlay.classList.remove("hidden");
btn.onclick = () => {
overlay.classList.add("hidden");
if (cb) cb();
};
}
}
// ── main ──
// WIRE UP INTERFACES, BIND DOM ONCLICK EVENTS, EXECUTE SYSTEM ENGINE RUN!
// Title Screen Initializing and continue state loaders
const serializedSave = localStorage.getItem("aethera_save_v1");
const contBtn = document.getElementById("continue-btn");
if (serializedSave && contBtn) {
contBtn.disabled = false;
}
// Sound click listener
document.getElementById("mute-btn").onclick = () => {
const isMuted = AudioController.toggle();
document.getElementById("mute-btn").textContent = isMuted ? "🔇" : "🔊";
};
// Title action bindings
document.getElementById("new-game-btn").onclick = () => {
AudioController.playAmbientLoop();
// Start custom tutorial flow
document.getElementById("title-screen").classList.add("hidden");
document.getElementById("intro-modal").classList.remove("hidden");
// Render starter selection inside modal
const starterGrid = document.getElementById("starter-choice-container");
starterGrid.classList.remove("hidden");
starterGrid.innerHTML = `
<div class="starter-card selected" data-id="1">
<div class="starter-icon">🐢</div>
<div class="starter-name">Pyroshell</div>
<div class="starter-affinity">Pyre</div>
</div>
<div class="starter-card" data-id="2">
<div class="starter-icon">🐕</div>
<div class="starter-name">Sporepup</div>
<div class="starter-affinity">Bloom</div>
</div>
<div class="starter-card" data-id="3">
<div class="starter-icon">🐬</div>
<div class="starter-name">Finlet</div>
<div class="starter-affinity">Hydro</div>
</div>
`;
document.getElementById("intro-body").innerHTML = `
Welcome Cosmic Resonator. Biological constructs known as the Resonated exist across the fractured domains of Aethera.<br><br>
Choose your starting cosmic partner signature to begin balancing spatial anchors!
`;
// Bind starter clicks
let selectedId = 1;
const cards = document.querySelectorAll(".starter-card");
cards.forEach(card => {
card.onclick = () => {
cards.forEach(c => c.classList.remove("selected"));
card.classList.add("selected");
selectedId = parseInt(card.getAttribute("data-id"));
};
});
// Finish tutorial button
document.getElementById("intro-next-btn").onclick = () => {
// Add selected starter
CurrentState.addCreatureToTeam(selectedId, 6);
document.getElementById("intro-modal").classList.add("hidden");
// Transition directly to field adventure
document.getElementById("adventure-screen").classList.remove("hidden");
// Boot canvas engine
const world = new OverworldEngine("overworld-canvas");
OverworldController.init(world);
// Initial narrative dialogue guidance
triggerNarrativeVignette(
"ZONE MISSION ACTIVATED",
ZONE_DATABASES[0].storyNode,
null
);
};
};
document.getElementById("continue-btn").onclick = () => {
const save = localStorage.getItem("aethera_save_v1");
if (save) {
CurrentState.deserialize(save);
AudioController.playAmbientLoop();
document.getElementById("title-screen").classList.add("hidden");
document.getElementById("adventure-screen").classList.remove("hidden");
const world = new OverworldEngine("overworld-canvas");
OverworldController.init(world);
}
};
// Keyboard listener for overworld navigation
window.onkeydown = (e) => {
// Only receive moves when adventure overworld is fully visible and menu is hidden
const screen = document.getElementById("adventure-screen");
if (screen && !screen.classList.contains("hidden")) {
const codex = document.getElementById("menu-overlay");
if (codex && codex.classList.contains("hidden")) {
if (e.key === "KeyW" || e.key === "ArrowUp") OverworldController.tryMove(0, -1);
if (e.key === "KeyS" || e.key === "ArrowDown") OverworldController.tryMove(0, 1);
if (e.key === "KeyA" || e.key === "ArrowLeft") OverworldController.tryMove(-1, 0);
if (e.key === "KeyD" || e.key === "ArrowRight") OverworldController.tryMove(1, 0);
}
}
};
// On-screen field pad arrow buttons mapping
document.getElementById("pad-up").onclick = () => OverworldController.tryMove(0, -1);
document.getElementById("pad-down").onclick = () => OverworldController.tryMove(0, 1);
document.getElementById("pad-left").onclick = () => OverworldController.tryMove(-1, 0);
document.getElementById("pad-right").onclick = () => OverworldController.tryMove(1, 0);
// Codex Menu Overlay Button bindings
document.getElementById("menu-btn").onclick = () => {
document.getElementById("menu-overlay").classList.remove("hidden");
UIRenderer.refreshCodexMenu();
};
document.getElementById("codex-close").onclick = () => {
document.getElementById("menu-overlay").classList.add("hidden");
};
// Menu overlay tab buttons
const tabBtns = document.querySelectorAll(".codex-tabs .tab-btn:not(#codex-close)");
tabBtns.forEach(btn => {
btn.onclick = () => {
tabBtns.forEach(b => b.classList.remove("active"));
btn.classList.add("active");
const targetTab = btn.getAttribute("data-tab");
const panels = document.querySelectorAll(".codex-content .tab-panel");
panels.forEach(p => p.classList.remove("active"));
const targetPanel = document.getElementById(targetTab);
if (targetPanel) {
targetPanel.classList.add("active");
}
};
});
// COMBAT ACTION CONTROLLER BUTTONS WIRE-UPS
document.getElementById("act-attack-btn").onclick = () => {
document.getElementById("combat-main-actions").classList.add("hidden");
document.getElementById("combat-abilities-panel").classList.remove("hidden");
};
document.getElementById("combat-abilities-back").onclick = () => {
hideAllCombatContextPanels();
UIRenderer.refreshCombatScreen();
};
document.getElementById("act-switch-btn").onclick = () => {
document.getElementById("combat-main-actions").classList.add("hidden");
const panel = document.getElementById("combat-allies-panel");
panel.classList.remove("hidden");
// Load switch list grid of available reserves
const grid = document.getElementById("switch-allies-grid");
grid.innerHTML = "";
CurrentState.team.forEach((ally, idx) => {
const isCurrentActive = idx === ActiveCombat.activeAllyIdx;
const card = document.createElement("button");
card.className = "combat-act-btn";
card.disabled = ally.hp <= 0 || isCurrentActive;
card.style.minWidth = "160px";
card.innerHTML = `
<div class="btn-title-row">
<span>${ally.avatar} ${ally.name}</span>
</div>
<div>
<div style="font-size:10px;">Level: ${ally.level}</div>
<div style="font-size:10px; color:var(--text-muted);">HP: ${ally.hp}/${ally.maxHp}</div>
</div>
`;
card.onclick = () => {
ActiveCombat.switchActiveAlly(idx);
hideAllCombatContextPanels();
};
grid.appendChild(card);
});
};
document.getElementById("combat-switch-back").onclick = () => {
hideAllCombatContextPanels();
};
document.getElementById("act-collect-btn").onclick = () => {
// Use first available Harmonizer item instantly
const captureItemKey = Object.keys(CurrentState.inventory).find(k => INVENTORY_DB[k].type === 'capture');
if (captureItemKey) {
triggerItemCombatUse(captureItemKey);
} else {
alert("You have no Harmonizer tuning cores in inventory reservoir! Go buy some at regional Shrines.");
}
};
document.getElementById("act-inventory-btn").onclick = () => {
document.getElementById("combat-main-actions").classList.add("hidden");
const panel = document.getElementById("combat-items-panel");
panel.classList.remove("hidden");
const grid = document.getElementById("combat-items-grid");
grid.innerHTML = "";
Object.keys(CurrentState.inventory).forEach(itemKey => {
const item = INVENTORY_DB[itemKey];
const qty = CurrentState.inventory[itemKey];
if (qty <= 0) return;
const btn = document.createElement("button");
btn.className = "combat-act-btn";
btn.innerHTML = `
<div class="btn-title-row">
<span>${item.dName}</span>
<span style="color:var(--accent-gold);">x${qty}</span>
</div>
<div style="font-size:9px; color:var(--text-muted);">${item.desc}</div>
`;
btn.onclick = () => {
triggerItemCombatUse(itemKey);
};
grid.appendChild(btn);
});
};
document.getElementById("combat-items-back").onclick = () => {
hideAllCombatContextPanels();
};
document.getElementById("harmonize-tap-btn").onclick = () => {
ActiveHarmonizer.triggerTap();
};
// SHRINE CONTROL DECK
document.getElementById("shrine-heal").onclick = () => {
triggerSanctuaryHealAll();
};
document.getElementById("shrine-save").onclick = () => {
localStorage.setItem("aethera_save_v1", CurrentState.serialize());
alert("💾 Spatial index progression successfully synchronized to local continuum state!");
};
document.getElementById("shrine-shop").onclick = () => {
const drawer = document.getElementById("shop-drawer");
drawer.classList.toggle("hidden");
// Populate Shop Items
const grid = document.getElementById("shop-items-grid");
grid.innerHTML = "";
Object.keys(INVENTORY_DB).forEach(key => {
const item = INVENTORY_DB[key];
const card = document.createElement("div");
card.className = "shop-card";
card.innerHTML = `
<div class="shop-card-info">
<span class="shop-item-name">${item.dName}</span>
<span class="shop-item-cost">💸 ${item.cost} flux</span>
</div>
<div>🛒</div>
`;
card.onclick = () => triggerShopExchange(key);
grid.appendChild(card);
});
};
document.getElementById("shrine-exit").onclick = () => {
exitShrineToField();
};
// Start ambient particles background loop execution
setInterval(() => {
const adventure = document.getElementById("adventure-screen");
if (adventure && !adventure.classList.contains("hidden")) {
OverworldController.refresh();
}
}, 50);
</script>
</body>
</html>
<!-- agent-meta {"model":"google/gemini-3.5-flash","provider":"openrouter","persona":"composer","sessionId":"a63724ef-3698-4a8e-a49b-35d0373eb2cf","tokensIn":648127,"tokensOut":41752,"tokensTotal":689879,"turns":27,"toolCalls":26,"failedToolCalls":0,"timestamp":"2026-07-14T05:11:32.278Z"} -->