Creature Collection
inkling · basic

Browser check: Artifact runtime error. This is a technical check, not a quality review.
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 57,108 bytes · SHA-256 0381f50a4a3d
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Veil of Aetheris</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Cinzel+Decorative:wght@400;700&family=Orbitron:wght@400;700&family=Quicksand:wght@300;500;700&display=swap');
:root{
--bg: #0b0c15;
--glass: rgba(255,255,255,0.06);
--glass-border: rgba(255,255,255,0.12);
--text: #e8eaf6;
--dim: #8b8fa3;
--accent: #c7a4ff;
--sol: #ffb347;
--lun: #7ee787;
--abyss: #9d4bff;
--aether: #4fc3f7;
--echo: #f093fb;
--prism: #ffe082;
--obsidian: #b0bec5;
--danger: #ff6b6b;
--heal: #69f0ae;
}
*{box-sizing:border-box;touch-action:manipulation;user-select:none;-webkit-user-select:none;}
html,body{margin:0;padding:0;width:100%;height:100%;overflow:hidden;background:var(--bg);color:var(--text);font-family:'Quicksand',sans-serif;}
canvas{display:block;}
#game{width:100%;height:100%;position:relative;overflow:hidden;}
.screen{position:absolute;inset:0;width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;transition:opacity .6s ease;}
.screen.hide{opacity:0;pointer-events:none;}
.screen.show{opacity:1;pointer-events:auto;}
.glass{background:var(--glass);backdrop-filter:blur(14px) saturate(140%);-webkit-backdrop-filter:blur(14px) saturate(140%);border:1px solid var(--glass-border);border-radius:24px;box-shadow:0 20px 60px rgba(0,0,0,.6),inset 0 1px 0 rgba(255,255,255,.08);}
.glass-modal{background:rgba(11,12,21,.85);backdrop-filter:blur(18px);border:1px solid rgba(255,255,255,.15);border-radius:28px;padding:32px;max-width:92vw;width:640px;box-shadow:0 30px 80px rgba(0,0,0,.7);animation:fadeUp .5s ease;}
@keyframes fadeUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}
@keyframes float{0%,100%{transform:translateY(0)}50%{transform:translateY(-6px)}}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.6}}
@keyframes shimmer{0%{background-position:-200% center}100%{background-position:200% center}}
@keyframes shake{0%,100%{transform:translate(0,0)}10%,30%,50%,70%,90%{transform:translate(-3px,2px)}20%,40%,60%,80%{transform:translate(3px,-2px)}}
.h1-title{font-family:'Cinzel Decorative',serif;font-weight:700;font-size:clamp(2.8rem,8vw,5.5rem);letter-spacing:.12em;text-align:center;background:linear-gradient(180deg,#fff 0%,#c7a4ff 60%,#ffe082 100%);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;filter:drop-shadow(0 0 30px rgba(199,164,255,.35));margin-bottom:8px;animation:float 6s ease-in-out infinite;}
.subtitle{font-family:'Orbitron',sans-serif;font-size:1rem;color:var(--dim);letter-spacing:.25em;text-transform:uppercase;margin-bottom:48px;}
.btn{font-family:'Orbitron',sans-serif;padding:18px 36px;border-radius:50px;border:1px solid rgba(255,255,255,.2);background:rgba(255,255,255,.05);color:var(--text);font-size:1rem;letter-spacing:.1em;cursor:pointer;transition:all .2s ease;position:relative;overflow:hidden;}
.btn:hover{background:rgba(255,255,255,.12);border-color:rgba(255,255,255,.35);transform:translateY(-2px);box-shadow:0 10px 30px rgba(199,164,255,.15);}
.btn-primary{background:linear-gradient(135deg,rgba(199,164,255,.25),rgba(255,178,102,.15));border-color:rgba(199,164,255,.4);color:#fff;}
.btn-primary:hover{background:linear-gradient(135deg,rgba(199,164,255,.4),rgba(255,178,102,.25));border-color:rgba(199,164,255,.6);box-shadow:0 0 30px rgba(199,164,255,.3);}
.title-screen{background:radial-gradient(circle at 50% 30%,#15152a 0%,#0b0c15 70%);z-index:100;}
.title-screen .bg-stars{position:absolute;inset:0;pointer-events:none;background-image:radial-gradient(circle at 20% 30%,rgba(255,255,255,.12) 1px,transparent 1px),radial-gradient(circle at 80% 70%,rgba(255,255,255,.08) 1px,transparent 1px),radial-gradient(circle at 50% 50%,rgba(199,164,255,.08) 2px,transparent 2px);background-size:300px 300px,200px 200px,400px 400px;animation:drift 120s linear infinite;}
@keyframes drift{from{background-position:0 0,100px 50px,200px 100px}to{background-position:300px 300px,400px 350px,600px 400px}}
#titleCanvas{position:absolute;inset:0;width:100%;height:100%;z-index:0;}
.title-content{position:relative;z-index:10;text-align:center;padding:20px;}
.nav-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px;margin-top:32px;max-width:420px;width:100%;margin-left:auto;margin-right:auto;}
.btn-small{padding:14px 18px;font-size:.9rem;border-radius:16px;white-space:nowrap;}
.screen-overlay{position:absolute;inset:0;background:radial-gradient(circle at center,rgba(199,164,255,.15) 0%,transparent 70%);pointer-events:none;opacity:.6;z-index:1;}
#overworld{z-index:10;}
#battle{z-index:20;}
#menu{z-index:30;}
#dialog{z-index:40;}
#shrine{z-index:35;}
#tutorial{z-index:50;}
#mapScreen{z-index:30;}
#bagScreen{z-index:30;}
#encyclopedia{z-index:30;}
#teamScreen{z-index:30;}
#saveScreen{z-index:30;}
#endScreen{z-index:100;}
#overworldCanvas{width:100%;height:100%;display:block;}
#battleCanvas{position:absolute;inset:0;width:100%;height:100%;display:block;}
.battle-ui{position:absolute;bottom:0;left:0;right:0;padding:24px;display:flex;flex-direction:column;gap:12px;z-index:5;}
.battle-info-top{display:flex;justify-content:space-between;align-items:center;padding:8px 20px;}
.enemy-row{display:flex;gap:20px;justify-content:center;align-items:center;}
.player-row{display:flex;gap:12px;justify-content:center;align-items:center;margin-top:8px;}
.health-bar{width:120px;height:14px;border-radius:7px;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);overflow:hidden;position:relative;}
.health-inner{height:100%;width:100%;background:linear-gradient(90deg,#69f0ae,#4fc3f7);transition:width .3s ease;box-shadow:0 0 10px rgba(69,240,174,.3);}
.ability-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px;max-width:480px;margin:0 auto;}
.ability-btn{background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.12);border-radius:16px;padding:14px;text-align:center;cursor:pointer;transition:all .2s;}
.ability-btn:hover:not(:disabled){background:rgba(255,255,255,.1);border-color:rgba(255,255,255,.3);transform:scale(1.03);}
.ability-btn:disabled{opacity:.3;cursor:not-allowed;}
.ability-name{font-family:'Orbitron',sans-serif;font-size:.85rem;color:var(--text);margin-bottom:4px;}
.ability-meta{font-size:.7rem;color:var(--dim);}
.turn-indicator{font-family:'Orbitron',sans-serif;color:var(--prism);font-size:.9rem;letter-spacing:.15em;text-shadow:0 0 15px rgba(255,224,130,.4);}
.party-card{background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);border-radius:12px;padding:8px;display:flex;align-items:center;gap:8px;min-width:80px;}
.party-avatar{width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,var(--accent),var(--echo));display:flex;align-items:center;justify-content:center;font-size:.7rem;font-weight:bold;}
.party-stats{font-size:.65rem;color:var(--dim);line-height:1.2;}
.floating-text{position:absolute;font-family:'Orbitron',sans-serif;font-weight:bold;font-size:1.1rem;pointer-events:none;animation:floatUp .8s ease-out forwards;}
@keyframes floatUp{from{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(-40px)}}
.zone-label{position:absolute;top:20px;left:20px;background:rgba(11,12,21,.7);backdrop-filter:blur(8px);padding:10px 22px;border-radius:50px;border:1px solid rgba(255,255,255,.1);font-family:'Orbitron',sans-serif;font-size:.85rem;color:var(--dim);letter-spacing:.15em;z-index:5;}
.modal-body{display:flex;flex-direction:column;gap:24px;}
.dialog-text{font-size:1.15rem;line-height:1.7;color:var(--text);text-align:center;}
.dialog-vignette{position:absolute;inset:0;background:radial-gradient(circle at 50% 60%,rgba(199,164,255,.4) 0%,rgba(11,12,21,.9) 80%);z-index:-1;border-radius:24px;}
.story-progress{display:flex;gap:8px;justify-content:center;margin-top:12px;}
.story-dot{width:10px;height:10px;border-radius:50%;background:rgba(255,255,255,.15);transition:all .3s;}
.story-dot.active{background:var(--prism);box-shadow:0 0 10px var(--prism);}
.cutscene-box{position:absolute;inset:40px;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;z-index:25;padding:40px;}
.cutscene-box h2{font-family:'Cinzel Decorative',serif;color:var(--prism);font-size:2.2rem;margin-bottom:12px;text-shadow:0 0 30px rgba(255,224,130,.5);}
.cutscene-box p{font-size:1.2rem;max-width:600px;color:var(--text);line-height:1.8;}
.cutscene-vignette{position:absolute;inset:0;background:radial-gradient(circle at 50% 40%,rgba(255,178,102,.2) 0%,rgba(11,12,21,.85) 70%);z-index:-1;}
.collection-mini-game{display:flex;flex-direction:column;align-items:center;gap:20px;margin-top:8px;}
.tap-zone{width:240px;height:80px;background:linear-gradient(90deg,rgba(255,255,255,.05),rgba(255,255,255,.02));border:2px solid rgba(255,255,255,.15);border-radius:40px;position:relative;overflow:hidden;cursor:pointer;}
.tap-zone .marker{position:absolute;top:-4px;left:20%;width:40px;height:calc(100% + 8px);background:var(--prism);opacity:.8;border-radius:20px;animation:markerSlide 2s linear infinite;}
.tap-zone .hit-area{position:absolute;left:55%;width:30%;height:100%;background:rgba(255,255,255,.08);border-left:1px solid rgba(255,255,255,.2);border-right:1px solid rgba(255,255,255,.2);}
@keyframes markerSlide{from{left:-10%}to{left:110%}}
.enc-entry{display:grid;grid-template-columns:48px 1fr;gap:14px;padding:10px 0;border-bottom:1px solid rgba(255,255,255,.08);align-items:center;}
.enc-avatar{width:48px;height:48px;border-radius:50%;background:linear-gradient(135deg,rgba(255,255,255,.1),rgba(255,255,255,.05));display:flex;align-items:center;justify-content:center;font-size:1.2rem;}
.enc-info h3{font-family:'Orbitron',sans-serif;color:var(--text);margin:0 0 4px;}
.enc-info span{font-size:.75rem;color:var(--dim);}
.tutorial-overlay{position:absolute;inset:0;background:rgba(0,0,0,.75);backdrop-filter:blur(10px);z-index:60;display:flex;align-items:center;justify-content:center;}
.tut-card{max-width:520px;padding:32px;text-align:center;}
.tut-card h3{font-family:'Orbitron',sans-serif;color:var(--sol);margin-bottom:16px;letter-spacing:.1em;}
.tut-card p{color:var(--dim);margin-bottom:24px;line-height:1.6;}
.tut-arrow{font-size:3rem;color:var(--accent);animation:float 2s infinite;}
</style>
</head>
<body>
<div id="game">
<!-- Screens -->
<div id="titleScreen" class="screen show title-screen">
<canvas id="titleCanvas"></canvas>
<div class="bg-stars"></div>
<div class="title-content">
<h1 class="h1-title">VEIL OF AETHERIS</h1>
<p class="subtitle">Bind the Fragments, Restore the Veil</p>
<div style="margin-top:48px;">
<button class="btn btn-primary" onclick="startNewGame()">NEW BINDING</button>
<button class="btn" onclick="continueGame()" style="margin-left:16px;">CONTINUE</button>
</div>
<p style="margin-top:32px;font-size:.75rem;color:rgba(255,255,255,.3);">WASD / Arrows to move • Touch controls available</p>
</div>
</div>
<div id="overworld" class="screen hide">
<canvas id="overworldCanvas"></canvas>
<div class="zone-label" id="zoneLabel">Bloomreach</div>
<div class="nav-grid" style="position:absolute;bottom:24px;left:50%;transform:translateX(-50%);z-index:10;max-width:340px;">
<button class="btn btn-small" onclick="openMenu('team')">TEAM</button>
<button class="btn btn-small" onclick="openMenu('bag')">BAG</button>
<button class="btn btn-small" onclick="openMenu('map')">MAP</button>
<button class="btn btn-small" onclick="openMenu('save')">SAVE</button>
<button class="btn btn-small" onclick="openMenu('encyclopedia')">CODEX</button>
<button class="btn btn-small" onclick="openMenu('shrine')">REST</button>
</div>
</div>
<div id="battle" class="screen hide">
<canvas id="battleCanvas"></canvas>
<div class="screen-overlay"></div>
<div class="battle-ui" id="battleUI">
<div class="battle-info-top">
<div id="turnIndicator" class="turn-indicator">PLAYER TURN</div>
<div id="enemyCount">ENEMY: 1</div>
</div>
<div class="enemy-row" id="enemyRow"></div>
<div class="player-row" id="playerRow"></div>
<div class="ability-grid" id="abilityGrid"></div>
</div>
</div>
<div id="dialog" class="screen hide">
<div class="glass-modal" style="position:relative;">
<div class="dialog-vignette"></div>
<h2 id="dialogTitle" style="font-family:'Cinzel Decorative',serif;color:var(--accent);margin-bottom:8px;">Binding Initiated</h2>
<p class="dialog-text" id="dialogBody">A strange energy pulses in the air. A creature has appeared before you.</p>
<div class="story-progress" id="storyDots">
<span class="story-dot active"></span><span class="story-dot"></span><span class="story-dot"></span><span class="story-dot"></span>
</div>
<div style="margin-top:24px;display:flex;gap:12px;justify-content:center;">
<button class="btn btn-primary" onclick="closeDialog()">CONTINUE</button>
</div>
</div>
</div>
<div id="shrine" class="screen hide">
<div class="glass-modal" style="position:relative;">
<h2 style="font-family:'Cinzel Decorative',serif;color:var(--heal);margin-bottom:8px;">SANCTUARY SHRINE</h2>
<p style="color:var(--dim);margin-bottom:24px;">Rest here to restore vitality and record your progress.</p>
<div style="display:flex;gap:12px;justify-content:center;">
<button class="btn btn-primary" onclick="healParty()">REST & HEAL</button>
<button class="btn" onclick="saveGame(true)">SAVE PROGRESS</button>
</div>
</div>
</div>
<div id="menu" class="screen hide">
<div class="glass-modal" style="max-width:640px;width:92vw;padding:24px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
<h2 id="menuTitle" style="font-family:'Orbitron',sans-serif;color:var(--accent);margin:0;">MENU</h2>
<button onclick="closeMenu()" style="background:none;border:none;color:var(--text);font-size:1.2rem;">✕</button>
</div>
<div id="menuContent"></div>
</div>
</div>
<div id="mapScreen" class="screen hide" style="background:rgba(11,12,21,.9);">
<div class="glass-modal" style="width:92vw;max-width:640px;">
<h2 style="font-family:'Orbitron',sans-serif;color:var(--aether);margin-bottom:16px;">VEIL MAP</h2>
<div id="mapContent" style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;"></div>
</div>
</div>
<div id="bagScreen" class="screen hide">
<div class="glass-modal" style="width:92vw;max-width:640px;">
<h2 style="font-family:'Orbitron',sans-serif;color:var(--sol);margin-bottom:16px;">INVENTORY</h2>
<div id="bagContent"></div>
</div>
</div>
<div id="teamScreen" class="screen hide">
<div class="glass-modal" style="width:92vw;max-width:640px;">
<h2 style="font-family:'Orbitron',sans-serif;color:var(--echo);margin-bottom:16px;">PARTY COMPOSITION</h2>
<div id="teamContent"></div>
</div>
</div>
<div id="encyclopedia" class="screen hide">
<div class="glass-modal" style="width:92vw;max-width:640px;">
<h2 style="font-family:'Orbitron',sans-serif;color:var(--prism);margin-bottom:16px;">AETHER ENCYCLOPEDIA</h2>
<div id="encContent"></div>
</div>
</div>
<div id="saveScreen" class="screen hide">
<div class="glass-modal" style="width:92vw;max-width:640px;">
<h2 style="font-family:'Orbitron',sans-serif;color:var(--accent);margin-bottom:16px;">ARCHIVE SLOT</h2>
<div style="display:flex;gap:12px;flex-wrap:wrap;">
<button class="btn" onclick="loadSlot(0)">SLOT 1</button>
<button class="btn" onclick="loadSlot(1)">SLOT 2</button>
<button class="btn" onclick="loadSlot(2)">SLOT 3</button>
</div>
<div style="margin-top:16px;">
<button class="btn btn-primary" onclick="saveGame(false)">OVERWRITE CURRENT</button>
</div>
</div>
</div>
<div id="tutorial" class="screen hide">
<div class="tut-card glass-modal">
<h3 id="tutTitle">WELCOME, BINDER</h3>
<p id="tutBody">Use WASD or Arrow keys to move through the Veil. Approach creatures to challenge them.</p>
<div class="tut-arrow">↓</div>
<button class="btn btn-primary" onclick="nextTutorialStep()">BEGIN</button>
</div>
</div>
<div id="endScreen" class="screen hide" style="background:radial-gradient(circle at 50% 40%,#1a1033 0%,#0b0c15 80%);">
<div style="text-align:center;padding:40px;">
<h1 class="h1-title" style="font-size:3rem;">VEIL RESTORED</h1>
<h3 style="font-family:'Orbitron',sans-serif;color:var(--prism);letter-spacing:.2em;margin-bottom:24px;">THE BINDING IS COMPLETE</h3>
<p style="max-width:480px;margin:0 auto;color:var(--dim);line-height:1.8;margin-bottom:32px;">You have bound the Fragments, defeated the Anchors, and restored harmony to the shattered realms. The Aetheris now breathes freely.</p>
<button class="btn btn-primary" onclick="returnToTitle()">RETURN TO START</button>
</div>
</div>
<div id="floatingContainer" style="position:absolute;inset:0;pointer-events:none;overflow:hidden;z-index:200;"></div>
</div>
<script>
/* ===================== GAME DATA ===================== */
const TYPES = [
{name:'Solar',abbr:'SOL',color:'#ffb347',bg:'linear-gradient(135deg,#ffb347,#ff8c42)'},
{name:'Lunar',abbr:'LUN',color:'#7ee787',bg:'linear-gradient(135deg,#7ee787,#4caf50)'},
{name:'Void',abbr:'ABY',color:'#9d4bff',bg:'linear-gradient(135deg,#9d4bff,#7b1fa2)'},
{name:'Aether',abbr:'AET',color:'#4fc3f7',bg:'linear-gradient(135deg,#4fc3f7,#0288d1)'},
{name:'Echo',abbr:'ECH',color:'#f093fb',bg:'linear-gradient(135deg,#f093fb,#e91e8c)'},
{name:'Prism',abbr:'PRI',color:'#ffe082',bg:'linear-gradient(135deg,#ffe082,#ff9800)'},
{name:'Obsidian',abbr:'OBS',color:'#b0bec5',bg:'linear-gradient(135deg,#b0bec5,#607d8b)'}
];
const TYPE_CHART = {
'Solar':['Lunar'],
'Lunar':['Void'],
'Void':['Solar'],
'Aether':['Solar','Void'],
'Echo':['Aether','Lunar'],
'Prism':[], // beats all slightly
'Obsidian':[] // resists all
};
const ABILITIES = [
{name:'Solar Lance',type:'Solar',power:25,acc:95,pp:15,effect:'burn',desc:'A piercing beam that scorches the target.'},
{name:'Moon Veil',type:'Lunar',power:20,acc:100,pp:20,effect:'frost',desc:'Shrouds the enemy in cold mist.'},
{name:'Void Pulse',type:'Void',power:30,acc:90,pp:15,effect:'shock',desc:'Disrupts with chaotic energy.'},
{name:'Aether Surge',type:'Aether',power:35,acc:85,pp:20,effect:'speed',desc:'Accelerates the user with wind.'},
{name:'Echo Strike',type:'Echo',power:28,acc:90,pp:15,effect:'multi',desc:'Strikes twice with sonic force.'},
{name:'Prism Shard',type:'Prism',power:40,acc:80,pp:25,effect:'crit',desc:'Shatters light into piercing fragments.'},
{name:'Obsidian Shell',type:'Obsidian',power:15,acc:100,pp:25,effect:'barrier',desc:'Creates a defensive crystal barrier.'},
{name:'Flame Burst',type:'Solar',power:22,acc:95,pp:12,effect:'burn',desc:'A sudden explosion of solar fire.'},
{name:'Frost Nova',type:'Lunar',power:35,acc:85,pp:20,effect:'frost',desc:'Freezes the area around the target.'},
{name:'Shadow Claw',type:'Void',power:32,acc:90,pp:18,effect:'burn',desc:'Tears with dark, burning talons.'},
{name:'Wind Slash',type:'Aether',power:18,acc:100,pp:15,effect:'speed',desc:'Cuts swiftly, increasing velocity.'},
{name:'Sonic Boom',type:'Echo',power:25,acc:95,pp:15,effect:'multi',desc:'Emits a shockwave that hits broadly.'},
{name:'Crystal Rain',type:'Prism',power:30,acc:85,pp:20,effect:'heal',desc:'Heals the user with refracted light.'},
{name:'Rock Bash',type:'Obsidian',power:38,acc:80,pp:25,effect:'barrier',desc:'Crushes with heavy stone, forming shields.'},
{name:'Solar Flare',type:'Solar',power:45,acc:75,pp:30,effect:'burn',desc:'A blinding flash of solar energy.'},
{name:'Lunar Eclipse',type:'Lunar',power:40,acc:80,pp:25,effect:'frost',desc:'Obscures vision and freezes blood.'},
{name:'Void Rift',type:'Void',power:42,acc:75,pp:25,effect:'shock',desc:'Opens a rift that stuns the enemy.'},
{name:'Aether Storm',type:'Aether',power:38,acc:85,pp:22,effect:'speed',desc:'Calls a storm that hastens allies.'},
{name:'Echo Chamber',type:'Echo',power:28,acc:95,pp:15,effect:'multi',desc:'Bounces sound to strike multiple times.'},
{name:'Prism Spiral',type:'Prism',power:35,acc:80,pp:22,effect:'crit',desc:'Spins light into a piercing vortex.'},
{name:'Obsidian Wall',type:'Obsidian',power:20,acc:100,pp:20,effect:'barrier',desc:'Erects an impenetrable crystal wall.'},
{name:'Ignition',type:'Solar',power:30,acc:90,pp:18,effect:'burn',desc:'Sets the ground ablaze.'},
{name:'Glacial Spike',type:'Lunar',power:40,acc:80,pp:22,effect:'frost',desc:'Launches a massive ice spike.'},
{name:'Abyss Grip',type:'Void',power:35,acc:85,pp:20,effect:'shock',desc:'Grasps with void energy, stuns target.'},
{name:'Sky Shatter',type:'Aether',power:32,acc:90,pp:18,effect:'speed',desc:'Breaks air resistance for swift strikes.'},
{name:'Vibration',type:'Echo',power:22,acc:95,pp:14,effect:'multi',desc:'Rapid vibrations deal repeated damage.'},
{name:'Light Cascade',type:'Prism',power:25,acc:95,pp:15,effect:'heal',desc:'Showers with healing light.'},
{name:'Stone Fist',type:'Obsidian',power:42,acc:75,pp:28,effect:'barrier',desc:'A massive stone blow, fortifying defense.'},
{name:'Star Burn',type:'Solar',power:50,acc:70,pp:35,effect:'burn',desc:'Channels pure starlight to incinerate.'},
{name:'Moonlight Dance',type:'Lunar',power:28,acc:95,pp:16,effect:'frost',desc:'A graceful dance that chills foes.'},
{name:'Void Bite',type:'Void',power:45,acc:80,pp:25,effect:'shock',desc:'Bites with dark energy, stunning prey.'},
{name:'Cyclone',type:'Aether',power:42,acc:80,pp:22,effect:'speed',desc:'Creates a cyclone to enhance speed.'},
{name:'Resonance',type:'Echo',power:30,acc:90,pp:18,effect:'multi',desc:'Amplifies sound for additional hits.'},
{name:'Spectral Blade',type:'Prism',power:28,acc:95,pp:18,effect:'crit',desc:'Slices with rainbow-edged light.'},
{name:'Earth Tremor',type:'Obsidian',power:45,acc:75,pp:28,effect:'barrier',desc:'Shakes the ground, shielding user.'},
{name:'Solar Guard',type:'Solar',power:15,acc:100,pp:15,effect:'barrier',desc:'Forms a solar shield around user.'},
{name:'Frost Shield',type:'Lunar',power:15,acc:100,pp:15,effect:'barrier',desc:'Creates a frozen barrier.'},
{name:'Shadow Step',type:'Void',power:30,acc:85,pp:18,effect:'speed',desc:'Moves so quickly it confuses the enemy.'},
{name:'Wind Guard',type:'Aether',power:15,acc:100,pp:15,effect:'barrier',desc:'Wraps in protective winds.'},
{name:'Echo Shield',type:'Echo',power:20,acc:95,pp:12,effect:'barrier',desc:'Reflects damage with sound waves.'},
{name:'Crystal Armor',type:'Prism',power:15,acc:100,pp:15,effect:'barrier',desc:'Forms crystal plating on user.'},
{name:'Stone Armor',type:'Obsidian',power:15,acc:100,pp:15,effect:'barrier',desc:'Hardens skin like stone.'}
];
const CREATURES = [
{id:'sprigkin',name:'Sprigkin',type:'Solar',baseHP:60,baseATK:14,baseDEF:8,baseSPD:12,level:1,abilities:[0,7,1,23],lore:'A tiny seed-spirit that grows stronger with sunlight. It binds to caretakers with gentle warmth.',evolve:[20,24],item:'Sunseed'},
{id:'mosshell',name:'Mosshell',type:'Void',baseHP:75,baseATK:10,baseDEF:16,baseSPD:6,level:1,abilities:[9,6,37,39],lore:'A creeping shell covered in bioluminescent moss. It absorbs negative energy and transforms it into growth.',evolve:[28,29],item:'Shadow Catalyst'},
{id:'lumenfly',name:'Lumenfly',type:'Aether',baseHP:50,baseATK:12,baseDEF:6,baseSPD:18,level:1,abilities:[3,10,17,34],lore:'A delicate moth made of pure aether-wind. Its wings shimmer with the colors of unseen skies.',evolve:[25,26],item:'Aether Dust'},
{id:'frostbite',name:'Frostbite',type:'Void',baseHP:65,baseATK:16,baseDEF:10,baseSPD:8,level:1,abilities:[2,8,36,24],lore:'A predator of frozen caves that breathes cold enough to freeze air into glass.',evolve:[30,31],item:'Frost Orb'},
{id:'shiverscale',name:'Shiverscale',type:'Lunar',baseHP:70,baseATK:12,baseDEF:14,baseSPD:9,level:1,abilities:[1,26,38,40],lore:'Covered in shimmering lunar scales, it channels moonlight into freezing strikes.',evolve:[33,32],item:'Moon Pearl'},
{id:'glacialis',name:'Glacialis',type:'Echo',baseHP:60,baseATK:14,baseDEF:9,baseSPD:13,level:1,abilities:[4,12,27,32],lore:'A phantom of frozen echoes, it mimics sounds to disorient prey before striking.',evolve:[34,27],item:'Echo Orb'},
{id:'shadowmoth',name:'Shadowmoth',type:'Lunar',baseHP:55,baseATK:15,baseDEF:7,baseSPD:15,level:1,abilities:[1,9,11,43],lore:'Its wings are made of pure darkness. Where it flies, light dims and temperature drops.',evolve:[26,27],item:'Shadow Crystal'},
{id:'voidling',name:'Voidling',type:'Void',baseHP:80,baseATK:11,baseDEF:18,baseSPD:5,level:1,abilities:[2,10,15,41],lore:'Born from the deepest abyss, it moves slowly but hits with crushing void-force.',evolve:[30,28],item:'Void Shard'},
{id:'obscura',name:'Obscura',type:'Obsidian',baseHP:100,baseATK:8,baseDEF:22,baseSPD:4,level:1,abilities:[6,16,42,35],lore:'A walking crystal monolith. It protects the weak by absorbing harm into its black body.',evolve:[40,36],item:'Dark Catalyst'},
{id:'aetherwing',name:'Aetherwing',type:'Aether',baseHP:55,baseATK:13,baseDEF:8,baseSPD:17,level:1,abilities:[3,17,33,20],lore:'A bird of luminous winds, it soars between realms leaving trails of stardust.',evolve:[35,37],item:'Star Shard'},
{id:'echohawk',name:'Echohawk',type:'Echo',baseHP:65,baseATK:17,baseDEF:10,baseSPD:12,level:1,abilities:[4,11,21,28],lore:'A raptor that hunts with sound. Its cry can shatter glass and stun prey instantly.',evolve:[38,39],item:'Sonic Feather'},
{id:'solarite',name:'Solarite',type:'Solar',baseHP:60,baseATK:20,baseDEF:8,baseSPD:10,level:1,abilities:[0,14,22,44],lore:'A being of pure solar core. Its presence warms the air and fuels growth in plants.',evolve:[45,46],item:'Solar Core'},
{id:'prismira',name:'Prismira',type:'Prism',baseHP:55,baseATK:15,baseDEF:6,baseSPD:14,level:1,abilities:[5,12,19,30],lore:'A creature formed from refracted reality. It exists in multiple colors simultaneously.',evolve:[40,41],item:'Rainbow Prism'},
{id:'luminox',name:'Luminox',type:'Prism',baseHP:70,baseATK:18,baseDEF:10,baseSPD:11,level:1,abilities:[5,13,18,43],lore:'A serpentine being of light. It uses its prism-body to bend energy and heal allies.',evolve:[42,43],item:'Light Gem'},
{id:'vortex',name:'Vortex',type:'Prism',baseHP:50,baseATK:22,baseDEF:5,baseSPD:20,level:1,abilities:[5,17,12,31],lore:'Spinning faster than lightning, this creature distorts reality with each rotation.',evolve:[48,49],item:'Spiral Core'},
{id:'cragbeast',name:'Cragbeast',type:'Obsidian',baseHP:110,baseATK:18,baseDEF:26,baseSPD:3,level:1,abilities:[6,15,35,46],lore:'A colossal rock-golem that walks through mountains. It defends territories from invaders.',evolve:[50,36],item:'Rock Catalyst'},
{id:'shardling',name:'Shardling',type:'Obsidian',baseHP:90,baseATK:14,baseDEF:24,baseSPD:6,level:1,abilities:[6,39,42,47],lore:'Covered in razor-sharp crystal plates, it moves slowly but strikes with devastating force.',evolve:[42,36],item:'Crystal Shard'},
{id:'ebonwhisper',name:'Ebonwhisper',type:'Lunar',baseHP:65,baseATK:16,baseDEF:11,baseSPD:14,level:1,abilities:[1,9,11,37],lore:'A shadowy whisperer that communicates in dreams. It binds to strong-willed partners.',evolve:[34,36],item:'Dark Seed'}
];
const ZONES = [
{id:1,name:'Bloomreach',color:'#155c2e',bg:'linear-gradient(135deg,#0f3a1f,#1a4a2a)',encounters:[0,1,2],boss:{creature:3,type:'Aether',name:'Guardian of Light'},particles:'green',story:'In the Bloomreach, life begins anew. The air smells of nectar and sunlight. The first Anchor stirs here.'},
{id:2,name:'Frostveil',color:'#0a3a5c',bg:'linear-gradient(135deg,#042640,#0a3a5c)',encounters:[3,4,5],boss:{creature:3,type:'Lunar',name:'Guardian of Frost'},particles:'blue',story:'The Frostveil is silent and cold. Even breath freezes here. The second Anchor sleeps beneath the ice.'},
{id:3,name:'Shadowmere',color:'#2d1a5c',bg:'linear-gradient(135deg,#1a0f3d,#2d1a5c)',encounters:[6,7,8],boss:{creature:8,type:'Obsidian',name:'Guardian of Darkness'},particles:'purple',story:'Shadowmere is a place of whispers. Light is devoured. The third Anchor waits in the deep.'},
{id:4,name:'Aether Sky',color:'#0a5c6e',bg:'linear-gradient(135deg,#042b38,#0a5c6e)',encounters:[9,10,11],boss:{creature:11,type:'Echo',name:'Guardian of Echoes'},particles:'cyan',story:'Above the clouds lies the Aether Sky, where winds carry memories. The fourth Anchor floats above.'},
{id:5,name:'Prism Ridge',color:'#5c2a5c',bg:'linear-gradient(135deg,#3a155c,#5c2a5c)',encounters:[12,13,14],boss:{creature:14,type:'Prism',name:'Guardian of Light'},particles:'rainbow',story:'At Prism Ridge, reality fractures into colors. The fifth Anchor shines with impossible light.'},
{id:6,name:'Obsid Vault',color:'#151515',bg:'linear-gradient(135deg,#0f0f12,#181820)',encounters:[15,16,17],boss:{creature:7,type:'Obsidian',name:'Guardian of Stone'},particles:'grey',story:'The final realm. Black crystal towers scrape the sky. The last Anchor must be broken here.'}
];
const ITEMS = [
{id:'capsule_a',name:'Basic Capsule',tier:1,desc:'Standard collection orb.',type:'capsule'},
{id:'capsule_b',name:'Shimmer Capsule',tier:2,desc:'Improved capture rate.',type:'capsule'},
{id:'capsule_c',name:'Aether Capsule',tier:3,desc:'Highest capture potential.',type:'capsule'},
{id:'heal_potion',name:'Vital Elixir',tier:1,desc:'Restores 40 HP to one creature.',use:'heal',value:40,type:'heal'},
{id:'heal_major',name:'Greater Elixir',tier:2,desc:'Restores 100 HP.',value:100,type:'heal'},
{id:'sunseed',name:'Sunseed',tier:1,desc:'Catalyst for Solar evolution.',use:'evolve',type:'catalyst'},
{id:'moon_pearl',name:'Moon Pearl',tier:2,desc:'Catalyst for Lunar growth.',use:'evolve',type:'catalyst'},
{id:'star_shard',name:'Star Shard',tier:2,desc:'Catalyst for Aether transformation.',use:'evolve',type:'catalyst'},
{id:'shadow_crystal',name:'Shadow Crystal',tier:2,desc:'Catalyst for Void evolution.',use:'evolve',type:'catalyst'},
{id:'frost_orb',name:'Frost Orb',tier:2,desc:'Catalyst for Void frost forms.',use:'evolve',type:'catalyst'}
];
const STARTERS = [
{id:'sprigkin',name:'Sprigkin',type:'Solar'},
{id:'mosshell',name:'Mosshell',type:'Void'},
{id:'lumenfly',name:'Lumenfly',type:'Aether'}
];
/* ===================== STATE ===================== */
let state = {
zone:1,
zoneIndex:0,
player:{x:10,y:5,zone:1},
party:[],
bag:[
{item:'capsule_a',qty:3},
{item:'capsule_b',qty:1},
{item:'heal_potion',qty:2},
{item:'sunseed',qty:1},
{item:'moon_pearl',qty:0},
{item:'star_shard',qty:0}
],
inventory:[],
captured:new Set(),
anchorsDefeated:0,
storyStage:0,
battlesWon:0,
tutorialStep:0,
gold:100,
lastSave:null,
activeBattle:null,
screen:'title',
menuScreen:'main',
playingMusic:true,
particles:[]
};
function initState(){
try{
const s = localStorage.getItem('aetheris_save');
if(s){
const d = JSON.parse(s);
if(d && d.party){
state = {...state, ...d};
state.screen = 'title';
state.activeBattle = null;
}
}
}catch(e){}
}
function saveGame(manual=true){
const data = {...state};
data.screen = manual ? 'title' : state.screen;
data.activeBattle = null;
try{localStorage.setItem('aetheris_save',JSON.stringify(data));}catch(e){}
if(manual) showNotification('Progress Archived');
}
function loadSlot(slot){
try{
const k = 'aetheris_save_'+slot;
const s = localStorage.getItem(k);
if(s){
const d = JSON.parse(s);
if(d && d.party){
state = {...state, ...d};
state.screen = 'overworld';
state.activeBattle = null;
startOverworld();
} else {showNotification('Slot Empty');}
} else {showNotification('Slot Empty');}
}catch(e){showNotification('Load Failed');}
}
/* ===================== AUDIO ===================== */
let audioCtx, masterGain, bgOsc, bgOsc2;
function initAudio(){
if(!audioCtx){
try{
audioCtx = new (window.AudioContext||window.webkitAudioContext)();
masterGain = audioCtx.createGain();
masterGain.gain.value = 0.15;
masterGain.connect(audioCtx.destination);
}catch(e){console.warn('Audio init failed');}
}
}
function playTone(freq,dur,vol=0.1,type='sine'){
if(!audioCtx || !state.playingMusic) return;
const osc = audioCtx.createOscillator();
const g = audioCtx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq,audioCtx.currentTime);
g.gain.setValueAtTime(vol,audioCtx.currentTime);
g.gain.exponentialRampToValueAtTime(0.001,audioCtx.currentTime+dur);
osc.connect(g); g.connect(masterGain); osc.start(); osc.stop(audioCtx.currentTime+dur);
}
function startAmbient(){
if(!audioCtx) return;
stopAmbient();
bgOsc = audioCtx.createOscillator();
bgOsc2 = audioCtx.createOscillator();
bgOsc.type = 'sine'; bgOsc2.type = 'triangle';
bgOsc.frequency.setValueAtTime(110,audioCtx.currentTime);
bgOsc2.frequency.setValueAtTime(82.4,audioCtx.currentTime);
const filter = audioCtx.createBiquadFilter();
filter.type = 'lowpass'; filter.frequency.value = 600;
const g = audioCtx.createGain(); g.gain.value = 0.04;
bgOsc.connect(filter); bgOsc2.connect(filter); filter.connect(g); g.connect(masterGain);
bgOsc.start(); bgOsc2.start();
setInterval(()=>{
if(bgOsc && bgOsc2){
bgOsc.frequency.setValueAtTime(110 + Math.random()*20, audioCtx.currentTime);
bgOsc2.frequency.setValueAtTime(82 + Math.random()*10, audioCtx.currentTime);
}
},3000);
}
function stopAmbient(){
if(bgOsc){try{bgOsc.stop();bgOsc.disconnect();}catch(e){}}
if(bgOsc2){try{bgOsc2.stop();bgOsc2.disconnect();}catch(e){}}
bgOsc=null; bgOsc2=null;
}
/* ===================== RENDERING ===================== */
const overworldCanvas = document.getElementById('overworldCanvas');
const owCtx = overworldCanvas.getContext('2d');
const battleCanvas = document.getElementById('battleCanvas');
const bCtx = battleCanvas.getContext('2d');
function resize(){
overworldCanvas.width = window.innerWidth; overworldCanvas.height = window.innerHeight;
battleCanvas.width = window.innerWidth; battleCanvas.height = window.innerHeight;
}
window.addEventListener('resize',resize); resize();
function drawOverworld(){
const w = overworldCanvas.width; const h = overworldCanvas.height;
const zone = ZONES[state.zone-1];
owCtx.fillStyle = zone.bg; owCtx.fillRect(0,0,w,h);
// Particles
state.particles = state.particles.filter(p => p.y < h+20 && p.life > 0);
if(Math.random()<.3) state.particles.push({x:Math.random()*w, y:-10, speed:1+Math.random()*2, size:2+Math.random()*4, life:1, color:zone.color});
for(const p of state.particles){
p.y += p.speed; p.life -= 0.005;
owCtx.beginPath();
owCtx.arc(p.x,p.y,p.size,p.life*Math.PI*2,p.life*Math.PI*2+Math.PI);
owCtx.fillStyle = p.color + Math.round(p.life*255).toString(16).padStart(2,'0');
owCtx.fill();
}
// Tile grid
const tileSize = 60;
const cols = Math.ceil(w/tileSize)+2;
const rows = Math.ceil(h/tileSize)+2;
owCtx.strokeStyle = 'rgba(255,255,255,.05)';
owCtx.lineWidth = 1;
for(let x=0;x<cols;x++) for(let y=0;y<rows;y++){
owCtx.beginPath(); owCtx.rect(x*tileSize, y*tileSize, tileSize, tileSize); owCtx.stroke();
}
// Player
const px = state.player.x * tileSize + tileSize/2;
const py = state.player.y * tileSize + tileSize/2;
owCtx.beginPath();
owCtx.arc(px,py,14,0,Math.PI*2);
owCtx.fillStyle = '#c7a4ff'; owCtx.fill();
owCtx.beginPath();
owCtx.arc(px,py,6,0,Math.PI*2);
owCtx.fillStyle = '#fff'; owCtx.fill();
// Enemies on overworld
const zoneData = ZONES[state.zone-1];
for(const enc of zoneData.encounters){
const cx = ((enc+5)*tileSize + 30) % (Math.ceil(w/tileSize)*tileSize);
const cy = ((enc+3)*tileSize + 30) % (Math.ceil(h/tileSize)*tileSize);
owCtx.beginPath();
owCtx.arc(cx,cy,10,0,Math.PI*2);
owCtx.fillStyle = TYPES[enc%7].color; owCtx.fill();
owCtx.strokeStyle = 'rgba(255,255,255,.3)'; owCtx.lineWidth=2; owCtx.stroke();
}
}
let owAnim = null;
function startOverworld(){
state.screen = 'overworld';
showScreen('overworld');
document.getElementById('zoneLabel').textContent = ZONES[state.zone-1].name;
startAmbient();
owAnim = requestAnimationFrame(overworldLoop);
}
function overworldLoop(){
if(state.screen !== 'overworld') return;
drawOverworld();
owAnim = requestAnimationFrame(overworldLoop);
}
function movePlayer(dx,dy){
const newX = state.player.x + dx;
const newY = state.player.y + dy;
if(newX>=0 && newX<30 && newY>=0 && newY<20){
state.player.x = newX; state.player.y = newY;
// Random encounter
if(Math.random()<.01){
const zoneData = ZONES[state.zone-1];
const enc = zoneData.encounters[Math.floor(Math.random()*zoneData.encounters.length)];
startBattle(enc);
}
}
}
document.addEventListener('keydown',(e)=>{
if(state.screen==='overworld'){
if(e.key==='ArrowUp'||e.key==='w') movePlayer(0,-1);
if(e.key==='ArrowDown'||e.key==='s') movePlayer(0,1);
if(e.key==='ArrowLeft'||e.key==='a') movePlayer(-1,0);
if(e.key==='ArrowRight'||e.key==='d') movePlayer(1,0);
if(e.key==='Escape') openMenu('main');
}
});
/* Touch controls for mobile */
const touchBtnContainer = document.createElement('div');
touchBtnContainer.style.cssText = 'position:absolute;bottom:100px;left:50%;transform:translateX(-50%);display:grid;grid-template-columns:repeat(3,60px);grid-template-rows:repeat(3,60px);gap:4px;z-index:15;';
touchBtnContainer.innerHTML = `
<button onclick="movePlayer(0,-1)" style="grid-column:2">▲</button>
<button onclick="movePlayer(-1,0)" style="grid-column:1;grid-row:2">◀</button>
<button onclick="movePlayer(1,0)" style="grid-column:3;grid-row:2">▶</button>
<button onclick="movePlayer(0,1)" style="grid-column:2;grid-row:3">▼</button>
`;
document.getElementById('overworld').appendChild(touchBtnContainer);
/* ===================== BATTLE ===================== */
function startBattle(encounterIndex){
state.screen = 'battle';
showScreen('battle');
state.activeBattle = {enemy:creatureCopy(CREATURES[encounterIndex]),turn:'player',turnOrder:0,turnCount:0};
initBattleUI();
requestAnimationFrame(battleLoop);
}
function creatureCopy(creature){
const c = {...creature};
c.hp = Math.floor(c.baseHP + c.level*10);
c.atk = Math.floor(c.baseATK + c.level*2);
c.def = Math.floor(c.baseDEF + c.level*2);
c.spd = Math.floor(c.baseSPD + c.level*1.5);
return c;
}
function initBattleUI(){
const ui = document.getElementById('battleUI');
document.getElementById('enemyRow').innerHTML = `<div class="party-card" style="border-color:${TYPES[state.activeBattle.enemy.type].color}">
<div class="party-avatar" style="background:${TYPES[state.activeBattle.enemy.type].bg}">${state.activeBattle.enemy.name.slice(0,2)}</div>
<div><div style="font-family:'Orbitron',sans-serif;font-size:.8rem;color:${TYPES[state.activeBattle.enemy.type].color}">${state.activeBattle.enemy.name}</div>
<div class="health-bar"><div class="health-inner" id="enemyHP" style="width:100%"></div></div></div>
</div>`;
document.getElementById('playerRow').innerHTML = state.party.map((p,pidx)=>`
<button class="party-card" onclick="switchCreature(${pidx})" style="border-color:${TYPES[p.type].color}">
<div class="party-avatar" style="background:${TYPES[p.type].bg}">${p.name.slice(0,2)}</div>
<div><div style="font-size:.7rem;font-family:'Orbitron';color:${TYPES[p.type].color}">${p.name}</div>
<div class="health-bar"><div class="health-inner" style="width:${Math.round((p.hp/p.maxHP)*100)}%"></div></div></div>
</button>
`).join('');
document.getElementById('abilityGrid').innerHTML = state.party[0].abilities.map((abIdx,i)=>`
<button class="ability-btn" onclick="useAbility(${i},'player')" id="abBtn${i}">
<div class="ability-name">${ABILITIES[abIdx].name}</div>
<div class="ability-meta">${ABILITIES[abIdx].type} • Pwr ${ABILITIES[abIdx].power} • PP ${ABILITIES[abIdx].pp}</div>
</button>
`).join('');
}
function switchCreature(idx){
if(state.activeBattle.turn !== 'player') return;
// Swap active creature (for simplicity we just change active, but in this version only 1 active shown)
// Actually we'll just make the party rotate visually
state.party.push(state.party.splice(idx,1)[0]); // rotate
initBattleUI();
}
function useAbility(abIdx,source){
if(state.activeBattle.turn !== 'player') return;
const ab = ABILITIES[state.party[0].abilities[abIdx]];
const attacker = state.party[0];
const defender = state.activeBattle.enemy;
const result = calculateDamage(attacker, defender, ab);
// Visual feedback
const container = document.getElementById('floatingContainer');
const text = document.createElement('div');
text.className = 'floating-text';
text.textContent = result.damage > 0 ? (result.crit ? 'CRITICAL!' : result.damage) : 'MISS';
text.style.left = '50%'; text.style.top = '30%';
text.style.color = result.crit ? '#ffe082' : (result.super ? TYPES[ab.type].color : '#fff');
container.appendChild(text);
setTimeout(()=>text.remove(),800);
playTone(300,0.2,'triangle');
if(result.crit) playTone(800,0.3,'sawtooth');
// Apply damage and effects
defender.hp -= result.damage;
if(result.super) playTone(600,0.2);
// Status effects (simple)
if(ab.effect === 'burn') defender.burned = 3;
else if(ab.effect === 'frost') defender.frozen = 3;
else if(ab.effect === 'barrier') attacker.barrier = 1;
else if(ab.effect === 'heal') attacker.hp = Math.min(attacker.maxHP, attacker.hp + ab.power);
document.getElementById('enemyHP').style.width = Math.max(0, (defender.hp/defender.maxHP)*100)+'%';
if(defender.hp <= 0){
endBattle(true);
} else {
state.activeBattle.turn = 'enemy';
setTimeout(enemyTurn, 1000);
}
initBattleUI();
}
function calculateDamage(attacker, defender, ab){
const level = attacker.level;
const atk = attacker.atk;
const def = defender.def;
const mult = Math.random()*0.3 + 0.85;
let dmg = Math.floor((0.4*level*atk/def*mult) + Math.random()*5);
let stab = 1;
if(attacker.type === ab.type) stab = 1.25;
let typeMult = 1;
if(TYPE_CHART[ab.type] && TYPE_CHART[ab.type].includes(defender.type)) typeMult = 1.5;
else if(TYPE_CHART[defender.type] && TYPE_CHART[defender.type].includes(ab.type)) typeMult = 0.7;
if(ab.type === 'Prism') typeMult = 1.2; // beats all slightly
if(defender.type === 'Obsidian' && ab.type !== 'Prism') typeMult = 0.6;
if(ab.type === 'Obsidian' && defender.type === 'Prism') typeMult = 1.4; // weak to prism
dmg = Math.floor(dmg * stab * typeMult);
const crit = Math.random() < 0.1 ? true : false;
if(crit) dmg = Math.floor(dmg * 1.5);
const super = typeMult > 1;
return {damage: Math.max(1, dmg), crit:crit, super:super};
}
function enemyTurn(){
const enemy = state.activeBattle.enemy;
const player = state.party[0];
const ab = ABILITIES[enemy.abilities[Math.floor(Math.random()*enemy.abilities.length)]];
const result = calculateDamage(enemy, player, ab);
const container = document.getElementById('floatingContainer');
const text = document.createElement('div');
text.className = 'floating-text';
text.textContent = result.damage > 0 ? (result.crit ? 'CRITICAL!' : result.damage) : 'MISS';
text.style.left = '50%'; text.style.top = '60%';
text.style.color = result.crit ? '#ff6b6b' : '#fff';
container.appendChild(text);
setTimeout(()=>text.remove(),800);
playTone(150,0.2,'triangle');
player.hp -= result.damage;
document.getElementById('playerRow').innerHTML = state.party.map((p,pidx)=>`
<div class="party-card" style="border-color:${TYPES[p.type].color}">
<div class="party-avatar" style="background:${TYPES[p.type].bg}">${p.name.slice(0,2)}</div>
<div><div style="font-size:.7rem;font-family:'Orbitron';color:${TYPES[p.type].color}">${p.name}</div><div class="health-bar"><div class="health-inner" style="width:${Math.round((p.hp/p.maxHP)*100)}%"></div></div></div>
</div>
`).join('');
if(player.hp <= 0){
player.hp = 1; // prevent death for now
showNotification('Critical! Your creature barely survived.');
}
state.activeBattle.turn = 'player';
document.getElementById('turnIndicator').textContent = 'PLAYER TURN';
initBattleUI();
setTimeout(()=>{
if(state.screen === 'battle') initBattleUI();
}, 800);
}
function endBattle(win){
state.screen = 'dialog';
showScreen('dialog');
state.activeBattle = null;
document.getElementById('dialogTitle').textContent = win ? 'Victory!' : 'Defeat';
document.getElementById('dialogBody').textContent = win ? 'The creature has fallen. You earned experience and capture the fragment.' : 'You were defeated. Rest at a shrine to recover.';
if(win){
state.battlesWon++;
// EXP
const exp = 15 + state.activeBattle ? 20 : 10;
state.party.forEach(c => { c.hp = Math.floor(c.hp + 10); });
// Capture chance
const hpPercent = (state.activeBattle ? state.activeBattle.enemy.hp : 0) / (state.activeBattle ? state.activeBattle.enemy.maxHP : 1);
const chance = Math.max(0.1, 0.7 - hpPercent);
if(Math.random() < chance){
state.captured.add(state.activeBattle.enemy.id);
document.getElementById('dialogBody').innerHTML += '<br><br><span style="color:var(--prism)">CAPTURED: ' + state.activeBattle.enemy.name + '</span>';
playTone(500,0.5,'sine');
}
// Check zone anchor defeat for story progression
if(state.battlesWon % 5 === 0 && state.storyStage < 4){
state.storyStage++;
showStoryCutscene();
}
}
}
function battleLoop(){
if(state.screen !== 'battle'){
// Draw battle canvas
bCtx.fillStyle = '#0b0c15'; bCtx.fillRect(0,0,battleCanvas.width,battleCanvas.height);
// Simple visual
bCtx.fillStyle = 'rgba(255,255,255,.03)';
for(let i=0;i<20;i++) bCtx.fillRect(Math.random()*battleCanvas.width, Math.random()*battleCanvas.height, 60, 2);
if(state.screen === 'battle') requestAnimationFrame(battleLoop);
}
}
/* ===================== STORY & DIALOG ===================== */
function showStoryCutscene(){
const stage = state.storyStage;
const zoneData = ZONES[state.zone-1];
const box = document.createElement('div');
box.className = 'cutscene-box';
box.innerHTML = `
<div class="cutscene-vignette"></div>
<h2>STAGE ${stage}: ${zoneData.name}</h2>
<p>${zoneData.story}</p>
`;
box.onclick = () => {box.remove(); state.screen='dialog'; showScreen('dialog'); document.getElementById('dialogBody').textContent='Continue your journey.';};
document.getElementById('game').appendChild(box);
playTone(400,1.5,'triangle');
}
function openMenu(type){
state.screen = 'menu';
state.menuScreen = type;
showScreen('menu');
document.getElementById('menuTitle').textContent = type.toUpperCase();
document.getElementById('menuContent').innerHTML = renderMenu(type);
}
function closeMenu(){
showScreen('overworld');
state.screen = 'overworld';
if(owAnim) cancelAnimationFrame(owAnim);
startOverworld();
}
function renderMenu(type){
if(type === 'team') return state.party.map((c,idx)=>`
<div style="display:flex;gap:12px;align-items:center;padding:8px 0;border-bottom:1px solid rgba(255,255,255,.1);">
<div style="width:40px;height:40px;border-radius:50%;background:${TYPES[c.type].bg};display:flex;align-items:center;justify-content:center;font-weight:bold;font-size:.8rem;">${c.name.slice(0,2)}</div>
<div>
<div style="font-family:'Orbitron';color:${TYPES[c.type].color}">${c.name} <span style="font-size:.7rem;color:var(--dim);">Lv ${c.level}</span></div>
<div style="font-size:.75rem;color:var(--dim);">HP: ${Math.round(c.hp)}/${c.maxHP || c.baseHP} • ATK: ${c.atk}</div>
</div>
</div>
`).join('');
else if(type === 'bag') return state.bag.map(i=>`
<div style="padding:8px 0;border-bottom:1px solid rgba(255,255,255,.08);display:flex;justify-content:space-between;">
<div><strong style="font-family:'Orbitron';">${i.item ? ITEMS.find(x=>x.id===i.item)?.name || i.item : i}</strong> <span style="color:var(--dim);font-size:.75rem;">×${i.qty}</span></div>
</div>
`).join('');
else return '<p>Menu content here.</p>';
}
/* ===================== ENCYCLOPEDIA ===================== */
function openEncyclopedia(){
openMenu('encyclopedia');
document.getElementById('menuContent').innerHTML = `<div id="encContent"></div>`;
document.getElementById('encContent').innerHTML = CREATURES.map(c=>`
<div class="enc-entry">
<div class="enc-avatar" style="background:${TYPES[c.type].bg}">${c.name.slice(0,1)}</div>
<div class="enc-info">
<h3>${c.name}</h3>
<span>${TYPES[c.type].name} • HP:${c.baseHP} ATK:${c.baseATK} DEF:${c.baseDEF} SPD:${c.baseSPD}</span>
<p style="font-size:.85rem;color:var(--dim);margin-top:4px;">${c.lore}</p>
</div>
</div>
`).join('');
document.getElementById('menuTitle').textContent = 'ENCYCLOPEDIA';
}
/* ===================== CAPTURE MINI-GAME ===================== */
function captureMiniGame(creature){
const modal = document.createElement('div');
modal.className = 'glass-modal';
modal.style.cssText = 'position:absolute;inset:40px;z-index:50;background:rgba(11,12,21,.95);display:flex;flex-direction:column;align-items:center;';
modal.innerHTML = `
<h2 style="font-family:'Orbitron';color:var(--prism);">CAPTURE ATTEMPT</h2>
<p style="margin-bottom:16px;color:var(--dim);">Press the zone as the marker passes through.</p>
<div class="collection-mini-game">
<div class="tap-zone" id="tapZone" onclick="handleCaptureTap()">
<div class="marker"></div>
<div class="hit-area"></div>
</div>
<div id="captureResult" style="font-family:'Orbitron';font-size:1.2rem;color:var(--text);"></div>
</div>
<button onclick="this.parentElement.remove()" style="margin-top:20px;">CONTINUE</button>
`;
document.getElementById('game').appendChild(modal);
window.captureActive = {start:Date.now(), hits:0, zoneWidth:.3};
}
function handleCaptureTap(){
if(!window.captureActive) return;
window.captureActive.hits++;
const hitArea = .3;
const rand = Math.random();
const success = rand > .5;
const el = document.getElementById('captureResult');
if(success){
el.textContent = 'SUCCESS!';
el.style.color = '#69f0ae';
playTone(800,0.3,'sine');
} else {
el.textContent = 'MISS!';
el.style.color = '#ff6b6b';
playTone(150,0.2,'triangle');
}
window.captureActive = null;
setTimeout(()=>document.getElementById('game').lastChild.remove(), 1200);
}
/* ===================== MAP ===================== */
function renderMap(){
const content = document.getElementById('mapContent');
content.innerHTML = ZONES.map((z,i)=>`
<button onclick="travelZone(${z.id})" style="padding:20px;border-radius:16px;background:${z.bg};border:1px solid rgba(255,255,255,.1);color:#fff;cursor:pointer;">
<div style="font-family:'Orbitron';font-weight:bold;">${z.name}</div>
<div style="font-size:.7rem;color:rgba(255,255,255,.7);">${i+1}/6 Zones</div>
</button>
`).join('');
}
function travelZone(z){
if(z <= state.zone + 1 || z === state.zone){
state.zone = z;
startOverworld();
closeMenu();
} else {
showNotification('Zone not yet unlocked');
}
}
/* ===================== SHRINE & SAVE ===================== */
function openMenu(type){
state.screen = 'menu';
state.menuScreen = type;
showScreen('menu');
document.getElementById('menuTitle').textContent = type.toUpperCase();
document.getElementById('menuContent').innerHTML = renderMenu(type);
if(type==='map') renderMap();
if(type==='encyclopedia') openEncyclopedia();
if(type==='save'){
document.getElementById('menuContent').innerHTML = `
<div style="display:flex;gap:12px;flex-wrap:wrap;">
<button class="btn" onclick="loadSlot(0)">SLOT 1</button>
<button class="btn" onclick="loadSlot(1)">SLOT 2</button>
<button class="btn" onclick="loadSlot(2)">SLOT 3</button>
</div>
<div style="margin-top:16px;"><button class="btn btn-primary" onclick="saveGame(false)">SAVE</button></div>
`;
}
}
function healParty(){
state.party.forEach(c => { c.hp = c.maxHP || c.baseHP + c.level*10; });
showNotification('Party fully restored');
startOverworld();
closeMenu();
}
/* ===================== UTILS ===================== */
function showNotification(msg){
const el = document.createElement('div');
el.style.cssText = 'position:absolute;top:20px;left:50%;transform:translateX(-50%);padding:12px 24px;background:rgba(11,12,21,.9);backdrop-filter:blur(8px);border:1px solid rgba(255,255,255,.2);border-radius:20px;color:var(--text);font-family:"Orbitron";z-index:300;animation:fadeUp .3s ease;';
el.textContent = msg;
document.getElementById('game').appendChild(el);
setTimeout(()=>el.remove(), 2000);
}
function showScreen(id){
document.querySelectorAll('.screen').forEach(s=>s.classList.remove('show')); document.querySelectorAll('.screen').forEach(s=>s.classList.add('hide'));
document.getElementById(id).classList.remove('hide'); document.getElementById(id).classList.add('show');
}
function closeDialog(){
showScreen('overworld');
state.screen = 'overworld';
if(owAnim) cancelAnimationFrame(owAnim);
startOverworld();
}
function closeMenu(){
showScreen('overworld');
state.screen = 'overworld';
if(owAnim) cancelAnimationFrame(owAnim);
startOverworld();
}
function startTutorial(){
state.tutorialStep = 0;
document.getElementById('tutTitle').textContent = 'WELCOME, BINDER';
document.getElementById('tutBody').textContent = 'Use WASD or Arrow keys to move through the Veil. Approach glowing orbs to challenge wild creatures.';
showScreen('tutorial');
}
function nextTutorialStep(){
state.tutorialStep++;
if(state.tutorialStep >= 2){
document.getElementById('tutorial').classList.add('hide');
showScreen('overworld');
startOverworld();
if(state.party.length === 0){
state.party.push(creatureCopy(CREATURES[0])); // Give starter
showNotification('You received Sprigkin!');
}
} else {
document.getElementById('tutTitle').textContent = 'COMBAT & BINDING';
document.getElementById('tutBody').textContent = 'Select abilities to fight. After victory, try to capture the creature with your capsules.';
}
}
/* ===================== GAME INIT ===================== */
function startNewGame(){
state = {
zone:1, zoneIndex:0, player:{x:10,y:5,zone:1}, party:[],
bag:[{item:'capsule_a',qty:3},{item:'capsule_b',qty:1},{item:'heal_potion',qty:2},{item:'sunseed',qty:1}],
inventory:[], captured:new Set(), anchorsDefeated:0, storyStage:0, battlesWon:0, tutorialStep:0,
gold:100, lastSave:null, activeBattle:null, screen:'overworld', menuScreen:'main', playingMusic:true, particles:[]
};
state.party.push(creatureCopy(CREATURES[0]), creatureCopy(CREATURES[3]), creatureCopy(CREATURES[5]));
saveGame(true);
startOverworld();
startTutorial();
initAudio(); startAmbient();
}
function continueGame(){
initState();
initAudio(); startAmbient();
if(state.screen === 'title' && state.party && state.party.length > 0){
startOverworld();
} else if(state.party && state.party.length === 0){
startNewGame();
}
}
function returnToTitle(){
showScreen('titleScreen');
state.screen = 'title';
stopAmbient();
initAudio();
}
document.addEventListener('DOMContentLoaded', ()=>{
initState();
resize();
// Initialize title background animation
const tCanvas = document.getElementById('titleCanvas');
const tCtx = tCanvas.getContext('2d');
tCanvas.width = window.innerWidth; tCanvas.height = window.innerHeight;
function titleLoop(){
tCtx.fillStyle = '#0b0c15'; tCtx.fillRect(0,0,tCanvas.width,tCanvas.height);
for(let i=0;i<50;i++){
tCtx.beginPath();
tCtx.arc(Math.random()*tCanvas.width, Math.random()*tCanvas.height, 1+Math.random()*2, 0, Math.PI*2);
tCtx.fillStyle = `rgba(199,164,255,${Math.random()*0.5})`;
tCtx.fill();
}
requestAnimationFrame(titleLoop);
}
titleLoop();
if(state.party && state.party.length > 0){
document.querySelector('#titleScreen .btn').focus();
}
});
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingmachines/inkling","provider":"openrouter","persona":"basic","sessionId":"8d9ba1f9-2001-4f07-8321-9dfd4cbffffe","tokensIn":1288,"tokensOut":19335,"tokensTotal":20623,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-07-18T20:53:39.177Z"} -->