Creature Collection
Qwen 3.6 35b a3b · middle

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 96,219 bytes · SHA-256 4ebcb97eb11e
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Loombound</title>
<style>
:root {
--bg-dark: #0a0a1a;
--bg-mid: #1a1a3a;
--bg-light: #2a2a5a;
--accent: #7b68ee;
--accent2: #ff6b9d;
--accent3: #00e5ff;
--text: #e8e8f0;
--text-dim: #8888aa;
--success: #4ade80;
--danger: #f87171;
--warning: #fbbf24;
--glass-bg: rgba(20, 20, 50, 0.85);
--glass-border: rgba(123, 104, 238, 0.3);
--flux: #a855f7;
--stone: #9ca3af;
--tide: #3b82f6;
--ember: #ef4444;
--gale: #22c55e;
--void: #6b21a8;
--aether: #fbbf24;
}
* { margin:0; padding:0; box-sizing:border-box; }
html, body { width:100%; height:100%; overflow:hidden; background:var(--bg-dark); color:var(--text); font-family:system-ui,sans-serif; }
canvas { display:block; }
#game-container { position:relative; width:100vw; height:100vh; }
#title-screen { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; background:radial-gradient(ellipse at center, #1a1a4a 0%, #0a0a1a 70%); z-index:100; transition:opacity 0.5s; }
#title-screen.hidden { opacity:0; pointer-events:none; }
#title-screen h1 { font-size:clamp(2rem,8vw,5rem); background:linear-gradient(135deg,var(--accent),var(--accent2),var(--accent3)); -webkit-background-clip:text; -webkit-text-fill-color:transparent; text-shadow:none; margin-bottom:0.5rem; letter-spacing:0.1em; }
#title-screen .subtitle { color:var(--text-dim); font-size:clamp(0.8rem,2.5vw,1.2rem); margin-bottom:3rem; }
.title-btn { padding:1rem 3rem; margin:0.5rem; font-size:1.1rem; border:1px solid var(--glass-border); border-radius:12px; background:var(--glass-bg); color:var(--text); cursor:pointer; transition:all 0.3s; backdrop-filter:blur(10px); }
.title-btn:hover { background:rgba(123,104,238,0.3); border-color:var(--accent); transform:scale(1.05); }
#title-particles { position:absolute; inset:0; pointer-events:none; }
#overlay { position:absolute; inset:0; z-index:50; display:none; }
#overlay.active { display:flex; align-items:center; justify-content:center; }
.modal { background:var(--glass-bg); border:1px solid var(--glass-border); border-radius:16px; padding:1.5rem; max-width:90vw; max-height:85vh; overflow-y:auto; backdrop-filter:blur(15px); box-shadow:0 20px 60px rgba(0,0,0,0.5); }
.modal h2 { margin-bottom:1rem; font-size:1.3rem; }
.modal-close { position:absolute; top:0.5rem; right:1rem; background:none; border:none; color:var(--text-dim); font-size:1.5rem; cursor:pointer; }
.modal-close:hover { color:var(--text); }
#battle-screen { position:absolute; inset:0; z-index:40; display:none; }
#battle-screen.active { display:flex; flex-direction:column; }
#battle-canvas { flex:1; width:100%; }
#battle-ui { position:absolute; bottom:0; left:0; right:0; background:var(--glass-bg); border-top:1px solid var(--glass-border); backdrop-filter:blur(10px); padding:0.8rem; min-height:120px; }
#battle-text { font-size:0.95rem; margin-bottom:0.5rem; min-height:1.5em; }
#battle-actions { display:flex; gap:0.5rem; flex-wrap:wrap; }
.battle-btn { padding:0.5rem 1rem; border:1px solid var(--glass-border); border-radius:8px; background:rgba(40,40,80,0.6); color:var(--text); cursor:pointer; font-size:0.85rem; transition:all 0.2s; }
.battle-btn:hover { background:rgba(123,104,238,0.4); border-color:var(--accent); }
.battle-btn:disabled { opacity:0.4; cursor:default; }
.battle-btn.ability { min-width:80px; }
.battle-btn.ability .type-indicator { display:inline-block; width:8px; height:8px; border-radius:50%; margin-right:4px; vertical-align:middle; }
#hud { position:absolute; top:0; left:0; right:0; z-index:30; display:none; padding:0.5rem 1rem; background:linear-gradient(180deg,rgba(0,0,0,0.6),transparent); }
#hud.active { display:flex; gap:1rem; align-items:center; }
.hud-btn { background:var(--glass-bg); border:1px solid var(--glass-border); border-radius:8px; padding:0.4rem 0.8rem; color:var(--text); cursor:pointer; font-size:0.85rem; backdrop-filter:blur(5px); }
.hud-btn:hover { border-color:var(--accent); }
#hp-bar-container { width:140px; height:10px; background:rgba(0,0,0,0.5); border-radius:5px; overflow:hidden; }
#hp-bar { height:100%; background:linear-gradient(90deg,var(--success),#86efac); transition:width 0.5s; border-radius:5px; }
#hp-bar.low { background:linear-gradient(90deg,var(--danger),#fca5a5); }
#hp-bar.mid { background:linear-gradient(90deg,var(--warning),#fde68a); }
#minimap { position:absolute; bottom:10px; right:10px; z-index:25; border:1px solid var(--glass-border); border-radius:8px; overflow:hidden; }
#touch-controls { position:absolute; bottom:10px; left:10px; z-index:25; display:none; }
.touch-btn { width:50px; height:50px; border-radius:50%; background:rgba(255,255,255,0.15); border:1px solid rgba(255,255,255,0.2); color:white; font-size:1.2rem; display:flex; align-items:center; justify-content:center; cursor:pointer; user-select:none; -webkit-user-select:none; touch-action:none; }
.touch-btn:active { background:rgba(255,255,255,0.3); }
.touch-dpad { display:grid; grid-template-columns:repeat(3,50px); grid-template-rows:repeat(3,50px); gap:2px; }
#inventory-screen, #team-screen, #pokedex-screen, #map-screen { width:90vw; max-width:600px; }
.inv-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(80px,1fr)); gap:0.5rem; }
.inv-item { background:rgba(40,40,80,0.5); border:1px solid var(--glass-border); border-radius:8px; padding:0.5rem; text-align:center; cursor:pointer; transition:all 0.2s; }
.inv-item:hover { border-color:var(--accent); background:rgba(60,60,120,0.5); }
.inv-item .icon { font-size:1.5rem; }
.inv-item .count { font-size:0.7rem; color:var(--text-dim); }
.team-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(150px,1fr)); gap:0.5rem; }
.team-card { background:rgba(30,30,60,0.6); border:1px solid var(--glass-border); border-radius:10px; padding:0.8rem; cursor:pointer; transition:all 0.2s; }
.team-card:hover { border-color:var(--accent); }
.team-card.selected { border-color:var(--accent3); box-shadow:0 0 10px rgba(0,229,255,0.3); }
.team-card .name { font-weight:bold; font-size:0.9rem; }
.team-card .level { color:var(--text-dim); font-size:0.75rem; }
.team-card .hp-mini { width:100%; height:6px; background:rgba(0,0,0,0.4); border-radius:3px; margin-top:4px; overflow:hidden; }
.team-card .hp-mini-fill { height:100%; background:var(--success); border-radius:3px; transition:width 0.3s; }
.pokedex-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(120px,1fr)); gap:0.5rem; }
.pokedex-entry { background:rgba(30,30,60,0.5); border:1px solid var(--glass-border); border-radius:8px; padding:0.6rem; text-align:center; opacity:0.4; }
.pokedex-entry.discovered { opacity:1; border-color:var(--accent); }
.pokedex-entry .picon { font-size:1.8rem; }
.pokedex-entry .pname { font-size:0.75rem; margin-top:2px; }
.pokedex-entry .ptype { font-size:0.65rem; color:var(--text-dim); }
#tutorial-overlay { position:absolute; inset:0; z-index:60; display:none; align-items:center; justify-content:center; background:rgba(0,0,0,0.7); }
#tutorial-overlay.active { display:flex; }
.tutorial-box { background:var(--glass-bg); border:1px solid var(--glass-border); border-radius:16px; padding:2rem; max-width:500px; text-align:center; backdrop-filter:blur(15px); }
.tutorial-box h3 { margin-bottom:1rem; color:var(--accent3); }
.tutorial-box p { margin-bottom:1rem; line-height:1.6; }
.tutorial-box button { padding:0.6rem 2rem; border:1px solid var(--accent); border-radius:8px; background:rgba(123,104,238,0.3); color:var(--text); cursor:pointer; }
.tutorial-box button:hover { background:rgba(123,104,238,0.5); }
#story-overlay { position:absolute; inset:0; z-index:55; display:none; align-items:center; justify-content:center; }
#story-overlay.active { display:flex; }
.story-vignette { position:absolute; inset:0; }
.story-text { position:relative; z-index:2; background:var(--glass-bg); border:1px solid var(--glass-border); border-radius:16px; padding:2rem; max-width:600px; text-align:center; backdrop-filter:blur(15px); }
.story-text h3 { margin-bottom:0.5rem; }
.story-text p { line-height:1.8; margin-bottom:1rem; }
.story-text button { padding:0.5rem 2rem; border:1px solid var(--accent); border-radius:8px; background:rgba(123,104,238,0.3); color:var(--text); cursor:pointer; }
.float-text { position:absolute; font-weight:bold; pointer-events:none; animation:floatUp 1s ease-out forwards; z-index:45; }
@keyframes floatUp { 0% { opacity:1; transform:translateY(0); } 100% { opacity:0; transform:translateY(-60px); } }
.screen-shake { animation:shake 0.3s ease-out; }
@keyframes shake { 0%,100% { transform:translate(0); } 25% { transform:translate(-5px,3px); } 50% { transform:translate(5px,-3px); } 75% { transform:translate(-3px,5px); } }
.crit-flash { animation:critFlash 0.4s ease-out; }
@keyframes critFlash { 0% { filter:brightness(1); } 50% { filter:brightness(2); } 100% { filter:brightness(1); } }
#mute-btn { position:absolute; top:10px; right:10px; z-index:100; background:var(--glass-bg); border:1px solid var(--glass-border); border-radius:50%; width:36px; height:36px; display:flex; align-items:center; justify-content:center; cursor:pointer; color:var(--text); font-size:1rem; }
#zone-name { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); z-index:35; font-size:clamp(1.5rem,5vw,3rem); font-weight:bold; color:white; text-shadow:0 0 30px rgba(123,104,238,0.8); opacity:0; pointer-events:none; transition:opacity 0.5s; }
#zone-name.show { opacity:1; }
::-webkit-scrollbar { width:6px; }
::-webkit-scrollbar-track { background:transparent; }
::-webkit-scrollbar-thumb { background:var(--glass-border); border-radius:3px; }
@media (pointer:coarse) { #touch-controls { display:block; } }
</style>
</head>
<body>
<div id="game-container">
<canvas id="game-canvas"></canvas>
<canvas id="minimap" width="120" height="120"></canvas>
<div id="hud">
<span id="zone-label">Zone 1</span>
<div id="hp-bar-container"><div id="hp-bar" style="width:100%"></div></div>
<span id="hp-text">100/100</span>
<button class="hud-btn" onclick="Game.openTeam()">Team</button>
<button class="hud-btn" onclick="Game.openInventory()">Bag</button>
<button class="hud-btn" onclick="Game.openPokedex()">Pokedex</button>
<button class="hud-btn" onclick="Game.saveGame()">Save</button>
</div>
<div id="touch-controls">
<div class="touch-dpad">
<div></div><div class="touch-btn" data-dir="up">▲</div><div></div>
<div class="touch-btn" data-dir="left">◀</div><div></div><div class="touch-btn" data-dir="right">▶</div>
<div></div><div class="touch-btn" data-dir="down">▼</div><div></div>
</div>
</div>
<div id="title-screen">
<canvas id="title-particles"></canvas>
<h1>LOOMBOUND</h1>
<p class="subtitle">Weave the threads of fate</p>
<button class="title-btn" onclick="Game.newGame()">New Binding</button>
<button class="title-btn" id="continue-btn" onclick="Game.continueGame()" style="display:none">Continue</button>
</div>
<div id="overlay" class="modal-container"></div>
<div id="battle-screen">
<canvas id="battle-canvas"></canvas>
<div id="battle-ui">
<div id="battle-text">A wild creature appears!</div>
<div id="battle-actions"></div>
</div>
</div>
<div id="tutorial-overlay"><div class="tutorial-box" id="tutorial-content"></div></div>
<div id="story-overlay"><div class="story-vignette" id="story-bg"></div><div class="story-text" id="story-content"></div></div>
<div id="zone-name"></div>
<button id="mute-btn" onclick="Game.toggleMute()">🔊</button>
</div>
<script>
"use strict";
// ==================== TYPE SYSTEM ====================
const TYPES = {
FLUX: {name:'Flux',color:'#a855f7',emoji:'⚡'},
STONE: {name:'Stone',color:'#9ca3af',emoji:'🪨'},
TIDE: {name:'Tide',color:'#3b82f6',emoji:'🌊'},
EMBER: {name:'Ember',color:'#ef4444',emoji:'🔥'},
GALE: {name:'Gale',color:'#22c55e',emoji:'🍃'},
VOID: {name:'Void',color:'#6b21a8',emoji:'🌀'},
AETHER: {name:'Aether',color:'#fbbf24',emoji:'✨'}
};
const TYPE_CHART = {
FLUX: {vs:{FLUX:0.5,STONE:2,TIDE:0.5,EMBER:0.5,GALE:1,VOID:0.5,AETHER:0.5}},
STONE: {vs:{FLUX:0.5,STONE:1,TIDE:2,EMBER:1,GALE:0.5,VOID:0.5,AETHER:0.5}},
TIDE: {vs:{FLUX:2,STONE:0.5,TIDE:0.5,EMBER:0.5,GALE:1,VOID:0.5,AETHER:0.5}},
EMBER: {vs:{FLUX:2,STONE:1,TIDE:2,EMBER:0.5,GALE:1,VOID:0.5,AETHER:0.5}},
GALE: {vs:{FLUX:1,STONE:2,TIDE:1,EMBER:2,GALE:0.5,VOID:0.5,AETHER:0.5}},
VOID: {vs:{FLUX:1.5,STONE:1.5,TIDE:1.5,EMBER:1.5,GALE:1.5,VOID:0.5,AETHER:0.5}},
AETHER: {vs:{FLUX:0.75,STONE:0.75,TIDE:0.75,EMBER:0.75,GALE:0.75,VOID:2,AETHER:1}}
};
// ==================== ABILITIES (40+) ====================
const ABILITIES = [
{id:'thrust',name:'Thrust',type:'FLUX',power:40,acc:100,pp:20,eff:'none',desc:'A basic energy strike.'},
{id:'slam',name:'Slam',type:'STONE',power:45,acc:95,pp:20,eff:'none',desc:'A heavy physical blow.'},
{id:'lash',name:'Lash',type:'TIDE',power:40,acc:100,pp:25,eff:'none',desc:'A whipping water strike.'},
{id:'burn',name:'Burn',type:'EMBER',power:40,acc:100,pp:20,eff:'burn',desc:'Attacks that burn the foe.'},
{id:'slash',name:'Slash',type:'GALE',power:45,acc:95,pp:20,eff:'none',desc:'A swift cutting attack.'},
{id:'drain',name:'Drain',type:'FLUX',power:35,acc:100,pp:15,eff:'drain',desc:'Drains life from foe.'},
{id:'frost',name:'Frost',type:'TIDE',power:45,acc:90,pp:15,eff:'frost',desc:'Freezes foe, halving SPD.'},
{id:'thunder',name:'Thunder',type:'EMBER',power:50,acc:85,pp:10,eff:'shock',desc:'Strikes with lightning.'},
{id:'barrier',name:'Barrier',type:'AETHER',power:0,acc:100,pp:10,eff:'barrier',desc:'Creates a deflection shield.'},
{id:'regen',name:'Regen',type:'GALE',power:0,acc:100,pp:10,eff:'regen',desc:'Restores HP over time.'},
{id:'void_bolt',name:'Void Bolt',type:'VOID',power:50,acc:95,pp:15,eff:'none',desc:'A dark energy bolt.'},
{id:'stone_wall',name:'Stone Wall',type:'STONE',power:0,acc:100,pp:15,eff:'defense_up',desc:'Raises DEF sharply.'},
{id:'gale_strike',name:'Gale Strike',type:'GALE',power:55,acc:90,pp:10,eff:'multi',desc:'Hits 2-3 times.'},
{id:'ember_blast',name:'Ember Blast',type:'EMBER',power:60,acc:85,pp:10,eff:'burn',desc:'A powerful burning blast.'},
{id:'tide_pillar',name:'Tide Pillar',type:'TIDE',power:55,acc:90,pp:10,eff:'frost',desc:'A pillar of freezing water.'},
{id:'flux_wave',name:'Flux Wave',type:'FLUX',power:60,acc:85,pp:10,eff:'none',desc:'A wave of raw energy.'},
{id:'aether_beam',name:'Aether Beam',type:'AETHER',power:65,acc:80,pp:8,eff:'none',desc:'A concentrated light beam.'},
{id:'void_scream',name:'Void Scream',type:'VOID',power:70,acc:75,pp:5,eff:'shock',desc:'A devastating dark scream.'},
{id:'priority',name:'Quick Strike',type:'GALE',power:40,acc:100,pp:20,eff:'priority',desc:'Always strikes first.'},
{id:'earthquake',name:'Quake',type:'STONE',power:70,acc:80,pp:8,eff:'none',desc:'Shakes the ground.'},
{id:'waterfall',name:'Waterfall',type:'TIDE',power:65,acc:85,pp:10,eff:'frost',desc:'A crashing water attack.'},
{id:'fire_pillar',name:'Fire Pillar',type:'EMBER',power:70,acc:80,pp:8,eff:'burn',desc:'A towering inferno.'},
{id:'void_drain',name:'Void Drain',type:'VOID',power:45,acc:95,pp:15,eff:'drain',desc:'Drains with dark energy.'},
{id:'aether_shield',name:'Aether Shield',type:'AETHER',power:0,acc:100,pp:10,eff:'barrier',desc:'A glowing protective barrier.'},
{id:'gale_spin',name:'Gale Spin',type:'GALE',power:50,acc:95,pp:15,eff:'speed_up',desc:'Spins faster each hit.'},
{id:'stone_crash',name:'Stone Crash',type:'STONE',power:65,acc:85,pp:10,eff:'none',desc:'Crushing rock impact.'},
{id:'flux_burst',name:'Flux Burst',type:'FLUX',power:75,acc:75,pp:5,eff:'none',desc:'A massive energy explosion.'},
{id:'tide_lash',name:'Tide Lash',type:'TIDE',power:50,acc:95,pp:15,eff:'none',desc:'A rapid water whip.'},
{id:'ember_wave',name:'Ember Wave',type:'EMBER',power:55,acc:90,pp:12,eff:'burn',desc:'A wave of fire.'},
{id:'void_step',name:'Void Step',type:'VOID',power:0,acc:100,pp:10,eff:'speed_up',desc:'Phases through space.'},
{id:'aether_heal',name:'Aether Heal',type:'AETHER',power:0,acc:100,pp:10,eff:'heal',desc:'Heals a large amount.'},
{id:'gale_burst',name:'Gale Burst',type:'GALE',power:60,acc:85,pp:10,eff:'multi',desc:'A burst of wind blades.'},
{id:'stone_fang',name:'Stone Fang',type:'STONE',power:50,acc:95,pp:15,eff:'none',desc:'A crushing bite.'},
{id:'flux_drain',name:'Flux Drain',type:'FLUX',power:40,acc:95,pp:15,eff:'drain',desc:'Siphons energy.'},
{id:'tide_burst',name:'Tide Burst',type:'TIDE',power:65,acc:85,pp:10,eff:'none',desc:'A powerful water explosion.'},
{id:'ember_strike',name:'Ember Strike',type:'EMBER',power:55,acc:90,pp:15,eff:'burn',desc:'A flaming punch.'},
{id:'void_pulse',name:'Void Pulse',type:'VOID',power:55,acc:90,pp:12,eff:'none',desc:'A dark energy pulse.'},
{id:'aether_wave',name:'Aether Wave',type:'AETHER',power:50,acc:90,pp:12,eff:'none',desc:'A wave of pure light.'},
{id:'gale_fang',name:'Gale Fang',type:'GALE',power:55,acc:90,pp:12,eff:'multi',desc:'Swift fanged strikes.'},
{id:'stone_bolt',name:'Stone Bolt',type:'STONE',power:55,acc:90,pp:12,eff:'none',desc:'A projectile of rock.'},
{id:'flux_scream',name:'Flux Scream',type:'FLUX',power:65,acc:80,pp:8,eff:'shock',desc:'A disorienting energy cry.'},
{id:'tide_drain',name:'Tide Drain',type:'TIDE',power:45,acc:95,pp:15,eff:'drain',desc:'Siphons water energy.'},
{id:'ember_burst',name:'Ember Burst',type:'EMBER',power:70,acc:80,pp:8,eff:'burn',desc:'A fiery explosion.'},
{id:'void_barrier',name:'Void Barrier',type:'VOID',power:0,acc:100,pp:8,eff:'barrier',desc:'A dark protective wall.'},
{id:'aether_bolt',name:'Aether Bolt',type:'AETHER',power:60,acc:85,pp:10,eff:'none',desc:'A concentrated light bolt.'},
{id:'gale_heal',name:'Gale Heal',type:'GALE',power:0,acc:100,pp:10,eff:'regen',desc:'Heals over several turns.'},
{id:'stone_heal',name:'Stone Heal',type:'STONE',power:0,acc:100,pp:8,eff:'heal',desc:'A restorative earth touch.'},
{id:'void_burst',name:'Void Burst',type:'VOID',power:80,acc:70,pp:3,eff:'shock',desc:'An ultimate dark explosion.'},
{id:'aether_burst',name:'Aether Burst',type:'AETHER',power:75,acc:75,pp:5,eff:'none',desc:'A radiant energy burst.'}
];
const ABILITY_MAP = {};
ABILITIES.forEach(a => ABILITY_MAP[a.id] = a);
// ==================== CREATURES (27 total) ====================
const CREATURES = [
{id:'sporel',name:'Sporel',affinity:'FLUX',baseHp:45,baseAtk:35,baseDef:25,baseSpd:30,
lore:'A tiny spore-creature that drifts on thermal currents, leaving trails of shimmering dust.',
abilities:['thrust','drain','flux_wave','gale_spin'],
evolveTo:'sporel_prism',evoLevel:25,
sprite:'🟣',color:'#a855f7'},
{id:'sporel_prism',name:'Prism Sporel',affinity:'FLUX',baseHp:65,baseAtk:50,baseDef:40,baseSpd:38,
lore:'A refracted bloom of light and spore, scattering prismatic shards across the sky.',
abilities:['thrust','drain','flux_wave','gale_spin','flux_burst'],
evolved:true,
sprite:'🔮',color:'#c084fc'},
{id:'rockle',name:'Rockle',affinity:'STONE',baseHp:55,baseAtk:40,baseDef:45,baseSpd:18,
lore:'A slow-moving stone golem that grows crystalline formations on its back over centuries.',
abilities:['slam','stone_wall','earthquake','stone_heal'],
evolveTo:'rockle_golem',evoLevel:28,
sprite:'🪨',color:'#9ca3af'},
{id:'rockle_golem',name:'Golem Rockle',affinity:'STONE',baseHp:85,baseAtk:60,baseDef:70,baseSpd:22,
lore:'A towering monolith of living rock, its crystalline crown hums with ancient resonance.',
abilities:['slam','stone_wall','earthquake','stone_heal','stone_crash'],
evolved:true,
sprite:'🗿',color:'#6b7280'},
{id:'tidalisk',name:'Tidalisk',affinity:'TIDE',baseHp:48,baseAtk:32,baseDef:30,baseSpd:35,
lore:'A ribbon-like water spirit that weaves through river currents, singing in harmonic frequencies.',
abilities:['lash','frost','tide_pillar','tide_drain'],
evolveTo:'tidalisk_naga',evoLevel:26,
sprite:'🌊',color:'#3b82f6'},
{id:'tidalisk_naga',name:'Naga Tidalisk',affinity:'TIDE',baseHp:70,baseAtk:48,baseDef:45,baseSpd:42,
lore:'A serpentine water sovereign, its coils can summon tidal waves and freezing mists.',
abilities:['lash','frost','tide_pillar','tide_drain','tide_burst'],
evolved:true,
sprite:'🐉',color:'#60a5fa'},
{id:'cinderwisp',name:'Cinderwisp',affinity:'EMBER',baseHp:42,baseAtk:38,baseDef:22,baseSpd:32,
lore:'A flickering ember-sprite born from volcanic vents, dancing with restless heat.',
abilities:['burn','thunder','ember_blast','ember_wave'],
evolveTo:'cinderwisp_dragon',evoLevel:27,
sprite:'🔥',color:'#ef4444'},
{id:'cinderwisp_dragon',name:'Dragewisp',affinity:'EMBER',baseHp:68,baseAtk:58,baseDef:35,baseSpd:38,
lore:'A winged ember-beast whose breath can melt stone and ignite the sky.',
abilities:['burn','thunder','ember_blast','ember_wave','ember_burst'],
evolved:true,
sprite:'🐲',color:'#f87171'},
{id:'leaflet',name:'Leaflet',affinity:'GALE',baseHp:40,baseAtk:30,baseDef:20,baseSpd:40,
lore:'A leaf-like creature that rides wind currents, scattering seeds of new growth.',
abilities:['slash','regen','gale_strike','gale_heal'],
evolveTo:'leaflet_wraith',evoLevel:24,
sprite:'🍃',color:'#22c55e'},
{id:'leaflet_wraith',name:'Wraith Leaflet',affinity:'GALE',baseHp:60,baseAtk:48,baseDef:30,baseSpd:50,
lore:'A spectral leaf-spirit that becomes invisible in the wind, striking with razor precision.',
abilities:['slash','regen','gale_strike','gale_heal','gale_burst'],
evolved:true,
sprite:'👻',color:'#4ade80'},
{id:'voidling',name:'Voidling',affinity:'VOID',baseHp:44,baseAtk:36,baseDef:28,baseSpd:33,
lore:'A small shadow-creature that feeds on ambient light, leaving darkness in its wake.',
abilities:['void_bolt','void_drain','void_step','void_barrier'],
evolveTo:'voidling_maw',evoLevel:29,
sprite:'🌀',color:'#6b21a8'},
{id:'voidling_maw',name:'Maw Voidling',affinity:'VOID',baseHp:72,baseAtk:55,baseDef:42,baseSpd:40,
lore:'A gaping void-entity that devours light itself, whispering in forgotten tongues.',
abilities:['void_bolt','void_drain','void_step','void_barrier','void_burst'],
evolved:true,
sprite:'🕳️',color:'#9333ea'},
{id:'aetheling',name:'Aetheling',affinity:'AETHER',baseHp:46,baseAtk:33,baseDef:32,baseSpd:28,
lore:'A tiny golden being radiating warm light, said to be born from the first dawn.',
abilities:['barrier','aether_beam','aether_heal','aether_wave'],
evolveTo:'aetheling_seraph',evoLevel:30,
sprite:'✨',color:'#fbbf24'},
{id:'aetheling_seraph',name:'Seraph Aetheling',affinity:'AETHER',baseHp:75,baseAtk:52,baseDef:50,baseSpd:35,
lore:'A radiant celestial being whose wings cast healing light across the land.',
abilities:['barrier','aether_beam','aether_heal','aether_wave','aether_burst'],
evolved:true,
sprite:'👼',color:'#fde68a'},
{id:'mossback',name:'Mossback',affinity:'STONE',baseHp:60,baseAtk:35,baseDef:50,baseSpd:15,
lore:'A turtle-like creature with a moss-covered shell that harbors tiny ecosystems.',
abilities:['slam','stone_wall','stone_fang','stone_heal'],
evolveTo:'mossback_turtle',evoLevel:22,
sprite:'🐢',color:'#65a30d'},
{id:'mossback_turtle',name:'Turtle Mossback',affinity:'STONE',baseHp:90,baseAtk:45,baseDef:75,baseSpd:18,
lore:'An ancient shell-bearer whose mossy dome is a living garden of rare flora.',
abilities:['slam','stone_wall','stone_fang','stone_heal','stone_crash'],
evolved:true,
sprite:'🐢',color:'#84cc16'},
{id:'flamefox',name:'Flamefox',affinity:'EMBER',baseHp:40,baseAtk:42,baseDef:20,baseSpd:38,
lore:'A fox-like creature with a tail of living flame, quick and cunning.',
abilities:['burn','thunder','ember_strike','gale_strike'],
evolveTo:'flamefox_demon',evoLevel:26,
sprite:'🦊',color:'#f97316'},
{id:'flamefox_demon',name:'Demon Flamefox',affinity:'EMBER',baseHp:65,baseAtk:62,baseDef:30,baseSpd:48,
lore:'A dual-tailed fire-beast of legendary speed, its eyes burn with ancient fire.',
abilities:['burn','thunder','ember_strike','gale_strike','ember_burst'],
evolved:true,
sprite:'🦊',color:'#fb923c'},
{id:'aqualisk',name:'Aqualisk',affinity:'TIDE',baseHp:50,baseAtk:38,baseDef:35,baseSpd:30,
lore:'A fish-like creature with bioluminescent scales that glow in the deep dark.',
abilities:['lash','frost','tide_lash','tide_burst'],
evolveTo:'aqualisk_leviathan',evoLevel:28,
sprite:'🐟',color:'#0ea5e9'},
{id:'aqualisk_leviathan',name:'Leviathan Aqualisk',affinity:'TIDE',baseHp:80,baseAtk:55,baseDef:50,baseSpd:36,
lore:'A massive deep-sea leviathan whose bioluminescence can blind predators.',
abilities:['lash','frost','tide_lash','tide_burst','waterfall'],
evolved:true,
sprite:'🐋',color:'#38bdf8'},
{id:'stormhawk',name:'Stormhawk',affinity:'GALE',baseHp:45,baseAtk:40,baseDef:25,baseSpd:42,
lore:'A hawk-like bird that rides thunderstorms, its wings crackling with static.',
abilities:['slash','thunder','gale_strike','gale_fang'],
evolveTo:'stormhawk_griffin',evoLevel:27,
sprite:'🦅',color:'#06b6d4'},
{id:'stormhawk_griffin',name:'Griffin Stormhawk',affinity:'GALE',baseHp:70,baseAtk:58,baseDef:38,baseSpd:52,
lore:'A griffin-like storm-beast whose wings summon thunder and whose claws split clouds.',
abilities:['slash','thunder','gale_strike','gale_fang','gale_burst'],
evolved:true,
sprite:'🦅',color:'#22d3ee'},
{id:'shadewolf',name:'Shadewolf',affinity:'VOID',baseHp:48,baseAtk:44,baseDef:28,baseSpd:36,
lore:'A wolf-like shadow that materializes from darkness, hunting in packs.',
abilities:['void_bolt','void_drain','void_step','void_barrier'],
evolveTo:'shadewolf_alpha',evoLevel:28,
sprite:'🐺',color:'#7c3aed'},
{id:'shadewolf_alpha',name:'Alpha Shadewolf',affinity:'VOID',baseHp:75,baseAtk:65,baseDef:40,baseSpd:45,
lore:'The pack leader of shadows, its howl can shatter light itself.',
abilities:['void_bolt','void_drain','void_step','void_barrier','void_burst'],
evolved:true,
sprite:'🐺',color:'#a78bfa'},
{id:'sunbird',name:'Sunbird',affinity:'AETHER',baseHp:44,baseAtk:36,baseDef:30,baseSpd:34,
lore:'A golden bird that nests in high peaks, its song said to heal the wounded.',
abilities:['barrier','aether_beam','aether_heal','aether_wave'],
evolveTo:'sunbird_phoenix',evoLevel:29,
sprite:'🐦',color:'#eab308'},
{id:'sunbird_phoenix',name:'Phoenix Sunbird',affinity:'AETHER',baseHp:72,baseAtk:54,baseDef:48,baseSpd:42,
lore:'A legendary phoenix-like bird reborn from golden flames, its wings heal all they touch.',
abilities:['barrier','aether_beam','aether_heal','aether_wave','aether_burst'],
evolved:true,
sprite:'🐦',color:'#facc15'},
{id:'crystalbug',name:'Crystalbug',affinity:'FLUX',baseHp:38,baseAtk:28,baseDef:35,baseSpd:32,
lore:'A tiny insect with a crystal shell that refracts light into harmless rainbows.',
abilities:['thrust','drain','flux_drain','barrier'],
evolveTo:'crystalbug_king',evoLevel:24,
sprite:'🐛',color:'#a78bfa'},
{id:'crystalbug_king',name:'King Crystalbug',affinity:'FLUX',baseHp:62,baseAtk:42,baseDef:55,baseSpd:38,
lore:'A majestic crystal-beetle whose shell can refract energy into devastating beams.',
abilities:['thrust','drain','flux_drain','barrier','flux_burst'],
evolved:true,
sprite:'🪲',color:'#c084fc'},
{id:'ashdrake',name:'Ashdrake',affinity:'EMBER',baseHp:52,baseAtk:45,baseDef:32,baseSpd:26,
lore:'A small dragon-like creature born from volcanic ash, breathing smoky flames.',
abilities:['burn','thunder','ember_blast','stone_fang'],
evolveTo:'ashdrake_inferno',evoLevel:28,
sprite:'🐲',color:'#dc2626'},
{id:'ashdrake_inferno',name:'Inferno Ashdrake',affinity:'EMBER',baseHp:82,baseAtk:68,baseDef:45,baseSpd:30,
lore:'A massive ash-dragon whose breath can level mountains and ignite the sky.',
abilities:['burn','thunder','ember_blast','stone_fang','ember_burst'],
evolved:true,
sprite:'🐲',color:'#ef4444'},
{id:'mistwisp',name:'Mistwisp',affinity:'GALE',baseHp:42,baseAtk:34,baseDef:24,baseSpd:36,
lore:'A wispy cloud-creature that drifts through fog, leaving trails of dew.',
abilities:['slash','regen','gale_spin','gale_heal'],
evolveTo:'mistwisp_spirit',evoLevel:25,
sprite:'💨',color:'#67e8f9'},
{id:'mistwisp_spirit',name:'Spirit Mistwisp',affinity:'GALE',baseHp:66,baseAtk:50,baseDef:35,baseSpd:48,
lore:'A spectral cloud-being that can phase through solid matter and strike from anywhere.',
abilities:['slash','regen','gale_spin','gale_heal','gale_burst'],
evolved:true,
sprite:'👻',color:'#a5f3fc'},
{id:'abyssal',name:'Abyssal',affinity:'VOID',baseHp:55,baseAtk:48,baseDef:35,baseSpd:28,
lore:'A deep-dwelling shadow-creature from the abyssal trench, feeding on darkness.',
abilities:['void_bolt','void_drain','void_step','void_barrier'],
evolveTo:'abyssal_titan',evoLevel:30,
sprite:'🦑',color:'#581c87'},
{id:'abyssal_titan',name:'Titan Abyssal',affinity:'VOID',baseHp:90,baseAtk:72,baseDef:50,baseSpd:32,
lore:'A colossal abyssal entity that commands the darkness of the deepest trenches.',
abilities:['void_bolt','void_drain','void_step','void_barrier','void_burst'],
evolved:true,
sprite:'🦑',color:'#7c3aed'},
{id:'dewdrop',name:'Dewdrop',affinity:'TIDE',baseHp:40,baseAtk:28,baseDef:30,baseSpd:34,
lore:'A tiny water-bead creature that rolls through morning dew, collecting nutrients.',
abilities:['lash','frost','tide_lash','regen'],
evolveTo:'dewdrop_tide',evoLevel:23,
sprite:'💧',color:'#06b6d4'},
{id:'dewdrop_tide',name:'Tide Dewdrop',affinity:'TIDE',baseHp:64,baseAtk:44,baseDef:44,baseSpd:40,
lore:'A large water-orb that can manipulate tides and create whirlpools.',
abilities:['lash','frost','tide_lash','regen','tide_burst'],
evolved:true,
sprite:'💧',color:'#22d3ee'},
];
// ==================== ZONES ====================
const ZONES = [
{id:0,name:'Verdant Hollow',gradient:['#1a3a1a','#0a2a0a'],particleColor:'#4ade80',particleCount:30,encounterRate:0.08,
enemies:[{id:'sporel',weight:30},{id:'crystalbug',weight:25},{id:'leaflet',weight:20},{id:'dewdrop',weight:25}],
boss:{id:'rockle',name:'Guardian Rockle',level:12},
shrine:true,story:'The Verdant Hollow hums with life. Ancient spores drift on warm currents, and crystal beetles navigate the undergrowth. You feel the threads of fate beginning to weave.',
storyColor:'rgba(10,42,10,0.85)'},
{id:1,name:'Crystal Caverns',gradient:['#1a1a3a','#0a0a2a'],particleColor:'#a78bfa',particleCount:40,encounterRate:0.10,
enemies:[{id:'crystalbug',weight:25},{id:'rockle',weight:20},{id:'aetheling',weight:20},{id:'mossback',weight:35}],
boss:{id:'aetheling',name:'Guardian Aetheling',level:18},
shrine:true,story:'The caverns glow with crystalline light. Mossback turtles navigate the dark passages, and the air vibrates with dormant energy. The Weave grows stronger here.',
storyColor:'rgba(10,10,42,0.85)'},
{id:2,name:'Ember Wastes',gradient:['#3a1a0a','#2a0a0a'],particleColor:'#f97316',particleCount:35,encounterRate:0.12,
enemies:[{id:'cinderwisp',weight:25},{id:'flamefox',weight:20},{id:'ashdrake',weight:20},{id:'aqualisk',weight:35}],
boss:{id:'cinderwisp',name:'Guardian Cinderwisp',level:24},
shrine:true,story:'The Ember Wastes stretch endlessly, volcanic vents breathing fire into the sky. Flamefoxes prowl the ash fields, and the ground trembles with ancient power.',
storyColor:'rgba(42,10,10,0.85)'},
{id:3,name:'Storm Peaks',gradient:['#1a2a3a','#0a1a2a'],particleColor:'#22d3ee',particleCount:45,encounterRate:0.14,
enemies:[{id:'stormhawk',weight:25},{id:'shadewolf',weight:20},{id:'aqualisk',weight:20},{id:'leaflet',weight:35}],
boss:{id:'stormhawk',name:'Guardian Stormhawk',level:30},
shrine:true,story:'The Storm Peaks pierce the clouds, lightning crackling between jagged spires. Stormhawks nest on the highest crags, and the wind carries whispers of the Weave.',
storyColor:'rgba(10,26,42,0.85)'},
{id:4,name:'Abyssal Trench',gradient:['#0a1a2a','#000a1a'],particleColor:'#06b6d4',particleCount:50,encounterRate:0.16,
enemies:[{id:'abyssal',weight:25},{id:'tidalisk',weight:20},{id:'aqualisk',weight:20},{id:'shadewolf',weight:35}],
boss:{id:'abyssal',name:'Guardian Abyssal',level:36},
shrine:true,story:'The Abyssal Trench descends into darkness. Abyssal titans dwell in the crushing depths, and the water glows with bioluminescent life. The Weave is frayed here.',
storyColor:'rgba(0,10,26,0.85)'},
{id:5,name:'Void\'s Edge',gradient:['#1a0a2a','#0a001a'],particleColor:'#9333ea',particleCount:55,encounterRate:0.20,
enemies:[{id:'voidling',weight:25},{id:'shadewolf',weight:20},{id:'abyssal',weight:20},{id:'sunbird',weight:35}],
boss:{id:'voidling',name:'Anchor Voidling',level:42},
shrine:false,story:'At the Edge of the Void, reality itself frays. The final anchor pulses with dark energy. You must weave the threads of fate to restore the Loom.',
storyColor:'rgba(10,0,26,0.9)'}
];
// ==================== ITEMS ====================
const ITEMS = {
'thread_pouch':{name:'Thread Pouch',icon:'🧵',type:'capture',tier:1,desc:'Basic capture tool.',price:100},
'thread_pouch_l':{name:'Thread Pouch+',icon:'🧶',type:'capture',tier:2,desc:'Improved capture tool.',price:300},
'thread_pouch_g':{name:'Thread Pouch++',icon:'✨',type:'capture',tier:3,desc:'Master capture tool.',price:800},
'mender_tear':{name:'Mender Tear',icon:'💧',type:'heal',tier:1,desc:'Restores 30 HP.',price:50,heal:30},
'mender_tear_l':{name:'Mender Tear+',icon:'💧',type:'heal',tier:2,desc:'Restores 80 HP.',price:150,heal:80},
'mender_tear_g':{name:'Mender Tear++',icon:'💧',type:'heal',tier:3,desc:'Restores all HP.',price:400,heal:999},
'catalyst_flux':{name:'Flux Catalyst',icon:'⚡',type:'catalyst',affinity:'FLUX',desc:'Transforms Flux creatures.',price:500},
'catalyst_stone':{name:'Stone Catalyst',icon:'🪨',type:'catalyst',affinity:'STONE',desc:'Transforms Stone creatures.',price:500},
'catalyst_tide':{name:'Tide Catalyst',icon:'🌊',type:'catalyst',affinity:'TIDE',desc:'Transforms Tide creatures.',price:500},
'catalyst_ember':{name:'Ember Catalyst',icon:'🔥',type:'catalyst',affinity:'EMBER',desc:'Transforms Ember creatures.',price:500},
'catalyst_gale':{name:'Gale Catalyst',icon:'🍃',type:'catalyst',affinity:'GALE',desc:'Transforms Gale creatures.',price:500},
'catalyst_void':{name:'Void Catalyst',icon:'🌀',type:'catalyst',affinity:'VOID',desc:'Transforms Void creatures.',price:500},
'catalyst_aether':{name:'Aether Catalyst',icon:'✨',type:'catalyst',affinity:'AETHER',desc:'Transforms Aether creatures.',price:500},
'field_rope':{name:'Field Rope',icon:'🪢',type:'field',desc:'Cross gaps.',price:200},
'field_lantern':{name:'Field Lantern',icon:'🏮',type:'field',desc:'Illuminates dark areas.',price:300},
'exp_boost':{name:'Exp Boost',icon:'📈',type:'consumable',desc:'Boosts EXP gain.',price:250},
'atk_boost':{name:'Atk Boost',icon:'⚔️',type:'consumable',desc:'Boosts ATK.',price:250},
'def_boost':{name:'Def Boost',icon:'🛡️',type:'consumable',desc:'Boosts DEF.',price:250},
'spd_boost':{name:'Spd Boost',icon:'💨',type:'consumable',desc:'Boosts SPD.',price:250}
};
// ==================== GAME STATE ====================
const Game = {
state: 'title',
canvas: null, ctx: null,
battleCanvas: null, battleCtx: null,
titleCanvas: null, titleCtx: null,
minimapCanvas: null, minimapCtx: null,
player: {x:5,y:5,zone:0,party:[],inventory:{},gold:500,flags:{},shrinePos:null,storyProgress:0},
currentEnemy: null, battleState: null,
particles: [], battleParticles: [],
audioCtx: null, muted: false, currentOsc: null,
moveDir: null, moveTimer: 0, moveInterval: 0.15,
touchDir: null,
tileW: 32, tileH: 32,
mapTiles: [], mapW: 40, mapH: 40,
camera: {x:0,y:0},
floatTexts: [],
animFrame: 0, lastTime: 0,
tutorialStep: 0, tutorialActive: false,
storyActive: false, storyQueue: [],
captured: new Set(), defeatedBosses: new Set(),
postGame: false, collectedCount: 0,
init() {
this.canvas = document.getElementById('game-canvas');
this.ctx = this.canvas.getContext('2d');
this.battleCanvas = document.getElementById('battle-canvas');
this.battleCtx = this.battleCanvas.getContext('2d');
this.titleCanvas = document.getElementById('title-particles');
this.titleCtx = this.titleCanvas.getContext('2d');
this.minimapCanvas = document.getElementById('minimap');
this.minimapCtx = this.minimapCanvas.getContext('2d');
this.resize();
window.addEventListener('resize', () => this.resize());
this.setupInput();
this.setupTouch();
this.initParticles();
this.initTitleParticles();
this.checkSave();
this.renderTitle();
this.loop();
},
resize() {
const dpr = window.devicePixelRatio || 1;
[this.canvas, this.battleCanvas].forEach(c => {
c.width = c.clientWidth * dpr;
c.height = c.clientHeight * dpr;
c.getContext('2d').setTransform(dpr,0,0,dpr,0,0);
});
this.minimapCanvas.width = 120;
this.minimapCanvas.height = 120;
},
setupInput() {
const keys = {};
window.addEventListener('keydown', e => {
keys[e.key] = true;
if(e.key === 'Escape') this.handleEscape();
if(e.key === 's' || e.key === 'S') this.saveGame();
});
window.addEventListener('keyup', e => { keys[e.key] = false; });
this.keyState = keys;
},
setupTouch() {
document.querySelectorAll('.touch-btn').forEach(btn => {
const dir = btn.dataset.dir;
btn.addEventListener('touchstart', e => { e.preventDefault(); this.touchDir = dir; });
btn.addEventListener('touchend', e => { e.preventDefault(); this.touchDir = null; });
btn.addEventListener('mousedown', () => { this.touchDir = dir; });
btn.addEventListener('mouseup', () => { this.touchDir = null; });
});
},
initParticles() {
this.particles = [];
for(let i = 0; i < 60; i++) {
this.particles.push({
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
vx: (Math.random() - 0.5) * 0.5,
vy: -Math.random() * 1 - 0.2,
size: Math.random() * 3 + 1,
alpha: Math.random() * 0.5 + 0.1,
life: Math.random() * 200 + 100
});
}
},
initTitleParticles() {
this.titleParticles = [];
for(let i = 0; i < 80; i++) {
this.titleParticles.push({
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
vx: (Math.random() - 0.5) * 0.3,
vy: (Math.random() - 0.5) * 0.3,
size: Math.random() * 4 + 1,
alpha: Math.random() * 0.6 + 0.1,
hue: Math.random() * 60 + 240
});
}
},
renderTitle() {
const c = this.titleCanvas;
c.width = c.clientWidth;
c.height = c.clientHeight;
const ctx = this.titleCtx;
ctx.clearRect(0, 0, c.width, c.height);
this.titleParticles.forEach(p => {
p.x += p.vx; p.y += p.vy;
if(p.x < 0) p.x = c.width;
if(p.x > c.width) p.x = 0;
if(p.y < 0) p.y = c.height;
if(p.y > c.height) p.y = 0;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fillStyle = `hsla(${p.hue},70%,70%,${p.alpha})`;
ctx.fill();
});
},
// ==================== SAVE/LOAD ====================
checkSave() {
try {
const s = localStorage.getItem('loombound_save');
if(s) document.getElementById('continue-btn').style.display = 'inline-block';
} catch(e) {}
},
saveGame() {
const save = {
player: {
x: this.player.x, y: this.player.y, zone: this.player.zone,
party: this.player.party.map(p => ({...p})),
inventory: {...this.player.inventory},
gold: this.player.gold,
flags: {...this.player.flags},
storyProgress: this.player.storyProgress,
captured: [...this.captured],
defeatedBosses: [...this.defeatedBosses],
postGame: this.postGame
},
version: 1
};
localStorage.setItem('loombound_save', JSON.stringify(save));
this.showFloatText(this.player.x * this.tileW, this.player.y * this.tileH, 'Saved!', '#4ade80');
},
loadGame() {
try {
const s = localStorage.getItem('loombound_save');
if(!s) return false;
const save = JSON.parse(s);
this.player.x = save.player.x;
this.player.y = save.player.y;
this.player.zone = save.player.zone;
this.player.party = save.player.party.map(p => ({...p}));
this.player.inventory = save.player.inventory;
this.player.gold = save.player.gold;
this.player.flags = save.player.flags;
this.player.storyProgress = save.player.storyProgress;
this.captured = new Set(save.player.captured);
this.defeatedBosses = new Set(save.player.defeatedBosses);
this.postGame = save.player.postGame || false;
return true;
} catch(e) { return false; }
},
// ==================== MAP GENERATION ====================
generateMap(zoneId) {
const map = [];
const w = this.mapW, h = this.mapH;
const zone = ZONES[zoneId];
for(let y = 0; y < h; y++) {
map[y] = [];
for(let x = 0; x < w; x++) {
let tile = 0; // grass
// Borders
if(x === 0 || y === 0 || x === w-1 || y === h-1) tile = 1; // wall
else if(x === 1 || y === 1 || x === w-2 || y === h-2) tile = 2; // path edge
else tile = 0;
// Add some variation
const r = Math.random();
if(r < 0.05 && tile === 0) tile = 3; // tree/rock
if(r < 0.02 && tile === 0) tile = 4; // water
// Path to shrine
const mid = Math.floor(w/2);
if(x >= mid-1 && x <= mid+1 && y >= mid-1 && y <= mid+1) tile = 2;
// Shrine position
if(x === mid && y === mid) tile = 5; // shrine
// Boss position
if(x === mid && y === h-3) tile = 6; // boss
map[y][x] = tile;
}
}
// Ensure path from start to shrine
const sx = Math.floor(w/2), sy = h-2;
for(let y = sy; y >= mid; y--) {
for(let dx = -2; dx <= 2; dx++) {
if(sx+dx >= 0 && sx+dx < w) map[y][sx+dx] = 2;
}
}
// Ensure path from shrine to boss
const bossY = h-3;
for(let y = mid; y <= bossY; y++) {
for(let dx = -2; dx <= 2; dx++) {
if(sx+dx >= 0 && sx+dx < w) map[y][sx+dx] = 2;
}
}
this.mapTiles = map;
this.player.shrinePos = {x: mid, y: mid};
},
// ==================== NEW GAME / CONTINUE ====================
newGame() {
document.getElementById('title-screen').classList.add('hidden');
this.player = {
x: 5, y: this.mapH - 2, zone: 0,
party: [], inventory: {'mender_tear':3, 'thread_pouch':3, 'field_rope':1},
gold: 500, flags: {}, storyProgress: 0, shrinePos: null
};
this.captured = new Set();
this.defeatedBosses = new Set();
this.postGame = false;
this.tutorialStep = 0;
this.tutorialActive = true;
this.storyActive = false;
this.generateMap(0);
this.startTutorial();
this.showHUD();
this.playAmbient();
},
continueGame() {
if(this.loadGame()) {
document.getElementById('title-screen').classList.add('hidden');
this.generateMap(this.player.zone);
this.showHUD();
this.playAmbient();
}
},
// ==================== TUTORIAL ====================
startTutorial() {
const steps = [
{title:'Welcome to Loombound', text:'You are a Threadweaver, tasked with collecting creatures and restoring the Shattered Loom. Use WASD or arrow keys to move. On mobile, use the touch D-pad.', btn:'Continue'},
{title:'Movement', text:'Walk through the Verdant Hollow to find your first wild creature. Approach it to initiate a battle.', btn:'Got it'},
{title:'First Battle', text:'In battle, choose abilities to attack. Each creature has 4 abilities. Match your affinity for bonus damage!', btn:'Fight!'},
{title:'Collection', text:'After weakening a creature, use a Thread Pouch to capture it. Lower HP = better capture chance!', btn:'Understood'},
{title:'Your Party', text:'Build a team of up to 6 creatures. Visit shrines to heal, save, and buy items. Defeat guardians to unlock new zones.', btn:'Begin Weaving'}
];
this.tutorialStep = 0;
this.tutorialActive = true;
this.showTutorialStep(steps[0]);
},
showTutorialStep(step) {
const el = document.getElementById('tutorial-overlay');
el.classList.add('active');
document.getElementById('tutorial-content').innerHTML = `
<h3>${step.title}</h3>
<p>${step.text}</p>
<button onclick="Game.nextTutorial()">${step.btn}</button>
`;
},
nextTutorial() {
const steps = [
{title:'Welcome to Loombound', text:'You are a Threadweaver, tasked with collecting creatures and restoring the Shattered Loom. Use WASD or arrow keys to move. On mobile, use the touch D-pad.', btn:'Continue'},
{title:'Movement', text:'Walk through the Verdant Hollow to find your first wild creature. Approach it to initiate a battle.', btn:'Got it'},
{title:'First Battle', text:'In battle, choose abilities to attack. Each creature has 4 abilities. Match your affinity for bonus damage!', btn:'Fight!'},
{title:'Collection', text:'After weakening a creature, use a Thread Pouch to capture it. Lower HP = better capture chance!', btn:'Understood'},
{title:'Your Party', text:'Build a team of up to 6 creatures. Visit shrines to heal, save, and buy items. Defeat guardians to unlock new zones.', btn:'Begin Weaving'}
];
this.tutorialStep++;
if(this.tutorialStep < steps.length) {
this.showTutorialStep(steps[this.tutorialStep]);
} else {
document.getElementById('tutorial-overlay').classList.remove('active');
this.tutorialActive = false;
this.player.storyProgress = 1;
this.showStory(ZONES[0].story, ZONES[0].storyColor);
}
},
// ==================== STORY ====================
showStory(text, color) {
this.storyActive = true;
const el = document.getElementById('story-overlay');
el.classList.add('active');
document.getElementById('story-bg').style.background = color;
document.getElementById('story-content').innerHTML = `
<h3>${ZONES[this.player.zone].name}</h3>
<p>${text}</p>
<button onclick="Game.dismissStory()">Continue</button>
`;
},
dismissStory() {
document.getElementById('story-overlay').classList.remove('active');
this.storyActive = false;
},
// ==================== HUD ====================
showHUD() {
document.getElementById('hud').classList.add('active');
document.getElementById('zone-label').textContent = ZONES[this.player.zone].name;
this.updateHUD();
},
hideHUD() {
document.getElementById('hud').classList.remove('active');
},
updateHUD() {
const front = this.player.party[0];
if(!front) return;
const hpPct = (front.hp / front.maxHp * 100);
const bar = document.getElementById('hp-bar');
bar.style.width = hpPct + '%';
bar.className = hpPct < 25 ? 'low' : hpPct < 50 ? 'mid' : '';
document.getElementById('hp-text').textContent = `${front.hp}/${front.maxHp}`;
},
// ==================== MOVEMENT ====================
tryMove(dx, dy) {
if(this.state !== 'overworld' || this.tutorialActive || this.storyActive || this.battleState) return;
const nx = this.player.x + dx;
const ny = this.player.y + dy;
const map = this.mapTiles;
if(!map || !map[ny] || !map[ny][nx]) return;
const tile = map[ny][nx];
if(tile === 1) return; // wall
if(tile === 3 && Math.random() > 0.3) return; // partial obstacle
if(tile === 4 && !this.player.inventory['field_rope']) return; // water
this.player.x = nx;
this.player.y = ny;
// Shrine interaction
if(this.player.shrinePos && nx === this.player.shrinePos.x && ny === this.player.shrinePos.y) {
this.restAtShrine();
return;
}
// Boss interaction
if(nx === Math.floor(this.mapW/2) && ny === this.mapH - 3) {
const zone = ZONES[this.player.zone];
if(!this.defeatedBosses.has(zone.boss.id)) {
this.startBossBattle(zone.boss);
}
return;
}
// Random encounter
const zone = ZONES[this.player.zone];
const rate = this.postGame ? zone.encounterRate * 1.5 : zone.encounterRate;
if(Math.random() < rate) {
this.startWildEncounter();
}
},
// ==================== SHRINE ====================
restAtShrine() {
// Heal party
this.player.party.forEach(p => { p.hp = p.maxHp; });
this.updateHUD();
// Show shrine UI
const overlay = document.getElementById('overlay');
overlay.classList.add('active');
overlay.innerHTML = `
<div class="modal" style="position:relative;min-width:300px">
<button class="modal-close" onclick="Game.closeOverlay()">✕</button>
<h2>🏛️ Shrine</h2>
<p style="margin-bottom:1rem;color:var(--text-dim)">A place of rest and renewal.</p>
<div style="display:flex;flex-direction:column;gap:0.5rem">
<button class="battle-btn" onclick="Game.shrineHeal()">Heal Party (${this.player.gold}g)</button>
<button class="battle-btn" onclick="Game.shrineShop()">Shop</button>
<button class="battle-btn" onclick="Game.saveGame();Game.closeOverlay()">Save Game</button>
${this.player.storyProgress <= 2 ? `<button class="battle-btn" onclick="Game.advanceStory()">Continue Story</button>` : ''}
</div>
</div>
`;
},
shrineHeal() {
const cost = this.player.party.length * 20;
if(this.player.gold >= cost) {
this.player.gold -= cost;
this.player.party.forEach(p => { p.hp = p.maxHp; });
this.updateHUD();
this.restAtShrine();
} else {
this.showFloatText(this.player.x * this.tileW, this.player.y * this.tileH, 'Not enough gold!', '#f87171');
}
},
shrineShop() {
const overlay = document.getElementById('overlay');
overlay.innerHTML = `
<div class="modal" style="position:relative;min-width:350px" id="shop-modal">
<button class="modal-close" onclick="Game.closeOverlay()">✕</button>
<h2>🏪 Shrine Shop</h2>
<p style="margin-bottom:0.5rem;color:var(--text-dim)">Gold: ${this.player.gold}g</p>
<div class="inv-grid" style="margin-bottom:1rem">
${Object.entries(ITEMS).filter(([k,v]) => v.price <= this.player.gold * 2).map(([k,v]) => `
<div class="inv-item" onclick="Game.buyItem('${k}')">
<div class="icon">${v.icon}</div>
<div style="font-size:0.7rem">${v.name}</div>
<div style="font-size:0.65rem;color:var(--warning)">${v.price}g</div>
</div>
`).join('')}
</div>
</div>
`;
},
buyItem(itemId) {
const item = ITEMS[itemId];
if(!item || this.player.gold < item.price) return;
this.player.gold -= item.price;
this.player.inventory[itemId] = (this.player.inventory[itemId] || 0) + 1;
this.shrineShop();
this.showFloatText(this.player.x * this.tileW, this.player.y * this.tileH, `+${item.name}`, '#4ade80');
},
advanceStory() {
this.player.storyProgress++;
const zone = ZONES[this.player.zone];
if(!this.defeatedBosses.has(zone.boss.id)) {
this.showStory('Defeat the zone guardian to advance the story.', zone.storyColor);
return;
}
if(this.player.storyProgress <= 4) {
const nextZone = this.player.storyProgress;
if(nextZone < 6) {
this.showStory(`The path to ${ZONES[nextZone].name} opens before you. The threads of fate guide your way.`, ZONES[nextZone].storyColor);
}
}
if(this.player.storyProgress >= 5) {
this.player.storyProgress = 0;
this.showStory('You have woven the threads. The Loom is restored. The Weave continues...', '#0a0a1a');
}
},
// ==================== OVERWORLD RENDERING ====================
renderOverworld(dt) {
const ctx = this.ctx;
const w = this.canvas.clientWidth;
const h = this.canvas.clientHeight;
const zone = ZONES[this.player.zone];
// Background gradient
const grad = ctx.createLinearGradient(0, 0, 0, h);
grad.addColorStop(0, zone.gradient[0]);
grad.addColorStop(1, zone.gradient[1]);
ctx.fillStyle = grad;
ctx.fillRect(0, 0, w, h);
// Camera
this.camera.x = this.player.x * this.tileW - w/2 + this.tileW/2;
this.camera.y = this.player.y * this.tileH - h/2 + this.tileH/2;
ctx.save();
ctx.translate(-this.camera.x, -this.camera.y);
// Draw map
const map = this.mapTiles;
const tileColors = {
0: '#1a3a1a', 1: '#0a0a1a', 2: '#2a4a2a',
3: '#0a2a0a', 4: '#0a1a3a', 5: '#2a2a5a', 6: '#3a1a1a'
};
const tileEmojis = { 5: '🏛️', 6: '💀' };
const start = Math.max(0, Math.floor(this.camera.x / this.tileW));
const end = Math.min(this.mapW, Math.ceil((this.camera.x + w) / this.tileW) + 1);
const startY = Math.max(0, Math.floor(this.camera.y / this.tileH));
const endY = Math.min(this.mapH, Math.ceil((this.camera.y + h) / this.tileH) + 1);
for(let y = startY; y < endY; y++) {
for(let x = start; x < end; x++) {
if(!map[y] || !map[y][x]) continue;
const tile = map[y][x];
ctx.fillStyle = tileColors[tile] || '#1a1a1a';
ctx.fillRect(x * this.tileW, y * this.tileH, this.tileW, this.tileH);
if(tileEmojis[tile]) {
ctx.font = `${this.tileW * 0.7}px serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(tileEmojis[tile], x * this.tileW + this.tileW/2, y * this.tileH + this.tileH/2);
}
}
}
// Player
const px = this.player.x * this.tileW;
const py = this.player.y * this.tileH;
const pulse = Math.sin(Date.now() / 300) * 0.1 + 1;
ctx.save();
ctx.translate(px + this.tileW/2, py + this.tileH/2);
ctx.scale(pulse, pulse);
ctx.font = `${this.tileW * 0.8}px serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('🧝', 0, 0);
ctx.restore();
// Ambient particles
const pColor = zone.particleColor;
this.particles.forEach(p => {
p.x += p.vx; p.y += p.vy;
p.life -= dt;
if(p.life <= 0) {
p.x = this.camera.x + Math.random() * w;
p.y = this.camera.y + Math.random() * h;
p.life = Math.random() * 200 + 100;
}
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fillStyle = pColor + Math.floor(p.alpha * 255).toString(16).padStart(2,'0');
ctx.fill();
});
ctx.restore();
// Zone name display
const zn = document.getElementById('zone-name');
if(this.zoneNameTimer > 0) {
zn.classList.add('show');
this.zoneNameTimer -= dt;
if(this.zoneNameTimer <= 0) zn.classList.remove('show');
}
},
// ==================== BATTLE SYSTEM ====================
startWildEncounter() {
const zone = ZONES[this.player.zone];
const spawnPool = this.postGame ? zone.enemies.map(e => ({...e, weight: e.weight * 1.5})) : zone.enemies;
const totalWeight = spawnPool.reduce((s,e) => s + e.weight, 0);
let r = Math.random() * totalWeight;
let chosen = spawnPool[0];
for(const e of spawnPool) {
r -= e.weight;
if(r <= 0) { chosen = e; break; }
}
const base = CREATURES.find(c => c.id === chosen.id);
if(!base) return;
const level = 3 + this.player.zone * 5 + Math.floor(Math.random() * 8);
const enemy = this.createCreature(base, level);
const numEnemies = Math.random() < 0.2 ? 2 : 1;
const enemies = [enemy];
if(numEnemies === 2) {
const extra = CREATURES.find(c => c.id === chosen.id);
if(extra) enemies.push(this.createCreature(extra, level - 1));
}
this.currentEnemy = enemies;
this.startBattle(enemies, false);
},
startBossBattle(bossData) {
const base = CREATURES.find(c => c.id === bossData.id);
if(!base) return;
const enemy = this.createCreature(base, bossData.level);
this.currentEnemy = [enemy];
this.startBattle([enemy], true, bossData);
},
createCreature(base, level) {
const hp = base.baseHp + level * 5;
const atk = base.baseAtk + level * 3;
const def = base.baseDef + level * 2;
const spd = base.baseSpd + Math.floor(level * 0.5);
return {
...base,
level, hp, maxHp: hp, atk, def, spd,
status: null, statusTurns: 0,
barrierActive: false, regenTurns: 0, drainTurns: 0
};
},
startBattle(enemies, isBoss, bossData) {
this.state = 'battle';
this.hideHUD();
document.getElementById('battle-screen').classList.add('active');
this.battleState = {
enemies,
isBoss,
bossData,
turn: 0,
turnOrder: [],
phase: 'player_turn', // player_turn, enemy_turn, capture, victory
playerIdx: 0,
selectedCreature: 0,
animating: false,
result: null
};
this.battleParticles = [];
this.initBattleParticles();
// Build turn order based on SPD
const all = [...enemies.map((e,i) => ({unit:e, type:'enemy', idx:i, spd:e.spd}))];
this.player.party.forEach((p,i) => {
all.push({unit:p, type:'player', idx:i, spd:p.spd});
});
all.sort((a,b) => b.spd - a.spd);
this.battleState.turnOrder = all;
// Show encounter text
if(isBoss) {
this.showBattleText(`⚠️ ${bossData.name} blocks your path!`);
} else {
const name = enemies[0].name;
this.showBattleText(`A wild ${name} appears!`);
}
// Show capture items
this.renderBattleActions();
this.renderBattleScene();
},
initBattleParticles() {
this.battleParticles = [];
for(let i = 0; i < 30; i++) {
this.battleParticles.push({
x: Math.random() * this.battleCanvas.clientWidth,
y: Math.random() * this.battleCanvas.clientHeight,
vx: (Math.random() - 0.5) * 0.3,
vy: (Math.random() - 0.5) * 0.3,
size: Math.random() * 2 + 0.5,
alpha: Math.random() * 0.3 + 0.05,
color: '#ffffff'
});
}
},
showBattleText(text) {
document.getElementById('battle-text').textContent = text;
},
renderBattleActions() {
const bs = this.battleState;
if(bs.phase !== 'player_turn') return;
const container = document.getElementById('battle-actions');
const creature = this.player.party[bs.selectedCreature];
if(!creature) return;
let html = '';
// Abilities
creature.abilities.forEach(abId => {
const ab = ABILITY_MAP[abId];
if(!ab) return;
const ppLeft = creature.pp ? (creature.pp[abId] || ab.pp) : ab.pp;
const typeColor = TYPES[ab.type]?.color || '#fff';
html += `<button class="battle-btn ability" onclick="Game.useAbility('${abId}')" ${ppLeft <= 0 ? 'disabled' : ''}>
<span class="type-indicator" style="background:${typeColor}"></span>
${ab.name}
<span style="font-size:0.65rem;color:var(--text-dim)">PP:${ppLeft}</span>
</button>`;
});
// Switch
html += `<button class="battle-btn" onclick="Game.openSwitch()">Switch</button>`;
// Capture
const hasCapture = Object.keys(this.player.inventory).some(k => ITEMS[k]?.type === 'capture');
if(hasCapture && !bs.isBoss) {
html += `<button class="battle-btn" onclick="Game.tryCapture()">Capture</button>`;
}
// Run
html += `<button class="battle-btn" onclick="Game.runAway()">Run</button>`;
container.innerHTML = html;
},
useAbility(abId) {
const bs = this.battleState;
if(bs.phase !== 'player_turn' || bs.animating) return;
const creature = this.player.party[bs.selectedCreature];
if(!creature) return;
const ab = ABILITY_MAP[abId];
if(!ab) return;
bs.animating = true;
bs.phase = 'animating';
// Check PP
if(!creature.pp) creature.pp = {};
if(creature.pp[abId] !== undefined) {
creature.pp[abId]--;
} else {
creature.pp[abId] = ab.pp - 1;
}
// Handle priority
if(ab.eff === 'priority') {
// Will be handled in turn order
}
// Target enemies
const targets = bs.enemies.filter(e => e.hp > 0);
if(targets.length === 0) {
this.endBattle(true);
return;
}
// Execute ability
this.executeAbility(creature, ab, targets, 'player');
},
executeAbility(attacker, ability, targets, side) {
const bs = this.battleState;
const isPlayer = side === 'player';
// Check for barrier on target
let barrierDeflected = false;
if(!isPlayer) {
for(const t of targets) {
if(t.barrierActive) {
t.barrierActive = false;
barrierDeflected = true;
this.showBattleText(`${t.name}'s barrier deflects the attack!`);
this.addBattleParticle(t.x || 200, t.y || 300, '#fbbf24', 10);
break;
}
}
}
if(!barrierDeflected) {
// Calculate damage
for(const target of targets) {
if(target.hp <= 0) continue;
let power = ability.power;
let accuracy = ability.acc;
if(power > 0) {
// STAB
let stab = 1;
if(ability.type === attacker.affinity) stab = 1.25;
// Type effectiveness
let eff = 1;
const typeChart = TYPE_CHART[ability.type];
if(typeChart && typeChart.vs[attacker.affinity] !== undefined) {
eff = typeChart.vs[target.affinity] || 1;
}
// Damage formula
const raw = (0.4 * attacker.level * attacker.atk / target.def) * stab * eff;
const variance = 0.9 + Math.random() * 0.2;
const damage = Math.max(1, Math.floor(raw * variance));
// Critical hit (6.25% chance)
let crit = false;
if(Math.random() < 0.0625) {
crit = true;
target.hp -= Math.floor(damage * 1.5);
this.showFloatText(target.x || 200, target.y || 250, `💥${Math.floor(damage * 1.5)}`, '#fbbf24');
} else {
target.hp -= damage;
this.showFloatText(target.x || 200, target.y || 250, `-${damage}`, crit ? '#fbbf24' : '#f87171');
}
// Screen shake on hit
if(damage > 30) {
document.getElementById('battle-screen').classList.add('screen-shake');
setTimeout(() => document.getElementById('battle-screen').classList.remove('screen-shake'), 300);
}
// Super effective flash
if(eff > 1) {
this.addBattleParticle(target.x || 200, target.y || 300, TYPES[target.affinity]?.color || '#fff', 15);
}
// Apply effects
if(ability.eff === 'burn' && !target.status) {
target.status = 'burn';
target.statusTurns = 3;
this.showBattleText(`${target.name} is burned!`);
}
if(ability.eff === 'frost' && !target.status) {
target.status = 'frost';
target.statusTurns = 3;
this.showBattleText(`${target.name} is frozen!`);
}
if(ability.eff === 'shock' && !target.status) {
target.status = 'shock';
target.statusTurns = 1;
this.showBattleText(`${target.name} is shocked!`);
}
if(ability.eff === 'barrier') {
attacker.barrierActive = true;
this.showBattleText(`${attacker.name} raised a barrier!`);
}
if(ability.eff === 'regen') {
attacker.regenTurns = 4;
this.showBattleText(`${attacker.name} is regenerating!`);
}
if(ability.eff === 'heal') {
const healAmt = Math.floor(attacker.maxHp * 0.4);
attacker.hp = Math.min(attacker.maxHp, attacker.hp + healAmt);
this.showFloatText(attacker.x || 150, attacker.y || 250, `+${healAmt}`, '#4ade80');
}
if(ability.eff === 'drain') {
const drainAmt = Math.floor(damage * 0.5);
attacker.hp = Math.min(attacker.maxHp, attacker.hp + drainAmt);
this.showFloatText(attacker.x || 150, attacker.y || 250, `+${drainAmt}`, '#4ade80');
}
if(ability.eff === 'defense_up') {
attacker.def = Math.floor(attacker.def * 1.3);
this.showBattleText(`${attacker.name}'s DEF rose!`);
}
if(ability.eff === 'speed_up') {
attacker.spd = Math.floor(attacker.spd * 1.2);
this.showBattleText(`${attacker.name}'s SPD rose!`);
}
if(ability.eff === 'multi') {
// Multi-hit handled separately
}
} else {
// Status-only ability
if(ability.eff === 'barrier') {
attacker.barrierActive = true;
this.showBattleText(`${attacker.name} raised a barrier!`);
}
if(ability.eff === 'heal') {
const healAmt = Math.floor(attacker.maxHp * 0.4);
attacker.hp = Math.min(attacker.maxHp, attacker.hp + healAmt);
this.showFloatText(attacker.x || 150, attacker.y || 250, `+${healAmt}`, '#4ade80');
}
if(ability.eff === 'regen') {
attacker.regenTurns = 4;
this.showBattleText(`${attacker.name} is regenerating!`);
}
if(ability.eff === 'defense_up') {
attacker.def = Math.floor(attacker.def * 1.3);
this.showBattleText(`${attacker.name}'s DEF rose!`);
}
if(ability.eff === 'speed_up') {
attacker.spd = Math.floor(attacker.spd * 1.2);
this.showBattleText(`${attacker.name}'s SPD rose!`);
}
}
// Clamp HP
target.hp = Math.max(0, Math.min(target.maxHp, target.hp));
}
}
this.renderBattleScene();
// Check for KO
setTimeout(() => {
const aliveEnemies = bs.enemies.filter(e => e.hp > 0);
if(aliveEnemies.length === 0) {
this.endBattle(true);
return;
}
// Check if player has any creatures alive
const alivePlayer = this.player.party.filter(p => p.hp > 0);
if(alivePlayer.length === 0) {
this.endBattle(false);
return;
}
// Enemy turn
bs.phase = 'enemy_turn';
this.renderBattleActions();
setTimeout(() => this.enemyTurn(), 800);
}, 600);
},
enemyTurn() {
const bs = this.battleState;
const enemyUnits = bs.enemies.filter(e => e.hp > 0);
if(enemyUnits.length === 0) {
this.endBattle(true);
return;
}
let idx = 0;
const processNext = () => {
if(idx >= enemyUnits.length) {
// Back to player turn
bs.phase = 'player_turn';
bs.animating = false;
this.renderBattleActions();
this.renderBattleScene();
return;
}
const enemy = enemyUnits[idx];
idx++;
// Check status effects
if(enemy.status === 'shock' && enemy.statusTurns > 0) {
enemy.statusTurns--;
this.showBattleText(`${enemy.name} is paralyzed and can't move!`);
setTimeout(processNext, 600);
return;
}
// Choose ability
const availableAbilities = enemy.abilities.filter(abId => {
const ab = ABILITY_MAP[abId];
if(!ab) return false;
if(!enemy.pp) enemy.pp = {};
return (enemy.pp[abId] !== undefined ? enemy.pp[abId] : ab.pp) > 0;
});
if(availableAbilities.length === 0) {
// Struggle
const alivePlayer = this.player.party.filter(p => p.hp > 0);
if(alivePlayer.length > 0) {
const target = alivePlayer[Math.floor(Math.random() * alivePlayer.length)];
const damage = Math.max(1, Math.floor(enemy.atk * 0.3));
target.hp = Math.max(0, target.hp - damage);
this.showBattleText(`${enemy.name} struggles! ${target.name} takes ${damage} damage.`);
this.showFloatText(target.x || 150, target.y || 250, `-${damage}`, '#f87171');
}
setTimeout(processNext, 600);
return;
}
const abId = availableAbilities[Math.floor(Math.random() * availableAbilities.length)];
const ab = ABILITY_MAP[abId];
// Consume PP
if(!enemy.pp) enemy.pp = {};
if(enemy.pp[abId] === undefined) enemy.pp[abId] = ab.pp;
enemy.pp[abId]--;
const alivePlayer = this.player.party.filter(p => p.hp > 0);
if(alivePlayer.length === 0) {
this.endBattle(false);
return;
}
// Target random alive player
const targets = [alivePlayer[Math.floor(Math.random() * alivePlayer.length)]];
this.executeAbility(enemy, ab, targets, 'enemy');
setTimeout(processNext, 800);
};
processNext();
},
tryCapture() {
const bs = this.battleState;
if(bs.phase !== 'player_turn' || bs.animating) return;
if(bs.isBoss) return;
const target = bs.enemies[0];
if(target.hp <= 0) return;
// Find best capture item
let bestItem = null;
let bestTier = 0;
for(const [k, v] of Object.entries(this.player.inventory)) {
if(ITEMS[k] && ITEMS[k].type === 'capture' && v > 0 && ITEMS[k].tier > bestTier) {
bestItem = k;
bestTier = ITEMS[k].tier;
}
}
if(!bestItem) {
this.showBattleText('No capture items!');
return;
}
// Calculate capture rate
const hpRatio = target.hp / target.maxHp;
const tierBonus = bestTier * 0.15;
const captureRate = (1 - hpRatio) * 0.6 + tierBonus + 0.1;
const success = Math.random() < Math.min(captureRate, 0.95);
// Consume item
this.player.inventory[bestItem]--;
if(this.player.inventory[bestItem] <= 0) delete this.player.inventory[bestItem];
if(success) {
// Add to party
const newCreature = this.createCreature(target, target.level);
if(this.player.party.length < 6) {
this.player.party.push(newCreature);
} else {
// Replace last with captured
this.player.party[this.player.party.length - 1] = newCreature;
}
this.captured.add(target.id);
this.collectedCount = this.captured.size;
this.showBattleText(`Captured ${target.name}!`);
this.addBattleParticle(200, 300, '#4ade80', 20);
// Check collection goal
if(this.collectedCount >= 8 && !this.postGame) {
this.postGame = true;
this.showBattleText('You have collected 8 creatures! The post-game opens!');
}
this.endBattle(true);
} else {
this.showBattleText(`${target.name} broke free!`);
this.addBattleParticle(200, 300, '#f87171', 10);
bs.phase = 'enemy_turn';
this.renderBattleActions();
setTimeout(() => this.enemyTurn(), 800);
}
},
runAway() {
const bs = this.battleState;
if(bs.isBoss) {
this.showBattleText('Cannot flee from a boss!');
return;
}
if(Math.random() < 0.7) {
this.showBattleText('Got away safely!');
setTimeout(() => this.endBattle(false), 500);
} else {
this.showBattleText('Couldn\'t escape!');
bs.phase = 'enemy_turn';
this.renderBattleActions();
setTimeout(() => this.enemyTurn(), 800);
}
},
openSwitch() {
const bs = this.battleState;
if(bs.phase !== 'player_turn' || bs.animating) return;
const overlay = document.getElementById('overlay');
overlay.classList.add('active');
overlay.innerHTML = `
<div class="modal" style="position:relative;min-width:300px" id="switch-modal">
<button class="modal-close" onclick="Game.closeOverlay()">✕</button>
<h2>🔄 Switch Creature</h2>
<div class="team-grid" style="margin-top:1rem">
${this.player.party.map((p, i) => `
<div class="team-card ${i === bs.selectedCreature ? 'selected' : ''}" onclick="Game.switchCreature(${i})">
<div class="name">${p.sprite} ${p.name}</div>
<div class="level">Lv.${p.level}</div>
<div class="hp-mini"><div class="hp-mini-fill" style="width:${p.hp/p.maxHp*100}%"></div></div>
<div style="font-size:0.7rem;color:var(--text-dim)">${p.hp}/${p.maxHp} HP</div>
</div>
`).join('')}
</div>
</div>
`;
},
switchCreature(idx) {
this.battleState.selectedCreature = idx;
this.closeOverlay();
this.renderBattleActions();
},
endBattle(won) {
const bs = this.battleState;
bs.phase = won ? 'victory' : 'defeat';
if(won) {
// EXP calculation
const totalExp = bs.enemies.reduce((s, e) => {
return s + e.level * 10 * (bs.isBoss ? 3 : 1);
}, 0);
// Distribute EXP to party
this.player.party.forEach(p => {
if(p.hp > 0) {
let expGain = Math.floor(totalExp / this.player.party.filter(pp => pp.hp > 0).length);
// Apply exp boost
if(this.player.inventory['exp_boost']) {
expGain = Math.floor(expGain * 1.2);
}
p.exp = (p.exp || 0) + expGain;
// Level up check
while(p.exp >= p.level * 20) {
p.exp -= p.level * 20;
p.level++;
p.maxHp += 5;
p.hp = Math.min(p.hp + 5, p.maxHp);
p.atk += 3;
p.def += 2;
p.spd += 1;
// Learn new abilities
this.learnAbility(p);
}
p.hp = Math.min(p.hp, p.maxHp);
}
});
// Gold reward
this.player.gold += bs.isBoss ? 200 : 20 + bs.enemies[0]?.level * 5;
// Boss defeated
if(bs.isBoss && bs.bossData) {
this.defeatedBosses.add(bs.bossData.id);
this.showBattleText(`Defeated ${bs.bossData.name}!`);
} else {
this.showBattleText('Victory!');
}
this.renderBattleScene();
// Check zone completion
if(bs.isBoss && bs.bossData) {
setTimeout(() => {
this.endBattleScreen();
this.showStory(`The guardian of ${ZONES[this.player.zone].name} has fallen. The path forward opens.`, ZONES[this.player.zone].storyColor);
}, 1500);
} else {
setTimeout(() => this.endBattleScreen(), 1000);
}
} else {
this.showBattleText('Defeat...');
// Auto-heal party
this.player.party.forEach(p => { p.hp = Math.floor(p.maxHp * 0.5); });
this.player.gold = Math.max(0, this.player.gold - 50);
setTimeout(() => {
this.endBattleScreen();
this.showStory('You were defeated. Rest at the nearest shrine to recover.', '#1a0a0a');
}, 1500);
}
},
learnAbility(creature) {
const base = CREATURES.find(c => c.id === creature.id || (c.evolved && c.evolveTo === creature.id));
if(!base) return;
const allAbilities = base.abilities;
const learned = creature.abilities || base.abilities;
// Simple: add one new ability at certain levels
if(creature.level === 10 && !learned.includes('thunder')) learned.push('thunder');
if(creature.level === 15 && !learned.includes('gale_spin')) learned.push('gale_spin');
if(creature.level === 20 && !learned.includes('gale_strike')) learned.push('gale_strike');
if(creature.level === 25 && !learned.includes('flux_wave')) learned.push('flux_wave');
if(creature.level === 30 && !learned.includes('earthquake')) learned.push('earthquake');
if(creature.level === 35 && !learned.includes('void_burst')) learned.push('void_burst');
if(creature.level === 40 && !learned.includes('aether_burst')) learned.push('aether_burst');
},
endBattleScreen() {
this.state = 'overworld';
document.getElementById('battle-screen').classList.remove('active');
this.showHUD();
this.updateHUD();
this.battleState = null;
this.currentEnemy = null;
},
// ==================== BATTLE SCENE RENDERING ====================
renderBattleScene() {
const ctx = this.battleCtx;
const w = this.battleCanvas.clientWidth;
const h = this.battleCanvas.clientHeight;
const bs = this.battleState;
if(!bs) return;
ctx.clearRect(0, 0, w, h);
// Background gradient based on zone
const zone = ZONES[this.player.zone];
const grad = ctx.createLinearGradient(0, 0, 0, h);
grad.addColorStop(0, zone.gradient[0]);
grad.addColorStop(1, zone.gradient[1]);
ctx.fillStyle = grad;
ctx.fillRect(0, 0, w, h);
// Ground
ctx.fillStyle = 'rgba(30,30,60,0.5)';
ctx.fillRect(0, h * 0.6, w, h * 0.4);
// Draw enemies
bs.enemies.forEach((enemy, i) => {
if(enemy.hp <= 0) return;
const ex = w * 0.6 + i * 80;
const ey = h * 0.35;
enemy.x = ex;
enemy.y = ey;
// Enemy sprite
ctx.font = '48px serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(enemy.sprite, ex, ey);
// Enemy name & level
ctx.font = '14px system-ui';
ctx.fillStyle = '#e8e8f0';
ctx.fillText(`${enemy.name} Lv.${enemy.level}`, ex, ey - 40);
// Enemy HP bar
const barW = 100;
const barH = 8;
const barX = ex - barW/2;
const barY = ey - 25;
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(barX, barY, barW, barH);
const hpPct = enemy.hp / enemy.maxHp;
ctx.fillStyle = hpPct < 0.25 ? '#f87171' : hpPct < 0.5 ? '#fbbf24' : '#4ade80';
ctx.fillRect(barX, barY, barW * hpPct, barH);
// Status indicators
if(enemy.status) {
const statusEmoji = enemy.status === 'burn' ? '🔥' : enemy.status === 'frost' ? '❄️' : enemy.status === 'shock' ? '⚡' : '';
if(statusEmoji) {
ctx.font = '16px serif';
ctx.fillText(statusEmoji, ex + 25, ey - 10);
}
}
if(enemy.barrierActive) {
ctx.font = '20px serif';
ctx.fillText('🛡️', ex, ey + 30);
}
});
// Draw player party (front creature)
const playerCreature = this.player.party[bs.selectedCreature];
if(playerCreature && playerCreature.hp > 0) {
const px = w * 0.2;
const py = h * 0.55;
playerCreature.x = px;
playerCreature.y = py;
// Creature sprite
ctx.font = '56px serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(playerCreature.sprite, px, py);
// Name & level
ctx.font = '14px system-ui';
ctx.fillStyle = '#e8e8f0';
ctx.fillText(`${playerCreature.name} Lv.${playerCreature.level}`, px, py - 45);
// HP bar
const barW = 120;
const barH = 10;
const barX = px - barW/2;
const barY = py - 30;
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(barX, barY, barW, barH);
const hpPct = playerCreature.hp / playerCreature.maxHp;
ctx.fillStyle = hpPct < 0.25 ? '#f87171' : hpPct < 0.5 ? '#fbbf24' : '#4ade80';
ctx.fillRect(barX, barY, barW * hpPct, barH);
// Status
if(playerCreature.status) {
const statusEmoji = playerCreature.status === 'burn' ? '🔥' : playerCreature.status === 'frost' ? '❄️' : playerCreature.status === 'shock' ? '⚡' : '';
if(statusEmoji) {
ctx.font = '16px serif';
ctx.fillText(statusEmoji, px + 30, py - 10);
}
}
if(playerCreature.barrierActive) {
ctx.font = '20px serif';
ctx.fillText('🛡️', px, py + 35);
}
if(playerCreature.regenTurns > 0) {
ctx.font = '14px serif';
ctx.fillText('💚', px - 30, py - 10);
}
}
// Battle particles
this.battleParticles.forEach(p => {
p.x += p.vx; p.y += p.vy;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fillStyle = p.color + Math.floor(p.alpha * 255).toString(16).padStart(2,'0');
ctx.fill();
});
},
addBattleParticle(x, y, color, count) {
for(let i = 0; i < count; i++) {
this.battleParticles.push({
x, y,
vx: (Math.random() - 0.5) * 4,
vy: (Math.random() - 0.5) * 4,
size: Math.random() * 4 + 1,
alpha: Math.random() * 0.8 + 0.2,
color
});
}
},
showFloatText(x, y, text, color) {
const el = document.createElement('div');
el.className = 'float-text';
el.textContent = text;
el.style.left = x + 'px';
el.style.top = y + 'px';
el.style.color = color;
el.style.fontSize = text.length > 5 ? '1rem' : '1.2rem';
document.getElementById('game-container').appendChild(el);
setTimeout(() => el.remove(), 1000);
},
// ==================== MENUS ====================
openTeam() {
const overlay = document.getElementById('overlay');
overlay.classList.add('active');
overlay.innerHTML = `
<div class="modal" style="position:relative;min-width:400px" id="team-modal">
<button class="modal-close" onclick="Game.closeOverlay()">✕</button>
<h2>👥 Team</h2>
<p style="margin-bottom:1rem;color:var(--text-dim)">Collected: ${this.captured.size}/27</p>
<div class="team-grid">
${this.player.party.map((p, i) => `
<div class="team-card" onclick="Game.openTeamDetail(${i})">
<div class="name">${p.sprite} ${p.name}</div>
<div class="level">Lv.${p.level} ${p.affinity}</div>
<div class="hp-mini"><div class="hp-mini-fill" style="width:${p.hp/p.maxHp*100}%"></div></div>
<div style="font-size:0.7rem;color:var(--text-dim)">HP:${p.hp}/${p.maxHp} ATK:${p.atk} DEF:${p.def} SPD:${p.spd}</div>
</div>
`).join('')}
</div>
</div>
`;
},
openTeamDetail(idx) {
const p = this.player.party[idx];
const overlay = document.getElementById('overlay');
overlay.innerHTML = `
<div class="modal" style="position:relative;min-width:350px" id="detail-modal">
<button class="modal-close" onclick="Game.closeOverlay()">✕</button>
<h2>${p.sprite} ${p.name}</h2>
<p style="color:var(--text-dim);margin-bottom:0.5rem">Lv.${p.level} ${p.affinity}</p>
<p style="font-size:0.85rem;margin-bottom:1rem;font-style:italic">"${p.lore}"</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.5rem;margin-bottom:1rem">
<div>HP: ${p.hp}/${p.maxHp}</div>
<div>ATK: ${p.atk}</div>
<div>DEF: ${p.def}</div>
<div>SPD: ${p.spd}</div>
</div>
<h3 style="margin-bottom:0.5rem">Abilities</h3>
<div style="display:flex;flex-direction:column;gap:0.3rem">
${p.abilities.map(abId => {
const ab = ABILITY_MAP[abId];
if(!ab) return '';
const ppLeft = p.pp ? (p.pp[abId] !== undefined ? p.pp[abId] : ab.pp) : ab.pp;
const typeColor = TYPES[ab.type]?.color || '#fff';
return `<div style="display:flex;align-items:center;gap:0.5rem;padding:0.3rem;background:rgba(30,30,60,0.5);border-radius:6px">
<span style="width:8px;height:8px;border-radius:50%;background:${typeColor};flex-shrink:0"></span>
<span style="flex:1;font-size:0.85rem">${ab.name}</span>
<span style="font-size:0.7rem;color:var(--text-dim)">PP:${ppLeft}/${ab.pp}</span>
<span style="font-size:0.7rem;color:var(--text-dim)">${ab.power > 0 ? ab.power + ' power' : ab.eff}</span>
</div>`;
}).join('')}
</div>
${p.evolved ? '' : `<div style="margin-top:1rem;padding:0.5rem;background:rgba(123,104,238,0.2);border-radius:8px;font-size:0.8rem">
<strong>Evolution:</strong> ${p.evoLevel || '?'}+ ${p.evolveTo ? '→ ' + CREATURES.find(c=>c.id===p.evolveTo)?.name : '?'}
</div>`}
</div>
`;
},
openInventory() {
const overlay = document.getElementById('overlay');
overlay.classList.add('active');
const invItems = Object.entries(this.player.inventory).filter(([k,v]) => v > 0);
overlay.innerHTML = `
<div class="modal" style="position:relative;min-width:350px" id="inv-modal">
<button class="modal-close" onclick="Game.closeOverlay()">✕</button>
<h2>🎒 Bag</h2>
<p style="margin-bottom:1rem;color:var(--text-dim)">Gold: ${this.player.gold}g</p>
${invItems.length === 0 ? '<p style="color:var(--text-dim)">Empty</p>' : ''}
<div class="inv-grid">
${invItems.map(([k,v]) => {
const item = ITEMS[k];
if(!item) return '';
return `<div class="inv-item" onclick="Game.useItem('${k}')">
<div class="icon">${item.icon}</div>
<div style="font-size:0.7rem">${item.name}</div>
<div style="font-size:0.65rem;color:var(--text-dim)">x${v}</div>
</div>`;
}).join('')}
</div>
</div>
`;
},
useItem(itemId) {
const item = ITEMS[itemId];
if(!item || !this.player.inventory[itemId] || this.player.inventory[itemId] <= 0) return;
if(item.type === 'heal') {
const front = this.player.party[0];
if(!front) return;
const healAmt = item.heal >= 999 ? front.maxHp - front.hp : item.heal;
front.hp = Math.min(front.maxHp, front.hp + healAmt);
this.player.inventory[itemId]--;
if(this.player.inventory[itemId] <= 0) delete this.player.inventory[itemId];
this.showFloatText(this.player.x * this.tileW, this.player.y * this.tileH, `+${healAmt} HP`, '#4ade80');
this.updateHUD();
this.openInventory();
} else if(item.type === 'catalyst') {
// Check if any creature can evolve
const canEvolve = this.player.party.find(p => {
if(p.evolved) return false;
const base = CREATURES.find(c => c.id === p.id);
if(!base || !base.evoLevel) return false;
return p.level >= base.evoLevel;
});
if(canEvolve && canEvolve.affinity === item.affinity) {
const newBase = CREATURES.find(c => c.id === canEvolve.evolveTo);
if(newBase) {
const newCreature = this.createCreature(newBase, canEvolve.level);
newCreature.abilities = newBase.abilities;
newCreature.exp = canEvolve.exp || 0;
newCreature.pp = canEvolve.pp || {};
newCreature.status = canEvolve.status;
newCreature.statusTurns = canEvolve.statusTurns;
newCreature.barrierActive = canEvolve.barrierActive;
newCreature.regenTurns = canEvolve.regenTurns;
newCreature.drainTurns = canEvolve.drainTurns;
const idx = this.player.party.indexOf(canEvolve);
this.player.party[idx] = newCreature;
this.captured.delete(canEvolve.id);
this.captured.add(newBase.id);
this.player.inventory[itemId]--;
if(this.player.inventory[itemId] <= 0) delete this.player.inventory[itemId];
this.showFloatText(this.player.x * this.tileW, this.player.y * this.tileH, `Evolved to ${newBase.name}!`, '#fbbf24');
this.updateHUD();
this.openInventory();
}
} else {
this.showFloatText(this.player.x * this.tileW, this.player.y * this.tileH, 'No compatible creature!', '#f87171');
}
} else if(item.type === 'consumable') {
const front = this.player.party[0];
if(!front) return;
if(itemId === 'atk_boost') { front.atk += 2; }
if(itemId === 'def_boost') { front.def += 2; }
if(itemId === 'spd_boost') { front.spd += 2; }
this.player.inventory[itemId]--;
if(this.player.inventory[itemId] <= 0) delete this.player.inventory[itemId];
this.showFloatText(this.player.x * this.tileW, this.player.y * this.tileH, 'Stat boosted!', '#4ade80');
this.updateHUD();
this.openInventory();
} else if(item.type === 'capture') {
// Use in battle
this.tryCapture();
} else {
this.player.inventory[itemId]--;
if(this.player.inventory[itemId] <= 0) delete this.player.inventory[itemId];
this.openInventory();
}
},
openPokedex() {
const overlay = document.getElementById('overlay');
overlay.classList.add('active');
overlay.innerHTML = `
<div class="modal" style="position:relative;min-width:400px" id="pokedex-modal">
<button class="modal-close" onclick="Game.closeOverlay()">✕</button>
<h2>📖 Creature Codex</h2>
<p style="margin-bottom:1rem;color:var(--text-dim)">Discovered: ${this.captured.size}/27</p>
<div class="pokedex-grid">
${CREATURES.map(c => {
const discovered = this.captured.has(c.id);
return `<div class="pokedex-entry ${discovered ? 'discovered' : ''}">
<div class="picon">${discovered ? c.sprite : '❓'}</div>
<div class="pname">${discovered ? c.name : '???'}</div>
<div class="ptype" style="color:${discovered ? TYPES[c.affinity]?.color : '#555'}">${discovered ? c.affinity : '???'}</div>
</div>`;
}).join('')}
</div>
</div>
`;
},
closeOverlay() {
document.getElementById('overlay').classList.remove('active');
},
// ==================== AUDIO ====================
toggleMute() {
this.muted = !this.muted;
document.getElementById('mute-btn').textContent = this.muted ? '🔇' : '🔊';
if(this.muted) {
if(this.audioCtx) this.audioCtx.suspend();
} else {
if(this.audioCtx) this.audioCtx.resume();
}
},
playAmbient() {
if(this.muted) return;
try {
if(!this.audioCtx) {
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
if(this.audioCtx.state === 'suspended') this.audioCtx.resume();
// Create ambient pad
const osc1 = this.audioCtx.createOscillator();
const osc2 = this.audioCtx.createOscillator();
const gain = this.audioCtx.createGain();
const filter = this.audioCtx.createBiquadFilter();
osc1.type = 'sine';
osc2.type = 'sine';
osc1.frequency.value = 110;
osc2.frequency.value = 165;
gain.gain.value = 0.03;
filter.type = 'lowpass';
filter.frequency.value = 400;
osc1.connect(filter);
osc2.connect(filter);
filter.connect(gain);
gain.connect(this.audioCtx.destination);
osc1.start();
osc2.start();
// LFO for movement
const lfo = this.audioCtx.createOscillator();
const lfoGain = this.audioCtx.createGain();
lfo.frequency.value = 0.1;
lfoGain.gain.value = 10;
lfo.connect(lfoGain);
lfoGain.connect(osc1.frequency);
lfo.start();
this.currentOsc = {osc1, osc2, gain, filter, lfo, lfoGain};
} catch(e) {}
},
playSound(type) {
if(this.muted || !this.audioCtx) return;
try {
const osc = this.audioCtx.createOscillator();
const gain = this.audioCtx.createGain();
osc.connect(gain);
gain.connect(this.audioCtx.destination);
if(type === 'hit') {
osc.type = 'square';
osc.frequency.value = 200;
gain.gain.setValueAtTime(0.1, this.audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.audioCtx.currentTime + 0.2);
osc.start();
osc.stop(this.audioCtx.currentTime + 0.2);
} else if(type === 'capture') {
osc.type = 'sine';
osc.frequency.setValueAtTime(400, this.audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(800, this.audioCtx.currentTime + 0.3);
gain.gain.setValueAtTime(0.08, this.audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.audioCtx.currentTime + 0.3);
osc.start();
osc.stop(this.audioCtx.currentTime + 0.3);
} else if(type === 'victory') {
osc.type = 'sine';
osc.frequency.setValueAtTime(300, this.audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(600, this.audioCtx.currentTime + 0.5);
gain.gain.setValueAtTime(0.06, this.audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.audioCtx.currentTime + 0.5);
osc.start();
osc.stop(this.audioCtx.currentTime + 0.5);
}
} catch(e) {}
},
// ==================== MINIMAP ====================
renderMinimap() {
const ctx = this.minimapCtx;
const cw = this.minimapCanvas.width;
const ch = this.minimapCanvas.height;
ctx.clearRect(0, 0, cw, ch);
const map = this.mapTiles;
if(!map) return;
const scaleX = cw / this.mapW;
const scaleY = ch / this.mapH;
// Draw tiles
for(let y = 0; y < this.mapH; y++) {
for(let x = 0; x < this.mapW; x++) {
const tile = map[y][x];
if(tile === 1) ctx.fillStyle = '#0a0a1a';
else if(tile === 5) ctx.fillStyle = '#fbbf24';
else if(tile === 6) ctx.fillStyle = '#ef4444';
else if(tile === 2) ctx.fillStyle = '#2a4a2a';
else if(tile === 4) ctx.fillStyle = '#1a3a5a';
else ctx.fillStyle = '#1a3a1a';
ctx.fillRect(x * scaleX, y * scaleY, scaleX + 0.5, scaleY + 0.5);
}
}
// Player dot
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(this.player.x * scaleX, this.player.y * scaleY, 3, 0, Math.PI * 2);
ctx.fill();
// Zone name
ctx.fillStyle = '#8888aa';
ctx.font = '8px system-ui';
ctx.textAlign = 'center';
ctx.fillText(ZONES[this.player.zone].name, cw/2, ch - 3);
},
// ==================== MAIN LOOP ====================
loop() {
const now = performance.now();
const dt = Math.min((now - (this.lastTime || now)) / 1000, 0.05);
this.lastTime = now;
this.animFrame++;
if(this.state === 'overworld') {
// Movement
if(!this.tutorialActive && !this.storyActive) {
this.moveTimer += dt;
if(this.moveTimer >= this.moveInterval) {
this.moveTimer = 0;
if(this.keyState['ArrowUp'] || this.keyState['w'] || this.keyState['W'] || this.touchDir === 'up') this.tryMove(0, -1);
if(this.keyState['ArrowDown'] || this.keyState['s'] || this.keyState['S'] || this.touchDir === 'down') this.tryMove(0, 1);
if(this.keyState['ArrowLeft'] || this.keyState['a'] || this.keyState['A'] || this.touchDir === 'left') this.tryMove(-1, 0);
if(this.keyState['ArrowRight'] || this.keyState['d'] || this.keyState['D'] || this.touchDir === 'right') this.tryMove(1, 0);
}
}
// Zone name timer
if(this.zoneNameTimer !== undefined && this.zoneNameTimer > 0) {
this.zoneNameTimer -= dt;
}
this.renderOverworld(dt);
this.renderMinimap();
} else if(this.state === 'battle') {
this.renderBattleScene();
// Update particles
this.battleParticles = this.battleParticles.filter(p => {
p.alpha -= dt * 0.5;
return p.alpha > 0;
});
}
// Title particles
if(!document.getElementById('title-screen').classList.contains('hidden')) {
this.renderTitle();
}
requestAnimationFrame(() => this.loop());
},
handleEscape() {
if(this.state === 'battle') return;
if(document.getElementById('overlay').classList.contains('active')) {
this.closeOverlay();
}
},
zoneNameTimer: 0
};
// ==================== INIT ====================
window.addEventListener('DOMContentLoaded', () => Game.init());
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.6-35b-a3b","provider":"lmstudio","persona":"middle","sessionId":"d0c378f3-ad1d-4e12-aa4b-63de17e6d635","tokensIn":725643,"tokensOut":40002,"tokensTotal":765645,"turns":17,"toolCalls":16,"failedToolCalls":0,"timestamp":"2026-07-13T00:30:35.125Z"} -->