← Creature Collection results

Creature Collection

Qwen 3.6 27b MTP · composer

Initial view of Creature Collection

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Imported historical artifact. Its exact original instructions and lineage are not known.

Generation prompt · exact text unavailable

See the current prompt for reference. It may differ from the instructions used to generate this artifact.

Inspect original source 123,984 bytes · SHA-256 74ca27f988ee
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Aetherbound — Weaver's Chronicle</title>
<style>
/* ── css: layout ── */
:root {
  --bg-deep: #0a0e1a;
  --bg-mid: #141b2d;
  --glass-bg: rgba(20, 27, 45, 0.85);
  --glass-border: rgba(100, 140, 255, 0.15);
  --glass-highlight: rgba(160, 200, 255, 0.08);
  --text-primary: #e8edf5;
  --text-secondary: #9ba8c4;
  --accent-blue: #5b8def;
  --accent-gold: #f0c040;
  --accent-red: #ef5b5b;
  --accent-green: #5bef8d;
  --type-ember: #ff6b3a;
  --type-frost: #5bcfff;
  --type-stone: #a09070;
  --type-spark: #ffe040;
  --type-wave: #40c8e0;
  --type-prism: #f0d0ff;
  --type-void: #8060b0;
}

* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: var(--bg-deep); font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; color: var(--text-primary); user-select: none; -webkit-user-select: none; }

#gameCanvas { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 0; }
.screen { position: fixed; top: 0; left: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; z-index: 10; pointer-events: none; opacity: 0; transition: opacity 0.3s ease; }
.screen.active { opacity: 1; pointer-events: auto; }
.hidden { display: none !important; }

.glass-panel { background: var(--glass-bg); border: 1px solid var(--glass-border); border-radius: 16px; padding: 24px; backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); box-shadow: 0 8px 32px rgba(0,0,0,0.4), inset 0 1px 0 var(--glass-highlight); max-width: 560px; width: 90%; }
.glass-btn { background: rgba(91, 141, 239, 0.15); border: 1px solid rgba(91, 141, 239, 0.3); color: var(--text-primary); padding: 10px 20px; border-radius: 8px; cursor: pointer; font-size: 14px; transition: all 0.2s ease; backdrop-filter: blur(4px); }
.glass-btn:hover { background: rgba(91, 141, 239, 0.3); border-color: rgba(91, 141, 239, 0.5); transform: translateY(-1px); }
.glass-btn.primary { background: rgba(91, 141, 239, 0.3); border-color: var(--accent-blue); font-weight: 600; }
.glass-btn.secondary { background: rgba(255,255,255,0.08); border-color: rgba(255,255,255,0.15); }
.glass-btn.tertiary { background: rgba(239, 91, 91, 0.15); border-color: rgba(239, 91, 91, 0.3); }
.glass-btn.small { padding: 6px 14px; font-size: 12px; }
.glass-btn:active { transform: scale(0.97); }

.menu-container { max-height: 85vh; display: flex; flex-direction: column; }
.scrollable .list-grid { overflow-y: auto; flex: 1; padding-right: 4px; }
.list-grid::-webkit-scrollbar { width: 4px; }
.list-grid::-webkit-scrollbar-thumb { background: rgba(91,141,239,0.3); border-radius: 2px; }

.menu-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 8px; margin: 16px 0; }
.settings-grid { display: flex; flex-direction: column; gap: 8px; margin: 16px 0; }
.menu-item { text-align: center; padding: 14px; font-size: 15px; }

h2 { font-size: 20px; margin-bottom: 12px; color: var(--accent-blue); letter-spacing: 1px; }
h3 { font-size: 16px; margin-bottom: 8px; }
/* ── css: title-screen ── */
.title-content { text-align: center; }
.game-title { font-size: clamp(36px, 8vw, 72px); letter-spacing: 12px; background: linear-gradient(135deg, #5b8def, #a0c4ff, #f0d0ff, #5bef8d); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; animation: titleGlow 4s ease-in-out infinite alternate; margin-bottom: 8px; }
@keyframes titleGlow { 0% { filter: brightness(1) drop-shadow(0 0 20px rgba(91,141,239,0.3)); } 100% { filter: brightness(1.2) drop-shadow(0 0 40px rgba(160,196,255,0.5)); } }
.subtitle { font-size: clamp(14px, 3vw, 22px); color: var(--text-secondary); letter-spacing: 6px; margin-bottom: 40px; }
.title-buttons { display: flex; gap: 16px; justify-content: center; margin-bottom: 30px; }
.title-credits { font-size: 12px; color: var(--text-secondary); opacity: 0.6; font-style: italic; }

/* HUD */
.hud-zone { position: fixed; top: 12px; left: 50%; transform: translateX(-50%); background: var(--glass-bg); padding: 6px 20px; border-radius: 20px; font-size: 13px; color: var(--accent-blue); letter-spacing: 2px; z-index: 5; backdrop-filter: blur(8px); border: 1px solid var(--glass-border); }
.hud-player { position: fixed; top: 12px; right: 16px; display: flex; gap: 12px; font-size: 13px; z-index: 5; }
.hud-player span { background: var(--glass-bg); padding: 4px 12px; border-radius: 12px; backdrop-filter: blur(8px); border: 1px solid var(--glass-border); }

/* Touch Controls */
.touch-controls { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); display: grid; grid-template-columns: repeat(3, 56px); grid-template-rows: repeat(2, 56px); gap: 4px; z-index: 15; }
.touch-btn { width: 56px; height: 56px; border-radius: 12px; background: rgba(91,141,239,0.2); border: 1px solid rgba(91,141,239,0.3); color: var(--text-primary); font-size: 20px; cursor: pointer; display: flex; align-items: center; justify-content: center; }
.touch-btn.action { grid-column: 3; grid-row: 2; background: rgba(240,192,64,0.25); border-color: var(--accent-gold); }

/* Battle UI */
.battle-ui { position: fixed; bottom: 0; left: 0; right: 0; z-index: 20; pointer-events: none; }
.enemy-area { padding: 16px; text-align: center; min-height: 80px; }
.party-area { display: flex; gap: 8px; padding: 8px 16px; overflow-x: auto; }

.creature-card { background: var(--glass-bg); border: 1px solid var(--glass-border); border-radius: 12px; padding: 8px 12px; min-width: 140px; backdrop-filter: blur(8px); cursor: pointer; transition: all 0.2s; }
.creature-card:hover { border-color: var(--accent-blue); }
.creature-card.active-turn { border-color: var(--accent-gold); box-shadow: 0 0 12px rgba(240,192,64,0.3); }
.creature-card.fainted { opacity: 0.4; filter: grayscale(1); }
.creature-name { font-size: 13px; font-weight: 600; margin-bottom: 4px; display: flex; justify-content: space-between; }
.hp-bar-bg { width: 100%; height: 6px; background: rgba(255,255,255,0.1); border-radius: 3px; overflow: hidden; margin-bottom: 4px; }
.hp-bar-fill { height: 100%; border-radius: 3px; transition: width 0.5s ease; background: var(--accent-green); }
.hp-bar-fill.low { background: var(--accent-red); }
.hp-bar-fill.mid { background: var(--accent-gold); }
.creature-stats { font-size: 11px; color: var(--text-secondary); display: flex; justify-content: space-between; }

.battle-menu { pointer-events: auto; padding: 8px 16px 16px; }
.menu-options { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; margin-bottom: 8px; }
.ability-list { display: grid; grid-template-columns: repeat(2, 1fr); gap: 6px; max-height: 160px; overflow-y: auto; }
.ability-btn { background: rgba(91,141,239,0.1); border: 1px solid rgba(91,141,239,0.2); color: var(--text-primary); padding: 8px; border-radius: 6px; cursor: pointer; font-size: 12px; text-align: left; transition: all 0.15s; }
.ability-btn:hover { background: rgba(91,141,239,0.25); }
.ability-btn .ab-name { font-weight: 600; display: block; }
.ability-btn .ab-info { color: var(--text-secondary); font-size: 10px; }

.battle-log { padding: 4px 16px; font-size: 12px; color: var(--text-secondary); min-height: 24px; text-align: center; }

/* Capture */
.capture-container { text-align: center; }
.capture-bar { width: 100%; height: 32px; background: rgba(255,255,255,0.08); border-radius: 16px; position: relative; margin: 16px 0; overflow: hidden; }
.capture-zone { position: absolute; top: 0; height: 100%; background: rgba(91,239,141,0.3); border-radius: 16px; transition: all 0.3s; }
.capture-indicator { position: absolute; top: -2px; width: 8px; height: 36px; background: var(--accent-gold); border-radius: 4px; box-shadow: 0 0 8px rgba(240,192,64,0.5); }

/* Dialogue */
.dialogue-container { text-align: center; min-width: 300px; }
#dialogueText { font-size: 15px; line-height: 1.6; margin-bottom: 16px; }

/* Story Vignette */
.vignette-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: -1; }
.vignette-content { max-width: 500px; text-align: center; }
#vignetteTitle { color: var(--accent-gold); margin-bottom: 12px; font-size: 24px; letter-spacing: 3px; }
#vignetteText { line-height: 1.7; margin-bottom: 20px; font-size: 15px; }

/* Notification */
.notification { position: fixed; top: 60px; left: 50%; transform: translateX(-50%); background: var(--glass-bg); border: 1px solid var(--accent-gold); padding: 8px 24px; border-radius: 20px; font-size: 13px; z-index: 100; backdrop-filter: blur(12px); animation: notifSlide 0.3s ease; }
@keyframes notifSlide { from { opacity: 0; transform: translateX(-50%) translateY(-10px); } to { opacity: 1; transform: translateX(-50%) translateY(0); } }

/* Shrine */
.shrine-options { display: flex; flex-direction: column; gap: 8px; margin-top: 16px; }

/* Codex/Team items */
.codex-item, .team-item, .bag-item { background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; padding: 10px 14px; margin-bottom: 6px; cursor: pointer; transition: all 0.15s; display: flex; justify-content: space-between; align-items: center; }
.codex-item:hover, .team-item:hover, .bag-item:hover { background: rgba(91,141,239,0.1); border-color: var(--accent-blue); }

/* Type badges */
.type-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 10px; font-weight: 600; letter-spacing: 1px; text-transform: uppercase; }
.type-ember { background: rgba(255,107,58,0.3); color: var(--type-ember); }
.type-frost { background: rgba(91,207,255,0.3); color: var(--type-frost); }
.type-stone { background: rgba(160,144,112,0.3); color: var(--type-stone); }
.type-spark { background: rgba(255,224,64,0.3); color: var(--type-spark); }
.type-wave { background: rgba(64,200,224,0.3); color: var(--type-wave); }
.type-prism { background: rgba(240,208,255,0.3); color: var(--type-prism); }
.type-void { background: rgba(128,96,176,0.3); color: var(--type-void); }

/* Animations */
@keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
@keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.05); } }
@keyframes shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-4px); } 75% { transform: translateX(4px); } }

/* Map */
#mapCanvas { border-radius: 8px; margin: 12px 0; max-width: 100%; }

/* Responsive */
@media (max-width: 600px) {
  .menu-grid { grid-template-columns: repeat(2, 1fr); }
  .ability-list { grid-template-columns: 1fr; }
  .glass-panel { padding: 16px; width: 95%; }
}
/* ── css: polish ── */
/* Enhanced creature cards */
.creature-card { position: relative; overflow: hidden; }
.creature-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px; background: linear-gradient(90deg, transparent, var(--accent-blue), transparent); opacity: 0.5; }

/* Animated HP bars */
.hp-bar-fill { transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1); }
.hp-bar-fill.low { animation: hpPulse 1s ease-in-out infinite; }
@keyframes hpPulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } }

/* Battle menu glow */
.battle-menu.active-turn::after { content: ''; position: absolute; bottom: 0; left: 0; right: 0; height: 2px; background: var(--accent-gold); animation: menuGlow 1s ease-in-out infinite alternate; }
@keyframes menuGlow { from { opacity: 0.3; } to { opacity: 1; } }

/* Ability button type colors */
.ability-btn[data-type="ember"] { border-left: 3px solid var(--type-ember); }
.ability-btn[data-type="frost"] { border-left: 3px solid var(--type-frost); }
.ability-btn[data-type="stone"] { border-left: 3px solid var(--type-stone); }
.ability-btn[data-type="spark"] { border-left: 3px solid var(--type-spark); }
.ability-btn[data-type="wave"] { border-left: 3px solid var(--type-wave); }
.ability-btn[data-type="prism"] { border-left: 3px solid var(--type-prism); }
.ability-btn[data-type="void"] { border-left: 3px solid var(--type-void); }

/* Title screen ambient animation */
.title-content { position: relative; z-index: 2; }
.game-title::after { content: ''; position: absolute; bottom: -10px; left: 50%; transform: translateX(-50%); width: 60%; height: 1px; background: linear-gradient(90deg, transparent, var(--accent-blue), transparent); }

/* Scrollbar styling */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: rgba(255,255,255,0.03); border-radius: 3px; }
::-webkit-scrollbar-thumb { background: rgba(91,141,239,0.3); border-radius: 3px; }

/* Notification animation */
.notification { transition: opacity 0.3s ease, transform 0.3s ease; }
.notification.hidden { opacity: 0; transform: translateX(-50%) translateY(-10px); pointer-events: none; }

/* Creature detail table styling */
#detailContent table td { padding: 4px 8px; border-bottom: 1px solid rgba(255,255,255,0.05); }
#detailContent table tr:first-child td { font-weight: 600; color: var(--accent-blue); }

/* Map zone hover */
.map-zone { transition: all 0.2s ease; cursor: pointer; }
/* ── css: layout-fixes ── */
/* Fix list-grid rendering */
.list-grid { min-height: 0; }
.scrollable .list-grid { overflow-y: auto; flex: 1; padding-right: 4px; }

/* Ensure codex/team/bag items have minimum height */
.codex-item, .team-item, .bag-item { min-height: 36px; display: flex; align-items: center; }

/* Fix menu container sizing - ensure content renders */
.menu-container { max-width: 560px; width: 90%; max-height: 85vh; display: flex; flex-direction: column; min-height: 200px; }

/* Ensure glass-panel has proper dimensions when used as menu container */
.glass-panel { min-height: auto; }

/* Fix starter screen specifically - ensure items render */
#starterScreen .glass-panel h2 { margin-bottom: 8px; font-size: 18px; color: var(--accent-blue); }
#starterScreen p { margin-bottom: 12px; }
#starterList { display: flex; flex-direction: column; gap: 8px; }

/* Override backdrop-filter for file:// compatibility */
.glass-panel { -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px); }
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div id="titleScreen" class="screen active">
  <div class="title-content">
    <h1 class="game-title">AETHERBOUND</h1>
    <p class="subtitle">Weaver's Chronicle</p>
    <div class="title-buttons">
      <button id="btnNewGame" class="glass-btn primary">New Binding</button>
      <button id="btnContinue" class="glass-btn secondary hidden">Continue</button>
    </div>
    <div class="title-credits">A creature of the Shattered Expanse awaits.</div>
  </div>
</div><div id="starterScreen" class="screen hidden"><div class="menu-container glass-panel"><h2>Choose Your Luminary</h2><p style="color:var(--text-secondary);margin-bottom:16px;">Select the companion that resonates with your spirit.</p><div id="starterList" class="list-grid"></div></div></div>
<div id="overworldHUD" class="screen hidden">
  <div id="zoneName" class="hud-zone"></div>
  <div id="playerInfo" class="hud-player"><span id="goldDisplay">✦ 0</span><span id="shardCount">◇ 0</span></div>
  <div id="touchControls" class="touch-controls hidden">
    <button data-dir="up" class="touch-btn">▲</button>
    <button data-dir="down" class="touch-btn">▼</button>
    <button data-dir="left" class="touch-btn">◄</button>
    <button data-dir="right" class="touch-btn">►</button>
    <button id="actionBtn" class="touch-btn action">⚡</button>
  </div>
