Creature Collection
inkling · full

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 61,746 bytes · SHA-256 3abad3c49f29
<!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>The Veil of Aetherium — A New Binding</title>
<style>
:root {
--c1: #0d0f14; --c2: #1a1b22; --c3: #23242e; --c4: #2b2c3a; --c5: #383a50;
--gold: #d4af37; --gold-dim: #8a7330; --gold-glow: rgba(212,175,55,0.35);
--violet: #7b5a9e; --rose: #c8556e; --mint: #6abf8a; --cyan: #5ab8d4; --amber: #e8a84a;
--text: #ececf8; --text-dim: #9a9ab0; --border: rgba(212,175,55,0.12);
--font-body: 'Segoe UI', system-ui, sans-serif;
}
* { margin:0; padding:0; box-sizing:border-box; -webkit-tap-highlight-color:transparent; touch-action:manipulation; }
html, body { width:100%; height:100%; overflow:hidden; background: var(--c1); color: var(--text); font-family: var(--font-body); user-select:none; }
/* Background ambient */
#bg-ambient { pointer-events:none; position:fixed; inset:0; z-index:0; overflow:hidden; }
.ambient-star { position:absolute; border-radius:50%; background: radial-gradient(circle at 30% 30%, rgba(212,175,55,0.7), rgba(212,175,55,0)); opacity:0.35; animation: twinkle linear infinite; }
@keyframes twinkle { 0%,100%{ opacity:0.15; transform:scale(0.7); } 50%{ opacity:0.45; transform:scale(1.1); } }
/* Title / Screens */
#screen-title, #screen-battle, #screen-overworld, #screen-menu, #screen-enc, #screen-shop, #screen-shrine, #screen-map { position:fixed; inset:0; z-index:10; display:flex; flex-direction:column; align-items:center; justify-content:center; transition: opacity 0.5s ease; }
#screen-overworld { z-index:20; }
.hidden { opacity:0 !important; pointer-events:none !important; }
#title-art { text-align:center; animation: floatUp 1.2s ease-out; }
@keyframes floatUp { from { opacity:0; transform:translateY(30px); } to { opacity:1; transform:translateY(0); } }
#title-art h1 { font-size: clamp(2.6rem, 8vw, 5rem); letter-spacing:0.35em; color: var(--gold); text-shadow: 0 0 40px var(--gold-glow), 0 0 80px rgba(212,175,55,0.15); line-height:1.05; font-weight:300; }
#title-art .sub { letter-spacing:0.45em; font-size:0.95rem; color: var(--text-dim); margin-top:0.3em; text-transform:uppercase; }
#title-art .line { width:120px; height:1px; background: linear-gradient(90deg, transparent, var(--gold), transparent); margin:1.2rem auto; }
.btn-main { position:relative; overflow:hidden; border:1px solid var(--border); background: rgba(255,255,255,0.03); color: var(--gold); padding: 0.9rem 2.4rem; font-size:1.05rem; letter-spacing:0.15em; border-radius: 0.6rem; cursor:pointer; transition: all 0.3s ease; backdrop-filter: blur(6px); box-shadow: 0 0 0 transparent; }
.btn-main:hover { background: rgba(212,175,55,0.08); border-color: rgba(212,175,55,0.45); box-shadow: 0 0 20px var(--gold-glow); transform: translateY(-2px); }
/* Overworld */
#screen-overworld { background: linear-gradient(180deg, #0a0d12 0%, #0f1118 100%); }
#world-canvas { position:absolute; inset:0; width:100%; height:100%; image-rendering:pixelated; }
#hud-zone { position:absolute; top:0; left:0; right:0; height:68px; display:flex; align-items:center; justify-content:space-between; padding:0 24px; pointer-events:none; z-index:30; background: linear-gradient(180deg, rgba(10,13,18,0.85) 0%, rgba(10,13,18,0) 100%); backdrop-filter: blur(4px); }
#hud-zone strong { color: var(--gold); letter-spacing:0.15em; font-size:0.85rem; text-transform:uppercase; }
#hud-zone span { color: var(--text-dim); font-size:0.75rem; letter-spacing:0.05em; }
/* Mobile controls */
#mobile-controls { position:absolute; bottom:24px; left:50%; transform:translateX(-50%); z-index:30; display:grid; grid-template-columns: repeat(3, 64px); grid-template-rows: repeat(3, 64px); gap:8px; pointer-events:auto; opacity:0.9; }
#mobile-controls button { width:64px; height:64px; border-radius:16px; border:1px solid var(--border); background: rgba(255,255,255,0.04); color: var(--gold); font-size:1.4rem; display:flex; align-items:center; justify-content:center; cursor:pointer; backdrop-filter: blur(6px); transition: background 0.2s; }
#mobile-controls button:active { background: rgba(212,175,55,0.15); }
/* Battle */
#screen-battle { background: linear-gradient(160deg, #0a0c15 0%, #111423 50%, #0d0b18 100%); justify-content:flex-start; padding-top:60px; }
#battle-stage { position:relative; width:min(960px, 100vw); height:420px; margin:0 auto; border:1px solid var(--border); border-radius:16px; overflow:hidden; background: radial-gradient(circle at 50% 30%, rgba(123,90,158,0.15) 0%, transparent 70%); box-shadow: 0 0 50px rgba(123,90,158,0.08); }
#battle-stage .enemy-row, #battle-stage .ally-row { display:flex; gap:12px; padding:12px; align-items:center; justify-content:center; position:absolute; left:0; right:0; }
#battle-stage .enemy-row { top:40px; } #battle-stage .ally-row { bottom:40px; }
.battle-entity { width:110px; height:140px; background: rgba(255,255,255,0.03); border:1px solid var(--border); border-radius:12px; display:flex; flex-direction:column; align-items:center; justify-content:center; position:relative; overflow:hidden; transition: transform 0.2s; }
.battle-entity.active { border-color: var(--gold); box-shadow: 0 0 20px var(--gold-glow); }
.battle-entity .name { font-size:0.7rem; color: var(--gold); letter-spacing:0.08em; margin-bottom:2px; }
.battle-entity .hp-bar { width:70%; height:8px; background: rgba(255,255,255,0.08); border-radius:4px; overflow:hidden; margin-top:4px; }
.battle-entity .hp-bar .fill { width:100%; height:100%; background: linear-gradient(90deg, var(--mint), var(--mint) 80%, var(--mint) 100%); border-radius:4px; transition: width 0.4s ease; }
.battle-entity .hp-bar .fill.low { background: linear-gradient(90deg, var(--rose), #8a3344); }
.battle-entity .hp-bar .fill.dead { background: linear-gradient(90deg, #666, #333); }
#battle-log { width:min(960px, 100vw); max-height:160px; overflow-y:auto; margin:16px auto 0; padding:12px 18px; border:1px solid var(--border); border-radius:10px; background: rgba(255,255,255,0.015); font-size:0.85rem; line-height:1.45; color: var(--text-dim); }
#battle-log .line { margin-bottom:4px; } .line-crit { color: var(--amber); font-weight:bold; } .line-eff { color: var(--mint); } .line-dmg { color: var(--rose); }
/* Action panel */
#battle-actions { width:min(960px, 100vw); margin:16px auto 0; display:grid; grid-template-columns: repeat(4, 1fr); gap:8px; }
.battle-btn { padding:12px 8px; border-radius:8px; border:1px solid var(--border); background: rgba(255,255,255,0.03); color: var(--text); font-size:0.8rem; cursor:pointer; transition: all 0.2s; text-align:center; }
.battle-btn:hover { background: rgba(212,175,55,0.06); border-color: rgba(212,175,55,0.3); }
.battle-btn .pp { font-size:0.65rem; color: var(--gold-dim); }
/* Menu screens */
#screen-menu { z-index:50; background: rgba(10,13,14,0.88); backdrop-filter: blur(14px); }
.menu-card { width:min(600px, 92vw); background: linear-gradient(160deg, rgba(26,27,34,0.92), rgba(20,19,26,0.92)); border:1px solid var(--border); border-radius:16px; padding:28px; box-shadow: 0 20px 60px rgba(0,0,0,0.6); }
.menu-card h2 { color: var(--gold); letter-spacing:0.2em; font-size:1.1rem; margin-bottom:16px; text-transform:uppercase; font-weight:300; }
.menu-row { display:flex; align-items:center; gap:14px; padding:10px 0; border-bottom:1px solid rgba(255,255,255,0.04); }
.menu-row .label { flex:1; } .menu-row .value { color: var(--gold); font-weight:bold; font-size:0.85rem; }
.menu-row .sub { font-size:0.7rem; color: var(--text-dim); }
.grid-2 { display:grid; grid-template-columns: repeat(2, 1fr); gap:8px; }
.ability-row { display:flex; align-items:center; gap:10px; padding:8px; border-radius:8px; border:1px solid rgba(255,255,255,0.04); background: rgba(255,255,255,0.015); }
.ability-row .power { color: var(--amber); font-weight:bold; width:32px; text-align:center; }
.ability-row .name { flex:1; font-size:0.85rem; }
.ability-row .pp { color: var(--gold-dim); font-size:0.75rem; }
/* Encyclopedia */
.enc-grid { display:grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap:8px; }
.enc-card { background: rgba(255,255,255,0.03); border:1px solid var(--border); border-radius:10px; padding:12px; text-align:center; transition: all 0.2s; cursor:pointer; }
.enc-card:hover { border-color: var(--gold); }
.enc-card .icon { width:48px; height:48px; border-radius:50%; background: radial-gradient(circle at 35% 35%, rgba(255,255,255,0.12), rgba(255,255,255,0.02)); margin:0 auto 8px; display:flex; align-items:center; justify-content:center; font-size:1.4rem; }
.enc-card .name { font-size:0.75rem; color: var(--gold); letter-spacing:0.05em; }
.enc-card .catch { font-size:0.65rem; color: var(--text-dim); }
/* Dialog / Story */
#dialog-overlay { position:fixed; inset:0; z-index:60; display:flex; align-items:flex-end; justify-content:center; background: rgba(0,0,0,0.65); backdrop-filter: blur(4px); transition: opacity 0.3s; }
#dialog-box { width:min(700px, 92vw); margin-bottom:60px; padding:28px 32px; border-radius:14px; background: linear-gradient(160deg, rgba(26,27,34,0.95), rgba(20,19,26,0.95)); border:1px solid var(--border); box-shadow: 0 0 60px rgba(123,90,158,0.3); animation: floatUp 0.4s ease-out; }
#dialog-box .speaker { color: var(--gold); letter-spacing:0.15em; font-size:0.75rem; text-transform:uppercase; margin-bottom:8px; }
#dialog-box .text { font-size:1.05rem; line-height:1.7; color: var(--text); }
#dialog-box .btn-ok { margin-top:18px; float:right; }
/* Capture mini-game */
#capture-mini { position:fixed; inset:0; z-index:70; display:flex; align-items:center; justify-content:center; background: rgba(10,13,14,0.9); backdrop-filter: blur(10px); }
#capture-area { width:320px; text-align:center; }
#capture-area h3 { color: var(--gold); letter-spacing:0.15em; margin-bottom:6px; font-weight:300; }
#capture-area .instruction { color: var(--text-dim); font-size:0.85rem; margin-bottom:20px; }
#capture-track { width:100%; height:20px; background: rgba(255,255,255,0.06); border-radius:10px; position:relative; overflow:hidden; border:1px solid var(--border); }
#capture-track .sweet { position:absolute; top:0; bottom:0; width:60px; background: linear-gradient(90deg, transparent, var(--mint), transparent); opacity:0.6; border-radius:10px; }
#capture-track .needle { position:absolute; top:2px; bottom:2px; width:4px; background: var(--gold); border-radius:2px; left:10px; transition: left 0.05s linear; box-shadow: 0 0 8px var(--gold); }
/* Utility */
.vignette { position:absolute; inset:0; pointer-events:none; background: radial-gradient(circle at 50% 50%, transparent 50%, rgba(10,13,14,0.6) 100%); }
.hidden { display:none !important; }
@media (max-width: 600px) {
#battle-stage { height:340px; }
#battle-stage .enemy-row, #battle-stage .ally-row { padding:8px; gap:4px; }
.battle-entity { width:85px; height:115px; }
}
</style>
</head>
<body>
<div id="bg-ambient"></div>
<!-- OVERWORLD -->
<div id="screen-overworld">
<canvas id="world-canvas"></canvas>
<div id="hud-zone"><div><strong id="zone-label">Aetheria</strong> <span id="zone-desc">The Sacred Grove</span></div><div><span>Zone 1 / 6</span></div></div>
<div id="mobile-controls" aria-label="Movement controls">
<div></div><button onclick="moveDir(0,-1)">▲</button><div></div>
<button onclick="moveDir(-1,0)">◀</button><button onclick="startCapture()">●</button><button onclick="moveDir(1,0)">▶</button>
<div></div><button onclick="moveDir(0,1)">▼</button><div></div>
</div>
<div style="position:absolute; bottom:70px; left:0; right:0; text-align:center; pointer-events:none; z-index:30; font-size:0.75rem; color:var(--text-dim); letter-spacing:0.08em;">WASD / Arrows to move</div>
</div>
<!-- BATTLE -->
<div id="screen-battle" class="hidden">
<div id="battle-stage">
<div class="enemy-row" id="enemy-row"></div>
<div class="ally-row" id="ally-row"></div>
<div class="vignette"></div>
</div>
<div id="battle-log"><div class="line" id="log-start">The air hums with tension...</div></div>
<div id="battle-actions"></div>
</div>
<!-- MENU -->
<div id="screen-menu" class="hidden">
<div class="menu-card">
<h2>The Veil of Aetherium</h2>
<div class="grid-2">
<button class="btn-main" onclick="openScreen('enc')">Encyclopedia</button>
<button class="btn-main" onclick="openScreen('map')">World Map</button>
<button class="btn-main" onclick="openTeam()">Party</button>
<button class="btn-main" onclick="showStory()">Story</button>
<button class="btn-main" onclick="openScreen('shop')">Shrine</button>
<button class="btn-main" onclick="closeMenu()">Continue</button>
</div>
</div>
</div>
<!-- ENCYCLOPEDIA -->
<div id="screen-enc" class="hidden">
<div class="menu-card" style="max-height:85vh; overflow-y:auto;">
<h2>Creature Encyclopedia</h2>
<div id="enc-content" class="enc-grid"></div>
</div>
<button class="btn-main" onclick="openScreen('menu')" style="margin-top:12px;">Back</button>
</div>
<!-- SHOP / SHRINE -->
<div id="screen-shop" class="hidden">
<div class="menu-card" style="max-height:85vh; overflow-y:auto;">
<h2>Sacred Shrine</h2>
<p style="font-size:0.85rem; color:var(--text-dim); margin-bottom:16px;">Rest, restore, and acquire catalysts.</p>
<div id="shop-list"></div>
<hr style="margin:14px 0; border-color:rgba(255,255,255,0.06);">
<button class="btn-main" onclick="restParty()" style="width:100%; margin-top:6px;">Rest & Heal Party (Save Game)</button>
</div>
<button class="btn-main" onclick="openScreen('menu')" style="margin-top:12px;">Back</button>
</div>
<!-- MAP -->
<div id="screen-map" class="hidden">
<div class="menu-card" style="text-align:center;">
<h2>World Map — The Veil of Aetherium</h2>
<div style="display:flex; gap:8px; justify-content:center; flex-wrap:wrap; margin-top:14px;">
<div style="padding:8px 14px; border-radius:8px; background:rgba(212,175,55,0.08); border:1px solid rgba(212,175,55,0.25); color:var(--gold); font-size:0.8rem;">Zone 1 — Lumina Grove</div>
<div style="padding:8px 14px; border-radius:8px; background:rgba(212,175,55,0.08); border:1px solid rgba(212,175,55,0.25); color:var(--gold); font-size:0.8rem;">Zone 2 — Tenebra Chasm</div>
<div style="padding:8px 14px; border-radius:8px; background:rgba(212,175,55,0.08); border:1px solid rgba(212,175,55,0.25); color:var(--gold); font-size:0.8rem;">Zone 3 — Florae Canopy</div>
<div style="padding:8px 14px; border-radius:8px; background:rgba(212,175,55,0.08); border:1px solid rgba(212,175,55,0.25); color:var(--gold); font-size:0.8rem;">Zone 4 — Metallum Forge</div>
<div style="padding:8px 14px; border-radius:8px; background:rgba(212,175,55,0.08); border:1px solid rgba(212,175,55,0.25); color:var(--gold); font-size:0.8rem;">Zone 5 — Aether Spires</div>
<div style="padding:8px 14px; border-radius:8px; background:rgba(255,255,255,0.04); border:1px solid var(--border); color:var(--text-dim); font-size:0.8rem;">Zone 6 — Void Citadel</div>
</div>
<button class="btn-main" onclick="openScreen('menu')" style="margin-top:18px;">Back</button>
</div>
</div>
<!-- TEAM -->
<div id="screen-team" class="hidden">
<div class="menu-card" style="max-height:85vh; overflow-y:auto;">
<h2>Party Members</h2>
<div id="team-list"></div>
</div>
<button class="btn-main" onclick="closeMenu()" style="margin-top:12px;">Back</button>
</div>
<!-- TITLE -->
<div id="screen-title">
<div id="title-art">
<h1>The Veil of<br>Aetherium</h1>
<div class="line"></div>
<p class="sub">A New Binding</p>
</div>
<div style="margin-top:48px; display:flex; gap:16px; flex-wrap:wrap; justify-content:center;">
<button class="btn-main" onclick="startNewGame()">New Binding</button>
<button class="btn-main" onclick="loadContinue()">Continue</button>
</div>
<p style="margin-top:30px; color:var(--text-dim); font-size:0.7rem; letter-spacing:0.1em;">An original myth of ethereal creatures</p>
</div>
<!-- DIALOG -->
<div id="dialog-overlay" class="hidden" onclick="if(event.target===this)hideDialog()">
<div id="dialog-box">
<div class="speaker" id="dialog-speaker">The Veil</div>
<div class="text" id="dialog-text">The Aetherium stirs. A new Binding awaits.</div>
<button class="btn-main btn-ok" onclick="hideDialog()">Continue</button>
</div>
</div>
<!-- CAPTURE -->
<div id="capture-mini" class="hidden">
<div id="capture-area">
<h3>Binding Ritual</h3>
<p class="instruction">Tap when the gold line crosses the emerald zone</p>
<div id="capture-track"><div class="sweet" style="left:20%;"></div><div class="needle" id="capture-needle"></div></div>
<button class="btn-main" onclick="runCapture()" style="margin-top:24px;">Bind</button>
</div>
</div>
<script>
// ============================================
// WORLD DATA — Invented from scratch
// ============================================
const TYPES = [
{ key:'Lum', name:'Lumina', color:'#e8a84a', desc:'Sun-forged light' },
{ key:'Ten', name:'Tenebra', color:'#7b5a9e', desc:'Shadow essence' },
{ key:'Flo', name:'Florae', color:'#6abf8a', desc:'Verdant growth' },
{ key:'Met', name:'Metallum', color:'#5ab8d4', desc:'Forged steel' },
{ key:'Aet', name:'Aether', color:'#c8556e', desc:'Arcane energy' },
{ key:'Pri', name:'Primal', color:'#e8a84a', desc:'Raw instinct' },
{ key:'Voi', name:'Void', color:'#383a50', desc:'Absence made form' }
];
function typeName(key){ return TYPES.find(t=>t.key===key)?.name || key; }
function typeColor(key){ return TYPES.find(t=>t.key===key)?.color || '#ececf8'; }
// Type chart: Lum > Flo > Met > Lum; Aet > Lum & Met; Pri > Aet & Flo; Voi resists all except Pri
function typeMult(keyA, keyB) {
if (keyA===keyB) return 1.0;
// Lum > Flo
if (keyA==='Lum' && keyB==='Flo') return 1.45;
if (keyB==='Lum' && keyA==='Flo') return 0.65;
// Flo > Met
if (keyA==='Flo' && keyB==='Met') return 1.45;
if (keyB==='Flo' && keyA==='Met') return 0.65;
// Met > Lum
if (keyA==='Met' && keyB==='Lum') return 1.45;
if (keyB==='Met' && keyA==='Lum') return 0.65;
// Aet > Lum & Met
if (keyA==='Aet' && (keyB==='Lum'||keyB==='Met')) return 1.35;
if ((keyA==='Lum'||keyA==='Met') && keyB==='Aet') return 0.7;
// Pri > Aet & Flo
if (keyA==='Pri' && (keyB==='Aet'||keyB==='Flo')) return 1.35;
if ((keyA==='Aet'||keyA==='Flo') && keyB==='Pri') return 0.7;
// Voi resists everything except Pri
if (keyA==='Voi') return 0.85;
if (keyB==='Voi') return 1.15;
if (keyA==='Pri') return 1.25;
return 1.0;
}
// ============================================
// CREATURE DATA (27 total)
// ============================================
const CREATURES = [];
function addCreature(id, name, affinity, baseHP, baseATK, baseDEF, baseSPD, lore, learnset) {
CREATURES.push({ id, name, affinity, baseHP, baseATK, baseDEF, baseSPD, lore, learnset, level:1, xp:0, hp:baseHP, atk:baseATK, def:baseDEF, spd:baseSPD, pp:{}, abilities:[], transformedFrom:null, transformsTo:null, captured:false });
}
// Starters + 15 base + 9 transforms
function initCreatures(){
addCreature('velis','Velis the Dawnstrider','Lum', 45, 22, 14, 18, 'A luminous stag born from the first sunrise, its antlers weave light into shields.', [
{name:'Solar Lash', power:35, aff:'Lum', pp:10, effect:'burn'},
{name:'Dawn Shield', power:0, aff:'Lum', pp:8, effect:'barrier'},
{name:'Radiant Charge', power:55, aff:'Lum', pp:12, effect:'multi'},
{name:'Beacon', power:0, aff:'Lum', pp:6, effect:'heal', learnAt:8}
]);
addCreature('umbra','Umbra the Shadeweaver','Ten', 42, 25, 12, 15, 'Spun from twilight threads, it binds shadows into blades that forget their targets only at dawn.', [
{name:'Night Slash', power:40, aff:'Ten', pp:10, effect:'burn'},
{name:'Shadow Weave', power:0, aff:'Ten', pp:7, effect:'frost'},
{name:'Eclipse Fang', power:60, aff:'Ten', pp:13, effect:'priority'},
{name:'Gloom Pulse', power:25, aff:'Ten', pp:8, effect:'multi', learnAt:10}
]);
addCreature('sprig','Sprig the Rootbound','Flo', 50, 18, 18, 10, 'A small creature of moss and marrow whose roots remember every forest it has walked through.', [
{name:'Vine Whip', power:30, aff:'Flo', pp:8, effect:'frost'},
{name:'Burrow', power:0, aff:'Flo', pp:9, effect:'heal'},
{name:'Spore Burst', power:45, aff:'Flo', pp:10, effect:'multi'},
{name:'Verdant Ward', power:0, aff:'Flo', pp:6, effect:'barrier', learnAt:9}
]);
// More base
addCreature('forge','Forge the Metalheart','Met', 55, 28, 22, 8, 'Forged not in fire but in conviction. Each scar tells of a battle it refused to lose.', [
{name:'Hammer Fall', power:50, aff:'Met', pp:10, effect:'burn'},
{name:'Steel Shell', power:0, aff:'Met', pp:8, effect:'barrier'},
{name:'Iron Lash', power:30, aff:'Met', pp:7, effect:'multi'},
{name:'Anvil Strike', power:75, aff:'Met', pp:15, effect:'priority', learnAt:12}
]);
addCreature('aetheris','Aetheris the Veilwalker','Aet', 38, 20, 10, 25, 'An apparition that steps between moments. It remembers futures that have not yet been born.', [
{name:'Arc Spark', power:40, aff:'Aet', pp:9, effect:'burn'},
{name:'Temporal Shift', power:0, aff:'Aet', pp:7, effect:'speed'},
{name:'Mind Lash', power:35, aff:'Aet', pp:8, effect:'frost'},
{name:'Veil Tear', power:60, aff:'Aet', pp:13, effect:'multi', learnAt:11}
]);
addCreature('primalus','Primalus the Unbound','Pri', 60, 32, 14, 14, 'Without name or origin, it is the first thought that walked. Raw instinct given weight.', [
{name:'Claw Rift', power:45, aff:'Pri', pp:9, effect:'burn'},
{name:'Roar', power:0, aff:'Pri', pp:6, effect:'speed'},
{name:'Feral Pounce', power:70, aff:'Pri', pp:14, effect:'priority'},
{name:'Savage Surge', power:30, aff:'Pri', pp:7, effect:'multi', learnAt:8}
]);
addCreature('voidling','Voidling the Hollow','Voi', 48, 15, 28, 8, 'Not dark but empty. Where it stands, sound forgets to travel and light turns inward.', [
{name:'Abyss Gaze', power:20, aff:'Voi', pp:7, effect:'frost'},
{name:'Null Field', power:0, aff:'Voi', pp:9, effect:'barrier'},
{name:'Silence Claw', power:35, aff:'Voi', pp:8, effect:'priority'},
{name:'Void Pulse', power:50, aff:'Voi', pp:11, effect:'multi', learnAt:14}
]);
addCreature('lumi','Lumi the Emberfly','Lum', 30, 18, 8, 28, 'A tiny creature of floating embers. Its warmth is gentle, but its speed is relentless.', [
{name:'Flash', power:25, aff:'Lum', pp:6, effect:'speed'},
{name:'Ember Shot', power:30, aff:'Lum', pp:7, effect:'burn'},
{name:'Heat Wave', power:40, aff:'Lum', pp:10, effect:'multi'},
{name:'Solar Drift', power:0, aff:'Lum', pp:5, effect:'heal', learnAt:6}
]);
addCreature('mossar','Mossar the Stonehide','Flo', 65, 16, 30, 6, 'It grew from a single seed dropped in a canyon. Now it is older than the mountains around it.', [
{name:'Root Slam', power:30, aff:'Flo', pp:8, effect:'frost'},
{name:'Bark Shield', power:0, aff:'Flo', pp:9, effect:'barrier'},
{name:'Thorn Burst', power:45, aff:'Flo', pp:10, effect:'multi'},
{name:'Growth Surge', power:0, aff:'Flo', pp:7, effect:'heal', learnAt:7}
]);
addCreature('cinder','Cinder the Ashwing','Ten', 40, 24, 16, 22, 'Born from the smoke of a forgotten temple, it carries the memory of prayers never spoken.', [
{name:'Smoke Lash', power:35, aff:'Ten', pp:8, effect:'burn'},
{name:'Ash Step', power:0, aff:'Ten', pp:7, effect:'speed'},
{name:'Ember Fang', power:55, aff:'Ten', pp:12, effect:'priority'},
{name:'Smoke Veil', power:0, aff:'Ten', pp:8, effect:'frost', learnAt:10}
]);
addCreature('glint','Glint the Mirrorborn','Met', 35, 30, 10, 20, 'Every surface reflects what others fear. It teaches courage by showing you yourself.', [
{name:'Shine Strike', power:35, aff:'Met', pp:8, effect:'burn'},
{name:'Mirror Shell', power:0, aff:'Met', pp:9, effect:'barrier'},
{name:'Refract', power:45, aff:'Met', pp:10, effect:'multi'},
{name:'Gleam Flash', power:60, aff:'Met', pp:13, effect:'priority', learnAt:9}
]);
addCreature('wisp','Wisp the Whisper','Aet', 32, 16, 10, 30, 'It is the sound between words. When it speaks, it does not make noise; it creates silence in your memory.', [
{name:'Whisper Lash', power:30, aff:'Aet', pp:7, effect:'burn'},
{name:'Hush', power:0, aff:'Aet', pp:6, effect:'frost'},
{name:'Memory Tear', power:50, aff:'Aet', pp:11, effect:'multi'},
{name:'Silent Drift', power:25, aff:'Aet', pp:6, effect:'speed', learnAt:8}
]);
addCreature('feral','Feral the Wildfang','Pri', 55, 28, 12, 18, 'No master, no name chosen for it. It carved its own legend into the bark of every tree it passed.', [
{name:'Savage Bite', power:40, aff:'Pri', pp:9, effect:'burn'},
{name:'Wild Roar', power:0, aff:'Pri', pp:6, effect:'speed'},
{name:'Beast Leap', power:60, aff:'Pri', pp:13, effect:'priority'},
{name:'Predator Surge', power:35, aff:'Pri', pp:8, effect:'multi', learnAt:7}
]);
addCreature('hollow','Hollow the Silent','Voi', 42, 12, 32, 6, 'Not a creature but a space shaped like one. Where it stands, nothing remembers what it was.', [
{name:'Absence Touch', power:20, aff:'Voi', pp:7, effect:'frost'},
{name:'Void Shield', power:0, aff:'Voi', pp:10, effect:'barrier'},
{name:'Null Bite', power:40, aff:'Voi', pp:9, effect:'priority'},
{name:'Silence Wave', power:30, aff:'Voi', pp:8, effect:'multi', learnAt:13}
]);
addCreature('flame','Flame the Emberpetal','Lum', 25, 22, 8, 26, 'A flower made of fire. Its beauty is brief, its heat eternal.', [
{name:'Petal Lash', power:25, aff:'Lum', pp:6, effect:'burn'},
{name:'Heat Bloom', power:35, aff:'Lum', pp:8, effect:'multi'},
{name:'Ember Guard', power:0, aff:'Lum', pp:7, effect:'barrier'},
{name:'Solar Bloom', power:50, aff:'Lum', pp:11, effect:'priority', learnAt:10}
]);
addCreature('thorn','Thorn the Vinebound','Flo', 58, 20, 24, 8, 'Every thorn is a word it never spoke. It grows in silence until someone tries to take it.', [
{name:'Thorn Whip', power:30, aff:'Flo', pp:8, effect:'frost'},
{name:'Vine Bind', power:0, aff:'Flo', pp:9, effect:'barrier'},
{name:'Spine Burst', power:45, aff:'Flo', pp:10, effect:'multi'},
{name:'Growth Spire', power:55, aff:'Flo', pp:12, effect:'priority', learnAt:11}
]);
addCreature('frost','Frost the Glacialis','Ten', 45, 20, 22, 12, 'Formed in the breath of a sleeping giant, it carries winter without knowing cold.', [
{name:'Ice Lash', power:30, aff:'Ten', pp:8, effect:'frost'},
{name:'Frost Shell', power:0, aff:'Ten', pp:9, effect:'barrier'},
{name:'Glacial Strike', power:50, aff:'Ten', pp:11, effect:'priority'},
{name:'Winter Breath', power:35, aff:'Ten', pp:8, effect:'multi', learnAt:9}
]);
addCreature('crystal','Crystal the Prismborn','Met', 40, 26, 18, 14, 'Every facet holds a different past. It remembers what you have forgotten.', [
{name:'Prism Lash', power:30, aff:'Met', pp:8, effect:'burn'},
{name:'Crystal Shell', power:0, aff:'Met', pp:8, effect:'barrier'},
{name:'Refract Strike', power:45, aff:'Met', pp:10, effect:'multi'},
{name:'Shatter Flash', power:65, aff:'Met', pp:14, effect:'priority', learnAt:12}
]);
addCreature('phantom','Phantom the Unseen','Aet', 35, 28, 8, 28, 'It exists between your blinks. When you turn quickly, you see it already leaving.', [
{name:'Ghost Lash', power:35, aff:'Aet', pp:8, effect:'burn'},
{name:'Unseen Step', power:0, aff:'Aet', pp:7, effect:'speed'},
{name:'Specter Tear', power:55, aff:'Aet', pp:12, effect:'multi'},
{name:'Shadow Flash', power:70, aff:'Aet', pp:15, effect:'priority', learnAt:13}
]);
addCreature('beast','Beast the Rawborn','Pri', 62, 35, 10, 10, 'No language, no memory of being taught. Only hunger, only the joy of running.', [
{name:'Raw Bite', power:45, aff:'Pri', pp:9, effect:'burn'},
{name:'Beast Roar', power:0, aff:'Pri', pp:6, effect:'speed'},
{name:'Savage Leap', power:75, aff:'Pri', pp:15, effect:'priority'},
{name:'Wild Surge', power:30, aff:'Pri', pp:8, effect:'multi', learnAt:6}
]);
addCreature('voidheart','Voidheart the Endform','Voi', 50, 18, 35, 7, 'A shape that looks like grief. It does not hunt; it waits for you to realize you have already lost.', [
{name:'End Touch', power:25, aff:'Voi', pp:7, effect:'frost'},
{name:'Null Shield', power:0, aff:'Voi', pp:11, effect:'barrier'},
{name:'End Lash', power:45, aff:'Voi', pp:10, effect:'priority'},
{name:'Void End', power:60, aff:'Voi', pp:14, effect:'multi', learnAt:15}
]);
// Transforms (9)
addCreature('velisawaken','Velis the Dawnawakened','Lum', 90, 42, 30, 26, 'The stags antlers have become sunfire. It does not walk; it arrives.', [
{name:'Solar Flare', power:70, aff:'Lum', pp:14, effect:'burn'},
{name:'Dawn Aegis', power:0, aff:'Lum', pp:10, effect:'barrier'},
{name:'Radiant Nova', power:100, aff:'Lum', pp:18, effect:'multi'},
{name:'Beacon Surge', power:0, aff:'Lum', pp:10, effect:'heal', learnAt:25}
], 30, 50, [{name:'Solar Aether', desc:'A crystal forged at sunrise'}]);
addCreature('umbrawraith','Umbra the Shadowwraith','Ten', 85, 48, 24, 22, 'No longer woven but woven into reality. Shadows serve it now.', [
{name:'Nightfall Lash', power:75, aff:'Ten', pp:14, effect:'burn'},
{name:'Wraith Weave', power:0, aff:'Ten', pp:10, effect:'frost'},
{name:'Eclipse Strike', power:110, aff:'Ten', pp:18, effect:'priority'},
{name:'Gloom Nova', power:60, aff:'Ten', pp:12, effect:'multi', learnAt:30}
], 30, 50, [{name:'Shadow Core', desc:'A heart shaped from darkness'}]);
addCreature('sprigtitan','Sprig the Roottitan','Flo', 95, 32, 46, 12, 'Not one tree but all trees. Its form is a forest made singular.', [
{name:'Root Cascade', power:50, aff:'Flo', pp:12, effect:'frost'},
{name:'Titan Ward', power:0, aff:'Flo', pp:11, effect:'barrier'},
{name:'Spore Storm', power:80, aff:'Flo', pp:16, effect:'multi'},
{name:'Verdant Surge', power:0, aff:'Flo', pp:12, effect:'heal', learnAt:28}
], 30, 50, [{name:'Growth Heart', desc:'A seed older than time'}]);
addCreature('forgearchon','Forge the Archonheart','Met', 100, 55, 50, 14, 'It has become the anvil upon which the world is struck.', [
{name:'Archon Fall', power:85, aff:'Met', pp:16, effect:'burn'},
{name:'Steel Throne', power:0, aff:'Met', pp:12, effect:'barrier'},
{name:'Anvil Storm', power:120, aff:'Met', pp:20, effect:'priority'},
{name:'Iron Surge', power:0, aff:'Met', pp:14, effect:'heal', learnAt:35}
], 30, 50, [{name:'Anvil Heart', desc:'The core of the forge'}]);
addCreature('aetherveil','Aetheris the Veilbroken','Aet', 70, 40, 20, 35, 'The boundary between worlds has dissolved. It is the tear.', [
{name:'Arc Nova', power:80, aff:'Aet', pp:15, effect:'burn'},
{name:'Temporal Break', power:0, aff:'Aet', pp:12, effect:'speed'},
{name:'Mind Storm', power:70, aff:'Aet', pp:14, effect:'multi'},
{name:'Veil Break', power:110, aff:'Aet', pp:20, effect:'priority', learnAt:32}
], 30, 50, [{name:'Veil Shard', desc:'Fragment of a broken dimension'}]);
addCreature('primalord','Primalus the Primalord','Pri', 105, 58, 28, 22, 'The first thought has become the only thought that remains.', [
{name:'Claw Apocalypse', power:90, aff:'Pri', pp:16, effect:'burn'},
{name:'Lord Roar', power:0, aff:'Pri', pp:12, effect:'speed'},
{name:'Feral Storm', power:130, aff:'Pri', pp:22, effect:'priority'},
{name:'Savage Nova', power:0, aff:'Pri', pp:14, effect:'multi', learnAt:35}
], 30, 50, [{name:'Primal Heart', desc:'The origin of all instinct'}]);
addCreature('voidking','Voidling the Voidking','Voi', 90, 28, 55, 14, 'Not a ruler but a vacancy crowned. The crown is absence.', [
{name:'Abyss Nova', power:70, aff:'Voi', pp:14, effect:'frost'},
{name:'Null Throne', power:0, aff:'Voi', pp:13, effect:'barrier'},
{name:'Silence King', power:100, aff:'Voi', pp:18, effect:'priority'},
{name:'Void Storm', power:90, aff:'Voi', pp:16, effect:'multi', learnAt:36}
], 30, 50, [{name:'Void Crown', desc:'Crown of absolute absence'}]);
addCreature('lumiang','Lumi the Angelic','Lum', 65, 38, 22, 36, 'Wings of pure light. It does not fly; it is carried by the air that remembers sunlight.', [
{name:'Angelic Lash', power:60, aff:'Lum', pp:13, effect:'burn'},
{name:'Halo Shield', power:0, aff:'Lum', pp:10, effect:'barrier'},
{name:'Heaven Flash', power:85, aff:'Lum', pp:17, effect:'priority'},
{name:'Light Surge', power:40, aff:'Lum', pp:10, effect:'heal', learnAt:20}
], 20, 50, [{name:'Angel Feather', desc:'Feather fallen from a sun angel'}]);
addCreature('mossartitan','Mossar the Titanstone','Flo', 110, 28, 60, 10, 'A walking mountain with a gentle heartbeat. Mountains are patient.', [
{name:'Stone Cascade', power:55, aff:'Flo', pp:12, effect:'frost'},
{name:'Titan Shield', power:0, aff:'Flo', pp:14, effect:'barrier'},
{name:'Stone Storm', power:85, aff:'Flo', pp:17, effect:'multi'},
{name:'Growth Titan', power:0, aff:'Flo', pp:14, effect:'heal', learnAt:22}
], 25, 50, [{name:'Stone Heart', desc:'Heart of a stone titan'}]);
addCreature('phantomlord','Phantom the Phantomlord','Aet', 65, 45, 16, 38, 'Every mirror shows a different lord. All of them are the same. None of them are real.', [
{name:'Specter Nova', power:80, aff:'Aet', pp:15, effect:'burn'},
{name:'Phantom Shield', power:0, aff:'Aet', pp:12, effect:'barrier'},
{name:'Shadow King', power:110, aff:'Aet', pp:20, effect:'priority'},
{name:'Specter Surge', power:50, aff:'Aet', pp:12, effect:'multi', learnAt:30}
], 28, 50, [{name:'Specter Crown', desc:'Crown of the unseen lord'}]);
}
initCreatures();
const ABILITIES = [];
function collectAbilities() { ABILITIES.length=0; CREATURES.forEach(c=>c.learnset.forEach(a=>{ if(!ABILITIES.find(x=>x.name===a.name)) ABILITIES.push({name:a.name, power:a.power, aff:c.affinity, pp:a.pp, effect:a.effect}); })); }
collectAbilities();
// ============================================
// GAME STATE
// ============================================
let state = {
zone: 1, zonePos: {x:8, y:8}, party: [], inventory: {catalyst:2, heal:3, revive:1, gold:200},
battlesWon: 0, creaturesSeen: {}, storyStage: 0, unlockedZones: 1,
shrinesVisited: 0, savedZonePos: null, savedParty: null, savedInventory: null
};
function resetStateForNew(){
state.zone = 1; state.zonePos = {x:8,y:8};
state.party = [JSON.parse(JSON.stringify(CREATURES.find(c=>c.id==='velis'))), JSON.parse(JSON.stringify(CREATURES.find(c=>c.id==='sprig'))), JSON.parse(JSON.stringify(CREATURES.find(c=>c.id==='umbra')))].map(p=>{ p.hp = p.baseHP; p.atk = p.baseATK; p.def = p.baseDEF; p.spd = p.baseSPD; p.level = 3; p.xp = 0; p.abilities = p.learnset.filter(a=>!a.learnAt || p.level>=a.learnAt).map(a=>a.name); p.pp = {}; p.abilities.forEach(abName=>{ const ab = ABILITIES.find(x=>x.name===abName); if(ab) p.pp[abName] = ab.pp; }); return p; });
state.inventory = {catalyst:1, heal:3, revive:0, gold:350};
state.battlesWon = 0; state.creaturesSeen = {}; state.storyStage = 1; state.unlockedZones = 1; state.shrinesVisited = 0;
state.savedZonePos = {x:8,y:8}; state.savedParty = JSON.parse(JSON.stringify(state.party)).map(p=>({id:p.id, hp:p.hp, xp:p.xp, level:p.level, pp:JSON.parse(JSON.stringify(p.pp||{})), abilities:[...p.abilities]}));
state.savedInventory = JSON.parse(JSON.stringify(state.inventory));
saveToStorage();
}
function loadContinue(){
const raw = localStorage.getItem('aetherium_save');
if(raw){
try { const s = JSON.parse(raw); state = s; state.party = state.party.map(p=>{ const base = CREATURES.find(c=>c.id===p.id); if(!base) return p; const full = {...base, ...p, learnset: base.learnset}; full.hp = full.baseHP; full.atk = full.baseATK; full.def = full.baseDEF; full.spd = full.baseSPD; if(p.hp !== undefined) full.hp = Math.min(p.hp, full.baseHP + (full.level-1)*5); if(p.atk !== undefined) full.atk = p.atk; if(p.def !== undefined) full.def = p.def; if(p.spd !== undefined) full.spd = p.spd; if(p.level) full.level = p.level; if(p.xp) full.xp = p.xp; if(p.pp) full.pp = p.pp; if(p.abilities) full.abilities = p.abilities; return full; }); openScreen('overworld'); } catch(e){ resetStateForNew(); openScreen('overworld'); }
} else { resetStateForNew(); openScreen('overworld'); }
}
function saveToStorage(){ localStorage.setItem('aetherium_save', JSON.stringify(state)); }
// ============================================
// OVERWORLD
// ============================================
const ZONES = [
{ name:'Lumina Grove', color:'#2a2518', desc:'First light touches the moss.', bg:[30,22,12], particles:'gold', enemyRate:0.08 },
{ name:'Tenebra Chasm', color:'#1a1522', desc:'Shadows breathe here.', bg:[26,21,34], particles:'violet', enemyRate:0.10 },
{ name:'Florae Canopy', color:'#182218', desc:'Life climbs endlessly.', bg:[24,34,24], particles:'mint', enemyRate:0.09 },
{ name:'Metallum Forge', color:'#222830', desc:'The anvil never cools.', bg:[34,40,46], particles:'cyan', enemyRate:0.12 },
{ name:'Aether Spires', color:'#201828', desc:'Thought made architecture.', bg:[32,24,40], particles:'rose', enemyRate:0.11 },
{ name:'Void Citadel', color:'#0d0d12', desc:'Silence crowns all.', bg:[13,13,18], particles:'dark', enemyRate:0.15 },
];
const OVERWORLD_TILE = 48;
const WORLD_W = 20; const WORLD_H = 15;
let canvas, ctx, animTick = 0;
function initCanvas(){
canvas = document.getElementById('world-canvas');
ctx = canvas.getContext('2d');
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
requestAnimationFrame(renderWorldLoop);
}
function resizeCanvas(){
const dpr = window.devicePixelRatio || 1;
const w = window.innerWidth; const h = window.innerHeight;
canvas.width = w * dpr; canvas.height = h * dpr;
canvas.style.width = w + 'px'; canvas.style.height = h + 'px';
ctx.scale(dpr, dpr);
}
function renderWorldLoop(){
animTick++;
const zone = ZONES[Math.min(state.zone-1, ZONES.length-1)];
const w = window.innerWidth; const h = window.innerHeight;
ctx.clearRect(0,0,w,h);
// Gradient background
const g = ctx.createLinearGradient(0,0,0,h);
g.addColorStop(0, `rgb(${zone.bg[0]-5},${zone.bg[1]-2},${zone.bg[2]-3})`);
g.addColorStop(1, `rgb(${zone.bg[0]+10},${zone.bg[1]+8},${zone.bg[2]+5})`);
ctx.fillStyle = g; ctx.fillRect(0,0,w,h);
// Grid tiles (subtle)
ctx.save();
ctx.strokeStyle = 'rgba(255,255,255,0.03)'; ctx.lineWidth = 1;
const cx = w/2; const cy = h/2; const startX = cx - (state.zonePos.x * OVERWORLD_TILE) + OVERWORLD_TILE/2; const startY = cy - (state.zonePos.y * OVERWORLD_TILE) + OVERWORLD_TILE/2;
for(let i=-12; i<=12; i++){ for(let j=-10; j<=10; j++){ const tx = cx + (i-state.zonePos.x)*OVERWORLD_TILE; const ty = cy + (j-state.zonePos.y)*OVERWORLD_TILE; ctx.strokeRect(tx-OVERWORLD_TILE/2, ty-OVERWORLD_TILE/2, OVERWORLD_TILE, OVERWORLD_TILE); } }
ctx.restore();
// Player on grid
const px = w/2; const py = h/2;
ctx.save();
ctx.shadowColor = 'rgba(212,175,55,0.5)'; ctx.shadowBlur = 15;
ctx.fillStyle = '#d4af37';
ctx.beginPath(); ctx.arc(px, py, 10, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#ececf8'; ctx.font = '10px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('✦', px, py);
ctx.restore();
// Particles
for(let i=0;i<25;i++){
const sx = Math.random()*w; const sy = Math.random()*h;
const life = (animTick + i*7) % 120;
const alpha = 0.2 + 0.4*Math.sin(life/20);
ctx.fillStyle = zone.particles==='gold'?`rgba(212,175,55,${alpha})`: zone.particles==='violet'?`rgba(123,90,158,${alpha})`: zone.particles==='mint'?`rgba(106,191,138,${alpha})`: zone.particles==='cyan'?`rgba(90,184,212,${alpha})`: zone.particles==='rose'?`rgba(200,85,110,${alpha})`: `rgba(255,255,255,${alpha*0.3})`;
const r = 1+Math.random()*2;
ctx.beginPath(); ctx.arc(sx, sy, r, 0, Math.PI*2); ctx.fill();
}
// Story nodes / shrines
const nodes = [
{name:'Shrine', x:4, y:4, type:'shrine'},
{name:'Story', x:14, y:3, type:'story'},
{name:'Guardian', x:16, y:10, type:'boss'},
{name:'Story', x:6, y:12, type:'story'},
];
nodes.forEach(n=>{
const nx = cx + (n.x-state.zonePos.x)*OVERWORLD_TILE; const ny = cy + (n.y-state.zonePos.y)*OVERWORLD_TILE;
if(Math.abs(n.x-state.zonePos.x) <= 8 && Math.abs(n.y-state.zonePos.y) <= 8){
ctx.save();
if(n.type==='shrine'){ ctx.fillStyle='rgba(212,175,55,0.35)'; ctx.beginPath(); ctx.arc(nx,ny,16,0,Math.PI*2); ctx.fill(); ctx.fillStyle='#ececf8'; ctx.font='10px sans-serif'; ctx.fillText('✦', nx, ny); }
if(n.type==='story'){ ctx.fillStyle='rgba(123,90,158,0.35)'; ctx.beginPath(); ctx.arc(nx,ny,14,0,Math.PI*2); ctx.fill(); ctx.fillStyle='#ececf8'; ctx.font='9px sans-serif'; ctx.fillText('?', nx, ny); }
if(n.type==='boss'){ ctx.fillStyle='rgba(200,85,110,0.4)'; ctx.beginPath(); ctx.moveTo(nx,ny-16); ctx.lineTo(nx+14,ny+8); ctx.lineTo(nx-14,ny+8); ctx.closePath(); ctx.fill(); }
ctx.restore();
}
});
requestAnimationFrame(renderWorldLoop);
}
function moveDir(dx, dy){
state.zonePos.x += dx; state.zonePos.y += dy;
state.zonePos.x = Math.max(1, Math.min(WORLD_W-2, state.zonePos.x));
state.zonePos.y = Math.max(1, Math.min(WORLD_H-2, state.zonePos.y));
document.getElementById('zone-label').textContent = ZONES[Math.min(state.zone-1, ZONES.length-1)].name;
document.getElementById('zone-desc').textContent = ZONES[Math.min(state.zone-1, ZONES.length-1)].desc;
checkOverworldEvent();
}
function checkOverworldEvent(){
// Random encounter
const zone = ZONES[Math.min(state.zone-1, ZONES.length-1)];
if(Math.random() < zone.enemyRate) startRandomBattle();
// Story node
if(state.storyStage === 1 && state.zonePos.x >= 13 && state.zonePos.y <= 4) { state.storyStage = 2; showDialog('Wanderer of Aether', 'You have crossed the first threshold. The Veil deepens.'); saveToStorage(); }
if(state.storyStage === 2 && state.zonePos.x <= 7 && state.zonePos.y >= 11) { state.storyStage = 3; showDialog('The Old Voice', 'Every zone has an anchor. Defeat them to bind the realm together.'); saveToStorage(); }
// Shrine
if(Math.abs(state.zonePos.x - 4) < 2 && Math.abs(state.zonePos.y - 4) < 2) { openScreen('shop'); saveToStorage(); }
// Guardian (simplified trigger for demo)
if(Math.abs(state.zonePos.x - 16) < 2 && Math.abs(state.zonePos.y - 10) < 2 && state.battlesWon >= 2) startBossBattle();
}
function startCapture(){
document.getElementById('capture-mini').classList.remove('hidden');
document.getElementById('capture-area').style.animation = 'floatUp 0.3s ease-out';
let pos = 10; let dir = 1; let interval = setInterval(()=>{ pos += dir*3; if(pos>=80){dir=-1;} else if(pos<=10){dir=1;} document.getElementById('capture-needle').style.left = pos + '%'; }, 50);
window._captureInterval = interval;
}
function runCapture(){ clearInterval(window._captureInterval); const needlePos = parseFloat(document.getElementById('capture-needle').style.left || '10'); const sweetLeft = 20; const sweetW = 60; const success = needlePos >= sweetLeft && needlePos <= sweetLeft + sweetW; document.getElementById('capture-mini').classList.add('hidden'); return success; }
// ============================================
// BATTLE SYSTEM
// ============================================
let battleState = { allies:[null], enemies:[null], turnIndex:0, log:[""], currentAbility:null, targetIndex:0, activeParty:[null] };
function startRandomBattle(){
openScreen('battle');
const zone = ZONES[Math.min(state.zone-1, ZONES.length-1)];
const base = ['velis','umbra','sprig','forge','aetheris','primalus','voidling','lumi'];
const pool = base.map(id=>CREATURES.find(c=>c.id===id)).filter(Boolean);
const count = Math.min(3, Math.floor(Math.random()*2)+1);
const picked = [];
for(let i=0;i<count;i++){ const c = pool[Math.floor(Math.random()*pool.length)]; picked.push(JSON.parse(JSON.stringify(c))); picked[i].hp = Math.round(picked[i].baseHP*(0.7+Math.random()*0.9)); picked[i].level = 2+Math.floor(Math.random()*state.zone); picked[i].atk = Math.round(picked[i].baseATK + (picked[i].level-1)*1.5); picked[i].def = Math.round(picked[i].baseDEF + (picked[i].level-1)*1.2); picked[i].spd = Math.round(picked[i].baseSPD + (picked[i].level-1)*0.8); }
battleState.enemies = picked;
battleState.allies = JSON.parse(JSON.stringify(state.party)).map(p=>{ p.hp = Math.min(p.hp, p.baseHP + (p.level-1)*6); return p; });
battleState.turnIndex = 0;
battleState.log = [`Wild ${battleState.enemies.length > 1 ? 'creatures appear' : 'creature appears'}!`];
renderBattle();
logLine('Battle begins...');
}
function startBossBattle(){ openScreen('battle'); const zone = ZONES[Math.min(state.zone-1, ZONES.length-1)]; const bossPool = ['forgearchon','velisawaken','umbrawraith','sprigtitan']; const bossId = bossPool[Math.floor(Math.random()*bossPool.length)]; const b = CREATURES.find(c=>c.id===bossId); const boss = JSON.parse(JSON.stringify(b)); boss.hp = Math.round(boss.baseHP * (1 + state.zone*0.2)); boss.level = 15 + state.zone*3; boss.atk = Math.round(boss.baseATK + boss.level*2); boss.def = Math.round(boss.baseDEF + boss.level*2); boss.spd = Math.round(boss.baseSPD + boss.level); battleState.enemies = [boss]; battleState.allies = JSON.parse(JSON.stringify(state.party)).map(p=>{ p.hp = Math.min(p.hp, p.baseHP + (p.level-1)*6); return p; }); battleState.turnIndex = 0; battleState.log = [`The Zone Anchor — ${boss.name} — emerges!`]; renderBattle(); logLine('A Guardian stands before you.'); }
function renderBattle(){
document.getElementById('enemy-row').innerHTML = battleState.enemies.map((e,i)=>entityHTML('enemy', e, i)).join('');
document.getElementById('ally-row').innerHTML = battleState.allies.map((a,i)=>entityHTML('ally', a, i)).join('');
document.getElementById('battle-log').innerHTML = battleState.log.map(l=>`<div class="line">${l}</div>`).join('');
document.getElementById('battle-log').scrollTop = document.getElementById('battle-log').scrollHeight;
renderActions();
}
function entityHTML(team, e, idx){
const isActive = (team==='ally' && battleState.turnIndex === idx) || false;
const hpPct = Math.max(0, Math.min(100, (e.hp / (e.baseHP + (e.level-1)*6)) * 100));
const hpClass = hpPct <= 20 ? 'low' : '';
const deadClass = e.hp <= 0 ? 'dead' : '';
return `<div class="battle-entity ${isActive ? 'active' : ''}" onclick="selectTarget(${idx},'${team}')">` +
`<div class="name">${e.name}</div>` +
`<div style="font-size:0.6rem; color:var(--text-dim);">Lv.${e.level}</div>` +
`<div class="hp-bar"><div class="fill ${hpClass} ${deadClass}" style="width:${hpPct}%"></div></div>` +
`<div style="font-size:0.65rem; color:var(--gold-dim);">HP ${Math.round(e.hp)}/${Math.round(e.baseHP + (e.level-1)*6)}</div>` +
`</div>`;
}
function renderActions(){
const ally = battleState.allies[battleState.turnIndex % battleState.allies.length];
if(ally.hp <= 0){ document.getElementById('battle-actions').innerHTML = '<div style="text-align:center; color:var(--text-dim);">Awaiting...</div>'; return; }
let html = '<div class="battle-btn" onclick="doSwitch()">Switch</div>';
ally.abilities = ally.abilities || []; ally.abilities = ally.abilities.map(aName=>aName);
ally.abilities.forEach((abName, i) => {
const ab = ABILITIES.find(x=>x.name===abName);
if(ab){ const ppLeft = (ally.pp[abName] || ab.pp); html += `<div class="battle-btn" onclick="useAbility('${abName}')"><div>${ab.name}</div><div class="pp">PP ${ppLeft}</div></div>`; }
});
document.getElementById('battle-actions').innerHTML = html;
}
function logLine(text){ battleState.log.push(text); renderBattle(); }
function selectTarget(idx, team){ if(team==='enemy') battleState.targetIndex = idx; }
function useAbility(name){
const ally = battleState.allies[battleState.turnIndex % battleState.allies.length];
const ab = ABILITIES.find(a=>a.name===name);
if(!ab || (ally.pp[name]||ab.pp) <= 0) return;
if(ally.hp <= 0 || battleState.enemies.every(e=>e.hp<=0)) return;
ally.pp[name] = (ally.pp[name]||ab.pp) - 1;
const target = battleState.enemies[battleState.targetIndex % battleState.enemies.length];
battleState.currentAbility = ab;
resolveAbility(ally, ab, target);
}
function resolveAbility(ally, ab, target){
if(!target || target.hp <= 0){ logLine(`${ally.name} used ${ab.name}! But no target!`); endTurn(); return; }
const mult = typeMult(ab.aff, target.affinity);
const stab = ab.aff === ally.affinity ? 1.25 : 1;
const dmg = Math.max(1, Math.round((0.4 * ally.level * ally.atk / target.def) * mult * stab * (ab.power/100) * (0.9 + Math.random()*0.2)));
const isCrit = Math.random() < 0.08;
const finalDmg = isCrit ? Math.round(dmg * 1.5) : dmg;
target.hp -= finalDmg;
let msg = `${ally.name} used ${ab.name}! `;
if(isCrit) msg += '<span class="line-crit">CRITICAL HIT!</span> ';
msg += `<span class="line-dmg">${finalDmg} damage</span> to ${target.name}!`;
if(mult > 1.2) msg += ` <span class="line-eff">(Super Effective!)</span>`;
else if(mult < 0.8) msg += ' (Not very effective...)';
logLine(msg);
// Apply special effects
if(ab.effect === 'burn' && target.hp > 0) applyBurn(target);
if(ab.effect === 'frost' && target.hp > 0) applyFrost(target);
if(ab.effect === 'speed' && ally.hp > 0) { ally.spd = Math.round(ally.spd * 1.15); msg += ` ${ally.name}'s speed rose!`; logLine(msg); }
if(ab.effect === 'heal'){ ally.hp = Math.min(ally.hp + Math.round(ally.baseHP * 0.35), ally.baseHP + (ally.level-1)*6); logLine(`${ally.name} healed!`); }
if(ab.effect === 'barrier' && ally.hp > 0) { if(!ally.status) ally.status = []; ally.status.push({name:'Barrier', turns:1}); logLine(`${ally.name} gained Barrier!`); }
if(ab.effect === 'multi'){ const hits = 2 + Math.floor(Math.random()*3); logLine(`It struck ${hits} times!`); }
if(target.hp <= 0){ logLine(`${target.name} was bound!`); state.battlesWon++; saveToStorage(); }
else if(target.status && target.status.find(s=>s.name==='Burn')){
const burnDmg = Math.round(target.baseHP * 0.05 + (target.level-1)*0.5);
target.hp -= burnDmg; logLine(`${target.name} was burned for ${burnDmg}!`);
}
// Enemy turn
setTimeout(() => {
if(!battleState.enemies.some(e=>e.hp>0)){ endBattle('win'); return; }
enemyTurn();
if(!battleState.allies.some(a=>a.hp>0)){ endBattle('lose'); return; }
}, 600);
}
function applyBurn(e){ if(!e.status) e.status=[]; if(!e.status.find(s=>s.name==='Burn')){ e.status.push({name:'Burn', turns:3}); } }
function applyFrost(e){ if(!e.status) e.status=[]; if(!e.status.find(s=>s.name==='Frost')){ e.status.push({name:'Frost', turns:3}); } }
function enemyTurn(){
const alive = battleState.enemies.filter(e=>e.hp>0);
if(alive.length===0) return;
const enemy = alive[0];
const ally = battleState.allies.filter(a=>a.hp>0);
const target = ally[Math.floor(Math.random()*ally.length)];
const ab = ABILITIES.filter(a=>a.aff===enemy.affinity || a.aff==='Pri').sort((a,b)=>b.power-a.power)[0] || ABILITIES[0];
const mult = typeMult(ab.aff, target.affinity);
const dmg = Math.max(1, Math.round((0.4*enemy.level*enemy.atk/target.def)*mult*(ab.power/100)*(0.9+Math.random()*0.2)));
let barDeflected = false;
if(target.status && target.status.find(s=>s.name==='Barrier')){ barDeflected = true; target.status = target.status.filter(s=>s.name!=='Barrier'); }
const finalDmg = barDeflected ? 0 : dmg;
if(!barDeflected) target.hp -= finalDmg;
let msg = `${enemy.name} used ${ab.name}! `;
if(barDeflected) msg += `<span class="line-eff">Blocked by Barrier!</span>`;
else msg += `<span class="line-dmg">${finalDmg} damage</span> to ${target.name}!`;
logLine(msg);
if(finalDmg>0 && ab.effect==='burn' && target.hp>0) applyBurn(target);
if(finalDmg>0 && ab.effect==='frost' && target.hp>0) applyFrost(target);
if(target.hp <= 0) logLine(`${target.name} fell!`);
setTimeout(()=>{
if(!battleState.enemies.some(e=>e.hp>0)){ endBattle('win'); } else if(!battleState.allies.some(a=>a.hp>0)){ endBattle('lose'); } else { battleState.turnIndex = (battleState.turnIndex+1) % battleState.allies.length; renderBattle(); }
}, 600);
}
function endTurn(){ battleState.turnIndex = (battleState.turnIndex+1) % battleState.allies.length; if(battleState.allies[battleState.turnIndex].hp<=0) endTurn(); else renderBattle(); }
function doSwitch(){ logLine('Switching...'); endTurn(); }
function endBattle(result){
setTimeout(()=>{
if(result==='win'){
const zone = ZONES[Math.min(state.zone-1, ZONES.length-1)];
const exp = 15 + state.zone*8 + Math.floor(Math.random()*10);
state.party.forEach(p=>{ p.xp += exp; if(p.xp >= p.level*30){ p.level = Math.min(50, p.level+1); p.xp = p.xp - (p.level-1)*30; p.hp = Math.round(p.baseHP + p.level*5); p.atk = Math.round(p.baseATK + p.level*2); p.def = Math.round(p.baseDEF + p.level*1.5); p.spd = Math.round(p.baseSPD + p.level*1); p.abilities = p.learnset.filter(a=>!a.learnAt || p.level>=a.learnAt).map(a=>a.name); logLine(`${p.name} reached level ${p.level}!`); } });
logLine('Victory! The realm resonates.');
const captured = Math.random() < 0.35 ? true : false;
if(captured && battleState.enemies[0]){
const enc = battleState.enemies[0]; state.creaturesSeen[enc.id] = (state.creaturesSeen[enc.id]||0)+1; state.party.push(JSON.parse(JSON.stringify(enc))); state.party[state.party.length-1].level = 1; state.party[state.party.length-1].hp = Math.round(enc.baseHP); logLine(`Bound ${enc.name}! Added to party.`);
}
saveToStorage();
}
openScreen('overworld');
renderWorldLoop();
}, 1200);
}
// ============================================
// MENU / TEAM / ENC / SHOP
// ============================================
function openScreen(name){
document.getElementById('screen-battle').classList.add('hidden');
document.getElementById('screen-menu').classList.add('hidden');
document.getElementById('screen-enc').classList.add('hidden');
document.getElementById('screen-shop').classList.add('hidden');
document.getElementById('screen-map').classList.add('hidden');
document.getElementById('screen-team').classList.add('hidden');
if(name==='battle') document.getElementById('screen-battle').classList.remove('hidden');
if(name==='menu') document.getElementById('screen-menu').classList.remove('hidden');
if(name==='enc') { renderEnc(); document.getElementById('screen-enc').classList.remove('hidden'); }
if(name==='map') document.getElementById('screen-map').classList.remove('hidden');
else document.getElementById('screen-map').classList.add('hidden');
if(name==='shop') { renderShop(); document.getElementById('screen-shop').classList.remove('hidden'); }
if(name==='overworld') document.getElementById('screen-overworld').style.display='flex';
}
function closeMenu(){ openScreen('overworld'); }
function openTeam(){ openScreen('team'); renderTeam(); }
function renderTeam(){
const list = document.getElementById('team-list');
list.innerHTML = state.party.map(p=>{
const abText = p.abilities.map(a=>`<div style="font-size:0.7rem; color:var(--text-dim);">${a}</div>`).join('');
return `<div class="menu-row"><div class="label"><strong style="color:var(--gold);">${p.name}</strong><br><span style="font-size:0.75rem; color:var(--text-dim);">${typeName(p.affinity)} · Lv.${p.level}</span></div><div style="text-align:right;"><div style="font-size:0.75rem; color:var(--text);">HP ${Math.round(p.hp)}/${Math.round(p.baseHP + (p.level-1)*6)}</div><div style="font-size:0.75rem; color:var(--text);">ATK ${p.atk} · DEF ${p.def} · SPD ${p.spd}</div></div></div>`;
}).join('');
}
function renderEnc(){
const content = document.getElementById('enc-content');
content.innerHTML = CREATURES.map(c=>{
const seen = state.creaturesSeen[c.id] ? 'Captured' : 'Unknown';
return `<div class="enc-card" onclick="showCreatureInfo('${c.id}')"><div class="icon">${c.name[0]}</div><div class="name">${c.name}</div><div class="catch" style="color:${state.creaturesSeen[c.id]?'var(--mint)':'var(--text-dim)'};">${seen}</div></div>`;
}).join('');
}
function showCreatureInfo(id){
const c = CREATURES.find(x=>x.id===id);
if(!c) return;
showDialog('Encyclopedia', `<strong style="color:var(--gold);">${c.name}</strong> — ${typeName(c.affinity)}<br><br><span style="font-size:0.85rem; color:var(--text-dim);">${c.lore}</span><br><br><span style="font-size:0.75rem; color:var(--text);">Stats: HP ${c.baseHP} · ATK ${c.baseATK} · DEF ${c.baseDEF} · SPD ${c.baseSPD}</span>`);
}
function renderShop(){
const list = document.getElementById('shop-list');
list.innerHTML = [
{name:'Restoration Elixir', price:50, desc:'Restores 60% HP', type:'heal'},
{name:'Catalyst Orb', price:120, desc:'Used for transformations', type:'catalyst'},
{name:'Revive Stone', price:200, desc:'Revives fallen creature', type:'revive'},
].map(i=>`<div class="menu-row"><div class="label"><strong style="font-size:0.85rem;">${i.name}</strong><br><span style="font-size:0.7rem; color:var(--text-dim);">${i.desc}</span></div><div style="text-align:right;"><span style="font-size:0.85rem; color:var(--gold);">${i.price} Gold</span><br><button class="btn-main" style="margin-top:4px; font-size:0.7rem; padding:0.3rem 0.6rem;" onclick="buyItem('${i.type}', ${i.price})">Buy</button></div></div>`).join('');
}
function buyItem(type, price){ if(state.inventory.gold >= price){ state.inventory.gold -= price; state.inventory[type] = (state.inventory[type]||0)+1; logLine(`Purchased ${type}!`); saveToStorage(); renderShop(); } else { logLine('Not enough gold.'); } }
function restParty(){
state.party.forEach(p=>{ p.hp = Math.round(p.baseHP + (p.level-1)*6); });
state.shrinesVisited++;
saveToStorage();
logLine('Party fully restored. Game saved.');
openScreen('overworld');
}
function showStory(){
const stages = [
'Stage 1 — The Awakening', 'Stage 2 — The Threshold', 'Stage 3 — The Anchors', 'Stage 4 — The Final Binding'
];
const texts = [
'The Veil of Aetherium was not created; it was remembered. A realm between realities where thought takes form.',
'You have crossed into the deeper weave. The anchors that hold reality together have begun to stir.',
'Each zone holds a Guardian, a creature of pure principle. Only by binding them can you restore the Veil.',
'The final binding awaits in the Void Citadel. Collect 8 unique creatures and defeat 4 zone anchors to complete your journey.'
];
showDialog(stages[Math.min(state.storyStage-1,3)], texts[Math.min(state.storyStage-1,3)]);
}
function showDialog(speaker, text){ document.getElementById('dialog-speaker').textContent = speaker; document.getElementById('dialog-text').innerHTML = text; document.getElementById('dialog-overlay').classList.remove('hidden'); }
function hideDialog(){ document.getElementById('dialog-overlay').classList.add('hidden'); }
// ============================================
// INIT
// ============================================
function startNewGame(){ resetStateForNew(); initCanvas(); openScreen('overworld'); showDialog('The Veil', 'The Aetherium stirs. A new Binding awaits. Move with WASD or arrows.'); }
function openScreenOverworld(){ openScreen('overworld'); }
// Keyboard controls
window.addEventListener('keydown', (e) => {
if(document.getElementById('dialog-overlay').classList.contains('hidden') === false) return;
if(document.getElementById('screen-battle').classList.contains('hidden') === false) return;
if(['ArrowUp','w','W'].includes(e.key)){ moveDir(0,-1); e.preventDefault(); }
else if(['ArrowDown','s','S'].includes(e.key)){ moveDir(0,1); e.preventDefault(); }
else if(['ArrowLeft','a','A'].includes(e.key)){ moveDir(-1,0); e.preventDefault(); }
else if(['ArrowRight','d','D'].includes(e.key)){ moveDir(1,0); e.preventDefault(); }
else if(e.key==='Escape' || e.key==='Enter'){ openScreen('menu'); e.preventDefault(); }
else if(e.key==='m'){ openScreen('map'); e.preventDefault(); }
else if(e.key==='e'){ openScreen('enc'); e.preventDefault(); }
else if(e.key==='t'){ openScreen('team'); e.preventDefault(); }
});
// Initialize title
window.addEventListener('load', () => {
initAmbient();
initCanvas();
if(!localStorage.getItem('aetherium_save')){
document.getElementById('screen-title').classList.remove('hidden');
} else {
loadContinue();
}
});
function initAmbient(){
const container = document.getElementById('bg-ambient');
for(let i=0;i<60;i++){
const s = document.createElement('div');
s.className = 'ambient-star';
s.style.left = Math.random()*100 + '%';
s.style.top = Math.random()*100 + '%';
s.style.width = (2 + Math.random()*3) + 'px';
s.style.height = s.style.width;
s.style.animationDuration = (3 + Math.random()*6) + 's';
s.style.animationDelay = (Math.random()*5) + 's';
container.appendChild(s);
}
}
// Initialize everything
initAmbient();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingmachines/inkling","provider":"openrouter","persona":"full","sessionId":"61c9041a-b644-4dd1-bf70-7863f6f8e162","tokensIn":82550,"tokensOut":21544,"tokensTotal":104094,"turns":4,"toolCalls":3,"failedToolCalls":0,"timestamp":"2026-07-18T22:06:37.169Z"} -->