Creature Collection
GPT 6 Luna · full

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 52,432 bytes · SHA-256 43d5e4981697
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"><title>VESPERLOOM — The Bound Wilds</title>
<style>
:root{--ink:#edf3f1;--muted:#a3b7b7;--glass:rgba(11,24,37,.78);--line:rgba(203,236,226,.19);--mint:#9df2cf;--gold:#ffd890;--rose:#ff9caa;--shadow:0 22px 80px #02081099}*{box-sizing:border-box}html,body{height:100%;margin:0;background:#09131d;color:var(--ink);font:15px/1.45 system-ui,-apple-system,Segoe UI,sans-serif;overflow:hidden}button{font:inherit;color:inherit;border:1px solid var(--line);background:rgba(225,250,239,.075);border-radius:13px;padding:10px 14px;cursor:pointer;transition:.2s}button:hover:not(:disabled){background:rgba(157,242,207,.17);border-color:#9df2cf88;transform:translateY(-1px)}button:disabled{opacity:.35;cursor:not-allowed}.eyebrow{font-size:10px;letter-spacing:.2em;text-transform:uppercase;color:var(--mint);font-weight:750}.title{font:500 clamp(35px,6vw,72px)/.96 Georgia,serif;letter-spacing:.015em}.subtle{color:var(--muted)}#app{width:100%;height:100%;position:relative;background:radial-gradient(ellipse at 52% 25%,#183746 0,#0c1b29 48%,#07101a 100%)}#game{position:absolute;inset:0;display:grid;grid-template-rows:70px 1fr 74px;opacity:0;transition:opacity .7s}#game.ready{opacity:1}.topbar{display:flex;align-items:center;justify-content:space-between;padding:10px 22px;border-bottom:1px solid #c5e7d51b;background:#07131dbc;backdrop-filter:blur(15px);z-index:2}.brand{font:20px Georgia,serif;letter-spacing:.12em}.topright{display:flex;gap:8px;align-items:center}.pill{font-size:12px;color:var(--muted);padding:7px 11px;border:1px solid var(--line);border-radius:99px}.layout{display:grid;grid-template-columns:minmax(0,1fr) 285px;gap:16px;padding:16px 18px;min-height:0}.scene-wrap{position:relative;min-height:0;border:1px solid #d6f7e329;border-radius:23px;overflow:hidden;box-shadow:var(--shadow);background:#102332}.scene-wrap canvas{width:100%;height:100%;display:block}.scene-label{position:absolute;top:18px;left:20px;text-shadow:0 2px 14px #000;pointer-events:none}.scene-label strong{display:block;font:26px Georgia,serif}.scene-label span{font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:#d4e8df}.toast{position:absolute;left:50%;bottom:18px;transform:translateX(-50%);padding:9px 16px;background:#081623d9;border:1px solid var(--line);border-radius:99px;white-space:nowrap;opacity:0;transition:.25s;pointer-events:none}.toast.on{opacity:1}.side{display:flex;flex-direction:column;gap:12px;min-height:0}.card{background:var(--glass);border:1px solid var(--line);border-radius:18px;padding:15px;backdrop-filter:blur(14px);box-shadow:0 12px 40px #02081136}.profile{display:flex;align-items:center;gap:12px}.sigil{width:48px;height:48px;border-radius:15px;background:linear-gradient(135deg,#9df2cf33,#c99bff2a);display:grid;place-items:center;font-size:25px;border:1px solid #d5f5e92b}.profile h3{margin:0;font-size:16px}.profile small{color:var(--muted)}.bar{height:7px;border-radius:9px;background:#ffffff17;overflow:hidden;margin-top:7px}.bar i{display:block;height:100%;border-radius:9px;transition:width .5s}.partylist{display:grid;gap:9px;margin-top:12px}.partymini{display:grid;grid-template-columns:28px 1fr auto;align-items:center;gap:8px;font-size:12px}.partymini .bar{grid-column:2/4;margin-top:-3px;height:4px}.partymini span:nth-child(2){white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.actions{display:grid;grid-template-columns:1fr 1fr;gap:8px}.actions button{font-size:12px;padding:10px 7px}.objective{font:18px Georgia,serif;margin:7px 0}.questcopy{font-size:12px;color:var(--muted)}.footer{display:flex;align-items:center;justify-content:space-between;padding:10px 22px;border-top:1px solid #c5e7d51b;background:#07131dbc}.keys{font-size:11px;color:var(--muted)}.touch{display:none;gap:5px;grid-template-columns:38px 38px 38px;grid-template-rows:32px 32px}.touch button{padding:0;border-radius:9px;font-size:14px}.touch .up{grid-column:2}.touch .left{grid-column:1;grid-row:2}.touch .down{grid-column:2;grid-row:2}.touch .right{grid-column:3;grid-row:2}.overlay{position:fixed;inset:0;z-index:30;display:none;place-items:center;background:#030912b8;backdrop-filter:blur(9px);padding:18px}.overlay.show{display:grid;animation:fade .22s}.panel{width:min(850px,96vw);max-height:90vh;overflow:auto;border:1px solid #d6f7e340;border-radius:24px;background:linear-gradient(145deg,#132b38f5,#0a1724f7);box-shadow:var(--shadow);padding:clamp(20px,4vw,38px);position:relative}.panel.narrow{width:min(560px,96vw)}.panel h2{font:32px Georgia,serif;margin:4px 0 8px}.close{position:absolute;right:16px;top:16px}.gridcards{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:10px;margin:18px 0}.option{border:1px solid var(--line);border-radius:16px;background:#ffffff08;padding:14px;cursor:pointer;transition:.2s}.option:hover{border-color:#9df2cf99;background:#9df2cf12}.option.selected{border-color:var(--mint);box-shadow:inset 0 0 0 1px #9df2cf55}.option h3{font:19px Georgia,serif;margin:3px 0}.option p{font-size:12px;color:var(--muted);margin:5px 0}.tag{display:inline-block;color:var(--mint);font-size:10px;letter-spacing:.1em;text-transform:uppercase}.row{display:flex;gap:9px;flex-wrap:wrap;align-items:center}.row>*{flex:1}.divider{height:1px;background:var(--line);margin:18px 0}.story{font:20px/1.55 Georgia,serif;max-width:660px}.story-vignette{position:fixed;inset:0;z-index:9;display:none;place-items:center;background:radial-gradient(ellipse,#183747dd,#09131df2);padding:22px}.story-vignette.show{display:grid;animation:fade .7s}.battlehud{position:absolute;inset:0;pointer-events:none}.enemybox,.playerbox{position:absolute;padding:12px 15px;border-radius:15px;background:#07131dcc;border:1px solid #d7f4e72e;min-width:190px;backdrop-filter:blur(8px)}.enemybox{top:18px;right:18px}.playerbox{bottom:20px;left:18px}.battlehud .bar{height:8px}.combatlog{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);width:min(370px,45%);text-align:center;background:#091722c9;padding:10px 15px;border-radius:13px;border:1px solid var(--line);font-size:13px}.combatcontrols{position:absolute;right:18px;bottom:20px;width:min(285px,43%);display:grid;grid-template-columns:1fr 1fr;gap:7px;pointer-events:auto}.combatcontrols button{font-size:12px;padding:9px}.capture-gauge{height:12px;background:#ffffff20;border-radius:20px;overflow:hidden;margin:12px 0}.capture-gauge i{display:block;height:100%;width:0;background:linear-gradient(90deg,#9df2cf,#ffe59a);border-radius:20px}.intro{position:fixed;inset:0;z-index:20;display:grid;place-items:center;text-align:center;background:radial-gradient(ellipse at 51% 43%,#214353 0,#102532 32%,#07111c 72%);padding:20px;transition:opacity .8s}.intro.hidden{opacity:0;pointer-events:none}.intro:before,.intro:after{content:"";position:absolute;width:480px;height:480px;border:1px solid #b5ffe31c;border-radius:50%;animation:orbit 22s linear infinite}.intro:after{width:650px;height:650px;animation-direction:reverse}.intro-content{z-index:1}.intro .title{margin:14px 0}.intro p{max-width:490px;color:#bad0cc;margin:16px auto 25px}.intro-buttons{display:flex;justify-content:center;gap:12px}.primary{background:linear-gradient(120deg,#9df2cf,#9edbd4);color:#0a1b20;border:0;font-weight:800;padding:13px 24px}.intro-note{font-size:11px;color:#789390;margin-top:22px}.glow{color:#c5ffe4;text-shadow:0 0 30px #9df2cf99}.floating{position:absolute;font-weight:900;pointer-events:none;animation:floatup .9s forwards;text-shadow:0 2px 9px #000}.creature-card{display:grid;grid-template-columns:38px 1fr auto;gap:9px;align-items:center;padding:10px 0;border-bottom:1px solid #ffffff12}.creature-card small{color:var(--muted)}.mapdots{display:flex;gap:10px;flex-wrap:wrap}.zonechip{padding:10px;border:1px solid var(--line);border-radius:12px;min-width:110px}.zonechip.active{border-color:var(--mint)}.statusline{font-size:11px;color:var(--gold)}@keyframes fade{from{opacity:0}to{opacity:1}}@keyframes orbit{to{transform:rotate(360deg)}}@keyframes floatup{to{opacity:0;transform:translateY(-50px)}}
@media(max-width:760px){#game{grid-template-rows:58px 1fr 68px}.topbar{padding:7px 12px}.layout{grid-template-columns:1fr;padding:9px;gap:8px}.scene-wrap{min-height:0}.side{display:none}.footer{padding:7px 12px}.keys{display:none}.touch{display:grid}.pill{font-size:10px;padding:6px 8px}.brand{font-size:17px}.combatcontrols{bottom:10px;right:10px;width:53%}.playerbox{bottom:126px;left:10px;min-width:150px}.combatlog{bottom:80px;left:10px;transform:none;width:42%;font-size:11px}.enemybox{top:10px;right:10px;min-width:150px}.scene-label{top:12px;left:13px}.scene-label strong{font-size:21px}}
</style></head><body>
<div id="app">
<div class="intro" id="intro"><div class="intro-content"><div class="eyebrow">A field journal from the quiet edge</div><div class="title glow">VESPERLOOM</div><div style="letter-spacing:.28em;font-size:11px;color:#a9c9c2">THE BOUND WILDS</div><p>Beyond the last bell, living light gathers into curious shapes. Walk the six reaches, learn their rhythms, and make a small constellation of your own.</p><div class="intro-buttons"><button class="primary" id="newBtn">New Binding</button><button id="continueBtn">Continue</button></div><div class="intro-note">An original, quiet adventure · WASD / arrows to wander</div></div></div>
<div id="game"><header class="topbar"><div class="brand">VESPER<span style="color:var(--mint)">LOOM</span></div><div class="topright"><span class="pill" id="zonePill">—</span><span class="pill" id="shardPill">◈ 0</span><button id="soundBtn" title="Sound">♫</button><button id="pauseBtn">Ⅱ</button></div></header>
<div class="layout"><div class="scene-wrap"><canvas id="scene"></canvas><div class="scene-label"><span id="stageLabel">THE FIELD</span><strong id="zoneName">—</strong></div><div class="toast" id="toast"></div><div class="battlehud" id="battlehud" style="display:none"><div class="enemybox" id="enemybox"></div><div class="playerbox" id="playerbox"></div><div class="combatlog" id="combatlog">A quiet rustle...</div><div class="combatcontrols" id="combatcontrols"></div></div></div>
<aside class="side"><div class="card" id="leadCard"></div><div class="card"><div class="eyebrow">Your constellation</div><div class="partylist" id="partyList"></div><button style="width:100%;margin-top:12px" onclick="openMenu('team')">Arrange & study team</button></div><div class="card" style="flex:1"><div class="eyebrow">Current thread</div><div class="objective" id="objective">Begin</div><div class="questcopy" id="questcopy"></div><div class="divider"></div><div class="actions"><button onclick="openMenu('map')">✧ Reaches</button><button onclick="openMenu('bag')">▧ Satchel</button><button onclick="openMenu('codex')">⌘ Fieldbook</button><button onclick="openMenu('save')">◉ Save</button></div></div></aside></div>
<footer class="footer"><div class="keys">WASD / ARROWS · E interact · SPACE pause · Reach shrine to mend & save</div><div class="touch"><button class="up" data-dir="up">▲</button><button class="left" data-dir="left">◀</button><button class="down" data-dir="down">▼</button><button class="right" data-dir="right">▶</button></div><button onclick="interact()">E · Listen / Interact</button></footer></div>
<div class="overlay" id="modal"><div class="panel" id="panel"></div></div><div class="story-vignette" id="story"><div class="panel narrow" style="text-align:center"><div class="eyebrow" id="storytag">A THREAD STIRS</div><h2 id="storytitle">—</h2><p class="story" id="storytext"></p><button class="primary" onclick="closeStory()">Carry on</button></div></div>
</div>
<script>
(()=>{'use strict';
const $=s=>document.querySelector(s), canvas=$('#scene'),ctx=canvas.getContext('2d');
const TYPES=['Morrow','Cinder','Brine','Gale','Moss','Prism','Hush'];
const typeColor={Morrow:'#e6bb80',Cinder:'#ff987f',Brine:'#79d9e8',Gale:'#b7d9ff',Moss:'#9ce3a9',Prism:'#e1a7ff',Hush:'#c6c9d4'};
const chart={Morrow:['Brine'],Cinder:['Morrow'],Brine:['Cinder'],Gale:['Morrow','Brine'],Moss:['Gale','Cinder'],Prism:TYPES.filter(x=>x!=='Prism'),Hush:TYPES.filter(x=>x!=='Prism')};
const zones=[
{name:'Liltmeadow',sub:'where the grass remembers',colors:['#173b3b','#287260','#6da77c'],accent:'#b6f4c5',rate:.13,story:'A line of pale seedlights drift against the breeze. They gather around your open palm, as if the valley has been waiting to recognize a new listener.',anchor:'The Bellwether',wild:[0,1,2]},
{name:'Coppershade',sub:'the amber undergrowth',colors:['#392b35','#794b42','#b47755'],accent:'#ffd09a',rate:.17,story:'Copper leaves ring without falling. A far-off pulse answers each footstep; the old paths are waking, one measured beat at a time.',anchor:'The Root-Hum',wild:[3,4,5]},
{name:'Glassmere',sub:'a lake folded into the sky',colors:['#142c49','#276781','#78aeb5'],accent:'#a8edfa',rate:.18,story:'The water holds a second sky beneath its surface. Something vast turns there, slow as a thought, and the shore gives way to your next step.',anchor:'The Stillwake',wild:[6,7,8]},
{name:'Mothlight Fens',sub:'lanterns with no flame',colors:['#242943','#514e73','#a5849a'],accent:'#e4b8ff',rate:.2,story:'The fen-lights dim as you pass, then kindle behind you. The dark is not empty here; it is simply listening from farther away.',anchor:'The Pale Chorus',wild:[9,10,11]},
{name:'Serein Reach',sub:'the high, rainless coast',colors:['#163846','#337487','#94c9bd'],accent:'#b9fff0',rate:.22,story:'Wind combs the headland into silver lines. Across the water, six distant lights now answer the same small rhythm in your chest.',anchor:'The Far Current',wild:[12,13,14]},
{name:'Nightglass Crown',sub:'the seam of the world',colors:['#171b38','#343d74','#7977a8'],accent:'#d5c3ff',rate:.25,story:'At the crown, every path converges—not into an ending, but into a quiet choice. The wilds lean close, ready to be heard without a wall between you.',anchor:'The Unbinding',wild:[15,16,17]}
];
const species=[
['Pipkin','A seed-shell wanderer tucks forgotten dawns beneath its cap.','Moss',42,12,10,13,'✿'],['Emberet','A warm mote curls around the sound of a kettle no one lit.','Cinder',38,15,8,14,'◉'],['Driftle','A small current with a curious eye, it maps every puddle twice.','Brine',45,11,12,10,'◌'],
['Ruskettle','Its bronze fronds store the last color of every sunset.','Morrow',58,16,14,11,'❋'],['Vellwing','A velvet glider that folds moonless skies into its wings.','Gale',47,14,10,19,'⌁'],['Mirekin','It leaves soft green footprints that sprout only in dreams.','Moss',62,14,17,8,'❧'],
['Siltwhorl','A patient spiral carries tiny shorelines wherever it goes.','Brine',55,18,13,13,'◎'],['Tallowfin','It swims through mist, trailing beads of cool light.','Cinder',48,18,11,16,'◈'],['Auralet','A hollow reed-being whose voice makes the lake hold still.','Gale',51,15,12,18,'♫'],
['Gloamuff','It wears a lantern-seed like a borrowed, bashful star.','Hush',68,18,20,9,'☾'],['Thistrel','Its burrs carry gentle messages between distant banks.','Moss',56,22,12,16,'❉'],['Frostleam','A shard of winter that has never learned to be cold.','Prism',52,20,11,20,'✧'],
['Kestrelune','A highland listener that sleeps only while the wind speaks.','Gale',63,21,15,19,'⌇'],['Brindleap','It crosses rainless streams in a single bright bound.','Morrow',72,20,18,14,'⌑'],['Cindervane','Its ember-tail points toward the kindest shelter.','Cinder',58,24,13,20,'⌁'],
['Nocturneel','A ribbon of hush, visible only when the world exhales.','Hush',66,24,16,18,'〰'],['Prismole','It mines the colors hidden in the underside of stones.','Prism',78,22,21,12,'◇'],['Vesperling','A twilight bell with a heartbeat soft enough to share.','Morrow',74,24,18,17,'✺']
];
const forms=[['Pipkin','Pipkin → Hearthcap','Moss',78,24,22,18,'✿'],['Emberet','Emberet → Kindflare','Cinder',72,29,17,22,'◉'],['Driftle','Driftle → Rillcrown','Brine',84,23,24,17,'◌'],['Ruskettle','Ruskettle → Sunbloom','Morrow',98,28,24,19,'❋'],['Vellwing','Vellwing → Dusksail','Gale',82,27,19,28,'⌁'],['Mirekin','Mirekin → Mosswarden','Moss',104,25,29,15,'❧'],['Siltwhorl','Siltwhorl → Shoalcoil','Brine',96,30,23,21,'◎'],['Tallowfin','Tallowfin → Gleamwake','Cinder',89,30,20,25,'◈'],['Auralet','Auralet → Chimewake','Gale',91,28,22,26,'♫']];
const names=['Thimble Sun','Quiet Cinder','Rainthread','Fernstep','Hollow Chime','Bright Rill','Soft Gale','Rooted Hour','Pale Mantle','Kindling Arc','Wintercup','Tideglass','Velvet Rush','Mosslight','Hush Bloom','Comet Stitch','Silver Turn','Dawn Ripple','Glasswing','Murmur Coil','Warm Current','Starling Knot','Sere Bell','Cloud Pollen','Moonseed','Last Lantern','Open Palm','Afterglow','Dapplefall','Bracken Waltz','Echo Spoon','Blue Orchard','Still Ember','Woven Wake','Lantern Drift','Satin Storm','Lilt of Rain','Gentle Orbit','Crown of Mists','Faraway Hum'];
const effects=['burn','frost','shock','speed','heal','multi','priority','regen','barrier','drain'];
const abilities=names.map((n,i)=>({name:n,power: i%8===0?0:9+(i*7%18),acc:82+(i*13%19),type:TYPES[i%7],cost:3+i%5,effect:effects[i%effects.length],desc:''}));
const abilityDesc={burn:'may kindle a lingering burn',frost:'may slow the target',shock:'may interrupt its next action',speed:'quickens the user',heal:'restores a little vitality',multi:'strikes twice',priority:'acts before slower moves',regen:'starts a renewing pulse',barrier:'raises a one-hit veil',drain:'draws vitality from the target'};
abilities.forEach(a=>a.desc=abilityDesc[a.effect]);
const baseStats=s=>({max:Math.floor(s[3]+(s[3]*.12)),atk:s[4],def:s[5],spd:s[6]});
const allSpecies=species.concat(forms.map(f=>[f[1].split(' → ')[1],`A deeper shape, reached when a familiar bond finds its next rhythm.`,f[2],f[3],f[4],f[5],f[6],f[7]]));
let state=null, mode='field', battle=null, particles=[], last=0, worldTime=0, keys={},toastTimer, tutorialStep=0, audioCtx=null, muted=true;
function fresh(idx){let starter=makeCreature(idx,5);return {zone:0,x:2,y:4,party:[starter],box:[],inventory:{'Dewcatch':5,'Glowcatch':1,'Heartseed':3,'Catalyst':0,'Mendleaf':2},shards:12,unlocked:1,anchors:[],encounters:0,steps:0,caught:[idx],seen:[idx],storyNodes:[],tutorial:true,post:false,stage:0,saveZone:0,saveX:2,saveY:4,playtime:0,forms:[],flags:{}}}
function makeCreature(id,lvl=3){const s=species[id]||forms[id-18];let base=species[id]||s, level=Math.min(50,lvl), mult=1+level*.075;let max=Math.floor(base[3]*mult),c={uid:Math.random().toString(36).slice(2,9),id,name:s[0],level,aff:s[2],hp:max,max,atk:Math.floor(s[4]*mult),def:Math.floor(s[5]*mult),spd:Math.floor(s[6]*mult),energy:12,maxEnergy:12,exp:0,status:null,statusN:0,barrier:false,regen:0,abilities:[(id*2)%40,(id*2+1)%40,(id*2+7)%40,(id*2+13)%40],mastery:{}};return c}
function save(){if(!state)return;try{localStorage.setItem('vesperloom-save',JSON.stringify(state));toast('Thread held at the last shrine.')}catch(e){toast('The save could not be held.')}}
function load(){try{let d=JSON.parse(localStorage.getItem('vesperloom-save'));if(d&&d.party?.length){state=d;state.box||=[];state.anchors||=[];state.caught||=[];state.storyNodes||=[];state.inventory||={};state.zone=Math.min(5,state.zone||0);return true}}catch(e){}return false}
function toast(t){let el=$('#toast');el.textContent=t;el.classList.add('on');clearTimeout(toastTimer);toastTimer=setTimeout(()=>el.classList.remove('on'),2400)}
function startGame(newGame){if(newGame){openStarter();return}if(!load()){openStarter();return}enter();}
function openStarter(){modal(`<div class="eyebrow">Choose the first small light</div><h2>Who will walk beside you?</h2><p class="subtle">Three wild-born companions have found the same quiet path.</p><div class="gridcards">${[0,1,2].map(i=>{let s=species[i];return `<div class="option" onclick="chooseStarter(${i})"><div style="font-size:32px;color:${typeColor[s[2]]}">${s[7]}</div><h3>${s[0]}</h3><span class="tag">${s[2]} affinity</span><p>${s[1]}</p></div>`}).join('')}</div><button onclick="closeModal()">Back</button>`)}
window.chooseStarter=i=>{state=fresh(i);closeModal();enter();showStory('A first binding','The little creature does not belong to you. It simply chooses to keep pace. A thread of light forms between your steps—not a tether, but a promise to listen.','FIRST THREAD');tutorialStep=1;};
function enter(){mode='field';$('#intro').classList.add('hidden');$('#game').classList.add('ready');$('#battlehud').style.display='none';renderUI();resize();if(state.tutorial&&tutorialStep===0){showStory('A gentle beginning','Use the arrow keys or WASD to wander. The pale ring ahead is a listening shrine: approach it and press E to rest. A wild shape may answer your footsteps.','FIELD NOTES');tutorialStep=1;}requestAnimationFrame(loop)}
function showStory(title,text,tag='A THREAD STIRS'){ $('#storytitle').textContent=title;$('#storytext').textContent=text;$('#storytag').textContent=tag;$('#story').classList.add('show')}
window.closeStory=()=>{$('#story').classList.remove('show');if(state?.tutorial&&tutorialStep===1){toast('Wander a little. The wilds are listening.');tutorialStep=2}}
function resize(){let r=canvas.getBoundingClientRect(),d=Math.min(devicePixelRatio||1,2);canvas.width=Math.floor(r.width*d);canvas.height=Math.floor(r.height*d);ctx.setTransform(d,0,0,d,0,0)}window.addEventListener('resize',resize);
function renderUI(){if(!state)return;let z=zones[state.zone];$('#zonePill').textContent=z.name;$('#zoneName').textContent=z.name;$('#stageLabel').textContent=state.post?'OPEN ROAM · THREAD '+(state.stage+1):`REACH ${state.zone+1} · ${state.stage<4?'THREAD '+(state.stage+1):'OPEN ROAM'}`;$('#shardPill').textContent='◈ '+state.shards;let c=state.party[0];$('#leadCard').innerHTML=c?`<div class="profile"><div class="sigil" style="color:${typeColor[c.aff]}">${species[c.id]?.[7]||forms[c.id-18]?.[7]||'✦'}</div><div style="flex:1"><h3>${c.name} <small>Lv ${c.level}</small></h3><small>${c.aff} · ${c.hp}/${c.max} vitality</small><div class="bar"><i style="width:${100*c.hp/c.max}%;background:linear-gradient(90deg,#62d8aa,#b8f4c7)"></i></div></div></div>`:'No companion';$('#partyList').innerHTML=state.party.map((p,i)=>`<div class="partymini"><span style="color:${typeColor[p.aff]}">${species[p.id]?.[7]||forms[p.id-18]?.[7]}</span><span>${p.name} · Lv ${p.level}</span><span>${p.hp}/${p.max}</span><div class="bar"><i style="width:${100*p.hp/p.max}%;background:#8de2bd"></i></div></div>`).join('')||'<small>Find a new friend.</small>';let anchors=state.anchors.length;$('#objective').textContent=state.post?'The wilds are yours':'Anchor '+Math.min(anchors+1,4)+' · '+zones[Math.min(anchors,3)].anchor;$('#questcopy').textContent=state.post?'Roam, fill your fieldbook, and discover every shape.':'Reach the shrine at the end of each trail. Four anchors keep the old paths closed.';$('#continueBtn').disabled=!localStorage.getItem('vesperloom-save')}
function loop(t){if(!state)return;let dt=Math.min(.05,(t-last)/1000||0);last=t;worldTime+=dt;if(mode==='field')drawField(dt);else if(mode==='battle')drawBattle(dt);requestAnimationFrame(loop)}
function drawField(dt){let w=canvas.clientWidth,h=canvas.clientHeight,z=zones[state.zone];let g=ctx.createLinearGradient(0,0,w,h);g.addColorStop(0,z.colors[0]);g.addColorStop(.55,z.colors[1]);g.addColorStop(1,z.colors[2]);ctx.fillStyle=g;ctx.fillRect(0,0,w,h);let scale=Math.min(w/22,h/13),ox=(w-scale*14)/2,oy=(h-scale*10)/2; // trail 14 x 10
// atmospheric glows
for(let i=0;i<5;i++){let x=(i*197+worldTime*8)%(w+100)-50,y=h*.15+(i*83%h*.7);let rad=90+i*23;let glow=ctx.createRadialGradient(x,y,0,x,y,rad);glow.addColorStop(0,z.accent+'16');glow.addColorStop(1,z.accent+'00');ctx.fillStyle=glow;ctx.fillRect(x-rad,y-rad,rad*2,rad*2)}
// ground paths and tiles
for(let y=0;y<10;y++)for(let x=0;x<14;x++){let px=ox+x*scale,py=oy+y*scale;let edge=(x===0||y===0||x===13||y===9);ctx.fillStyle=edge?'#06192325':((x+y)%2?'#ffffff07':'#071a2010');ctx.fillRect(px,py,scale,scale);if((x*7+y*11+state.zone*9)%13===0){ctx.fillStyle=z.accent+'45';ctx.beginPath();ctx.arc(px+scale*.5,py+scale*.56,scale*.035,0,7);ctx.fill()}}
// winding path
ctx.strokeStyle='#e9edc222';ctx.lineWidth=scale*.52;ctx.lineCap='round';ctx.beginPath();ctx.moveTo(ox+scale*1.5,oy+scale*5);ctx.bezierCurveTo(ox+scale*4,oy+scale*2,ox+scale*5,oy+scale*8,ox+scale*7,oy+scale*5);ctx.bezierCurveTo(ox+scale*9,oy+scale*2,ox+scale*10,oy+scale*8,ox+scale*12,oy+scale*5);ctx.stroke();
// flowers / luminous particles
for(let i=0;i<30;i++){let px=(i*91+Math.sin(worldTime*.5+i)*12)%w,py=(i*57+Math.cos(worldTime*.4+i)*9)%h;ctx.fillStyle=z.accent+(i%3?'35':'88');ctx.beginPath();ctx.arc(px,py,1.2+i%3,0,7);ctx.fill()}
// shrine and gate
let sx=ox+scale*11.4,sy=oy+scale*4.1;drawShrine(sx,sy,scale,z);if(state.zone<5){ctx.fillStyle='#d3f2dc77';ctx.beginPath();ctx.arc(ox+scale*12.5,oy+scale*5,scale*.24,0,7);ctx.fill();ctx.strokeStyle=z.accent+'aa';ctx.lineWidth=2;ctx.stroke()}
// wild silhouettes
for(let i=0;i<4;i++){let x=ox+scale*(2+i*2.35),y=oy+scale*(2.2+(i%2)*4.5);drawCreatureMark(x,y,scale*.46,z.accent,i,worldTime)}
// player
let px=ox+scale*(state.x+.5),py=oy+scale*(state.y+.5);ctx.fillStyle='#040c1377';ctx.beginPath();ctx.ellipse(px,py+scale*.22,scale*.23,scale*.09,0,0,7);ctx.fill();let a=ctx.createRadialGradient(px,py,1,px,py,scale*.72);a.addColorStop(0,'#f5ffe7bb');a.addColorStop(1,z.accent+'00');ctx.fillStyle=a;ctx.beginPath();ctx.arc(px,py,scale*.72,0,7);ctx.fill();ctx.fillStyle='#f1f3d9';ctx.beginPath();ctx.arc(px,py,scale*.16,0,7);ctx.fill();ctx.fillStyle='#9df2cf';ctx.beginPath();ctx.arc(px,py-scale*.23,scale*.11,0,7);ctx.fill();
}
function drawShrine(x,y,s,z){ctx.save();ctx.translate(x,y);ctx.strokeStyle=z.accent+'bb';ctx.lineWidth=2;ctx.beginPath();ctx.arc(0,0,s*.4,0,Math.PI*2);ctx.stroke();ctx.beginPath();ctx.arc(0,0,s*.27,worldTime*.4,worldTime*.4+Math.PI*1.5);ctx.stroke();ctx.fillStyle='#ecf3d9';ctx.beginPath();ctx.moveTo(0,-s*.24);ctx.lineTo(s*.14,0);ctx.lineTo(0,s*.21);ctx.lineTo(-s*.14,0);ctx.closePath();ctx.fill();ctx.restore()}
function drawCreatureMark(x,y,s,color,i,t){ctx.save();ctx.translate(x,y+Math.sin(t*2+i)*s*.07);ctx.fillStyle=color+'aa';ctx.beginPath();ctx.ellipse(0,0,s*.34,s*.25,0,0,7);ctx.fill();ctx.beginPath();ctx.arc(s*.18,-s*.19,s*.19,0,7);ctx.fill();ctx.fillStyle='#f3ffe8';ctx.beginPath();ctx.arc(s*.25,-s*.2,s*.035,0,7);ctx.fill();ctx.restore()}
function move(dx,dy){if(mode!=='field'||$('#modal').classList.contains('show')||$('#story').classList.contains('show'))return;let nx=state.x+dx,ny=state.y+dy;if(nx<0||nx>13||ny<0||ny>9)return;state.x=nx;state.y=ny;state.steps++;if(nx>=12&&ny>=4&&ny<=6){interact();return}if(nx===11&&ny>=3&&ny<=5){interact();return}let chance=zones[state.zone].rate*(state.post?1.25:1);if(state.tutorial&&tutorialStep===2){tutorialStep=3;wildEncounter();return}if(Math.random()<chance){wildEncounter();return}if(state.steps%7===0)renderUI()}
window.addEventListener('keydown',e=>{if(['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' '].includes(e.key))e.preventDefault();if(e.repeat)return;let k=e.key.toLowerCase();if(['w','arrowup'].includes(k))move(0,-1);if(['s','arrowdown'].includes(k))move(0,1);if(['a','arrowleft'].includes(k))move(-1,0);if(['d','arrowright'].includes(k))move(1,0);if(k==='e'||k==='enter')interact();if(k===' '||k==='escape')openMenu('pause')});document.querySelectorAll('.touch button').forEach(b=>b.addEventListener('click',()=>{let d=b.dataset.dir;move(d==='left'?-1:d==='right'?1:0,d==='up'?-1:d==='down'?1:0)}));
function interact(){if(mode!=='field')return;let near=state.x>=10&&state.y>=3&&state.y<=6;if(!near){if(state.tutorial&&tutorialStep===2){state.tutorial=false;tutorialStep=3;toast('Listen for the little lights in the grass.')}else toast('Only the grass answers here.');return}let zone=state.zone;if(state.zone<state.unlocked){let anchorIndex=state.anchors.length;if(anchorIndex<4&&state.zone===anchorIndex){if(state.zone>0&&state.zone>=state.unlocked){toast('The path is held by an anchor.');return}startBattle(true);return}state.party.forEach(c=>{c.hp=c.max;c.energy=c.maxEnergy;c.status=null;c.barrier=false;c.regen=0});state.shards+=4;state.saveZone=state.zone;state.saveX=state.x;state.saveY=state.y;save();openMenu('shrine');return}if(state.zone<5&&state.zone>=state.unlocked){toast('A quiet barrier holds this reach.');return}openMenu('shrine')}
function wildEncounter(){state.encounters++;let pool=zones[state.zone].wild;let id=pool[Math.floor(Math.random()*pool.length)];let lvl=Math.min(40,3+state.zone*4+Math.floor(state.encounters/3)+(state.post?7:0));let enemies=[makeCreature(id,lvl)];if(Math.random()<.2+state.zone*.025)enemies.push(makeCreature(pool[Math.floor(Math.random()*pool.length)],lvl-1));state.seen.push(id);beginBattle(enemies,false);if(state.tutorial&&tutorialStep>=2){tutorialStep=3;}}
function startBattle(boss){let idx=state.anchors.length;let id=zones[Math.min(idx,5)].wild[2];let lvl=9+idx*5;let e=makeCreature(id,lvl);e.name=zones[Math.min(idx,5)].anchor+' · '+e.name;e.max=Math.floor(e.max*1.4);e.hp=e.max;e.atk=Math.floor(e.atk*1.14);beginBattle([e],true)}
function beginBattle(enemies,boss){mode='battle';battle={enemies,boss,turn:0,log:boss?`${enemies[0].name} stirs beneath the shrine.`:`A wild shape answers your steps.`,phase:'player',captureMeter:0,active:0,shake:0,turnQueue:[],actionBusy:false};$('#battlehud').style.display='block';renderBattleControls();renderBattleHud();}
function renderBattleHud(){if(mode!=='battle')return;let e=battle.enemies[0],p=state.party[battle.active];$('#enemybox').innerHTML=`<div class="eyebrow">${battle.boss?'ANCHOR':'WILD · '+(e.aff)}</div><b>${e.name}</b> <small>Lv ${e.level}</small><div class="bar"><i style="width:${Math.max(0,100*e.hp/e.max)}%;background:linear-gradient(90deg,#ff8196,#ffd08a)"></i></div><small>${Math.max(0,e.hp)} / ${e.max}</small>`;$('#playerbox').innerHTML=p?`<div class="eyebrow">YOUR BINDING</div><b>${p.name}</b> <small>Lv ${p.level}</small><div class="bar"><i style="width:${100*p.hp/p.max}%;background:#8fe8bd"></i></div><small>${p.hp}/${p.max} · ${p.aff}</small>`:'No companion';$('#combatlog').textContent=battle.log}
function renderBattleControls(){if(mode!=='battle')return;let p=state.party[battle.active],html='';if(battle.phase==='player'){p.abilities.forEach((id,j)=>{let a=abilities[id];html+=`<button onclick="actAbility(${j})" ${p.energy<a.cost||battle.actionBusy?'disabled':''}>${a.name} <small>· ${a.cost}E</small></button>`});html+=`<button onclick="tryCapture()" ${battle.boss?'disabled':''}>✦ Bind wild</button><button onclick="openMenu('switch')">⇄ Switch</button><button onclick="openMenu('battlebag')">▧ Satchel</button><button onclick="tryFlee()">↗ Withdraw</button>`}else if(battle.phase==='capture'){html=`<button class="primary" onclick="tapCapture()">✦ Tap the pulse</button><button onclick="finishCapture()">Release</button>`}else html=`<button onclick="finishTurn()">Continue</button>`;$('#combatcontrols').innerHTML=html}
window.actAbility=async j=>{if(mode!=='battle'||battle.actionBusy||battle.phase!=='player')return;let p=state.party[battle.active],a=abilities[p.abilities[j]],e=battle.enemies[0];if(p.energy<a.cost)return;battle.actionBusy=true;p.energy-=a.cost;let order=[p,e].sort((a,b)=>b.spd-a.spd);let first=order[0]===p;if(a.effect==='priority')first=true;if(!first&&e.hp>0){await enemyAct(e,p);if(p.hp<=0){battle.actionBusy=false;checkPartyDown();return}}if(p.hp>0&&e.hp>0)useMove(p,e,a);renderBattleHud();if(e.hp<=0){battle.actionBusy=false;setTimeout(victory,650);return}if(first){await delay(350);if(e.hp>0&&p.hp>0)await enemyAct(e,p)}battle.actionBusy=false;if(p.hp<=0){checkPartyDown();return}if(e.hp<=0){setTimeout(victory,500);return}battle.phase='player';renderBattleHud();renderBattleControls()};
function delay(ms){return new Promise(r=>setTimeout(r,ms))}
async function enemyAct(e,p){battle.phase='enemy';renderBattleControls();if(e.status==='shock'&&Math.random()<.28){battle.log=`${e.name} loses its rhythm.`;e.status=null;renderBattleHud();await delay(450);return}let a=abilities[e.abilities[Math.floor(Math.random()*e.abilities.length)]];if(e.energy<a.cost)a=abilities[1];e.energy=Math.max(0,e.energy-a.cost);useMove(e,p,a);renderBattleHud();await delay(520)}
function effectiveness(a,b){if(a==='Prism')return b==='Hush'?1.6:1.18;if(a==='Hush')return b==='Prism'?1.6:.5;if(a==='Gale')return ['Morrow','Brine'].includes(b)?1.6:1;if(a==='Moss')return ['Gale','Cinder'].includes(b)?1.6:1;if(a==='Morrow')return b==='Brine'?1.6:1;if(a==='Cinder')return b==='Morrow'?1.6:1;if(a==='Brine')return b==='Cinder'?1.6:1;return 1}
function useMove(user,target,a){if(Math.random()*100>a.acc){battle.log=`${user.name}'s ${a.name} slips wide.`;return}let mult=effectiveness(a.type,target.aff),stab=a.type===user.aff?1.25:1,crit=Math.random()<.08?1.5:1;let atk=user.atk*(user.status==='burn'?.5:1);let base=a.power===0?0:(.4*user.level*atk/Math.max(1,target.def)*mult*stab*crit);let dmg=a.power===0?0:Math.max(1,Math.floor(base*a.power/15+Math.random()*3));if(target.barrier){target.barrier=false;dmg=0;battle.log=`A veil deflects ${a.name}!`}else{target.hp=Math.max(0,target.hp-dmg);battle.log=`${user.name} used ${a.name}${crit>1?' — lucid strike!':''}${mult>1?' · resonant!':''} ${dmg?`(${dmg})`:''}`;if(mult>1){battle.shake=7;burst(typeColor[a.type])}if(dmg)floatDamage(dmg,target===battle.enemies[0]);}applyEffect(user,target,a);if(a.effect==='multi'&&target.hp>0){let d2=Math.max(1,Math.floor(dmg*.65));target.hp=Math.max(0,target.hp-d2);battle.log+=` Second ripple: ${d2}.`}if(a.effect==='heal')user.hp=Math.min(user.max,user.hp+Math.floor(user.max*.18));if(a.effect==='drain')user.hp=Math.min(user.max,user.hp+Math.floor(dmg*.35));if(user.mastery[a.name]===undefined)user.mastery[a.name]=0;user.mastery[a.name]++;renderBattleHud()}
function applyEffect(user,target,a){switch(a.effect){case'burn':if(Math.random()<.35){target.status='burn';target.statusN=3}break;case'frost':if(Math.random()<.35){target.status='frost';target.statusN=3}break;case'shock':if(Math.random()<.3){target.status='shock';target.statusN=2}break;case'speed':user.spd+=2;break;case'regen':user.regen=3;break;case'barrier':user.barrier=true;break;}if(target.status)battle.log+=` ${target.name} is ${target.status}.`}
function floatDamage(n,enemy){let b=$('#battlehud'),el=document.createElement('div');el.className='floating';el.textContent='−'+n;el.style.color=enemy?'#ffd294':'#ff9caa';el.style.left=enemy?'75%':'20%';el.style.top=enemy?'27%':'55%';b.append(el);setTimeout(()=>el.remove(),950)}
function burst(color){for(let i=0;i<18;i++)particles.push({x:Math.random()*canvas.clientWidth,y:Math.random()*canvas.clientHeight,vx:(Math.random()-.5)*3,vy:(Math.random()-.5)*3,life:1,color})}
function drawBattle(dt){let w=canvas.clientWidth,h=canvas.clientHeight,z=zones[state.zone];let grad=ctx.createLinearGradient(0,0,w,h);grad.addColorStop(0,z.colors[0]);grad.addColorStop(1,z.colors[2]);ctx.fillStyle=grad;ctx.fillRect(0,0,w,h);for(let i=0;i<18;i++){let x=i*w/17,y=h*.72+Math.sin(i*.8+worldTime)*14;ctx.fillStyle='#06121b44';ctx.beginPath();ctx.ellipse(x,y,30,9,0,0,7);ctx.fill()}let e=battle?.enemies[0],p=state.party[battle?.active||0];if(e){drawCombatCreature(w*.69,h*.43,Math.min(w,h)*.19,e.aff,e.id,worldTime);drawCombatCreature(w*.29,h*.67,Math.min(w,h)*.15,p?.aff||'Morrow',p?.id||0,worldTime+1)}particles=particles.filter(q=>q.life>0);particles.forEach(q=>{q.x+=q.vx;q.y+=q.vy;q.life-=dt*1.4;ctx.globalAlpha=Math.max(0,q.life);ctx.fillStyle=q.color;ctx.beginPath();ctx.arc(q.x,q.y,3,0,7);ctx.fill()});ctx.globalAlpha=1}
function drawCombatCreature(x,y,s,aff,id,t){ctx.save();ctx.translate(x,y+Math.sin(t*2)*5);let c=typeColor[aff]||'#e9f4d9';let glow=ctx.createRadialGradient(0,0,s*.1,0,0,s*1.5);glow.addColorStop(0,c+'66');glow.addColorStop(1,c+'00');ctx.fillStyle=glow;ctx.beginPath();ctx.arc(0,0,s*1.5,0,7);ctx.fill();ctx.fillStyle=c+'dd';ctx.beginPath();ctx.ellipse(0,0,s*.68,s*.54,0,0,7);ctx.fill();ctx.beginPath();ctx.arc(s*.35,-s*.3,s*.35,0,7);ctx.fill();ctx.fillStyle='#faffef';ctx.beginPath();ctx.arc(s*.49,-s*.33,s*.07,0,7);ctx.fill();ctx.fillStyle='#152632';ctx.beginPath();ctx.arc(s*.51,-s*.33,s*.03,0,7);ctx.fill();for(let i=0;i<3;i++){ctx.strokeStyle=c+'99';ctx.lineWidth=3;ctx.beginPath();ctx.moveTo(-s*.35+i*s*.22,s*.26);ctx.quadraticCurveTo(-s*.4+i*s*.25,s*.7,-s*.25+i*s*.23,s*.64);ctx.stroke()}ctx.restore()}
window.tapCapture=()=>{if(battle.phase!=='capture')return;battle.captureMeter=Math.min(100,battle.captureMeter+17+Math.random()*9);renderBattleControls();let c=$('#combatlog');c.innerHTML=`Hold at the pulse — ${Math.floor(battle.captureMeter)}%<div class="capture-gauge"><i style="width:${battle.captureMeter}%"></i></div>`;if(battle.captureMeter>=100)finishCapture()};window.finishCapture=()=>{if(!battle||battle.phase!=='capture')return;let item=battle.captureItem||'Dewcatch', quality=item==='Glowcatch'?1.25:item==='Mooncatch'?1.65:1;let enemy=battle.enemies[0],chance=Math.min(.98,(.22+.55*(1-enemy.hp/enemy.max))*quality*(.45+.55*battle.captureMeter/100));if(Math.random()<chance){enemy.hp=enemy.hp;let c=makeCreature(enemy.id,enemy.level);c.hp=Math.max(1,Math.floor(c.max*enemy.hp/enemy.max));if(state.party.length<6)state.party.push(c);else state.box.push(c);state.caught.push(c.id);state.inventory[item]--;battle.log=`${c.name} settles into a ${item}.`;if(state.tutorial){state.tutorial=false;showStory('A second rhythm','The light does not close around the creature. It opens a little place beside yours. Your fieldbook has begun.', 'FIRST COLLECTION');}setTimeout(()=>endBattle(true),800)}else{battle.log='The light unthreads. The wild one stays.';battle.phase='player';renderBattleHud();renderBattleControls()}};
window.tryCapture=()=>{if(!battle||battle.phase!=='player')return;let e=battle.enemies[0];if(e.hp/e.max>.88){toast('So much wildness still shines in it.');return}let item=state.inventory.Glowcatch>0?'Glowcatch':'Dewcatch';if(!state.inventory[item]){toast('No collection lights remain.');return}battle.captureItem=item;state.inventory[item]--;battle.phase='capture';battle.captureMeter=0;battle.log='Tap the pulse to steady your light.';renderBattleHud();renderBattleControls()};
window.tryFlee=()=>{if(battle.boss){toast('The anchor holds the path.');return}if(Math.random()<.72){battle.log='You step softly away.';setTimeout(()=>endBattle(false),450)}else{battle.log='The wild blocks the narrow path.';battle.phase='enemy';renderBattleControls();enemyAct(battle.enemies[0],state.party[battle.active]).then(()=>{battle.phase='player';renderBattleControls();renderBattleHud()})}};
function victory(){let e=battle.enemies[0];if(!battle.boss&&e.hp>0)return;let exp=8+e.level*5+(battle.boss?30:0);state.party.forEach(c=>{c.exp+=exp;while(c.exp>=c.level*14&&c.level<50){c.exp-=c.level*14;levelUp(c)}});state.shards+=battle.boss?20:2+Math.floor(e.level/4);battle.log=`The rhythm settles. +${exp} resonance · +${battle.boss?20:2} shards.`;renderBattleHud();if(battle.boss){let idx=state.anchors.length;state.anchors.push(idx);state.unlocked=Math.min(6,Math.max(state.unlocked,idx+2));state.stage++;state.post=state.anchors.length>=4;state.party.forEach(c=>{c.max=Math.floor(c.max*1.05);c.atk++;c.hp=Math.min(c.max,c.hp+Math.floor(c.max*.35))});state.inventory.Catalyst=(state.inventory.Catalyst||0)+1;setTimeout(()=>{let story=zones[idx]?.story||'The whole wild gives a single, slow answer.';endBattle(true);showStory(`${zones[idx]?.anchor||'The Last Anchor'} is quiet`,story,'ANCHOR '+(idx+1)+' · THREAD RELEASED');if(idx===3){state.post=true;toast('Four anchors quiet. The six reaches open to you.')}} ,900);return}setTimeout(()=>endBattle(true),900)}
function levelUp(c){c.level++;let base=species[c.id]||forms[c.id-18];c.max+=Math.max(2,Math.floor(base[3]*.075));c.hp=Math.min(c.max,c.hp+Math.floor(c.max*.35));c.atk+=1;c.def+=1;if(c.level%2===0)c.spd+=1;if(c.level%3===0){let next=(c.id*3+c.level)%40;if(!c.abilities.includes(next))c.abilities[c.level%4]=next}toast(`${c.name} reached level ${c.level}.`);if(c.level>=20&&c.id<9&&state.inventory.Catalyst>0&&!state.forms.includes(c.id))setTimeout(()=>transformPrompt(c),500)}
function transformPrompt(c){if(!confirm(`${c.name} is ready to transform. Spend 1 Catalyst?`))return;state.inventory.Catalyst--;let id=c.id+18,f=forms[c.id];let next=makeCreature(id,c.level);next.uid=c.uid;next.hp=c.hp;next.exp=c.exp;next.abilities=c.abilities;Object.assign(c,next);state.forms.push(id);renderUI();toast(`${c.name} found a deeper shape.`)}
function checkPartyDown(){if(state.party.every(c=>c.hp<=0)){state.party.forEach(c=>{c.hp=Math.max(1,Math.floor(c.max*.35));c.status=null});state.zone=state.saveZone;state.x=state.saveX;state.y=state.saveY;endBattle(false);toast('The shrine calls you back, gently.');return}let next=state.party.findIndex(c=>c.hp>0);if(next>=0){state.party[battle.active].hp=0;state.party[next].hp=Math.max(1,state.party[next].hp);battle.active=next;battle.phase='player';renderBattleHud();renderBattleControls()}}
function endBattle(v){mode='field';battle=null;$('#battlehud').style.display='none';state.party.forEach(c=>{if(c.hp>0)c.energy=Math.min(c.maxEnergy,c.energy+2);if(c.status==='burn')c.hp=Math.max(1,c.hp-2);if(c.regen){c.hp=Math.min(c.max,c.hp+3);c.regen--}if(c.statusN>0&&!--c.statusN)c.status=null});renderUI();if(state.tutorial&&tutorialStep===3&&!v){toast('A collection light is available when a wild creature is weakened.')}}
window.finishTurn=()=>{};
function modal(html){$('#panel').innerHTML=html;$('#modal').classList.add('show')}window.closeModal=()=>$('#modal').classList.remove('show');window.openMenu=kind=>{if(!state)return;if(kind==='pause'){}if(kind==='switch'){let b=state.party.map((c,i)=>`<div class="creature-card"><span style="color:${typeColor[c.aff]}">${species[c.id]?.[7]||forms[c.id-18]?.[7]}</span><div><b>${c.name}</b><br><small>Lv ${c.level} · ${c.hp}/${c.max} vitality</small></div><button onclick="switchTo(${i})" ${c.hp<=0||i===battle.active?'disabled':''}>Choose</button></div>`).join('');modal(`<button class="close" onclick="closeModal()">✕</button><div class="eyebrow">Change your rhythm</div><h2>Choose a companion</h2>${b}`);return}
if(kind==='shrine'){modal(`<button class="close" onclick="closeModal()">✕</button><div class="eyebrow">Resting place · ${zones[state.zone].name}</div><h2>The listening shrine</h2><p class="subtle">A small sanctuary where the path can be held, mended, and provisioned.</p><div class="gridcards"><div class="option" onclick="restAtShrine()"><span class="tag">Free</span><h3>Rest & save</h3><p>Restore all companions and hold your place.</p></div><div class="option" onclick="openMenu('shop')"><span class="tag">Field provisions</span><h3>Browse wares</h3><p>Spend shards on collection lights and remedies.</p></div><div class="option" onclick="advanceZone()"><span class="tag">Path onward</span><h3>Take the next reach</h3><p>${state.zone<5?(state.zone<state.unlocked?'The trail leads onward.':'A barrier still listens for its anchor.'):'Stay among the open wilds.'}</p></div></div>`);return}
if(kind==='shop'){modal(`<button class="close" onclick="openMenu('shrine')">✕</button><div class="eyebrow">Shrine provisioner</div><h2>Quiet wares <small style="font:14px system-ui;color:var(--gold)">◈ ${state.shards}</small></h2><div class="gridcards">${[['Dewcatch','A humble collection light.',4],['Glowcatch','A steadier, brighter binding.',10],['Heartseed','Restore 35% vitality.',5],['Mendleaf','Cures affliction and restores 8 HP.',3],['Catalyst','A shape-change catalyst.',18],['Mooncatch','A finely tuned capture light.',22]].map(x=>`<div class="option" onclick="buyItem('${x[0]}',${x[2]})"><span class="tag">${x[2]} shards</span><h3>${x[0]}</h3><p>${x[1]}</p></div>`).join('')}</div><button onclick="openMenu('shrine')">Back</button>`);return}
if(kind==='team'){modal(`<button class="close" onclick="closeModal()">✕</button><div class="eyebrow">Constellation · ${state.party.length}/6</div><h2>Your companions</h2>${state.party.map((c,i)=>`<div class="creature-card"><span style="font-size:23px;color:${typeColor[c.aff]}">${species[c.id]?.[7]||forms[c.id-18]?.[7]}</span><div><b>${c.name}</b><br><small>Lv ${c.level} · ${c.aff} · HP ${c.hp}/${c.max} · ATK ${c.atk} · DEF ${c.def} · SPD ${c.spd}</small><br><small>${c.abilities.map(id=>abilities[id].name).join(' · ')}</small></div><button onclick="teamAction(${i})">${i===0?'Lead':'Details'}</button></div>`).join('')}${state.box.length?`<p class="subtle">Reserve fieldbook · ${state.box.length} companions</p>`:''}<div class="divider"></div><button onclick="closeModal()">Done</button>`);return}
if(kind==='codex'){let discovered=[...new Set(state.seen.concat(state.caught))];modal(`<button class="close" onclick="closeModal()">✕</button><div class="eyebrow">A record of what answers</div><h2>Fieldbook <small style="font:14px system-ui;color:var(--muted)">${discovered.length}/27 forms</small></h2><div class="gridcards">${allSpecies.map((s,i)=>{let seen=discovered.includes(i)||discovered.includes(i-18);return `<div class="option" style="opacity:${seen?1:.42}"><div style="font-size:25px;color:${typeColor[s[2]]}">${seen?s[7]:'· · ·'}</div><h3>${seen?s[0]:'Unwritten'}</h3><span class="tag">${seen?s[2]+' affinity':'unrecorded'}</span><p>${seen?s[1]:'A shape yet to be met.'}</p></div>`}).join('')}</div><button onclick="closeModal()">Close</button>`);return}
if(kind==='map'){modal(`<button class="close" onclick="closeModal()">✕</button><div class="eyebrow">Six reaches · four anchors</div><h2>The listening map</h2><p class="subtle">A path opens after its anchor releases the thread.</p><div class="mapdots">${zones.map((z,i)=>`<div class="zonechip ${state.zone===i?'active':''}" style="opacity:${i<state.unlocked||state.post?1:.42}"><span style="color:${z.accent}">✧ ${i+1}</span><br><b>${z.name}</b><br><small>${i<state.unlocked||state.post?'Open':`Held · ${zones[Math.min(i-1,3)].anchor}`}</small>${state.anchors.includes(i)?'<br><small style="color:var(--mint)">Anchor quiet</small>':''}</div>`).join('')}</div><div class="divider"></div><div class="eyebrow">Seven affinities</div><p>${TYPES.map(t=>`<span class="tag" style="color:${typeColor[t]};margin-right:9px">${t}</span>`).join('')}</p><p class="subtle" style="font-size:12px">Morrow > Brine > Cinder > Morrow · Gale > Morrow & Brine · Moss > Gale & Cinder · Prism presses all affinities lightly · Hush resists all except Prism.</p><button onclick="closeModal()">Close</button>`);return}
if(kind==='bag'||kind==='battlebag'){modal(`<button class="close" onclick="closeModal()">✕</button><div class="eyebrow">Your satchel</div><h2>Field provisions</h2><div class="gridcards">${Object.entries(state.inventory).map(([n,q])=>`<div class="option"><span class="tag">${q} held</span><h3>${n}</h3><p>${itemDescription(n)}</p>${['Heartseed','Mendleaf'].includes(n)?`<button onclick="useItem('${n}')">Use</button>`:''}${kind==='battlebag'&&['Dewcatch','Glowcatch','Mooncatch'].includes(n)?`<button onclick="captureWith('${n}')">Collect</button>`:''}</div>`).join('')}</div><div class="row"><button onclick="openMenu('team')">Team</button><button onclick="closeModal()">Close</button></div>`);return}
if(kind==='save'){modal(`<button class="close" onclick="closeModal()">✕</button><div class="eyebrow">Keep your thread</div><h2>Save binding?</h2><p class="subtle">Your journey is also held automatically at every shrine.</p><button class="primary" onclick="save();closeModal()">Save now</button> <button onclick="closeModal()">Return</button>`);return}
if(kind==='pause'){modal(`<button class="close" onclick="closeModal()">✕</button><div class="eyebrow">The field can wait</div><h2>Pause</h2><div class="gridcards"><button onclick="openMenu('team')">Constellation</button><button onclick="openMenu('bag')">Satchel</button><button onclick="openMenu('map')">Reaches</button><button onclick="openMenu('codex')">Fieldbook</button><button onclick="save();closeModal()">Save now</button><button onclick="closeModal()">Return to field</button></div>`);return}}
window.switchTo=i=>{if(state.party[i].hp<=0)return;state.party[state.active||0]?.status;let old=state.party[battle.active];battle.active=i;battle.phase='enemy';closeModal();battle.log=`${state.party[i].name} steps forward.`;renderBattleHud();enemyAct(battle.enemies[0],state.party[i]).then(()=>{if(state.party[i].hp<=0)checkPartyDown();else{battle.phase='player';renderBattleHud();renderBattleControls()}})};
window.teamAction=i=>{let c=state.party[i];modal(`<button class="close" onclick="openMenu('team')">✕</button><div class="eyebrow">Companion · level ${c.level}</div><h2>${c.name}</h2><p>${species[c.id]?.[1]||forms[c.id-18]?.[1]}</p><p class="subtle">${c.aff} · HP ${c.hp}/${c.max} · ATK ${c.atk} · DEF ${c.def} · SPD ${c.spd}</p><div class="divider"></div>${c.abilities.map(id=>{let a=abilities[id];return `<div class="creature-card"><span class="tag">${a.type}</span><div><b>${a.name}</b><br><small>Power ${a.power} · Accuracy ${a.acc}% · Energy ${a.cost} · ${a.desc}</small></div><small>${c.mastery[a.name]||0}✦</small></div>`}).join('')}<button onclick="openMenu('team')">Back</button>`)};
window.buyItem=(name,cost)=>{if(state.shards<cost){toast('Not enough shards.');return}state.shards-=cost;state.inventory[name]=(state.inventory[name]||0)+1;renderUI();openMenu('shop');toast(`${name} added to your satchel.`)};
window.restAtShrine=()=>{state.party.forEach(c=>{c.hp=c.max;c.energy=c.maxEnergy;c.status=null;c.statusN=0;c.barrier=false;c.regen=0});state.saveZone=state.zone;state.saveX=state.x;state.saveY=state.y;save();renderUI();closeModal();toast('All threads mended.')};
window.advanceZone=()=>{if(state.zone>=5){closeModal();toast('The whole map is open. Follow any light.');return}if(state.zone>=state.unlocked){toast('The path is held by an anchor.');return}state.zone++;state.x=2;state.y=5;state.stage++;state.storyNodes.push(state.zone);closeModal();renderUI();showStory(zones[state.zone].name,zones[state.zone].story,`REACH ${state.zone+1} · ${zones[state.zone].sub.toUpperCase()}`);};
window.useItem=name=>{if(!state.inventory[name])return;let c=state.party.find(x=>x.hp<x.max||x.status);if(!c){toast('Everyone is already well.');return}if(name==='Heartseed')c.hp=Math.min(c.max,c.hp+Math.floor(c.max*.35));else{c.hp=Math.min(c.max,c.hp+8);c.status=null}state.inventory[name]--;renderUI();if(mode==='battle'){closeModal();battle.phase='enemy';renderBattleControls();enemyAct(battle.enemies[0],state.party[battle.active]).then(()=>{battle.phase='player';renderBattleHud();renderBattleControls()})}else openMenu('bag')};
window.captureWith=name=>{closeModal();if((state.inventory[name]||0)<=0){toast('None left.');return}battle.captureItem=name;battle.phase='capture';battle.captureMeter=0;battle.log='Tap the pulse to steady your light.';renderBattleHud();renderBattleControls()};
function itemDescription(n){return {Dewcatch:'A humble collection light.',Glowcatch:'A bright, steady collection light.',Mooncatch:'A finely tuned collection light.',Heartseed:'Restore 35% vitality to one companion.',Mendleaf:'Restore 8 vitality and clear an affliction.',Catalyst:'At level 20+, transforms one of the first nine companions.',}[n]||'A useful field provision.'}
$('#newBtn').onclick=()=>startGame(true);$('#continueBtn').onclick=()=>startGame(false);$('#pauseBtn').onclick=()=>openMenu('pause');$('#soundBtn').onclick=()=>{muted=!muted;$('#soundBtn').textContent=muted?'♫':'♪';if(!muted)startAudio()};function startAudio(){try{audioCtx=audioCtx||new(window.AudioContext||window.webkitAudioContext)();let o=audioCtx.createOscillator(),g=audioCtx.createGain();o.type='sine';o.frequency.value=110;g.gain.value=muted?0:.012;o.connect(g);g.connect(audioCtx.destination);o.start();setInterval(()=>{if(g&&audioCtx){g.gain.setTargetAtTime(muted?0:.012,audioCtx.currentTime,.5);o.frequency.setTargetAtTime(95+Math.random()*35,audioCtx.currentTime,2)}},3000)}catch(e){}}
})();
</script></body></html>
<!-- agent-meta {"model":"openai/gpt-6-luna","provider":"openrouter","persona":"full","sessionId":"c0a4446e-f731-4faa-9866-af95c0561418","tokensIn":417674,"tokensOut":23912,"tokensTotal":441586,"cost":0.019855125000000005,"turns":16,"toolCalls":15,"failedToolCalls":0,"timestamp":"2026-09-23T02:37:25.917Z"} -->