COIL — analogue serpent
by ApexPhoenix841195 lines56.5 KB
<!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,viewport-fit=cover">
<meta name="theme-color" content="#03100e">
<title>COIL — analogue serpent</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@600;700;800&family=Spline+Sans+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root{
--ink:#03100e; --deep:#071d1b; --moss:#0d332c; --line:#1c5347; --line2:#2a6d5c;
--jade:#31b184; --mint:#93f2c6; --cream:#e9f5ec; --dim:#7fa89a;
--gold:#f6c65a; --coral:#ff7d5c;
--f-disp:'Syne','Trebuchet MS',system-ui,sans-serif;
--f-mono:'Spline Sans Mono',ui-monospace,'Cascadia Mono',Menlo,monospace;
--pad: clamp(10px, 2.4vw, 26px);
}
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html,body{height:100%;margin:0;overflow:hidden;overscroll-behavior:none}
body{
background:var(--ink);color:var(--cream);font-family:var(--f-mono);
position:fixed;inset:0;touch-action:none;user-select:none;-webkit-user-select:none;
}
canvas#stage{position:fixed;inset:0;display:block;width:100%;height:100%}
/* ---------- ambient screen layers ---------- */
#vignette,#grain,#danger{position:fixed;inset:0;pointer-events:none}
#vignette{background:radial-gradient(120% 90% at 50% 45%,transparent 38%,rgba(2,10,9,.55) 78%,rgba(1,7,6,.92) 100%)}
#grain{opacity:.055;mix-blend-mode:overlay;
background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='140' height='140'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/></filter><rect width='140' height='140' filter='url(%23n)'/></svg>");
animation:grainShift 6s steps(6) infinite}
@keyframes grainShift{0%{transform:translate(0,0)}20%{transform:translate(-3%,2%)}40%{transform:translate(2%,-3%)}60%{transform:translate(-2%,-2%)}80%{transform:translate(3%,1%)}100%{transform:translate(0,0)}}
#danger{box-shadow:inset 0 0 90px 12px rgba(255,96,60,.55);opacity:0;transition:opacity .12s linear}
#scanline{position:fixed;inset:0;pointer-events:none;opacity:.14;
background:repeating-linear-gradient(180deg,rgba(147,242,198,.09) 0 1px,transparent 1px 4px)}
/* ---------- shared panel ---------- */
.panel{
background:linear-gradient(165deg,rgba(11,44,39,.93),rgba(4,20,18,.95));
border:1px solid var(--line);
box-shadow:0 18px 44px -22px #000, inset 0 1px 0 rgba(147,242,198,.09);
clip-path:polygon(0 0,calc(100% - 15px) 0,100% 15px,100% 100%,15px 100%,0 calc(100% - 15px));
}
.hud{position:fixed;z-index:6}
.label{display:block;font-size:9px;letter-spacing:.22em;text-transform:uppercase;color:var(--dim)}
/* ---------- brand ---------- */
#brand{top:var(--pad);left:var(--pad);display:flex;align-items:center;gap:12px}
#brand .glyph{width:44px;height:44px;flex:none;filter:drop-shadow(0 0 10px rgba(49,177,132,.5))}
#brand .glyph path.coil{fill:none;stroke:var(--mint);stroke-width:2.6;stroke-linecap:round;
stroke-dasharray:var(--len);stroke-dashoffset:var(--len);animation:draw 1.5s .2s cubic-bezier(.6,.05,.2,1) forwards}
#brand .glyph g{transform-origin:22px 22px;animation:spin 26s linear infinite}
@keyframes draw{to{stroke-dashoffset:0}}
@keyframes spin{to{transform:rotate(360deg)}}
#brand h1{margin:0;font-family:var(--f-disp);font-weight:800;font-size:30px;line-height:.86;letter-spacing:-.03em}
#brand p{margin:3px 0 0;font-size:9px;letter-spacing:.3em;text-transform:uppercase;color:var(--jade)}
@media (max-width:520px){#brand .glyph{width:34px;height:34px}#brand h1{font-size:23px}}
/* ---------- readout ---------- */
#readout{top:var(--pad);right:var(--pad);padding:12px 16px 11px;min-width:186px;text-align:right}
#readout .big{font-family:var(--f-mono);font-weight:700;font-size:clamp(34px,6vw,52px);line-height:.9;
letter-spacing:-.03em;font-variant-numeric:tabular-nums;color:var(--cream);text-shadow:0 0 22px rgba(147,242,198,.28)}
#readout .big.pop{animation:pop .28s cubic-bezier(.2,1.7,.4,1)}
@keyframes pop{0%{transform:scale(1)}35%{transform:scale(1.14);color:var(--gold)}100%{transform:scale(1)}}
#readout .row{display:flex;justify-content:flex-end;gap:16px;margin-top:9px;padding-top:9px;border-top:1px solid rgba(42,109,92,.5)}
#readout .row b{display:block;font-size:14px;font-weight:700;font-variant-numeric:tabular-nums;color:var(--mint)}
#combo{margin-top:9px;height:12px;position:relative;opacity:0;transition:opacity .2s}
#combo.on{opacity:1}
#combo i{position:absolute;right:0;bottom:6px;height:2px;background:var(--gold);transition:width .1s linear}
#combo span{font-size:11px;font-weight:700;color:var(--gold);letter-spacing:.1em}
/* ---------- meters ---------- */
#meters{top:calc(var(--pad) + 150px);right:var(--pad);width:186px;padding:11px 14px 13px;display:grid;gap:10px}
@media (max-width:640px){#meters{top:auto;bottom:calc(var(--pad) + 4px);right:calc(var(--pad) + 4px);width:132px;padding:8px 10px 9px;gap:7px}}
.mtr .label{margin-bottom:4px}
.bar{height:5px;background:rgba(4,20,18,.9);border:1px solid rgba(42,109,92,.6);position:relative;overflow:hidden}
.bar>i{position:absolute;inset:0 auto 0 0;width:100%;transform-origin:left;
background:linear-gradient(90deg,var(--jade),var(--mint));transition:transform .08s linear}
.bar.energy.low>i{background:linear-gradient(90deg,#a3372a,var(--coral))}
.bar.vel>i{background:linear-gradient(90deg,#8a6b21,var(--gold))}
#steerTrack{height:9px;border:1px solid rgba(42,109,92,.6);position:relative;background:
linear-gradient(90deg,rgba(4,20,18,.9),rgba(13,51,44,.9),rgba(4,20,18,.9))}
#steerTrack::before{content:"";position:absolute;left:50%;top:1px;bottom:1px;width:1px;background:rgba(147,242,198,.4)}
#steerKnob{position:absolute;top:50%;left:50%;width:9px;height:13px;margin:-6.5px 0 0 -4.5px;background:var(--mint);
box-shadow:0 0 10px rgba(147,242,198,.8);transition:transform .05s linear,background .2s}
/* ---------- devices ---------- */
#devices{bottom:var(--pad);left:var(--pad);display:flex;gap:7px;align-items:center;flex-wrap:wrap;max-width:60vw}
.chip{font:700 9px/1 var(--f-mono);letter-spacing:.18em;text-transform:uppercase;color:var(--dim);
background:rgba(4,20,18,.75);border:1px solid rgba(28,83,71,.9);padding:8px 10px;cursor:pointer;
transition:transform .16s,color .2s,border-color .2s,box-shadow .2s;display:flex;align-items:center;gap:6px}
.chip em{font-style:normal;font-size:11px;opacity:.8}
.chip:hover{color:var(--cream);border-color:var(--line2);transform:translateY(-2px)}
.chip.live{color:var(--ink);background:var(--mint);border-color:var(--mint);box-shadow:0 0 22px rgba(147,242,198,.45)}
.chip.seen{color:var(--mint);border-color:var(--line2)}
.chip.off{color:#4d6b62;text-decoration:line-through}
/* ---------- touch controls ---------- */
#joy{position:fixed;z-index:7;pointer-events:none;opacity:0;transition:opacity .18s}
#joy.on{opacity:1}
#joy .base{position:absolute;width:150px;height:150px;margin:-75px 0 0 -75px;border-radius:50%;
border:1px solid rgba(147,242,198,.35);background:radial-gradient(circle,rgba(13,51,44,.5),rgba(3,16,14,.15) 70%)}
#joy .base::after{content:"";position:absolute;inset:26px;border-radius:50%;border:1px dashed rgba(147,242,198,.18)}
#joy .knob{position:absolute;width:64px;height:64px;margin:-32px 0 0 -32px;border-radius:50%;
background:radial-gradient(circle at 35% 30%,#bfffd f,#31b184);
background:radial-gradient(circle at 35% 30%,#bdf5da,#268a66);
box-shadow:0 0 26px rgba(49,177,132,.6),inset 0 -6px 12px rgba(0,0,0,.3)}
#boostBtn{position:fixed;z-index:7;right:calc(var(--pad) + 6px);bottom:calc(var(--pad) + 62px);
width:96px;height:96px;border-radius:50%;font:800 11px/1 var(--f-disp);letter-spacing:.2em;color:var(--gold);
background:radial-gradient(circle at 50% 35%,rgba(60,40,10,.9),rgba(20,12,2,.85));border:1px solid rgba(246,198,90,.5);
display:none;align-items:center;justify-content:center;cursor:pointer;transition:transform .12s,box-shadow .2s}
#boostBtn:active{transform:scale(.92);box-shadow:0 0 34px rgba(246,198,90,.5)}
body.touch #boostBtn{display:flex}
body.touch #meters{bottom:calc(var(--pad) + 168px)}
@media (max-width:520px){#boostBtn{width:80px;height:80px}body.touch #meters{bottom:calc(var(--pad) + 148px)}}
/* ---------- overlays ---------- */
.overlay{position:fixed;inset:0;z-index:9;display:grid;padding:calc(var(--pad) + 4px);
opacity:0;pointer-events:none;transition:opacity .34s ease}
.overlay.show{opacity:1;pointer-events:auto}
#over-title{align-content:end;justify-items:start}
#over-pause,#over-dead{place-items:center}
.card{transform:translateY(18px) scale(.985);transition:transform .45s cubic-bezier(.2,.9,.2,1);width:100%}
.overlay.show .card{transform:none}
#over-title .card{max-width:min(980px,100%);display:grid;grid-template-columns:1.05fr .95fr}
.tv-l{padding:clamp(20px,3vw,34px)}
.tv-r{padding:clamp(18px,2.4vw,28px);border-left:1px solid rgba(28,83,71,.85);background:rgba(3,16,14,.35)}
.eyebrow{font-size:9px;letter-spacing:.34em;text-transform:uppercase;color:var(--coral);margin:0 0 10px}
.title{font-family:var(--f-disp);font-weight:800;font-size:clamp(58px,12vw,116px);line-height:.78;letter-spacing:-.055em;margin:0}
.title span{color:var(--jade)}
.lede{margin:16px 0 0;font-size:clamp(12px,1.4vw,14px);line-height:1.65;color:#c3ddd2;max-width:38ch}
.lede b{color:var(--mint);font-weight:700}
.startline{margin-top:22px;padding-top:16px;border-top:1px solid rgba(28,83,71,.7);
font-family:var(--f-disp);font-weight:700;font-size:clamp(14px,2vw,19px);letter-spacing:.02em;color:var(--gold);
animation:breathe 1.9s ease-in-out infinite}
@keyframes breathe{0%,100%{opacity:.5}50%{opacity:1}}
.legend{display:grid;gap:14px}
.legend h3{margin:0;font-size:9px;letter-spacing:.26em;text-transform:uppercase;color:var(--jade);
display:flex;align-items:center;gap:8px}
.legend h3::after{content:"";flex:1;height:1px;background:linear-gradient(90deg,rgba(42,109,92,.9),transparent)}
.lrow{display:flex;justify-content:space-between;align-items:center;gap:12px;font-size:11px;padding:3px 0;color:#b7d3c8}
kbd{font:700 10px/1 var(--f-mono);background:rgba(13,51,44,.9);border:1px solid var(--line2);
border-bottom-width:2px;padding:5px 7px;color:var(--cream);border-radius:3px;white-space:nowrap}
@media (max-width:760px){
#over-title .card{grid-template-columns:1fr}
.tv-r{border-left:0;border-top:1px solid rgba(28,83,71,.85)}
.legend{grid-template-columns:1fr 1fr;gap:12px 20px}
.legend>:nth-child(3){grid-column:1/-1}
}
@media (max-width:430px){.legend{grid-template-columns:1fr}}
.dead-card{max-width:min(660px,100%);padding:clamp(20px,3vw,32px);display:grid;grid-template-columns:auto 1fr;gap:clamp(18px,3vw,32px)}
.dead-card .cause{grid-column:1/-1;font-family:var(--f-disp);font-weight:800;font-size:clamp(26px,5vw,42px);
line-height:.95;letter-spacing:-.03em;margin:0;color:var(--coral)}
.dead-card .cause small{display:block;font-family:var(--f-mono);font-weight:400;font-size:11px;
letter-spacing:.14em;text-transform:uppercase;color:var(--dim);margin-bottom:10px}
.stats{grid-column:1/-1;display:grid;grid-template-columns:repeat(4,1fr);gap:1px;background:rgba(28,83,71,.7);margin-top:6px}
.stats div{background:rgba(4,20,18,.75);padding:12px 10px;text-align:left}
.stats b{display:block;font-size:clamp(17px,3vw,25px);font-weight:700;font-variant-numeric:tabular-nums;color:var(--mint);line-height:1}
.stats span{font-size:8.5px;letter-spacing:.2em;text-transform:uppercase;color:var(--dim)}
@media (max-width:560px){.stats{grid-template-columns:repeat(2,1fr)}}
.ribbon{display:inline-block;margin-top:16px;font-size:9px;letter-spacing:.26em;text-transform:uppercase;
background:var(--gold);color:#241701;padding:5px 10px;font-weight:700;transform:rotate(-1.5deg);opacity:0;transition:opacity .3s}
.ribbon.on{opacity:1}
.pause-card{max-width:340px;padding:24px 28px;text-align:center}
.pause-card h2{font-family:var(--f-disp);font-weight:800;font-size:34px;letter-spacing:-.03em;margin:0 0 6px}
.pause-card p{font-size:11px;color:var(--dim);margin:0 0 14px;line-height:1.7}
/* ---------- toast ---------- */
#toast{position:fixed;z-index:11;left:50%;top:calc(var(--pad) + 2px);transform:translate(-50%,-24px);
font-size:10px;letter-spacing:.2em;text-transform:uppercase;padding:9px 14px;color:var(--ink);
background:var(--mint);opacity:0;transition:opacity .3s,transform .3s;pointer-events:none;white-space:nowrap}
#toast.on{opacity:1;transform:translate(-50%,0)}
#hint{position:fixed;z-index:5;left:50%;bottom:calc(var(--pad) + 46px);transform:translateX(-50%);
font-size:10px;letter-spacing:.24em;text-transform:uppercase;color:var(--dim);opacity:0;transition:opacity .6s}
#hint.on{opacity:1}
</style>
</head>
<body>
<canvas id="stage"></canvas>
<div id="scanline"></div>
<div id="danger"></div>
<div id="vignette"></div>
<div id="grain"></div>
<header id="brand" class="hud">
<svg class="glyph" viewBox="0 0 44 44" aria-hidden="true"><g><path class="coil" id="coilPath" d=""/></g><circle id="coilHead" r="3.1" fill="#f6c65a"/></svg>
<div><h1>COIL</h1><p>analogue serpent</p></div>
</header>
<div id="readout" class="hud panel">
<span class="label">score</span>
<div class="big" id="score">0</div>
<div class="row">
<div><span class="label">best</span><b id="best">0</b></div>
<div><span class="label">fruit</span><b id="fruit">0</b></div>
<div><span class="label">length</span><b id="len">1.8m</b></div>
</div>
<div id="combo"><i></i><span>×1 chain</span></div>
</div>
<div id="meters" class="hud panel">
<div class="mtr"><span class="label">energy</span><div class="bar energy" id="energyBar"><i id="energyFill"></i></div></div>
<div class="mtr"><span class="label">velocity</span><div class="bar vel"><i id="velFill" style="transform:scaleX(.3)"></i></div></div>
<div class="mtr"><span class="label">steer</span><div id="steerTrack"><div id="steerKnob"></div></div></div>
</div>
<div id="devices" class="hud">
<span class="chip" data-src="key"><em>⌨</em>keyboard</span>
<span class="chip" data-src="pad"><em>🎮</em>xinput pad</span>
<span class="chip" data-src="touch"><em>✳</em>touch</span>
<button class="chip" id="muteBtn"><em>♪</em>sound</button>
<button class="chip" id="pauseBtn"><em>❙❙</em>pause</button>
</div>
<div id="joy"><div class="base"></div><div class="knob"></div></div>
<button id="boostBtn">BOOST</button>
<div id="hint">steer · hold to boost</div>
<section class="overlay show" id="over-title">
<div class="card panel">
<div class="tv-l">
<p class="eyebrow">terrarium build 1.0 · continuous locomotion</p>
<h2 class="title">COIL<span>.</span></h2>
<p class="lede">No grid, no tick, no 90° corners. The serpent has a <b>heading</b> and a <b>turn radius</b> — you feed it an analogue vector and it carves the arc. Grow long enough and your own body becomes the maze.</p>
<p class="startline" id="startLine">▸ press any key · tap · press A</p>
</div>
<div class="tv-r">
<div class="legend">
<div>
<h3>keyboard</h3>
<div class="lrow"><span>steer (8-way, smoothed)</span><span><kbd>W A S D</kbd> <kbd>↑←↓→</kbd></span></div>
<div class="lrow"><span>boost</span><kbd>shift</kbd></div>
<div class="lrow"><span>start / pause / mute</span><span><kbd>space</kbd> <kbd>p</kbd> <kbd>m</kbd></span></div>
</div>
<div>
<h3>xinput gamepad</h3>
<div class="lrow"><span>analogue steer — push farther, turn tighter</span><kbd>left stick</kbd></div>
<div class="lrow"><span>boost · rumble on impact</span><span><kbd>RT</kbd> <kbd>RB</kbd></span></div>
<div class="lrow"><span>start / pause / retry</span><span><kbd>A</kbd> <kbd>start</kbd> <kbd>B</kbd></span></div>
</div>
<div>
<h3>touchscreen</h3>
<div class="lrow"><span>drag anywhere → thumbstick appears under your thumb</span><kbd>drag</kbd></div>
<div class="lrow"><span>boost</span><kbd>hold boost</kbd></div>
<div class="lrow"><span>mouse players: hold left button to steer toward the cursor</span><kbd>hold</kbd></div>
</div>
</div>
</div>
</div>
</section>
<section class="overlay" id="over-pause">
<div class="card pause-card panel">
<h2>PAUSED</h2>
<p>the serpent holds its breath</p>
<p class="startline" style="margin:0;padding:0;border:0;font-size:13px">▸ p · start · tap to resume</p>
</div>
</section>
<section class="overlay" id="over-dead">
<div class="card dead-card panel">
<h2 class="cause"><small id="deadTag">run terminated</small><span id="deadCause">you swallowed your own tail.</span></h2>
<div class="stats">
<div><b id="dScore">0</b><span>score</span></div>
<div><b id="dFruit">0</b><span>fruit</span></div>
<div><b id="dLen">0m</b><span>length</span></div>
<div><b id="dTime">0s</b><span>alive</span></div>
</div>
<div style="grid-column:1/-1">
<span class="ribbon" id="ribbon">new personal best</span>
<p class="startline" style="border:0;padding:0;margin-top:14px">▸ space · a · tap to run it again</p>
</div>
</div>
</section>
<div id="toast"></div>
<script>
(()=>{'use strict';
/* ═══════════════ helpers ═══════════════ */
const TAU=Math.PI*2;
const clamp=(v,a,b)=>v<a?a:(v>b?b:v);
const lerp=(a,b,t)=>a+(b-a)*t;
const rand=(a,b)=>a+Math.random()*(b-a);
const pick=a=>a[(Math.random()*a.length)|0];
const norm=a=>{while(a>Math.PI)a-=TAU;while(a<-Math.PI)a+=TAU;return a;};
const $=id=>document.getElementById(id);
function hash2(x,y){let n=x*374761393+y*668265263;n=(n^(n>>13))*1274126177;return ((n^(n>>16))>>>0)/4294967295;}
function sprite(size,fn){const c=document.createElement('canvas');c.width=c.height=size;const g=c.getContext('2d');
const grd=fn(g,size);g.fillStyle=grd;g.fillRect(0,0,size,size);return c;}
/* ═══════════════ canvas ═══════════════ */
const cvs=$('stage'), ctx=cvs.getContext('2d',{alpha:false});
let W=0,H=0,DPR=1,BASE=1;
function resize(){
DPR=Math.min(window.devicePixelRatio||1,2);
W=window.innerWidth; H=window.innerHeight;
cvs.width=Math.round(W*DPR); cvs.height=Math.round(H*DPR);
cvs.style.width=W+'px'; cvs.style.height=H+'px';
BASE=clamp(Math.max(W/1020,H/1420),.40,1.9);
}
addEventListener('resize',resize); resize();
/* ═══════════════ world constants ═══════════════ */
const ARENA={w:2160,h:1460}, WALL=26;
const HEAD_R=13, SPACING=8, START_LEN=182, MAX_LEN=4200;
const BASE_SPEED=188, BOOST_SPEED=302, TURN_RATE=3.25;
const CAUSES={
wall:['wall contact','you kissed the terrarium glass.'],
stone:['blunt object','you met a mossback stone.'],
self:['self annihilation','you swallowed your own tail.']
};
/* ═══════════════ sprites ═══════════════ */
const spGlow={
mint:sprite(128,(g,s)=>{const r=g.createRadialGradient(s/2,s/2,0,s/2,s/2,s/2);
r.addColorStop(0,'rgba(147,242,198,.95)');r.addColorStop(.35,'rgba(49,177,132,.35)');r.addColorStop(1,'rgba(49,177,132,0)');return r;}),
gold:sprite(128,(g,s)=>{const r=g.createRadialGradient(s/2,s/2,0,s/2,s/2,s/2);
r.addColorStop(0,'rgba(255,236,180,.95)');r.addColorStop(.3,'rgba(246,198,90,.4)');r.addColorStop(1,'rgba(246,198,90,0)');return r;}),
coral:sprite(128,(g,s)=>{const r=g.createRadialGradient(s/2,s/2,0,s/2,s/2,s/2);
r.addColorStop(0,'rgba(255,180,150,.9)');r.addColorStop(.32,'rgba(255,125,92,.35)');r.addColorStop(1,'rgba(255,125,92,0)');return r;})
};
const spMoss=sprite(160,(g,s)=>{const r=g.createRadialGradient(s/2,s/2,0,s/2,s/2,s/2);
r.addColorStop(0,'rgba(24,88,68,.55)');r.addColorStop(.6,'rgba(16,62,48,.25)');r.addColorStop(1,'rgba(12,48,38,0)');return r;});
const tile=(()=>{const c=document.createElement('canvas');c.width=c.height=110;const g=c.getContext('2d');
for(let i=0;i<130;i++){const x=Math.random()*110,y=Math.random()*110,a=Math.random();
g.fillStyle=a>.82?'rgba(147,242,198,.10)':a>.5?'rgba(255,255,255,.028)':'rgba(0,0,0,.16)';
g.fillRect(x,y,a>.82?1.6:1,a>.82?1.6:1);}
return c;})();
let tilePat=null;
/* ═══════════════ state ═══════════════ */
const S={state:'title',t:0,time:0,score:0,fruit:0,combo:1,comboT:0,best:0,topSpeed:0,shake:0,deadT:0,flash:0};
const snake={x:0,y:0,angle:0,speed:BASE_SPEED,energy:100,gassed:0,path:[],pathLen:0,bodyLen:START_LEN,
steerVis:0,auth:0,boosting:false,blink:1,blinkT:2,tongueT:1.2,alive:true};
const cam={x:0,y:0,scale:1,zoom:1};
const obs=[], foods=[], parts=[], flies=[];
let spine=[], spineCount=0;
for(let i=0;i<760;i++) spine.push({x:0,y:0,r:0,nx:0,ny:0,tx:0,ty:1});
try{S.best=parseInt(localStorage.getItem('coil.best')||'0',10)||0;}catch(e){}
/* ═══════════════ audio ═══════════════ */
let AC=null,MG=null,muted=false,noiseBuf=null;
function ac(){
if(!AC){const C=window.AudioContext||window.webkitAudioContext; if(!C)return null;
AC=new C(); MG=AC.createGain(); MG.gain.value=.42; MG.connect(AC.destination);
noiseBuf=AC.createBuffer(1,AC.sampleRate*.5,AC.sampleRate);
const d=noiseBuf.getChannelData(0); for(let i=0;i<d.length;i++) d[i]=(Math.random()*2-1)*(1-i/d.length);
}
if(AC.state==='suspended') AC.resume();
return AC;
}
function tone(f,dur,type,vol,slide,delay){
const a=ac(); if(!a||muted)return; const t0=a.currentTime+(delay||0);
const o=a.createOscillator(),g=a.createGain();
o.type=type||'sine'; o.frequency.setValueAtTime(f,t0);
if(slide) o.frequency.exponentialRampToValueAtTime(Math.max(30,slide),t0+dur);
g.gain.setValueAtTime(0,t0); g.gain.linearRampToValueAtTime(vol||.2,t0+.012);
g.gain.exponentialRampToValueAtTime(.0001,t0+dur);
o.connect(g); g.connect(MG); o.start(t0); o.stop(t0+dur+.05);
}
function thud(dur,vol){
const a=ac(); if(!a||muted||!noiseBuf)return;
const s=a.createBufferSource(); s.buffer=noiseBuf;
const f=a.createBiquadFilter(); f.type='lowpass'; f.frequency.setValueAtTime(1400,a.currentTime);
f.frequency.exponentialRampToValueAtTime(120,a.currentTime+dur);
const g=a.createGain(); g.gain.setValueAtTime(vol,a.currentTime);
g.gain.exponentialRampToValueAtTime(.0001,a.currentTime+dur);
s.connect(f); f.connect(g); g.connect(MG); s.start();
}
const sfx={
eat(){tone(520,.09,'triangle',.22);tone(880,.14,'sine',.18,null,.055);},
gold(){[660,880,1320].forEach((f,i)=>tone(f,.18,'triangle',.16,null,i*.06));},
boost(){tone(180,.16,'sawtooth',.09,420);},
die(){thud(.55,.5);tone(220,.5,'sawtooth',.22,42);},
ui(){tone(340,.07,'square',.10);},
start(){[440,660,990].forEach((f,i)=>tone(f,.14,'triangle',.15,null,i*.05));}
};
/* ═══════════════ input ═══════════════ */
const IN={
keys:new Set(), boostKey:false, dir:{x:1,y:0}, auth:0, source:'key',
padIndex:null, padPrev:[], padName:'',
touch:{on:false,id:null,ox:0,oy:0,x:0,y:0},
ptr:{down:false,x:0,y:0,type:'mouse'},
boostTouch:false,
stamp:{key:0,pad:0,touch:0,ptr:0},
confirm:false, restart:false, pauseEdge:false
};
const chipEls={}; document.querySelectorAll('.chip[data-src]').forEach(c=>chipEls[c.dataset.src]=c);
function markSrc(s){ if(!s)return; IN.stamp[s]=performance.now();
chipEls[s]&&chipEls[s].classList.add('seen'); }
function setLive(){
let best=null,bt=-1;
for(const k in IN.stamp) if(IN.stamp[k]>bt){bt=IN.stamp[k];best=k;}
IN.source=best==='ptr'?'key':best||'key';
for(const k in chipEls) chipEls[k].classList.toggle('live',k===IN.source||(k==='key'&&IN.source==='key'));
}
addEventListener('gamepadconnected',e=>{
IN.padIndex=e.gamepad.index; IN.padName=e.gamepad.id;
chipEls.pad.classList.add('seen'); toast('🎮 '+e.gamepad.id.slice(0,34)+' linked');
});
addEventListener('gamepaddisconnected',e=>{
if(IN.padIndex===e.gamepad.index) IN.padIndex=null;
chipEls.pad.classList.remove('seen','live'); toast('gamepad disconnected');
});
if(matchMedia('(hover:none)').matches||navigator.maxTouchPoints>0) document.body.classList.add('touch');
addEventListener('keydown',e=>{
const k=e.key.toLowerCase();
if([' ','arrowup','arrowdown','arrowleft','arrowright'].includes(k)) e.preventDefault();
IN.keys.add(k); markSrc('key');
if(e.repeat) return;
if(k===' '||k==='enter') IN.confirm=true;
if(k==='p'||k==='escape') IN.pauseEdge=true;
if(k==='m') toggleMute();
if(k==='r') IN.restart=true;
},{passive:false});
addEventListener('keyup',e=>IN.keys.delete(e.key.toLowerCase()));
addEventListener('blur',()=>{IN.keys.clear(); IN.ptr.down=false; IN.touch.on=false; IN.boostTouch=false;
if(S.state==='play') pause(true);});
document.addEventListener('visibilitychange',()=>{if(document.hidden&&S.state==='play') pause(true);});
function ptFromEvent(e){return {x:e.clientX,y:e.clientY};}
cvs.addEventListener('pointerdown',e=>{
if(e.pointerType==='touch'){
if(IN.touch.on) return;
IN.touch.on=true; IN.touch.id=e.pointerId; IN.touch.ox=e.clientX; IN.touch.oy=e.clientY;
IN.touch.x=e.clientX; IN.touch.y=e.clientY;
joy.style.left=e.clientX+'px'; joy.style.top=e.clientY+'px'; joy.classList.add('on');
markSrc('touch');
} else {
IN.ptr.down=true; IN.ptr.type=e.pointerType; IN.ptr.x=e.clientX; IN.ptr.y=e.clientY; markSrc('ptr');
}
if(S.state==='title'||S.state==='dead') IN.confirm=true;
else if(S.state==='pause') IN.pauseEdge=true;
cvs.setPointerCapture?.(e.pointerId);
ac();
});
cvs.addEventListener('pointermove',e=>{
if(e.pointerType==='touch'){ if(IN.touch.on&&e.pointerId===IN.touch.id){IN.touch.x=e.clientX;IN.touch.y=e.clientY;markSrc('touch');} }
else if(IN.ptr.down){ IN.ptr.x=e.clientX; IN.ptr.y=e.clientY; markSrc('ptr'); }
});
const endPtr=e=>{
if(e.pointerType==='touch'){ if(e.pointerId===IN.touch.id){IN.touch.on=false;IN.touch.id=null;joy.classList.remove('on');} }
else IN.ptr.down=false;
};
cvs.addEventListener('pointerup',endPtr); cvs.addEventListener('pointercancel',endPtr);
const joy=$('joy'), joyKnob=joy.querySelector('.knob');
$('boostBtn').addEventListener('pointerdown',e=>{e.preventDefault();IN.boostTouch=true;markSrc('touch');ac();});
$('boostBtn').addEventListener('pointerup',e=>{e.preventDefault();IN.boostTouch=false;});
$('boostBtn').addEventListener('pointercancel',()=>IN.boostTouch=false);
$('boostBtn').addEventListener('contextmenu',e=>e.preventDefault());
$('muteBtn').addEventListener('click',()=>toggleMute());
$('pauseBtn').addEventListener('click',()=>{IN.pauseEdge=true;});
document.querySelectorAll('.overlay').forEach(o=>o.addEventListener('pointerdown',e=>{
if(o.classList.contains('show')){ IN.confirm=true; ac(); }
}));
function toggleMute(){
muted=!muted; const b=$('muteBtn');
b.classList.toggle('off',muted); b.innerHTML=muted?'<em>✕</em>muted':'<em>♪</em>sound';
if(!muted) sfx.ui();
}
let toastT=0;
function toast(msg){const el=$('toast');el.textContent=msg;el.classList.add('on');toastT=2.6;}
function pollGamepad(){
if(!navigator.getGamepads) return;
let gp=null; const pads=navigator.getGamepads();
if(IN.padIndex!=null) gp=pads[IN.padIndex];
if(!gp){for(const p of pads){if(p){gp=p;IN.padIndex=p.index;break;}}}
if(!gp) return;
const btn=i=>!!(gp.buttons[i]&&gp.buttons[i].pressed);
const val=i=>gp.buttons[i]?(gp.buttons[i].value!=null?gp.buttons[i].value:(btn(i)?1:0)):0;
const edge=i=>{const p=btn(i),w=!!IN.padPrev[i];IN.padPrev[i]=p;return p&&!w;};
let ax=gp.axes[0]||0, ay=gp.axes[1]||0;
let mag=Math.hypot(ax,ay); const DZ=.20;
if(mag<DZ){ ax=0;ay=0;mag=0; }
if(mag>1) mag=1;
let dx=ax,dy=ay,auth=0;
if(mag>DZ){ auth=clamp((mag-DZ)/(1-DZ),0,1); const inv=1/Math.max(mag,1e-4); dx*=inv; dy*=inv; }
// d-pad fallback
if(!auth){
const rx=(btn(15)?1:0)-(btn(14)?1:0), ry=(btn(13)?1:0)-(btn(12)?1:0);
if(rx||ry){ dx=rx; dy=ry; auth=1; }
}
if(auth>.02){ IN.padDir={x:dx,y:dy,a:auth}; markSrc('pad'); } else IN.padDir=null;
IN.padBoost = val(7)>.45 || val(5)>.45 || btn(6)&&val(6)>.45;
if(edge(0)) IN.confirm=true;
if(edge(1)) IN.restart=true;
if(edge(9)) IN.pauseEdge=true;
if(edge(2)) toggleMute();
IN.padObj=gp;
}
function rumble(str,weak,dur){
const gp=IN.padObj; if(!gp)return;
try{ gp.vibrationActuator&&gp.vibrationActuator.playEffect&&gp.vibrationActuator.playEffect('dual-rumble',
{startDelay:0,duration:dur,weakMagnitude:weak,strongMagnitude:str}); }catch(e){}
}
function readInput(){
let dx=0,dy=0,auth=0,boost=false,winner=null,bt=-1;
const now=performance.now();
const cand=(src,x,y,a,b)=>{ if(a>.02||b){ if(IN.stamp[src]>bt){bt=IN.stamp[src];winner=src;} } };
// keyboard
const K=IN.keys;
const kx=((K.has('d')||K.has('arrowright'))?1:0)-((K.has('a')||K.has('arrowleft'))?1:0);
const ky=((K.has('s')||K.has('arrowdown'))?1:0)-((K.has('w')||K.has('arrowup'))?1:0);
const kb=K.has('shift')||K.has(' ')||K.has('f');
if(kx||ky){ const m=Math.hypot(kx,ky); cand('key',kx/m,ky/m,1,kb); IN.kbDir={x:kx/m,y:ky/m,a:1}; } else IN.kbDir=null;
IN.kbBoost=kb;
// gamepad
if(IN.padDir) cand('pad',IN.padDir.x,IN.padDir.y,IN.padDir.a,IN.padBoost);
// touch stick
if(IN.touch.on){
let vx=IN.touch.x-IN.touch.ox, vy=IN.touch.y-IN.touch.oy;
const m=Math.hypot(vx,vy), R=62;
if(m>6){ const cm=Math.min(m,R)/R; vx/=m; vy/=m; cand('touch',vx,vy,cm,IN.boostTouch); IN.tDir={x:vx,y:vy,a:cm}; }
else IN.tDir=null;
const kn=Math.min(m,R);
joyKnob.style.transform=`translate(${(vx*kn||0)}px,${(vy*kn||0)}px)`;
} else IN.tDir=null;
// pointer (mouse hold)
if(IN.ptr.down&&IN.ptr.type!=='touch'&&S.state==='play'){
const wx=(IN.ptr.x-W/2)/cam.scale+cam.x, wy=(IN.ptr.y-H/2)/cam.scale+cam.y;
const vx=wx-snake.x, vy=wy-snake.y, m=Math.hypot(vx,vy);
if(m>12){ cand('ptr',vx/m,vy/m,1,false); IN.pDir={x:vx/m,y:vy/m,a:1}; } else IN.pDir=null;
} else IN.pDir=null;
const map={key:IN.kbDir,pad:IN.padDir,touch:IN.tDir,ptr:IN.pDir};
if(winner&&map[winner]){ dx=map[winner].x; dy=map[winner].y; auth=map[winner].a; }
boost=!!(IN.kbBoost||IN.padBoost||IN.boostTouch);
IN.dir.x=dx; IN.dir.y=dy; IN.auth=auth; IN.boost=boost;
setLive();
}
/* ═══════════════ world setup ═══════════════ */
function makeBlob(r){const n=9+((Math.random()*4)|0),a=[];
for(let i=0;i<n;i++){const th=i/n*TAU, rr=r*rand(.78,1.14);a.push({x:Math.cos(th)*rr,y:Math.sin(th)*rr});}return a;}
function makeObstacles(){
obs.length=0; let tries=0; const want=6+((Math.random()*4)|0);
while(obs.length<want&&tries<700){
tries++;
const r=rand(46,88);
const x=rand(WALL+r+70,ARENA.w-WALL-r-70), y=rand(WALL+r+70,ARENA.h-WALL-r-70);
if(Math.hypot(x-ARENA.w/2,y-ARENA.h/2)<360) continue;
if(obs.some(o=>Math.hypot(o.x-x,o.y-y)<o.r+r+170)) continue;
obs.push({x,y,r,pts:makeBlob(r),seed:Math.random()*100});
}
}
function makeFlies(){
flies.length=0;
for(let i=0;i<52;i++) flies.push({x:rand(0,ARENA.w),y:rand(0,ARENA.h),ph:rand(0,TAU),
sp:rand(4,16),r:rand(1.1,3.2),par:rand(.55,1),gold:Math.random()<.22});
}
function freeSpot(rad){
for(let t=0;t<400;t++){
const x=rand(WALL+rad+30,ARENA.w-WALL-rad-30), y=rand(WALL+rad+30,ARENA.h-WALL-rad-30);
if(obs.some(o=>Math.hypot(o.x-x,o.y-y)<o.r+rad+34)) continue;
if(foods.some(f=>Math.hypot(f.x-x,f.y-y)<130)) continue;
let hit=false;
for(let i=0;i<spineCount&&!hit;i+=6){const p=spine[i];if(Math.hypot(p.x-x,p.y-y)<rad+90)hit=true;}
if(hit) continue;
return {x,y};
}
return null;
}
function spawnFood(forceGold){
const gold=forceGold||(S.fruit>0&&S.fruit%7===0&&!foods.some(f=>f.gold));
const spot=freeSpot(gold?19:13); if(!spot) return;
foods.push({x:spot.x,y:spot.y,r:gold?17:12.5,gold,ph:rand(0,TAU),born:S.t,ttl:gold?13:Infinity,v:0});
}
/* ═══════════════ run lifecycle ═══════════════ */
function resetSnake(){
snake.x=ARENA.w/2; snake.y=ARENA.h/2; snake.angle=0; snake.speed=BASE_SPEED;
snake.energy=100; snake.gassed=0; snake.bodyLen=START_LEN; snake.alive=true; snake.steerVis=0;
snake.path=[]; snake.pathLen=0;
const n=Math.ceil(START_LEN/SPACING)+4;
for(let i=n;i>=1;i--) snake.path.push({x:snake.x-i*SPACING,y:snake.y});
snake.path.push({x:snake.x,y:snake.y}); snake.pathLen=n*SPACING;
cam.x=snake.x; cam.y=snake.y; cam.zoom=1; cam.scale=BASE;
buildSpine(START_LEN);
}
function newRun(){
makeObstacles(); makeFlies();
foods.length=0; parts.length=0;
resetSnake();
S.score=0;S.fruit=0;S.combo=1;S.comboT=0;S.time=0;S.topSpeed=0;S.shake=0;S.flash=0;S.deadT=0;
for(let i=0;i<5;i++) spawnFood(false);
setState('play'); sfx.start(); rumble(.3,.2,90);
$('hint').classList.add('on'); setTimeout(()=>$('hint').classList.remove('on'),2600);
}
function setState(st){
S.state=st;
$('over-title').classList.toggle('show',st==='title');
$('over-pause').classList.toggle('show',st==='pause');
$('over-dead').classList.toggle('show',st==='dead'&&S.deadT>.7);
}
function pause(on){ if(on){ if(S.state==='play'){setState('pause');sfx.ui();} }
else if(S.state==='pause'){ setState('play'); sfx.ui(); } }
function die(cause){
if(!snake.alive) return;
snake.alive=false; S.deadT=0; S.shake=22; S.flash=1;
sfx.die(); rumble(1,.7,320);
for(let i=0;i<spineCount;i+=3){
const p=spine[i];
parts.push({t:'chunk',x:p.x+rand(-4,4),y:p.y+rand(-4,4),vx:rand(-70,70),vy:rand(-120,-10),
g:280,life:rand(.7,1.5),max:1.5,r:p.r*rand(.35,.7),col:i%2?'#31b184':'#0d332c'});
}
burst(snake.x,snake.y,26,'#ff7d5c',260);
if(S.score>S.best){S.best=S.score;try{localStorage.setItem('coil.best',String(S.best));}catch(e){}$('ribbon').classList.add('on');}
else $('ribbon').classList.remove('on');
$('deadTag').textContent=CAUSES[cause][0];
$('deadCause').textContent=CAUSES[cause][1];
$('dScore').textContent=S.score; $('dFruit').textContent=S.fruit;
$('dLen').textContent=(snake.bodyLen/100).toFixed(1)+'m';
$('dTime').textContent=Math.floor(S.time)+'s';
setState('dead');
}
/* ═══════════════ particles ═══════════════ */
function burst(x,y,n,col,spd){
for(let i=0;i<n;i++){const a=rand(0,TAU),s=rand(spd*.25,spd);
parts.push({t:'spark',x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,g:60,life:rand(.3,.75),max:.75,r:rand(1.5,3.6),col});}
parts.push({t:'ring',x,y,r:6,vr:spd*.9,life:.45,max:.45,col});
}
/* ═══════════════ spine ═══════════════ */
function buildSpine(maxLen){
let n=0,px=snake.x,py=snake.y,target=SPACING,d=0;
let p=spine[n]; p.x=px;p.y=py;n++;
for(let i=snake.path.length-1;i>=0&&target<=maxLen&&n<spine.length-2;i--){
const q=snake.path[i];
const seg=Math.hypot(q.x-px,q.y-py);
if(seg>1e-6&&d+seg>=target){
while(d+seg>=target&&target<=maxLen&&n<spine.length-2){
const f=(target-d)/seg; const s=spine[n];
s.x=px+(q.x-px)*f; s.y=py+(q.y-py)*f; n++; target+=SPACING;
}
}
d+=seg; px=q.x; py=q.y;
}
spineCount=n;
const last=1-clamp((snake.bodyLen-260)/2600,0,1);
for(let i=0;i<n;i++){
const a=spine[Math.max(0,i-1)], b=spine[Math.min(n-1,i+1)];
let tx=b.x-a.x, ty=b.y-a.y; const m=Math.hypot(tx,ty)||1; tx/=m; ty/=m;
const p=spine[i]; p.tx=tx;p.ty=ty;p.nx=-ty;p.ny=tx;
const t=n>1?i/(n-1):0;
let r;
if(t<.045) r=HEAD_R*(.86+.34*(t/.045));
else { const k=(t-.045)/.955; r=HEAD_R*(1.20-1.02*Math.pow(k,1.55)); }
r*=(.86+.14*last);
p.r=Math.max(2.2,r);
}
}
function pathPush(){
const l=snake.path[snake.path.length-1];
const d=Math.hypot(snake.x-l.x,snake.y-l.y);
if(d<SPACING*.55) return;
snake.path.push({x:snake.x,y:snake.y}); snake.pathLen+=d;
}
function pathTrim(){
const need=snake.bodyLen+120;
while(snake.path.length>3&&snake.pathLen>need){
const a=snake.path[0],b=snake.path[1];
snake.pathLen-=Math.hypot(b.x-a.x,b.y-a.y); snake.path.shift();
}
}
/* ═══════════════ autopilot (attract mode) ═══════════════ */
function autopilot(){
let g=null,bd=1e9;
for(const f of foods){const d=(f.x-snake.x)**2+(f.y-snake.y)**2;if(d<bd){bd=d;g=f;}}
const ga=g?Math.atan2(g.y-snake.y,g.x-snake.x):snake.angle;
let best=-1e9,ba=snake.angle;
for(let i=-6;i<=6;i++){
const a=snake.angle+i*.17;
let sc=Math.cos(norm(a-ga))*3.2-Math.abs(i)*.09;
let cx=snake.x,cy=snake.y,clear=1e9;
for(let s=1;s<=7;s++){
cx+=Math.cos(a)*48; cy+=Math.sin(a)*48;
let dw=Math.min(cx-WALL-HEAD_R,ARENA.w-WALL-cx-HEAD_R,cy-WALL-HEAD_R,ARENA.h-WALL-cy-HEAD_R);
if(dw<0) sc-=240/s; clear=Math.min(clear,dw);
for(const o of obs){const dd=Math.hypot(cx-o.x,cy-o.y)-o.r-HEAD_R;if(dd<0)sc-=240/s;clear=Math.min(clear,dd);}
for(let k=6;k<spineCount;k+=8){const p=spine[k];const dd=Math.hypot(cx-p.x,cy-p.y)-p.r-HEAD_R*.7;if(dd<0)sc-=110/s;clear=Math.min(clear,dd);}
}
sc+=Math.min(clear,240)*.055;
if(sc>best){best=sc;ba=a;}
}
return ba;
}
/* ═══════════════ update ═══════════════ */
function update(dt){
S.t+=dt;
if(toastT>0){toastT-=dt; if(toastT<=0)$('toast').classList.remove('on');}
S.shake=Math.max(0,S.shake-dt*S.shake*6-dt*4);
S.flash=Math.max(0,S.flash-dt*2.6);
pollGamepad(); readInput();
const playing=S.state==='play', title=S.state==='title';
if(playing||title){
// ── steering
let desired=null,auth=0;
if(title){ desired=autopilot(); auth=1; }
else if(IN.auth>.02){ desired=Math.atan2(IN.dir.y,IN.dir.x); auth=IN.auth; }
snake.auth=lerp(snake.auth,auth,1-Math.exp(-14*dt));
let turnUsed=0;
if(desired!=null){
const diff=norm(desired-snake.angle);
const maxT=TURN_RATE*(.20+.80*auth)*dt;
turnUsed=clamp(diff,-maxT,maxT);
snake.angle+=turnUsed;
snake.steerVis=lerp(snake.steerVis,clamp(diff/maxT,-1,1),1-Math.exp(-18*dt));
} else snake.steerVis=lerp(snake.steerVis,0,1-Math.exp(-8*dt));
// ── throttle
let boost=false;
if(playing){
boost=IN.boost&&snake.energy>1&&snake.gassed<=0;
if(boost){ snake.energy=Math.max(0,snake.energy-31*dt); if(snake.energy<=0){snake.gassed=1.2;sfx.boost();} }
else snake.energy=Math.min(100,snake.energy+15*dt);
if(snake.gassed>0) snake.gassed-=dt;
} else snake.energy=100;
if(boost&&!snake.boosting) sfx.boost();
snake.boosting=boost;
const base=BASE_SPEED+(playing?Math.min(S.fruit*1.25,52):20);
const target=snake.gassed>0?base*.6:(boost?BOOST_SPEED:base);
snake.speed=lerp(snake.speed,target,1-Math.exp(-6*dt));
const eff=snake.speed*(1-.17*Math.abs(turnUsed)/Math.max(TURN_RATE*dt,1e-4)*clamp(auth,0,1));
snake.x+=Math.cos(snake.angle)*eff*dt;
snake.y+=Math.sin(snake.angle)*eff*dt;
if(playing) { S.time+=dt; S.topSpeed=Math.max(S.topSpeed,eff); }
// ── trail motes
if(boost&&Math.random()<dt*46){
const a=snake.angle+Math.PI+rand(-.4,.4);
parts.push({t:'spark',x:snake.x+Math.cos(a)*HEAD_R,y:snake.y+Math.sin(a)*HEAD_R,
vx:Math.cos(a)*rand(20,70),vy:Math.sin(a)*rand(20,70),g:0,life:.4,max:.4,r:rand(1.2,2.6),col:'#93f2c6'});
}
// ── head cosmetics
snake.blinkT-=dt;
if(snake.blinkT<0){ snake.blinkT=rand(1.8,5.4); }
const bt=snake.blinkT;
snake.blink=bt<.14?Math.abs(Math.cos(bt/.14*Math.PI)):1;
snake.tongueT-=dt; if(snake.tongueT<-.34) snake.tongueT=rand(1.1,2.8);
// ── bounds
const lim=WALL+HEAD_R*.75;
if(title){
if(snake.x<lim){snake.x=lim;snake.angle=Math.PI-snake.angle;}
if(snake.x>ARENA.w-lim){snake.x=ARENA.w-lim;snake.angle=Math.PI-snake.angle;}
if(snake.y<lim){snake.y=lim;snake.angle=-snake.angle;}
if(snake.y>ARENA.h-lim){snake.y=ARENA.h-lim;snake.angle=-snake.angle;}
} else if(snake.x<lim||snake.x>ARENA.w-lim||snake.y<lim||snake.y>ARENA.h-lim){
snake.x=clamp(snake.x,lim,ARENA.w-lim); snake.y=clamp(snake.y,lim,ARENA.h-lim); die('wall');
}
pathPush(); pathTrim(); buildSpine(snake.bodyLen);
if(snake.alive&&!title){
// obstacles
for(const o of obs){ if(Math.hypot(o.x-snake.x,o.y-snake.y)<o.r+HEAD_R*.62){ die('stone'); break; } }
// self
if(snake.alive){
const safe=Math.ceil((HEAD_R+18)/SPACING);
for(let i=safe;i<spineCount;i++){
const p=spine[i];
if(Math.hypot(p.x-snake.x,p.y-snake.y)<HEAD_R*.82+p.r*.78){ die('self'); break; }
}
}
}
// food
for(let i=foods.length-1;i>=0;i--){
const f=foods[i]; f.ttl-=dt; f.v=Math.max(0,f.v-dt*3);
if(f.ttl<=0){ foods.splice(i,1); continue; }
const d=Math.hypot(f.x-snake.x,f.y-snake.y);
f.v=lerp(f.v,clamp(1-(d-40)/150,0,1),1-Math.exp(-6*dt));
if(d<HEAD_R*.9+f.r){
foods.splice(i,1);
if(playing){
if(S.comboT>0) S.combo=Math.min(5,S.combo+1); else S.combo=1;
S.comboT=4.2;
S.score+=(f.gold?60:10)*S.combo; S.fruit++;
snake.bodyLen=Math.min(MAX_LEN,snake.bodyLen+(f.gold?54:24));
}
snake.bodyLen=Math.min(MAX_LEN,snake.bodyLen+(playing?0:(f.gold?40:18)));
burst(f.x,f.y,f.gold?26:14,f.gold?'#f6c65a':'#93f2c6',f.gold?260:180);
S.shake=Math.max(S.shake,f.gold?9:3.4);
f.gold?sfx.gold():sfx.eat();
if(f.gold) { snake.energy=100; snake.gassed=0; rumble(.25,.5,120); } else rumble(.08,.2,45);
if(playing) spawnFood(false);
}
}
while(foods.filter(f=>!f.gold).length<5) spawnFood(false);
}
if(S.state==='dead'){
S.deadT+=dt;
buildSpine(snake.bodyLen);
if(S.deadT>.7) setState('dead');
}
// particles
for(let i=parts.length-1;i>=0;i--){
const p=parts[i]; p.life-=dt;
if(p.life<=0){parts.splice(i,1);continue;}
if(p.t==='ring'){p.r+=p.vr*dt;continue;}
p.x+=p.vx*dt; p.y+=p.vy*dt; p.vy+=(p.g||0)*dt;
p.vx*=(1-1.6*dt); p.vy*=(1-1.6*dt);
}
// fireflies
for(const fl of flies){
fl.ph+=dt*.7;
fl.x+=Math.cos(fl.ph*1.3)*fl.sp*dt; fl.y+=Math.sin(fl.ph)*fl.sp*dt;
if(fl.x<0)fl.x+=ARENA.w; if(fl.x>ARENA.w)fl.x-=ARENA.w;
if(fl.y<0)fl.y+=ARENA.h; if(fl.y>ARENA.h)fl.y-=ARENA.h;
}
// camera
const lead=snake.boosting?96:62;
const tx=snake.x+Math.cos(snake.angle)*lead, ty=snake.y+Math.sin(snake.angle)*lead;
const k=1-Math.exp(-6.5*dt);
cam.x=lerp(cam.x,tx,k); cam.y=lerp(cam.y,ty,k);
const zTarget=clamp(1-(snake.bodyLen-START_LEN)/9200,.74,1)*(snake.boosting?.965:1);
cam.zoom=lerp(cam.zoom,zTarget,1-Math.exp(-2.2*dt));
cam.scale=BASE*cam.zoom;
const hw=W/(2*cam.scale), hh=H/(2*cam.scale);
cam.x=ARENA.w>2*hw?clamp(cam.x,hw-WALL,ARENA.w-hw+WALL):ARENA.w/2;
cam.y=ARENA.h>2*hh?clamp(cam.y,hh-WALL,ARENA.h-hh+WALL):ARENA.h/2;
// danger glow
let dang=1e9;
dang=Math.min(snake.x-WALL,ARENA.w-WALL-snake.x,snake.y-WALL,ARENA.h-WALL-snake.y);
for(const o of obs) dang=Math.min(dang,Math.hypot(o.x-snake.x,o.y-snake.y)-o.r);
$('danger').style.opacity=(S.state==='play')?clamp(1-dang/150,0,1)*.75:0;
// actions
if(IN.pauseEdge){ IN.pauseEdge=false; if(S.state==='play')pause(true); else if(S.state==='pause')pause(false); }
if(IN.confirm){
IN.confirm=false;
if(S.state==='title') newRun();
else if(S.state==='pause') pause(false);
else if(S.state==='dead'&&S.deadT>.75) newRun();
}
if(IN.restart){ IN.restart=false; if(S.state==='dead'||S.state==='play') newRun(); }
updateHUD(dt);
}
/* ═══════════════ HUD ═══════════════ */
let hudCache={score:-1,fruit:-1,len:'',best:-1};
function updateHUD(dt){
if(hudCache.score!==S.score){
hudCache.score=S.score; const el=$('score'); el.textContent=S.score;
el.classList.remove('pop'); void el.offsetWidth; el.classList.add('pop');
}
if(hudCache.fruit!==S.fruit){hudCache.fruit=S.fruit;$('fruit').textContent=S.fruit;}
const lm=(snake.bodyLen/100).toFixed(1)+'m';
if(hudCache.len!==lm){hudCache.len=lm;$('len').textContent=lm;}
if(hudCache.best!==S.best){hudCache.best=S.best;$('best').textContent=S.best;}
if(S.comboT>0){S.comboT-=dt; if(S.comboT<=0){S.combo=1;}}
const cb=$('combo');
cb.classList.toggle('on',S.combo>1&&S.state==='play');
cb.querySelector('span').textContent='×'+S.combo+' chain';
cb.querySelector('i').style.width=(clamp(S.comboT/4.2,0,1)*100)+'%';
$('energyFill').style.transform='scaleX('+(snake.energy/100)+')';
$('energyBar').classList.toggle('low',snake.energy<26);
$('velFill').style.transform='scaleX('+clamp((snake.speed-140)/200,.04,1)+')';
const sv=clamp(snake.steerVis,-1,1);
$('steerKnob').style.transform=`translate(${sv*58}%,0) scale(${1+Math.abs(sv)*.35})`;
$('steerKnob').style.background=Math.abs(sv)>.55?'#f6c65a':'#93f2c6';
}
/* ═══════════════ render ═══════════════ */
function setCam(par){
ctx.setTransform(DPR,0,0,DPR,0,0);
const sx=S.shake?rand(-S.shake,S.shake):0, sy=S.shake?rand(-S.shake,S.shake):0;
ctx.translate(W/2+sx,H/2+sy); ctx.scale(cam.scale,cam.scale);
ctx.translate(-cam.x*par,-cam.y*par);
}
function render(){
// sky
ctx.setTransform(DPR,0,0,DPR,0,0);
const g=ctx.createLinearGradient(0,0,0,H);
g.addColorStop(0,'#04140f'); g.addColorStop(.55,'#071d1b'); g.addColorStop(1,'#02100f');
ctx.fillStyle=g; ctx.fillRect(0,0,W,H);
// deep parallax moss
setCam(.45);
ctx.globalCompositeOperation='lighter';
for(let i=0;i<9;i++){
const x=(i*431%2600)-100, y=(i*733%1900)-100, r=260+((i*97)%180);
ctx.globalAlpha=.5; ctx.drawImage(spMoss,x-r/2,y-r/2,r,r);
}
ctx.globalAlpha=1; ctx.globalCompositeOperation='source-over';
// world
setCam(1);
// floor
if(!tilePat) tilePat=ctx.createPattern(tile,'repeat');
ctx.fillStyle='#0a2621'; ctx.fillRect(0,0,ARENA.w,ARENA.h);
ctx.fillStyle=tilePat; ctx.fillRect(0,0,ARENA.w,ARENA.h);
ctx.globalCompositeOperation='lighter';
for(let i=0;i<12;i++){
const x=(hash2(i,3)*ARENA.w), y=(hash2(i,7)*ARENA.h), r=300+hash2(i,11)*380;
ctx.globalAlpha=.42; ctx.drawImage(spMoss,x-r/2,y-r/2,r,r);
}
ctx.globalAlpha=1; ctx.globalCompositeOperation='source-over';
// walls
ctx.save();
ctx.strokeStyle='#062019'; ctx.lineWidth=WALL*2; ctx.strokeRect(0,0,ARENA.w,ARENA.h);
ctx.strokeStyle='rgba(147,242,198,.22)'; ctx.lineWidth=2; ctx.strokeRect(WALL,WALL,ARENA.w-WALL*2,ARENA.h-WALL*2);
ctx.setLineDash([26,20]); ctx.lineDashOffset=-S.t*34;
ctx.strokeStyle='rgba(49,177,132,.5)'; ctx.lineWidth=5;
ctx.strokeRect(WALL*.55,WALL*.55,ARENA.w-WALL,ARENA.h-WALL);
ctx.setLineDash([]);
ctx.restore();
// fireflies
ctx.globalCompositeOperation='lighter';
for(const fl of flies){
const tw=.35+.65*Math.abs(Math.sin(fl.ph*1.7));
const s=fl.r*11;
ctx.globalAlpha=tw*.5;
ctx.drawImage(fl.gold?spGlow.gold:spGlow.mint,fl.x-s/2,fl.y-s/2,s,s);
ctx.globalAlpha=tw; ctx.fillStyle=fl.gold?'#ffe6a8':'#d6ffe9';
ctx.beginPath(); ctx.arc(fl.x,fl.y,fl.r*.55,0,TAU); ctx.fill();
}
ctx.globalAlpha=1; ctx.globalCompositeOperation='source-over';
drawObstacles();
drawFood();
drawParticles(false);
drawSnake();
drawParticles(true);
// boost speed lines
if(snake.boosting&&S.state==='play'){
ctx.globalCompositeOperation='lighter';
for(let i=0;i<7;i++){
const a=snake.angle+Math.PI+rand(-.5,.5), d=rand(40,260);
const x=snake.x+Math.cos(a)*d+rand(-60,60), y=snake.y+Math.sin(a)*d+rand(-60,60);
ctx.strokeStyle='rgba(147,242,198,.16)'; ctx.lineWidth=rand(1,2.4);
ctx.beginPath(); ctx.moveTo(x,y); ctx.lineTo(x-Math.cos(snake.angle)*rand(20,60),y-Math.sin(snake.angle)*rand(20,60)); ctx.stroke();
}
ctx.globalCompositeOperation='source-over';
}
// flash
if(S.flash>0){ ctx.setTransform(DPR,0,0,DPR,0,0);
ctx.fillStyle='rgba(255,125,92,'+(S.flash*.35)+')'; ctx.fillRect(0,0,W,H); }
}
function drawObstacles(){
for(const o of obs){
ctx.save(); ctx.translate(o.x,o.y);
// shadow
ctx.beginPath(); blobPath(ctx,o.pts,7,9); ctx.fillStyle='rgba(1,9,8,.5)'; ctx.fill();
ctx.beginPath(); blobPath(ctx,o.pts,0,0);
const g=ctx.createLinearGradient(-o.r,-o.r,o.r,o.r);
g.addColorStop(0,'#20483f'); g.addColorStop(.5,'#123029'); g.addColorStop(1,'#08201c');
ctx.fillStyle=g; ctx.fill();
ctx.strokeStyle='rgba(147,242,198,.20)'; ctx.lineWidth=1.6; ctx.stroke();
// moss cap
ctx.clip();
ctx.globalAlpha=.75; ctx.drawImage(spMoss,-o.r*.9,-o.r*1.25,o.r*1.8,o.r*1.8); ctx.globalAlpha=1;
for(let i=0;i<9;i++){
const hx=hash2(o.seed|i,i*3), hy=hash2(i*5,o.seed|1);
ctx.fillStyle=i%3?'rgba(49,177,132,.35)':'rgba(147,242,198,.28)';
ctx.beginPath(); ctx.arc((hx-.5)*o.r*1.5,-o.r*.55+(hy-.5)*o.r*.7,rand(1.4,3.2)*(o.r/70),0,TAU); ctx.fill();
}
ctx.restore();
// rim light
ctx.strokeStyle='rgba(255,255,255,.07)'; ctx.lineWidth=2.5;
ctx.beginPath(); ctx.arc(o.x,o.y,o.r*.94,Math.PI*1.05,Math.PI*1.75); ctx.stroke();
}
}
function blobPath(c,pts,ox,oy){
const n=pts.length;
let mx=(pts[n-1].x+pts[0].x)/2+ox, my=(pts[n-1].y+pts[0].y)/2+oy;
c.moveTo(mx,my);
for(let i=0;i<n;i++){
const p=pts[i], q=pts[(i+1)%n];
c.quadraticCurveTo(p.x+ox,p.y+oy,(p.x+q.x)/2+ox,(p.y+q.y)/2+oy);
}
c.closePath();
}
function drawFood(){
for(const f of foods){
const pulse=1+.12*Math.sin(S.t*4+f.ph);
const bob=Math.sin(S.t*2.2+f.ph)*2.4;
const dying=f.ttl<4&&f.ttl<Infinity;
const a=dying?(.4+.6*Math.abs(Math.sin(f.ttl*9))):1;
const R=f.r*pulse*(1+f.v*.25);
ctx.save(); ctx.globalAlpha=a;
ctx.globalCompositeOperation='lighter';
const gs=R*(f.gold?13:9);
ctx.globalAlpha=a*(.55+f.v*.4);
ctx.drawImage(f.gold?spGlow.gold:spGlow.mint,f.x-gs/2,f.y+bob-gs/2,gs,gs);
ctx.globalAlpha=a; ctx.globalCompositeOperation='source-over';
ctx.translate(f.x,f.y+bob);
if(f.gold){
ctx.rotate(S.t*.7);
ctx.fillStyle='#f6c65a'; ctx.beginPath();
for(let i=0;i<10;i++){const th=i/10*TAU-Math.PI/2,rr=i%2?R*.48:R*1.12;
ctx[i?'lineTo':'moveTo'](Math.cos(th)*rr,Math.sin(th)*rr);}
ctx.closePath(); ctx.fill();
ctx.strokeStyle='rgba(255,246,214,.9)'; ctx.lineWidth=1.4; ctx.stroke();
ctx.rotate(-S.t*.7);
ctx.fillStyle='#fff5d6'; ctx.beginPath(); ctx.arc(0,0,R*.42,0,TAU); ctx.fill();
} else {
const g=ctx.createRadialGradient(-R*.35,-R*.4,R*.1,0,0,R*1.1);
g.addColorStop(0,'#ffd0b8'); g.addColorStop(.45,'#ff7d5c'); g.addColorStop(1,'#a83a24');
ctx.fillStyle=g; ctx.beginPath(); ctx.arc(0,0,R,0,TAU); ctx.fill();
ctx.fillStyle='rgba(255,255,255,.75)'; ctx.beginPath(); ctx.ellipse(-R*.34,-R*.4,R*.2,R*.13,-.6,0,TAU); ctx.fill();
ctx.fillStyle='#31b184'; ctx.beginPath(); ctx.ellipse(R*.18,-R*1.02,R*.42,R*.2,-.5,0,TAU); ctx.fill();
ctx.strokeStyle='#1c5347'; ctx.lineWidth=1.6; ctx.beginPath(); ctx.moveTo(0,-R*.9); ctx.lineTo(R*.1,-R*1.25); ctx.stroke();
}
ctx.restore();
}
}
function drawParticles(front){
ctx.save();
for(const p of parts){
const isSpark=p.t==='spark';
if(isSpark!==front) continue;
const a=clamp(p.life/p.max,0,1);
if(p.t==='ring'){
ctx.globalAlpha=a*.6; ctx.strokeStyle=p.col; ctx.lineWidth=2.4*a+.5;
ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,TAU); ctx.stroke(); continue;
}
ctx.globalAlpha=a;
ctx.fillStyle=p.col;
if(p.t==='chunk'){
ctx.beginPath(); ctx.arc(p.x,p.y,p.r*a,0,TAU); ctx.fill();
} else {
ctx.globalCompositeOperation='lighter';
ctx.beginPath(); ctx.arc(p.x,p.y,p.r*(.4+a*.8),0,TAU); ctx.fill();
ctx.globalCompositeOperation='source-over';
}
}
ctx.restore();
}
function drawSnake(){
const n=spineCount; if(n<2) return;
const dead=!snake.alive;
const tube=new Path2D(), tubeOut=new Path2D();
for(let i=0;i<n;i++){
const p=spine[i];
tube.moveTo(p.x+p.r,p.y); tube.arc(p.x,p.y,p.r,0,TAU);
const ro=p.r+3.2;
tubeOut.moveTo(p.x+ro,p.y); tubeOut.arc(p.x,p.y,ro,0,TAU);
}
ctx.save();
// shadow
ctx.save(); ctx.translate(7,11); ctx.fillStyle='rgba(1,10,9,.42)'; ctx.fill(tubeOut); ctx.restore();
// outline
ctx.fillStyle=dead?'#2a1512':'#061f1a'; ctx.fill(tubeOut);
// body gradient
const head=spine[0], tail=spine[n-1];
const g=ctx.createLinearGradient(head.x,head.y,tail.x,tail.y);
if(dead){ g.addColorStop(0,'#7a5a52'); g.addColorStop(.5,'#4d3935'); g.addColorStop(1,'#2a1f1c'); }
else{
g.addColorStop(0,'#4fd39e'); g.addColorStop(.18,'#31b184');
g.addColorStop(.62,'#1d7f5f'); g.addColorStop(1,'#0f4a3a');
}
ctx.fillStyle=g; ctx.fill(tube);
// interior shading
ctx.save(); ctx.clip(tube);
for(let i=0;i<n;i+=1){
const p=spine[i];
// sheen
ctx.fillStyle='rgba(200,255,225,.085)';
ctx.beginPath(); ctx.arc(p.x-p.ny*0+p.nx*-p.r*.34-p.tx*0,p.y+p.ny*-p.r*.34,p.r*.5,0,TAU); ctx.fill();
// shadow flank
ctx.fillStyle='rgba(2,20,16,.20)';
ctx.beginPath(); ctx.arc(p.x+p.nx*p.r*.42,p.y+p.ny*p.r*.42,p.r*.55,0,TAU); ctx.fill();
if(i%4===0&&i>2){
const side=(i/4)%2?1:-1;
ctx.save(); ctx.translate(p.x+p.nx*p.r*.36*side,p.y+p.ny*p.r*.36*side);
ctx.rotate(Math.atan2(p.ty,p.tx));
ctx.fillStyle=dead?'rgba(255,125,92,.13)':'rgba(6,44,34,.34)';
ctx.beginPath(); ctx.ellipse(0,0,p.r*.62,p.r*.34,0,0,TAU); ctx.fill();
ctx.fillStyle=dead?'rgba(255,220,200,.08)':'rgba(246,198,90,.13)';
ctx.beginPath(); ctx.ellipse(0,0,p.r*.34,p.r*.17,0,0,TAU); ctx.fill();
ctx.restore();
}
}
if(snake.boosting&&!dead){
ctx.globalCompositeOperation='lighter'; ctx.fillStyle='rgba(147,242,198,.10)'; ctx.fill(tube);
ctx.globalCompositeOperation='source-over';
}
ctx.restore();
ctx.restore();
drawHead(dead);
}
function drawHead(dead){
const hr=HEAD_R*(1+.035*Math.sin(S.t*6));
ctx.save(); ctx.translate(snake.x,snake.y); ctx.rotate(snake.angle);
// tongue
if(!dead&&snake.tongueT<0){
const L=hr*(1.5+1.5*Math.sin(clamp(-snake.tongueT/.34,0,1)*Math.PI));
ctx.strokeStyle='#ff7d5c'; ctx.lineWidth=hr*.16; ctx.lineCap='round';
ctx.beginPath(); ctx.moveTo(hr*1.35,0); ctx.lineTo(hr*1.35+L,0);
ctx.moveTo(hr*1.35+L,0); ctx.lineTo(hr*1.35+L+hr*.5,-hr*.42);
ctx.moveTo(hr*1.35+L,0); ctx.lineTo(hr*1.35+L+hr*.5,hr*.42); ctx.stroke();
}
// skull
ctx.beginPath();
ctx.moveTo(hr*1.62,0);
ctx.bezierCurveTo(hr*1.35,-hr*.92,-hr*.35,-hr*1.28,-hr*1.05,-hr*.66);
ctx.bezierCurveTo(-hr*1.55,-hr*.18,-hr*1.55,hr*.18,-hr*1.05,hr*.66);
ctx.bezierCurveTo(-hr*.35,hr*1.28,hr*1.35,hr*.92,hr*1.62,0);
ctx.closePath();
const g=ctx.createLinearGradient(-hr,-hr,hr,hr);
if(dead){g.addColorStop(0,'#8a6660');g.addColorStop(1,'#3a2a26');}
else{g.addColorStop(0,'#63e7ae');g.addColorStop(.5,'#31b184');g.addColorStop(1,'#155c45');}
ctx.fillStyle=g; ctx.fill();
ctx.strokeStyle=dead?'#2a1512':'#061f1a'; ctx.lineWidth=2.2; ctx.stroke();
// brow ridge
ctx.fillStyle='rgba(2,20,16,.25)';
ctx.beginPath(); ctx.ellipse(-hr*.35,0,hr*.85,hr*.42,0,0,TAU); ctx.fill();
// eyes
for(const s of[-1,1]){
const ex=hr*.46, ey=s*hr*.63, bl=dead?1:snake.blink;
ctx.fillStyle=dead?'#e6d7cf':'#f4fbf2';
ctx.beginPath(); ctx.ellipse(ex,ey,hr*.40,hr*.42*bl,0,0,TAU); ctx.fill();
if(dead){
ctx.strokeStyle='#7a2d1c'; ctx.lineWidth=hr*.16; ctx.lineCap='round';
ctx.beginPath(); ctx.moveTo(ex-hr*.22,ey-hr*.22); ctx.lineTo(ex+hr*.22,ey+hr*.22);
ctx.moveTo(ex+hr*.22,ey-hr*.22); ctx.lineTo(ex-hr*.22,ey+hr*.22); ctx.stroke();
} else {
ctx.fillStyle='#08201c';
ctx.beginPath(); ctx.ellipse(ex+hr*.07,ey,hr*.12,hr*.30*bl,0,0,TAU); ctx.fill();
ctx.fillStyle='rgba(255,255,255,.85)';
ctx.beginPath(); ctx.arc(ex-hr*.06,ey-hr*.14,hr*.09*bl,0,TAU); ctx.fill();
}
}
ctx.restore();
// head glow
if(!dead){
ctx.save(); ctx.globalCompositeOperation='lighter';
const gs=HEAD_R*(snake.boosting?15:10);
ctx.globalAlpha=.32; ctx.drawImage(spGlow.mint,snake.x-gs/2,snake.y-gs/2,gs,gs); ctx.restore();
}
}
/* ═══════════════ loop ═══════════════ */
let last=performance.now();
function frame(now){
requestAnimationFrame(frame);
let dt=(now-last)/1000; last=now;
if(dt>.05) dt=.05;
if(S.state!=='pause') update(dt);
else { pollGamepad(); readInput();
if(IN.pauseEdge){IN.pauseEdge=false;pause(false);}
if(IN.confirm){IN.confirm=false;pause(false);}
if(IN.restart){IN.restart=false;newRun();}
if(toastT>0){toastT-=dt;if(toastT<=0)$('toast').classList.remove('on');}
}
render();
}
/* ═══════════════ boot ═══════════════ */
(function boot(){
// brand spiral
let d='',n=140,cx=22,cy=22,rMax=17;
for(let i=0;i<=n;i++){const t=i/n,th=t*3.6*TAU+Math.PI,r=rMax*(.14+.86*t);
const x=cx+Math.cos(th)*r,y=cy+Math.sin(th)*r;d+=(i?'L':'M')+x.toFixed(1)+' '+y.toFixed(1);}
const p=$('coilPath'); p.setAttribute('d',d);
p.style.setProperty('--len',(p.getTotalLength?p.getTotalLength():260).toFixed(0));
const hx=cx+Math.cos(3.6*TAU+Math.PI)*rMax, hy=cy+Math.sin(3.6*TAU+Math.PI)*rMax;
$('coilHead').setAttribute('cx',hx.toFixed(1)); $('coilHead').setAttribute('cy',hy.toFixed(1));
makeObstacles(); makeFlies(); resetSnake();
for(let i=0;i<5;i++) spawnFood(false);
$('best').textContent=S.best;
setState('title');
requestAnimationFrame(frame);
})();
})();
</script>
</body>
</html>Game Source: COIL — analogue serpent
Creator: ApexPhoenix84
Libraries: none
Complexity: complex (1195 lines, 56.5 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: coil-analogue-serpent-apexphoenix84" to link back to the original. Then publish at arcadelab.ai/publish.