Creature Collection
GPT 6 Sol · 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 55,571 bytes · SHA-256 0f360da9ddb3
<!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>Vellum Wake</title><style>
:root{--ink:#e9f4f6;--muted:#9ab5bd;--glass:rgba(12,28,39,.83);--line:rgba(185,240,245,.19);--gold:#f4d59b;--cyan:#93ede7;--rose:#ff9cba}*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#07131d;color:var(--ink);font-family:Trebuchet MS,Segoe UI,sans-serif}button{font:inherit;cursor:pointer;color:inherit}#scene{display:block;width:100vw;height:100vh;touch-action:none}#shade{position:fixed;inset:0;pointer-events:none;background:radial-gradient(circle at 50% 42%,transparent 30%,rgba(1,8,19,.5) 100%)}#hud{position:fixed;top:16px;left:18px;right:18px;display:flex;align-items:flex-start;justify-content:space-between;pointer-events:none;gap:10px}#hud>*{pointer-events:auto}.glass{background:var(--glass);backdrop-filter:blur(18px);border:1px solid var(--line);box-shadow:0 12px 45px #0006,inset 0 1px #ffffff12;border-radius:18px}.badge{padding:11px 17px;font-size:13px;letter-spacing:.1em}#zoneLabel{font-weight:bold;color:var(--gold)}#hudRight{display:flex;gap:8px}.smallBtn{border:1px solid var(--line);background:#102b39d9;border-radius:12px;padding:10px 13px;transition:.2s}.smallBtn:hover,.btn:hover{border-color:var(--cyan);background:#26505a;transform:translateY(-2px)}#toast{position:fixed;top:83px;left:50%;transform:translateX(-50%);background:#112b36e8;border:1px solid var(--gold);padding:9px 20px;border-radius:30px;opacity:0;transition:opacity .3s;pointer-events:none;text-align:center;max-width:90vw;z-index:20}#toast.on{opacity:1}#overlay{position:fixed;inset:0;z-index:10;display:flex;align-items:center;justify-content:center;padding:16px;background:rgba(3,10,18,.5);backdrop-filter:blur(7px)}#overlay.hidden{display:none}.panel{width:min(760px,100%);max-height:min(88vh,850px);overflow:auto;padding:27px;background:linear-gradient(135deg,#163340ef,#0a1b2bee);border:1px solid #b8eff04a;box-shadow:0 28px 100px #000a,0 0 80px #62d8d61c;border-radius:24px;animation:appear .3s ease-out}@keyframes appear{from{opacity:0;transform:translateY(15px) scale(.98)}to{opacity:1;transform:none}}h1,h2,h3,p{margin-top:0}h1{font-family:Georgia,serif;font-weight:normal;font-size:clamp(48px,8vw,88px);letter-spacing:.06em;line-height:1;text-shadow:0 0 35px #8af5e888}h2{font-family:Georgia,serif;font-weight:normal;font-size:30px;color:var(--gold);margin-bottom:13px}h3{font-size:15px;letter-spacing:.12em;text-transform:uppercase;color:var(--cyan);margin-bottom:7px}p{line-height:1.6;color:#c5d9dd}.eyebrow{text-transform:uppercase;letter-spacing:.25em;color:var(--cyan);font-size:11px;font-weight:bold}.btn{background:#153744;border:1px solid #76babb66;padding:11px 18px;border-radius:12px;min-height:43px;transition:.2s;text-align:left}.btn.primary{background:linear-gradient(110deg,#c9ad76,#f5dba5);color:#142633;border-color:#fff6;font-weight:bold}.btn:disabled{opacity:.35;cursor:not-allowed;transform:none}.row{display:flex;flex-wrap:wrap;gap:9px;align-items:center}.stack{display:grid;gap:9px}.grid2{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.grid3{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.card{border:1px solid var(--line);background:#ffffff0b;border-radius:14px;padding:13px}.card.active{border-color:var(--gold);background:#f5dba517}.tag{display:inline-block;border:1px solid currentColor;border-radius:50px;padding:3px 8px;font-size:11px;margin-right:4px}.muted{color:var(--muted);font-size:13px}.divider{height:1px;background:var(--line);margin:17px 0}.scroll{max-height:55vh;overflow:auto;padding-right:5px}.bar{height:8px;border-radius:9px;background:#071821;overflow:hidden;margin:7px 0}.fill{height:100%;border-radius:9px;transition:width .55s;background:linear-gradient(90deg,#59c8a9,#b9efd2)}.fill.low{background:linear-gradient(90deg,#d65f7b,#f2a296)}.battle-ui{position:fixed;bottom:15px;left:50%;transform:translateX(-50%);z-index:5;width:min(820px,calc(100vw - 24px));pointer-events:auto}.battle-ui .glass{padding:15px 18px}.battle-top{display:flex;justify-content:space-between;gap:10px;margin-bottom:10px}.combat-log{font-size:14px;min-height:25px;color:#f6e3be;margin-bottom:9px}.moveGrid{display:grid;grid-template-columns:repeat(2,1fr);gap:8px}.move{display:flex;justify-content:space-between;align-items:center;gap:7px}.move small{color:var(--muted)}#touch{position:fixed;left:18px;bottom:20px;display:none;z-index:4;grid-template-columns:repeat(3,52px);gap:4px;touch-action:none}#touch button{height:50px;border:1px solid #a6e7e568;background:#102938b8;border-radius:12px;font-size:21px}#touch button:nth-child(1){grid-column:2}#touch button:nth-child(2){grid-column:1;grid-row:2}#touch button:nth-child(3){grid-column:2;grid-row:2}#touch button:nth-child(4){grid-column:3;grid-row:2}#actionTouch{position:fixed;right:18px;bottom:30px;z-index:4;display:none;border-radius:50%;width:66px;height:66px;background:#cfad76;color:#17303b;border:0;font-weight:bold}#floatLayer{position:fixed;inset:0;pointer-events:none;z-index:12;overflow:hidden}.float{position:absolute;font-weight:bold;font-size:22px;text-shadow:0 2px 8px #000;animation:rise .95s forwards}@keyframes rise{to{transform:translateY(-65px);opacity:0}}.flash{animation:flash .4s}@keyframes flash{50%{filter:brightness(2)}}.shake{animation:shake .3s}@keyframes shake{25%{transform:translateX(-5px)}50%{transform:translateX(5px)}75%{transform:translateX(-3px)}}.titlePanel{text-align:center;width:min(700px,100%);padding:50px 30px;background:radial-gradient(circle at 50% 15%,#27606a99,transparent 60%),linear-gradient(135deg,#102b3de9,#091524f5)}.titlePanel p{max-width:490px;margin:0 auto 27px}.titlePanel .row{justify-content:center}.storyPanel{border-color:#e7d39a99;text-align:center;background:radial-gradient(circle at 50% 20%,#8d6b6a88,transparent 65%),#10232eed}.storyPanel p{font-family:Georgia,serif;font-size:19px}.compact{font-size:12px}.footnote{font-size:12px;color:#9bb8bd;margin-top:14px}@media(max-width:680px){#touch,#actionTouch{display:grid}#hud{top:8px;left:8px;right:8px}.badge{font-size:10px;padding:9px}.smallBtn{padding:9px;font-size:11px}.panel{padding:18px;max-height:86vh}.grid3{grid-template-columns:repeat(2,1fr)}.battle-ui{bottom:6px}.battle-ui .glass{padding:10px}.battle-ui .move{padding:8px;font-size:12px}.battle-top{font-size:12px}}
</style></head><body><canvas id="scene"></canvas><div id="shade"></div><div id="hud"><div class="badge glass" id="zoneLabel"></div><div id="hudRight"><button class="smallBtn" id="mapBtn">◇ Map</button><button class="smallBtn" id="menuBtn">☷ Menu</button><button class="smallBtn" id="muteBtn">♫</button></div></div><div id="toast"></div><div id="battleUI" class="battle-ui"></div><div id="touch"><button data-dir="up">▲</button><button data-dir="left">◀</button><button data-dir="down">▼</button><button data-dir="right">▶</button></div><button id="actionTouch">ACT</button><div id="floatLayer"></div><div id="overlay"></div><script>
'use strict';
const $=s=>document.querySelector(s), canvas=$('#scene'),ctx=canvas.getContext('2d'),overlay=$('#overlay'),bui=$('#battleUI');
const T=['Lume','Murk','Vela','Cairn','Serein','Rift','Hush'];const TC=['#f3cd83','#927cc0','#75dcbb','#e68a79','#81b8f2','#e6a5d9','#c4dadf'];
const zones=[
{name:'The Silt Atrium',grad:['#102e3c','#245e68'],floor:'#346a70',rate:.095,particles:'motes',story:'Where the first unspoken names gather.',wild:[0,1,2,3,4],guardian:0},
{name:'Orchard of Quiet Bells',grad:['#173d3a','#476650'],floor:'#5e8066',rate:.12,particles:'leaves',story:'Every bell here rings only when no one listens.',wild:[2,3,4,5,6,7],guardian:1},
{name:'The Vermilion Lattice',grad:['#412838','#94565b'],floor:'#91636a',rate:.14,particles:'embers',story:'The lattice keeps an old promise folded inside its wires.',wild:[5,6,7,8,9,10],guardian:2},
{name:'Glasswater Shelf',grad:['#1a3457','#377c91'],floor:'#50899b',rate:.15,particles:'rain',story:'A sea held upright by the memory of its shore.',wild:[8,9,10,11,12,13],guardian:3},
{name:'The Pale Interval',grad:['#34344e','#777795'],floor:'#88889c',rate:.17,particles:'ash',story:'Time rests here between two heartbeats.',wild:[11,12,13,14,15,16],guardian:4},
{name:'Crown of Unmaking',grad:['#352a4d','#9b728e'],floor:'#987e9a',rate:.20,particles:'stars',story:'All lost names return as light.',wild:[13,14,15,16,17],guardian:5}];
// Original bestiary: name, affinities, HP/ATK/DEF/SPD, lore, transformation (first nine).
const raw=[
['Orrili',[0],42,14,12,15,'It gathers loose light in the pleats of its paper-thin fins. It never casts the same shadow twice.'],
['Veshkin',[1],48,17,11,12,'A patient listener beneath root and stone. Its hum can be heard only after the rain.'],
['Tavrel',[2],40,13,12,19,'Small currents follow its trailing ribbons. It sleeps standing in the wind.'],
['Brummit',[3],55,15,17,9,'A walking knot of mineral filaments. It stacks pebbles to mark pleasant memories.'],
['Seluun',[4],43,14,13,17,'It traces cold circles on still water. Each circle holds a fleeting reflection of tomorrow.'],
['Drivvi',[5],39,19,10,19,'Its translucent wings bend the horizon at dusk. It is drawn to unfinished journeys.'],
['Hovren',[6],58,12,19,10,'A hollow-bodied wanderer that shelters smaller life. Its footsteps sound like distant doors.'],
['Celmora',[0,2],47,17,13,16,'Lantern seeds drift from its crown at night. None have ever been seen to land.'],
['Nekril',[1,3],52,18,15,11,'Its knuckles are polished by underground streams. It follows the pulse of buried ore.'],
['Yssari',[2,4],42,15,12,21,'It braids dew into bright threads before dawn. Those threads unravel by noon.'],
['Pelvore',[3,5],55,18,16,12,'An echo walks a step behind its shell. No two echoes agree on where it has been.'],
['Avennix',[4,6],49,15,18,15,'It hides among frost-silver reeds. Its soft call is said to calm unsettled waters.'],
['Vorlum',[5,0],43,20,11,19,'A spark glides beneath its clear skin. The spark brightens whenever it discovers a new path.'],
['Osslet',[6,1],60,13,20,10,'It wears a cloak of fallen silence. In crowded places, it becomes nearly invisible.'],
['Rhevin',[0,3],53,18,16,15,'Its branching horns store heat from passing suns. It releases it slowly through winter.'],
['Mirrel',[1,4],46,17,13,22,'Every movement leaves a thin violet afterimage. It tends to revisit places at twilight.'],
['Fenora',[2,5],48,19,12,20,'It folds air into tiny glimmering prisms. Their colors change with its mood.'],
['Qiravel',[6,5],62,19,19,17,'No one knows whether its many eyes are open. It waits at the edge of every unfinished story.'] ];
const formNames=['Orravelle','Veshkarim','Tavrelyn','Brumora','Seluvara','Drivareth','Hovrallis','Celmoriel','Nekrivar'];
const species=raw.map((r,i)=>({id:i,name:r[0],types:r[1],base:r.slice(2,6),lore:r[6],form:i<9?18+i:null,at:16+(i%3)*3,catalyst:i<9?['Dawn Shard','Still Prism','Fold Seed'][i%3]:null}));
for(let i=0;i<9;i++){let o=species[i];species.push({id:18+i,name:formNames[i],types:i%2?[...new Set([...o.types,(i+2)%7])]:o.types,base:o.base.map((v,j)=>v+([20,9,8,5][j])),lore:`A rare unfolding of ${o.name}, its inner pattern now visible. Its presence makes the air seem briefly wider.`,form:null,at:null,catalyst:null})}
// 42 authored techniques; one effect each.
const moveNames=[['Gleam Thread','Sunwrit','Halo Bend','Bright Wound','Morrow Call','Scribe Flare'],['Dusk Needle','Hollow Murmur','Inkfall','Veil Stitch','Umbral Knock','Night Current'],['Wind Filament','Reed Sweep','Sky Knot','Aerial Whorl','Draft Tether','Zephyr Pin'],['Stone Chime','Crag Palm','Ore Spiral','Bastion Pulse','Gravel Choir','Weight of Earth'],['Rain Script','Cool Current','Silver Drizzle','Rime Touch','Tide Hymn','Deepwater Arc'],['Fold Strike','Rift Curl','Elsewhere Flash','Seam Break','Prism Errand','Unbound Line'],['Quiet Touch','Blank Verse','Stillness','Silent Ring','Hush Mantle','Last Echo']];
const effects=['none','burn','heal','priority','regen','barrier','none','shock','drain','frost','weaken','multihit','none','speed','priority','multihit','frost','none','none','barrier','weaken','burn','heal','none','none','heal','regen','frost','speed','multihit','none','shock','priority','drain','barrier','none','none','weaken','barrier','regen','heal','multihit'];
const moves=moveNames.flatMap((arr,t)=>arr.map((name,k)=>({id:t*6+k,name,type:t,power:[40,53,65,45,30,76][k],acc:[98,92,86,96,100,78][k],cost:[4,5,7,5,6,9][k],effect:effects[t*6+k]})));
const items={woven:0,etched:0,stellar:0,mend:0,restore:0,revive:0,'Dawn Shard':0,'Still Prism':0,'Fold Seed':0};
const itemLabels={woven:'Woven Loop',etched:'Etched Loop',stellar:'Astral Loop',mend:'Mendleaf',restore:'Full Dew',revive:'Wake Salt'};
let state=null, battle=null, mode='title', muted=false, audio=null, audioTimer=0, frame=0, lastMove=0, particles=[], msgTimer=0;
const W=20,H=13,SAVE='vellumWakeSave2';
const rand=(a,b)=>Math.floor(Math.random()*(b-a+1))+a, clamp=(n,a,b)=>Math.max(a,Math.min(b,n));
function learnset(id){let t=species[id].types[0];return [[1,t*6],[4,t*6+1],[8,t*6+2],[12,t*6+3],[17,t*6+4],[23,t*6+5],[28,((t+1)%7)*6+1],[36,((t+2)%7)*6+5]]}
function stats(c){let b=species[c.id].base,L=c.lv,blessing=state?.team?.includes(c)?(state.bonuses||0):0;return {max:Math.floor(b[0]+L*3.1+blessing*3),atk:Math.floor(b[1]+L*1.27+c.proficiency*.18+blessing),def:Math.floor(b[2]+L*1.12+blessing),spd:Math.floor(b[3]+L*.82+blessing)}}
function makeCreature(id,lv){let c={id,lv,exp:0,hp:1,known:learnset(id).filter(x=>x[0]<=lv).map(x=>x[1]).slice(-4),pp:{},status:{},proficiency:0};c.hp=stats(c).max;refreshPP(c);return c}
function refreshPP(c){for(let id of c.known)c.pp[id]=Math.max(1,Math.floor(34/moves[id].cost))}
function maxPP(id){return Math.max(1,Math.floor(34/moves[id].cost))}
function newState(starter){battle=null;bui.innerHTML='';state={zone:0,x:3,y:7,shrine:{zone:0,x:3,y:7},unlocked:0,anchors:[],story:0,team:[makeCreature(starter,5)],reserve:[],seen:[starter],caught:[starter],items:{...items,woven:7,mend:4,restore:1},credits:90,steps:0,post:false,tutorial:0,battles:0,bonuses:0};save();showStory('THE FIRST THREAD','The world is losing its edges. The Wardens of the six intervals wait beyond the quiet paths. Bind wandering echoes and let the world remember its shape.',()=>tutorial(0))}
function save(){if(state){try{localStorage.setItem(SAVE,JSON.stringify(state))}catch(e){}}}
function load(){try{let s=JSON.parse(localStorage.getItem(SAVE));if(!s||!s.team?.length)return false;state=s;battle=null;bui.innerHTML='';state.zone=state.shrine.zone;state.x=state.shrine.x;state.y=state.shrine.y;state.team.forEach(fixCreature);state.reserve.forEach(fixCreature);mode='world';closeOverlay();toast('Returned to the last sanctuary.');return true}catch(e){return false}}
function fixCreature(c){c.status={};c.pp=c.pp||{};c.known=c.known||learnset(c.id).filter(x=>x[0]<=c.lv).map(x=>x[1]).slice(-4);c.proficiency??=0;c.hp=clamp(c.hp,0,stats(c).max);for(let m of c.known)c.pp[m]??=maxPP(m)}
function overlayHTML(html,cls=''){overlay.className='';overlay.innerHTML=`<div class="panel ${cls}">${html}</div>`}function closeOverlay(){overlay.className='hidden';overlay.innerHTML=''}function btn(text,fn,klass=''){return `<button class="btn ${klass}" onclick="${fn}">${text}</button>`}function toast(s){let e=$('#toast');e.textContent=s;e.classList.add('on');clearTimeout(msgTimer);msgTimer=setTimeout(()=>e.classList.remove('on'),2900)}
function showStory(title,text,next){mode='story';overlayHTML(`<div class="eyebrow">A memory takes shape</div><h2>${title}</h2><p>${text}</p><button class="btn primary" id="storyNext">Continue →</button>`,'storyPanel');$('#storyNext').onclick=next;chime(400, .3)}
function title(){battle=null;bui.innerHTML='';mode='title';overlayHTML(`<div class="eyebrow">A world written in echoes</div><h1>Vellum<br>Wake</h1><p>In the intervals between places, small lives hold the world together. Find them. Remember them. Wake the sixfold path.</p><div class="row">${btn('New Binding','chooseStarter()','primary')}${btn('Continue','continueGame()',localStorage.getItem(SAVE)?'':'') }</div><div class="footnote">Move: WASD / arrows · Interact: E / Space · Menu: Esc · Sound: ♫</div>`,'titlePanel')}
function continueGame(){if(!load())toast('No saved binding yet. Begin a new one.')}function chooseStarter(){mode='menu';overlayHTML(`<div class="eyebrow">The first binding</div><h2>Choose a companion</h2><p>Three echoes wait beside the sleeping sanctuary. Who will walk with you?</p><div class="grid3">${[0,1,2].map(i=>`<div class="stack">${creatureCard(makeCreature(i,5),true)}<button class="btn primary" onclick="newState(${i})">Bind ${species[i].name}</button></div>`).join('')}</div>`)}
function tutorial(n){if(!state)return;state.tutorial=n;mode='world';if(n===0){overlayHTML(`<div class="eyebrow">A gentle beginning · 1 / 3</div><h2>Follow the path</h2><p>Move with WASD or the arrow keys. Walk to the glowing marker to the east. Press E or Space near a sanctuary to rest and save.</p>${btn('Step into the Atrium','closeOverlay()','primary')}`)}else if(n===1){overlayHTML(`<div class="eyebrow">A gentle beginning · 2 / 3</div><h2>Listen, then act</h2><p>Wild echoes cross your path. In a clash, choose one of four techniques. Speed decides who acts first. Weaken an echo to make a binding easier.</p>${btn('Meet the echo',"startBattle('tutorial')",'primary')}`)}else if(n===2){mode='battle';overlayHTML(`<div class="eyebrow">A gentle beginning · 3 / 3</div><h2>Make a binding</h2><p>Choose a Loop in battle. Tap when the luminous pulse overlaps the center mark. Low vitality, better Loops, and good timing improve the binding.</p>${btn('Try a Loop','resumeGame()','primary')}`);if(battle){battle.promptCapture=true;renderBattleUI()}}else{state.tutorial=3;closeOverlay();toast('Your path is your own now. Seek the glowing anchor.');save()}}
function creatureCard(c,short=false){let sp=species[c.id],st=stats(c);return `<div class="card"><strong style="color:${TC[sp.types[0]]}">${sp.name}</strong> <span class="muted">Lv ${c.lv}</span><div class="muted">${sp.types.map(t=>`<span class="tag" style="color:${TC[t]}">${T[t]}</span>`).join('')}</div><div class="bar"><div class="fill ${c.hp/st.max<.3?'low':''}" style="width:${100*c.hp/st.max}%"></div></div><div class="muted">${c.hp}/${st.max} vitality · ${st.atk} force · ${st.def} guard · ${st.spd} tempo</div>${short?`<p class="compact" style="margin:8px 0 0">${sp.lore}</p>`:''}</div>`}
function typeMultiplier(atk,def){if(atk===def)return 1;let m=1;for(let d of def){let x=1;if(atk===0&&d===1||atk===1&&d===2||atk===2&&d===0)x=1.5;else if(d===0&&atk===1||d===1&&atk===2||d===2&&atk===0)x=.75;if(atk===3&&(d===0||d===2))x=1.5;if(atk===4&&(d===3||d===1))x=1.5;if(atk===5&&d!==5)x=1.16;if(d===6&&atk!==5)x=.73;if(d===6&&atk===5)x=1.5;if(atk===6&&d!==5&&d!==6)x=.82;m*=x}return clamp(m,.45,2.25)}
function openMenu(tab='main'){if(mode==='battle'&&battle?.busy)return;let prev=mode;mode='menu';let nav=`<div class="row" style="margin-bottom:17px">${[['main','Pause'],['team','Team'],['bag','Bag'],['atlas','Lexicon'],['map','Map']].map(([k,v])=>btn(v,`openMenu('${k}')`,k===tab?'primary':'')).join('')}</div>`;let body='';if(tab==='main')body=`<h2>Between breaths</h2><p>Four anchors must fall. Eight unique echoes must join your binding. Sanctuary rest restores your team and records your journey.</p><div class="grid2"><div class="card">Anchors stilled: <b>${state.anchors.length}/4</b><br>Unique bindings: <b>${state.caught.length}/8</b><br>Intervals open: <b>${state.unlocked+1}/6</b></div><div class="card">Shards: <b>${state.credits}</b><br>Current interval: <b>${zones[state.zone].name}</b><br>${state.post?'The world is awake. Wild echoes have grown stronger.':'The path is still unfolding.'}</div></div><div class="divider"></div><div class="row">${btn('Resume','resumeGame()','primary')}${btn('Save binding','manualSave()')}${btn('Controls','showControls()')}${btn('Title screen','title()')}</div>`;
if(tab==='team')body=`<h2>Your binding <span class="muted">${state.team.length}/6</span></h2><div class="scroll stack">${state.team.map((c,i)=>`<div>${creatureCard(c,true)}<div class="row" style="margin:7px 0">${btn('Techniques',`moveEditor(${i})`)}${btn('Unfold',`transform(${i})`,c.id>=18?'':'')}${btn('Store',`storeCreature(${i})`)}${i>0?btn('Lead',`leadCreature(${i})`):''}</div></div>`).join('')}${state.reserve.length?`<h3>Sanctuary reserve (${state.reserve.length})</h3>${state.reserve.map((c,i)=>`<div>${creatureCard(c)}${btn('Join team',`retrieveCreature(${i})`)}</div>`).join('')}`:''}</div>`;
if(tab==='bag')body=`<h2>Satchel</h2><p>Loops bind wild echoes. Catalysts unfold a companion once it reaches its threshold.</p><div class="grid2">${Object.keys(state.items).map(k=>`<div class="card"><b>${itemLabels[k]||k}</b> × ${state.items[k]}<div class="muted">${itemDescription(k)}</div>${['mend','restore','revive'].includes(k)?btn('Use',`chooseItemTarget('${k}')`):''}</div>`).join('')}</div>`;
if(tab==='atlas')body=`<h2>Lexicon <span class="muted">${state.caught.length}/27 bound · ${state.seen.length}/27 witnessed</span></h2><div class="scroll grid2">${species.map((s,i)=>`<div class="card"><strong style="color:${TC[s.types[0]]}">${state.seen.includes(i)?s.name:'Unwritten #'+(i+1)}</strong><div class="muted">${s.types.map(t=>T[t]).join(' / ')}</div><p class="compact">${state.seen.includes(i)?s.lore:'Its shape has not yet entered your memory.'}</p>${state.caught.includes(i)?'<span class="tag">BOUND</span>':''}</div>`).join('')}</div>`;
if(tab==='map')body=`<h2>The six intervals</h2><p>Cross the eastern threshold after stilling the local Warden. You may always travel back west.</p><div class="stack">${zones.map((z,i)=>`<div class="card ${i===state.zone?'active':''}"><span style="color:${TC[i%7]}">◆</span> <b>${i+1}. ${z.name}</b> <span class="muted">${i>state.unlocked?'· sealed':state.anchors.includes(i)?'· Warden stilled':i===state.zone?'· here':'· open'}</span><div class="muted">${z.story}</div>${i<=state.unlocked?btn('Travel',`travel(${i})`):''}</div>`).join('')}</div>`;
overlayHTML(nav+body+`<div class="divider"></div>${btn('Close','resumeGame()')}`)}
function resumeGame(){closeOverlay();mode=battle?'battle':'world';if(battle)renderBattleUI()}function manualSave(){save();toast('Binding recorded.')}function showControls(){overlayHTML(`<h2>Ways of the Wake</h2><p>WASD / arrows: move · E / Space: interact · Esc: pause · Touch: directional pad and ACT. Walk on luminous stones to challenge a Warden. Sanctuary tiles heal, save, and offer supplies. On the eastern edge, move onward after defeating the Warden.</p>${btn('Back','openMenu()','primary')}`)}function travel(i){state.zone=i;state.x=3;state.y=7;mode='world';closeOverlay();toast(zones[i].name);save()}
function itemDescription(k){return {woven:'Basic binding loop.',etched:'Stronger binding loop.',stellar:'Rare high-quality binding loop.',mend:'Restores 50 vitality.',restore:'Restores full vitality and technique energy.',revive:'Revives with half vitality.','Dawn Shard':'Unfolds radiant patterns.','Still Prism':'Unfolds quiet patterns.','Fold Seed':'Unfolds shifting patterns.'}[k]}
function leadCreature(i){state.team.unshift(state.team.splice(i,1)[0]);openMenu('team')}function storeCreature(i){if(state.team.length<=1)return toast('One companion must remain with you.');state.reserve.push(state.team.splice(i,1)[0]);openMenu('team')}function retrieveCreature(i){if(state.team.length>=6)return toast('Your binding holds only six.');state.team.push(state.reserve.splice(i,1)[0]);openMenu('team')}
function moveEditor(i){let c=state.team[i],pool=learnset(c.id).filter(x=>x[0]<=c.lv).map(x=>x[1]);overlayHTML(`<h2>${species[c.id].name} · techniques</h2><p>Select a learned technique to equip it. Four can be carried at once; selecting a fifth replaces the oldest equipped technique.</p><div class="stack">${pool.map(id=>`<button class="btn" onclick="toggleMove(${i},${id})"><b>${moves[id].name}</b> · ${T[moves[id].type]} · ${moves[id].power} power · ${moves[id].effect}<span style="float:right">${c.known.includes(id)?'✓ EQUIPPED':'Equip'}</span></button>`).join('')}</div><div class="divider"></div>${btn('Back','openMenu(\'team\')')}`)}function toggleMove(i,id){let c=state.team[i];if(c.known.includes(id)){if(c.known.length<=1)return toast('Keep at least one technique.');c.known=c.known.filter(x=>x!==id)}else{if(c.known.length>=4)c.known.shift();c.known.push(id);c.pp[id]=maxPP(id)}moveEditor(i)}
function transform(i){let c=state.team[i],s=species[c.id];if(!s.form)return toast('This pattern has no further unfolding.');if(c.lv<s.at)return toast(`Reach level ${s.at} to unfold.`);if(!state.items[s.catalyst])return toast(`Requires a ${s.catalyst}. Find one at a sanctuary.`);state.items[s.catalyst]--;let old=stats(c).max;c.id=s.form;c.hp+=stats(c).max-old;c.known=learnset(c.id).filter(x=>x[0]<=c.lv).map(x=>x[1]).slice(-4);refreshPP(c);if(!state.caught.includes(c.id))state.caught.push(c.id);if(!state.seen.includes(c.id))state.seen.push(c.id);chime(650,.6);openMenu('team');toast(`${species[c.id].name} unfolded from its old pattern!`);checkGoal()}
function chooseItemTarget(k){if(!state.items[k])return toast('None left.');overlayHTML(`<h2>Use ${itemLabels[k]}</h2><div class="stack">${state.team.map((c,i)=>`<button class="btn" onclick="useItem('${k}',${i})">${species[c.id].name} · ${c.hp}/${stats(c).max} vitality</button>`).join('')}</div>${btn('Back','openMenu(\'bag\')')}`)}function useItem(k,i,inBattle=false){let c=state.team[i];if(!state.items[k])return;if(k==='revive'&&c.hp>0)return toast('Only for a fallen companion.');if(k!=='revive'&&c.hp<=0)return toast('Use Wake Salt first.');state.items[k]--;if(k==='mend')c.hp=Math.min(stats(c).max,c.hp+50);if(k==='restore'){c.hp=stats(c).max;refreshPP(c);c.status={}}if(k==='revive')c.hp=Math.floor(stats(c).max/2);if(inBattle){closeOverlay();mode='battle';enemyAfterAction(`${species[c.id].name} recovered.`)}else openMenu('bag')}
function shrine(){mode='menu';for(let c of state.team){c.hp=stats(c).max;c.status={};refreshPP(c)}state.shrine={zone:state.zone,x:state.x,y:state.y};save();chime(520,.4);overlayHTML(`<div class="eyebrow">Sanctuary of held breaths</div><h2>Rest in the stillness</h2><p>Your companions are restored. The binding has been recorded. Supplies may be traded for shards earned in battle.</p><div class="row">${btn('Trade supplies','shop()','primary')}${btn('Team','openMenu(\'team\')')}${btn('Continue','resumeGame()')}</div><p class="footnote">Shards: ${state.credits}</p>`)}
const prices={woven:18,etched:44,stellar:95,mend:25,restore:65,revive:80,'Dawn Shard':140,'Still Prism':140,'Fold Seed':140};function shop(){overlayHTML(`<h2>The sanctuary cache</h2><p>Shards: <b>${state.credits}</b></p><div class="grid2">${Object.keys(prices).map(k=>`<button class="btn" onclick="buy('${k}')"><b>${itemLabels[k]||k}</b> <span style="float:right;color:var(--gold)">${prices[k]} ◇</span><div class="muted">Owned: ${state.items[k]} · ${itemDescription(k)}</div></button>`).join('')}</div><div class="divider"></div>${btn('Return','shrine()')}`)}function buy(k){if(state.credits<prices[k])return toast('Not enough shards.');state.credits-=prices[k];state.items[k]++;chime(560,.12);shop();toast(`Received ${itemLabels[k]||k}.`)}
function tileKind(z,x,y){if(x<0||y<0||x>=W||y>=H)return 'wall';if(x===3&&y===7)return 'shrine';if(x===15&&y===5)return 'anchor';if(z===0&&x===7&&y===7)return 'tutorial';if(x===19&&y===7)return 'gate';if(x===0&&y===7)return 'back';let n=(x*17+y*31+z*13+x*y*7)%37;if((x===3&&y>=5&&y<=9)||(y===7)||(x===15&&y>=5&&y<=7))return 'path';return n===0||n===1||n===2?'obstacle':'path'}
function moveWorld(dx,dy){if(mode!=='world'||overlay.className!=='hidden')return;let now=performance.now();if(now-lastMove<105)return;lastMove=now;let nx=state.x+dx,ny=state.y+dy;if(nx<0&&state.zone>0){state.zone--;state.x=18;state.y=7;toast(zones[state.zone].name);return}if(nx>=W){if(state.zone<state.unlocked){state.zone++;state.x=1;state.y=7;toast(zones[state.zone].name)}else toast('The eastern threshold remains sealed.');return}let kind=tileKind(state.zone,nx,ny);if(kind==='wall'||kind==='obstacle')return;state.x=nx;state.y=ny;state.steps++;chime(220+state.zone*20,.025,.018);if(kind==='shrine'){shrine();return}if(kind==='anchor'&&!state.anchors.includes(state.zone)){startBattle('guardian');return}if(kind==='tutorial'&&state.tutorial<3){tutorial(1);return}if(state.tutorial<3)return;if(state.steps%3===0&&Math.random()<zones[state.zone].rate*2.5){startBattle('wild');return}if(state.steps%20===0)save()}
function interact(){if(mode==='battle')return;if(mode!=='world'||overlay.className!=='hidden')return;let x=state.x,y=state.y;if(Math.abs(x-3)+Math.abs(y-7)<=1){shrine();return}if(Math.abs(x-15)+Math.abs(y-5)<=1&&!state.anchors.includes(state.zone)){startBattle('guardian');return}if(state.zone===0&&state.tutorial===0&&Math.abs(x-7)+Math.abs(y-7)<=1){tutorial(1);return}toast('The interval is quiet here.')}
function startBattle(kind){if(battle)return;let z=state.zone,base=5+z*5+(state.post?13:0);let ids=zones[z].wild;let enemies=[];if(kind==='tutorial')enemies=[makeCreature(state.team[0].id===1?0:1,3)];else if(kind==='guardian'){let id=[6,8,10,13,16,17][z];enemies=[makeCreature(id,Math.min(50,base+4))];if(z>=2)enemies.unshift(makeCreature(ids[1],Math.min(50,base)));if(z>=4)enemies.push(makeCreature(ids[2],Math.min(50,base+1)))}else{let count=Math.random()<.16+z*.08?2:1;if(z>=4&&Math.random()<.18)count=3;for(let i=0;i<count;i++)enemies.push(makeCreature(ids[rand(0,ids.length-1)],clamp(base+rand(-2,3),2,50)))}for(let e of enemies)if(!state.seen.includes(e.id))state.seen.push(e.id);battle={kind,enemies,active:state.team.findIndex(c=>c.hp>0),target:0,busy:false,log:kind==='guardian'?'A Warden gathers its echoes.':kind==='tutorial'?'A wandering echo appears. Try a technique!':'Wild echoes gather around you.',round:0,promptCapture:false};mode='battle';closeOverlay();bui.style.display='block';renderBattleUI();chime(310,.25);if(kind==='guardian')toast('WARDEN OF THE INTERVAL')}
function livingEnemies(){return battle.enemies.filter(e=>e.hp>0)}function active(){return state.team[battle.active]}
function renderBattleUI(){if(!battle||mode!=='battle'){bui.innerHTML='';return}if(!livingEnemies().length){bui.innerHTML=`<div class="glass" style="padding:20px">${battle.log}</div>`;return}let a=active(),target=battle.enemies[battle.target];if(!target||target.hp<=0){battle.target=battle.enemies.findIndex(e=>e.hp>0);target=battle.enemies[battle.target]}let s=stats(a);bui.innerHTML=`<div class="glass"><div class="battle-top"><div><strong style="color:${TC[species[a.id].types[0]]}">${species[a.id].name}</strong> <span class="muted">Lv ${a.lv}</span><div class="bar" style="width:min(200px,32vw)"><div class="fill ${a.hp/s.max<.3?'low':''}" style="width:${a.hp/s.max*100}%"></div></div><span class="muted">${a.hp}/${s.max} · ${statusText(a)}</span></div><div style="text-align:right"><span class="eyebrow">${battle.kind==='guardian'?'WARDEN':'WILD'} · ${livingEnemies().length} remaining</span><div class="muted">Target: ${species[target.id].name} Lv ${target.lv}</div></div></div><div class="combat-log">${battle.log}</div><div class="moveGrid">${a.known.map(id=>`<button class="btn move" ${battle.busy||!a.pp[id]?'disabled':''} onclick="playerMove(${id})"><span><b>${moves[id].name}</b><br><small style="color:${TC[moves[id].type]}">${T[moves[id].type]} · ${moves[id].effect}</small></span><small>${a.pp[id]}/${maxPP(id)}</small></button>`).join('')}</div><div class="row" style="margin-top:9px">${btn('◎ Bind', 'captureMenu()')}${btn('↔ Switch','switchMenu()')}${btn('▣ Satchel','battleBag()')}${btn('✧ Breathe','catchBreath()')}${btn('◎ Target','cycleTarget()')}${battle.kind==='wild'?btn('↗ Retreat','fleeBattle()'):''}</div></div>`}
function statusText(c){return Object.keys(c.status).filter(k=>c.status[k]).map(k=>k.toUpperCase()).join(' · ')||'steady'}function cycleTarget(){if(battle.busy)return;let list=battle.enemies.map((e,i)=>e.hp>0?i:-1).filter(x=>x>=0);battle.target=list[(list.indexOf(battle.target)+1)%list.length];renderBattleUI()}
function catchBreath(){if(!battle||battle.busy)return;let c=active();for(let id of c.known)c.pp[id]=Math.min(maxPP(id),(c.pp[id]||0)+2);enemyAfterAction(`${species[c.id].name} steadies its rhythm and restores technique energy.`)}function battleBag(){if(!battle||battle.busy)return;mode='menu';overlayHTML(`<h2>Battle satchel</h2><div class="stack">${['mend','restore','revive'].map(k=>`<button class="btn" onclick="battleItemTargets('${k}')">${itemLabels[k]} × ${state.items[k]}<div class="muted">${itemDescription(k)}</div></button>`).join('')}</div>${btn('Back','resumeGame()')}`)}function battleItemTargets(k){if(!state.items[k])return toast('None left.');overlayHTML(`<h2>${itemLabels[k]} · choose a companion</h2><div class="stack">${state.team.map((c,i)=>`<button class="btn" onclick="useItem('${k}',${i},true)">${species[c.id].name} · ${c.hp}/${stats(c).max}</button>`).join('')}</div>${btn('Back','battleBag()')}`)}
function switchMenu(){if(battle.busy)return;mode='menu';overlayHTML(`<h2>Change the lead</h2><div class="stack">${state.team.map((c,i)=>`<button class="btn" ${c.hp<=0||i===battle.active?'disabled':''} onclick="switchTo(${i})">${species[c.id].name} · Lv ${c.lv} · ${c.hp}/${stats(c).max} vitality</button>`).join('')}</div>${btn('Back','resumeGame()')}`)}function switchTo(i){if(!battle||state.team[i].hp<=0)return;battle.active=i;mode='battle';closeOverlay();enemyAfterAction(`${species[state.team[i].id].name} steps forward.`)}
function damage(att,def,m){let as=stats(att),ds=stats(def),atk=as.atk*(att.status.burn?.5:1),mult=typeMultiplier(m.type,species[def.id].types)*(species[att.id].types.includes(m.type)?1.25:1),crit=Math.random()<.06+att.proficiency*.0015;if(crit)mult*=1.5;let val=Math.max(1,Math.floor(.4*att.lv*atk/ds.def*mult+rand(1,4)));return {val,crit,mult}}
function effectApply(att,def,m,amount){let e=m.effect;if(e==='burn'&&Math.random()<.3)def.status.burn=3;if(e==='frost'&&Math.random()<.4)def.status.frost=3;if(e==='shock'&&Math.random()<.35)def.status.shock=3;if(e==='weaken'&&Math.random()<.55)def.status.weaken=2;if(e==='speed')att.status.speed=3;if(e==='barrier')att.status.barrier=1;if(e==='regen')att.status.regen=3;if(e==='heal')att.hp=Math.min(stats(att).max,att.hp+Math.floor(stats(att).max*.25));if(e==='drain')att.hp=Math.min(stats(att).max,att.hp+Math.floor(amount*.45))}
function combatAction(att,def,m,isPlayer){if(att.hp<=0||def.hp<=0)return `${species[att.id].name} cannot act.`;if(att.status.weaken&&Math.random()<.3)return `${species[att.id].name}'s pattern faltered!`;if(att.status.shock&&Math.random()<.26){att.status.shock--;return `${species[att.id].name} lost its rhythm!`}if(isPlayer){if(!att.pp[m.id])return 'No energy left for that technique.';att.pp[m.id]--}let name=species[att.id].name;if(Math.random()*100>m.acc)return `${name} used ${m.name}, but missed.`;if(def.status.barrier){def.status.barrier=0;burst(isPlayer?.69:.31,.47,TC[6]);return `${name}'s ${m.name} struck a barrier!`}let d=damage(att,def,m);let hits=m.effect==='multihit'?rand(2,3):1;let total=0;for(let i=0;i<hits;i++){let val=m.effect==='multihit'?Math.max(1,Math.floor(d.val*.48)):d.val;def.hp=Math.max(0,def.hp-val);total+=val;if(def.hp===0)break}effectApply(att,def,m,total);let x=isPlayer?.69:.30;floating('-'+total,x,.47,d.crit?'#ffe1a0':'#ffffff');burst(x,.47,d.mult>1.2?TC[m.type]:'#b0d9e1',d.mult>1.2?19:7);if(d.crit){canvas.classList.remove('flash');void canvas.offsetWidth;canvas.classList.add('flash')}if(total>20){canvas.classList.remove('shake');void canvas.offsetWidth;canvas.classList.add('shake')}if(isPlayer)att.proficiency=Math.min(50,att.proficiency+.12);chime(d.mult>1.2?680:280,.08);return `${name} used ${m.name} · ${total} damage${d.crit?' · brilliant strike!':''}${d.mult>1.2?' · resonant!':''}${hits>1?' · '+hits+' hits':''}`}
function tickStatus(c){if(c.hp<=0)return '';let notes=[];if(c.status.burn){c.hp=Math.max(0,c.hp-Math.max(2,Math.floor(stats(c).max*.06)));notes.push(`${species[c.id].name} smolders`)}if(c.status.regen){c.hp=Math.min(stats(c).max,c.hp+Math.max(2,Math.floor(stats(c).max*.07)));notes.push(`${species[c.id].name} renews`)}for(let k of ['burn','frost','shock','speed','weaken','regen'])if(c.status[k]){c.status[k]--;if(!c.status[k])delete c.status[k]}return notes.join(' · ')}
function effectiveSpeed(c,m){return stats(c).spd*(c.status.frost?.5:1)*(c.status.speed?1.5:1)+(m?.effect==='priority'?1000:0)+Math.random()*.2}
function playerMove(id){if(!battle||battle.busy||mode!=='battle')return;let a=active(),m=moves[id];if(!a.pp[id])return;let target=battle.enemies[battle.target];battle.busy=true;renderBattleUI();let foes=livingEnemies().map(e=>({c:e,m:moves[learnset(e.id).filter(x=>x[0]<=e.lv).map(x=>x[1]).filter(x=>x%6<4)[rand(0,Math.max(0,Math.min(3,learnset(e.id).filter(x=>x[0]<=e.lv).length-1)))]]||moves[species[e.id].types[0]*6]}));let order=[{c:a,m,player:true},...foes.map(f=>({...f,player:false}))].sort((u,v)=>effectiveSpeed(v.c,v.m)-effectiveSpeed(u.c,u.m));let i=0;function next(){if(!battle)return;if(i>=order.length){let notes=[...state.team,...battle.enemies].map(tickStatus).filter(Boolean);if(notes.length)battle.log=notes.join(' · ');battle.round++;checkBattleEnd();if(battle){battle.busy=false;renderBattleUI()}return}let turn=order[i++];if(turn.c.hp<=0){next();return}let def=turn.player?(target.hp>0?target:livingEnemies()[0]):active();if(!def||def.hp<=0){next();return}battle.log=combatAction(turn.c,def,turn.m,turn.player);renderBattleUI();if(checkBattleEnd())return;setTimeout(next,450)}setTimeout(next,120)}
function enemyAfterAction(text){if(!battle)return;battle.busy=true;battle.log=text;renderBattleUI();let enemies=livingEnemies();let i=0;function next(){if(!battle)return;if(i>=enemies.length){let notes=[...state.team,...battle.enemies].map(tickStatus).filter(Boolean);if(notes.length)battle.log=notes.join(' · ');if(checkBattleEnd())return;battle.busy=false;renderBattleUI();return}let e=enemies[i++];if(e.hp<=0){next();return}let pool=learnset(e.id).filter(x=>x[0]<=e.lv).map(x=>x[1]).filter(x=>x%6<4);let m=moves[pool[rand(0,pool.length-1)]];battle.log=combatAction(e,active(),m,false);renderBattleUI();if(checkBattleEnd())return;setTimeout(next,420)}setTimeout(next,350)}
function checkBattleEnd(){if(!battle)return true;if(!livingEnemies().length){if(battle.kind==='tutorial'){let e=battle.enemies[0];e.hp=1;e.status={};battle.busy=false;tutorial(2);return true}winBattle();return true}if(state.team.every(c=>c.hp<=0)){loseBattle();return true}if(active().hp<=0){let i=state.team.findIndex(c=>c.hp>0);battle.active=i;battle.log=`${species[state.team[i].id].name} takes the lead!`;renderBattleUI()}return false}
function winBattle(){let b=battle,defeated=b.enemies,avg=defeated.reduce((s,e)=>s+e.lv,0)/defeated.length;let credits=Math.round(avg*(b.kind==='guardian'?9:2.3)+rand(4,12)),xp=Math.round(defeated.reduce((sum,e)=>sum+e.lv*6,0)*(b.kind==='guardian'?1.5:1));state.credits+=credits;let levelText=[];for(let c of state.team){if(c.hp<=0)continue;let gained=Math.max(5,Math.round(xp*(c===active()?1:.55)*clamp(avg/c.lv,.65,1.6)));let r=gainXP(c,gained);levelText.push(`${species[c.id].name} +${gained} insight${r?' · Lv '+c.lv:''}`)}if(b.kind==='guardian'){let z=state.zone;if(!state.anchors.includes(z)){state.anchors.push(z);state.unlocked=Math.max(state.unlocked,Math.min(5,z+1));state.bonuses++;state.items.etched+=2;state.items.mend+=2;for(let c of state.team){c.hp=Math.min(stats(c).max,c.hp+Math.floor(stats(c).max*.3))}if([0,1,3,5].includes(z)){let stage=[0,1,3,5].indexOf(z)+1;state.story=Math.max(state.story,stage)}}}state.battles++;battle=null;bui.innerHTML='';mode='menu';save();overlayHTML(`<div class="eyebrow">${b.kind==='guardian'?'An interval opens':'The echoes settle'}</div><h2>${b.kind==='guardian'?'Warden stilled':'A bright victory'}</h2><p>${b.kind==='guardian'?'The path ahead unfolds. A permanent blessing strengthens your binding, and the sanctuary offers fresh supplies.':'The wandering echoes leave fragments of their pattern behind.'}</p><div class="card">+ ${credits} shards<br>${levelText.join('<br>')}<br>${b.kind==='guardian'?'+ 2 Etched Loops · + 2 Mendleaves · passage opened':''}</div><div class="divider"></div>${btn('Continue','afterVictory()','primary')}`);if(b.kind==='guardian')chime(690,.8)}
function gainXP(c,n){if(c.lv>=50)return false;c.exp+=n;let leveled=false;while(c.lv<50&&c.exp>=c.lv*18+25){c.exp-=c.lv*18+25;let old=stats(c).max;c.lv++;c.hp+=stats(c).max-old;leveled=true;let newMove=learnset(c.id).find(x=>x[0]===c.lv);if(newMove){if(c.known.length>=4)c.known.shift();c.known.push(newMove[1]);c.pp[newMove[1]]=maxPP(newMove[1])}}return leveled}
function afterVictory(){mode='world';closeOverlay();if(state.story>0&&[1,2,3,4].includes(state.story)&&!state['shown'+state.story]){let n=state.story;state['shown'+n]=true;let lines=[['THE SECOND THREAD','A bell rings without a hand. The intervals were not broken by force; they simply forgot how to touch.'],['THE THIRD THREAD','Beneath the lattice, you find a word without a speaker. It feels like a promise kept across centuries.'],['THE FOURTH THREAD','The upright sea reflects a sky that has never existed. Every echo you bind gives it a new star.'],['THE LAST THREAD','At the crown, silence becomes a doorway. The world is ready to name itself again.']];showStory(lines[n-1][0],lines[n-1][1],()=>{mode='world';closeOverlay();checkGoal()});return}checkGoal()}
function loseBattle(){battle=null;bui.innerHTML='';state.zone=state.shrine.zone;state.x=state.shrine.x;state.y=state.shrine.y;for(let c of state.team){c.hp=stats(c).max;c.status={};refreshPP(c)}state.credits=Math.max(0,state.credits-20);mode='menu';save();overlayHTML(`<h2>The binding holds</h2><p>The sanctuary draws you back. Your companions are restored, but some shards scatter into the quiet.</p>${btn('Rise again','resumeGame()','primary')}`)}function fleeBattle(){if(!battle||battle.busy||battle.kind!=='wild')return;if(Math.random()<.85){battle=null;bui.innerHTML='';mode='world';toast('You slipped into the quiet.')}else enemyAfterAction('The echoes block the way!')}
function checkGoal(){if(!state||state.post)return;if(state.caught.length>=8&&state.anchors.length>=4){state.post=true;save();showStory('THE WORLD REMEMBERS','Eight distinct lives and four stilled Wardens have restored the first great pattern. The remaining intervals are yours to explore; stronger echoes now wander every path.',()=>{mode='world';closeOverlay();toast('Vellum Wake · the path continues')})}}
function captureMenu(){if(!battle||battle.busy)return;if(battle.kind==='guardian')return toast('Wardens cannot be bound.');mode='menu';overlayHTML(`<div class="eyebrow">A pattern waits</div><h2>Choose a binding loop</h2><p>Lower the target’s vitality to improve your chance. Your timing will add a final measure.</p><div class="stack">${['woven','etched','stellar'].map((k,i)=>`<button class="btn" ${!state.items[k]?'disabled':''} onclick="captureGame('${k}')">${itemLabels[k]} × ${state.items[k]} <span class="muted">· ${['ordinary','strong','exceptional'][i]} hold</span></button>`).join('')}</div>${btn('Back','resumeGame()')}`)}
let captureStart=0,captureKey=null;function captureGame(k){if(!state.items[k]||!battle)return;captureKey=k;captureStart=performance.now();mode='capture';overlayHTML(`<div class="eyebrow">Hold the thread</div><h2>Find the center</h2><p>Tap the button or press Space when the wandering light passes the luminous central band.</p><div style="position:relative;height:46px;border:1px solid var(--line);border-radius:24px;background:#071a26;margin:27px 4px;overflow:hidden"><div style="position:absolute;left:43%;width:14%;height:100%;background:#f4d59b55;border-left:2px solid #f4d59b;border-right:2px solid #f4d59b"></div><div id="pulse" style="position:absolute;top:4px;left:0%;width:13px;height:36px;border-radius:10px;background:#a8fff3;box-shadow:0 0 22px #8fffee"></div></div>${btn('Bind now!','attemptCapture()','primary')} <span class="muted">One loop per attempt</span>`);animatePulse()}
function pulsePos(){return .5+.5*Math.sin((performance.now()-captureStart)/490-Math.PI/2)}function animatePulse(){if(mode!=='capture')return;let e=$('#pulse');if(e)e.style.left=`calc(${pulsePos()*96}% - 6px)`;requestAnimationFrame(animatePulse)}
function attemptCapture(){if(mode!=='capture'||!battle||!captureKey)return;let k=captureKey;captureKey=null;let target=battle.enemies[battle.target],hp=target.hp/stats(target).max,timing=1-Math.abs(pulsePos()-.5)*2,quality={woven:0,etched:.18,stellar:.36}[k],chance=clamp(.19+(1-hp)*.46+quality+timing*.20,.07,.96);state.items[k]--;mode='battle';closeOverlay();if(Math.random()<chance){let c=JSON.parse(JSON.stringify(target));target.hp=0;c.status={};c.hp=Math.max(1,c.hp);refreshPP(c);if(state.team.length<6)state.team.push(c);else state.reserve.push(c);if(!state.caught.includes(c.id))state.caught.push(c.id);battle.log=`${species[c.id].name} joined your binding!${state.team.includes(c)?'':' Sent to reserve.'}`;chime(760,.55);burst(.5,.45,'#d2ffdd',35);if(battle.kind==='tutorial'){battle=null;bui.innerHTML='';mode='world';showStory('A NEW NAME','The first bond is made. Your companion is not a tool, but a life walking beside your own.',()=>tutorial(3));save();return}if(checkBattleEnd()){checkGoal();return}renderBattleUI();checkGoal();return}battle.log=`The loop loosened · ${Math.round(chance*100)}% chance. The echo remains free.`;if(battle.kind==='tutorial'){battle.enemies[battle.target].hp=Math.max(1,battle.enemies[battle.target].hp);state.items.woven++;battle.log='The thread slipped! Your practice loop returns. Try again.';renderBattleUI();return}enemyAfterAction(battle.log)}
function resize(){let d=Math.min(devicePixelRatio||1,2);canvas.width=Math.round(innerWidth*d);canvas.height=Math.round(innerHeight*d);ctx.setTransform(d,0,0,d,0,0)}window.addEventListener('resize',resize);resize();
function roundRect(x,y,w,h,r,fill){ctx.beginPath();ctx.roundRect(x,y,w,h,r);ctx.fillStyle=fill;ctx.fill()}
function gradient(a,b){let g=ctx.createLinearGradient(0,0,innerWidth,innerHeight);g.addColorStop(0,a);g.addColorStop(1,b);return g}
function initParticles(){particles=Array.from({length:85},()=>({x:Math.random()*innerWidth,y:Math.random()*innerHeight,r:Math.random()*2+0.7,s:Math.random()*.65+.2,p:Math.random()*7}))}initParticles();window.addEventListener('resize',initParticles);
function backdrop(z){let zone=zones[z]||zones[0];ctx.fillStyle=gradient(...zone.grad);ctx.fillRect(0,0,innerWidth,innerHeight);let glow=ctx.createRadialGradient(innerWidth*.5,innerHeight*.46,10,innerWidth*.5,innerHeight*.46,innerWidth*.65);glow.addColorStop(0,'#d7f7e718');glow.addColorStop(1,'#00000000');ctx.fillStyle=glow;ctx.fillRect(0,0,innerWidth,innerHeight);for(let p of particles){p.y-=p.s*(zone.particles==='rain'?4:1);p.x+=Math.sin(frame*.007+p.p)*.13+(zone.particles==='leaves'?.22:0);if(p.y<0){p.y=innerHeight;p.x=Math.random()*innerWidth}if(p.x>innerWidth)p.x=0;ctx.globalAlpha=.22+Math.sin(frame*.03+p.p)*.17;ctx.fillStyle=['#f4e2ad','#c7ffea','#ffe6d6','#d5f3ff','#fff'][z%5];ctx.beginPath();ctx.arc(p.x,p.y,p.r*(zone.particles==='stars'?1.6:1),0,Math.PI*2);ctx.fill()}ctx.globalAlpha=1}
function glyph(x,y,r,type,phase=0){ctx.save();ctx.translate(x,y);ctx.rotate(Math.sin(frame*.013+phase)*.1);ctx.shadowBlur=26;ctx.shadowColor=TC[type];let g=ctx.createRadialGradient(-r*.3,-r*.4,1,0,0,r);g.addColorStop(0,'#ffffff');g.addColorStop(.28,TC[type]);g.addColorStop(1,'#213949');ctx.fillStyle=g;ctx.beginPath();for(let i=0;i<10;i++){let a=i*Math.PI/5-Math.PI/2,rr=r*(i%2?.77:1);let xx=Math.cos(a)*rr,yy=Math.sin(a)*rr;if(!i)ctx.moveTo(xx,yy);else ctx.lineTo(xx,yy)}ctx.closePath();ctx.fill();ctx.shadowBlur=0;ctx.strokeStyle='#ffffff99';ctx.lineWidth=1.6;ctx.stroke();ctx.beginPath();ctx.ellipse(0,0,r*.28,r*.42,0,0,7);ctx.stroke();ctx.beginPath();ctx.arc(-r*.2,-r*.25,r*.11,0,7);ctx.fillStyle='#fff';ctx.fill();ctx.restore()}
function drawWorld(){let z=state.zone;backdrop(z);let tile=Math.min((innerWidth-24)/W,(innerHeight-120)/H,57);tile=Math.max(16,tile);let ox=(innerWidth-W*tile)/2,oy=(innerHeight-H*tile)/2+22;ctx.save();ctx.fillStyle='#06182366';roundRect(ox-8,oy-8,W*tile+16,H*tile+16,14,'#06182388');for(let y=0;y<H;y++)for(let x=0;x<W;x++){let k=tileKind(z,x,y),px=ox+x*tile,py=oy+y*tile;let noise=(x*13+y*19+z*5)%7;roundRect(px+1,py+1,tile-2,tile-2,Math.max(2,tile*.10),k==='obstacle'?'#071d2d99':noise===0?'#ffffff12':'#ffffff0a');if(k==='obstacle'){ctx.fillStyle='#071c27aa';ctx.beginPath();ctx.moveTo(px+tile*.12,py+tile*.78);ctx.lineTo(px+tile*.43,py+tile*.22);ctx.lineTo(px+tile*.82,py+tile*.72);ctx.fill();ctx.strokeStyle='#b8d7d03b';ctx.stroke()}else if(k==='shrine'){ctx.strokeStyle='#b2fff0';ctx.lineWidth=2;ctx.beginPath();ctx.arc(px+tile/2,py+tile/2,tile*.31+Math.sin(frame*.04)*1.5,0,7);ctx.stroke();glyph(px+tile/2,py+tile/2,tile*.19,4)}else if(k==='anchor'){ctx.strokeStyle=state.anchors.includes(z)?'#8baca077':'#f7d6a7';ctx.lineWidth=2;ctx.beginPath();ctx.arc(px+tile/2,py+tile/2,tile*.36,0,7);ctx.stroke();glyph(px+tile/2,py+tile/2,tile*.22,state.anchors.includes(z)?6:5)}else if(k==='tutorial'&&state.tutorial===0){glyph(px+tile/2,py+tile/2,tile*.16,0)}else if(k==='gate'||k==='back'){ctx.fillStyle='#e9d9af66';ctx.fillRect(px+tile*.46,py+tile*.19,tile*.09,tile*.62)}else if(noise===2){ctx.strokeStyle='#c2e9dc33';ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(px+tile*.5,py+tile*.75);ctx.lineTo(px+tile*.45,py+tile*.49);ctx.lineTo(px+tile*.32,py+tile*.4);ctx.moveTo(px+tile*.45,py+tile*.49);ctx.lineTo(px+tile*.65,py+tile*.36);ctx.stroke()}}let px=ox+(state.x+.5)*tile,py=oy+(state.y+.5)*tile;ctx.fillStyle='#00172299';ctx.beginPath();ctx.ellipse(px,py+tile*.25,tile*.34,tile*.12,0,0,7);ctx.fill();glyph(px,py-3+Math.sin(frame*.07)*2,tile*.35,species[state.team[0].id].types[0]);ctx.restore();ctx.textAlign='center';ctx.fillStyle='#f5e2b4';ctx.font='14px Georgia';ctx.fillText(zones[z].story,innerWidth/2,Math.max(27,oy-19));ctx.font='11px Trebuchet MS';ctx.fillStyle='#d6efea99';ctx.fillText('◈ sanctuary ◇ warden → passage',innerWidth/2,Math.min(innerHeight-13,oy+H*tile+27))}
function drawBattle(){backdrop(state.zone);let mid=innerWidth/2,w=innerWidth,h=innerHeight;ctx.strokeStyle='#d3f6ea22';ctx.lineWidth=1;for(let i=0;i<7;i++){ctx.beginPath();ctx.ellipse(mid,h*.56,w*(.15+i*.085),h*(.04+i*.025),0,0,7);ctx.stroke()}let a=active(),living=livingEnemies();let sy=h*.47+Math.sin(frame*.04)*5;glyph(w*.28,sy,Math.min(w*.105,h*.16,87),species[a.id].types[0],1);ctx.textAlign='center';ctx.font='19px Georgia';ctx.fillStyle='#f3f4ef';ctx.fillText(species[a.id].name,w*.28,sy+Math.min(w*.105,h*.16,87)+29);let positions=living.length===1?[.7]:living.length===2?[.62,.78]:[.58,.70,.82];living.forEach((e,i)=>{let x=w*positions[i],y=h*.37+Math.sin(frame*.04+i)*7,r=Math.min(w*.072,h*.105,65);glyph(x,y,r,species[e.id].types[0],i+2);ctx.fillStyle='#f1e9dc';ctx.font='16px Georgia';ctx.fillText(species[e.id].name,x,y+r+23);let bw=Math.min(126,w*.18);roundRect(x-bw/2,y+r+32,bw,7,5,'#09232d');roundRect(x-bw/2,y+r+32,Math.max(0,bw*e.hp/stats(e).max),7,5,e.hp/stats(e).max<.3?'#ec8790':'#b2ecc8');if(battle.enemies.indexOf(e)===battle.target){ctx.strokeStyle='#f2dea3';ctx.lineWidth=2;ctx.beginPath();ctx.arc(x,y,r+11,0,7);ctx.stroke()}});ctx.textAlign='left'}
function drawTitle(){backdrop(0);let x=innerWidth*.5,y=innerHeight*.48;for(let i=0;i<7;i++){ctx.save();ctx.translate(x,y);ctx.rotate(frame*.001*(i%2?-1:1)+i);ctx.strokeStyle=TC[i]+'44';ctx.lineWidth=2;ctx.beginPath();ctx.ellipse(0,0,innerWidth*.25+i*14,innerHeight*.22+i*8,i*.28,0,7);ctx.stroke();ctx.restore()}glyph(x,y,Math.min(innerWidth*.13,110),0)}
function loop(){frame++;if(state){$('#zoneLabel').innerHTML=`<span style="color:var(--gold)">${zones[state.zone].name}</span><br><span class="muted">${state.post?'Afterwake · ':''}${state.anchors.length} anchors · ${state.caught.length} bound · ${state.credits} ◇</span>`}else $('#zoneLabel').innerHTML='<span style="color:var(--gold)">VELLUM WAKE</span>';if(mode==='battle'||mode==='capture'||(mode==='menu'&&battle))drawBattle();else if(state&&mode!=='title')drawWorld();else drawTitle();requestAnimationFrame(loop)}requestAnimationFrame(loop);
function floating(text,x,y,color){let el=document.createElement('div');el.className='float';el.style.left=`${x*100}%`;el.style.top=`${y*100}%`;el.style.color=color;el.textContent=text;$('#floatLayer').append(el);setTimeout(()=>el.remove(),960)}function burst(x,y,color,n=12){for(let i=0;i<n;i++){let e=document.createElement('span');let a=Math.random()*7,r=18+Math.random()*90;e.style.cssText=`position:absolute;left:${x*100}%;top:${y*100}%;width:${rand(2,6)}px;height:${rand(2,6)}px;border-radius:50%;background:${color};box-shadow:0 0 9px ${color};transition:transform .65s ease-out,opacity .65s;`;$('#floatLayer').append(e);requestAnimationFrame(()=>{e.style.transform=`translate(${Math.cos(a)*r}px,${Math.sin(a)*r}px)`;e.style.opacity='0'});setTimeout(()=>e.remove(),700)}}
function chime(freq=440,dur=.12,vol=.055){if(muted)return;try{audio??=new (window.AudioContext||window.webkitAudioContext)();if(audio.state==='suspended')audio.resume();let o=audio.createOscillator(),g=audio.createGain();o.type='sine';o.frequency.setValueAtTime(freq,audio.currentTime);o.frequency.exponentialRampToValueAtTime(Math.max(70,freq*.72),audio.currentTime+dur);g.gain.setValueAtTime(vol,audio.currentTime);g.gain.exponentialRampToValueAtTime(.0001,audio.currentTime+dur);o.connect(g);g.connect(audio.destination);o.start();o.stop(audio.currentTime+dur)}catch(e){}}
setInterval(()=>{if(muted||!state||mode==='title')return;let notes=[174,220,261,330,392,440,523];chime(notes[(audioTimer++*3+state.zone)%notes.length],.85,.008)},1700);
window.addEventListener('keydown',e=>{let k=e.key.toLowerCase();if(['arrowup','arrowdown','arrowleft','arrowright',' ','escape'].includes(k))e.preventDefault();if(mode==='capture'&&k===' '){attemptCapture();return}if(k==='escape'){if(mode==='menu'&&battle)resumeGame();else if(mode==='menu')resumeGame();else if(mode==='world'||mode==='battle')openMenu();return}if(mode==='world'){let dirs={w:[0,-1],arrowup:[0,-1],s:[0,1],arrowdown:[0,1],a:[-1,0],arrowleft:[-1,0],d:[1,0],arrowright:[1,0]};if(dirs[k])moveWorld(...dirs[k]);if(k==='e'||k===' ')interact()}});
$('#menuBtn').onclick=()=>{if(state)openMenu()};$('#mapBtn').onclick=()=>{if(state)openMenu('map')};$('#muteBtn').onclick=()=>{muted=!muted;$('#muteBtn').textContent=muted?'♫ Off':'♫';if(!muted)chime(540,.15)};$('#actionTouch').onclick=interact;document.querySelectorAll('#touch button').forEach(b=>{let d={up:[0,-1],down:[0,1],left:[-1,0],right:[1,0]}[b.dataset.dir],timer;b.addEventListener('pointerdown',e=>{e.preventDefault();moveWorld(...d);timer=setInterval(()=>moveWorld(...d),140)});for(let event of ['pointerup','pointerleave','pointercancel'])b.addEventListener(event,()=>clearInterval(timer))});
title();
</script></body></html>
<!-- agent-meta {"model":"openai/gpt-6-sol","provider":"openrouter","persona":"full","sessionId":"1d8016a2-623d-414c-8f7d-78a544ad865e","tokensIn":1095445,"tokensOut":27502,"tokensTotal":1122947,"cost":0.5989579,"turns":33,"toolCalls":32,"failedToolCalls":0,"timestamp":"2026-09-22T22:08:18.000Z"} -->