</div>
<div id="battleScreen" class="screen hidden"><canvas id="battleCanvas"></canvas><div id="battleUI" class="battle-ui"><div id="enemyArea" class="enemy-area"></div><div id="partyArea" class="party-area"></div><div id="battleMenu" class="battle-menu hidden"><div id="menuOptions" class="menu-options"></div><div id="abilityList" class="ability-list hidden"></div></div><div id="battleLog" class="battle-log"></div></div></div>
<div id="captureScreen" class="screen hidden"><div class="capture-container glass-panel"><h3>Capture Attempt</h3><p id="captureTargetName"></p><div id="captureBar" class="capture-bar"><div id="captureZone" class="capture-zone"></div><div id="captureIndicator" class="capture-indicator"></div></div><p id="captureInstruction">Tap when the indicator is in the green zone!</p><div id="captureAttempts" class="capture-attempts"></div></div></div>
<div id="dialogueBox" class="screen hidden"><div class="dialogue-container glass-panel"><p id="dialogueText"></p><button id="dialogueNext" class="glass-btn small">Continue</button></div></div>
<div id="storyVignette" class="screen hidden"><div class="vignette-overlay"></div><div class="vignette-content glass-panel"><h2 id="vignetteTitle"></h2><p id="vignetteText"></p><button id="vignetteNext" class="glass-btn primary">Continue</button></div></div>
<div id="menuOverlay" class="screen hidden"><div class="menu-container glass-panel"><h2>Main Menu</h2><div class="menu-grid"><button data-menu="team" class="glass-btn menu-item">Team</button><button data-menu="bag" class="glass-btn menu-item">Bag</button><button data-menu="encyclopedia" class="glass-btn menu-item">Codex</button><button data-menu="map" class="glass-btn menu-item">Map</button><button data-menu="settings" class="glass-btn menu-item">Settings</button></div><button id="menuClose" class="glass-btn secondary close">Back</button></div></div>
<div id="teamScreen" class="screen hidden"><div class="menu-container glass-panel scrollable"><h2>Your Luminaries</h2><div id="teamList" class="list-grid"></div><button data-back="true" class="glass-btn secondary close">Back</button></div></div>
<div id="bagScreen" class="screen hidden"><div class="menu-container glass-panel scrollable"><h2>Inventory</h2><div id="bagList" class="list-grid"></div><button data-back="true" class="glass-btn secondary close">Back</button></div></div>
<div id="codexScreen" class="screen hidden"><div class="menu-container glass-panel scrollable"><h2>Luminary Codex</h2><div id="codexList" class="list-grid"></div><button data-back="true" class="glass-btn secondary close">Back</button></div></div>
<div id="mapScreen" class="screen hidden"><div class="menu-container glass-panel"><h2>Zone Map</h2><canvas id="mapCanvas"></canvas><button data-back="true" class="glass-btn secondary close">Back</button></div></div>
<div id="settingsScreen" class="screen hidden"><div class="menu-container glass-panel"><h2>Settings</h2><div class="settings-grid"><button id="muteBtn" class="glass-btn menu-item">🔊 Sound: On</button><button id="saveBtn" class="glass-btn menu-item">💾 Save Game</button><button id="loadBtn" class="glass-btn menu-item">📂 Load Game</button></div><button data-back="true" class="glass-btn secondary close">Back</button></div></div>
<div id="shrineScreen" class="screen hidden"><div class="menu-container glass-panel"><h2>✦ Aether Shrine ✦</h2><p>Rest and restore your Luminaries.</p><div class="shrine-options"><button id="shrineHeal" class="glass-btn primary">Restore Party (Free)</button><button id="shrineShop" class="glass-btn secondary">Supply Cache</button><button id="shrineLeave" class="glass-btn tertiary">Depart</button></div></div></div>
<div id="shopScreen" class="screen hidden"><div class="menu-container glass-panel scrollable"><h2>Supply Cache</h2><p id="shopGold">✦ Gold: 0</p><div id="shopList" class="list-grid"></div><button data-back="true" class="glass-btn secondary close">Leave</button></div></div>
<div id="creatureDetail" class="screen hidden"><div class="menu-container glass-panel scrollable"><h2 id="detailName"></h2><div id="detailContent"></div><button data-back="true" class="glass-btn secondary close">Back</button></div></div>
<div id="victoryScreen" class="screen hidden"><div class="menu-container glass-panel"><h2 id="victoryTitle"></h2><p id="victoryText"></p><button id="victoryContinue" class="glass-btn primary">Continue</button></div></div>
<div id="tutorialOverlay" class="screen hidden"><div class="menu-container glass-panel"><h2 id="tutorialTitle"></h2><p id="tutorialText"></p><button id="tutorialNext" class="glass-btn primary">Continue</button></div></div>
<div id="notification" class="notification hidden"></div>
<script>
'use strict';

// ── const: TYPES ──
const TYPES = ['ember', 'frost', 'stone', 'spark', 'wave', 'prism', 'void'];

// ── const: TYPE_COLORS ──
const TYPE_COLORS = { ember:'#ff6b3a', frost:'#5bcfff', stone:'#a09070', spark:'#ffe040', wave:'#40c8e0', prism:'#f0d0ff', void:'#8060b0' };

// ── const: TYPE_CHART ──
// Type effectiveness: attacker -> { defender: multiplier }
const TYPE_CHART = {
  ember:  { frost:2, stone:2, spark:0.5, wave:0.5, prism:1, void:1 },
  frost:  { stone:2, spark:2, ember:0.5, wave:0.5, prism:1, void:1 },
  stone:  { ember:2, wave:2, frost:0.5, spark:0.5, prism:1, void:1 },
  spark:  { ember:2, stone:2, wave:0.5, prism:1, void:1, frost:1 },
  wave:   { spark:2, frost:2, stone:0.5, prism:1, void:1, ember:1 },
  prism:  { ember:1.3, frost:1.3, stone:1.3, spark:1.3, wave:1.3, void:2, prism:1 },
  void:   { ember:0.5, frost:0.5, stone:0.5, spark:0.5, wave:0.5, prism:0.5, void:1 }
};

// ── function: getTypeMultiplier ──
function getTypeMultiplier(atkType, defType) {
  if (atkType === defType) return 1;
  const row = TYPE_CHART[atkType];
  return row ? (row[defType] !== undefined ? row[defType] : 1) : 1;
}

// ── const: STATUS_EFFECTS ──
const STATUS_EFFECTS = ['burn', 'frost', 'shock', 'barrier', 'regen', 'none'];

// ── const: MAX_LEVEL ──
const MAX_LEVEL = 50;

// ── const: PARTY_SIZE ──
const PARTY_SIZE = 6;

// ── const: ABILITIES ──
const ABILITIES = [
  // Ember abilities (8)
  { id:'flame_burst', name:'Flame Burst', type:'ember', power:40, acc:100, pp:25, effect:'burn', effectChance:0.2, desc:'A burst of flame. May burn.' },
  { id:'ember_claw', name:'Ember Claw', type:'ember', power:50, acc:95, pp:20, effect:'none', effectChance:0, desc:'Scorching claw strike.' },
  { id:'scorch_wave', name:'Scorch Wave', type:'ember', power:60, acc:85, pp:15, effect:'burn', effectChance:0.3, desc:'Wave of heat. Burns often.' },
  { id:'fire_dance', name:'Fire Dance', type:'ember', power:45, acc:100, pp:20, hits:2, effect:'none', effectChance:0, desc:'Two rapid flame strikes.' },
  { id:'blaze_storm', name:'Blaze Storm', type:'ember', power:80, acc:75, pp:10, effect:'burn', effectChance:0.4, desc:'Devastating firestorm.' },
  { id:'inferno_pulse', name:'Inferno Pulse', type:'ember', power:90, acc:80, pp:8, effect:'burn', effectChance:0.5, desc:'Massive pulse of flame.' },
  { id:'cinder_rain', name:'Cinder Rain', type:'ember', power:35, acc:100, pp:30, hits:3, effect:'none', effectChance:0, desc:'Three cinder strikes.' },
  { id:'pyre_fang', name:'Pyre Fang', type:'ember', power:70, acc:90, pp:12, effect:'burn', effectChance:0.25, desc:'Biting flame attack.' },
  // Frost abilities (8)
  { id:'ice_shard', name:'Ice Shard', type:'frost', power:40, acc:100, pp:25, effect:'frost', effectChance:0.2, desc:'Sharp ice projectile.' },
  { id:'frost_bite', name:'Frost Bite', type:'frost', power:50, acc:95, pp:20, effect:'none', effectChance:0, desc:'Freezing bite attack.' },
  { id:'blizzard_sweep', name:'Blizzard Sweep', type:'frost', power:60, acc:85, pp:15, effect:'frost', effectChance:0.3, desc:'Sweeping blizzard.' },
  { id:'glacier_crash', name:'Glacier Crash', type:'frost', power:80, acc:75, pp:10, effect:'frost', effectChance:0.4, desc:'Crushing ice avalanche.' },
  { id:'snow_veil', name:'Snow Veil', type:'frost', power:0, acc:100, pp:20, effect:'barrier', effectChance:1, desc:'Shield of frozen mist.' },
  { id:'rime_touch', name:'Rime Touch', type:'frost', power:35, acc:100, pp:30, hits:2, effect:'frost', effectChance:0.15, desc:'Double frost touch.' },
  { id:'crystal_lance', name:'Crystal Lance', type:'frost', power:70, acc:90, pp:12, effect:'none', effectChance:0, desc:'Piercing ice lance.' },
  { id:'permafrost', name:'Permafrost', type:'frost', power:90, acc:80, pp:8, effect:'frost', effectChance:0.5, desc:'Deep freeze devastation.' },
  // Stone abilities (8)
  { id:'rock_throw', name:'Rock Throw', type:'stone', power:40, acc:100, pp:25, effect:'none', effectChance:0, desc:'Hurl a stone.' },
  { id:'earth_slam', name:'Earth Slam', type:'stone', power:60, acc:90, pp:15, effect:'none', effectChance:0, desc:'Slam the ground hard.' },
  { id:'stone_wall', name:'Stone Wall', type:'stone', power:0, acc:100, pp:20, effect:'barrier', effectChance:1, desc:'Raise a stone barrier.' },
  { id:'boulder_crush', name:'Boulder Crush', type:'stone', power:80, acc:75, pp:10, effect:'none', effectChance:0, desc:'Crushing boulder drop.' },
  { id:'tectonic_shift', name:'Tectonic Shift', type:'stone', power:90, acc:80, pp:8, effect:'shock', effectChance:0.3, desc:'Shake the earth violently.' },
  { id:'pebble_barrage', name:'Pebble Barrage', type:'stone', power:25, acc:100, pp:30, hits:4, effect:'none', effectChance:0, desc:'Four pebble strikes.' },
  { id:'granite_shield', name:'Granite Shield', type:'stone', power:0, acc:100, pp:25, effect:'barrier', effectChance:1, desc:'Hard granite defense.' },
  { id:'quake_pulse', name:'Quake Pulse', type:'stone', power:70, acc:85, pp:12, effect:'shock', effectChance:0.2, desc:'Seismic pulse attack.' },
  // Spark abilities (8)
  { id:'shock_bolt', name:'Shock Bolt', type:'spark', power:40, acc:100, pp:25, effect:'shock', effectChance:0.2, desc:'Electric bolt strike.' },
  { id:'thunder_strike', name:'Thunder Strike', type:'spark', power:60, acc:90, pp:15, effect:'shock', effectChance:0.3, desc:'Lightning from above.' },
  { id:'lightning_arc', name:'Lightning Arc', type:'spark', power:50, acc:95, pp:20, effect:'none', effectChance:0, desc:'Arcing lightning chain.' },
  { id:'storm_surge', name:'Storm Surge', type:'spark', power:80, acc:75, pp:10, effect:'shock', effectChance:0.4, desc:'Massive electrical surge.' },
  { id:'voltage_drain', name:'Voltage Drain', type:'spark', power:30, acc:100, pp:20, effect:'regen', effectChance:1, desc:'Drain energy to heal self.' },
  { id:'electric_fang', name:'Electric Fang', type:'spark', power:70, acc:90, pp:12, effect:'shock', effectChance:0.25, desc:'Charged bite attack.' },
  { id:'plasma_burst', name:'Plasma Burst', type:'spark', power:90, acc:80, pp:8, effect:'burn', effectChance:0.3, desc:'Superheated plasma blast.' },
  { id:'static_field', name:'Static Field', type:'spark', power:45, acc:100, pp:25, hits:2, effect:'shock', effectChance:0.15, desc:'Double static discharge.' },
  // Wave abilities (8)
  { id:'tide_splash', name:'Tide Splash', type:'wave', power:40, acc:100, pp:25, effect:'none', effectChance:0, desc:'Splash of water.' },
  { id:'water_jet', name:'Water Jet', type:'wave', power:60, acc:90, pp:15, effect:'none', effectChance:0, desc:'Pressurized water stream.' },
  { id:'wave_crash', name:'Wave Crash', type:'wave', power:80, acc:75, pp:10, effect:'shock', effectChance:0.2, desc:'Crashing tidal wave.' },
  { id:'tsunami_roar', name:'Tsunami Roar', type:'wave', power:90, acc:80, pp:8, effect:'none', effectChance:0, desc:'Devastating tsunami force.' },
  { id:'mist_heal', name:'Mist Heal', type:'wave', power:0, acc:100, pp:20, effect:'regen', effectChance:1, desc:'Healing mist restores HP.' },
  { id:'coral_guard', name:'Coral Guard', type:'wave', power:0, acc:100, pp:25, effect:'barrier', effectChance:1, desc:'Protective coral shield.' },
  { id:'abyssal_pull', name:'Abyssal Pull', type:'wave', power:70, acc:85, pp:12, effect:'frost', effectChance:0.2, desc:'Deep water pull attack.' },
  { id:'riptide_spin', name:'Riptide Spin', type:'wave', power:45, acc:100, pp:30, hits:3, effect:'none', effectChance:0, desc:'Three spinning strikes.' },
  // Prism abilities (8)
  { id:'light_beam', name:'Light Beam', type:'prism', power:50, acc:95, pp:20, effect:'none', effectChance:0, desc:'Focused beam of light.' },
  { id:'prism_ray', name:'Prism Ray', type:'prism', power:60, acc:90, pp:15, effect:'burn', effectChance:0.2, desc:'Refracted light ray.' },
  { id:'radiant_burst', name:'Radiant Burst', type:'prism', power:80, acc:80, pp:10, effect:'none', effectChance:0, desc:'Explosion of radiance.' },
  { id:'solar_flare', name:'Solar Flare', type:'prism', power:90, acc:75, pp:8, effect:'burn', effectChance:0.4, desc:'Blinding solar eruption.' },
  { id:'dawn_strike', name:'Dawn Strike', type:'prism', power:70, acc:90, pp:12, effect:'none', effectChance:0, desc:'First light of dawn.' },
  { id:'gleam_shield', name:'Gleam Shield', type:'prism', power:0, acc:100, pp:25, effect:'barrier', effectChance:1, desc:'Shimmering protective glow.' },
  { id:'aurora_wave', name:'Aurora Wave', type:'prism', power:45, acc:100, pp:30, hits:2, effect:'regen', effectChance:0.5, desc:'Healing aurora strikes twice.' },
  { id:'starfall', name:'Starfall', type:'prism', power:95, acc:70, pp:6, effect:'burn', effectChance:0.3, desc:'Stars rain from above.' },
  // Void abilities (8)
  { id:'shadow_claw', name:'Shadow Claw', type:'void', power:40, acc:100, pp:25, effect:'none', effectChance:0, desc:'Claws of darkness.' },
  { id:'void_drain', name:'Void Drain', type:'void', power:30, acc:100, pp:20, effect:'regen', effectChance:1, desc:'Drain life from the void.' },
  { id:'dark_pulse', name:'Dark Pulse', type:'void', power:60, acc:90, pp:15, effect:'shock', effectChance:0.2, desc:'Pulse of dark energy.' },
  { id:'null_field', name:'Null Field', type:'void', power:0, acc:100, pp:20, effect:'barrier', effectChance:1, desc:'Field that nullifies attacks.' },
  { id:'abyss_gaze', name:'Abyss Gaze', type:'void', power:70, acc:85, pp:12, effect:'shock', effectChance:0.3, desc:'Paralyzing void stare.' },
  { id:'phantom_swipe', name:'Phantom Swipe', type:'void', power:45, acc:100, pp:30, hits:2, effect:'none', effectChance:0, desc:'Two ghostly strikes.' },
  { id:'entropy_touch', name:'Entropy Touch', type:'void', power:80, acc:80, pp:10, effect:'frost', effectChance:0.3, desc:'Decay-inducing touch.' },
  { id:'oblivion_call', name:'Oblivion Call', type:'void', power:95, acc:70, pp:6, effect:'shock', effectChance:0.4, desc:'Call from the abyss.' },
  // General utility abilities (8)
  { id:'swift_step', name:'Swift Step', type:'prism', power:0, acc:100, pp:30, effect:'speed_boost', effectChance:1, desc:'Boost own speed sharply.' },
  { id:'barrier_weave', name:'Barrier Weave', type:'wave', power:0, acc:100, pp:25, effect:'barrier', effectChance:1, desc:'Weave a deflecting barrier.' },
  { id:'vitality_surge', name:'Vitality Surge', type:'wave', power:0, acc:100, pp:20, effect:'regen', effectChance:1, desc:'Surge of healing energy.' },
  { id:'focus_mind', name:'Focus Mind', type:'prism', power:0, acc:100, pp:25, effect:'atk_boost', effectChance:1, desc:'Sharpen focus for stronger hits.' },
  { id:'echo_strike', name:'Echo Strike', type:'spark', power:30, acc:95, pp:20, hits:3, effect:'shock', effectChance:0.1, desc:'Three echoing strikes.' },
  { id:'priority_dash', name:'Priority Dash', type:'frost', power:40, acc:100, pp:25, priority:1, effect:'none', effectChance:0, desc:'Strikes first every time.' },
  { id:'iron_bark', name:'Iron Bark', type:'stone', power:0, acc:100, pp:30, effect:'def_boost', effectChance:1, desc:'Harden defenses greatly.' },
  { id:'life_drain', name:'Life Drain', type:'void', power:40, acc:95, pp:15, effect:'regen', effectChance:1, desc:'Steal life force to heal.' }
];

// ── function: getAbility ──
function getAbility(id) { return ABILITIES.find(a => a.id === id); }

