Creature Collection
Qwen 3.6 27b MTP · composer

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"} -->