Creature Collection
GPT 5.6 Sol · middle

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 54,240 bytes · SHA-256 671b40cb2544
<!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>Veyra: The Lattice Wake</title><style>
:root{--ink:#e9fbff;--dim:#91a9bd;--deep:#07111d;--glass:rgba(9,23,38,.82);--line:rgba(159,235,255,.22);--glow:#7df4e5;--gold:#ffd57a;--danger:#ff6688;--ok:#75f0ae}*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#040a12;color:var(--ink);font-family:ui-rounded,"Trebuchet MS",system-ui,sans-serif}button{font:inherit;color:inherit}.hidden{display:none!important}#game{position:fixed;inset:0}canvas{position:absolute;inset:0;width:100%;height:100%;image-rendering:auto}#world,#battle{transition:filter .4s,opacity .4s}#battle{display:none}.grain{position:fixed;inset:0;pointer-events:none;opacity:.12;background-image:radial-gradient(#fff 0.55px,transparent .8px);background-size:5px 5px;mix-blend-mode:overlay}.topbar{position:fixed;left:16px;right:16px;top:14px;display:flex;align-items:center;justify-content:space-between;pointer-events:none;z-index:5}.brand{font-weight:900;letter-spacing:.16em;text-transform:uppercase;text-shadow:0 0 18px var(--glow)}.pill{pointer-events:auto;border:1px solid var(--line);background:rgba(5,15,25,.72);backdrop-filter:blur(12px);padding:8px 12px;border-radius:999px;box-shadow:0 8px 30px #0006}.iconbtn{width:40px;height:40px;padding:0;border:1px solid var(--line);border-radius:50%;background:#081827cc;cursor:pointer}.hud{position:fixed;z-index:4;left:16px;bottom:18px;width:min(330px,calc(100% - 32px));pointer-events:none}.zonecard{padding:12px 15px;border:1px solid var(--line);border-radius:15px;background:#071725b8;backdrop-filter:blur(12px);box-shadow:0 12px 40px #0009}.zonecard b{font-size:18px}.zonecard small{display:block;color:var(--dim);margin-top:3px}.progress{height:5px;margin-top:9px;border-radius:5px;background:#ffffff12;overflow:hidden}.progress i{display:block;height:100%;background:linear-gradient(90deg,var(--glow),#a38bff);box-shadow:0 0 10px var(--glow)}
#touch{position:fixed;right:18px;bottom:18px;width:154px;height:154px;z-index:7;display:none}.dpad{position:absolute;width:48px;height:48px;border:1px solid #ffffff30;background:#071827b8;border-radius:14px;backdrop-filter:blur(8px);font-size:21px}.dpad:active{background:#3ee5ca55}.up{top:0;left:53px}.down{bottom:0;left:53px}.left{left:0;top:53px}.right{right:0;top:53px}@media(pointer:coarse),(max-width:700px){#touch{display:block}.hud{bottom:180px}.brand{font-size:12px}}
.overlay{position:fixed;inset:0;z-index:20;display:grid;place-items:center;background:radial-gradient(circle at 50% 35%,#17334ad9,#03080ff5);padding:18px}.panel{width:min(940px,100%);max-height:92vh;overflow:auto;border:1px solid var(--line);border-radius:24px;background:linear-gradient(145deg,rgba(17,40,58,.93),rgba(5,13,24,.96));box-shadow:0 30px 100px #000,inset 0 1px #ffffff1b;backdrop-filter:blur(22px);padding:24px}.titlepanel{text-align:center;width:min(720px,100%);padding:48px 25px}.sigil{width:150px;height:150px;margin:0 auto 25px;border:2px solid var(--glow);border-radius:38% 62% 55% 45%;animation:orbit 8s linear infinite;box-shadow:0 0 40px #55efdc55,inset 0 0 35px #7df4e544;position:relative}.sigil:after{content:"✦";position:absolute;inset:0;display:grid;place-items:center;font-size:62px;animation:orbit 8s linear infinite reverse;text-shadow:0 0 25px var(--glow)}@keyframes orbit{to{transform:rotate(360deg)}}h1{margin:5px 0;font-size:clamp(38px,8vw,76px);line-height:.9;letter-spacing:.04em;background:linear-gradient(#fff,#72eadc 70%,#857ef3);-webkit-background-clip:text;color:transparent;text-shadow:0 10px 40px #4ce5d333}.subtitle{letter-spacing:.32em;text-transform:uppercase;color:#a7bdcf;font-size:12px;margin:15px}.btnrow{display:flex;justify-content:center;gap:12px;flex-wrap:wrap;margin-top:25px}.btn{border:1px solid var(--line);background:linear-gradient(135deg,#16354b,#102236);border-radius:13px;padding:12px 20px;cursor:pointer;box-shadow:0 7px 20px #0005;transition:.18s}.btn:hover,.btn:focus{transform:translateY(-2px);border-color:var(--glow);box-shadow:0 8px 30px #58e5d333}.btn.primary{background:linear-gradient(135deg,#25bda9,#5566d8);font-weight:800}.btn.warn{border-color:#ff789055;color:#ffc0cc}.btn:disabled{opacity:.35;filter:grayscale(1);cursor:not-allowed;transform:none}.close{position:absolute;right:24px;top:20px}.menuhead{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--line);padding-bottom:15px;margin-bottom:16px}.menuhead h2{margin:0}.tabs{display:flex;gap:8px;overflow:auto;margin-bottom:15px}.tab{white-space:nowrap;padding:9px 13px;border-radius:10px;border:1px solid var(--line);background:#0b1c2c}.tab.on{background:#21606a;border-color:var(--glow)}.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:12px}.card{position:relative;border:1px solid var(--line);border-radius:16px;background:#ffffff08;padding:14px;overflow:hidden;min-height:100px}.card:before{content:"";position:absolute;width:100px;height:100px;right:-35px;top:-40px;border-radius:50%;background:var(--c,#70e6d2);filter:blur(35px);opacity:.18}.card h3{margin:0 0 5px}.card p{color:var(--dim);font-size:13px;margin:6px 0}.orb{width:58px;height:58px;float:left;margin-right:11px;border-radius:50%;display:grid;place-items:center;background:radial-gradient(circle at 35% 30%,#fff,var(--c) 20%,#102535 70%);box-shadow:0 0 20px color-mix(in srgb,var(--c),transparent 45%);font-size:24px}.tag{display:inline-block;padding:3px 7px;border:1px solid #ffffff24;border-radius:20px;font-size:10px;margin:2px;background:#0003}.bar{height:8px;background:#02080f;border-radius:10px;overflow:hidden;border:1px solid #ffffff12}.bar i{display:block;height:100%;transition:width .55s;background:linear-gradient(90deg,#54e0aa,#d8f781)}.bar.hp i.low{background:linear-gradient(90deg,#f0446d,#ff9b70)}
#dialog{position:fixed;z-index:16;left:50%;transform:translateX(-50%);bottom:25px;width:min(780px,calc(100% - 32px));padding:20px 22px;border-radius:18px;border:1px solid var(--line);background:#071522ee;box-shadow:0 18px 50px #000a;cursor:pointer}#dialog b{color:var(--glow);letter-spacing:.1em}#dialog p{margin:6px 0;line-height:1.5}#toast{position:fixed;z-index:60;left:50%;top:72px;transform:translate(-50%,-20px);opacity:0;pointer-events:none;padding:10px 18px;background:#071624e8;border:1px solid var(--line);border-radius:50px;transition:.3s;box-shadow:0 10px 35px #0008}#toast.show{opacity:1;transform:translate(-50%,0)}
#battleUI{position:fixed;inset:0;z-index:8;pointer-events:none}.bcard{position:absolute;width:min(330px,44vw);padding:12px 14px;border-radius:15px;background:#061522dc;border:1px solid var(--line);backdrop-filter:blur(10px)}.enemycard{right:3%;top:11%}.allycard{left:3%;bottom:28%}.battleCommands{pointer-events:auto;position:absolute;right:3%;bottom:3%;width:min(570px,94%);padding:14px;border-radius:20px;border:1px solid var(--line);background:#061421e8;display:grid;grid-template-columns:repeat(2,1fr);gap:9px}.move{min-height:63px;text-align:left;padding:10px;border:1px solid #ffffff20;border-radius:12px;background:#10263a;cursor:pointer}.move:hover{border-color:var(--mc,var(--glow));background:#163148}.move b,.move small{display:block}.move small{color:#9bb0c0;margin-top:4px}.battleTools{grid-column:span 2;display:flex;gap:8px}.battleTools .btn{flex:1;padding:8px}.log{position:absolute;left:3%;bottom:3%;width:min(390px,45%);height:94px;overflow:hidden;padding:10px 14px;border-radius:14px;background:#04101bcc;border:1px solid var(--line);color:#b9cbd7;font-size:13px}.float{position:fixed;z-index:50;font-size:25px;font-weight:900;pointer-events:none;text-shadow:0 2px 6px #000;animation:float 1s forwards}@keyframes float{to{transform:translateY(-70px) scale(1.2);opacity:0}}.flash{animation:flash .24s}@keyframes flash{50%{filter:brightness(2.3)}}.shake{animation:shake .28s}@keyframes shake{25%{transform:translate(7px,2px)}50%{transform:translate(-6px,-1px)}75%{transform:translate(3px,-2px)}}
.choicegrid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}@media(max-width:650px){.choicegrid{grid-template-columns:1fr}.titlepanel{padding:25px 16px}.panel{padding:18px}.battleCommands{left:3%;right:3%;width:auto}.log{display:none}.allycard{bottom:38%;width:55%}.enemycard{width:55%}.move{min-height:55px}.orb{width:48px;height:48px}}.rhythm{height:34px;border-radius:18px;background:#020811;position:relative;overflow:hidden;border:1px solid var(--line);margin:20px 0}.sweet{position:absolute;left:44%;width:14%;height:100%;background:#5de8be55;border-left:2px solid #8fffdc;border-right:2px solid #8fffdc}.needle{position:absolute;width:5px;height:100%;background:#fff;box-shadow:0 0 12px #fff}.vignette{box-shadow:inset 0 0 160px var(--vig,#7df4e566)}
</style></head><body><div id="game"><canvas id="world"></canvas><canvas id="battle"></canvas><div class="grain"></div><div class="topbar hidden" id="topbar"><div class="brand">VEYRA // LATTICE WAKE</div><div><button class="iconbtn" id="muteBtn" title="Sound">♫</button> <button class="iconbtn" id="menuBtn" title="Menu">☰</button></div></div><div class="hud hidden" id="hud"><div class="zonecard"><b id="zoneName">—</b><small id="zoneSub">—</small><div class="progress"><i id="goalProgress"></i></div></div></div><div id="touch" class="hidden"><button class="dpad up" data-k="ArrowUp">▲</button><button class="dpad down" data-k="ArrowDown">▼</button><button class="dpad left" data-k="ArrowLeft">◀</button><button class="dpad right" data-k="ArrowRight">▶</button></div><div id="battleUI" class="hidden"></div></div><div id="title" class="overlay"><div class="panel titlepanel"><div class="sigil"></div><h1>VEYRA</h1><div class="subtitle">The Lattice Wake</div><p style="color:var(--dim)">Bind the wandering echoes. Mend a sky that remembers breaking.</p><div class="btnrow"><button class="btn primary" id="newBtn">New Binding</button><button class="btn" id="continueBtn">Continue</button></div><p style="font-size:11px;color:#647b8d;margin-top:30px">WASD / ARROWS · TOUCH READY · AUTOSAVES AT WELLS</p></div></div><div id="modal" class="overlay hidden"></div><div id="dialog" class="hidden"></div><div id="toast"></div>
<script>
'use strict';
const $=s=>document.querySelector(s), $$=s=>[...document.querySelectorAll(s)];
const clamp=(v,a,b)=>Math.max(a,Math.min(b,v)), rnd=(a,b)=>Math.floor(Math.random()*(b-a+1))+a, pick=a=>a[Math.floor(Math.random()*a.length)];
const AFF={Lume:'#ffe177',Mire:'#63dfb0',Cinder:'#ff6f70',Gale:'#70cfff',Umber:'#a78aef',Rift:'#f59cff',Aegis:'#d6e4ed'};
const glyph={Lume:'✦',Mire:'❧',Cinder:'◆',Gale:'≋',Umber:'◈',Rift:'⌁',Aegis:'⬡'};
const zones=[
{name:'Hushfen Verge',sub:'Where sleeping rain gathers',grad:['#071923','#165046'],rate:.12,amb:'#61e8be',wild:[0,1,2,3],min:2,max:7,story:'A hushwell sings beneath the reeds. Its note knows your name.'},
{name:'Cinnabar Steps',sub:'Terraces of patient ember',grad:['#25101c','#8a3d34'],rate:.15,amb:'#ff8a62',wild:[3,4,5,6,1],min:7,max:14,story:'Heat-script crawls across the stone: FOUR NODES HOLD THE WOUND.'},
{name:'Serein Canopy',sub:'Cloud-roots and blue wind',grad:['#101b3b','#287f8b'],rate:.17,amb:'#8beaff',wild:[6,7,8,9,2],min:13,max:22,story:'The canopy parts. Beyond it, the sky is stitched with violet scars.'},
{name:'Nacre Hollow',sub:'A moonlit chamber of echoes',grad:['#151330','#544086'],rate:.19,amb:'#c7adff',wild:[9,10,11,12,5],min:21,max:31,story:'The third anchor dreams aloud, mourning a maker called the Quiet Hand.'},
{name:'Riven Loom',sub:'Reality drawn into threads',grad:['#2b102d','#8a276d'],rate:.21,amb:'#ff91dc',wild:[12,13,14,15,8],min:30,max:41,story:'Every bound echo answers at once: We are not fragments. We are witnesses.'},
{name:'Pale Meridian',sub:'The last line before dawn',grad:['#17202d','#668297'],rate:.23,amb:'#eefaff',wild:[15,16,17,14,11],min:39,max:50,story:'At the meridian, the old wound waits—not hungry, only unfinished.'}
];
const effectNames=['none','burn','frost','shock','barrier','regen','boost','heal','multi','priority','drain','weaken'];
const M={};
function mv(n,p,a,t,c,e='none',d=''){M[n]={name:n,pow:p,acc:a,aff:t,cost:c,effect:e,desc:d||e};}
mv('Glimmer Prick',28,98,'Lume',3);mv('Sunthread',45,94,'Lume',5,'burn');mv('Halo Mend',0,100,'Lume',6,'heal');mv('Dawn Chorus',64,88,'Lume',8,'boost');mv('Lucent Lance',82,84,'Lume',10,'priority');mv('Starwell',0,100,'Lume',8,'regen');
mv('Reed Lash',31,98,'Mire',3);mv('Silt Bloom',46,93,'Mire',5,'weaken');mv('Verdant Sip',38,96,'Mire',5,'drain');mv('Bog Snare',52,90,'Mire',6,'frost');mv('Root Covenant',0,100,'Mire',7,'barrier');mv('Fen Crown',78,84,'Mire',10,'regen');
mv('Coal Nudge',30,97,'Cinder',3);mv('Ember Script',44,94,'Cinder',5,'burn');mv('Kiln Rush',58,89,'Cinder',6,'priority');mv('Scoria Rain',26,90,'Cinder',7,'multi');mv('Furnace Vow',76,85,'Cinder',9,'boost');mv('Ashen Mercy',0,100,'Cinder',7,'heal');
mv('Breeze Cut',29,99,'Gale',3);mv('Cloud Tumble',43,94,'Gale',5,'boost');mv('Squall Pin',55,91,'Gale',6,'shock');mv('Manywind',23,92,'Gale',7,'multi');mv('Blue Descent',74,86,'Gale',9,'frost');mv('Slipstream',0,100,'Gale',6,'priority');
mv('Dusk Tap',32,98,'Umber',3);mv('Shade Fold',0,100,'Umber',5,'barrier');mv('Gloam Bite',49,92,'Umber',5,'drain');mv('Night Bell',61,89,'Umber',7,'shock');mv('Quietus Arc',79,84,'Umber',10,'weaken');mv('Duskmend',0,100,'Umber',7,'regen');
mv('Seam Flicker',34,96,'Rift',4);mv('Parallax',47,93,'Rift',5,'priority');mv('Fray Pulse',57,90,'Rift',6,'shock');mv('Unmake',72,87,'Rift',9,'weaken');mv('Elsewhere Rain',25,90,'Rift',8,'multi');mv('Woundlight',88,80,'Rift',11,'burn');
mv('Slate Knock',30,99,'Aegis',3);mv('Glass Ward',0,100,'Aegis',5,'barrier');mv('Bastion Hum',0,100,'Aegis',6,'regen');mv('Prism Weight',52,92,'Aegis',6,'frost');mv('Citadel Fall',73,85,'Aegis',9,'weaken');mv('Mirror Wake',42,95,'Aegis',6,'boost');
</script>
<script>
const species=[
['Vellune','A lantern-pod that floats toward unspoken wishes. Its inner glow pulses when paths divide.','Lume',[43,12,10,13],['Glimmer Prick','Halo Mend','Sunthread','Dawn Chorus'],18,'Prism Husk'],
['Mossquill','It writes curling green marks in wet soil with its tail. No two marks have ever repeated.','Mire',[49,11,13,9],['Reed Lash','Verdant Sip','Silt Bloom','Root Covenant'],20,'Loam Pearl'],
['Cindlet','A warm shale mote with a furnace-heart. It sleeps in circles of its own cooling sparks.','Cinder',[45,14,9,12],['Coal Nudge','Ember Script','Kiln Rush','Scoria Rain'],19,'Ember Seed'],
['Zephyroo','A hollow-furred hopper that cages tiny winds in its ears. Its leaps leave rings in mist.','Gale',[41,12,9,16],['Breeze Cut','Cloud Tumble','Squall Pin','Manywind'],22,'Sky Filament'],
['Murkrin','A shy duskling whose shadow arrives a moment early. It hoards the last colors of evening.','Umber',[46,13,11,12],['Dusk Tap','Shade Fold','Gloam Bite','Night Bell'],23,'Gloam Lens'],
['Frayfin','It swims through hairline cracks in space, surfacing wherever glass has chimed.','Rift',[40,15,8,17],['Seam Flicker','Parallax','Fray Pulse','Elsewhere Rain'],24,'Seam Needle'],
['Platel','A many-sided grazer that grows clear armor from mineral song. It is almost impossible to startle.','Aegis',[55,10,17,7],['Slate Knock','Glass Ward','Bastion Hum','Prism Weight'],25,'Ward Shard'],
['Solmidge','A bright-winged mote born at the edge of dawn. Colonies chart silent spirals over still water.','Lume',[38,13,8,18],['Glimmer Prick','Sunthread','Starwell','Lucent Lance'],0,''],
['Brinewisp','It gathers dew into a cloak and rolls over marsh grass. Old wells answer its bubbling call.','Mire',[52,10,14,8],['Reed Lash','Bog Snare','Verdant Sip','Fen Crown'],0,''],
['Flaretail','A quick clinker with a comet-shaped tail. It races its own sparks down stone slopes.','Cinder',[44,16,8,16],['Coal Nudge','Kiln Rush','Furnace Vow','Scoria Rain'],27,'Ember Seed'],
['Aeralith','This buoyant stone whistles in seven tones as weather changes. Travelers once read storms by its orbit.','Gale',[50,12,14,13],['Breeze Cut','Cloud Tumble','Blue Descent','Slipstream'],0,''],
['Noctuft','A velvet bundle that drinks echoes after sunset. Its purr makes nearby memories feel pleasantly distant.','Umber',[48,13,12,14],['Dusk Tap','Gloam Bite','Duskmend','Quietus Arc'],0,''],
['Kinkray','It folds itself into impossible angles to shelter smaller echoes. Its eyes face yesterday and tomorrow.','Rift',[47,15,10,15],['Seam Flicker','Fray Pulse','Unmake','Woundlight'],0,''],
['Cairnix','A patient six-footed cairn that remembers every road it crosses. Moss refuses to grow on its crown.','Aegis',[61,12,18,7],['Slate Knock','Glass Ward','Mirror Wake','Citadel Fall'],0,''],
['Aurivane','Ribbon fins harvest stray radiance from the air. It releases the light as soft notes at midnight.','Lume',[53,15,11,15],['Glimmer Prick','Dawn Chorus','Halo Mend','Lucent Lance'],0,''],
['Thornwhorl','A rolling knot of thornless vines whose blooms point toward hidden water. It never roots twice.','Mire',[58,14,16,10],['Reed Lash','Silt Bloom','Root Covenant','Fen Crown'],0,''],
['Pyroclastel','Its ceramic shell bears maps of every fire it has crossed. Fresh lines glow before danger.','Cinder',[57,18,13,11],['Ember Script','Scoria Rain','Furnace Vow','Ashen Mercy'],0,''],
['Veilgrazer','A pale wanderer that feeds on turbulence near the world’s edge. Its steps briefly calm the air.','Aegis','Rift',[68,15,20,10],['Prism Weight','Mirror Wake','Elsewhere Rain','Citadel Fall'],0,''],
['Vellucent','Its lantern has opened into a miniature horizon. Lost travelers dream of its exact location.','Lume','Gale',[67,20,15,19],['Sunthread','Halo Mend','Dawn Chorus','Lucent Lance'],0,''],
['Mirecallig','The marks on its fronds can coax dormant seeds awake. Whole wetlands subtly lean toward it.','Mire','Aegis',[75,16,22,12],['Silt Bloom','Verdant Sip','Root Covenant','Fen Crown'],0,''],
['Cinerune','Its molten seams arrange into a language understood only by cooling stone. Each battle adds a word.','Cinder','Rift',[69,24,14,18],['Ember Script','Kiln Rush','Furnace Vow','Woundlight'],0,''],
['Tempestral','Four wind-rings orbit its long ears, each carrying weather from a different height.','Gale','Lume',[64,18,14,26],['Cloud Tumble','Squall Pin','Manywind','Blue Descent'],0,''],
['Umbroracle','It steps from its own shadow to listen at sealed doors. The silver eye on its brow never closes.','Umber','Rift',[70,22,16,18],['Shade Fold','Gloam Bite','Night Bell','Quietus Arc'],0,''],
['Paradoxin','Its body is a courteous disagreement between several possible shapes. All of them watch over the same friends.','Rift','Umber',[63,26,13,25],['Parallax','Fray Pulse','Unmake','Elsewhere Rain'],0,''],
['Aegidrome','A walking sanctuary faceted from resonant crystal. Blows against it become distant bell tones.','Aegis','Lume',[84,17,29,10],['Glass Ward','Bastion Hum','Mirror Wake','Citadel Fall'],0,''],
['Cometail','Its wake resembles a burning constellation drawn one spark at a time. It never takes the shortest road.','Cinder','Gale',[66,27,12,25],['Kiln Rush','Scoria Rain','Furnace Vow','Ashen Mercy'],0,''],
['Meridion','A calm colossus assembled from the sky’s discarded borders. It gazes beyond the lattice without fear.','Aegis','Rift',[94,25,27,17],['Mirror Wake','Prism Weight','Woundlight','Citadel Fall'],0,'']
].map((x,i)=>({id:i,name:x[0],lore:x[1],aff:Array.isArray(x[2])?x[2]:[x[2],...(typeof x[3]==='string'?[x[3]]:[])],base:Array.isArray(x[3])?x[3]:x[4],moves:Array.isArray(x[4])?x[4]:x[5],evo:Array.isArray(x[3])?x[5]:x[6],cat:Array.isArray(x[3])?x[6]:x[7]}));
// normalize two-affinity entries authored in compact form
species[17].aff=['Aegis','Rift'];species[17].base=[68,15,20,10];species[17].moves=['Prism Weight','Mirror Wake','Elsewhere Rain','Citadel Fall'];species[17].evo=0;species[17].cat='';
for(let i=18;i<27;i++){species[i].aff=species[i].aff.filter(x=>AFF[x]);}
const evoMap={0:18,1:19,2:20,3:21,4:22,5:23,6:24,9:25,17:26};
const shop=[['Hush Loop','caps',1,24,'A simple binding loop.'],['True Loop','caps2',1,65,'A tuned loop with better hold.'],['Crown Loop','caps3',1,145,'A rare flawless binding.'],['Dewglass','dew',1,22,'Restore 30 HP.'],['Fullsong Vial','full',1,72,'Restore all HP.'],['Ember Seed','Ember Seed',1,90,'A warm transformation catalyst.'],['Prism Husk','Prism Husk',1,90,'A radiant transformation catalyst.'],['Loam Pearl','Loam Pearl',1,90,'A living transformation catalyst.'],['Sky Filament','Sky Filament',1,100,'A weightless transformation catalyst.'],['Gloam Lens','Gloam Lens',1,100,'A dusk transformation catalyst.'],['Seam Needle','Seam Needle',1,110,'A rift transformation catalyst.'],['Ward Shard','Ward Shard',1,110,'A crystal transformation catalyst.']];
function fresh(){return {v:1,zone:0,x:2,y:6,atShrine:true,team:[],reserve:[],seen:{},bound:{},anchors:0,bonus:0,story:0,coins:90,inv:{caps:6,caps2:1,caps3:0,dew:4,full:1,wayglass:1,'Ember Seed':0,'Prism Husk':0,'Loam Pearl':0,'Sky Filament':0,'Gloam Lens':0,'Seam Needle':0,'Ward Shard':0},tutorial:0,steps:0,post:false,settings:{mute:false},lastSave:Date.now()}}
let G=fresh(), mode='title', current=null, keys={}, last=0, particles=[], worldTime=0, moving=false, dialogQueue=[], audio=null, rhythmTimer=null;
function makeMon(id,lv){const s=species[id], iv=()=>rnd(-2,3), stats=s.base.map((b,i)=>Math.max(5,Math.floor(b+(lv-1)*(i?1.28:2.65)+iv())));return {id,lv,xp:0,hp:stats[0],stats,moves:s.moves.slice(0,Math.min(4,2+Math.floor(lv/8))),pp:{},status:{},prof:{},uid:Date.now()+Math.random()}}
function recalc(m,heal=false){const s=species[m.id],old=m.stats?.[0]||1,r=m.stats=s.base.map((b,i)=>Math.max(5,Math.floor(b+(m.lv-1)*(i?1.28:2.65)+(i===0?(G.bonus||0)*3:0)));m.hp=heal?r[0]:clamp(m.hp+(r[0]-old),0,r[0]);}
function aff(m){return species[m.id].aff[0]}
function effectiveness(at,de){let v=1;if((at==='Lume'&&de==='Mire')||(at==='Mire'&&de==='Cinder')||(at==='Cinder'&&de==='Lume'))v=1.6;if(at==='Umber'&&(de==='Lume'||de==='Cinder'))v=1.6;if(at==='Gale'&&(de==='Umber'||de==='Mire'))v=1.6;if(at==='Rift'&&de!=='Rift')v=1.18;if(de==='Aegis')v=at==='Rift'?1.6:.72;return v}
function save(){try{G.atShrine=true;G.lastSave=Date.now();localStorage.setItem('veyraSave',JSON.stringify(G));toast('Binding inscribed');}catch(e){toast('Memory could not be inscribed')}}
function load(){try{let x=JSON.parse(localStorage.getItem('veyraSave'));if(x){G=Object.assign(fresh(),x);return true}}catch(e){}return false}
function toast(t){let e=$('#toast');e.textContent=t;e.classList.add('show');clearTimeout(e._t);e._t=setTimeout(()=>e.classList.remove('show'),1800)}
function showDialog(who,text,cb){dialogQueue.push({who,text,cb});if(dialogQueue.length===1)nextDialog()}
function nextDialog(){let d=dialogQueue[0];if(!d){$('#dialog').classList.add('hidden');return}let e=$('#dialog');e.innerHTML=`<b>${d.who}</b><p>${d.text}</p><small style="color:#6f8798">tap or press space ▸</small>`;e.classList.remove('hidden')}
function advanceDialog(){if(!dialogQueue.length)return;let d=dialogQueue.shift();$('#dialog').classList.add('hidden');d.cb&&d.cb();setTimeout(nextDialog,60)}
$('#dialog').onclick=advanceDialog;
function modal(html){$('#modal').innerHTML=`<div class="panel">${html}</div>`;$('#modal').classList.remove('hidden')}
function closeModal(){if(mode==='choice')return;$('#modal').classList.add('hidden');$('#modal').innerHTML='';if(mode==='menu')mode='world'}
function beginChoice(){mode='choice';modal(`<div class="menuhead"><h2>Choose your first echo</h2></div><p style="color:var(--dim)">The hushwell offers three answering notes. Each grows differently, but none walks alone.</p><div class="choicegrid">${[0,1,2].map(i=>monCard(makeMon(i,5),true)+`<button class="btn primary" style="width:100%;margin-top:8px" onclick="chooseStarter(${i})">Bind ${species[i].name}</button>`).join('')}</div>`)}
function chooseStarter(i){G.team=[makeMon(i,5)];G.seen[i]=1;G.bound[i]=1;G.tutorial=1;mode='world';$('#modal').classList.add('hidden');startWorld();showDialog('Ilyr, Wellkeeper',`The ${species[i].name} answered. Move through the reeds. When a wild echo draws near, choose a technique; weaken it before using a Hush Loop.`,()=>{});}
function startNew(){G=fresh();$('#title').classList.add('hidden');beginChoice()}
function continueGame(){if(!load())return toast('No binding found');$('#title').classList.add('hidden');startWorld();}
$('#newBtn').onclick=startNew;$('#continueBtn').onclick=continueGame;$('#continueBtn').disabled=!localStorage.getItem('veyraSave');
</script>
<script>
const wc=$('#world'),wctx=wc.getContext('2d'),bc=$('#battle'),bctx=bc.getContext('2d');let DPR=1,W=0,H=0;
function resize(){DPR=Math.min(2,devicePixelRatio||1);W=innerWidth;H=innerHeight;for(const c of [wc,bc]){c.width=W*DPR;c.height=H*DPR;c.style.width=W+'px';c.style.height=H+'px';c.getContext('2d').setTransform(DPR,0,0,DPR,0,0)}}addEventListener('resize',resize);resize();
function startWorld(){mode='world';wc.style.display='block';bc.style.display='none';$('#battleUI').classList.add('hidden');$('#topbar,#hud,#touch').forEach?.(()=>{});$('#topbar').classList.remove('hidden');$('#hud').classList.remove('hidden');$('#touch').classList.remove('hidden');G.cleared=G.cleared||{};G.nodes=G.nodes||{};G.x=clamp(G.x||2,1,18);G.y=clamp(G.y||6,1,10);updateHUD();initAudio();if(!window._veyraLoop){window._veyraLoop=1;requestAnimationFrame(loop)}}
function updateHUD(){let z=zones[G.zone];$('#zoneName').textContent=z.name;$('#zoneSub').textContent=`${z.sub} · ${Object.keys(G.bound).length}/8 echoes · ${G.anchors}/4 anchors`;$('#goalProgress').style.width=Math.min(100,((Object.keys(G.bound).length/8+G.anchors/4)/2)*100)+'%'}
function tileGeom(){let ts=Math.min(W/21,H/13),ox=(W-ts*20)/2,oy=(H-ts*12)/2;return{ts,ox,oy}}
function blocked(x,y){if(x<1||x>18||y<1||y>10)return true;let z=G.zone;if((x===6&&y>1&&y<9&&y!==5)||(x===13&&y>3&&y<11&&y!==7))return true;if((x+y*3+z*7)%23===0)return true;return false}
function move(dx,dy){if(mode!=='world'||moving||dialogQueue.length)return;let nx=G.x+dx,ny=G.y+dy;if(blocked(nx,ny)){tone(100,.05);return}moving=true;G.x=nx;G.y=ny;if(nx!==2||ny!==6)G.atShrine=false;G.steps++;setTimeout(()=>moving=false,105);tone(260+G.zone*25,.025,.02);onTile();}
function onTile(){let{x,y,zone:z}=G;if(x===2&&y===6){shrine();return}if(x===9&&y===3&&!G.nodes[z]){G.nodes[z]=1;storyNode();return}if(x===17&&y===2&&!G.cleared[z]){guardianBattle();return}if(x===18&&y===9&&G.cleared[z]){if(z<5){G.zone++;G.x=1;G.y=9;updateHUD();vignette(zones[G.zone].amb);toast(zones[G.zone].name)}return}let grace=G.tutorial===1&&G.steps<3;if(!grace&&Math.random()<zones[z].rate){wildBattle()}else if(G.tutorial===1&&G.steps===3){wildBattle(true)}}
function storyNode(){let z=G.zone,stage=Math.min(3,z);if(stage>=G.story){G.story=stage+1;vignette(zones[z].amb);showDialog('Lattice Memory',zones[z].story,()=>{if(stage===3)showDialog('Ilyr, distant',`Eight voices can steady the lattice. Four anchors can hold it. Beyond that, the Pale Meridian will open.`,null)})}}
function vignette(c){document.body.style.setProperty('--vig',c+'88');document.body.classList.add('vignette');setTimeout(()=>document.body.classList.remove('vignette'),1100)}
function shrine(){for(const m of G.team){recalc(m,true);m.status={}}G.atShrine=true;save();tone(520,.4,.08);modal(`<div class="menuhead"><h2>Hushwell</h2><button class="btn" onclick="closeModal()">Return</button></div><p>The well restores every bound echo and holds your last safe memory.</p><div class="btnrow"><button class="btn primary" onclick="openShop()">Well Exchange</button><button class="btn" onclick="openTeam()">Team</button><button class="btn" onclick="save()">Inscribe</button></div>`);mode='menu'}
function portalHint(ctx,x,y,txt){ctx.save();ctx.fillStyle='#ffffffbd';ctx.font='11px system-ui';ctx.textAlign='center';ctx.fillText(txt,x,y);ctx.restore()}
function drawWorld(t){let z=zones[G.zone],g=wctx,gr=g.createLinearGradient(0,0,W,H);gr.addColorStop(0,z.grad[0]);gr.addColorStop(1,z.grad[1]);g.fillStyle=gr;g.fillRect(0,0,W,H);let{ts,ox,oy}=tileGeom();g.save();g.translate(ox,oy);for(let y=0;y<12;y++)for(let x=0;x<20;x++){let edge=x===0||y===0||x===19||y===11,bl=blocked(x,y);g.fillStyle=edge?'#02080a88':bl?'#07101399':((x+y)%2?'#ffffff08':'#ffffff04');g.fillRect(x*ts+1,y*ts+1,ts-2,ts-2);if(bl&&!edge){g.fillStyle=z.amb+'25';g.beginPath();g.arc((x+.5)*ts,(y+.55)*ts,ts*.26,0,7);g.fill()}}
// well
let sx=2.5*ts,sy=6.5*ts;g.strokeStyle='#9fffe7';g.lineWidth=2;g.beginPath();g.arc(sx,sy,ts*.31,0,7);g.stroke();g.fillStyle='#8fffe733';g.beginPath();g.arc(sx,sy,ts*.23,0,7);g.fill();portalHint(g,sx,sy-ts*.38,'HUSHWELL');
// memory node
if(!G.nodes[G.zone]){let nx=9.5*ts,ny=3.5*ts;g.fillStyle=z.amb;g.shadowColor=z.amb;g.shadowBlur=18;g.beginPath();g.moveTo(nx,ny-ts*.3);g.lineTo(nx+ts*.25,ny);g.lineTo(nx,ny+ts*.3);g.lineTo(nx-ts*.25,ny);g.fill();g.shadowBlur=0}
// guardian
if(!G.cleared[G.zone]){let ax=17.5*ts,ay=2.5*ts;g.strokeStyle='#ffcc89';g.lineWidth=3;g.beginPath();for(let i=0;i<6;i++){let a=i*Math.PI/3+(t/1200);g.lineTo(ax+Math.cos(a)*ts*.35,ay+Math.sin(a)*ts*.35)}g.closePath();g.stroke();portalHint(g,ax,ay-ts*.48,G.zone<4?'ANCHOR':'WARDEN')}
else{let px=18.5*ts,py=9.5*ts;g.fillStyle='#bff';g.shadowColor='#8ff';g.shadowBlur=25;g.fillRect(px-ts*.12,py-ts*.42,ts*.24,ts*.84);g.shadowBlur=0;portalHint(g,px-ts*.3,py-ts*.52,G.zone<5?'NEXT VERGE':'MERIDIAN')}
// player
let px=(G.x+.5)*ts,py=(G.y+.5)*ts,bob=Math.sin(t/180)*ts*.05;g.shadowColor='#7df4e5';g.shadowBlur=18;g.fillStyle='#dffff9';g.beginPath();g.arc(px,py+bob,ts*.25,0,7);g.fill();g.fillStyle='#315f72';g.beginPath();g.arc(px-ts*.08,py-ts*.04+bob,ts*.055,0,7);g.arc(px+ts*.08,py-ts*.04+bob,ts*.055,0,7);g.fill();g.restore();
// ambient motes
if(particles.length<60&&Math.random()<.35)particles.push({x:Math.random()*W,y:H+5,v:Math.random()*.5+.2,s:Math.random()*2+1,a:1});for(let i=particles.length-1;i>=0;i--){let p=particles[i];p.y-=p.v;p.x+=Math.sin(t/600+i)*.2;p.a-=.0018;g.fillStyle=z.amb+Math.floor(clamp(p.a,0,1)*180).toString(16).padStart(2,'0');g.beginPath();g.arc(p.x,p.y,p.s,0,7);g.fill();if(p.a<=0||p.y<0)particles.splice(i,1)}}
function loop(t){if(t-last<16){requestAnimationFrame(loop);return}last=t;worldTime=t;if(mode==='world'||mode==='menu'){drawWorld(t)}if(mode==='battle')drawBattle(t);requestAnimationFrame(loop)}
addEventListener('keydown',e=>{if(e.key===' '&&dialogQueue.length){advanceDialog();return}if(mode==='world'){if(['ArrowUp','w','W'].includes(e.key))move(0,-1);if(['ArrowDown','s','S'].includes(e.key))move(0,1);if(['ArrowLeft','a','A'].includes(e.key))move(-1,0);if(['ArrowRight','d','D'].includes(e.key))move(1,0);if(e.key==='Escape')openMenu()}else if(e.key==='Escape'&&mode==='menu')closeModal()});
$$('.dpad').forEach(b=>{b.onpointerdown=e=>{e.preventDefault();let k=b.dataset.k;move(k==='ArrowLeft'?-1:k==='ArrowRight'?1:0,k==='ArrowUp'?-1:k==='ArrowDown'?1:0)}});
$('#menuBtn').onclick=openMenu;
function openMenu(tab='team'){if(mode!=='world'&&mode!=='menu')return;mode='menu';modal(`<div class="menuhead"><h2>Binding Ledger</h2><button class="btn" onclick="closeModal()">Return</button></div><div class="tabs"><button class="tab ${tab==='team'?'on':''}" onclick="openTeam()">Team</button><button class="tab ${tab==='bag'?'on':''}" onclick="openBag()">Satchel</button><button class="tab ${tab==='dex'?'on':''}" onclick="openDex()">Echoes</button><button class="tab ${tab==='map'?'on':''}" onclick="openMap()">Chart</button></div><div id="menuBody"></div>`);if(tab==='team')renderTeam();if(tab==='bag')renderBag();if(tab==='dex')renderDex();if(tab==='map')renderMap()}
function openTeam(){openMenu('team')}function openBag(){openMenu('bag')}function openDex(){openMenu('dex')}function openMap(){openMenu('map')}
function monCard(m,brief=false){let s=species[m.id],hp=m.hp/m.stats[0]*100;return `<div class="card" style="--c:${AFF[aff(m)]}"><div class="orb">${glyph[aff(m)]}</div><h3>${s.name} <small>Lv.${m.lv}</small></h3><span class="tag">${s.aff.join(' · ')}</span>${brief?`<p>${s.lore}</p>`:`<div class="bar hp"><i class="${hp<25?'low':''}" style="width:${hp}%"></i></div><small>${m.hp}/${m.stats[0]} HP · ATK ${m.stats[1]} · DEF ${m.stats[2]} · SPD ${m.stats[3]}</small><p>${s.lore}</p>`}</div>`}
function renderTeam(){let reserve=G.reserve.length?`<h3>Resonance Reserve</h3><div class="grid">${G.reserve.map((m,i)=>monCard(m)+`<button class="btn" onclick="swapReserve(${i})">Join team</button>`).join('')}</div>`:'';$('#menuBody').innerHTML=`<p>${G.team.length}/6 active echoes. Select a catalyst-ready echo to transform.</p><div class="grid">${G.team.map((m,i)=>monCard(m)+`<div class="btnrow"><button class="btn" onclick="details(${i})">Techniques</button>${canEvo(m)?`<button class="btn primary" onclick="evolve(${i})">Transform</button>`:''}</div>`).join('')}</div>${reserve}`}
function canEvo(m){let e=evoMap[m.id],s=species[m.id];return e&&m.lv>=s.evo&&(!s.cat||G.inv[s.cat]>0)}
function evolve(i){let m=G.team[i],to=evoMap[m.id],s=species[m.id];if(!canEvo(m))return;if(s.cat)G.inv[s.cat]--;m.id=to;m.moves=species[to].moves.slice();G.bound[to]=1;G.seen[to]=1;recalc(m,true);tone(660,.7,.12);vignette(AFF[aff(m)]);toast(`${species[to].name} awakened`);renderTeam();updateHUD()}
function swapReserve(i){if(G.team.length>=6)return toast('Active team is full');G.team.push(G.reserve.splice(i,1)[0]);renderTeam()}
function details(i){let m=G.team[i];modal(`<div class="menuhead"><h2>${species[m.id].name} · Techniques</h2><button class="btn" onclick="openTeam()">Back</button></div><div class="grid">${m.moves.map(n=>{let x=M[n];return `<div class="card" style="--c:${AFF[x.aff]}"><h3>${n}</h3><span class="tag">${x.aff}</span><p>Power ${x.pow||'—'} · Accuracy ${x.acc}% · Energy ${x.cost}</p><p>${x.effect==='none'?'A direct resonance technique.':x.effect}</p></div>`}).join('')}</div>`)}
function renderBag(){let names={caps:'Hush Loop',caps2:'True Loop',caps3:'Crown Loop',dew:'Dewglass',full:'Fullsong Vial',wayglass:'Wayglass (field chart)'};$('#menuBody').innerHTML=`<div class="grid">${Object.entries(G.inv).map(([k,v])=>`<div class="card"><h3>${names[k]||k}</h3><p>Held: ${v}</p>${['dew','full'].includes(k)?`<button class="btn" ${v<1?'disabled':''} onclick="useRest('${k}')">Use on wounded</button>`:''}</div>`).join('')}</div><p>Shards: <b>${G.coins}</b></p><div class="btnrow"><button class="btn" onclick="save()">Manual Inscription</button><button class="btn warn" onclick="if(confirm('Return to title?'))location.reload()">Title</button></div>`}function useRest(k){let m=G.team.find(x=>x.hp<x.stats[0]);if(!m)return toast('No echo is wounded');if(!G.inv[k])return;G.inv[k]--;m.hp=k==='full'?m.stats[0]:Math.min(m.stats[0],m.hp+30);tone(540,.2);renderBag()}
function renderDex(){ $('#menuBody').innerHTML=`<p>${Object.keys(G.bound).length} bound · ${Object.keys(G.seen).length} observed · 27 known forms</p><div class="grid">${species.map((s,i)=>G.seen[i]?`<div class="card" style="--c:${AFF[s.aff[0]]}"><div class="orb">${glyph[s.aff[0]]}</div><h3>${G.bound[i]?'✦ ':''}${s.name}</h3><span class="tag">${s.aff.join(' · ')}</span><p>${s.lore}</p></div>`:`<div class="card"><h3>Unheard Echo</h3><p>Its note has not entered your ledger.</p></div>`).join('')}</div>`}
function renderMap(){ $('#menuBody').innerHTML=`<div class="grid">${zones.map((z,i)=>`<div class="card" style="--c:${z.amb};opacity:${i<=G.zone?1:.35}"><h3>${i<=G.zone?z.name:'Uncharted Verge'}</h3><p>${i<=G.zone?z.sub:'The lattice veils this place.'}</p><span class="tag">${G.cleared[i]?'WARD STILLED':i===G.zone?'CURRENT':''}</span>${G.post?`<button class="btn" onclick="travel(${i})">Walk here</button>`:''}</div>`).join('')}</div><p>Objective: bind 8 unique echoes and still 4 anchors. Then cross the Pale Meridian.</p>`}function travel(i){if(!G.post)return;G.zone=i;G.x=2;G.y=6;closeModal();mode='world';updateHUD();vignette(zones[i].amb)}
function openShop(){modal(`<div class="menuhead"><h2>Well Exchange · ${G.coins} shards</h2><button class="btn" onclick="shrine()">Back</button></div><div class="grid">${shop.map((x,i)=>`<div class="card"><h3>${x[0]}</h3><p>${x[4]}</p><button class="btn" onclick="buy(${i})">${x[3]} shards</button></div>`).join('')}</div>`)}
function buy(i){let x=shop[i];if(G.coins<x[3])return toast('Not enough shards');G.coins-=x[3];G.inv[x[1]]=(G.inv[x[1]]||0)+x[2];tone(740,.1);openShop()}
</script>
<script>
function wildBattle(tutorial=false){let z=zones[G.zone],count=tutorial?1:(z>1&&Math.random()<.32?rnd(2,Math.min(3,1+Math.floor(z/2))):1),en=[];for(let i=0;i<count;i++){let id=tutorial?3:pick(z.wild),lv=tutorial?4:rnd(z.min,z.max)+(G.post?rnd(3,7):0);en.push(makeMon(id,clamp(lv,1,50)));G.seen[id]=1}startBattle(en,{wild:true,tutorial})}
function guardianBattle(){let z=G.zone;if(z===5&&(G.anchors<4||Object.keys(G.bound).length<8)){showDialog('Meridian Seal',`The last line remains closed. It asks for eight bound voices and four stilled anchors.`,null);return}let ids=[6,9,12,14,16,26],base=[8,15,24,33,42,50][z],en=[makeMon(ids[z],base)];if(z>=2)en.unshift(makeMon(pick(zones[z].wild),base-2));if(z>=4)en.unshift(makeMon(pick(zones[z].wild),base-3));startBattle(en,{guardian:true,zone:z})}
function startBattle(enemies,flags){mode='battle';current={enemies,flags,active:G.team.findIndex(m=>m.hp>0),turn:0,busy:false,log:[],selected:0};if(current.active<0){G.team.forEach(m=>recalc(m,true));current.active=0}for(const m of [...G.team,...enemies]){m.energy=18;m.status=m.status||{}}wc.style.display='none';bc.style.display='block';$('#battleUI').classList.remove('hidden');$('#hud,#touch').forEach?.(()=>{});$('#hud').classList.add('hidden');$('#touch').classList.add('hidden');logBattle(flags.guardian?'A warded presence bars the way.':'Wild echoes gather from the verge.');renderBattleUI();tone(flags.guardian?110:180,.35,.08)}
function aliveEnemies(){return current.enemies.filter(m=>m.hp>0)}function ally(){return G.team[current.active]}function target(){let a=aliveEnemies();return a[current.selected%a.length]}
function logBattle(s){if(!current)return;current.log.unshift(s);current.log=current.log.slice(0,5)}
function statusText(m){let s=Object.keys(m.status||{}).filter(k=>m.status[k]);return s.length?s.join(' · '):'clear'}
function renderBattleUI(){if(!current)return;let a=ally(),e=target()||current.enemies[0];let moves=a.moves.map((n,i)=>{let x=M[n],disabled=a.energy<x.cost||current.busy;return `<button class="move" style="--mc:${AFF[x.aff]}" ${disabled?'disabled':''} onclick="chooseMove(${i})"><b>${glyph[x.aff]} ${n}</b><small>${x.aff} · ${x.pow||'ward'} power · ${x.acc}% · ${x.cost}E</small></button>`}).join('');$('#battleUI').innerHTML=`<div class="bcard enemycard"><b>${species[e.id].name} <small>Lv.${e.lv}</small></b><div class="bar hp"><i class="${e.hp/e.stats[0]<.25?'low':''}" style="width:${100*e.hp/e.stats[0]}%"></i></div><small>${e.hp}/${e.stats[0]} HP · ${statusText(e)} · foes ${aliveEnemies().length}</small></div><div class="bcard allycard"><b>${species[a.id].name} <small>Lv.${a.lv}</small></b><div class="bar hp"><i class="${a.hp/a.stats[0]<.25?'low':''}" style="width:${100*a.hp/a.stats[0]}%"></i></div><small>${a.hp}/${a.stats[0]} HP · ${a.energy}/18 energy · ${statusText(a)}</small></div><div class="log">${current.log.map(x=>`<div>› ${x}</div>`).join('')}</div><div class="battleCommands">${moves}<div class="battleTools"><button class="btn" ${current.busy?'disabled':''} onclick="switchMenu()">Switch</button><button class="btn" ${current.busy?'disabled':''} onclick="centerChord()">Center</button><button class="btn primary" ${!current.flags.wild||current.busy?'disabled':''} onclick="captureMenu()">Bind</button><button class="btn" ${current.flags.guardian||current.busy?'disabled':''} onclick="runAway()">Withdraw</button></div></div>`}
function effStats(m){return {atk:Math.floor(m.stats[1]*(m.status.burn?.5:1)*(m.status.weaken?.75:1)),def:m.stats[2],spd:Math.floor(m.stats[3]*(m.status.frost?.5:1)*(m.status.boost?1.45:1))}}
function chooseMove(i){if(current.busy)return;let a=ally(),move=M[a.moves[i]];if(a.energy<move.cost)return toast('Not enough energy');current.busy=true;let acts=[{m:a,side:'ally',move,target:target()}];for(const e of aliveEnemies()){let opts=e.moves.map(n=>M[n]).filter(x=>e.energy>=x.cost);let em=pick(opts.length?opts:e.moves.map(n=>M[n]));acts.push({m:e,side:'enemy',move:em,target:a})}acts.sort((x,y)=>((y.move.effect==='priority')-(x.move.effect==='priority'))||(effStats(y.m).spd-effStats(x.m).spd));resolveQueue(acts,0)}
function resolveQueue(q,i){if(!current)return;if(i>=q.length){endRound();return}let act=q[i];if(act.m.hp<=0||act.target.hp<=0){setTimeout(()=>resolveQueue(q,i+1),120);return}if(act.m.status.shock&&Math.random()<.28){logBattle(`${species[act.m.id].name} jolted still.`);tone(85,.13);renderBattleUI();setTimeout(()=>resolveQueue(q,i+1),450);return}performMove(act,()=>resolveQueue(q,i+1))}
function performMove(act,done){let{m,move,targeт}=act,t=act.target;m.energy=Math.max(0,m.energy-move.cost);logBattle(`${species[m.id].name} used ${move.name}.`);if(Math.random()*100>move.acc){logBattle('The resonance slipped wide.');renderBattleUI();setTimeout(done,500);return}let hit=()=>{if(move.pow>0){if(t.status.barrier){delete t.status.barrier;logBattle('A barrier deflected the strike.');tone(880,.12);return}let es=effStats(m),ds=effStats(t),mult=effectiveness(move.aff,aff(t)),stab=species[m.id].aff.includes(move.aff)?1.25:1,crit=Math.random()<.08,damage=Math.max(1,Math.floor((.4*m.lv*es.atk/ds.def*mult)+rnd(1,4))*stab*(crit?1.55:1));t.hp=Math.max(0,t.hp-damage);if(current.flags.tutorial&¤t.enemies.includes(t)&&t.hp===0){t.hp=1;logBattle('Its note waits—use a Hush Loop now.')}floatDamage(t,damage,crit,mult);if(mult>1.2){burst(AFF[move.aff]);logBattle('The affinity rang true!')}if(crit)logBattle('A brilliant fracture!');if(move.effect==='drain')m.hp=Math.min(m.stats[0],m.hp+Math.ceil(damage*.45));}applyEffect(move,m,t)};if(move.effect==='multi'&&move.pow){let n=rnd(2,3);for(let j=0;j<n;j++)hit();logBattle(`${n} echoes struck in sequence.`)}else hit();renderBattleUI();setTimeout(done,580)}
function applyEffect(x,u,t){if(x.effect==='burn'&&Math.random()<.45)t.status.burn=3;if(x.effect==='frost'&&Math.random()<.5)t.status.frost=3;if(x.effect==='shock'&&Math.random()<.45)t.status.shock=3;if(x.effect==='barrier')u.status.barrier=1;if(x.effect==='regen')u.status.regen=3;if(x.effect==='boost')u.status.boost=3;if(x.effect==='weaken'&&Math.random()<.6)t.status.weaken=3;if(x.effect==='heal')u.hp=Math.min(u.stats[0],u.hp+Math.floor(u.stats[0]*.38))}
function endRound(){if(!current)return;for(const m of [...G.team,...current.enemies]){if(m.hp<=0)continue;if(m.status.burn){let d=Math.max(1,Math.floor(m.stats[0]*.07));m.hp=Math.max(0,m.hp-d);logBattle(`${species[m.id].name}'s ember-mark dealt ${d}.`)}if(m.status.regen)m.hp=Math.min(m.stats[0],m.hp+Math.max(2,Math.floor(m.stats[0]*.07)));for(const k of ['burn','frost','shock','regen','boost','weaken'])if(m.status[k]&&--m.status[k]<=0)delete m.status[k];m.energy=Math.min(18,m.energy+3)}current.turn++;if(aliveEnemies().length===0)return victory();if(ally().hp<=0){let next=G.team.findIndex(m=>m.hp>0);if(next<0)return defeat();current.active=next;logBattle(`${species[ally().id].name} enters the chord.`)}current.busy=false;renderBattleUI()}
function floatDamage(m,d,crit,mult){let enemy=current.enemies.includes(m),el=document.createElement('div');el.className='float';el.style.left=(enemy?'73%':'25%');el.style.top=(enemy?'25%':'58%');el.style.color=crit?'#ffe777':mult>1.2?'#8ffff0':'#fff';el.textContent=(crit?'✦ ':'−')+d;document.body.append(el);setTimeout(()=>el.remove(),1000);$('#game').classList.add(d>20?'shake':'flash');setTimeout(()=>$('#game').classList.remove('shake','flash'),300)}
function burst(c){for(let i=0;i<12;i++)particles.push({battle:true,x:W*.7,y:H*.3,vx:(Math.random()-.5)*6,vy:(Math.random()-.5)*6,c,life:1})}
function centerChord(){if(current.busy)return;current.busy=true;ally().energy=Math.min(18,ally().energy+5);logBattle(`${species[ally().id].name} centers its resonance.`);let a=ally(),q=aliveEnemies().map(e=>({m:e,side:'enemy',move:pick((e.moves.map(n=>M[n]).filter(x=>e.energy>=x.cost).length?e.moves.map(n=>M[n]).filter(x=>e.energy>=x.cost):e.moves.map(n=>M[n]))),target:a})).sort((x,y)=>effStats(y.m).spd-effStats(x.m).spd);resolveQueue(q,0)}
function switchMenu(){if(current.busy)return;modal(`<div class="menuhead"><h2>Shift the chord</h2><button class="btn" onclick="closeBattleModal()">Back</button></div><div class="grid">${G.team.map((m,i)=>monCard(m)+`<button class="btn" ${m.hp<=0||i===current.active?'disabled':''} onclick="doSwitch(${i})">Enter</button>`).join('')}</div>`)}
function closeBattleModal(){$('#modal').classList.add('hidden');$('#modal').innerHTML=''}
function doSwitch(i){closeBattleModal();current.active=i;current.busy=true;logBattle(`${species[ally().id].name} enters the chord.`);let e=target(),em=pick(e.moves.map(n=>M[n]));performMove({m:e,move:em,target:ally()},()=>{current.busy=false;if(ally().hp<=0)endRound();else renderBattleUI()})}
function captureMenu(){if(current.busy)return;let e=target();modal(`<div class="menuhead"><h2>Choose a binding loop</h2><button class="btn" onclick="closeBattleModal()">Back</button></div><p>Lower vitality gives the loop a clearer note. You can hold no more than six active echoes; extras rest in resonance reserve.</p><div class="grid">${[['caps','Hush Loop',1],['caps2','True Loop',1.45],['caps3','Crown Loop',2.05]].map(x=>`<div class="card"><h3>${x[1]}</h3><p>${G.inv[x[0]]||0} held</p><button class="btn primary" ${(G.inv[x[0]]||0)<1?'disabled':''} onclick="startRhythm('${x[0]}',${x[2]})">Tune</button></div>`).join('')}</div>`)}
function startRhythm(key,quality){G.inv[key]--;let start=performance.now(),dir=1;modal(`<div class="menuhead"><h2>Match the wandering note</h2></div><p>Tap when the bright needle crosses the green chord.</p><div class="rhythm"><div class="sweet"></div><div class="needle" id="needle"></div></div><button class="btn primary" style="width:100%" onclick="finishRhythm('${key}',${quality})">BIND NOW</button>`);rhythmTimer=setInterval(()=>{let p=((performance.now()-start)%1400)/1400,pos=p<.5?p*2:2-p*2;let n=$('#needle');if(n){n.style.left=(pos*98)+'%';n.dataset.pos=pos}},16);setTimeout(()=>{if(rhythmTimer)finishRhythm(key,quality)},5000)}
function finishRhythm(key,quality){if(!rhythmTimer)return;clearInterval(rhythmTimer);rhythmTimer=null;let n=$('#needle'),pos=parseFloat(n?.dataset.pos||0),rhythm=clamp(1-Math.abs(pos-.51)*2.2,0,1),e=target(),hp=1-e.hp/e.stats[0],base=.12+hp*.58+(quality-1)*.25+rhythm*.22;if(current.flags.tutorial)base=.98;closeBattleModal();current.busy=true;tone(400+rhythm*350,.35,.1);setTimeout(()=>{if(Math.random()<base){bindTarget(e)}else{logBattle(`${species[e.id].name} broke the loop.`);let em=pick(e.moves.map(x=>M[x]));performMove({m:e,move:em,target:ally()},()=>{current.busy=false;if(ally().hp<=0)endRound();else renderBattleUI()})}},700)}
function bindTarget(e){G.bound[e.id]=1;e.hp=Math.max(1,e.hp);e.status={};if(G.team.length<6)G.team.push(e);else G.reserve.push(e);current.enemies.splice(current.enemies.indexOf(e),1);logBattle(`${species[e.id].name} joined your binding.`);vignette(AFF[aff(e)]);tone(760,.6,.12);updateHUD();if(G.tutorial===1){G.tutorial=2;showDialog('Ilyr, Wellkeeper','A clean binding. The loop does not own an echo; it gives two wandering notes a shared direction. Return to the hushwell whenever the road grows sharp.',null)}if(!aliveEnemies().length)victory(true);else{current.busy=false;renderBattleUI()}}
function runAway(){if(current.flags.guardian)return;logBattle('You loosen the chord and withdraw.');setTimeout(endBattle,400)}
function victory(bound=false){if(!current)return;let flags=current.flags,defeated=current.enemies.filter(m=>m.hp<=0),total=defeated.reduce((s,m)=>s+m.lv,0);if(!bound||defeated.length){let a=ally(),xp=Math.max(8,total*8+current.enemies.length*5),share=Math.floor(xp/Math.max(1,G.team.filter(m=>m.hp>0).length));for(const m of G.team.filter(m=>m.hp>0))gainXP(m,share);G.coins+=Math.max(6,total*2)}if(flags.guardian){G.cleared[flags.zone]=1;if(flags.zone<4){G.anchors++;G.coins+=80;giveCatalyst(flags.zone)}if(flags.zone===5){G.post=true;showEnding();return}showDialog(flags.zone<4?'Anchor Stilled':'Ward Unwoven',flags.zone<4?`The ${zones[flags.zone].name} anchor folds into a stable, singing line. Something in every bound echo grows steadier.`:`The path to ${zones[Math.min(5,flags.zone+1)].name} opens.`,null)}setTimeout(endBattle,500)}else{logBattle(bound?'The new chord settles.':'The verge falls quiet.');setTimeout(endBattle,650)}}
function gainXP(m,x){if(m.lv>=50)return;m.xp+=x;let need=m.lv*18;while(m.xp>=need&&m.lv<50){m.xp-=need;m.lv++;recalc(m,false);let s=species[m.id],idx=m.lv>=25?3:m.lv>=16?2:m.lv>=8?1:0;if(s.moves[idx]&&!m.moves.includes(s.moves[idx])&&m.moves.length<4)m.moves.push(s.moves[idx]);toast(`${s.name} reached level ${m.lv}`);need=m.lv*18}}
function giveCatalyst(z){let c=['Prism Husk','Loam Pearl','Ember Seed','Sky Filament'][z];G.inv[c]=(G.inv[c]||0)+1;G.bonus=(G.bonus||0)+1;toast(`Permanent ward: + vitality · found ${c}`);for(const m of G.team)recalc(m,false)}
function defeat(){showDialog('The chord breaks','The hushwell catches your fading note and draws you back.',null);G.x=2;G.y=6;G.team.forEach(m=>recalc(m,true));setTimeout(endBattle,700)}
function endBattle(){current=null;closeBattleModal();startWorld();updateHUD()}
function showEnding(){endBattle();G.post=true;save();setTimeout(()=>{modal(`<div style="text-align:center;padding:35px"><div class="sigil" style="width:100px;height:100px"></div><h2>THE LATTICE WAKES</h2><p style="max-width:620px;margin:15px auto;color:var(--dim);line-height:1.7">Eight voices answer the four anchors. The wound above the Pale Meridian does not close; it becomes a window, held gently by every road you walked. Ilyr hears the new sky-song and laughs for the first time in an age.</p><p><b>Free roam awakened.</b> Wild echoes now emerge stronger across every verge.</p><button class="btn primary" onclick="closeModal();mode='world'">Walk the waking world</button></div>`);mode='menu'},500)}
</script>
<script>
function drawCreature(ctx,m,x,y,scale,enemy,t){let c=AFF[aff(m)],id=m.id,seed=id*1.71;ctx.save();ctx.translate(x,y+Math.sin(t/260+seed)*8);ctx.scale(enemy?-scale:scale,scale);ctx.shadowColor=c;ctx.shadowBlur=28;let r=42+(id%4)*4;ctx.fillStyle=c+'55';ctx.beginPath();for(let i=0;i<10;i++){let a=i*Math.PI*2/10,rr=r*(i%2?0.78:1);ctx.lineTo(Math.cos(a)*rr,Math.sin(a)*rr)}ctx.closePath();ctx.fill();ctx.strokeStyle=c;ctx.lineWidth=3;ctx.stroke();ctx.shadowBlur=10;ctx.fillStyle='#0a1724';ctx.beginPath();ctx.ellipse(0,4,r*.72,r*.67,0,0,7);ctx.fill();ctx.stroke();ctx.fillStyle='#eaffff';let ey=-7;ctx.beginPath();ctx.arc(-14,ey,6,0,7);ctx.arc(14,ey,6,0,7);ctx.fill();ctx.fillStyle=c;ctx.beginPath();ctx.arc(-14,ey,2.5,0,7);ctx.arc(14,ey,2.5,0,7);ctx.fill();ctx.strokeStyle=c;ctx.lineWidth=4;let append=2+(id%4);for(let i=0;i<append;i++){let a=-Math.PI*.85+i*(Math.PI*1.7/(append-1||1));ctx.beginPath();ctx.moveTo(Math.cos(a)*r*.65,Math.sin(a)*r*.65);ctx.quadraticCurveTo(Math.cos(a)*r*1.25+Math.sin(t/300+i)*8,Math.sin(a)*r*1.15,Math.cos(a)*r*1.45,Math.sin(a)*r*1.35);ctx.stroke()}ctx.restore()}
function drawBattle(t){if(!current)return;let z=zones[G.zone],g=bctx,gr=g.createRadialGradient(W*.5,H*.45,20,W*.5,H*.5,W*.8);gr.addColorStop(0,z.grad[1]);gr.addColorStop(1,z.grad[0]);g.fillStyle=gr;g.fillRect(0,0,W,H);g.globalAlpha=.25;g.strokeStyle=z.amb;for(let i=0;i<9;i++){g.beginPath();g.ellipse(W*.5,H*.72,W*(.1+i*.07),H*(.018+i*.008),0,0,7);g.stroke()}g.globalAlpha=1;let ae=aliveEnemies();ae.forEach((m,i)=>drawCreature(g,m,W*(.68+(i-1)*.13),H*(.30+i%2*.06),Math.max(.65,1-ae.length*.08),true,t));drawCreature(g,ally(),W*.28,H*.62,1.12,false,t);for(let i=particles.length-1;i>=0;i--){let p=particles[i];if(!p.battle)continue;p.x+=p.vx;p.y+=p.vy;p.vy+=.08;p.life-=.035;g.fillStyle=p.c+Math.floor(clamp(p.life,0,1)*255).toString(16).padStart(2,'0');g.beginPath();g.arc(p.x,p.y,3+p.life*4,0,7);g.fill();if(p.life<=0)particles.splice(i,1)}}
function initAudio(){if(audio||G.settings.mute)return;try{audio=new (window.AudioContext||window.webkitAudioContext)();audio.resume();ambientPulse()}catch(e){}}
function tone(freq=300,dur=.1,vol=.04){if(G.settings.mute)return;initAudio();if(!audio)return;let o=audio.createOscillator(),gn=audio.createGain();o.type='sine';o.frequency.setValueAtTime(freq,audio.currentTime);o.frequency.exponentialRampToValueAtTime(Math.max(40,freq*.72),audio.currentTime+dur);gn.gain.setValueAtTime(vol,audio.currentTime);gn.gain.exponentialRampToValueAtTime(.0001,audio.currentTime+dur);o.connect(gn).connect(audio.destination);o.start();o.stop(audio.currentTime+dur)}
function ambientPulse(){if(!audio)return;let z=G.zone||0,base=[130.81,146.83,164.81,196,220,246.94][z];if(!G.settings.mute&&mode!=='title'){let o=audio.createOscillator(),g=audio.createGain(),f=audio.createBiquadFilter();o.type='triangle';o.frequency.value=base*(pick([1,1.25,1.5,2]));f.type='lowpass';f.frequency.value=600;g.gain.setValueAtTime(.0001,audio.currentTime);g.gain.linearRampToValueAtTime(.018,audio.currentTime+.7);g.gain.exponentialRampToValueAtTime(.0001,audio.currentTime+2.6);o.connect(f).connect(g).connect(audio.destination);o.start();o.stop(audio.currentTime+2.7)}setTimeout(ambientPulse,2800+rnd(0,1400))}
$('#muteBtn').onclick=()=>{G.settings.mute=!G.settings.mute;$('#muteBtn').textContent=G.settings.mute?'♩':'♫';if(!G.settings.mute){if(audio)audio.resume();else initAudio()}toast(G.settings.mute?'Sound rests':'Sound wakes')};
// hardening for resumed saves and long journeys
species[17].evo=42;species[17].cat='Ward Shard';
window.addEventListener('beforeunload',()=>{if(mode!=='title'&&G.atShrine)try{localStorage.setItem('veyraSave',JSON.stringify(G))}catch(e){}});
</script></body></html>
<!-- agent-meta {"model":"openai/gpt-5.6-sol","provider":"openrouter","persona":"middle","sessionId":"b8ac2d1d-f8d3-47ab-bf0a-71c5ea941ea7","tokensIn":458703,"tokensOut":23910,"tokensTotal":482613,"turns":20,"toolCalls":22,"failedToolCalls":0,"timestamp":"2026-07-12T23:42:16.774Z"} -->