// ── const: CREATURES ──
const CREATURES = {
  // Zone 1 - Ashen Wastes (Ember theme)
  ignisprite: { name:'Ignisprite', types:['ember'], lore:'A tiny spark given form, it dances through ash-choked winds seeking warmth.', baseStats:{hp:45,atk:52,def:38,spd:48}, learnset:{1:'flame_burst',4:'ember_claw',7:'fire_dance',10:'scorch_wave',15:'pyre_fang',20:'blaze_storm',25:'cinder_rain',30:'inferno_pulse'}, transform:{name:'emberwraith',level:30,catalyst:'ember_catalyst'} },
  cindermaw: { name:'Cindermaw', types:['ember','stone'], lore:'A burrowing beast whose jaws glow with molten heat from within.', baseStats:{hp:58,atk:48,def:52,spd:30}, learnset:{1:'rock_throw',4:'ember_claw',7:'earth_slam',10:'pyre_fang',15:'boulder_crush',20:'scorch_wave',25:'quake_pulse',30:'blaze_storm'} },
  pyrefox: { name:'Pyrefox', types:['ember','spark'], lore:'Swift and cunning, its tail leaves trails of crackling flame.', baseStats:{hp:42,atk:55,def:35,spd:60}, learnset:{1:'flame_burst',4:'shock_bolt',7:'fire_dance',10:'lightning_arc',15:'pyre_fang',20:'storm_surge',25:'blaze_storm',30:'plasma_burst'}, transform:{name:'infernalion',level:35,catalyst:'ember_catalyst'} },
  // Zone 2 - Frozen Expanse (Frost theme)
  glaciborn: { name:'Glaciborn', types:['frost'], lore:'Born from ancient ice, it hums with the quiet power of frozen time.', baseStats:{hp:50,atk:45,def:50,spd:38}, learnset:{1:'ice_shard',4:'frost_bite',7:'rime_touch',10:'blizzard_sweep',15:'crystal_lance',20:'snow_veil',25:'glacier_crash',30:'permafrost'}, transform:{name:'frostveil',level:30,catalyst:'frost_catalyst'} },
  iceclaw: { name:'Iceclaw', types:['frost','stone'], lore:'Its crystalline claws can shatter boulders with a single strike.', baseStats:{hp:55,atk:52,def:55,spd:28}, learnset:{1:'rock_throw',4:'ice_shard',7:'earth_slam',10:'crystal_lance',15:'boulder_crush',20:'blizzard_sweep',25:'quake_pulse',30:'glacier_crash'} },
  snowdrift: { name:'Snowdrift', types:['frost','wave'], lore:'A gentle spirit that rides the wind, leaving frost-kissed trails.', baseStats:{hp:48,atk:42,def:45,spd:50}, learnset:{1:'tide_splash',4:'ice_shard',7:'mist_heal',10:'blizzard_sweep',15:'riptide_spin',20:'snow_veil',25:'wave_crash',30:'permafrost'} },
  // Zone 3 - Crystal Caverns (Stone theme)
  stoneshard: { name:'Stoneshard', types:['stone'], lore:'A fragment of the world\'s backbone, slow but unyielding.', baseStats:{hp:62,atk:40,def:58,spd:25}, learnset:{1:'rock_throw',4:'earth_slam',7:'pebble_barrage',10:'stone_wall',15:'boulder_crush',20:'granite_shield',25:'quake_pulse',30:'tectonic_shift'}, transform:{name:'earthwarden',level:30,catalyst:'stone_catalyst'} },
  geodebug: { name:'Geodebug', types:['stone','void'], lore:'A beetle with a gem-encrusted shell that absorbs dark energy.', baseStats:{hp:52,atk:48,def:50,spd:32}, learnset:{1:'shadow_claw',4:'rock_throw',7:'dark_pulse',10:'earth_slam',15:'void_drain',20:'boulder_crush',25:'entropy_touch',30:'tectonic_shift'} },
  rockfinch: { name:'Rockfinch', types:['stone','spark'], lore:'A bird that nests in lightning-struck cliffs, feathers crackling with static.', baseStats:{hp:40,atk:50,def:38,spd:55}, learnset:{1:'shock_bolt',4:'rock_throw',7:'lightning_arc',10:'earth_slam',15:'electric_fang',20:'boulder_crush',25:'storm_surge',30:'plasma_burst'} },
  // Zone 4 - Storm Peaks (Spark theme)
  voltwing: { name:'Voltwing', types:['spark'], lore:'Wings of pure electricity, it rides the storm currents between peaks.', baseStats:{hp:44,atk:56,def:36,spd:58}, learnset:{1:'shock_bolt',4:'lightning_arc',7:'static_field',10:'thunder_strike',15:'electric_fang',20:'storm_surge',25:'voltage_drain',30:'plasma_burst'}, transform:{name:'stormcaller',level:35,catalyst:'spark_catalyst'} },
  thunderbeast: { name:'Thunderbeast', types:['spark','ember'], lore:'A massive creature whose roar summons both lightning and flame.', baseStats:{hp:60,atk:58,def:42,spd:35}, learnset:{1:'flame_burst',4:'shock_bolt',7:'thunder_strike',10:'scorch_wave',15:'storm_surge',20:'blaze_storm',25:'plasma_burst',30:'inferno_pulse'} },
  zapfly: { name:'Zapfly', types:['spark','frost'], lore:'A moth whose wings freeze the air while discharging static sparks.', baseStats:{hp:42,atk:48,def:40,spd:56}, learnset:{1:'ice_shard',4:'shock_bolt',7:'static_field',10:'blizzard_sweep',15:'electric_fang',20:'storm_surge',25:'permafrost',30:'plasma_burst'} },
  // Zone 5 - Tide Marshes (Wave theme)
  tidewisp: { name:'Tidewisp', types:['wave'], lore:'A luminous wisp that rises from tidal pools, singing in frequencies only water hears.', baseStats:{hp:46,atk:48,def:42,spd:50}, learnset:{1:'tide_splash',4:'water_jet',7:'riptide_spin',10:'mist_heal',15:'abyssal_pull',20:'wave_crash',25:'coral_guard',30:'tsunami_roar'}, transform:{name:'abyssaldrift',level:35,catalyst:'wave_catalyst'} },
  coralhulk: { name:'Coralhulk', types:['wave','stone'], lore:'A hulking mass of living coral, slow but nearly indestructible.', baseStats:{hp:68,atk:42,def:60,spd:22}, learnset:{1:'rock_throw',4:'tide_splash',7:'earth_slam',10:'water_jet',15:'boulder_crush',20:'wave_crash',25:'granite_shield',30:'tsunami_roar'} },
  mistcrawler: { name:'Mistcrawler', types:['wave','void'], lore:'It moves through fog like a shadow, leaving damp footprints that vanish.', baseStats:{hp:48,atk:50,def:44,spd:46}, learnset:{1:'shadow_claw',4:'tide_splash',7:'dark_pulse',10:'water_jet',15:'void_drain',20:'wave_crash',25:'entropy_touch',30:'tsunami_roar'} },
  // Zone 6 - Prism Spire (Prism/Void theme)
  prismite: { name:'Prismite', types:['prism'], lore:'A crystalline being that refracts light into healing rainbows.', baseStats:{hp:48,atk:50,def:46,spd:44}, learnset:{1:'light_beam',4:'dawn_strike',7:'aurora_wave',10:'prism_ray',15:'gleam_shield',20:'radiant_burst',25:'solar_flare',30:'starfall'}, transform:{name:'luminary',level:40,catalyst:'prism_shard'} },
  voidling: { name:'Voidling', types:['void'], lore:'A creature from between the cracks of reality, whispering forgotten truths.', baseStats:{hp:52,atk:52,def:48,spd:42}, learnset:{1:'shadow_claw',4:'dark_pulse',7:'phantom_swipe',10:'void_drain',15:'abyss_gaze',20:'null_field',25:'entropy_touch',30:'oblivion_call'}, transform:{name:'nullweaver',level:40,catalyst:'void_core'} },
  aethermoth: { name:'Aethermoth', types:['prism','wave'], lore:'Its wings scatter prismatic dust that calms even the wildest storms.', baseStats:{hp:50,atk:46,def:48,spd:52}, learnset:{1:'light_beam',4:'tide_splash',7:'aurora_wave',10:'water_jet',15:'prism_ray',20:'wave_crash',25:'solar_flare',30:'starfall'}, transform:{name:'celestara',level:40,catalyst:'prism_shard'} }
};

// ── const: TRANSFORMED ──
// Transformed forms (9)
const TRANSFORMED = {
  emberwraith: { name:'Emberwraith', types:['ember'], lore:'The spark evolved into a wraith of living flame, wreathed in eternal fire.', baseStats:{hp:75,atk:82,def:60,spd:72}, learnset:{1:'flame_burst',4:'ember_claw',7:'fire_dance',10:'scorch_wave',15:'pyre_fang',20:'blaze_storm',25:'cinder_rain',30:'inferno_pulse'} },
  frostveil: { name:'Frostveil', types:['frost'], lore:'Cloaked in eternal winter, its presence freezes time itself.', baseStats:{hp:80,atk:72,def:76,spd:58}, learnset:{1:'ice_shard',4:'frost_bite',7:'rime_touch',10:'blizzard_sweep',15:'crystal_lance',20:'snow_veil',25:'glacier_crash',30:'permafrost'} },
  earthwarden: { name:'Earthwarden', types:['stone'], lore:'The living embodiment of the world\'s spine, unmovable and eternal.', baseStats:{hp:95,atk:62,def:88,spd:38}, learnset:{1:'rock_throw',4:'earth_slam',7:'pebble_barrage',10:'stone_wall',15:'boulder_crush',20:'granite_shield',25:'quake_pulse',30:'tectonic_shift'} },
  stormcaller: { name:'Stormcaller', types:['spark'], lore:'It commands the tempest itself, wings crackling with apocalyptic lightning.', baseStats:{hp:72,atk:86,def:54,spd:82}, learnset:{1:'shock_bolt',4:'lightning_arc',7:'static_field',10:'thunder_strike',15:'electric_fang',20:'storm_surge',25:'voltage_drain',30:'plasma_burst'} },
  abyssaldrift: { name:'Abyssaldrift', types:['wave'], lore:'Risen from the deepest trenches, it carries the weight of ancient oceans.', baseStats:{hp:78,atk:76,def:64,spd:70}, learnset:{1:'tide_splash',4:'water_jet',7:'riptide_spin',10:'mist_heal',15:'abyssal_pull',20:'wave_crash',25:'coral_guard',30:'tsunami_roar'} },
  luminary: { name:'Luminary', types:['prism'], lore:'A being of pure radiance, its light can mend even the deepest wounds.', baseStats:{hp:76,atk:78,def:68,spd:64}, learnset:{1:'light_beam',4:'dawn_strike',7:'aurora_wave',10:'prism_ray',15:'gleam_shield',20:'radiant_burst',25:'solar_flare',30:'starfall'} },
  nullweaver: { name:'Nullweaver', types:['void'], lore:'It weaves the fabric of nothingness, unraveling reality thread by thread.', baseStats:{hp:82,atk:80,def:72,spd:62}, learnset:{1:'shadow_claw',4:'dark_pulse',7:'phantom_swipe',10:'void_drain',15:'abyss_gaze',20:'null_field',25:'entropy_touch',30:'oblivion_call'} },
  infernalion: { name:'Infernalion', types:['ember','spark'], lore:'A blazing lion of fire and lightning, its roar shakes the heavens.', baseStats:{hp:74,atk:88,def:56,spd:78}, learnset:{1:'flame_burst',4:'shock_bolt',7:'fire_dance',10:'lightning_arc',15:'pyre_fang',20:'storm_surge',25:'blaze_storm',30:'plasma_burst'} },
  celestara: { name:'Celestara', types:['prism','wave'], lore:'A celestial being of light and tide, it sings the world into harmony.', baseStats:{hp:78,atk:72,def:70,spd:74}, learnset:{1:'light_beam',4:'tide_splash',7:'aurora_wave',10:'water_jet',15:'prism_ray',20:'wave_crash',25:'solar_flare',30:'starfall'} }
};

// ── function: getCreatureData ──
function getCreatureData(id) { return CREATURES[id] || TRANSFORMED[id]; }

// ── const: ZONES ──
const ZONES = [
  { id:'ashen_wastes', name:'The Ashen Wastes', gradient:['#1a0e08','#2d1510','#4a2018'], particles:{color:'#ff6b3a',speed:0.5,size:3}, encounterRate:0.08, storyNode:true, guardianId:'cindermaw_g', guardianLevel:8, unlockBonus:'+5 ATK to all Ember types' },
  { id:'frozen_expanse', name:'The Frozen Expanse', gradient:['#0a1428','#142040','#1e3060'], particles:{color:'#5bcfff',speed:0.3,size:4}, encounterRate:0.07, storyNode:true, guardianId:'iceclaw_g', guardianLevel:14, unlockBonus:'+5 DEF to all Frost types' },
  { id:'crystal_caverns', name:'The Crystal Caverns', gradient:['#1a1810','#2d2818','#3e3820'], particles:{color:'#ffe040',speed:0.4,size:2}, encounterRate:0.09, storyNode:true, guardianId:'geodebug_g', guardianLevel:20, unlockBonus:'+5 SPD to all Stone types' },
  { id:'storm_peaks', name:'The Storm Peaks', gradient:['#18102a','#24183e','#302052'], particles:{color:'#ffe040',speed:0.7,size:3}, encounterRate:0.06, storyNode:true, guardianId:'thunderbeast_g', guardianLevel:26, unlockBonus:'+5 HP to all Spark types' },
  { id:'tide_marshes', name:'The Tide Marshes', gradient:['#0a1818','#142828','#1e3838'], particles:{color:'#40c8e0',speed:0.5,size:3}, encounterRate:0.07, storyNode:true, guardianId:'coralhulk_g', guardianLevel:32, unlockBonus:'+5 ATK to all Wave types' },
  { id:'prism_spire', name:'The Prism Spire', gradient:['#1a1028','#241840','#302060'], particles:{color:'#f0d0ff',speed:0.6,size:4}, encounterRate:0.05, storyNode:true, guardianId:'voidling_g', guardianLevel:38, unlockBonus:'+5 all stats' }
];

// ── const: GUARDIANS ──
const GUARDIANS = {
  cindermaw_g: { name:'Cinder Maw (Guardian)', baseId:'cindermaw', level:8 },
  iceclaw_g: { name:'Iceclaw Prime (Guardian)', baseId:'iceclaw', level:14 },
  geodebug_g: { name:'Geodebug Sovereign (Guardian)', baseId:'geodebug', level:20 },
  thunderbeast_g: { name:'Thunderbeast Alpha (Guardian)', baseId:'thunderbeast', level:26 },
  coralhulk_g: { name:'Coralhulk Titan (Guardian)', baseId:'coralhulk', level:32 },
  voidling_g: { name:'Voidling Anchor (Guardian)', baseId:'voidling', level:38 }
};

// ── const: ITEMS ──
const ITEMS = [
  { id:'aether_orb_t1', name:'Aether Orb I', type:'capture', tier:1, desc:'Basic capture orb. Low success rate.', price:20 },
  { id:'aether_orb_t2', name:'Aether Orb II', type:'capture', tier:2, desc:'Enhanced capture orb. Moderate success.', price:50 },
  { id:'aether_orb_t3', name:'Aether Orb III', type:'capture', tier:3, desc:'Master capture orb. High success rate.', price:100 },
  { id:'vital_vial', name:'Vital Vial', type:'heal', amount:50, desc:'Restores 50 HP to a Luminary.', price:15 },
  { id:'full_restorative', name:'Full Restorative', type:'heal', amount:200, desc:'Fully restores one Luminary.', price:60 },
  { id:'ember_catalyst', name:'Ember Catalyst', type:'catalyst', desc:'Transforms eligible Ember Luminaries.', price:150 },
  { id:'frost_catalyst', name:'Frost Catalyst', type:'catalyst', desc:'Transforms eligible Frost Luminaries.', price:150 },
  { id:'stone_catalyst', name:'Stone Catalyst', type:'catalyst', desc:'Transforms eligible Stone Luminaries.', price:150 },
  { id:'spark_catalyst', name:'Spark Catalyst', type:'catalyst', desc:'Transforms eligible Spark Luminaries.', price:200 },
  { id:'wave_catalyst', name:'Wave Catalyst', type:'catalyst', desc:'Transforms eligible Wave Luminaries.', price:200 },
  { id:'prism_shard', name:'Prism Shard', type:'catalyst', desc:'Rare catalyst for Prism transformations.', price:300 },
  { id:'void_core', name:'Void Core', type:'catalyst', desc:'Rare catalyst for Void transformations.', price:300 }
];

// ── function: getItem ──
function getItem(id) { return ITEMS.find(i => i.id === id); }

// ── const: STORY_VIGNETTES ──
const STORY_VIGNETTES = [
  { title:'The Shattering', text:'Long ago, the Aetherium — source of all luminous life — shattered into fragments. From these shards arose the Luminaries, creatures of crystallized energy roaming the fractured zones of the Expanse.' },
  { title:'The Weaver\'s Call', text:'You are a Weaver, one who can bind with Luminaries through ancient pacts. The four Zone Anchors have grown unstable, threatening to collapse the remaining Aetherium entirely.' },
  { title:'Rising Tides', text:'As you journey deeper, the Anchors grow stronger. Other Weavers have fallen. Only by collecting eight unique Luminaries and defeating each Anchor can the Expanse be saved.' },
  { title:'The Final Binding', text:'The Prism Spire holds the last Anchor — a Voidling of immense power. Beyond it lies the heart of the Aetherium, where all fragments may yet be reunited.' }
];

// ── const: TUTORIAL_STEPS ──
const TUTORIAL_STEPS = [
  { title:'Welcome, Weaver', text:'Use WASD or arrow keys to move across the overworld. Touch controls appear on mobile devices.' },
  { title:'Wild Luminaries', text:'You will encounter wild Luminaries as you explore. Engage them in turn-based combat!' },
  { title:'Binding Creatures', text:'After weakening a Luminary, attempt to capture it with an Aether Orb. Time your tap when the indicator enters the green zone.' },
  { title:'Aether Shrines', text:'Visit shrines (✦) to heal your party, save progress, and purchase supplies from the cache.' }
];

// ── let: audioCtx ──
let audioCtx = null;

// ── let: masterGain ──
let masterGain = null;

// ── let: isMuted ──
let isMuted = false;

// ── let: ambientOsc ──
let ambientOsc = null;

// ── let: ambientGain ──
let ambientGain = null;

