Creature Collection
kat coder air v2.5 · composer

Reviews & generation details
Reviews
Human review · Jul 17, 2026 · rubric 1
Historical path-based review. Association with these exact bytes is unverified.
- Visual design
- 2 / 5
- UX / feel
- 3 / 5
- Works
- 1 / 5
- Prompt spirit
- 3 / 5
- Delight
- 2 / 5
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 113,594 bytes · SHA-256 81867ccd75de
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Echoes of the Weave</title>
<style>
/* ── css: game ── */
*{margin:0;padding:0;box-sizing:border-box}
body{background:#050510;overflow:hidden;font-family:'Segoe UI',system-ui,sans-serif}
#app{position:relative;width:100vw;height:100vh;user-select:none}
#gameCanvas{position:absolute;top:0;left:0;width:100%;height:100%}
#hud{position:absolute;bottom:0;left:0;right:0;padding:8px 16px;display:none;pointer-events:none;z-index:10}
#hud>*{pointer-events:auto}
#hud-top{position:absolute;top:10px;left:10px;display:flex;gap:8px;align-items:center;z-index:11}
#hud-top button{background:rgba(255,255,255,0.15);border:1px solid rgba(255,255,255,0.3);color:#fff;padding:6px 12px;border-radius:8px;cursor:pointer;font-size:13px;backdrop-filter:blur(8px)}
#hud-top button:hover{background:rgba(255,255,255,0.25)}
#zone-label{position:absolute;top:10px;left:50%;transform:translateX(-50%);color:#fff;font-size:16px;font-weight:600;text-shadow:0 0 10px rgba(255,255,255,0.5);z-index:11;display:none;pointer-events:none}
#party-hud{position:absolute;bottom:12px;left:50%;transform:translateX(-50%);display:flex;gap:6px;z-index:11}
.party-slot{width:60px;height:70px;background:rgba(0,0,0,0.6);border:1px solid rgba(255,255,255,0.2);border-radius:8px;padding:4px;display:flex;flex-direction:column;align-items:center;cursor:pointer;transition:all 0.2s;backdrop-filter:blur(4px)}
.party-slot:hover,.party-slot.active{border-color:rgba(255,200,100,0.8);box-shadow:0 0 8px rgba(255,200,100,0.3)}
.party-slot.fainted{opacity:0.4}
.party-slot .mini-name{font-size:9px;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:56px;text-align:center}
.party-slot .mini-hp{width:50px;height:4px;background:rgba(255,255,255,0.15);border-radius:2px;margin-top:2px;overflow:hidden}
.party-slot .mini-hp-fill{height:100%;background:linear-gradient(90deg,#4ade80,#22c55e);border-radius:2px;transition:width 0.3s}
.party-slot .mini-hp-fill.low{background:linear-gradient(90deg,#ef4444,#f97316)}
.party-slot .mini-hp-fill.mid{background:linear-gradient(90deg,#f59e0b,#eab308)}
#controls-hint{position:absolute;bottom:90px;left:50%;transform:translateX(-50%);color:rgba(255,255,255,0.5);font-size:11px;z-index:11;display:none;pointer-events:none;text-align:center}
#overlay{position:absolute;top:0;left:0;right:0;bottom:0;display:none;z-index:20}
#overlay.active{display:flex;align-items:center;justify-content:center}
#dialogue-box{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);width:90%;max-width:700px;background:rgba(5,5,20,0.92);border:1px solid rgba(255,255,255,0.2);border-radius:16px;padding:20px 24px;backdrop-filter:blur(12px);display:none}
#dialogue-box.active{display:block}
#dialogue-text{color:#e8e8f0;font-size:15px;line-height:1.6;min-height:48px}
#dialogue-text .speaker{color:#f0c060;font-weight:600;margin-bottom:4px;font-size:13px}
#dialogue-choices{display:flex;gap:10px;margin-top:14px;flex-wrap:wrap}
#dialogue-choices button{background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.25);color:#e8e8f0;padding:8px 18px;border-radius:10px;cursor:pointer;font-size:13px;transition:all 0.15s}
#dialogue-choices button:hover{background:rgba(255,200,100,0.2);border-color:rgba(255,200,100,0.6)}
#menu-screen{display:none;flex-direction:column;width:90%;max-width:700px;max-height:85vh;background:rgba(5,5,20,0.95);border:1px solid rgba(255,255,255,0.15);border-radius:20px;backdrop-filter:blur(16px);overflow:hidden}
#menu-screen.active{display:flex}
#menu-tabs{display:flex;border-bottom:1px solid rgba(255,255,255,0.1)}
#menu-tabs button{flex:1;padding:12px;background:none;border:none;color:rgba(255,255,255,0.5);font-size:13px;cursor:pointer;transition:all 0.15s;border-bottom:2px solid transparent}
#menu-tabs button.active{color:#f0c060;border-bottom-color:#f0c060}
#menu-tabs button:hover{color:#fff}
#menu-content{flex:1;overflow-y:auto;padding:16px}
.menu-item{display:flex;align-items:center;gap:12px;padding:10px 12px;border-radius:10px;cursor:pointer;transition:all 0.15s;margin-bottom:4px}
.menu-item:hover{background:rgba(255,255,255,0.08)}
.menu-item .mi-icon{width:36px;height:36px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:18px}
.menu-item .mi-info{flex:1}
.menu-item .mi-name{color:#fff;font-size:14px;font-weight:500}
.menu-item .mi-detail{color:rgba(255,255,255,0.5);font-size:11px}
.menu-item .mi-stats{display:flex;gap:8px;font-size:10px;color:rgba(255,255,255,0.6)}
.menu-item .mi-stats span{background:rgba(255,255,255,0.1);padding:2px 6px;border-radius:4px}
.menu-section-title{color:rgba(255,255,255,0.4);font-size:11px;text-transform:uppercase;letter-spacing:1px;margin:12px 0 8px}
#battle-ui{position:absolute;top:0;left:0;right:0;bottom:0;display:none;flex-direction:column}
#battle-ui.active{display:flex}
#battle-arena{flex:1;position:relative;overflow:hidden}
#battle-hud-top{position:absolute;top:0;left:0;right:0;padding:12px 20px;display:flex;justify-content:space-between;pointer-events:none}
.enemy-hud{background:rgba(0,0,0,0.7);border:1px solid rgba(255,255,255,0.15);border-radius:12px;padding:10px 14px;backdrop-filter:blur(8px);min-width:180px}
.enemy-hud .eh-name{color:#fff;font-size:14px;font-weight:600}
.enemy-hud .eh-level{color:rgba(255,255,255,0.5);font-size:11px}
.enemy-hud .eh-hp-bar{width:160px;height:6px;background:rgba(255,255,255,0.15);border-radius:3px;margin-top:4px;overflow:hidden}
.enemy-hd .eh-hp-fill{height:100%;background:linear-gradient(90deg,#4ade80,#22c55e);border-radius:3px;transition:width 0.4s}
#battle-player-area{position:absolute;bottom:80px;left:20px}
.player-hud{background:rgba(0,0,0,0.7);border:1px solid rgba(255,255,255,0.15);border-radius:12px;padding:10px 14px;backdrop-filter:blur(8px);min-width:200px}
.player-hud .ph-name{color:#fff;font-size:14px;font-weight:600}
.player-hud .ph-level{color:rgba(255,255,255,0.5);font-size:11px}
.player-hud .ph-hp-bar{width:180px;height:6px;background:rgba(255,255,255,0.15);border-radius:3px;margin-top:4px;overflow:hidden}
.player-hud .ph-hp-fill{height:100%;background:linear-gradient(90deg,#4ade80,#22c55e);border-radius:3px;transition:width 0.4s}
.player-hud .ph-hp-fill.low{background:linear-gradient(90deg,#ef4444,#f97316)}
.player-hud .ph-hp-fill.mid{background:linear-gradient(90deg,#f59e0b,#eab308)}
.player-hud .ph-exp-bar{width:180px;height:3px;background:rgba(255,255,255,0.1);border-radius:2px;margin-top:3px;overflow:hidden}
.player-hud .ph-exp-fill{height:100%;background:#a78bfa;border-radius:2px;transition:width 0.3s}
#battle-actions{position:absolute;bottom:0;left:0;right:0;padding:12px 20px;background:rgba(5,5,20,0.9);border-top:1px solid rgba(255,255,255,0.1);display:flex;gap:8px;flex-wrap:wrap;backdrop-filter:blur(8px)}
#battle-actions button{background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.2);color:#e8e8f0;padding:8px 16px;border-radius:10px;cursor:pointer;font-size:13px;transition:all 0.15s}
#battle-actions button:hover{background:rgba(255,200,100,0.15);border-color:rgba(255,200,100,0.5)}
#battle-actions button:disabled{opacity:0.3;cursor:not-allowed}
#battle-log{position:absolute;top:60px;left:50%;transform:translateX(-50%);color:#fff;font-size:14px;text-align:center;pointer-events:none;text-shadow:0 0 10px rgba(0,0,0,0.8);max-width:500px}
#battle-log .log-effect{color:#f0c060;font-weight:600}
#battle-log .log-supereff{color:#f87171;font-weight:600;font-size:16px}
#battle-log .log-noteff{color:#94a3b8}
#battle-log .log-catch{color:#4ade80;font-weight:600}
#floating-texts{position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;z-index:30}
.float-text{position:absolute;font-weight:700;font-size:20px;animation:floatUp 1s forwards;pointer-events:none;text-shadow:0 0 8px rgba(0,0,0,0.8)}
.float-text.crit{font-size:26px;color:#fbbf24}
.float-text.super{color:#f87171}
.float-text.heal{color:#4ade80}
.float-text.miss{color:#94a3b8;font-size:16px}
@keyframes floatUp{0%{opacity:1;transform:translateY(0) scale(1)}100%{opacity:0;transform:translateY(-60px) scale(0.8)}}
#title-screen{position:absolute;top:0;left:0;right:0;bottom:0;display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:50;background:radial-gradient(ellipse at center,rgba(20,10,40,0.95),rgba(5,5,16,0.99))}
#title-screen.hidden{display:none}
#title-screen h1{font-size:clamp(32px,6vw,64px);font-weight:700;color:#fff;text-shadow:0 0 30px rgba(200,150,255,0.6),0 0 60px rgba(150,100,255,0.3);margin-bottom:8px;letter-spacing:2px}
#title-screen .subtitle{color:rgba(200,180,255,0.7);font-size:16px;margin-bottom:40px;letter-spacing:4px;text-transform:uppercase}
#title-screen .menu-btn{background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.2);color:#e8e8f0;padding:14px 40px;border-radius:12px;cursor:pointer;font-size:16px;margin:8px;transition:all 0.2s;min-width:220px;text-align:center}
#title-screen .menu-btn:hover{background:rgba(200,150,255,0.15);border-color:rgba(200,150,255,0.5);box-shadow:0 0 20px rgba(200,150,255,0.2)}
#title-screen .menu-btn.primary{background:rgba(200,150,255,0.2);border-color:rgba(200,150,255,0.4)}
#title-screen .menu-btn.primary:hover{background:rgba(200,150,255,0.3)}
#title-screen .slot-info{color:rgba(255,255,255,0.4);font-size:12px;margin-top:4px}
#touch-controls{position:absolute;bottom:0;left:0;right:0;height:140px;display:none;z-index:15;pointer-events:none}
#touch-controls.active{display:flex;justify-content:space-between;align-items:flex-end;padding:10px 20px 20px}
#dpad{width:120px;height:120px;position:relative;pointer-events:auto}
#dpad button{position:absolute;width:40px;height:40px;background:rgba(255,255,255,0.15);border:1px solid rgba(255,255,255,0.3);border-radius:8px;color:#fff;font-size:18px;cursor:pointer;display:flex;align-items:center;justify-content:center}
#dpad .up{top:0;left:40px}
#dpad .down{bottom:0;left:40px}
#dpad .left{top:40px;left:0}
#dpad .right{top:40px;right:0}
#action-buttons{display:flex;gap:10px;pointer-events:auto}
#action-buttons button{width:56px;height:56px;background:rgba(255,255,255,0.15);border:1px solid rgba(255,255,255,0.3);border-radius:50%;color:#fff;font-size:12px;cursor:pointer;font-weight:600}
#mute-btn{position:absolute;top:10px;right:10px;z-index:60;background:rgba(0,0,0,0.5);border:1px solid rgba(255,255,255,0.2);color:#fff;width:36px;height:36px;border-radius:50%;cursor:pointer;font-size:16px;display:flex;align-items:center;justify-content:center}
.shake{animation:shake 0.3s}
@keyframes shake{0%,100%{transform:translateX(0)}20%{transform:translateX(-4px)}40%{transform:translateX(4px)}60%{transform:translateX(-2px)}80%{transform:translateX(2px)}}
#vignette{position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;z-index:5;background:radial-gradient(ellipse at center,transparent 60%,rgba(0,0,0,0.4));}
#capture-game{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);display:none;z-index:35}
#capture-game.active{display:block}
#capture-game .cg-bar{width:300px;height:40px;background:rgba(0,0,0,0.8);border:2px solid rgba(255,255,255,0.3);border-radius:20px;position:relative;overflow:hidden}
#capture-game .cg-zone{position:absolute;height:100%;background:rgba(74,222,128,0.6);border-radius:18px}
#capture-game .cg-cursor{position:absolute;width:4px;height:100%;background:#fff;border-radius:2px;top:0}
#capture-game .cg-hint{color:rgba(255,255,255,0.7);font-size:13px;margin-top:10px;text-align:center}
#capture-game .cg-tap{margin-top:12px;width:100px;height:100px;background:rgba(255,255,255,0.15);border:2px solid rgba(255,255,255,0.4);border-radius:50%;color:#fff;font-size:14px;cursor:pointer;display:block;margin:12px auto 0}
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div id="vignette"></div>
<div id="hud">
<div id="hud-top">
<button id="btn-menu">☰ Menu</button>
<button id="btn-mute">🔊</button>
</div>
<div id="zone-label"></div>
<div id="party-hud"></div>
<div id="controls-hint">WASD or Arrows to move · Space to interact</div>
</div>
<div id="overlay">
<div id="menu-screen">
<div id="menu-tabs">
<button class="tab active" data-tab="party">Party</button>
<button class="tab" data-tab="items">Items</button>
<button class="tab" data-tab="encyclopedia">Bestiary</button>
<button class="tab" data-tab="map">Map</button>
<button class="tab" data-tab="save">Save</button>
</div>
<div id="menu-content"></div>
</div>
</div>
<div id="dialogue-box">
<div id="dialogue-text"></div>
<div id="dialogue-choices"></div>
</div>
<div id="battle-ui">
<div id="battle-arena">
<div id="battle-hud-top"></div>
<div id="battle-player-area"></div>
<div id="battle-log"></div>
</div>
<div id="battle-actions"></div>
</div>
<div id="floating-texts"></div>
<div id="capture-game">
<div class="cg-bar"><div class="cg-zone"></div><div class="cg-cursor"></div></div>
<div class="cg-hint">Tap when the bar is in the green zone!</div>
<button class="cg-tap">TAP</button>
</div>
<div id="touch-controls">
<div id="dpad">
<button class="up" data-key="ArrowUp">▲</button>
<button class="down" data-key="ArrowDown">▼</button>
<button class="left" data-key="ArrowLeft">◄</button>
<button class="right" data-key="ArrowRight">►</button>
</div>
<div id="action-buttons">
<button id="touch-interact">A</button>
<button id="touch-menu">M</button>
</div>
</div>
<div id="title-screen">
<h1>Echoes of the Weave</h1>
<div class="subtitle">A Journey Through the Shattered Realms</div>
<button class="menu-btn primary" id="btn-new">New Binding</button>
<button class="menu-btn" id="btn-continue">Continue</button>
</div>
<button id="mute-btn">🔊</button>
<script>
'use strict';
// ── const: TYPE_NAMES ──
// ═══════════════════════════════════════════════════════════════
// GAME DATA: TYPES, CREATURES, ABILITIES, ZONES, ITEMS
// ═══════════════════════════════════════════════════════════════
const TYPE_NAMES = {
pyre: "Pyre", void: "Void", verd: "Verd",
frost: "Frost", storm: "Storm", stone: "Stone", aether: "Aether"
};
// ── const: TYPE_PREV ──
const TYPE_PREV = {
pyre: ["void","frost"],
void: ["storm","aether"],
verd: ["pyre","aether"],
frost: ["stone","pyre"],
storm: ["void","frost"],
stone: ["storm","aether"],
aether: ["stone","ver"]
};
// ── const: TYPE_WEAK ──
const TYPE_WEAK = {
pyre: ["verd"],
void: ["storm"],
verd: ["frost","stone"],
frost: ["pyre"],
storm: ["aether"],
stone: ["void"],
aether: ["pyre","frost","ver","storm","stone"]
};
// ── const: TYPE_RESIST ──
const TYPE_RESIST = {
pyre: ["frost","aether"],
void: ["pyre"],
verd: ["void","frost"],
frost: ["ver"],
storm: ["ver","stone"],
stone: ["void","aether"],
aether: ["frost","storm","stone"]
};
// ── const: CREATURES ──
// 27 creatures: 18 base + 9 transformations
const CREATURES = {
emberl: {
name: "Emberl", affinity: "pyre",
lore: "A restless spark that dances through the Pyre Wastes, leaving trails of warm light.",
baseHp: 45, baseAtk: 12, baseDef: 8, baseSpd: 10,
abilities: ["cinder", "ember_burst", "smoke_screen", "inferno"],
transforms: { level: 18, catalyst: "flame_core" },
exp: 12
},
gloamoth: {
name: "Gloamoth", affinity: "void",
lore: "A shadow-winged creature born from the silence between stars.",
baseHp: 40, baseAtk: 14, baseDef: 7, baseSpd: 12,
abilities: ["dark_pulse", "shadow_step", "void_drain", "event_horizon"],
transforms: { level: 18, catalyst: "void_shard" },
exp: 12
},
sproutling: {
name: "Sproutling", affinity: "ver",
lore: "A tiny seedling spirit that hums with the quiet energy of growing things.",
baseHp: 50, baseAtk: 10, baseDef: 10, baseSpd: 8,
abilities: ["vine_lash", "photosynthesis", "thorn_wall", "bloom"],
transforms: { level: 18, catalyst: "seed_of_life" },
exp: 12
},
glaci: {
name: "Glaci", affinity: "frost",
lore: "A crystalline being that forms when winter breath touches still water.",
baseHp: 42, baseAtk: 11, baseDef: 12, baseSpd: 9,
abilities: ["ice_shard", "freeze", "frost_breath", "absolute_zero"],
transforms: { level: 18, catalyst: "frost_heart" },
exp: 12
},
voltix: {
name: "Voltix", affinity: "storm",
lore: "A crackling entity that rides the currents between cloud and sky.",
baseHp: 38, baseAtk: 15, baseDef: 6, baseSpd: 14,
abilities: ["thunder", "lightning", "static_charge", "tempest"],
transforms: { level: 18, catalyst: "storm_heart" },
exp: 12
},
lithos: {
name: "Lithos", affinity: "stone",
lore: "A slow-moving guardian of ancient stone, patient as the mountains themselves.",
baseHp: 55, baseAtk: 10, baseDef: 14, baseSpd: 6,
abilities: ["rock_throw", "earthquake", "stone_skin", "tectonic"],
transforms: { level: 18, catalyst: "earth_core" },
exp: 12
},
aetherling: {
name: "Aetherling", affinity: "aether",
lore: "A shimmering wisp of raw cosmic energy, barely contained in form.",
baseHp: 35, baseAtk: 13, baseDef: 5, baseSpd: 11,
abilities: ["aether_bolt", "phase", "mana_shield", "cosmic_rift"],
transforms: { level: 18, catalyst: "aether_crystal" },
exp: 12
},
pyrewisp: {
name: "Pyrewisp", affinity: "pyre",
lore: "A small firefly-like creature that collects warmth from dying embers.",
baseHp: 32, baseAtk: 9, baseDef: 6, baseSpd: 13,
abilities: ["ember", "heat_wave", "flash", "blaze"],
transforms: { level: 22, catalyst: "pyre_gem" },
exp: 10
},
murkowl: {
name: "Murkowl", affinity: "void",
lore: "A silent watcher that perches on the edges of perception.",
baseHp: 36, baseAtk: 12, baseDef: 8, baseSpd: 11,
abilities: ["peck", "night_shade", "confuse_ray", "shadow_ball"],
transforms: { level: 22, catalyst: "void_gem" },
exp: 10
},
mosspon: {
name: "Mosspon", affinity: "ver",
lore: "A cheerful sponge-like being covered in soft green moss.",
baseHp: 48, baseAtk: 8, baseDef: 11, baseSpd: 5,
abilities: ["absorb", "spore", "toxic_pollens", "regrow"],
transforms: { level: 22, catalyst: "verd_gem" },
exp: 10
},
frostdrop: {
name: "Frostdrop", affinity: "frost",
lore: "A tiny icicle creature that chimes when the wind blows through it.",
baseHp: 30, baseAtk: 10, baseDef: 9, baseSpd: 12,
abilities: ["water_gun", "mist", "blizzard", "ice_beam"],
transforms: { level: 22, catalyst: "frost_gem" },
exp: 10
},
sparklet: {
name: "Sparklet", affinity: "storm",
lore: "A playful ball of static electricity that loves to jump between surfaces.",
baseHp: 34, baseAtk: 13, baseDef: 5, baseSpd: 15,
abilities: ["spark", "charge", "thunder_wave", "volt_tackle"],
transforms: { level: 22, catalyst: "storm_gem" },
exp: 10
},
cobble: {
name: "Cobble", affinity: "stone",
lore: "A small rock creature that rolls around, collecting shiny pebbles.",
baseHp: 50, baseAtk: 9, baseDef: 13, baseSpd: 5,
abilities: ["tackle", "rollout", "iron_defense", "rock_slide"],
transforms: { level: 22, catalyst: "stone_gem" },
exp: 10
},
phasling: {
name: "Phasling", affinity: "aether",
lore: "A translucent being that flickers between dimensions.",
baseHp: 33, baseAtk: 12, baseDef: 6, baseSpd: 10,
abilities: ["zap", "teleport", "reflect", "pulse"],
transforms: { level: 22, catalyst: "aether_gem" },
exp: 10
},
emberdrake: {
name: "Emberdrake", affinity: "pyre",
lore: "A young dragon whose breath can melt stone.",
baseHp: 44, baseAtk: 15, baseDef: 10, baseSpd: 9,
abilities: ["bite", "flamethrower", "dragon_fury", "scorch"],
transforms: { level: 26, catalyst: "dragon_scale" },
exp: 14
},
voidstalker: {
name: "Voidstalker", affinity: "void",
lore: "A predatory shadow that hunts in the spaces between light.",
baseHp: 40, baseAtk: 16, baseDef: 8, baseSpd: 13,
abilities: ["scratch", "shadow_strike", "nightmare", "dark_void"],
transforms: { level: 26, catalyst: "void_core" },
exp: 14
},
verdguardian: {
name: "Verdguardian", affinity: "ver",
lore: "An ancient tree spirit that protects the oldest forests.",
baseHp: 58, baseAtk: 12, baseDef: 15, baseSpd: 6,
abilities: ["wood_whip", "root_snare", "forest_shield", "nature_wrath"],
transforms: { level: 26, catalyst: "heartseed" },
exp: 14
},
frostweaver: {
name: "Frostweaver", affinity: "frost",
lore: "A spider-like creature that spins webs of pure ice.",
baseHp: 38, baseAtk: 14, baseDef: 10, baseSpd: 11,
abilities: ["ice_spinner", "frost_web", "cold_snap", "crystal_lance"],
transforms: { level: 26, catalyst: "ice_spider" },
exp: 14
},
// Transformed forms (9 total)
emberl_fuse: {
name: "Emberl Fuse", affinity: "pyre",
lore: "A blazing phoenix reborn from the eternal flame.",
baseHp: 80, baseAtk: 22, baseDef: 16, baseSpd: 16,
abilities: ["cinder", "ember_burst", "inferno", "phoenix_rise"],
transforms: null, exp: 0
},
gloamoth_void: {
name: "Gloamoth Void", affinity: "void",
lore: "A cosmic entity that has transcended into pure darkness.",
baseHp: 72, baseAtk: 26, baseDef: 14, baseSpd: 18,
abilities: ["dark_pulse", "shadow_step", "void_drain", "singularity"],
transforms: null, exp: 0
},
sproutling_ancient: {
name: "Sproutling Ancient", affinity: "ver",
lore: "A world-tree spirit that holds the memories of all forests.",
baseHp: 90, baseAtk: 20, baseDef: 20, baseSpd: 12,
abilities: ["vine_lash", "photosynthesis", "thorn_wall", "world_bloom"],
transforms: null, exp: 0
},
glaci_crystal: {
name: "Glaci Crystal", affinity: "frost",
lore: "A living glacier that freezes time itself.",
baseHp: 76, baseAtk: 22, baseDef: 24, baseSpd: 14,
abilities: ["ice_shard", "freeze", "frost_breath", "eternal_frost"],
transforms: null, exp: 0
},
voltix_storm: {
name: "Voltix Storm", affinity: "storm",
lore: "An apocalyptic tempest given form.",
baseHp: 68, baseAtk: 30, baseDef: 12, baseSpd: 20,
abilities: ["thunder", "lightning", "static_charge", "apocalypse"],
transforms: null, exp: 0
},
lithos_titan: {
name: "Lithos Titan", affinity: "stone",
lore: "A mountain-sized guardian that moves with surprising grace.",
baseHp: 100, baseAtk: 20, baseDef: 28, baseSpd: 10,
abilities: ["rock_throw", "earthquake", "stone_skin", "continental_drift"],
transforms: null, exp: 0
},
aetherling_cosmic: {
name: "Aetherling Cosmic", affinity: "aether",
lore: "A being of pure energy that exists in all dimensions at once.",
baseHp: 65, baseAtk: 24, baseDef: 10, baseSpd: 18,
abilities: ["aether_bolt", "phase", "mana_shield", "multiverse"],
transforms: null, exp: 0
},
pyrewisp_blaze: {
name: "Pyrewisp Blaze", affinity: "pyre",
lore: "A comet of living fire that streaks across the sky.",
baseHp: 60, baseAtk: 24, baseDef: 12, baseSpd: 20,
abilities: ["ember", "heat_wave", "flash", "supernova"],
transforms: null, exp: 0
},
murkowl_abyss: {
name: "Murkowl Abyss", affinity: "void",
lore: "A watcher from the deepest void, seeing all and judging all.",
baseHp: 64, baseAtk: 22, baseDef: 16, baseSpd: 18,
abilities: ["peck", "night_shade", "confuse_ray", "abyssal_gaze"],
transforms: null, exp: 0
}
};
// ── const: ABILITIES ──
// 40+ abilities
const ABILITIES = {
// Pyre abilities
cinder: { name: "Cinder", type: "pyre", power: 40, accuracy: 95, pp: 25, effect: "burn" },
ember_burst: { name: "Ember Burst", type: "pyre", power: 60, accuracy: 90, pp: 20, effect: "burn" },
inferno: { name: "Inferno", type: "pyre", power: 90, accuracy: 80, pp: 10, effect: "burn" },
flame: { name: "Flame", type: "pyre", power: 50, accuracy: 100, pp: 20, effect: "burn" },
heat_wave: { name: "Heat Wave", type: "pyre", power: 70, accuracy: 85, pp: 15, effect: "burn" },
flash: { name: "Flash", type: "pyre", power: 30, accuracy: 100, pp: 20, effect: "flinch" },
blaze: { name: "Blaze", type: "pyre", power: 80, accuracy: 90, pp: 12, effect: "burn" },
phoenix_rise: { name: "Phoenix Rise", type: "pyre", power: 100, accuracy: 85, pp: 5, effect: "revive" },
scorch: { name: "Scorch", type: "pyre", power: 75, accuracy: 90, pp: 10, effect: "burn" },
supernova: { name: "Supernova", type: "pyre", power: 110, accuracy: 75, pp: 3, effect: "burn" },
// Void abilities
dark_pulse: { name: "Dark Pulse", type: "void", power: 50, accuracy: 100, pp: 20, effect: "confuse" },
shadow_step: { name: "Shadow Step", type: "void", power: 30, accuracy: 100, pp: 20, effect: "speed_up" },
void_drain: { name: "Void Drain", type: "void", power: 40, accuracy: 100, pp: 15, effect: "drain" },
shadow_ball: { name: "Shadow Ball", type: "void", power: 70, accuracy: 90, pp: 10, effect: "def_down" },
night_shade: { name: "Night Shade", type: "void", power: 60, accuracy: 100, pp: 15, effect: "ignore_def" },
confuse_ray: { name: "Confuse Ray", type: "void", power: 0, accuracy: 100, pp: 10, effect: "confuse" },
shadow_strike: { name: "Shadow Strike", type: "void", power: 65, accuracy: 95, pp: 15, effect: "crit_up" },
nightmare: { name: "Nightmare", type: "void", power: 80, accuracy: 85, pp: 8, effect: "fear" },
dark_void: { name: "Dark Void", type: "void", power: 90, accuracy: 80, pp: 6, effect: "confuse" },
singularity: { name: "Singularity", type: "void", power: 100, accuracy: 75, pp: 4, effect: "absorb" },
abyssal_gaze: { name: "Abyssal Gaze", type: "void", power: 85, accuracy: 85, pp: 5, effect: "paralyze" },
// Verd abilities
vine_lash: { name: "Vine Lash", type: "ver", power: 40, accuracy: 100, pp: 25, effect: "bind" },
photosynthesis: { name: "Photosynthesis", type: "ver", power: 0, accuracy: 100, pp: 10, effect: "heal" },
thorn_wall: { name: "Thorn Wall", type: "ver", power: 0, accuracy: 100, pp: 10, effect: "barrier" },
bloom: { name: "Bloom", type: "ver", power: 70, accuracy: 90, pp: 8, effect: "heal_all" },
absorb: { name: "Absorb", type: "ver", power: 30, accuracy: 100, pp: 20, effect: "drain" },
spore: { name: "Spore", type: "ver", power: 0, accuracy: 90, pp: 10, effect: "sleep" },
toxic_pollens: { name: "Toxic Pollens", type: "ver", power: 0, accuracy: 85, pp: 10, effect: "poison" },
regrow: { name: "Regrow", type: "ver", power: 0, accuracy: 100, pp: 10, effect: "regen" },
wood_whip: { name: "Wood Whip", type: "ver", power: 50, accuracy: 95, pp: 20, effect: "bind" },
root_snare: { name: "Root Snare", type: "ver", power: 40, accuracy: 100, pp: 15, effect: "paralyze" },
forest_shield: { name: "Forest Shield", type: "ver", power: 0, accuracy: 100, pp: 5, effect: "barrier" },
nature_wrath: { name: "Nature Wrath", type: "ver", power: 80, accuracy: 85, pp: 8, effect: "def_down" },
world_bloom: { name: "World Bloom", type: "ver", power: 90, accuracy: 80, pp: 4, effect: "heal_all" },
// Frost abilities
ice_shard: { name: "Ice Shard", type: "frost", power: 40, accuracy: 100, pp: 25, effect: "frost" },
freeze: { name: "Freeze", type: "frost", power: 60, accuracy: 90, pp: 15, effect: "frost" },
frost_breath: { name: "Frost Breath", type: "frost", power: 75, accuracy: 85, pp: 10, effect: "frost" },
absolute_zero: { name: "Absolute Zero", type: "frost", power: 100, accuracy: 70, pp: 4, effect: "frost" },
water_gun: { name: "Water Gun", type: "frost", power: 40, accuracy: 100, pp: 25, effect: "none" },
mist: { name: "Mist", type: "frost", power: 0, accuracy: 100, pp: 10, effect: "defense_up" },
blizzard: { name: "Blizzard", type: "frost", power: 80, accuracy: 80, pp: 8, effect: "frost" },
ice_beam: { name: "Ice Beam", type: "frost", power: 85, accuracy: 85, pp: 8, effect: "frost" },
ice_spinner: { name: "Ice Spinner", type: "frost", power: 50, accuracy: 100, pp: 20, effect: "frost" },
frost_web: { name: "Frost Web", type: "frost", power: 0, accuracy: 90, pp: 10, effect: "speed_down" },
cold_snap: { name: "Cold Snap", type: "frost", power: 65, accuracy: 90, pp: 12, effect: "frost" },
crystal_lance: { name: "Crystal Lance", type: "frost", power: 90, accuracy: 85, pp: 6, effect: "frost" },
eternal_frost: { name: "Eternal Frost", type: "frost", power: 100, accuracy: 75, pp: 4, effect: "frost" },
// Storm abilities
thunder: { name: "Thunder", type: "storm", power: 80, accuracy: 75, pp: 10, effect: "paralyze" },
lightning: { name: "Lightning", type: "storm", power: 60, accuracy: 90, pp: 15, effect: "paralyze" },
static_charge: { name: "Static Charge", type: "storm", power: 0, accuracy: 100, pp: 10, effect: "attack_up" },
tempest: { name: "Tempest", type: "storm", power: 90, accuracy: 80, pp: 6, effect: "paralyze" },
spark: { name: "Spark", type: "storm", power: 30, accuracy: 100, pp: 20, effect: "paralyze" },
charge: { name: "Charge", type: "storm", power: 0, accuracy: 100, pp: 10, effect: "speed_up" },
thunder_wave: { name: "Thunder Wave", type: "storm", power: 0, accuracy: 90, pp: 10, effect: "paralyze" },
volt_tackle: { name: "Volt Tackle", type: "storm", power: 70, accuracy: 95, pp: 15, effect: "paralyze" },
apocalypse: { name: "Apocalypse", type: "storm", power: 110, accuracy: 70, pp: 3, effect: "paralyze" },
// Stone abilities
rock_throw: { name: "Rock Throw", type: "stone", power: 40, accuracy: 100, pp: 25, effect: "none" },
earthquake: { name: "Earthquake", type: "stone", power: 90, accuracy: 85, pp: 6, effect: "flinch" },
stone_skin: { name: "Stone Skin", type: "stone", power: 0, accuracy: 100, pp: 10, effect: "defense_up" },
tectonic: { name: "Tectonic", type: "stone", power: 100, accuracy: 75, pp: 4, effect: "flinch" },
tackle: { name: "Tackle", type: "stone", power: 30, accuracy: 100, pp: 20, effect: "none" },
rollout: { name: "Rollout", type: "stone", power: 50, accuracy: 90, pp: 10, effect: "defense_up" },
iron_defense: { name: "Iron Defense", type: "stone", power: 0, accuracy: 100, pp: 10, effect: "defense_up" },
rock_slide: { name: "Rock Slide", type: "stone", power: 60, accuracy: 90, pp: 10, effect: "flinch" },
continental_drift: { name: "Continental Drift", type: "stone", power: 100, accuracy: 80, pp: 4, effect: "flinch" },
// Aether abilities
aether_bolt: { name: "Aether Bolt", type: "aether", power: 50, accuracy: 100, pp: 20, effect: "phase" },
phase: { name: "Phase", type: "aether", power: 0, accuracy: 100, pp: 10, effect: "phase" },
mana_shield: { name: "Mana Shield", type: "aether", power: 0, accuracy: 100, pp: 10, effect: "barrier" },
cosmic_rift: { name: "Cosmic Rift", type: "aether", power: 85, accuracy: 85, pp: 6, effect: "phase" },
zap: { name: "Zap", type: "aether", power: 30, accuracy: 100, pp: 20, effect: "paralyze" },
teleport: { name: "Teleport", type: "aether", power: 0, accuracy: 100, pp: 10, effect: "speed_up" },
reflect: { name: "Reflect", type: "aether", power: 0, accuracy: 100, pp: 10, effect: "barrier" },
pulse: { name: "Pulse", type: "aether", power: 50, accuracy: 100, pp: 15, effect: "none" },
multiverse: { name: "Multiverse", type: "aether", power: 100, accuracy: 75, pp: 3, effect: "phase" },
// Generic abilities
bite: { name: "Bite", type: "pyre", power: 50, accuracy: 100, pp: 20, effect: "fear" },
dragon_fury: { name: "Dragon Fury", type: "pyre", power: 80, accuracy: 90, pp: 10, effect: "attack_up" },
scratch: { name: "Scratch", type: "void", power: 30, accuracy: 100, pp: 20, effect: "none" },
peck: { name: "Peck", type: "void", power: 30, accuracy: 100, pp: 20, effect: "none" },
smoke_screen: { name: "Smoke Screen", type: "pyre", power: 0, accuracy: 100, pp: 10, effect: "accuracy_down" },
flamethrower: { name: "Flamethrower", type: "pyre", power: 70, accuracy: 90, pp: 12, effect: "burn" },
event_horizon: { name: "Event Horizon", type: "void", power: 75, accuracy: 80, pp: 8, effect: "confuse" }
};
// ── const: ZONES ──
// 6 zones
const ZONES = [
{
id: "whispering_grove",
name: "Whispering Grove",
description: "A serene forest where ancient trees hum with forgotten melodies.",
bgGradient: "linear-gradient(180deg, #0a2a0a 0%, #1a3a1a 50%, #0d1f0d 100%)",
particleColor: "rgba(100, 200, 100, 0.6)",
particleType: "leaf",
encounterRate: 0.15,
wildCreatures: ["sproutling", "mosspon", "emberl"],
guardLevel: 5,
bossCreature: "verdguardian",
bossLevel: 12,
storyNode: "whispering_grove_story",
shrine: true,
color: "#4ade80"
},
{
id: "ember_wastes",
name: "Ember Wastes",
description: "A scorched desert where fire spirits dance among the molten sands.",
bgGradient: "linear-gradient(180deg, #2a0a0a 0%, #3a1a0a 50%, #1f0d0d 100%)",
particleColor: "rgba(255, 100, 50, 0.7)",
particleType: "ember",
encounterRate: 0.20,
wildCreatures: ["emberl", "pyrewisp", "emberdrake"],
guardLevel: 8,
bossCreature: "emberdrake",
bossLevel: 18,
storyNode: "ember_wastes_story",
shrine: true,
color: "#f97316"
},
{
id: "crystal_caverns",
name: "Crystal Caverns",
description: "Underground tunnels lined with singing crystals that hum with cold energy.",
bgGradient: "linear-gradient(180deg, #0a1a2a 0%, #1a2a3a 50%, #0d1520 100%)",
particleColor: "rgba(100, 150, 255, 0.6)",
particleType: "crystal",
encounterRate: 0.18,
wildCreatures: ["glaci", "frostdrop", "lithos"],
guardLevel: 10,
bossCreature: "frostweaver",
bossLevel: 20,
storyNode: "crystal_caverns_story",
shrine: true,
color: "#60a5fa"
},
{
id: "storm_peaks",
name: "Storm Peaks",
description: "Towering mountains perpetually wreathed in lightning and howling winds.",
bgGradient: "linear-gradient(180deg, #1a1a2a 0%, #2a2a3a 50%, #151520 100%)",
particleColor: "rgba(200, 200, 255, 0.7)",
particleType: "spark",
encounterRate: 0.22,
wildCreatures: ["voltix", "sparklet", "aetherling"],
guardLevel: 12,
bossCreature: "voltix",
bossLevel: 25,
storyNode: "storm_peaks_story",
shrine: true,
color: "#a78bfa"
},
{
id: "void_expanse",
name: "Void Expanse",
description: "A desolate plain where reality thins and shadow creatures prowl.",
bgGradient: "linear-gradient(180deg, #0a0a1a 0%, #151525 50%, #0d0d18 100%)",
particleColor: "rgba(100, 50, 150, 0.6)",
particleType: "void",
encounterRate: 0.25,
wildCreatures: ["gloamoth", "murkowl", "voidstalker"],
guardLevel: 15,
bossCreature: "voidstalker",
bossLevel: 30,
storyNode: "void_expanse_story",
shrine: true,
color: "#c084fc"
},
{
id: "shattered_realm",
name: "Shattered Realm",
description: "The final zone where all realities converge and the Weave itself frays.",
bgGradient: "linear-gradient(180deg, #1a0a2a 0%, #2a1a3a 50%, #150d20 100%)",
particleColor: "rgba(255, 200, 100, 0.8)",
particleType: "aether",
encounterRate: 0.30,
wildCreatures: ["aetherling", "glaci_crystal", "voltix_storm"],
guardLevel: 20,
bossCreature: "aetherling_cosmic",
bossLevel: 35,
storyNode: "shattered_realm_story",
shrine: false,
color: "#fbbf24"
}
];
// ── const: STORY_NODES ──
// Story nodes
const STORY_NODES = {
whispering_grove_story: {
title: "The Awakening",
vignette: "#0a2a0a",
text: "You awaken in the Whispering Grove, a place where the ancient Weave first took form. The spirits of the grove speak of a great fraying — the Weave is unraveling, and only by binding with the creatures of the realm can you hope to repair it.",
choices: [
{ text: "I will restore the Weave.", next: "starter_choice" },
{ text: "What creatures are these?", next: "starter_choice" }
]
},
starter_choice: {
title: "A Companion's Call",
vignette: "#1a3a1a",
text: "Three spirits emerge from the grove, each offering their bond. Choose wisely — your companion will journey with you through all the zones ahead.",
choices: [
{ text: "Bind with Emberl (Pyre)", action: "starter_emberl", next: "begin_exploration" },
{ text: "Bind with Gloamoth (Void)", action: "starter_gloamoth", next: "begin_exploration" },
{ text: "Bind with Sproutling (Verd)", action: "starter_sproutling", next: "begin_exploration" }
]
},
begin_exploration: {
title: "The Path Opens",
vignette: "#1a3a1a",
text: "Your new companion glows with warmth. The grove parts before you, revealing paths to the Ember Wastes, Crystal Caverns, and beyond. Rest at shrines to heal and strengthen your party. Defeat the Guardians to unlock each zone.",
choices: [
{ text: "Begin the journey!", next: null }
]
},
ember_wastes_story: {
title: "The Burning Path",
vignette: "#2a0a0a",
text: "The Ember Wastes stretch before you, a sea of molten sand and fire spirits. The Guardian here guards the flame that keeps the Weave from completely unraveling. Defeat it, and the path to the Crystal Caverns will open.",
choices: [{ text: "Challenge the Guardian!", next: null }]
},
crystal_caverns_story: {
title: "The Singing Stones",
vignette: "#0a1a2a",
text: "The Crystal Caverns hum with cold energy. The Frostweaver here weaves ice that preserves memories of the Weave's past. Its defeat will unlock the path to the Storm Peaks.",
choices: [{ text: "Face the Frostweaver!", next: null }]
},
storm_peaks_story: {
title: "The Thundering Heights",
vignette: "#1a1a2a",
text: "Lightning crashes around you as you climb the Storm Peaks. The Voltix here channels the raw energy of the Weave's future. Defeating it opens the way to the Void Expanse.",
choices: [{ text: "Climb higher!", next: null }]
},
void_expanse_story: {
title: "The Edge of Nothing",
vignette: "#0a0a1a",
text: "Reality thins here. The Voidstalker has been feeding on the Weave's frayed edges. Its defeat will open the path to the Shattered Realm — the final convergence point.",
choices: [{ text: "Enter the void!", next: null }]
},
shattered_realm_story: {
title: "The Final Convergence",
vignette: "#1a0a2a",
text: "You stand at the heart of the Shattered Realm. The Aetherling Cosmic guards the final thread of the Weave. Defeat it, collect 8 creatures, and the Weave will be restored.",
choices: [{ text: "Restore the Weave!", next: null }]
}
};
// ── const: ITEMS ──
// Items
const ITEMS = {
// Collection items
catch_sphere: { name: "Echo Sphere", type: "catch", tier: 1, power: 1.0, desc: "Basic collection sphere.", cost: 100 },
echo_sphere_plus: { name: "Echo Sphere+", type: "catch", tier: 2, power: 1.5, desc: "Improved collection sphere.", cost: 300 },
echo_sphere_ultimate: { name: "Echo Sphere Ult.", type: "catch", tier: 3, power: 2.0, desc: "Elite collection sphere.", cost: 800 },
// Restoration
health_draught: { name: "Health Draught", type: "heal", power: 30, desc: "Restores 30 HP to one creature.", cost: 50 },
health_draught_plus: { name: "Health Draught+", type: "heal", power: 60, desc: "Restores 60 HP to one creature.", cost: 100 },
health_draught_ultimate: { name: "Health Draught Ult.", type: "heal", power: 150, desc: "Fully restores one creature.", cost: 250 },
antidote: { name: "Antidote", type: "status", cure: "poison", desc: "Cures poison.", cost: 30 },
warmth_crystal: { name: "Warmth Crystal", type: "status", cure: "frost", desc: "Thaws frost.", cost: 30 },
lightning_rod: { name: "Lightning Rod", type: "status", cure: "paralyze", desc: "Cures paralysis.", cost: 30 },
revival_bloom: { name: "Revival Bloom", type: "revive", power: 0.5, desc: "Revives a fainted creature at 50% HP.", cost: 100 },
full_revival: { name: "Full Revival", type: "revive", power: 1.0, desc: "Fully revives a fainted creature.", cost: 250 },
// Catalysts (for transformation)
flame_core: { name: "Flame Core", type: "catalyst", creature: "emberl_fuse", desc: "Transforms Emberl into Emberl Fuse.", cost: 500 },
void_shard: { name: "Void Shard", type: "catalyst", creature: "gloamoth_void", desc: "Transforms Gloamoth into Gloamoth Void.", cost: 500 },
seed_of_life: { name: "Seed of Life", type: "catalyst", creature: "sproutling_ancient", desc: "Transforms Sproutling into Sproutling Ancient.", cost: 500 },
frost_heart: { name: "Frost Heart", type: "catalyst", creature: "glaci_crystal", desc: "Transforms Glaci into Glaci Crystal.", cost: 500 },
storm_heart: { name: "Storm Heart", type: "catalyst", creature: "voltix_storm", desc: "Transforms Voltix into Voltix Storm.", cost: 500 },
earth_core: { name: "Earth Core", type: "catalyst", creature: "lithos_titan", desc: "Transforms Lithos into Lithos Titan.", cost: 500 },
aether_crystal: { name: "Aether Crystal", type: "catalyst", creature: "aetherling_cosmic", desc: "Transforms Aetherling into Aetherling Cosmic.", cost: 500 },
pyre_gem: { name: "Pyre Gem", type: "catalyst", creature: "pyrewisp_blaze", desc: "Transforms Pyrewisp into Pyrewisp Blaze.", cost: 500 },
void_gem: { name: "Void Gem", type: "catalyst", creature: "murkowl_abyss", desc: "Transforms Murkowl into Murkowl Abyss.", cost: 500 },
// Field tools
map_scroll: { name: "Map Scroll", type: "field", desc: "Shows full map.", cost: 0 },
encounter_amp: { name: "Encounter Amp", type: "field", desc: "Increases encounter rate.", cost: 200 },
// EXP items
exp_shard: { name: "Exp. Shard", type: "exp", power: 50, desc: "Gives 50 EXP.", cost: 80 },
exp_crystal: { name: "Exp. Crystal", type: "exp", power: 150, desc: "Gives 150 EXP.", cost: 200 },
exp_gem: { name: "Exp. Gem", type: "exp", power: 400, desc: "Gives 400 EXP.", cost: 500 }
};
// ── const: ZONE_BOSSES ──
// Zone boss levels and story progression
const ZONE_BOSSES = [
{ zone: "whispering_grove", creature: "verdguardian", level: 12, bonus: "verd_resist" },
{ zone: "ember_wastes", creature: "emberdrake", level: 18, bonus: "pyre_resist" },
{ zone: "crystal_caverns", creature: "frostweaver", level: 20, bonus: "frost_resist" },
{ zone: "storm_peaks", creature: "voltix", level: 25, bonus: "storm_resist" }
];
// ── const: POSTGAME_MODIFIERS ──
// Post-game harder spawns
const POSTGAME_MODIFIERS = {
encounterRate: 0.40,
levelBonus: 5,
expMultiplier: 1.5
};
// ── const: TYPE_COLORS ──
// Type colors for UI
const TYPE_COLORS = {
pyre: "#f97316",
void: "#a855f7",
verd: "#4ade80",
frost: "#60a5fa",
storm: "#c084fc",
stone: "#d4a574",
aether: "#fbbf24"
};
// ── const: PARTICLE_TYPES ──
// Particle types
const PARTICLE_TYPES = {
leaf: { shape: "leaf", size: [4, 8], speed: [0.3, 0.8], drift: true },
ember: { shape: "ember", size: [3, 6], speed: [0.5, 1.2], drift: true },
crystal: { shape: "crystal", size: [3, 5], speed: [0.2, 0.5], drift: false },
spark: { shape: "spark", size: [2, 4], speed: [0.8, 1.5], drift: true },
void: { shape: "void", size: [5, 10], speed: [0.1, 0.4], drift: true },
aether: { shape: "aether", size: [4, 7], speed: [0.3, 0.7], drift: true }
};
// ── const: CREATURE_COLORS ──
// Creature sprite colors
const CREATURE_COLORS = {
emberl: "#f97316",
gloamoth: "#a855f7",
sproutling: "#4ade80",
glaci: "#60a5fa",
voltix: "#c084fc",
lithos: "#d4a574",
aetherling: "#fbbf24",
pyrewisp: "#fb923c",
murkowl: "#8b5cf6",
mosspon: "#22c55e",
frostdrop: "#38bdf8",
sparklet: "#e879f9",
cobble: "#b45309",
phasling: "#fde047",
emberdrake: "#dc2626",
voidstalker: "#7c3aed",
verdguardian: "#16a34a",
frostweaver: "#0ea5e9",
emberl_fuse: "#ef4444",
gloamoth_void: "#6b21a8",
sproutling_ancient: "#15803d",
glaci_crystal: "#0284c7",
voltix_storm: "#9333ea",
lithos_titan: "#92400e",
aetherling_cosmic: "#eab308",
pyrewisp_blaze: "#f97316",
murkowl_abyss: "#581c87"
};
// ── const: EFFECT_DESC ──
// Ability effect descriptions
const EFFECT_DESC = {
burn: "Inflicts burn (DoT + reduced ATK)",
frost: "Inflicts frost (halves SPD)",
paralyze: "Chance to skip turn",
confuse: "Target may attack itself",
poison: "Poison DoT",
sleep: "Target sleeps 1-3 turns",
drain: "Heals user for half damage",
heal: "Restores 50% max HP",
heal_all: "Restores all party 30% HP",
regen: "Heals 10% HP per turn",
barrier: "One-hit deflect",
defense_up: "+1 DEF stage",
attack_up: "+1 ATK stage",
speed_up: "+1 SPD stage",
defense_down: "-1 DEF stage",
attack_down: "-1 ATK stage",
speed_down: "-1 SPD stage",
accuracy_down: "-1 ACC stage",
phase: "May dodge next attack",
bind: "Traps target, can't switch",
flinch: "Target may flinch",
fear: "Target may flee",
crit_up: "Higher crit chance",
revive: "Revives at 50% if fainted",
absorb: "Absorbs enemy's buff",
ignore_def: "Ignores DEF",
none: "No special effect"
};
// ── let: canvas ──
// ═══════════════════════════════════════════════════════════════
// GAME ENGINE - Core Systems
// ═══════════════════════════════════════════════════════════════
let canvas;
// ── let: ctx ──
let ctx;
// ── let: W ──
let W;
// ── let: H ──
let H;
// ── let: gameState ──
let gameState = "title";
// ── let: audioCtx ──
let audioCtx = null;
// ── let: isMuted ──
let isMuted = false;
// ── let: animFrame ──
let animFrame = 0;
// ── let: player ──
// Player state
let player = {
party: [],
inventory: {},
money: 200,
currentZone: 0,
unlockedZones: [0],
defeatedBosses: [],
collectedCreatures: [],
storyProgress: {},
totalCreatures: 0,
postgame: false,
lastShrine: 0
};
// ── let: battle ──
// Battle state
let battle = null;
// ── let: battleTurnOrder ──
let battleTurnOrder = [];
// ── let: battleIndex ──
let battleIndex = 0;
// ── let: battleLog ──
let battleLog = [];
// ── let: battleAnim ──
let battleAnim = null;
// ── let: floatingTexts ──
let floatingTexts = [];
// ── let: shakeTimer ──
let shakeTimer = 0;
// ── let: battlePhase ──
let battlePhase = "select";
// ── let: overworld ──
// Overworld state
let overworld = {
camX: 0,
camY: 0,
playerX: 0,
playerY: 0,
tiles: [],
tileW: 40,
tileH: 40,
npcs: [],
interactables: [],
particles: []
};
// ── let: menuTab ──
// Menu state
let menuTab = "party";
// ── let: menuCreatureIndex ──
let menuCreatureIndex = 0;
// ── let: menuItemIndex ──
let menuItemIndex = 0;
// ── let: keys ──
// Input
let keys = {};
// ── let: touchActive ──
let touchActive = {};
// ── let: captureState ──
// Capture state
let captureState = null;
// ── function: createCreature ──
// ═══════════════════════════════════════════════════════════════
// CREATURE SYSTEM
// ═══════════════════════════════════════════════════════════════
function createCreature(specId, level) {
const spec = CREATURES[specId];
if (!spec) return null;
const stats = calcStats(spec, level);
// Learn abilities based on level
const availableAbilities = [];
const allAbilities = spec.abilities || [];
const levels = [1, 5, 12, 20];
for (let i = 0; i < Math.min(levels.length, allAbilities.length); i++) {
if (level >= levels[i]) {
availableAbilities.push(allAbilities[i]);
}
}
while (availableAbilities.length < 4 && allAbilities.length > availableAbilities.length) {
availableAbilities.push(allAbilities[availableAbilities.length]);
}
return {
id: specId,
name: spec.name,
level: level,
exp: 0,
expToNext: getExpToLevel(level),
maxHp: stats.hp,
hp: stats.hp,
atk: stats.atk,
def: stats.def,
spd: stats.spd,
affinity: spec.affinity,
abilities: availableAbilities.slice(0, 4),
pp: {},
status: null,
statusTurns: 0,
buffs: { atk: 0, def: 0, spd: 0, acc: 0 },
barrier: false,
fainted: false,
captured: false
};
}
// ── function: calcStats ──
function calcStats(spec, level) {
const hp = Math.floor((2 * spec.baseHp + 10) * level / 100 + level + 10);
const atk = Math.floor((2 * spec.baseAtk + 5) * level / 100 + 5);
const def = Math.floor((2 * spec.baseDef + 5) * level / 100 + 5);
const spd = Math.floor((2 * spec.baseSpd + 5) * level / 100 + 5);
return { hp, atk, def, spd };
}
// ── function: getExpToLevel ──
function getExpToLevel(level) {
return Math.floor(2 * level * level * level / 3);
}
// ── function: getExpNeeded ──
function getExpNeeded(level) {
return Math.floor(2 * (level + 1) * (level + 1) * (level + 1) / 3);
}
// ── function: addExp ──
function addExp(creature, expAmount) {
creature.exp += expAmount;
while (creature.exp >= getExpNeeded(creature.level) && creature.level < 50) {
creature.exp -= getExpNeeded(creature.level);
creature.level++;
const spec = CREATURES[creature.id];
const newStats = calcStats(spec, creature.level);
const hpDiff = newStats.hp - creature.maxHp;
creature.maxHp = newStats.hp;
creature.hp = Math.min(creature.hp + hpDiff, creature.maxHp);
creature.atk = newStats.atk;
creature.def = newStats.def;
creature.spd = newStats.spd;
const allAbilities = spec.abilities || [];
const levels = [1, 5, 12, 20];
for (let i = 0; i < levels.length; i++) {
if (levels[i] === creature.level && !creature.abilities.includes(allAbilities[i])) {
if (creature.abilities.length < 4) {
creature.abilities.push(allAbilities[i]);
showBattleLog(creature.name + " learned " + (ABILITIES[allAbilities[i]]?.name || allAbilities[i]) + "!", "log-effect");
}
}
}
showBattleLog(creature.name + " grew to level " + creature.level + "!", "log-effect");
}
}
// ── function: getEffectiveness ──
// ═══════════════════════════════════════════════════════════════
// TYPE EFFECTIVENESS & DAMAGE
// ═══════════════════════════════════════════════════════════════
function getEffectiveness(abilityType, targetAffinity) {
// Type A (pyre) > Type B (void)
if (abilityType === "pyre" && targetAffinity === "void") return 2.0;
// Type B (void) > Type C (verd)
if (abilityType === "void" && targetAffinity === "verd") return 2.0;
// Type C (verd) > Type A (pyre)
if (abilityType === "verd" && targetAffinity === "pyre") return 2.0;
// Type D (frost) > Type A (pyre) & Type C (verd)
if (abilityType === "frost" && (targetAffinity === "pyre" || targetAffinity === "verd")) return 2.0;
// Type E (storm) > Type D (frost) & Type B (void)
if (abilityType === "storm" && (targetAffinity === "frost" || targetAffinity === "void")) return 2.0;
// Type F (stone) beats all others slightly
if (abilityType === "stone" && targetAffinity !== "stone") return 1.25;
// Type G (aether) resists everything but weak to Type F (stone)
if (targetAffinity === "aether" && abilityType !== "stone") return 0.75;
if (abilityType === "stone" && targetAffinity === "aether") return 2.0;
return 1.0;
}
// ── function: getBuffMultiplier ──
function getBuffMultiplier(stage) {
const clamped = Math.max(-6, Math.min(6, stage));
return (2 + clamped) / 2;
}
// ── function: calcDamage ──
function calcDamage(attacker, defender, abilityId) {
const ability = ABILITIES[abilityId];
if (!ability) return { damage: 0, effectiveness: 1, crit: false, missed: false };
if (Math.random() * 100 > ability.accuracy) {
return { damage: 0, effectiveness: 1, crit: false, missed: true };
}
let power = ability.power;
const stab = ability.type === attacker.affinity ? 1.5 : 1.0;
const effectiveness = getEffectiveness(ability.type, defender.affinity);
const atkStat = Math.max(1, Math.floor(attacker.atk * getBuffMultiplier(attacker.buffs.atk)));
const defStat = Math.max(1, Math.floor(defender.def * getBuffMultiplier(defender.buffs.def)));
const level = attacker.level;
const baseDamage = (0.4 * level * atkStat / defStat) * power * stab * effectiveness;
const randomFactor = 0.85 + Math.random() * 0.30;
let damage = Math.floor(baseDamage * randomFactor) + 1;
let crit = false;
if (Math.random() < 0.0625) {
damage = Math.floor(damage * 1.5);
crit = true;
}
damage = Math.max(1, damage);
return { damage, effectiveness, crit, missed: false, ability };
}
// ── function: startBattle ──
// ═══════════════════════════════════════════════════════════════
// BATTLE SYSTEM
// ═══════════════════════════════════════════════════════════════
function startBattle(specIds, isBoss) {
if (isBoss === undefined) isBoss = false;
const enemies = [];
if (isBoss) {
const spec = CREATURES[specIds[0]];
const level = specIds[1] || 10;
const enemy = createCreature(specIds[0], level);
enemy.isBoss = true;
enemies.push(enemy);
} else {
const count = specIds.length > 1 ? specIds.length : 1;
for (let i = 0; i < count; i++) {
const specId = specIds[i] || specIds[0];
const level = 3 + Math.floor(Math.random() * 5) + player.currentZone * 3;
enemies.push(createCreature(specId, level));
}
}
battle = {
enemies: enemies,
playerParty: player.party.filter(function(c) { return !c.fainted; }),
turnOrder: [],
turnIndex: 0,
phase: "select",
selectedAbility: null,
selectedTarget: null,
selectedSwitch: null,
log: []
};
battleTurnOrder = calculateTurnOrder();
battleIndex = 0;
battleLog = [];
battlePhase = "select";
gameState = "battle";
document.getElementById("battle-ui").classList.add("active");
document.getElementById("hud").style.display = "none";
renderBattleUI();
processBattleTurn();
}
// ── function: calculateTurnOrder ──
function calculateTurnOrder() {
const units = [];
player.party.forEach(function(c, i) {
if (!c.fainted) {
units.push({ type: "player", index: i, spd: c.spd, creature: c });
}
});
battle.enemies.forEach(function(e, i) {
if (!e.fainted) {
units.push({ type: "enemy", index: i, spd: e.spd, creature: e });
}
});
units.sort(function(a, b) { return b.spd - a.spd; });
return units;
}
// ── function: processBattleTurn ──
function processBattleTurn() {
if (battlePhase !== "select") return;
battleTurnOrder = calculateTurnOrder();
battleIndex = 0;
if (battleTurnOrder.length === 0) { endBattle(); return; }
showBattleTurn();
}
// ── function: showBattleTurn ──
function showBattleTurn() {
if (battleIndex >= battleTurnOrder.length) {
battleIndex = 0;
return;
}
const unit = battleTurnOrder[battleIndex];
if (unit.type === "player") {
const creature = player.party[unit.index];
if (creature.fainted) { battleIndex++; showBattleTurn(); return; }
battlePhase = "player_select";
renderPlayerTurn(creature);
} else {
const enemy = battle.enemies[unit.index];
if (enemy.fainted) { battleIndex++; showBattleTurn(); return; }
battlePhase = "enemy_turn";
setTimeout(function() { enemyTurn(enemy); }, 800);
}
}
// ── function: renderPlayerTurn ──
function renderPlayerTurn(creature) {
const actionsDiv = document.getElementById("battle-actions");
actionsDiv.innerHTML = "";
creature.abilities.forEach(function(abilId) {
const ability = ABILITIES[abilId];
if (!ability) return;
const btn = document.createElement("button");
const pp = creature.pp[abilId] || ability.pp;
btn.textContent = ability.name + " (" + pp + "/" + ability.pp + ")";
btn.onclick = function() { selectAbility(creature, abilId); };
actionsDiv.appendChild(btn);
});
const switchBtn = document.createElement("button");
switchBtn.textContent = "Switch";
switchBtn.onclick = function() { showSwitchMenu(creature); };
actionsDiv.appendChild(switchBtn);
const itemBtn = document.createElement("button");
itemBtn.textContent = "Item";
itemBtn.onclick = function() { showItemMenu(); };
actionsDiv.appendChild(itemBtn);
const runBtn = document.createElement("button");
runBtn.textContent = "Run";
runBtn.onclick = function() { attemptRun(); };
actionsDiv.appendChild(runBtn);
renderBattleCreatureInfo();
}
// ── function: renderBattleCreatureInfo ──
function renderBattleCreatureInfo() {
const topDiv = document.getElementById("battle-hud-top");
const playerArea = document.getElementById("battle-player-area");
topDiv.innerHTML = "";
battle.enemies.forEach(function(enemy, i) {
if (enemy.fainted) return;
const hud = document.createElement("div");
hud.className = "enemy-hud";
hud.id = "enemy-hud-" + i;
hud.innerHTML = '<div class="eh-name">' + enemy.name + '</div>' +
'<div class="eh-level">Lv.' + enemy.level + '</div>' +
'<div class="eh-hp-bar"><div class="eh-hp-fill" style="width:' + (enemy.hp/enemy.maxHp*100) + '%"></div></div>';
topDiv.appendChild(hud);
});
const activeCreature = getActivePlayerCreature();
if (activeCreature) {
playerArea.innerHTML = '<div class="player-hud">' +
'<div class="ph-name">' + activeCreature.name + '</div>' +
'<div class="ph-level">Lv.' + activeCreature.level + '</div>' +
'<div class="ph-hp-bar"><div class="ph-hp-fill ' + getHpClass(activeCreature) + '" style="width:' + (activeCreature.hp/activeCreature.maxHp*100) + '%"></div></div>' +
'<div class="ph-exp-bar"><div class="ph-exp-fill" style="width:' + (activeCreature.exp/getExpNeeded(activeCreature.level)*100) + '%"></div></div>' +
'</div>';
}
}
// ── function: getActivePlayerCreature ──
function getActivePlayerCreature() {
for (let i = 0; i < player.party.length; i++) {
if (!player.party[i].fainted) return player.party[i];
}
return null;
}
// ── function: getHpClass ──
function getHpClass(creature) {
const pct = creature.hp / creature.maxHp;
if (pct < 0.2) return "low";
if (pct < 0.5) return "mid";
return "";
}
// ── function: selectAbility ──
function selectAbility(creature, abilityId) {
battlePhase = "animating";
battle.selectedAbility = abilityId;
const target = battle.enemies.find(function(e) { return !e.fainted; });
if (!target) { endBattle(); return; }
battle.selectedTarget = target;
useAbility(creature, target, abilityId);
}
// ── function: useAbility ──
function useAbility(attacker, defender, abilityId) {
const ability = ABILITIES[abilityId];
if (!ability) return;
if (!attacker.pp[abilityId]) attacker.pp[abilityId] = ability.pp;
if (attacker.pp[abilityId] <= 0) {
showBattleLog("No PP remaining!", "log-noteff");
battlePhase = "player_select";
return;
}
attacker.pp[abilityId]--;
if (defender.barrier) {
defender.barrier = false;
showBattleLog(defender.name + "'s barrier absorbed the attack!", "log-effect");
showFloatingText("enemy", "Blocked!", "heal");
battleIndex++;
setTimeout(showBattleTurn, 1000);
return;
}
const result = calcDamage(attacker, defender, abilityId);
if (result.missed) {
showBattleLog(attacker.name + " used " + ability.name + " but missed!", "log-noteff");
showFloatingText("enemy", "Miss!", "miss");
} else {
defender.hp = Math.max(0, defender.hp - result.damage);
let logMsg = attacker.name + " used " + ability.name + "!";
if (result.crit) logMsg += " Critical hit!";
if (result.effectiveness > 1) logMsg += " It's super effective!";
if (result.effectiveness < 1) logMsg += " It's not very effective...";
showBattleLog(logMsg, result.effectiveness > 1 ? "log-supereff" : result.crit ? "log-effect" : "");
showFloatingText("enemy", result.damage, result.crit ? "crit" : result.effectiveness > 1 ? "super" : "");
if (result.effectiveness > 1) {
shakeTimer = 15;
spawnBattleParticles(TYPE_COLORS[ability.type]);
}
if (ability.effect === "drain") {
const heal = Math.floor(result.damage * 0.5);
attacker.hp = Math.min(attacker.maxHp, attacker.hp + heal);
showFloatingText("player", "+" + heal, "heal");
}
if (ability.effect && ability.effect !== "none" && ability.effect !== "drain" && ability.power > 0) {
applyStatusEffect(defender, ability.effect);
}
if (defender.hp <= 0) {
defender.fainted = true;
showBattleLog(defender.name + " fainted!", "log-effect");
}
}
if (checkBattleEnd()) return;
battleIndex++;
setTimeout(showBattleTurn, 1200);
}
// ── function: applyStatusEffect ──
function applyStatusEffect(creature, effect) {
if (creature.status) return;
const chance = effect === "paralyze" ? 0.3 : effect === "burn" ? 0.3 :
effect === "frost" ? 0.5 : effect === "confuse" ? 0.5 :
effect === "poison" ? 0.4 : effect === "sleep" ? 0.75 : 0.3;
if (Math.random() < chance) {
creature.status = effect;
creature.statusTurns = effect === "sleep" ? 1 + Math.floor(Math.random() * 3) : 0;
showBattleLog(creature.name + " was afflicted with " + effect + "!", "log-effect");
}
}
// ── function: enemyTurn ──
function enemyTurn(enemy) {
if (enemy.fainted) { battleIndex++; showBattleTurn(); return; }
const availableAbilities = enemy.abilities.filter(function(id) {
const ability = ABILITIES[id];
return ability && (!enemy.pp[id] || enemy.pp[id] > 0);
});
if (availableAbilities.length === 0) {
const playerTarget = getActivePlayerCreature();
if (playerTarget) {
const result = calcDamage(enemy, playerTarget, "tackle");
if (!result.missed) {
playerTarget.hp = Math.max(0, playerTarget.hp - result.damage);
showBattleLog(enemy.name + " struggled!", "log-noteff");
showFloatingText("player", result.damage, "");
}
}
} else {
const abilityId = availableAbilities[Math.floor(Math.random() * availableAbilities.length)];
const playerTarget = getActivePlayerCreature();
if (playerTarget) {
useAbility(enemy, playerTarget, abilityId);
return;
}
}
battleIndex++;
setTimeout(showBattleTurn, 1000);
}
// ── function: showSwitchMenu ──
function showSwitchMenu(creature) {
const actionsDiv = document.getElementById("battle-actions");
actionsDiv.innerHTML = "";
player.party.forEach(function(c, i) {
if (c.fainted) return;
if (c === creature) return;
const btn = document.createElement("button");
btn.textContent = c.name + " (Lv." + c.level + ")";
btn.onclick = function() {
const emptySlot = player.party.findIndex(function(p) { return p.fainted; });
if (emptySlot >= 0) {
player.party[emptySlot] = c;
} else {
const faintedIdx = player.party.findIndex(function(p) { return p.fainted; });
if (faintedIdx >= 0) player.party[faintedIdx] = c;
}
c.fainted = false;
c.hp = c.maxHp;
showBattleLog("Sent out " + c.name + "!", "log-effect");
battlePhase = "player_select";
renderPlayerTurn(c);
};
actionsDiv.appendChild(btn);
});
const backBtn = document.createElement("button");
backBtn.textContent = "Back";
backBtn.onclick = function() { renderPlayerTurn(creature); };
actionsDiv.appendChild(backBtn);
}
// ── function: showItemMenu ──
function showItemMenu() {
const actionsDiv = document.getElementById("battle-actions");
actionsDiv.innerHTML = "";
const usableItems = Object.keys(player.inventory).filter(function(id) {
const item = ITEMS[id];
return item && (item.type === "heal" || item.type === "revive" || item.type === "status");
});
if (usableItems.length === 0) {
const msg = document.createElement("div");
msg.style.color = "rgba(255,255,255,0.5)";
msg.textContent = "No usable items";
actionsDiv.appendChild(msg);
}
usableItems.forEach(function(id) {
const item = ITEMS[id];
const count = player.inventory[id];
if (count <= 0) return;
const btn = document.createElement("button");
btn.textContent = item.name + " x" + count;
btn.onclick = function() { useBattleItem(id); };
actionsDiv.appendChild(btn);
});
const backBtn = document.createElement("button");
backBtn.textContent = "Back";
backBtn.onclick = function() {
const creature = getActivePlayerCreature();
if (creature) renderPlayerTurn(creature);
};
actionsDiv.appendChild(backBtn);
}
// ── function: useBattleItem ──
function useBattleItem(itemId) {
const item = ITEMS[itemId];
if (!item || player.inventory[itemId] <= 0) return;
player.inventory[itemId]--;
if (item.type === "heal") {
const target = getActivePlayerCreature();
if (target) {
const heal = Math.min(item.power, target.maxHp - target.hp);
target.hp += heal;
showBattleLog("Used " + item.name + "! Recovered " + heal + " HP.", "log-effect");
showFloatingText("player", "+" + heal, "heal");
}
} else if (item.type === "revive") {
const target = player.party.find(function(c) { return c.fainted; });
if (target) {
target.fainted = false;
target.hp = Math.floor(target.maxHp * item.power);
showBattleLog("Used " + item.name + "! " + target.name + " revived!", "log-effect");
showFloatingText("player", "Revived!", "heal");
}
} else if (item.type === "status") {
const target = getActivePlayerCreature();
if (target && target.status === item.cure) {
target.status = null;
showBattleLog("Used " + item.name + "! Cured " + item.cure + "!", "log-effect");
}
}
battlePhase = "player_select";
const creature = getActivePlayerCreature();
if (creature) renderPlayerTurn(creature);
}
// ── function: attemptRun ──
function attemptRun() {
const speedDiff = getActivePlayerCreature()?.spd || 10;
const enemySpeed = battle.enemies.find(function(e) { return !e.fainted; })?.spd || 10;
if (Math.random() < 0.5 + speedDiff / (speedDiff + enemySpeed) * 0.3) {
showBattleLog("Got away safely!", "log-effect");
setTimeout(function() { endBattle(); }, 1000);
} else {
showBattleLog("Couldn't escape!", "log-noteff");
battlePhase = "enemy_turn";
setTimeout(function() { enemyTurn(battle.enemies.find(function(e) { return !e.fainted; })); }, 800);
}
}
// ── function: checkBattleEnd ──
function checkBattleEnd() {
const allEnemiesFainted = battle.enemies.every(function(e) { return e.fainted; });
const allPlayerFainted = player.party.every(function(c) { return c.fainted; });
if (allEnemiesFainted) {
showBattleLog("Enemy party defeated!", "log-supereff");
setTimeout(function() { endBattle(true); }, 1500);
return true;
}
if (allPlayerFainted) {
showBattleLog("All party members fainted!", "log-supereff");
setTimeout(function() { endBattle(false); }, 1500);
return true;
}
return false;
}
// ── function: endBattle ──
function endBattle(playerWon) {
if (playerWon === undefined) playerWon = false;
battlePhase = "end";
if (playerWon) {
battle.enemies.forEach(function(enemy) {
const expGain = Math.floor(enemy.exp * (1 + enemy.level / 20));
player.party.forEach(function(c) {
if (!c.fainted) addExp(c, expGain);
});
});
const moneyGain = 20 + battle.enemies.reduce(function(sum, e) { return sum + e.level * 5; }, 0);
player.money += moneyGain;
const aliveEnemy = battle.enemies.find(function(e) { return !e.isBoss && e.hp > 0; });
if (aliveEnemy && !aliveEnemy.isBoss) {
startCaptureGame(aliveEnemy);
return;
}
const bossDefeated = battle.enemies.find(function(e) { return e.isBoss; });
if (bossDefeated) {
const zone = ZONES[player.currentZone];
if (zone && !player.defeatedBosses.includes(zone.id)) {
player.defeatedBosses.push(zone.id);
showBattleLog(zone.name + " Guardian defeated!", "log-supereff");
if (player.currentZone < ZONES.length - 1) {
const nextZone = ZONES[player.currentZone + 1];
if (nextZone && !player.unlockedZones.includes(nextZone.id)) {
player.unlockedZones.push(nextZone.id);
showBattleLog(nextZone.name + " unlocked!", "log-supereff");
}
}
if (player.defeatedBosses.length >= 4 && player.collectedCreatures.length >= 8) {
gameState = "victory";
document.getElementById("battle-ui").classList.remove("active");
showVictoryScreen();
return;
}
}
}
if (player.defeatedBosses.length >= 4 && !player.postgame) {
player.postgame = true;
showBattleLog("Post-game unlocked! Wild spawns are stronger.", "log-supereff");
}
}
player.party.forEach(function(c) {
if (!c.fainted) c.hp = Math.min(c.maxHp, c.hp + Math.floor(c.maxHp * 0.1));
});
setTimeout(function() {
gameState = "overworld";
document.getElementById("battle-ui").classList.remove("active");
document.getElementById("hud").style.display = "block";
updatePartyHud();
renderOverworld();
}, 2000);
}
// ── function: startCaptureGame ──
// ═══════════════════════════════════════════════════════════════
// CAPTURE SYSTEM
// ═══════════════════════════════════════════════════════════════
function startCaptureGame(enemy) {
gameState = "capture";
battlePhase = "capture";
captureState = {
enemy: enemy,
zone: { start: 10, width: 80 },
cursorPos: 0,
cursorDir: 1,
cursorSpeed: 2 + Math.random() * 2,
tapped: false,
result: null
};
document.getElementById("capture-game").classList.add("active");
const hpRatio = enemy.hp / enemy.maxHp;
captureState.zone.width = Math.max(20, 80 - hpRatio * 60);
captureState.zone.start = 10 + Math.random() * (100 - captureState.zone.width - 10);
captureLoop();
}
// ── function: captureLoop ──
function captureLoop() {
if (gameState !== "capture") return;
if (!captureState.tapped) {
captureState.cursorPos += captureState.cursorDir * captureState.cursorSpeed;
if (captureState.cursorPos >= 100 || captureState.cursorPos <= 0) {
captureState.cursorDir *= -1;
}
const cursor = document.querySelector(".cg-cursor");
if (cursor) cursor.style.left = captureState.cursorPos + "%";
requestAnimationFrame(captureLoop);
}
}
// ── function: attemptCapture ──
function attemptCapture() {
if (captureState.tapped) return;
captureState.tapped = true;
const { zone, cursorPos, enemy } = captureState;
const inZone = cursorPos >= zone.start && cursorPos <= zone.start + zone.width;
const hpRatio = enemy.hp / enemy.maxHp;
const baseChance = (1 - hpRatio) * 0.8 + 0.1;
const itemBonus = player.inventory["catch_sphere"] || player.inventory["echo_sphere_plus"] || 0;
const itemPower = itemBonus > 0 ? (ITEMS["echo_sphere_plus"]?.power || ITEMS["catch_sphere"]?.power || 1) : 1;
const finalChance = Math.min(0.95, baseChance * itemPower);
const caught = inZone ? Math.random() < finalChance : Math.random() < finalChance * 0.5;
captureState.result = caught;
if (caught) {
showBattleLog(enemy.name + " captured!", "log-catch");
showFloatingText("enemy", "Captured!", "heal");
const newCreature = createCreature(enemy.id, enemy.level);
newCreature.captured = true;
if (player.party.length < 6) {
player.party.push(newCreature);
} else {
const faintedIdx = player.party.findIndex(function(c) { return c.fainted; });
if (faintedIdx >= 0) player.party[faintedIdx] = newCreature;
}
player.collectedCreatures.push(enemy.id);
player.totalCreatures++;
} else {
showBattleLog(enemy.name + " broke free!", "log-noteff");
showFloatingText("enemy", "Broke free!", "miss");
}
setTimeout(function() {
document.getElementById("capture-game").classList.remove("active");
captureState = null;
setTimeout(function() { endBattle(true); }, 1000);
}, 1500);
}
// ── let: storyQueue ──
// ═══════════════════════════════════════════════════════════════
// STORY SYSTEM
// ═══════════════════════════════════════════════════════════════
let storyQueue = [];
// ── function: startStory ──
function startStory(nodeId) {
if (storyQueue.length === 0) gameState = "story";
storyQueue.push(nodeId);
processStory();
}
// ── function: processStory ──
function processStory() {
if (storyQueue.length === 0) {
gameState = "overworld";
document.getElementById("dialogue-box").classList.remove("active");
return;
}
const nodeId = storyQueue.shift();
const node = STORY_NODES[nodeId];
if (!node) { processStory(); return; }
const dialogueBox = document.getElementById("dialogue-box");
const textDiv = document.getElementById("dialogue-text");
const choicesDiv = document.getElementById("dialogue-choices");
dialogueBox.classList.add("active");
textDiv.innerHTML = '<div class="speaker">' + node.title + '</div>' + node.text;
choicesDiv.innerHTML = "";
node.choices.forEach(function(choice) {
const btn = document.createElement("button");
btn.textContent = choice.text;
btn.onclick = function() {
if (choice.action) handleStarterChoice(choice.action);
if (choice.next) storyQueue.push(choice.next);
processStory();
};
choicesDiv.appendChild(btn);
});
}
// ── function: handleStarterChoice ──
function handleStarterChoice(action) {
let specId;
switch (action) {
case "starter_emberl": specId = "emberl"; break;
case "starter_gloamoth": specId = "gloamoth"; break;
case "starter_sproutling": specId = "sproutling"; break;
default: specId = "emberl";
}
const starter = createCreature(specId, 5);
player.party.push(starter);
player.collectedCreatures.push(specId);
player.totalCreatures++;
player.inventory["catch_sphere"] = 5;
player.inventory["health_draught"] = 3;
}
// ── function: initOverworld ──
// ═══════════════════════════════════════════════════════════════
// OVERWORLD SYSTEM
// ═══════════════════════════════════════════════════════════════
function initOverworld() {
const zone = ZONES[player.currentZone];
if (!zone) return;
overworld.tiles = [];
const cols = Math.ceil(W / overworld.tileW) + 2;
const rows = Math.ceil(H / overworld.tileH) + 2;
for (let y = 0; y < rows; y++) {
overworld.tiles[y] = [];
for (let x = 0; x < cols; x++) {
const noise = Math.sin(x * 0.3) * Math.cos(y * 0.3) + Math.sin(x * 0.1 + y * 0.2) * 0.5;
overworld.tiles[y][x] = noise > 0 ? 1 : 0;
}
}
overworld.playerX = W / 2;
overworld.playerY = H / 2;
overworld.camX = 0;
overworld.camY = 0;
overworld.npcs = [];
overworld.interactables = [];
if (zone.shrine) {
overworld.interactables.push({ x: W / 2, y: H / 2 - 80, type: "shrine", zone: player.currentZone });
}
if (!player.storyProgress[zone.storyNode]) {
overworld.interactables.push({ x: W / 2, y: H / 2 + 80, type: "story", nodeId: zone.storyNode });
}
if (player.currentZone > 0) {
overworld.interactables.push({ x: 50, y: H / 2, type: "zone", targetZone: player.currentZone - 1 });
}
if (player.currentZone < ZONES.length - 1 && player.unlockedZones.includes(ZONES[player.currentZone + 1]?.id)) {
overworld.interactables.push({ x: W - 50, y: H / 2, type: "zone", targetZone: player.currentZone + 1 });
}
overworld.particles = [];
for (let i = 0; i < 30; i++) {
overworld.particles.push({
x: Math.random() * W,
y: Math.random() * H,
vx: (Math.random() - 0.5) * 0.5,
vy: -Math.random() * 0.5 - 0.2,
size: 2 + Math.random() * 4,
alpha: 0.3 + Math.random() * 0.5,
type: zone.particleType
});
}
document.getElementById("zone-label").textContent = zone.name;
document.getElementById("zone-label").style.display = "block";
renderOverworld();
}
// ── function: renderOverworld ──
function renderOverworld() {
const zone = ZONES[player.currentZone];
if (!zone) return;
ctx.fillStyle = "#050510";
ctx.fillRect(0, 0, W, H);
const gradient = ctx.createLinearGradient(0, 0, 0, H);
gradient.addColorStop(0, "#0a1a0a");
gradient.addColorStop(0.5, "#0d1f0d");
gradient.addColorStop(1, "#050510");
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, W, H);
const startCol = Math.floor(overworld.camX / overworld.tileW);
const endCol = startCol + Math.ceil(W / overworld.tileW) + 1;
const startRow = Math.floor(overworld.camY / overworld.tileH);
const endRow = startRow + Math.ceil(H / overworld.tileH) + 1;
for (let y = startRow; y <= endRow; y++) {
for (let x = startCol; x <= endCol; x++) {
if (y < 0 || !overworld.tiles[y] || x < 0 || x >= overworld.tiles[y].length) continue;
if (overworld.tiles[y][x] === 1) {
ctx.fillStyle = zone.color + "20";
ctx.fillRect(x * overworld.tileW - overworld.camX, y * overworld.tileH - overworld.camY, overworld.tileW, overworld.tileH);
}
}
}
overworld.interactables.forEach(function(interactable) {
const sx = interactable.x - overworld.camX;
const sy = interactable.y - overworld.camY;
if (interactable.type === "shrine") {
ctx.fillStyle = "#fbbf24";
ctx.beginPath();
ctx.arc(sx, sy, 20, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#fff";
ctx.font = "12px sans-serif";
ctx.textAlign = "center";
ctx.fillText("Shrine", sx, sy - 25);
} else if (interactable.type === "story") {
ctx.fillStyle = "#a78bfa";
ctx.beginPath();
ctx.arc(sx, sy, 15, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#fff";
ctx.font = "10px sans-serif";
ctx.textAlign = "center";
ctx.fillText("Story", sx, sy - 20);
} else if (interactable.type === "zone") {
ctx.fillStyle = "#60a5fa";
ctx.beginPath();
ctx.arc(sx, sy, 15, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#fff";
ctx.font = "10px sans-serif";
ctx.textAlign = "center";
ctx.fillText("Portal", sx, sy - 20);
}
});
overworld.npcs.forEach(function(npc) {
const sx = npc.x - overworld.camX;
const sy = npc.y - overworld.camY;
ctx.fillStyle = npc.color || "#f97316";
ctx.beginPath();
ctx.arc(sx, sy, 12, 0, Math.PI * 2);
ctx.fill();
});
const px = overworld.playerX - overworld.camX;
const py = overworld.playerY - overworld.camY;
const playerGlow = ctx.createRadialGradient(px, py, 0, px, py, 30);
playerGlow.addColorStop(0, "rgba(255, 200, 100, 0.5)");
playerGlow.addColorStop(1, "rgba(255, 200, 100, 0)");
ctx.fillStyle = playerGlow;
ctx.beginPath();
ctx.arc(px, py, 30, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#fff";
ctx.beginPath();
ctx.arc(px, py, 10, 0, Math.PI * 2);
ctx.fill();
overworld.particles.forEach(function(p) {
const sx = p.x - overworld.camX;
const sy = p.y - overworld.camY;
ctx.fillStyle = zone.particleColor.replace("0.6", p.alpha.toString());
ctx.beginPath();
ctx.arc(sx, sy, p.size, 0, Math.PI * 2);
ctx.fill();
});
updatePartyHud();
}
// ── function: updateOverworld ──
function updateOverworld() {
if (gameState !== "overworld") return;
const speed = 3;
let moved = false;
if (keys["ArrowUp"] || keys["KeyW"] || touchActive["up"]) { overworld.playerY -= speed; moved = true; }
if (keys["ArrowDown"] || keys["KeyS"] || touchActive["down"]) { overworld.playerY += speed; moved = true; }
if (keys["ArrowLeft"] || keys["KeyA"] || touchActive["left"]) { overworld.playerX -= speed; moved = true; }
if (keys["ArrowRight"] || keys["KeyD"] || touchActive["right"]) { overworld.playerX += speed; moved = true; }
overworld.camX = overworld.playerX - W / 2;
overworld.camY = overworld.playerY - H / 2;
overworld.particles.forEach(function(p) {
p.x += p.vx;
p.y += p.vy;
if (p.y < 0) p.y = H;
if (p.x < 0) p.x = W;
if (p.x > W) p.x = 0;
});
if (keys["Space"] || keys["Enter"] || touchActive["interact"]) {
checkInteractions();
keys["Space"] = false;
keys["Enter"] = false;
touchActive["interact"] = false;
}
if (moved && Math.random() < ZONES[player.currentZone].encounterRate) {
triggerRandomEncounter();
}
renderOverworld();
}
// ── function: checkInteractions ──
function checkInteractions() {
const px = overworld.playerX;
const py = overworld.playerY;
overworld.interactables.forEach(function(interactable) {
const dx = px - interactable.x;
const dy = py - interactable.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 40) {
if (interactable.type === "shrine") restAtShrine(interactable.zone);
else if (interactable.type === "story") startStory(interactable.nodeId);
else if (interactable.type === "zone") changeZone(interactable.targetZone);
}
});
}
// ── function: triggerRandomEncounter ──
function triggerRandomEncounter() {
const zone = ZONES[player.currentZone];
const specIds = zone.wildCreatures;
const count = Math.random() < 0.3 ? 2 : 1;
const enemies = [];
for (let i = 0; i < count; i++) {
enemies.push(specIds[Math.floor(Math.random() * specIds.length)]);
}
startBattle(enemies);
}
// ── function: restAtShrine ──
function restAtShrine(zoneIndex) {
player.party.forEach(function(c) {
c.hp = c.maxHp;
c.status = null;
c.fainted = false;
Object.keys(c.pp).forEach(function(key) {
const ability = ABILITIES[key];
if (ability) c.pp[key] = ability.pp;
});
});
saveGame();
showBattleLog("Restored at shrine. Game saved.", "log-effect");
player.money += 50;
}
// ── function: changeZone ──
function changeZone(zoneIndex) {
if (zoneIndex < 0 || zoneIndex >= ZONES.length) return;
if (!player.unlockedZones.includes(ZONES[zoneIndex]?.id)) return;
player.currentZone = zoneIndex;
player.lastShrine = zoneIndex;
initOverworld();
}
// ── function: updatePartyHud ──
// ═══════════════════════════════════════════════════════════════
// UI SYSTEM
// ═══════════════════════════════════════════════════════════════
function updatePartyHud() {
const partyHud = document.getElementById("party-hud");
partyHud.innerHTML = "";
player.party.forEach(function(creature, i) {
const slot = document.createElement("div");
slot.className = "party-slot " + (creature.fainted ? "fainted" : "") + (i === 0 ? " active" : "");
slot.onclick = function() { selectPartyMember(i); };
const hpPct = creature.hp / creature.maxHp * 100;
const hpClass = hpPct < 20 ? "low" : hpPct < 50 ? "mid" : "";
slot.innerHTML = '<div class="mini-name">' + creature.name + '</div>' +
'<div class="mini-hp"><div class="mini-hp-fill ' + hpClass + '" style="width:' + hpPct + '%"></div></div>' +
'<div style="font-size:8px;color:rgba(255,255,255,0.5)">' + creature.level + '</div>';
partyHud.appendChild(slot);
});
}
// ── function: selectPartyMember ──
function selectPartyMember(index) {
menuCreatureIndex = index;
openMenu("party");
}
// ── function: openMenu ──
function openMenu(tab) {
menuTab = tab;
gameState = "menu";
document.getElementById("overlay").classList.add("active");
document.querySelectorAll("#menu-tabs button").forEach(function(btn) {
btn.classList.toggle("active", btn.dataset.tab === tab);
});
renderMenu();
}
// ── function: closeMenu ──
function closeMenu() {
document.getElementById("overlay").classList.remove("active");
gameState = "overworld";
}
// ── function: renderMenu ──
function renderMenu() {
const content = document.getElementById("menu-content");
content.innerHTML = "";
switch (menuTab) {
case "party": renderPartyMenu(content); break;
case "items": renderItemMenu(content); break;
case "encyclopedia": renderEncyclopediaMenu(content); break;
case "map": renderMapMenu(content); break;
case "save": renderSaveMenu(content); break;
}
}
// ── function: renderPartyMenu ──
function renderPartyMenu(container) {
container.innerHTML = "<div class='menu-section-title'>Your Party</div>";
player.party.forEach(function(creature, i) {
const item = document.createElement("div");
item.className = "menu-item";
item.onclick = function() { showCreatureDetail(i); };
const typeColor = TYPE_COLORS[creature.affinity] || "#fff";
const hpPct = creature.hp / creature.maxHp * 100;
item.innerHTML = '<div class="mi-icon" style="background:' + typeColor + '30;border:1px solid ' + typeColor + '"><span style="color:' + typeColor + '">●</span></div>' +
'<div class="mi-info"><div class="mi-name">' + creature.name + (creature.fainted ? " (Fainted)" : "") + '</div>' +
'<div class="mi-detail">Lv.' + creature.level + " | HP: " + creature.hp + "/" + creature.maxHp + '</div></div>' +
'<div class="mi-stats"><span>ATK ' + creature.atk + '</span><span>DEF ' + creature.def + '</span><span>SPD ' + creature.spd + '</span></div>';
container.appendChild(item);
});
}
// ── function: showCreatureDetail ──
function showCreatureDetail(index) {
const creature = player.party[index];
if (!creature) return;
const content = document.getElementById("menu-content");
content.innerHTML = "";
const typeColor = TYPE_COLORS[creature.affinity] || "#fff";
const backBtn = document.createElement("button");
backBtn.textContent = "← Back";
backBtn.style.cssText = "background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.2);color:#fff;padding:8px 16px;border-radius:8px;cursor:pointer;margin-bottom:12px";
backBtn.onclick = function() { renderPartyMenu(content); };
content.appendChild(backBtn);
const info = document.createElement("div");
info.style.cssText = "color:#fff;margin-bottom:12px";
info.innerHTML = '<h3 style="color:' + typeColor + ';margin-bottom:4px">' + creature.name + '</h3>' +
'<p style="font-size:12px;color:rgba(255,255,255,0.6)">' + (CREATURES[creature.id]?.lore || "") + '</p>' +
'<p style="font-size:11px;color:rgba(255,255,255,0.4);margin-top:8px">Type: ' + TYPE_NAMES[creature.affinity] + '</p>';
content.appendChild(info);
const stats = document.createElement("div");
stats.style.cssText = "margin-bottom:12px";
stats.innerHTML = '<div style="color:rgba(255,255,255,0.5);font-size:12px;margin-bottom:8px">STATS</div>' +
'<div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;font-size:12px">' +
'<div style="color:rgba(255,255,255,0.7)">HP: ' + creature.maxHp + '</div>' +
'<div style="color:rgba(255,255,255,0.7)">ATK: ' + creature.atk + '</div>' +
'<div style="color:rgba(255,255,255,0.7)">DEF: ' + creature.def + '</div>' +
'<div style="color:rgba(255,255,255,0.7)">SPD: ' + creature.spd + '</div></div>';
content.appendChild(stats);
const abilities = document.createElement("div");
abilities.innerHTML = "<div style='color:rgba(255,255,255,0.5);font-size:12px;margin-bottom:8px'>ABILITIES</div>";
creature.abilities.forEach(function(abilId) {
const ability = ABILITIES[abilId];
if (!ability) return;
const abilDiv = document.createElement("div");
abilDiv.style.cssText = "display:flex;justify-content:space-between;padding:4px 0;border-bottom:1px solid rgba(255,255,255,0.1);font-size:11px";
abilDiv.innerHTML = '<span style="color:' + (TYPE_COLORS[ability.type] || "#fff") + '">' + ability.name + '</span>' +
'<span style="color:rgba(255,255,255,0.4)">Pp: ' + ability.pp + '</span>';
abilities.appendChild(abilDiv);
});
content.appendChild(abilities);
const spec = CREATURES[creature.id];
if (spec?.transforms) {
const transDiv = document.createElement("div");
transDiv.style.cssText = "margin-top:12px;padding:8px;background:rgba(255,255,255,0.05);border-radius:8px;font-size:11px;color:rgba(255,255,255,0.6)";
transDiv.innerHTML = '<div style="color:rgba(255,255,255,0.8);margin-bottom:4px">TRANSFORMATION</div>' +
'<div>Level: ' + spec.transforms.level + "+</div>" +
'<div>Item: ' + (ITEMS[spec.transforms.catalyst]?.name || spec.transforms.catalyst) + '</div>';
content.appendChild(transDiv);
}
}
// ── function: renderItemMenu ──
function renderItemMenu(container) {
container.innerHTML = "<div class='menu-section-title'>Inventory</div>";
const itemIds = Object.keys(player.inventory).filter(function(id) { return player.inventory[id] > 0; });
if (itemIds.length === 0) {
container.innerHTML += "<div style='color:rgba(255,255,255,0.4);text-align:center;padding:20px'>No items</div>";
return;
}
const types = {};
itemIds.forEach(function(id) {
const item = ITEMS[id];
if (!item) return;
if (!types[item.type]) types[item.type] = [];
types[item.type].push(id);
});
Object.entries(types).forEach(function(_a) {
const type = _a[0];
const ids = _a[1];
const section = document.createElement("div");
section.className = "menu-section-title";
section.textContent = type.toUpperCase();
container.appendChild(section);
ids.forEach(function(id) {
const item = ITEMS[id];
const count = player.inventory[id];
const itemDiv = document.createElement("div");
itemDiv.className = "menu-item";
itemDiv.innerHTML = '<div class="mi-info"><div class="mi-name">' + item.name + " x" + count + '</div>' +
'<div class="mi-detail">' + item.desc + '</div></div>';
if (item.type === "heal" || item.type === "revive" || item.type === "status") {
itemDiv.onclick = function() { useItemFromMenu(id); };
}
container.appendChild(itemDiv);
});
});
}
// ── function: useItemFromMenu ──
function useItemFromMenu(itemId) {
const item = ITEMS[itemId];
if (!item || player.inventory[itemId] <= 0) return;
if (item.type === "heal") {
const target = player.party.find(function(c) { return !c.fainted; });
if (target) {
const heal = Math.min(item.power, target.maxHp - target.hp);
target.hp += heal;
player.inventory[itemId]--;
showBattleLog("Used " + item.name + " on " + target.name + ". Recovered " + heal + " HP.", "log-effect");
}
} else if (item.type === "revive") {
const target = player.party.find(function(c) { return c.fainted; });
if (target) {
target.fainted = false;
target.hp = Math.floor(target.maxHp * item.power);
player.inventory[itemId]--;
showBattleLog("Used " + item.name + " on " + target.name + ". Revived!", "log-effect");
}
} else if (item.type === "status") {
const target = player.party.find(function(c) { return c.status === item.cure; });
if (target) {
target.status = null;
player.inventory[itemId]--;
showBattleLog("Used " + item.name + " on " + target.name + ". Cured " + item.cure + "!", "log-effect");
}
}
renderMenu();
}
// ── function: renderEncyclopediaMenu ──
function renderEncyclopediaMenu(container) {
container.innerHTML = "<div class='menu-section-title'>Bestiary</div>";
const collected = player.collectedCreatures || [];
const allIds = Object.keys(CREATURES);
allIds.forEach(function(id) {
const spec = CREATURES[id];
const isCollected = collected.includes(id);
if (!isCollected) return;
const item = document.createElement("div");
item.className = "menu-item";
const typeColor = TYPE_COLORS[spec.affinity] || "#fff";
item.innerHTML = '<div class="mi-icon" style="background:' + typeColor + '30;border:1px solid ' + typeColor + '"><span style="color:' + typeColor + '">●</span></div>' +
'<div class="mi-info"><div class="mi-name">' + spec.name + '</div>' +
'<div class="mi-detail">' + spec.lore + '</div>' +
'<div class="mi-stats"><span style="color:' + typeColor + '">' + TYPE_NAMES[spec.affinity] + '</span></div></div>';
container.appendChild(item);
});
}
// ── function: renderMapMenu ──
function renderMapMenu(container) {
container.innerHTML = "<div class='menu-section-title'>Map</div>";
ZONES.forEach(function(zone, i) {
const isUnlocked = player.unlockedZones.includes(zone.id);
const isCurrent = player.currentZone === i;
const isDefeated = player.defeatedBosses.includes(zone.id);
const item = document.createElement("div");
item.className = "menu-item";
item.style.opacity = isUnlocked ? "1" : "0.4";
item.innerHTML = '<div class="mi-icon" style="background:' + (isUnlocked ? zone.color + "30" : "rgba(255,255,255,0.1)") + ';border:1px solid ' + (isUnlocked ? zone.color : "rgba(255,255,255,0.2)") + '"><span style="color:' + (isUnlocked ? zone.color : "#666") + '">' + (isCurrent ? "►" : isDefeated ? "✓" : "○") + '</span></div>' +
'<div class="mi-info"><div class="mi-name">' + zone.name + '</div>' +
'<div class="mi-detail">' + (isUnlocked ? zone.description : "Locked") + '</div></div>';
if (isUnlocked && !isCurrent) {
item.onclick = function() { changeZone(i); closeMenu(); };
}
container.appendChild(item);
});
const progress = document.createElement("div");
progress.style.cssText = "margin-top:16px;padding:12px;background:rgba(255,255,255,0.05);border-radius:8px;font-size:12px;color:rgba(255,255,255,0.6)";
progress.innerHTML = '<div style="color:rgba(255,255,255,0.8);margin-bottom:8px">PROGRESS</div>' +
'<div>Bosses defeated: ' + player.defeatedBosses.length + '/4</div>' +
'<div>Creatures collected: ' + player.collectedCreatures.length + '/8</div>' +
'<div style="margin-top:8px;color:' + (player.postgame ? "#4ade80" : "rgba(255,255,255,0.4)") + '">' +
(player.postgame ? "Post-game unlocked!" : "Defeat 4 bosses and collect 8 creatures to win.") + '</div>';
container.appendChild(progress);
}
// ── function: renderSaveMenu ──
function renderSaveMenu(container) {
container.innerHTML = "<div class='menu-section-title'>Save & Load</div>";
const saveInfo = document.createElement("div");
saveInfo.style.cssText = "padding:12px;background:rgba(255,255,255,0.05);border-radius:8px;font-size:12px;color:rgba(255,255,255,0.6);margin-bottom:12px";
saveInfo.innerHTML = '<div style="color:rgba(255,255,255,0.8);margin-bottom:4px">AUTO-SAVE</div>' +
'<div>Game is auto-saved at shrines.</div>' +
'<div>Current: Zone ' + (player.currentZone + 1) + " - " + (ZONES[player.currentZone]?.name || "Unknown") + '</div>' +
'<div>Money: ' + player.money + '</div>';
container.appendChild(saveInfo);
const saveBtn = document.createElement("button");
saveBtn.textContent = "Save Game";
saveBtn.style.cssText = "width:100%;padding:12px;background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.2);color:#fff;border-radius:8px;cursor:pointer;margin-bottom:8px";
saveBtn.onclick = function() {
saveGame();
saveBtn.textContent = "Saved!";
setTimeout(function() { saveBtn.textContent = "Save Game"; }, 1000);
};
container.appendChild(saveBtn);
const loadBtn = document.createElement("button");
loadBtn.textContent = "Load Game";
loadBtn.style.cssText = "width:100%;padding:12px;background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.2);color:#fff;border-radius:8px;cursor:pointer";
loadBtn.onclick = function() {
if (loadGame()) { closeMenu(); initOverworld(); }
else { loadBtn.textContent = "No save found"; setTimeout(function() { loadBtn.textContent = "Load Game"; }, 1000); }
};
container.appendChild(loadBtn);
}
// ── function: saveGame ──
// ═══════════════════════════════════════════════════════════════
// SAVE/LOAD SYSTEM
// ═══════════════════════════════════════════════════════════════
function saveGame() {
const saveData = {
party: player.party.map(function(c) { return Object.assign({}, c); }),
inventory: player.inventory,
money: player.money,
currentZone: player.currentZone,
unlockedZones: player.unlockedZones,
defeatedBosses: player.defeatedBosses,
collectedCreatures: player.collectedCreatures,
storyProgress: player.storyProgress,
totalCreatures: player.totalCreatures,
postgame: player.postgame,
lastShrine: player.lastShrine
};
localStorage.setItem("echoes_save", JSON.stringify(saveData));
}
// ── function: loadGame ──
function loadGame() {
const data = localStorage.getItem("echoes_save");
if (!data) return false;
try {
const saveData = JSON.parse(data);
player.party = saveData.party || [];
player.inventory = saveData.inventory || {};
player.money = saveData.money || 200;
player.currentZone = saveData.currentZone || 0;
player.unlockedZones = saveData.unlockedZones || [0];
player.defeatedBosses = saveData.defeatedBosses || [];
player.collectedCreatures = saveData.collectedCreatures || [];
player.storyProgress = saveData.storyProgress || {};
player.totalCreatures = saveData.totalCreatures || 0;
player.postgame = saveData.postgame || false;
player.lastShrine = saveData.lastShrine || 0;
return true;
} catch (e) {
console.error("Failed to load save:", e);
return false;
}
}
// ── function: initAudio ──
// ═══════════════════════════════════════════════════════════════
// AUDIO SYSTEM
// ═══════════════════════════════════════════════════════════════
function initAudio() {
try {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
startAmbientMusic();
} catch (e) {
console.log("Audio not available");
}
}
// ── function: startAmbientMusic ──
function startAmbientMusic() {
if (!audioCtx || isMuted) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
const filter = audioCtx.createBiquadFilter();
osc.type = "sine";
osc.frequency.value = 220;
filter.type = "lowpass";
filter.frequency.value = 800;
gain.gain.value = 0.05;
osc.connect(filter);
filter.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
const lfo = audioCtx.createOscillator();
const lfoGain = audioCtx.createGain();
lfo.frequency.value = 0.1;
lfoGain.gain.value = 50;
lfo.connect(lfoGain);
lfoGain.connect(filter.frequency);
lfo.start();
window._ambientOsc = osc;
window._ambientLfo = lfo;
}
// ── function: playSound ──
function playSound(type) {
if (!audioCtx || isMuted) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
switch (type) {
case "hit":
osc.type = "square";
osc.frequency.value = 200;
gain.gain.value = 0.1;
osc.start();
osc.stop(audioCtx.currentTime + 0.1);
break;
case "crit":
osc.type = "sawtooth";
osc.frequency.value = 400;
gain.gain.value = 0.15;
osc.start();
osc.stop(audioCtx.currentTime + 0.2);
break;
case "heal":
osc.type = "sine";
osc.frequency.value = 600;
gain.gain.value = 0.08;
osc.start();
osc.frequency.exponentialRampToValueAtTime(800, audioCtx.currentTime + 0.3);
osc.stop(audioCtx.currentTime + 0.3);
break;
case "capture":
osc.type = "triangle";
osc.frequency.value = 400;
gain.gain.value = 0.1;
osc.start();
osc.frequency.exponentialRampToValueAtTime(800, audioCtx.currentTime + 0.5);
osc.stop(audioCtx.currentTime + 0.5);
break;
case "levelup":
osc.type = "square";
osc.frequency.value = 300;
gain.gain.value = 0.1;
osc.start();
osc.frequency.exponentialRampToValueAtTime(600, audioCtx.currentTime + 0.3);
osc.stop(audioCtx.currentTime + 0.3);
break;
case "select":
osc.type = "sine";
osc.frequency.value = 500;
gain.gain.value = 0.05;
osc.start();
osc.stop(audioCtx.currentTime + 0.05);
break;
}
}
// ── function: toggleMute ──
function toggleMute() {
isMuted = !isMuted;
const btn = document.getElementById("mute-btn");
if (btn) btn.textContent = isMuted ? "🔇" : "🔊";
if (isMuted) {
if (window._ambientOsc) window._ambientOsc.stop();
if (window._ambientLfo) window._ambientLfo.stop();
} else {
startAmbientMusic();
}
}
// ── let: battleParticles ──
// ═══════════════════════════════════════════════════════════════
// PARTICLE EFFECTS
// ═══════════════════════════════════════════════════════════════
let battleParticles = [];
// ── function: spawnBattleParticles ──
function spawnBattleParticles(color) {
for (let i = 0; i < 15; i++) {
battleParticles.push({
x: W / 2 + (Math.random() - 0.5) * 100,
y: H / 2 + (Math.random() - 0.5) * 50,
vx: (Math.random() - 0.5) * 8,
vy: (Math.random() - 0.5) * 8,
life: 30,
maxLife: 30,
color: color,
size: 2 + Math.random() * 4
});
}
}
// ── function: updateBattleParticles ──
function updateBattleParticles() {
battleParticles = battleParticles.filter(function(p) { return p.life > 0; });
battleParticles.forEach(function(p) {
p.x += p.vx;
p.y += p.vy;
p.life--;
p.vx *= 0.95;
p.vy *= 0.95;
});
}
// ── function: renderBattleParticles ──
function renderBattleParticles() {
battleParticles.forEach(function(p) {
const alpha = p.life / p.maxLife;
ctx.fillStyle = p.color + Math.floor(alpha * 255).toString(16).padStart(2, "0");
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
});
}
// ── function: showFloatingText ──
// ═══════════════════════════════════════════════════════════════
// FLOATING TEXT & BATTLE LOG
// ═══════════════════════════════════════════════════════════════
function showFloatingText(target, text, type) {
const container = document.getElementById("floating-texts");
const el = document.createElement("div");
el.className = "float-text " + type;
el.textContent = text;
if (target === "player") {
el.style.left = "20%";
el.style.bottom = "100px";
} else {
el.style.left = "50%";
el.style.top = "30%";
}
container.appendChild(el);
setTimeout(function() { el.remove(); }, 1000);
}
// ── function: showBattleLog ──
function showBattleLog(text, className) {
const logDiv = document.getElementById("battle-log");
logDiv.textContent = text;
logDiv.className = className || "";
}
// ── function: showTitleScreen ──
// ═══════════════════════════════════════════════════════════════
// TITLE SCREEN & VICTORY
// ═══════════════════════════════════════════════════════════════
function showTitleScreen() {
gameState = "title";
const ts = document.getElementById("title-screen");
if (ts) ts.classList.remove("hidden");
const hud = document.getElementById("hud");
if (hud) hud.style.display = "none";
const bu = document.getElementById("battle-ui");
if (bu) bu.classList.remove("active");
const ov = document.getElementById("overlay");
if (ov) ov.classList.remove("active");
const db = document.getElementById("dialogue-box");
if (db) db.classList.remove("active");
const cg = document.getElementById("capture-game");
if (cg) cg.classList.remove("active");
}
// ── function: startNewGame ──
function startNewGame() {
const ts = document.getElementById("title-screen");
if (ts) ts.classList.add("hidden");
player = {
party: [],
inventory: {},
money: 200,
currentZone: 0,
unlockedZones: [0],
defeatedBosses: [],
collectedCreatures: [],
storyProgress: {},
totalCreatures: 0,
postgame: false,
lastShrine: 0
};
startStory("whispering_grove_story");
initOverworld();
}
// ── function: continueGame ──
function continueGame() {
if (loadGame()) {
const ts = document.getElementById("title-screen");
if (ts) ts.classList.add("hidden");
gameState = "overworld";
initOverworld();
} else {
alert("No save data found.");
}
}
// ── function: showVictoryScreen ──
function showVictoryScreen() {
const ts = document.getElementById("title-screen");
if (!ts) return;
ts.classList.remove("hidden");
ts.innerHTML = '<h1>The Weave Restored</h1>' +
'<div class="subtitle">You have saved the realm</div>' +
'<div style="color:rgba(255,255,255,0.6);margin:20px 0;text-align:center">' +
'<p>Creatures collected: ' + player.collectedCreatures.length + '</p>' +
'<p>Bosses defeated: ' + player.defeatedBosses.length + '</p>' +
'<p>Party levels: ' + player.party.map(function(c) { return c.level; }).join(", ") + '</p>' +
'</div>' +
'<button class="menu-btn primary" onclick="showTitleScreen()">Main Menu</button>';
}
// ── function: gameLoop ──
// ═══════════════════════════════════════════════════════════════
// MAIN GAME LOOP
// ═══════════════════════════════════════════════════════════════
function gameLoop() {
animFrame++;
ctx.clearRect(0, 0, W, H);
switch (gameState) {
case "overworld": updateOverworld(); break;
case "battle": updateBattleParticles(); renderBattleParticles(); break;
}
if (shakeTimer > 0) {
shakeTimer--;
const shakeX = (Math.random() - 0.5) * 4;
const shakeY = (Math.random() - 0.5) * 4;
ctx.translate(shakeX, shakeY);
}
requestAnimationFrame(gameLoop);
}
// ── main ──
function initGame() {
canvas = document.getElementById("gameCanvas");
ctx = canvas.getContext("2d");
function resize() {
W = window.innerWidth;
H = window.innerHeight;
canvas.width = W;
canvas.height = H;
}
resize();
window.addEventListener("resize", resize);
window.addEventListener("keydown", function(e) {
keys[e.code] = true;
if (gameState === "overworld" && (e.code === "Space" || e.code === "Enter")) {
checkInteractions();
}
if (gameState === "menu") {
if (e.code === "Escape" || e.code === "KeyM") closeMenu();
}
if (gameState === "battle" && battlePhase === "select") {
const num = parseInt(e.key);
if (num >= 1 && num <= 4) {
const creature = getActivePlayerCreature();
if (creature && creature.abilities[num - 1]) {
selectAbility(creature, creature.abilities[num - 1]);
}
}
}
});
window.addEventListener("keyup", function(e) { keys[e.code] = false; });
const dpadButtons = document.querySelectorAll("#dpad button");
dpadButtons.forEach(function(btn) {
btn.addEventListener("touchstart", function(e) {
e.preventDefault();
touchActive[btn.dataset.key.replace("Arrow", "").toLowerCase()] = true;
});
btn.addEventListener("touchend", function(e) {
e.preventDefault();
touchActive[btn.dataset.key.replace("Arrow", "").toLowerCase()] = false;
});
});
const touchInteract = document.getElementById("touch-interact");
if (touchInteract) {
touchInteract.addEventListener("touchstart", function(e) {
e.preventDefault();
touchActive["interact"] = true;
});
}
const touchMenu = document.getElementById("touch-menu");
if (touchMenu) {
touchMenu.addEventListener("touchstart", function(e) {
e.preventDefault();
if (gameState === "overworld") openMenu("party");
else if (gameState === "menu") closeMenu();
});
}
const btnMenu = document.getElementById("btn-menu");
if (btnMenu) {
btnMenu.addEventListener("click", function() {
if (gameState === "overworld") openMenu("party");
else if (gameState === "menu") closeMenu();
});
}
const btnMute = document.getElementById("btn-mute");
if (btnMute) btnMute.addEventListener("click", toggleMute);
const muteBtn = document.getElementById("mute-btn");
if (muteBtn) muteBtn.addEventListener("click", toggleMute);
document.querySelectorAll("#menu-tabs button").forEach(function(btn) {
btn.addEventListener("click", function() {
menuTab = btn.dataset.tab;
document.querySelectorAll("#menu-tabs button").forEach(function(b) { b.classList.remove("active"); });
btn.classList.add("active");
renderMenu();
});
});
const btnNew = document.getElementById("btn-new");
if (btnNew) btnNew.addEventListener("click", startNewGame);
const btnContinue = document.getElementById("btn-continue");
if (btnContinue) btnContinue.addEventListener("click", continueGame);
const cgTap = document.querySelector(".cg-tap");
if (cgTap) {
cgTap.addEventListener("click", attemptCapture);
cgTap.addEventListener("touchstart", function(e) {
e.preventDefault();
attemptCapture();
});
}
if ("ontouchstart" in window) {
const tc = document.getElementById("touch-controls");
if (tc) tc.classList.add("active");
}
window.addEventListener("click", function() {
if (!audioCtx) initAudio();
}, { once: true });
gameLoop();
showTitleScreen();
}
window.addEventListener("DOMContentLoaded", initGame);
</script>
</body>
</html>
<!-- agent-meta {"model":"kwaipilot/kat-coder-air-v2.5","provider":"openrouter","persona":"composer","sessionId":"2ed65f6f-c9d2-4799-962c-16b4193884c3","tokensIn":4031922,"tokensOut":105885,"tokensTotal":4137807,"turns":50,"toolCalls":49,"failedToolCalls":8,"timestamp":"2026-07-14T05:35:16.876Z"} -->