// ── function: initAudio ──
function initAudio() {
  try {
    audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    masterGain = audioCtx.createGain();
    masterGain.gain.value = 0.3;
    masterGain.connect(audioCtx.destination);
    startAmbientPad();
  } catch(e) { console.log('Audio init failed:', e); }
}

// ── function: startAmbientPad ──
function startAmbientPad() {
  if (!audioCtx || ambientOsc) return;
  try {
    ambientGain = audioCtx.createGain();
    ambientGain.gain.value = 0.06;
    const filter = audioCtx.createBiquadFilter();
    filter.type = 'lowpass';
    filter.frequency.value = 400;
    ambientOsc = audioCtx.createOscillator();
    ambientOsc.type = 'sine';
    ambientOsc.frequency.value = 110;
    const osc2 = audioCtx.createOscillator();
    osc2.type = 'sine';
    osc2.frequency.value = 165;
    const g2 = audioCtx.createGain();
    g2.gain.value = 0.04;
    ambientOsc.connect(filter);
    osc2.connect(g2);
    g2.connect(filter);
    filter.connect(ambientGain);
    ambientGain.connect(masterGain);
    ambientOsc.start();
    osc2.start();
  } catch(e) {}
}

// ── function: playTone ──
function playTone(freq, duration, type='square', vol=0.15) {
  if (!audioCtx || isMuted) return;
  try {
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();
    osc.type = type;
    osc.frequency.value = freq;
    gain.gain.setValueAtTime(vol, audioCtx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration);
    osc.connect(gain);
    gain.connect(masterGain);
    osc.start();
    osc.stop(audioCtx.currentTime + duration);
  } catch(e) {}
}

// ── function: sfxAttack ──
function sfxAttack() { playTone(220, 0.15, 'square', 0.12); setTimeout(()=>playTone(330, 0.1, 'square', 0.08), 50); }

// ── function: sfxHit ──
function sfxHit() { playTone(150, 0.2, 'sawtooth', 0.1); }

// ── function: sfxCrit ──
function sfxCrit() { playTone(440, 0.1, 'square', 0.15); setTimeout(()=>playTone(660, 0.15, 'square', 0.12), 80); setTimeout(()=>playTone(880, 0.2, 'square', 0.1), 160); }

// ── function: sfxCapture ──
function sfxCapture() { playTone(523, 0.15, 'sine', 0.12); setTimeout(()=>playTone(659, 0.15, 'sine', 0.12), 100); setTimeout(()=>playTone(784, 0.3, 'sine', 0.15), 200); }

// ── function: sfxVictory ──
function sfxVictory() { [523,659,784,1047].forEach((f,i)=>setTimeout(()=>playTone(f,0.2,'square',0.1),i*120)); }

// ── function: sfxDefeat ──
function sfxDefeat() { playTone(200, 0.3, 'sawtooth', 0.1); setTimeout(()=>playTone(150, 0.4, 'sawtooth', 0.08), 200); }

// ── function: sfxStep ──
function sfxStep() { playTone(80 + Math.random()*40, 0.05, 'triangle', 0.03); }

// ── function: sfxMenu ──
function sfxMenu() { playTone(600, 0.08, 'sine', 0.08); }

// ── function: sfxLevelUp ──
function sfxLevelUp() { [440,554,659,880].forEach((f,i)=>setTimeout(()=>playTone(f,0.15,'square',0.1),i*100)); }

// ── function: toggleMute ──
function toggleMute() {
  isMuted = !isMuted;
  if (masterGain) masterGain.gain.value = isMuted ? 0 : 0.3;
  return isMuted;
}

// ── let: particles ──
// Particle System
let particles = [];

// ── class: Particle ──
class Particle {
  constructor(x, y, vx, vy, color, life, size) {
    this.x = x; this.y = y; this.vx = vx; this.vy = vy;
    this.color = color; this.life = life; this.maxLife = life;
    this.size = size || 3;
  }
  update() {
    this.x += this.vx; this.y += this.vy;
    this.life--;
    if (this.vy !== undefined) this.vy += 0.02; // slight gravity
  }
  get alpha() { return Math.max(0, this.life / this.maxLife); }
  get alive() { return this.life > 0; }
}

// ── function: spawnParticles ──
function spawnParticles(x, y, count, color, spread=3, life=40, size=3) {
  for (let i = 0; i < count; i++) {
    particles.push(new Particle(
      x + (Math.random()-0.5)*20, y + (Math.random()-0.5)*20,
      (Math.random()-0.5)*spread, (Math.random()-0.5)*spread - 1,
      color, life + Math.random()*20|0, size*(0.5+Math.random()*0.5)
    ));
  }
}

// ── function: updateParticles ──
function updateParticles() {
  particles = particles.filter(p => p.alive);
  particles.forEach(p => p.update());
}

// ── let: floatingTexts ──
// Floating damage numbers
let floatingTexts = [];

// ── class: FloatingText ──
class FloatingText {
  constructor(x, y, text, color) {
    this.x = x; this.y = y; this.text = text; this.color = color || '#fff';
    this.life = 60; this.maxLife = 60; this.vy = -1.5;
  }
  update() { this.y += this.vy; this.life--; }
  get alpha() { return Math.max(0, this.life / this.maxLife); }
  get alive() { return this.life > 0; }
}

// ── function: spawnFloatingText ──
function spawnFloatingText(x, y, text, color) {
  floatingTexts.push(new FloatingText(x, y, text, color));
}

// ── let: screenShake ──
// Screen shake
let screenShake = { x: 0, y: 0, intensity: 0, duration: 0 };

// ── function: triggerShake ──
function triggerShake(intensity, duration) {
  screenShake.intensity = intensity;
  screenShake.duration = duration;
}

// ── function: updateScreenShake ──
function updateScreenShake() {
  if (screenShake.duration > 0) {
    screenShake.x = (Math.random()-0.5) * screenShake.intensity;
    screenShake.y = (Math.random()-0.5) * screenShake.intensity;
    screenShake.duration--;
    screenShake.intensity *= 0.95;
  } else {
    screenShake.x = 0; screenShake.y = 0;
  }
}

// ── let: creatureIdCounter ──
// Creature instance factory
let creatureIdCounter = 0;

// ── function: createCreature ──
function createCreature(baseId, level) {
  const data = getCreatureData(baseId);
  if (!data) return null;
  const stats = {};
  for (const key of ['hp','atk','def','spd']) {
    stats[key] = Math.floor(data.baseStats[key] * (1 + level * 0.04));
  }
  const abilities = [];
  for (const [lvl, abId] of Object.entries(data.learnset)) {
    if (level >= parseInt(lvl)) abilities.push(abId);
  }
  return {
    id: creatureIdCounter++,
    baseId, name: data.name, types: [...data.types], level,
    maxHp: stats.hp, hp: stats.hp, atk: stats.atk, def: stats.def, spd: stats.spd,
    abilities, statusEffect: 'none', statusTurns: 0,
    statBoosts: {atk:0,def:0,spd:0},
    isTransformed: false, lore: data.lore || ''
  };
}

// ── function: calcDamage ──
function calcDamage(attacker, defender, abilityId) {
  const ab = getAbility(abilityId);
  if (!ab || ab.power === 0) return { damage: 0, crit: false, effective: 1 };
  
  let atkStat = attacker.atk + (attacker.statBoosts.atk || 0) * 5;
  let defStat = defender.def + (defender.statBoosts.def || 0) * 5;
  
  // Burn halves ATK
  if (attacker.statusEffect === 'burn') atkStat = Math.floor(atkStat * 0.5);
  
  const typeMult = getTypeMultiplier(ab.type, defender.types[0]);
  const stab = attacker.types.includes(ab.type) ? 1.25 : 1;
  const random = 0.85 + Math.random() * 0.15;
  const crit = Math.random() < (attacker.spd > defender.spd ? 0.12 : 0.06);
  
  let damage = Math.floor((0.4 * attacker.level * atkStat / Math.max(1, defStat) * ab.power / 50 * typeMult * stab * random));
  if (crit) damage = Math.floor(damage * 1.5);
  damage = Math.max(1, damage);
  
  return { damage, crit, effective: typeMult };
}

// ── function: applyStatusEffect ──
function applyStatusEffect(target, effect, turns=3) {
  target.statusEffect = effect;
  target.statusTurns = turns;
}

// ── function: processStatusEffects ──
function processStatusEffects(creature) {
  if (creature.statusTurns <= 0) { creature.statusEffect = 'none'; return; }
  switch(creature.statusEffect) {
    case 'burn':
      const burnDmg = Math.max(1, Math.floor(creature.maxHp * 0.08));
      creature.hp = Math.max(0, creature.hp - burnDmg);
      break;
    case 'regen':
      const healAmt = Math.max(1, Math.floor(creature.maxHp * 0.12));
      creature.hp = Math.min(creature.maxHp, creature.hp + healAmt);
      break;
  }
  creature.statusTurns--;
}

// ── function: getEffectiveSpd ──
function getEffectiveSpd(creature) {
  let spd = creature.spd + (creature.statBoosts.spd || 0) * 8;
  if (creature.statusEffect === 'frost') spd = Math.floor(spd * 0.5);
  return spd;
}

// ── function: shouldSkipTurn ──
function shouldSkipTurn(creature) {
  if (creature.statusEffect === 'shock' && Math.random() < 0.3) return true;
  return false;
}

// ── let: G ──
// Game State
let G = {};

// ── function: initGameState ──
// Global game state

function initGameState() {
  return {
    screen: 'title', // title, overworld, battle, capture, menu, shrine, shop, team, bag, codex, map, settings, dialogue, vignette, tutorial, victory
    zoneIndex: 0,
    playerX: 5, playerY: 5,
    party: [],
    inventory: { aether_orb_t1: 3, vital_vial: 2 },
    gold: 50,
    shards: 0,
    guardiansDefeated: [],
    storyStage: 0,
    tutorialStep: -1, // -1 = not started, 0-3 = steps, 4 = complete
    postGame: false,
    battleState: null,
    moveCooldown: 0,
    lastShrineZone: 0,
    discoveredCreatures: [],
    totalBattlesWon: 0,
    totalCaptured: 0
  };
}

// ── const: TILE_SIZE ──
// Overworld tile map (simple grid-based)
const TILE_SIZE = 48;

// ── const: MAP_W ──
const MAP_W = 20;

// ── const: MAP_H ──
const MAP_H = 15;

// ── let: moveCooldown ──
let moveCooldown = 0;

// ── let: holdTimer ──
let holdTimer = 0;

// ── let: lastMoveDir ──
let lastMoveDir = { x: 0, y: 0 };

// ── function: movePlayer ──
function movePlayer(dx, dy) {
  if (G.screen !== 'overworld') return false;
  
  // First press has a cooldown, subsequent holds are faster
  if (dx === lastMoveDir.x && dy === lastMoveDir.y) {
    holdTimer++;
    if (holdTimer < 3 || moveCooldown > 0) return false; // Initial delay then rapid
  } else {
    holdTimer = 0;
    if (moveCooldown > 0) return false;
  }
  
  const nx = G.playerX + dx;
  const ny = G.playerY + dy;
  
  if (nx < 0 || nx >= MAP_W || ny < 0 || ny >= MAP_H) return false;
  if (currentMap[ny][nx] === 1) return false; // wall
  
  G.playerX = nx;
  G.playerY = ny;
  moveCooldown = 6;
  
  sfxStep();
  
  // Check tile interactions
  const tile = currentMap[ny][nx];
  if (tile === 2) {
    enterShrine();
  } else if (tile === 3) {
    startGuardianBattle();
  } else if (tile === 4) {
    advanceZone();
  } else {
    // Random encounter check
    const zone = ZONES[G.zoneIndex];
    const rate = G.postGame ? zone.encounterRate * 1.5 : zone.encounterRate;
    if (Math.random() < rate && G.tutorialStep >= 4) {
      startWildBattle();
    } else if (G.tutorialStep === 0 && Math.abs(G.playerX - 5) + Math.abs(G.playerY - 5) >= 3) {
      startTutorialBattle();
    }
  }
  
  return true;
}

// ── function: resetMoveCooldown ──
function resetMoveCooldown() { moveCooldown = 0; holdTimer = 0; lastMoveDir = { x: 0, y: 0 }; }

// ── function: generateZoneMap ──
// Tile types: 0=walkable, 1=wall/obstacle, 2=shrine, 3=guardian, 4=zone_exit
function generateZoneMap(zoneIdx) {
  const map = Array.from({length: MAP_H}, () => Array(MAP_W).fill(0));
  
  // Border walls
  for (let x = 0; x < MAP_W; x++) { map[0][x] = 1; map[MAP_H-1][x] = 1; }
  for (let y = 0; y < MAP_H; y++) { map[y][0] = 1; map[y][MAP_W-1] = 1; }
  
  // Internal obstacles based on zone
  const seeds = [
    [[3,3],[4,3],[5,3],[7,6],[8,6],[9,6],[12,4],[13,4],[14,4],[10,10],[11,10],[12,10]],
    [[4,4],[5,4],[6,4],[8,8],[9,8],[10,8],[13,3],[14,3],[15,3],[7,11],[8,11],[9,11]],
    [[3,5],[4,5],[5,5],[7,3],[8,3],[9,3],[12,7],[13,7],[14,7],[6,10],[7,10],[8,10]],
    [[5,3],[6,3],[7,3],[9,6],[10,6],[11,6],[13,9],[14,9],[15,9],[4,11],[5,11],[6,11]],
    [[4,4],[5,4],[6,4],[8,7],[9,7],[10,7],[12,4],[13,4],[14,4],[7,10],[8,10],[9,10]],
    [[3,3],[4,3],[5,3],[7,5],[8,5],[9,5],[11,8],[12,8],[13,8],[6,11],[7,11],[8,11]]
  ];
  
  const obstacles = seeds[zoneIdx] || seeds[0];
  obstacles.forEach(([x,y]) => { if (y < MAP_H-1 && x < MAP_W-1) map[y][x] = 1; });
  
  // Place shrine at a fixed position
  map[MAP_H-2][3] = 2;
  
  // Place guardian near the end
  map[2][MAP_W-3] = 3;
  
  // Zone exit (if not last zone or post-game)
  if (zoneIdx < ZONES.length - 1 || G.postGame) {
    map[MAP_H-2][MAP_W-3] = 4;
  }
  
  return map;
}

// ── let: currentMap ──
let currentMap = [];

// ── let: time ──
let time = 0;

// ── function: renderOverworld ──
function renderOverworld(ctx, cw, ch) {
  const zone = ZONES[G.zoneIndex];
  if (!zone) return;
  
  time += 0.016;
  
  // Background gradient with subtle animation
  const grad = ctx.createLinearGradient(0, 0, 0, ch);
  zone.gradient.forEach((c, i) => grad.addColorStop(i / (zone.gradient.length-1), c));
  ctx.fillStyle = grad;
  ctx.fillRect(0, 0, cw, ch);
  
  // Camera offset centered on player
  const camX = G.playerX * TILE_SIZE - cw/2 + TILE_SIZE/2;
  const camY = G.playerY * TILE_SIZE - ch/2 + TILE_SIZE/2;
  
  ctx.save();
  ctx.translate(-camX, -camY);
  
  // Draw tiles with more detail
  for (let y = 0; y < MAP_H; y++) {
    for (let x = 0; x < MAP_W; x++) {
      const tile = currentMap[y][x];
      const px = x * TILE_SIZE, py = y * TILE_SIZE;
      
      // Subtle ground pattern
      if ((x + y) % 2 === 0) {
        ctx.fillStyle = 'rgba(255,255,255,0.01)';
        ctx.fillRect(px, py, TILE_SIZE, TILE_SIZE);
      }
      
      if (tile === 1) {
        // Wall/obstacle - more detailed
        const wallGrad = ctx.createLinearGradient(px, py, px + TILE_SIZE, py + TILE_SIZE);
        wallGrad.addColorStop(0, 'rgba(50,45,70,0.9)');
        wallGrad.addColorStop(1, 'rgba(35,30,55,0.9)');
        ctx.fillStyle = wallGrad;
        ctx.fillRect(px+1, py+1, TILE_SIZE-2, TILE_SIZE-2);
        
        // Wall highlight
        ctx.strokeStyle = 'rgba(80,70,120,0.4)';
        ctx.lineWidth = 1;
        ctx.strokeRect(px+3, py+3, TILE_SIZE-6, TILE_SIZE-6);
        
        // Crystal decoration on some walls
        if ((x * 7 + y * 13) % 5 === 0) {
          ctx.fillStyle = zone.particles.color;
          ctx.globalAlpha = 0.3 + Math.sin(time * 2 + x + y) * 0.15;
          ctx.beginPath();
          ctx.arc(px + TILE_SIZE/2, py + TILE_SIZE/2, 4, 0, Math.PI*2);
          ctx.fill();
          ctx.globalAlpha = 1;
        }
      } else if (tile === 2) {
        // Shrine - glowing
        const shrineGlow = 0.3 + Math.sin(time * 3) * 0.15;
        ctx.fillStyle = `rgba(91,141,239,${shrineGlow})`;
        ctx.fillRect(px+4, py+4, TILE_SIZE-8, TILE_SIZE-8);
        
        // Shrine symbol
        ctx.font = '22px serif';
        ctx.textAlign = 'center';
        ctx.fillStyle = '#5b8def';
        ctx.fillText('✦', px + TILE_SIZE/2, py + TILE_SIZE*0.65);
        
        // Glow ring
        ctx.strokeStyle = `rgba(91,141,239,${shrineGlow * 0.5})`;
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.arc(px + TILE_SIZE/2, py + TILE_SIZE/2, 18 + Math.sin(time*2)*3, 0, Math.PI*2);
        ctx.stroke();
      } else if (tile === 3) {
        // Guardian
        const defeated = G.guardiansDefeated.includes(G.zoneIndex);
        const pulse = Math.sin(time * 4) * 0.1;
        
        if (!defeated) {
          ctx.fillStyle = `rgba(239,91,91,${0.2 + pulse})`;
          ctx.fillRect(px+4, py+4, TILE_SIZE-8, TILE_SIZE-8);
          
          // Guardian symbol with glow
          const gGrad = ctx.createRadialGradient(px+TILE_SIZE/2, py+TILE_SIZE/2, 0, px+TILE_SIZE/2, py+TILE_SIZE/2, 20);
          gGrad.addColorStop(0, 'rgba(239,91,91,0.4)');
          gGrad.addColorStop(1, 'rgba(239,91,91,0)');
          ctx.fillStyle = gGrad;
          ctx.beginPath();
          ctx.arc(px+TILE_SIZE/2, py+TILE_SIZE/2, 20, 0, Math.PI*2);
          ctx.fill();
        } else {
          ctx.fillStyle = 'rgba(91,239,141,0.15)';
          ctx.fillRect(px+4, py+4, TILE_SIZE-8, TILE_SIZE-8);
        }
        
        ctx.font = '18px serif';
        ctx.textAlign = 'center';
        ctx.fillStyle = defeated ? '#5bef8d' : '#ef5b5b';
        ctx.fillText(defeated ? '✓' : '⚔', px + TILE_SIZE/2, py + TILE_SIZE*0.65);
      } else if (tile === 4) {
        // Zone exit - pulsing portal effect
        const portalPulse = Math.sin(time * 3) * 0.15;
        ctx.fillStyle = `rgba(240,192,64,${0.2 + portalPulse})`;
        ctx.fillRect(px+4, py+4, TILE_SIZE-8, TILE_SIZE-8);
        
        // Portal swirl effect
        for (let i = 0; i < 3; i++) {
          const angle = time * 2 + i * Math.PI * 2 / 3;
          const r = 12;
          ctx.fillStyle = `rgba(240,192,64,${0.3 - i*0.08})`;
          ctx.beginPath();
          ctx.arc(px + TILE_SIZE/2 + Math.cos(angle)*r, py + TILE_SIZE/2 + Math.sin(angle)*r, 3, 0, Math.PI*2);
          ctx.fill();
        }
        
        ctx.font = '16px serif';
        ctx.textAlign = 'center';
        ctx.fillStyle = '#f0c040';
        ctx.fillText('→', px + TILE_SIZE/2, py + TILE_SIZE*0.65);
      } else {
        // Walkable tile - subtle grid lines
        ctx.strokeStyle = 'rgba(255,255,255,0.02)';
        ctx.lineWidth = 0.5;
        ctx.strokeRect(px+0.5, py+0.5, TILE_SIZE-1, TILE_SIZE-1);
        
        // Random ground details
        if ((x * 3 + y * 7) % 11 === 0) {
          ctx.fillStyle = 'rgba(255,255,255,0.03)';
          ctx.beginPath();
          ctx.arc(px + TILE_SIZE/2, py + TILE_SIZE/2, 2, 0, Math.PI*2);
          ctx.fill();
        }
      }
    }
  }
  
  // Draw player with enhanced visuals
  const ppx = G.playerX * TILE_SIZE + TILE_SIZE/2;
  const ppy = G.playerY * TILE_SIZE + TILE_SIZE/2;
  
  // Player outer glow (pulsing)
  const glowSize = 30 + Math.sin(time * 4) * 5;
  const glowGrad = ctx.createRadialGradient(ppx, ppy, 0, ppx, ppy, glowSize);
  glowGrad.addColorStop(0, 'rgba(91,141,239,0.3)');
  glowGrad.addColorStop(0.5, 'rgba(91,141,239,0.1)');
  glowGrad.addColorStop(1, 'rgba(91,141,239,0)');
  ctx.fillStyle = glowGrad;
  ctx.beginPath();
  ctx.arc(ppx, ppy, glowSize, 0, Math.PI*2);
  ctx.fill();
  
  // Player body (diamond shape with inner detail)
  const playerPulse = 1 + Math.sin(time * 5) * 0.05;
  ctx.save();
  ctx.translate(ppx, ppy);
  ctx.scale(playerPulse, playerPulse);
  
  // Outer diamond
  ctx.fillStyle = '#5b8def';
  ctx.beginPath();
  ctx.moveTo(0, -14);
  ctx.lineTo(12, 0);
  ctx.lineTo(0, 14);
  ctx.lineTo(-12, 0);
  ctx.closePath();
  ctx.fill();
  
  // Inner diamond
  ctx.fillStyle = '#a0c4ff';
  ctx.beginPath();
  ctx.moveTo(0, -8);
  ctx.lineTo(6, 0);
  ctx.lineTo(0, 8);
  ctx.lineTo(-6, 0);
  ctx.closePath();
  ctx.fill();
  
  // Core glow
  ctx.fillStyle = '#fff';
  ctx.beginPath();
  ctx.arc(0, 0, 3, 0, Math.PI*2);
  ctx.fill();
  
  ctx.restore();
  
  // Draw companion creature next to player (if party has creatures)
  if (G.party.length > 0) {
    const comp = G.party[0];
    const compX = ppx + 25;
    const compY = ppy + 10;
    const compBob = Math.sin(time * 3) * 3;
    
    ctx.fillStyle = TYPE_COLORS[comp.types[0]] || '#fff';
    ctx.globalAlpha = 0.6;
    drawCreatureShape(ctx, compX, compY + compBob, comp.types[0], 12);
    ctx.globalAlpha = 1;
  }
  
  ctx.restore();
  
  // Zone name indicator at bottom
  ctx.fillStyle = 'rgba(0,0,0,0.3)';
  ctx.fillRect(cw/2 - 80, ch - 40, 160, 30);
  ctx.strokeStyle = 'rgba(91,141,239,0.3)';
  ctx.lineWidth = 1;
  ctx.strokeRect(cw/2 - 80, ch - 40, 160, 30);
  
  ctx.font = '12px sans-serif';
  ctx.textAlign = 'center';
  ctx.fillStyle = '#9ba8c4';
  ctx.fillText(zone.name, cw/2, ch - 20);
  
  // Ambient particles for zone
  if (Math.random() < 0.15) {
    const rx = Math.random() * cw;
    const ry = Math.random() * ch;
    spawnParticles(rx, ry, 1, zone.particles.color, zone.particles.speed*2, 60+Math.random()*40|0, zone.particles.size);
  }
}

// ── function: advanceZone ──
function advanceZone() {
  if (G.zoneIndex < ZONES.length - 1) {
    G.zoneIndex++;
    currentMap = generateZoneMap(G.zoneIndex);
    G.playerX = 3; G.playerY = MAP_H - 3;
    
    // Story vignette check
    const nextStage = Math.min(G.storyStage + 1, STORY_VIGNETTES.length - 1);
    if (nextStage > G.storyStage) {
      G.storyStage = nextStage;
      showVignette(STORY_VIGNETTES[G.storyStage]);
    } else {
      notify('Entered ' + ZONES[G.zoneIndex].name);
    }
    
    updateHUD();
  }
}

// ── function: enterShrine ──
function enterShrine() {
  G.lastShrineZone = G.zoneIndex;
  showScreen('shrine');
}

// ── function: startGuardianBattle ──
function startGuardianBattle() {
  const zone = ZONES[G.zoneIndex];
  if (G.guardiansDefeated.includes(G.zoneIndex)) {
    notify('This Guardian has already been defeated.');
    return;
  }
  
  const gData = GUARDIANS[zone.guardianId];
  const enemy = createCreature(gData.baseId, zone.guardianLevel);
  if (!enemy) return;
  
  G.battleState = {
    type: 'guardian',
    enemies: [enemy],
    party: [...G.party.filter(c => c.hp > 0)],
    activePartyIdx: 0,
    turnOrder: [],
    currentTurnIdx: 0,
    phase: 'select', // select, animate, capture, end
    selectedAbility: null,
    log: ['A Guardian blocks your path!'],
    zoneIndex: G.zoneIndex
  };
  
  calculateTurnOrder();
  showScreen('battle');
}

// ── function: startWildBattle ──
function startWildBattle() {
  const zone = ZONES[G.zoneIndex];
  const zoneIdx = G.zoneIndex;
  
  // Pick random creature from this zone or earlier zones
  const availableIds = Object.keys(CREATURES);
  const zoneCreatures = [];
  
  // Map creatures to zones roughly
  const zoneCreatureMap = [
    ['ignisprite','cindermaw','pyrefox'],
    ['glaciborn','iceclaw','snowdrift'],
    ['stoneshard','geodebug','rockfinch'],
    ['voltwing','thunderbeast','zapfly'],
    ['tidewisp','coralhulk','mistcrawler'],
    ['prismite','voidling','aethermoth']
  ];
  
  const pool = [];
  for (let z = 0; z <= Math.min(zoneIdx, zoneCreatureMap.length-1); z++) {
    pool.push(...zoneCreatureMap[z]);
  }
  
  const baseId = pool[Math.random()*pool.length|0];
  const levelRange = G.postGame ? [Math.max(5, zoneIdx*6), Math.min(MAX_LEVEL, zoneIdx*8+10)] : [Math.max(2, zoneIdx*3), Math.min(40, zoneIdx*5+5)];
  const level = levelRange[0] + Math.random()*(levelRange[1]-levelRange[0])|0;
  
  const enemyCount = Math.random() < 0.3 ? (2 + Math.random()*2|0) : 1;
  const enemies = [];
  for (let i = 0; i < Math.min(enemyCount, 3); i++) {
    const eId = pool[Math.random()*pool.length|0];
    const eLevel = Math.max(1, level + (Math.random()*4-2)|0);
    enemies.push(createCreature(eId, eLevel));
  }
  
  G.battleState = {
    type: 'wild',
    enemies,
    party: [...G.party.filter(c => c.hp > 0)],
    activePartyIdx: 0,
    turnOrder: [],
    currentTurnIdx: 0,
    phase: 'select',
    selectedAbility: null,
    log: ['Wild Luminaries appear!'],
    zoneIndex: G.zoneIndex
  };
  
  calculateTurnOrder();
  showScreen('battle');
}

// ── function: startTutorialBattle ──
function startTutorialBattle() {
  const enemy = createCreature('ignisprite', 3);
  G.battleState = {
    type: 'tutorial',
    enemies: [enemy],
    party: [...G.party.filter(c => c.hp > 0)],
    activePartyIdx: 0,
    turnOrder: [],
    currentTurnIdx: 0,
    phase: 'select',
    selectedAbility: null,
    log: ['A wild Ignisprite appears! Use an ability to attack.'],
    zoneIndex: G.zoneIndex
  };
  calculateTurnOrder();
  showScreen('battle');
}

// ── function: calculateTurnOrder ──
function calculateTurnOrder() {
  const bs = G.battleState;
  if (!bs) return;
  
  const allUnits = [];
  bs.party.forEach((c, i) => {
    if (c.hp > 0) allUnits.push({ type: 'party', idx: i, spd: getEffectiveSpd(c), creature: c });
  });
  bs.enemies.forEach((e, i) => {
    if (e.hp > 0) allUnits.push({ type: 'enemy', idx: i, spd: getEffectiveSpd(e), creature: e });
  });
  
  // Sort by SPD descending, then priority
  allUnits.sort((a, b) => b.spd - a.spd);
  bs.turnOrder = allUnits;
  bs.currentTurnIdx = 0;
}

// ── function: getActiveCreature ──
function getActiveCreature() {
  const bs = G.battleState;
  if (!bs || !bs.turnOrder[bs.currentTurnIdx]) return null;
  const unit = bs.turnOrder[bs.currentTurnIdx];
  return unit.type === 'party' ? bs.party[unit.idx] : bs.enemies[unit.idx];
}

// ── function: getActiveUnit ──
function getActiveUnit() {
  const bs = G.battleState;
  if (!bs || !bs.turnOrder[bs.currentTurnIdx]) return null;
  return bs.turnOrder[bs.currentTurnIdx];
}

// ── let: battleAnimQueue ──
// Battle Combat Resolution
let battleAnimQueue = [];

// ── let: battleAnimating ──
let battleAnimating = false;

// ── function: executePlayerTurn ──
function executePlayerTurn(abilityId) {
  const bs = G.battleState;
  if (!bs || bs.phase !== 'select') return;
  
  const unit = getActiveUnit();
  if (!unit || unit.type !== 'party') return;
  
  const attacker = unit.creature;
  const ab = getAbility(abilityId);
  if (!ab) return;
  
  // Check PP
  const usedCount = (attacker.abilitiesUsed ? attacker.abilitiesUsed[abilityId] || 0 : 0);
  if (!attacker.abilitiesUsed) attacker.abilitiesUsed = {};
  if (usedCount >= ab.pp) {
    bs.log.push('Not enough energy!');
    renderBattleUI();
    return;
  }
  attacker.abilitiesUsed[abilityId]++;
  
  bs.phase = 'animate';
  
  // Handle non-damage abilities
  if (ab.power === 0) {
    executeSupportAbility(attacker, ab);
    endTurn();
    return;
  }
  
  // Pick target (first alive enemy for simplicity, or random)
  const aliveEnemies = bs.enemies.filter(e => e.hp > 0);
  if (aliveEnemies.length === 0) { endBattle(true); return; }
  
  const targets = ab.hits ? aliveEnemies : [aliveEnemies[Math.random()*aliveEnemies.length|0]];
  
  let totalDmg = 0;
  let hitCount = ab.hits || 1;
  
  // Animate attack
  battleAnimQueue.push({ type: 'attack', attacker, ability: ab });
  
  setTimeout(() => {
    for (let h = 0; h < hitCount; h++) {
      const target = targets[Math.random()*targets.length|0];
      
      if (!target || target.hp <= 0) continue;
      
      // Accuracy check
      if (Math.random() * 100 > ab.acc) {
        bs.log.push(`${attacker.name}'s ${ab.name} missed!`);
        renderBattleUI();
        continue;
      }
      
      const result = calcDamage(attacker, target, abilityId);
      let dmg = result.damage;
      
      // Barrier deflect
      if (target.statusEffect === 'barrier') {
        bs.log.push(`${target.name}'s barrier deflected the attack!`);
        target.statusEffect = 'none';
        renderBattleUI();
        battleAnimQueue.push({ type: 'deflect', target });
        continue;
      }
      
      target.hp = Math.max(0, target.hp - dmg);
      totalDmg += dmg;
      
      // Visual feedback
      const effText = result.effective > 1 ? ' Super effective!' : result.effective < 1 ? ' Not very effective...' : '';
      bs.log.push(`${attacker.name} used ${ab.name}!${result.crit ? ' Critical hit!' : ''}${effText}`);
      
      battleAnimQueue.push({ type: 'hit', target, damage: dmg, crit: result.crit });
      
      if (result.effective > 1) {
        triggerShake(4, 8);
        spawnParticles(w/2 + Math.random()*100-50, h/3, 15, TYPE_COLORS[ab.type], 4, 30, 4);
      } else if (result.effective < 1) {
        triggerShake(2, 4);
      } else {
        triggerShake(3, 6);
      }
      
      // Apply status effect
      if (ab.effect !== 'none' && Math.random() < ab.effectChance && target.hp > 0) {
        applyStatusEffect(target, ab.effect);
        bs.log.push(`${target.name} was afflicted with ${ab.effect}!`);
      }
      
      // Self-heal effects
      if (ab.effect === 'regen' || ab.effect === 'atk_boost' || ab.effect === 'def_boost' || ab.effect === 'speed_boost') {
        applyStatusEffect(attacker, ab.effect);
        bs.log.push(`${attacker.name}'s ${ab.effect.replace('_',' ')} increased!`);
      }
      
      if (target.hp <= 0) {
        bs.log.push(`${target.name} fainted!`);
        battleAnimQueue.push({ type: 'faint', target });
      }
    }
    
    renderBattleUI();
    
    // Check if all enemies defeated
    const alive = bs.enemies.filter(e => e.hp > 0);
    if (alive.length === 0) {
      setTimeout(() => endBattle(true), 800);
    } else {
      setTimeout(() => endTurn(), 600);
    }
  }, 400);
  
  sfxAttack();
}

// ── function: executeSupportAbility ──
function executeSupportAbility(attacker, ab) {
  switch(ab.effect) {
    case 'barrier':
      applyStatusEffect(attacker, 'barrier', 1);
      G.battleState.log.push(`${attacker.name} raised a barrier!`);
      break;
    case 'regen':
      const healAmt = Math.floor(attacker.maxHp * 0.3);
      attacker.hp = Math.min(attacker.maxHp, attacker.hp + healAmt);
      G.battleState.log.push(`${attacker.name} healed ${healAmt} HP!`);
      break;
    case 'atk_boost':
      attacker.statBoosts.atk = (attacker.statBoosts.atk || 0) + 1;
      G.battleState.log.push(`${attacker.name}'s attack rose!`);
      break;
    case 'def_boost':
      attacker.statBoosts.def = (attacker.statBoosts.def || 0) + 1;
      G.battleState.log.push(`${attacker.name}'s defense rose!`);
      break;
    case 'speed_boost':
      attacker.statBoosts.spd = (attacker.statBoosts.spd || 0) + 1;
      G.battleState.log.push(`${attacker.name}'s speed rose!`);
      break;
  }
}

// ── function: executeEnemyTurn ──
function executeEnemyTurn() {
  const bs = G.battleState;
  if (!bs) return;
  
  const unit = getActiveUnit();
  if (!unit || unit.type !== 'enemy') return;
  
  const attacker = unit.creature;
  
  // Check skip (shock)
  if (shouldSkipTurn(attacker)) {
    bs.log.push(`${attacker.name} is paralyzed and can't move!`);
    renderBattleUI();
    setTimeout(() => endTurn(), 500);
    return;
  }
  
  // Pick random ability
  const usableAbilities = attacker.abilities.filter(aid => {
    const ab = getAbility(aid);
    if (!ab) return false;
    const used = (attacker.abilitiesUsed ? attacker.abilitiesUsed[aid] || 0 : 0);
    return used < ab.pp;
  });
  
  if (usableAbilities.length === 0) {
    bs.log.push(`${attacker.name} has no energy left!`);
    renderBattleUI();
    setTimeout(() => endTurn(), 500);
    return;
  }
  
  const abId = usableAbilities[Math.random()*usableAbilities.length|0];
  if (!attacker.abilitiesUsed) attacker.abilitiesUsed = {};
  attacker.abilitiesUsed[abId]++;
  
  const ab = getAbility(abId);
  
  // Target a random alive party member
  const aliveParty = bs.party.filter(c => c.hp > 0);
  if (aliveParty.length === 0) { endBattle(false); return; }
  
  const target = aliveParty[Math.random()*aliveParty.length|0];
  
  battleAnimQueue.push({ type: 'attack', attacker, ability: ab });
  
  setTimeout(() => {
    if (ab.power > 0 && Math.random() * 100 <= ab.acc) {
      const result = calcDamage(attacker, target, abId);
      
      if (target.statusEffect === 'barrier') {
        bs.log.push(`${target.name}'s barrier deflected the attack!`);
        target.statusEffect = 'none';
        battleAnimQueue.push({ type: 'deflect', target });
      } else {
        target.hp = Math.max(0, target.hp - result.damage);
        bs.log.push(`${attacker.name} used ${ab.name}!${result.crit ? ' Critical hit!' : ''}`);
        
        battleAnimQueue.push({ type: 'hit', target, damage: result.damage, crit: result.crit });
        triggerShake(result.crit ? 6 : 3, result.crit ? 10 : 6);
        
        if (ab.effect !== 'none' && Math.random() < ab.effectChance) {
          applyStatusEffect(target, ab.effect);
          bs.log.push(`${target.name} was afflicted with ${ab.effect}!`);
        }
      }
      
      if (target.hp <= 0) {
        bs.log.push(`${target.name} fainted!`);
        battleAnimQueue.push({ type: 'faint', target });
        
        // Check party wipe
        const alive = bs.party.filter(c => c.hp > 0);
        if (alive.length === 0) {
          setTimeout(() => endBattle(false), 800);
          return;
        }
      }
    } else if (ab.power === 0) {
      executeSupportAbility(attacker, ab);
    } else {
      bs.log.push(`${attacker.name}'s ${ab.name} missed!`);
    }
    
    renderBattleUI();
    setTimeout(() => endTurn(), 600);
  }, 400);
  
  sfxAttack();
}

// ── function: endTurn ──
function endTurn() {
  const bs = G.battleState;
  if (!bs) return;
  
  // Process status effects for all creatures
  [...bs.party, ...bs.enemies].forEach(c => processStatusEffects(c));
  
  // Recalculate turn order
  calculateTurnOrder();
  
  // Find next alive unit
  while (bs.currentTurnIdx < bs.turnOrder.length) {
    const unit = bs.turnOrder[bs.currentTurnIdx];
    if (unit && unit.creature.hp > 0) break;
    bs.currentTurnIdx++;
  }
  
  if (bs.currentTurnIdx >= bs.turnOrder.length) {
    // Reset turn order for new round
    calculateTurnOrder();
  }
  
  const nextUnit = getActiveUnit();
  if (!nextUnit) { endBattle(false); return; }
  
  if (nextUnit.type === 'party') {
    bs.phase = 'select';
    renderBattleUI();
  } else {
    bs.phase = 'animate';
    setTimeout(() => executeEnemyTurn(), 500);
  }
}

// ── function: endBattle ──
function endBattle(victory) {
  const bs = G.battleState;
  if (!bs) return;
  
  if (victory) {
    sfxVictory();
    
    // EXP and rewards
    let totalExp = 0;
    bs.enemies.forEach(e => {
      totalExp += Math.floor(e.level * 2.5);
    });
    
    bs.party.forEach(c => {
      if (c.hp > 0) {
        const expGain = Math.floor(totalExp / bs.party.filter(p=>p.hp>0).length);
        c.exp = (c.exp || 0) + expGain;
        
        // Level up check
        while ((c.exp || 0) >= c.level * 15 && c.level < MAX_LEVEL) {
          c.exp -= c.level * 15;
          c.level++;
          
          // Stat growth
          const data = getCreatureData(c.baseId);
          if (data) {
            for (const key of ['hp','atk','def','spd']) {
              c[key] += Math.floor(data.baseStats[key] * 0.04);
            }
            c.maxHp = c.hp + Math.floor(data.baseStats.hp * 0.04);
          }
          
          // Learn new abilities
          for (const [lvl, abId] of Object.entries(data.learnset || {})) {
            if (parseInt(lvl) === c.level && !c.abilities.includes(abId)) {
              c.abilities.push(abId);
              bs.log.push(`${c.name} learned ${getAbility(abId)?.name || abId}!`);
            }
          }
          
          sfxLevelUp();
        }
      }
    });
    
    G.gold += 10 + Math.floor(Math.random() * 20);
    G.totalBattlesWon++;
    
    // Guardian defeat bonus
    if (bs.type === 'guardian') {
      G.guardiansDefeated.push(bs.zoneIndex);
      const zone = ZONES[bs.zoneIndex];
      
      // Apply permanent bonus
      const bonusType = zone.unlockBonus;
      G.party.forEach(c => {
        if (bonusType.includes('ATK')) c.atk += 5;
        if (bonusType.includes('DEF')) c.def += 5;
        if (bonusType.includes('SPD')) c.spd += 5;
        if (bonusType.includes('HP')) { c.hp += 5; c.maxHp += 5; }
        if (bonusType.includes('all stats')) { c.atk += 5; c.def += 5; c.spd += 5; c.maxHp += 5; c.hp = Math.min(c.maxHp, c.hp + 5); }
      });
      
      // Drop catalyst
      const catalysts = ['ember_catalyst','frost_catalyst','stone_catalyst','spark_catalyst','wave_catalyst'];
      const catId = catalysts[bs.zoneIndex] || 'prism_shard';
      G.inventory[catId] = (G.inventory[catId] || 0) + 1;
      
      // Check post-game condition
      if (G.guardiansDefeated.length >= 4 && !G.postGame) {
        G.postGame = true;
        bs.log.push('The Anchors weaken! The Expanse shifts...');
      }
    }
    
    // Capture opportunity for wild battles
    if (bs.type === 'wild') {
      const aliveEnemies = bs.enemies.filter(e => e.hp > 0);
      if (aliveEnemies.length > 0 && G.party.length < PARTY_SIZE) {
        setTimeout(() => startCapture(aliveEnemies[0]), 500);
        return;
      }
    }
    
    // Tutorial progression
    if (bs.type === 'tutorial') {
      G.tutorialStep = Math.min(G.tutorialStep + 1, TUTORIAL_STEPS.length);
      if (G.tutorialStep < TUTORIAL_STEPS.length) {
        setTimeout(() => showTutorial(TUTORIAL_STEPS[G.tutorialStep]), 500);
        return;
      } else {
        G.tutorialStep = 4; // Complete
      }
    }
    
    bs.log.push(`Victory! Earned ${totalExp} EXP and some gold.`);
    renderBattleUI();
    
    setTimeout(() => {
      showScreen('overworld');
      updateHUD();
    }, 1500);
    
  } else {
    sfxDefeat();
    bs.log.push('Your party was defeated... Returning to last shrine.');
    renderBattleUI();
    
    // Heal party and return to shrine
    G.party.forEach(c => c.hp = Math.floor(c.maxHp * 0.5));
    
    setTimeout(() => {
      showScreen('overworld');
      G.playerX = 3; G.playerY = MAP_H - 2;
      notify('Party restored at the last shrine.');
    }, 1500);
  }
}

// ── function: switchCreature ──
function switchCreature(idx) {
  const bs = G.battleState;
  if (!bs || bs.phase !== 'select') return;
  
  const unit = getActiveUnit();
  if (unit && unit.type === 'enemy') return; // Can't switch during enemy turn
  
  const target = bs.party[idx];
  if (!target || target.hp <= 0) {
    notify('That Luminary has fainted!');
    return;
  }
  
  bs.activePartyIdx = idx;
  bs.log.push(`Switched to ${target.name}!`);
  renderBattleUI();
}

// ── function: runAway ──
function runAway() {
  const bs = G.battleState;
  if (!bs || bs.type === 'guardian' || bs.type === 'tutorial') {
    notify('Cannot flee from this battle!');
    return;
  }
  
  if (Math.random() < 0.7) {
    bs.log.push('Got away safely!');
    renderBattleUI();
    setTimeout(() => { showScreen('overworld'); updateHUD(); }, 500);
  } else {
    bs.log.push('Failed to escape!');
    renderBattleUI();
    setTimeout(() => endTurn(), 500);
  }
}

// ── let: captureState ──
// Capture Mini-Game
let captureState = { active: false, indicatorPos: 0, direction: 1, speed: 2, zoneStart: 35, zoneEnd: 65, attempts: 0, maxAttempts: 3, target: null };

// ── function: startCapture ──
function startCapture(enemy) {
  const bs = G.battleState;
  if (!bs) return;
  
  captureState = {
    active: true,
    indicatorPos: 0,
    direction: 1,
    speed: 2 + (1 - enemy.hp / enemy.maxHp) * 3, // Faster when more HP remains
    zoneStart: 40 - (1 - enemy.hp / enemy.maxHp) * 15, // Zone shrinks with higher HP
    zoneEnd: 60 + (1 - enemy.hp / enemy.maxHp) * 15,
    attempts: 0,
    maxAttempts: 3,
    target: enemy
  };
  
  if (captureState.zoneStart < 10) captureState.zoneStart = 10;
  if (captureState.zoneEnd > 90) captureState.zoneEnd = 90;
  
  document.getElementById('captureTargetName').textContent = `${enemy.name} (Lv.${enemy.level})`;
  showScreen('capture');
  sfxMenu();
}

// ── function: attemptCapture ──
function attemptCapture() {
  if (!captureState.active) return;
  
  captureState.attempts++;
  
  const inZone = captureState.indicatorPos >= captureState.zoneStart && captureState.indicatorPos <= captureState.zoneEnd;
  
  // Calculate success chance
  let baseChance = 0.3 + (1 - captureState.target.hp / captureState.target.maxHp) * 0.5;
  
  // Orb tier bonus
  const orbTiers = ['aether_orb_t1', 'aether_orb_t2', 'aether_orb_t3'];
  for (const orbId of orbTiers) {
    if ((G.inventory[orbId] || 0) > 0) {
      baseChance += orbId === 'aether_orb_t1' ? 0.1 : orbId === 'aether_orb_t2' ? 0.2 : 0.35;
      G.inventory[orbId]--;
      if (G.inventory[orbId] <= 0) delete G.inventory[orbId];
      break;
    }
  }
  
  const success = inZone && Math.random() < baseChance;
  
  if (success) {
    captureState.active = false;
    
    // Add to party
    const captured = createCreature(captureState.target.baseId, captureState.target.level);
    if (captured) {
      G.party.push(captured);
      G.totalCaptured++;
      
      // Track discovery
      if (!G.discoveredCreatures.includes(captureState.target.baseId)) {
        G.discoveredCreatures.push(captureState.target.baseId);
      }
    }
    
    sfxCapture();
    notify(`${captureState.target.name} captured!`);
    
    setTimeout(() => {
      const bs = G.battleState;
      if (bs) {
        // Remove captured enemy from battle
        const idx = bs.enemies.indexOf(captureState.target);
        if (idx >= 0) bs.enemies.splice(idx, 1);
        
        if (bs.enemies.length === 0) {
          endBattle(true);
        } else {
          showScreen('battle');
          renderBattleUI();
        }
      }
    }, 800);
    
  } else {
    // Show result
    const attemptsDiv = document.getElementById('captureAttempts');
    const dot = document.createElement('span');
    dot.style.cssText = `display:inline-block;width:12px;height:12px;border-radius:50%;margin:4px;${inZone ? 'background:#ef5b5b' : 'background:#9ba8c4'}`;
    dot.title = inZone ? 'In zone but failed!' : 'Missed the zone!';
    attemptsDiv.appendChild(dot);
    
    if (captureState.attempts >= captureState.maxAttempts) {
      captureState.active = false;
      notify('Capture failed. The Luminary escaped.');
      
      setTimeout(() => {
        const bs = G.battleState;
        if (bs) endBattle(true); // Still get EXP for defeating it
      }, 800);
    } else {
      // Reset indicator for next attempt
      captureState.indicatorPos = Math.random() * 100;
      captureState.direction = Math.random() < 0.5 ? 1 : -1;
    }
    
    renderCaptureUI();
  }
}

// ── function: updateCaptureGame ──
function updateCaptureGame() {
  if (!captureState.active) return;
  
  captureState.indicatorPos += captureState.direction * captureState.speed;
  if (captureState.indicatorPos >= 100) { captureState.indicatorPos = 100; captureState.direction = -1; }
  if (captureState.indicatorPos <= 0) { captureState.indicatorPos = 0; captureState.direction = 1; }
  
  renderCaptureUI();
}

// ── function: renderCaptureUI ──
function renderCaptureUI() {
  const zone = document.getElementById('captureZone');
  const indicator = document.getElementById('captureIndicator');
  
  if (zone) {
    zone.style.left = captureState.zoneStart + '%';
    zone.style.width = (captureState.zoneEnd - captureState.zoneStart) + '%';
  }
  if (indicator) {
    indicator.style.left = `calc(${captureState.indicatorPos}% - 4px)`;
  }
}

// ── const: STARTERS ──
const STARTERS = [
  { id:'ignisprite', desc:'A spark of living flame. Strong attacker, moderate speed.', color: TYPE_COLORS.ember },
  { id:'glaciborn', desc:'Born from ancient ice. Balanced stats with defensive prowess.', color: TYPE_COLORS.frost },
  { id:'voltwing', desc:'Wings crackling with lightning. Fast and powerful striker.', color: TYPE_COLORS.spark }
];

// ── function: showStarterScreen ──
function showStarterScreen() {
  const list = document.getElementById('starterList');
  if (!list) return;
  
  let html = '';
  STARTERS.forEach(s => {
    const data = getCreatureData(s.id);
    html += `<div onclick="selectStarter('${s.id}')" style="background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);border-left:3px solid ${s.color};border-radius:8px;padding:14px;margin-bottom:8px;cursor:pointer;display:flex;justify-content:space-between;align-items:center;min-height:60px;">
      <div>
        <strong style="color:${s.color};font-size:15px;">${data?.name || s.id}</strong><br>
        <small style="color:#9ba8c4;font-size:12px;">${s.desc}</small><br>
        <small style="color:#9ba8c4;font-size:11px;">HP:${data?.baseStats.hp} ATK:${data?.baseStats.atk} DEF:${data?.baseStats.def} SPD:${data?.baseStats.spd}</small>
      </div>
      <span class="type-badge type-${data?.types[0]}">${data?.types[0]}</span>
    </div>`;
  });
  
  list.innerHTML = html;
  showScreen('starter');
}

// ── function: selectStarter ──
function selectStarter(id) {
  sfxMenu();
  
  const starter = createCreature(id, 5);
  if (starter) {
    G.party.push(starter);
    G.discoveredCreatures.push(id);
  }
  
  // Give some starting items
  G.inventory.aether_orb_t1 = 3;
  G.inventory.vital_vial = 2;
  
  showScreen('overworld');
  updateHUD();
  
  // Start tutorial after a brief delay
  setTimeout(() => {
    showTutorial(TUTORIAL_STEPS[0]);
    G.tutorialStep = 1;
  }, 300);
}

// ── function: showScreen ──
function showScreen(name) {
  document.querySelectorAll('.screen').forEach(s => { s.classList.remove('active'); s.classList.add('hidden'); });
  
  const screenMap = {
    'title': 'titleScreen',
    'starter': 'starterScreen',
    'overworld': 'overworldHUD',
    'battle': 'battleScreen',
    'capture': 'captureScreen',
    'menu': 'menuOverlay',
    'shrine': 'shrineScreen',
    'shop': 'shopScreen',
    'team': 'teamScreen',
    'bag': 'bagScreen',
    'codex': 'codexScreen',
    'map': 'mapScreen',
    'settings': 'settingsScreen',
    'dialogue': 'dialogueBox',
    'vignette': 'storyVignette',
    'tutorial': 'tutorialOverlay',
    'victory': 'victoryScreen'
  };
  
  const el = document.getElementById(screenMap[name]);
  if (el) {
    el.classList.add('active');
    el.classList.remove('hidden');
    G.screen = name;
  }
}

// ── function: updateHUD ──
function updateHUD() {
  const zoneName = document.getElementById('zoneName');
  const goldDisp = document.getElementById('goldDisplay');
  const shardCount = document.getElementById('shardCount');
  
  if (zoneName) zoneName.textContent = ZONES[G.zoneIndex]?.name || '';
  if (goldDisp) goldDisp.textContent = `✦ ${G.gold}`;
  if (shardCount) shardCount.textContent = `◇ ${G.shards}`;
}

// ── function: notify ──
function notify(msg, duration=2000) {
  const el = document.getElementById('notification');
  if (!el) return;
  el.textContent = msg;
  el.classList.remove('hidden');
  setTimeout(() => el.classList.add('hidden'), duration);
}

// ── function: renderBattleUI ──
function renderBattleUI() {
  const bs = G.battleState;
  if (!bs) return;
  
  // Enemy area
  const enemyArea = document.getElementById('enemyArea');
  if (enemyArea) {
    let html = '';
    bs.enemies.forEach((e, i) => {
      const hpPct = Math.max(0, e.hp / e.maxHp * 100);
      const hpClass = hpPct < 25 ? 'low' : hpPct < 50 ? 'mid' : '';
      const isCurrentTarget = getActiveUnit()?.type === 'enemy' && getActiveUnit()?.idx === i;
      
      html += `<div class="creature-card ${e.hp <= 0 ? 'fainted' : ''} ${isCurrentTarget ? 'active-turn' : ''}" style="display:inline-block;margin:4px;">
        <div class="creature-name"><span>${e.name}</span><span>Lv.${e.level}</span></div>
        <div class="hp-bar-bg"><div class="hp-bar-fill ${hpClass}" style="width:${hpPct}%"></div></div>
        <div class="creature-stats">
          <span>HP: ${Math.max(0,e.hp)}/${e.maxHp}</span>
          <span>${e.types.map(t=>`<span class="type-badge type-${t}">${t}</span>`).join(' ')}</span>
          ${e.statusEffect !== 'none' ? `<span style="color:${getStatusColor(e.statusEffect)}">${getStatusIcon(e.statusEffect)}</span>` : ''}
        </div>
      </div>`;
    });
    enemyArea.innerHTML = html;
  }
  
  // Party area
  const partyArea = document.getElementById('partyArea');
  if (partyArea) {
    let html = '';
    bs.party.forEach((c, i) => {
      const hpPct = Math.max(0, c.hp / c.maxHp * 100);
      const hpClass = hpPct < 25 ? 'low' : hpPct < 50 ? 'mid' : '';
      const isActive = getActiveUnit()?.type === 'party' && getActiveUnit()?.idx === i;
      
      html += `<div class="creature-card ${c.hp <= 0 ? 'fainted' : ''} ${isActive ? 'active-turn' : ''}" onclick="switchCreature(${i})" style="display:inline-block;margin:4px;">
        <div class="creature-name"><span>${c.name}</span><span>Lv.${c.level}</span></div>
        <div class="hp-bar-bg"><div class="hp-bar-fill ${hpClass}" style="width:${hpPct}%"></div></div>
        <div class="creature-stats">
          <span>HP: ${Math.max(0,c.hp)}/${c.maxHp}</span>
          <span>${c.statusEffect !== 'none' ? getStatusIcon(c.statusEffect)+' '+c.statusEffect : ''}</span>
        </div>
      </div>`;
    });
    partyArea.innerHTML = html;
  }
  
  // Battle menu
  const battleMenu = document.getElementById('battleMenu');
  const menuOptions = document.getElementById('menuOptions');
  const abilityList = document.getElementById('abilityList');
  
  if (bs.phase === 'select') {
    const unit = getActiveUnit();
    if (unit && unit.type === 'party') {
      battleMenu.classList.remove('hidden');
      
      const creature = unit.creature;
      let abHtml = '';
      creature.abilities.forEach(abId => {
        const ab = getAbility(abId);
        if (!ab) return;
        const used = (creature.abilitiesUsed ? creature.abilitiesUsed[abId] || 0 : 0);
        const remaining = Math.max(0, ab.pp - used);
        
        let effectText = '';
        if (ab.hits) effectText += ` ${ab.hits}x`;
        if (ab.effect !== 'none' && ab.power > 0) effectText += ` [${ab.effect}]`;
        if (ab.priority) effectText += ` ⚡`;
        
        abHtml += `<button class="ability-btn" data-type="${ab.type}" onclick="executePlayerTurn('${abId}')">
          <span class="ab-name">${ab.name}</span>
          <span class="ab-info">${ab.type} | Pwr:${ab.power || '-'} | Acc:${ab.acc}% | PP:${remaining}/${ab.pp}${effectText}</span>
        </button>`;
      });
      
      menuOptions.innerHTML = `<button class="glass-btn small" onclick="showAbilities()">⚔ Fight</button>
        <button class="glass-btn small" onclick="runAway()">🏃 Flee</button>`;
      
      abilityList.innerHTML = abHtml;
      abilityList.classList.add('hidden');
    } else {
      battleMenu.classList.add('hidden');
    }
  } else {
    battleMenu.classList.add('hidden');
  }
  
  // Battle log
  const logEl = document.getElementById('battleLog');
  if (logEl) {
    logEl.textContent = bs.log[bs.log.length - 1] || '';
  }
}

// ── function: showAbilities ──
function showAbilities() {
  const abilityList = document.getElementById('abilityList');
  const menuOptions = document.getElementById('menuOptions');
  if (abilityList) {
    abilityList.classList.toggle('hidden');
  }
}

// ── let: dialogueQueue ──
// Dialogue & Vignette
let dialogueQueue = [];

// ── function: showDialogue ──
function showDialogue(text, callback) {
  dialogueQueue = [text];
  showScreen('dialogue');
  document.getElementById('dialogueText').textContent = text;
  
  const nextBtn = document.getElementById('dialogueNext');
  if (nextBtn) {
    nextBtn.onclick = () => {
      dialogueQueue.shift();
      if (dialogueQueue.length > 0) {
        document.getElementById('dialogueText').textContent = dialogueQueue[0];
      } else {
        showScreen(G.screen === 'battle' ? 'battle' : 'overworld');
        if (callback) callback();
      }
    };
  }
}

// ── function: showVignette ──
function showVignette(vignette) {
  document.getElementById('vignetteTitle').textContent = vignette.title;
  document.getElementById('vignetteText').textContent = vignette.text;
  showScreen('vignette');
  
  const nextBtn = document.getElementById('vignetteNext');
  if (nextBtn) {
    nextBtn.onclick = () => {
      showScreen('overworld');
      updateHUD();
    };
  }
}

// ── function: showTutorial ──
function showTutorial(step) {
  document.getElementById('tutorialTitle').textContent = step.title;
  document.getElementById('tutorialText').textContent = step.text;
  showScreen('tutorial');
  
  const nextBtn = document.getElementById('tutorialNext');
  if (nextBtn) {
    nextBtn.onclick = () => {
      G.tutorialStep++;
      if (G.tutorialStep < TUTORIAL_STEPS.length) {
        showTutorial(TUTORIAL_STEPS[G.tutorialStep]);
      } else {
        G.tutorialStep = 4; // Complete
        showScreen('overworld');
        updateHUD();
      }
    };
  }
}

// ── function: renderTeamScreen ──
// Menu Screens
function renderTeamScreen() {
  const list = document.getElementById('teamList');
  if (!list) return;
  
  let html = '';
  G.party.forEach((c, i) => {
    const hpPct = Math.max(0, c.hp / c.maxHp * 100);
    html += `<div class="team-item" onclick="showCreatureDetail(${i})">
      <div>
        <strong>${c.name}</strong> Lv.${c.level} ${c.isTransformed ? '✦' : ''}
        <br><small style="color:var(--text-secondary)">HP:${c.hp}/${c.maxHp} ATK:${c.atk} DEF:${c.def} SPD:${c.spd}</small>
      </div>
      <div>${c.types.map(t=>`<span class="type-badge type-${t}">${t}</span>`).join(' ')}</div>
    </div>`;
  });
  
  if (G.party.length === 0) html = '<p style="color:var(--text-secondary)">No Luminaries yet. Explore to find some!</p>';
  list.innerHTML = html;
}

// ── function: renderBagScreen ──
function renderBagScreen() {
  const list = document.getElementById('bagList');
  if (!list) return;
  
  let html = '';
  for (const [itemId, count] of Object.entries(G.inventory)) {
    const item = getItem(itemId);
    if (!item) continue;
    html += `<div class="bag-item">
      <div><strong>${item.name}</strong> x${count}<br><small style="color:var(--text-secondary)">${item.desc}</small></div>
    </div>`;
  }
  
  if (!html) html = '<p style="color:var(--text-secondary)">Inventory is empty.</p>';
  list.innerHTML = html;
}

// ── function: renderCodexScreen ──
function renderCodexScreen() {
  const list = document.getElementById('codexList');
  if (!list) return;
  
  let html = '';
  const allIds = [...Object.keys(CREATURES), ...Object.keys(TRANSFORMED)];
  
  allIds.forEach(id => {
    const data = getCreatureData(id);
    if (!data) return;
    const discovered = G.discoveredCreatures.includes(id) || Object.values(CREATURES).some(c => c.transform && c.transform.name === id);
    
    html += `<div class="codex-item" style="${!discovered ? 'opacity:0.4' : ''}">
      <div>
        <strong>${discovered ? data.name : '???'}</strong>
        ${discovered ? `<br><small style="color:var(--text-secondary)">${data.types.map(t=>`<span class="type-badge type-${t}">${t}</span>`).join(' ')}</small>` : ''}
      </div>
    </div>`;
  });
  
  list.innerHTML = html;
}

// ── function: renderShopScreen ──
function renderShopScreen() {
  const list = document.getElementById('shopList');
  const goldEl = document.getElementById('shopGold');
  if (!list) return;
  
  if (goldEl) goldEl.textContent = `✦ Gold: ${G.gold}`;
  
  let html = '';
  ITEMS.forEach(item => {
    const canBuy = G.gold >= item.price;
    html += `<div class="bag-item" onclick="buyItem('${item.id}')" style="${canBuy ? '' : 'opacity:0.4'}">
      <div><strong>${item.name}</strong> — ✦${item.price}<br><small style="color:var(--text-secondary)">${item.desc}</small></div>
    </div>`;
  });
  
  list.innerHTML = html;
}

// ── function: buyItem ──
function buyItem(itemId) {
  const item = getItem(itemId);
  if (!item || G.gold < item.price) return;
  
  G.gold -= item.price;
  G.inventory[itemId] = (G.inventory[itemId] || 0) + 1;
  
  sfxMenu();
  notify(`Bought ${item.name}`);
  renderShopScreen();
}

// ── function: showCreatureDetail ──
function showCreatureDetail(idx) {
  const c = G.party[idx];
  if (!c) return;
  
  document.getElementById('detailName').textContent = `${c.name} Lv.${c.level}`;
  
  const data = getCreatureData(c.baseId);
  let html = `<p style="color:var(--text-secondary);margin-bottom:12px;">${data?.lore || ''}</p>`;
  html += `<div style="display:flex;gap:8px;margin-bottom:12px;">${c.types.map(t=>`<span class="type-badge type-${t}">${t}</span>`).join('')}</div>`;
  html += `<table style="width:100%;font-size:13px;border-collapse:collapse;">`;
  html += `<tr><td>HP</td><td>${c.hp}/${c.maxHp}</td></tr>`;
  html += `<tr><td>ATK</td><td>${c.atk}</td></tr>`;
  html += `<tr><td>DEF</td><td>${c.def}</td></tr>`;
  html += `<tr><td>SPD</td><td>${c.spd}</td></tr>`;
  html += `<tr><td>Status</td><td>${c.statusEffect !== 'none' ? c.statusEffect : 'None'}</td></tr>`;
  html += `</table>`;
  
  // Abilities
  html += `<h3 style="margin-top:16px;">Abilities</h3>`;
  c.abilities.forEach(abId => {
    const ab = getAbility(abId);
    if (ab) html += `<div class="bag-item"><span>${ab.name}</span><small style="color:var(--text-secondary)"> ${ab.type} | Pwr:${ab.power || '-'}</small></div>`;
  });
  
  // Transformation check
  if (data?.transform && !c.isTransformed) {
    const t = data.transform;
    const hasCatalyst = G.inventory[t.catalyst] > 0;
    html += `<h3 style="margin-top:16px;">Transformation</h3>`;
    html += `<p>Can transform into <strong>${t.name}</strong> at Lv.${t.level} with ${getItem(t.catalyst)?.name || t.catalyst}.</p>`;
    
    if (c.level >= t.level && hasCatalyst) {
      html += `<button class="glass-btn primary" onclick="transformCreature(${idx})" style="margin-top:8px;">Transform Now</button>`;
    } else if (c.level < t.level) {
      html += `<p style="color:var(--text-secondary)">Need to reach Lv.${t.level}</p>`;
    } else if (!hasCatalyst) {
      html += `<p style="color:var(--text-secondary)">Missing catalyst</p>`;
    }
  }
  
  document.getElementById('detailContent').innerHTML = html;
  showScreen('creatureDetail');
}

// ── function: transformCreature ──
function transformCreature(idx) {
  const c = G.party[idx];
  if (!c) return;
  
  const data = getCreatureData(c.baseId);
  if (!data?.transform) return;
  
  const t = data.transform;
  if (c.level < t.level || !G.inventory[t.catalyst]) return;
  
  G.inventory[t.catalyst]--;
  if (G.inventory[t.catalyst] <= 0) delete G.inventory[t.catalyst];
  
  // Transform: update stats and abilities
  const newData = TRANSFORMED[t.name];
  if (!newData) return;
  
  c.baseId = t.name;
  c.name = newData.name;
  c.types = [...newData.types];
  c.isTransformed = true;
  c.lore = newData.lore;
  
  // Boost stats significantly
  for (const key of ['hp','atk','def','spd']) {
    const growth = Math.floor((newData.baseStats[key] - getCreatureData(c.baseId === t.name ? Object.keys(CREATURES).find(k => CREATURES[k].transform?.name === t.name) : c.baseId)?.baseStats[key] || 0) * (c.level / 30));
    c[key] += Math.max(5, growth);
  }
  c.maxHp = c.hp + 20;
  
  // Add new abilities from transformed learnset
  for (const [lvl, abId] of Object.entries(newData.learnset || {})) {
    if (c.level >= parseInt(lvl) && !c.abilities.includes(abId)) {
      c.abilities.push(abId);
    }
  }
  
  sfxLevelUp();
  notify(`${c.name} transformed into ${newData.name}!`);
  
  // Track discovery
  if (!G.discoveredCreatures.includes(t.name)) G.discoveredCreatures.push(t.name);
  
  showCreatureDetail(idx);
}

// ── function: renderMapScreen ──
function renderMapScreen() {
  const canvas = document.getElementById('mapCanvas');
  if (!canvas) return;
  
  const ctx = canvas.getContext('2d');
  canvas.width = Math.min(400, window.innerWidth - 60);
  canvas.height = 300;
  
  const zoneW = canvas.width / ZONES.length;
  
  ZONES.forEach((zone, i) => {
    const x = i * zoneW;
    
    // Zone background
    const grad = ctx.createLinearGradient(x, 0, x + zoneW, 300);
    zone.gradient.forEach((c, j) => grad.addColorStop(j / (zone.gradient.length-1), c));
    ctx.fillStyle = grad;
    ctx.fillRect(x+2, 2, zoneW-4, 296);
    
    // Zone name
    ctx.fillStyle = 'rgba(255,255,255,0.8)';
    ctx.font = '11px sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText(zone.name.split(' ').slice(-1)[0], x + zoneW/2, 30);
    
    // Guardian status
    const defeated = G.guardiansDefeated.includes(i);
    ctx.fillStyle = defeated ? '#5bef8d' : '#ef5b5b';
    ctx.font = '16px serif';
    ctx.fillText(defeated ? '✓' : '⚔', x + zoneW/2, 60);
    
    // Current zone indicator
    if (i === G.zoneIndex) {
      ctx.strokeStyle = '#5b8def';
      ctx.lineWidth = 3;
      ctx.strokeRect(x+1, 1, zoneW-2, 298);
      
      ctx.fillStyle = '#5b8def';
      ctx.font = '10px sans-serif';
      ctx.fillText('YOU', x + zoneW/2, 280);
    }
    
    // Unlock line
    if (i > 0 && G.guardiansDefeated.includes(i-1)) {
      ctx.fillStyle = '#5bef8d';
      ctx.font = '14px serif';
      ctx.fillText('→', x - 5, 150);
    }
  });
}

// ── function: saveGame ──
// Save/Load System
function saveGame(slot=0) {
  const saveData = {
    zoneIndex: G.zoneIndex,
    playerX: G.playerX, playerY: G.playerY,
    party: G.party.map(c => ({...c, abilitiesUsed: c.abilitiesUsed || {}})),
    inventory: {...G.inventory},
    gold: G.gold, shards: G.shards,
    guardiansDefeated: [...G.guardiansDefeated],
    storyStage: G.storyStage,
    tutorialStep: G.tutorialStep,
    postGame: G.postGame,
    lastShrineZone: G.lastShrineZone,
    discoveredCreatures: [...G.discoveredCreatures],
    totalBattlesWon: G.totalBattlesWon,
    totalCaptured: G.totalCaptured,
    timestamp: Date.now()
  };
  
  try {
    localStorage.setItem(`aetherbound_save_${slot}`, JSON.stringify(saveData));
    notify('Game saved!');
  } catch(e) { console.log('Save failed:', e); }
}

// ── function: loadGame ──
function loadGame(slot=0) {
  try {
    const data = localStorage.getItem(`aetherbound_save_${slot}`);
    if (!data) { notify('No save found.'); return false; }
    
    const saveData = JSON.parse(data);
    Object.assign(G, saveData);
    
    currentMap = generateZoneMap(G.zoneIndex);
    G.battleState = null;
    
    showScreen('overworld');
    updateHUD();
    notify('Game loaded!');
    return true;
  } catch(e) { console.log('Load failed:', e); return false; }
}

// ── function: hasSave ──
function hasSave() {
  try { return !!localStorage.getItem('aetherbound_save_0'); } catch(e) { return false; }
}

// ── let: w ──
let w = 0;

// ── let: h ──
let h = 0;

// ── let: animEffects ──
let animEffects = [];

// ── function: renderBattleCanvas ──
function renderBattleCanvas(ctx) {
  const bs = G.battleState;
  if (!bs || G.screen !== 'battle') return;
  
  const zone = ZONES[G.zoneIndex] || ZONES[0];
  const grad = ctx.createLinearGradient(0, 0, 0, h);
  zone.gradient.forEach((c, i) => grad.addColorStop(i / (zone.gradient.length-1), c));
  ctx.fillStyle = grad;
  ctx.fillRect(0, 0, w, h);
  
  ctx.save();
  updateScreenShake();
  ctx.translate(screenShake.x, screenShake.y);
  
  bs.enemies.forEach((e, i) => {
    if (e.hp <= 0) return;
    const ex = w * 0.3 + i * 120;
    const ey = h * 0.25;
    const isTarget = getActiveUnit()?.type === 'enemy' && getActiveUnit()?.idx === i;
    
    if (isTarget) {
      const glowGrad = ctx.createRadialGradient(ex, ey, 0, ex, ey, 60);
      glowGrad.addColorStop(0, 'rgba(239,91,91,0.2)');
      glowGrad.addColorStop(1, 'rgba(239,91,91,0)');
      ctx.fillStyle = glowGrad;
      ctx.beginPath();
      ctx.arc(ex, ey, 60, 0, Math.PI*2);
      ctx.fill();
    }
    
    const color = TYPE_COLORS[e.types[0]] || '#fff';
    ctx.fillStyle = color;
    ctx.globalAlpha = e.hp <= 0 ? 0.3 : 0.8;
    drawCreatureShape(ctx, ex, ey, e.types[0], 40);
    ctx.globalAlpha = 1;
    
    if (e.statusEffect !== 'none') {
      ctx.fillStyle = getStatusColor(e.statusEffect);
      ctx.font = '12px sans-serif';
      ctx.textAlign = 'center';
      ctx.fillText(getStatusIcon(e.statusEffect), ex, ey - 50);
    }
  });
  
  bs.party.forEach((c, i) => {
    if (c.hp <= 0) return;
    const cx = w * 0.2 + i * 80;
    const cy = h * 0.55;
    const isActive = getActiveUnit()?.type === 'party' && getActiveUnit()?.idx === i;
    
    if (isActive) {
      const glowGrad = ctx.createRadialGradient(cx, cy, 0, cx, cy, 40);
      glowGrad.addColorStop(0, 'rgba(91,141,239,0.3)');
      glowGrad.addColorStop(1, 'rgba(91,141,239,0)');
      ctx.fillStyle = glowGrad;
      ctx.beginPath();
      ctx.arc(cx, cy, 40, 0, Math.PI*2);
      ctx.fill();
    }
    
    const color = TYPE_COLORS[c.types[0]] || '#fff';
    ctx.fillStyle = color;
    ctx.globalAlpha = c.hp <= 0 ? 0.3 : 0.7;
    drawCreatureShape(ctx, cx, cy, c.types[0], 25);
    ctx.globalAlpha = 1;
    
    if (c.statusEffect !== 'none') {
      ctx.fillStyle = getStatusColor(c.statusEffect);
      ctx.font = '10px sans-serif';
      ctx.textAlign = 'center';
      ctx.fillText(getStatusIcon(c.statusEffect), cx, cy - 35);
    }
  });
  
  particles.forEach(p => {
    if (!p.alive) return;
    ctx.globalAlpha = p.alpha * 0.8;
    ctx.fillStyle = p.color;
    ctx.beginPath();
    ctx.arc(p.x, p.y, p.size * p.alpha, 0, Math.PI*2);
    ctx.fill();
  });
  ctx.globalAlpha = 1;
  
  floatingTexts.forEach(ft => {
    if (!ft.alive) return;
    ctx.globalAlpha = ft.alpha;
    ctx.fillStyle = ft.color;
    ctx.font = `bold ${ft.text.includes('!') ? '18' : '14'}px sans-serif`;
    ctx.textAlign = 'center';
    ctx.fillText(ft.text, ft.x, ft.y);
  });
  ctx.globalAlpha = 1;
  
  renderBattleAnimations(ctx);
  ctx.restore();
}

// ── function: drawCreatureShape ──
function drawCreatureShape(ctx, x, y, type, size) {
  switch(type) {
    case 'ember':
      ctx.beginPath();
      ctx.moveTo(x, y - size);
      ctx.lineTo(x + size*0.7, y + size*0.5);
      ctx.lineTo(x - size*0.7, y + size*0.5);
      ctx.closePath();
      ctx.fill();
      break;
    case 'frost':
      ctx.beginPath();
      ctx.moveTo(x, y - size);
      ctx.lineTo(x + size*0.6, y);
      ctx.lineTo(x, y + size);
      ctx.lineTo(x - size*0.6, y);
      ctx.closePath();
      ctx.fill();
      break;
    case 'stone':
      ctx.beginPath();
      for (let i = 0; i < 6; i++) {
        const angle = Math.PI / 3 * i - Math.PI/2;
        const px = x + Math.cos(angle) * size * 0.7;
        const py = y + Math.sin(angle) * size * 0.7;
        if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
      }
      ctx.closePath();
      ctx.fill();
      break;
    case 'spark':
      ctx.beginPath();
      ctx.moveTo(x + size*0.2, y - size);
      ctx.lineTo(x - size*0.3, y - size*0.1);
      ctx.lineTo(x + size*0.1, y - size*0.1);
      ctx.lineTo(x - size*0.2, y + size);
      ctx.lineTo(x + size*0.3, y + size*0.1);
      ctx.lineTo(x - size*0.1, y + size*0.1);
      ctx.closePath();
      ctx.fill();
      break;
    case 'wave':
      ctx.beginPath();
      ctx.arc(x, y, size * 0.7, 0, Math.PI*2);
      ctx.fill();
      break;
    case 'prism':
      ctx.beginPath();
      for (let i = 0; i < 5; i++) {
        const angle = Math.PI * 2 / 5 * i - Math.PI/2;
        const outerX = x + Math.cos(angle) * size * 0.8;
        const outerY = y + Math.sin(angle) * size * 0.8;
        const innerAngle = angle + Math.PI / 5;
        const innerX = x + Math.cos(innerAngle) * size * 0.35;
        const innerY = y + Math.sin(innerAngle) * size * 0.35;
        if (i === 0) ctx.moveTo(outerX, outerY); else ctx.lineTo(outerX, outerY);
        ctx.lineTo(innerX, innerY);
      }
      ctx.closePath();
      ctx.fill();
      break;
    case 'void':
      ctx.beginPath();
      ctx.arc(x, y, size * 0.7, 0, Math.PI*2);
      ctx.fill();
      ctx.globalCompositeOperation = 'destination-out';
      ctx.beginPath();
      ctx.arc(x, y, size * 0.3, 0, Math.PI*2);
      ctx.fill();
      ctx.globalCompositeOperation = 'source-over';
      break;
    default:
      ctx.beginPath();
      ctx.arc(x, y, size * 0.7, 0, Math.PI*2);
      ctx.fill();
  }
}

// ── function: getStatusColor ──
function getStatusColor(status) {
  const colors = { burn:'#ff6b3a', frost:'#5bcfff', shock:'#ffe040', barrier:'#f0d0ff', regen:'#5bef8d' };
  return colors[status] || '#fff';
}

// ── function: getStatusIcon ──
function getStatusIcon(status) {
  const icons = { burn:'🔥', frost:'❄️', shock:'⚡', barrier:'🛡️', regen:'💚' };
  return icons[status] || '';
}

// ── function: renderBattleAnimations ──
function renderBattleAnimations(ctx) {
  battleAnimQueue.forEach(anim => {
    switch(anim.type) {
      case 'attack':
        spawnParticles(w/2, h*0.35, 8, TYPE_COLORS[anim.ability?.type] || '#fff', 5, 20, 4);
        break;
      case 'hit':
        const color = anim.crit ? '#ffe040' : '#ef5b5b';
        spawnFloatingText(w/2 + Math.random()*60-30, h*0.25, '-' + anim.damage, color);
        if (anim.crit) {
          spawnParticles(w/2, h*0.25, 15, '#ffe040', 6, 25, 5);
        }
        break;
      case 'faint':
        spawnParticles(w/2, h*0.3, 20, '#fff', 4, 30, 3);
        break;
    }
  });
  
  if (battleAnimQueue.length > 0) {
    setTimeout(() => { battleAnimQueue = []; }, 500);
  }
}

// ── function: renderOverworldParticles ──
function renderOverworldParticles(ctx) {
  particles.forEach(p => {
    if (!p.alive) return;
    ctx.globalAlpha = p.alpha * 0.6;
    ctx.fillStyle = p.color;
    ctx.beginPath();
    ctx.arc(p.x, p.y, p.size * p.alpha, 0, Math.PI*2);
    ctx.fill();
  });
  ctx.globalAlpha = 1;
}

// ── main ──
(function() {
  const canvas = document.getElementById('gameCanvas');
  const ctx = canvas.getContext('2d');
  let lastTime = 0;
  
  function resize() {
    w = window.innerWidth;
    h = window.innerHeight;
    canvas.width = w * (window.devicePixelRatio || 1);
    canvas.height = h * (window.devicePixelRatio || 1);
    ctx.setTransform(window.devicePixelRatio || 1, 0, 0, window.devicePixelRatio || 1, 0, 0);
    
    const battleCanvas = document.getElementById('battleCanvas');
    if (battleCanvas) {
      battleCanvas.width = w * (window.devicePixelRatio || 1);
      battleCanvas.height = h * (window.devicePixelRatio || 1);
    }
  }
  
  window.addEventListener('resize', resize);
  resize();
  
  // Input handling with hold support
  let keyHoldInterval = null;
  
  document.addEventListener('keydown', e => {
    if (G.screen !== 'overworld') return;
    
    let dx = 0, dy = 0;
    switch(e.key) {
      case 'ArrowUp': case 'w': case 'W': dy = -1; break;
      case 'ArrowDown': case 's': case 'S': dy = 1; break;
      case 'ArrowLeft': case 'a': case 'A': dx = -1; break;
      case 'ArrowRight': case 'd': case 'D': dx = 1; break;
      case 'Escape': case 'm': case 'M': openMenu(); e.preventDefault(); return;
      default: return;
    }
    
    movePlayer(dx, dy);
    lastMoveDir = { x: dx, y: dy };
    
    if (keyHoldInterval) clearInterval(keyHoldInterval);
    keyHoldInterval = setInterval(() => {
      if (G.screen === 'overworld') movePlayer(dx, dy);
      else clearInterval(keyHoldInterval);
    }, 120);
    
    e.preventDefault();
  });
  
  document.addEventListener('keyup', () => {
    if (keyHoldInterval) { clearInterval(keyHoldInterval); keyHoldInterval = null; }
  });
  
  // Touch controls
  const touchBtns = document.querySelectorAll('.touch-btn[data-dir]');
  let touchIntervals = {};
  touchBtns.forEach(btn => {
    const dir = btn.dataset.dir;
    let dx = 0, dy = 0;
    switch(dir) { case 'up': dy=-1;break; case 'down': dy=1;break; case 'left': dx=-1;break; case 'right': dx=1;break; }
    
    btn.addEventListener('touchstart', e => {
      e.preventDefault();
      movePlayer(dx, dy); lastMoveDir = { x:dx, y:dy };
      if (touchIntervals[dir]) clearInterval(touchIntervals[dir]);
      touchIntervals[dir] = setInterval(() => {
        if (G.screen === 'overworld') movePlayer(dx, dy);
        else clearInterval(touchIntervals[dir]);
      }, 120);
    });
    btn.addEventListener('touchend', e => {
      e.preventDefault();
      if (touchIntervals[dir]) { clearInterval(touchIntervals[dir]); delete touchIntervals[dir]; }
    });
  });
  
  const actionBtn = document.getElementById('actionBtn');
  if (actionBtn) {
    actionBtn.addEventListener('click', () => {
      if (G.screen === 'overworld') openMenu();
      else if (G.screen === 'capture') attemptCapture();
    });
    actionBtn.addEventListener('touchstart', e => {
      e.preventDefault();
      if (G.screen === 'overworld') openMenu();
      else if (G.screen === 'capture') attemptCapture();
    });
  }
  
  // Capture tap/click
  const captureBar = document.getElementById('captureBar');
  if (captureBar) {
    captureBar.addEventListener('click', () => attemptCapture());
    captureBar.addEventListener('touchstart', e => { e.preventDefault(); attemptCapture(); });
  }
  
  // Title screen buttons
  document.getElementById('btnNewGame').addEventListener('click', startNewGame);
  document.getElementById('btnContinue').addEventListener('click', () => { if (loadGame(0)) sfxMenu(); });
  
  if (hasSave()) document.getElementById('btnContinue').classList.remove('hidden');
  
  // Menu buttons
  document.querySelectorAll('[data-menu]').forEach(btn => {
    btn.addEventListener('click', () => {
      sfxMenu();
      const menu = btn.dataset.menu;
      switch(menu) {
        case 'team': renderTeamScreen(); showScreen('team'); break;
        case 'bag': renderBagScreen(); showScreen('bag'); break;
        case 'encyclopedia': renderCodexScreen(); showScreen('codex'); break;
        case 'map': renderMapScreen(); showScreen('map'); break;
        case 'settings': showScreen('settings'); break;
      }
    });
  });
  
  document.querySelectorAll('[data-back]').forEach(btn => {
    btn.addEventListener('click', () => { sfxMenu(); closeSubmenu(); });
  });
  
  document.getElementById('menuClose').addEventListener('click', () => { sfxMenu(); showScreen('overworld'); updateHUD(); });
  
  // Shrine buttons
  document.getElementById('shrineHeal').addEventListener('click', () => {
    G.party.forEach(c => { c.hp = c.maxHp; c.statusEffect = 'none'; c.statusTurns = 0; c.statBoosts = {atk:0,def:0,spd:0}; });
    saveGame(0); sfxCapture(); notify('Party fully restored! Game saved.');
    setTimeout(() => { showScreen('overworld'); updateHUD(); }, 500);
  });
  
  document.getElementById('shrineShop').addEventListener('click', () => { sfxMenu(); renderShopScreen(); showScreen('shop'); });
  document.getElementById('shrineLeave').addEventListener('click', () => { sfxMenu(); showScreen('overworld'); updateHUD(); });
  
  // Settings buttons
  document.getElementById('muteBtn').addEventListener('click', () => {
    const muted = toggleMute();
    document.getElementById('muteBtn').textContent = muted ? '🔇 Sound: Off' : '🔊 Sound: On';
  });
  document.getElementById('saveBtn').addEventListener('click', () => saveGame(0));
  document.getElementById('loadBtn').addEventListener('click', () => { if (loadGame(0)) sfxMenu(); });
  
  // Victory continue
  document.getElementById('victoryContinue').addEventListener('click', () => { showScreen('overworld'); updateHUD(); });
  
  function closeSubmenu() {
    const current = G.screen;
    if (['team','bag','codex','map','settings','shop'].includes(current)) { showScreen('overworld'); updateHUD(); }
    else if (current === 'creatureDetail') { renderTeamScreen(); showScreen('team'); }
  }
  
  function openMenu() { sfxMenu(); showScreen('menu'); }
  
  function startNewGame() {
    initAudio();
    G = initGameState();
    currentMap = generateZoneMap(0);
    
    // Show starter selection screen
    showStarterScreen();
  }
  
  // Main game loop
  function gameLoop(timestamp) {
    const dt = timestamp - lastTime;
    lastTime = timestamp;
    
    if (moveCooldown > 0) moveCooldown--;
    
    updateParticles();
    floatingTexts = floatingTexts.filter(ft => ft.alive);
    floatingTexts.forEach(ft => ft.update());
    updateScreenShake();
    
    if (G.screen === 'capture') updateCaptureGame();
    
    ctx.clearRect(0, 0, w, h);
    
    switch(G.screen) {
      case 'title': renderTitleScreen(ctx); break;
      case 'overworld':
        renderOverworld(ctx, w, h);
        renderOverworldParticles(ctx);
        break;
      case 'battle': renderBattleCanvas(ctx); break;
      default:
        ctx.fillStyle = '#0a0e1a';
        ctx.fillRect(0, 0, w, h);
    }
    
    requestAnimationFrame(gameLoop);
  }
  
  function renderTitleScreen(ctx) {
    const grad = ctx.createLinearGradient(0, 0, 0, h);
    grad.addColorStop(0, '#0a0e1a');
    grad.addColorStop(0.5, '#141b2d');
    grad.addColorStop(1, '#1a1038');
    ctx.fillStyle = grad;
    ctx.fillRect(0, 0, w, h);
    
    if (Math.random() < 0.1) {
      const colors = ['#5b8def', '#f0d0ff', '#5bef8d', '#ffe040'];
      spawnParticles(Math.random()*w, Math.random()*h, 1, colors[Math.random()*colors.length|0], 1, 80+Math.random()*60|0, 2);
    }
    
    particles.forEach(p => {
      if (!p.alive) return;
      ctx.globalAlpha = p.alpha * 0.5;
      ctx.fillStyle = p.color;
      ctx.beginPath();
      ctx.arc(p.x, p.y, p.size * p.alpha, 0, Math.PI*2);
      ctx.fill();
    });
    ctx.globalAlpha = 1;
    updateParticles();
  }
  
  requestAnimationFrame(gameLoop);
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen3.6-27b-mtp","provider":"lmstudio","persona":"composer","sessionId":"b9093d9b-b428-418a-9536-29287568cafd","tokensIn":5718790,"tokensOut":73844,"tokensTotal":5792634,"turns":88,"toolCalls":87,"failedToolCalls":2,"timestamp":"2026-07-16T02:07:52.191Z"} -->