Back to Stick Runner
SOURCE / PINNED RELEASE

Made of little things.

Stick Runner

Release
50f4e4cd86f6…
Author-recorded commit
2960bafd6cd9…
License
Check the original source
Author’s source reference
nostr://npub1ny7l44q0au5xkv3mjgt6zks5f0e2ydefslq4nhh2ve2xypgc2mqqat8qx2/wss%3A%2F%2Fgit.napplet.soy%2F/n-938fb0aa7b7

The hash-verified playable HTML, which may contain bundled or minified code. This view does not execute it.

index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Stick Runner</title>
    <script type="module" crossorigin>var lt=Object.defineProperty;var at=(i,t,e)=>t in i?lt(i,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):i[t]=e;var d=(i,t,e)=>at(i,typeof t!="symbol"?t+"":t,e);var rt="config";function ht(){const i=window;if(!i.napplet)throw new Error("window.napplet is unavailable -- runtime did not inject the napplet namespace");return i.napplet}function T(i){const e=ht()[i];if(!e)throw new Error(`window.napplet.${String(i)} is unavailable -- runtime did not inject this domain`);return e}var X={get(){return T("config").get()},subscribe(i){return T("config").subscribe(i)},openSettings(i){T("config").openSettings(i)},registerSchema(i,t){return T("config").registerSchema(i,t)},onSchemaError(i){return T("config").onSchemaError(i)},get schema(){return T("config").schema}};function ft(i,t){return!!(i!=null&&i[t])}function ct(i){return ft(window.napplet,i)}const dt="http://json-schema.org/draft-07/schema#",ut=1,mt="object",yt=!1,pt={touchButtons:{type:"boolean",title:"Always show touch buttons",description:"Show the on-screen run and jump buttons even when a keyboard is available.",default:!1,"x-napplet-section":"Game","x-napplet-order":1}},vt={$schema:dt,$version:ut,type:mt,additionalProperties:yt,properties:pt};function gt(i){(async()=>{X.schema||await X.registerSchema(vt);const t=X.subscribe(e=>i(e));window.addEventListener("pagehide",()=>t.close(),{once:!0})})().catch(()=>{})}const p=480,y=270,wt={name:"Level 1",roofs:[[0,520,200],[60,320,200],[60,280,190],[70,260,190],[70,240,175],[240,220,195],[80,200,170],[100,220,185],[90,180,160],[260,220,200],[85,160,165],[120,150,200],[90,120,150],[115,130,175],[95,110,130],[260,140,180],[100,100,145],[120,120,165],[100,100,125],[125,420,175]],props:[[1,90,22,28,"chimney"],[3,110,40,22,"vent"],[5,90,24,30,"chimney"],[7,60,22,34,"chimney"],[9,80,30,40,"tank"],[19,90,22,30,"chimney"]],ice:[[2,100],[5,80],[6,70],[18,60]],clotheslines:[5,9,15],flag:"checkered",scenery:{sky:["#ffb36b","#ffd9a0","#fff1d6"],sun:"#fff4dc",farCity:["#e8a07a","#c97c68"]}},xt={name:"Level 2",roofs:[[0,460,170],[70,260,185],[60,220,165],[80,300,190],[100,200,175],[250,220,195],[90,180,170],[100,240,185],[95,160,160],[240,260,180],[110,200,200],[90,180,170],[100,220,190],[120,240,200],[250,200,175],[85,160,160],[100,260,180],[120,130,200],[95,110,150],[255,120,170],[115,110,135],[120,120,175],[105,140,150],[120,110,120],[125,420,165]],props:[[0,260,24,30,"chimney"],[1,120,30,40,"tank"],[3,100,22,30,"chimney"],[7,60,40,22,"vent"],[24,90,22,30,"chimney"]],ice:[[2,80],[6,70],[9,90],[12,80],[15,60],[21,50]],clotheslines:[5,9,14,19],flag:"gold",scenery:{sky:["#9b7fd1","#e3a6c8","#ffd8c9"],sun:"#fff0f5",farCity:["#b98bb8","#94709e"]}},St=y+40,Tt=y+30;function Mt(i){const t=[];let e=0;for(const[l,r,c]of i.roofs)t.push({x:e+l,y:c,w:r,h:St-c,kind:"roof"}),e+=l+r;const s=t.slice();for(const[l,r,c,a,h]of i.props){const f=s[l];t.push({x:f.x+r,y:f.y-a,w:c,h:a,kind:h})}const n=i.ice.map(([l,r])=>{const c=s[l];return{x:c.x+c.w-r,y:c.y,w:r}}),o=i.clotheslines.map(l=>{const r=s[l-1],c=s[l];return{x1:r.x+r.w,y1:r.y+8,x2:c.x,y2:c.y+8,sag:16}});return{name:i.name,blocks:t,ice:n,clotheslines:o,startX:60,finishX:e-110,end:e,flag:i.flag,scenery:i.scenery}}const C=[wt,xt].map(Mt);function G(i,t){const e=(t-i.x1)/(i.x2-i.x1);return i.y1+(i.y2-i.y1)*e+i.sag*Math.sin(Math.PI*e)}function N(i,t){const e=i.find(s=>s.kind==="roof"&&t>=s.x&&t<=s.x+s.w);return e?e.y:y}const D="#1d1724",O="#2fa84f",Lt="#1e7a37";function Et(i,t,e,s,n,o,l){if(i.save(),i.translate(t,e),i.scale(s,1),i.strokeStyle=D,i.fillStyle=D,i.lineWidth=2.6,i.lineCap="round",i.lineJoin="round",n==="roll"){Pt(i,o),i.restore();return}const r={x:0,y:-16};let c=0,a=[],h=[];if(n==="run"){const u=Math.sin(o);c=3,a=[$(r,.2+u*.8,16),$(r,.2-u*.8,16)],h=[[-u*7,7],[u*7,7]]}else if(n==="jump"){const u=l<0;c=2,a=u?[[7,-6],[-5,-2]]:[[5,-1],[-4,0]],h=u?[[6,-8],[-6,-6]]:[[8,-2],[-8,-3]]}else if(n==="slide"){const u=Math.sin(o*3)*2;c=-3,a=[[9,0],[-7,0]],h=[[10,-4+u],[-10,-6-u]]}else if(n==="fall")a=[[-7,-4],[7,-2]],h=[[-9,-12],[9,-12]];else if(n==="cheer"){const u=Math.abs(Math.sin(o))*3;i.translate(0,-u),a=[[-5,0],[5,0]],h=[[-7,-14],[7,-14]]}else a=[[-4,0],[4,0]],h=[[-4,9],[4,9]];const f={x:c,y:-29};bt(i,r.x,r.y,f.x,f.y);const m=f.x+c*.4,v=f.y-6;i.beginPath(),i.arc(m,v,5,0,Math.PI*2),i.fill(),Z(i,m,v,5,n!=="stand");for(const[u,x]of a){const I=(r.x+u)/2+3,A=(r.y+x)/2;E(i,[r.x,r.y,I,A,u,x])}for(const[u,x]of h){const I=f.x+u*.5+(n==="run"?-2:0),A=f.y+x*.5+2;E(i,[f.x,f.y+1,I,A,f.x+u,f.y+x])}i.restore()}function Pt(i,t){const e=Math.sin(t*Math.PI);i.translate(0,-10-(1-e)*4),i.rotate(t*Math.PI*2),i.beginPath(),i.arc(0,0,8,Math.PI*.75,Math.PI*2.1),i.stroke(),i.beginPath(),i.arc(6,-5,4.5,0,Math.PI*2),i.fill(),Z(i,6,-5,4.5,!0),E(i,[-6,5,2,-1,6,6]),E(i,[-5,6,4,3,7,8]),E(i,[3,-4,4,2,8,1])}function Z(i,t,e,s,n){i.save(),i.beginPath(),i.arc(t,e,s+.3,0,Math.PI*2),i.clip(),i.fillStyle=O,i.fillRect(t-s-1,e-s*.8,s*2+2,s*.65),i.restore();const o=t-s*.9,l=e-s*.45;i.fillStyle=Lt,i.beginPath(),i.arc(o,l,1.3,0,Math.PI*2),i.fill();const r=Math.sin(performance.now()/70),c=n?[[o-7,l-1+r*1.5],[o-6,l+2-r*1.5]]:[[o-2,l+6],[o-.5,l+7]];i.strokeStyle=O,i.lineWidth=1.6;for(const[a,h]of c)i.beginPath(),i.moveTo(o,l),i.quadraticCurveTo((o+a)/2,(l+h)/2+(n?r:0),a,h),i.stroke();i.strokeStyle=D,i.lineWidth=2.6}function $(i,t,e){return[i.x+Math.sin(t)*e,Math.min(0,i.y+Math.cos(t)*e)]}function bt(i,t,e,s,n){i.beginPath(),i.moveTo(t,e),i.lineTo(s,n),i.stroke()}function E(i,t){i.beginPath(),i.moveTo(t[0],t[1]);for(let e=2;e<t.length;e+=2)i.lineTo(t[e],t[e+1]);i.stroke()}const q=220,Ct=1600,Rt=1100,It=360,At=2400,Y=1800,U=900,Xt=.1,Gt=.12,B=1/120,M=14,K=36,Bt=1.3,Dt=2.2,_t=450,jt=.25,V=.4,Wt=.5,z=[[.18,"shirt","#e5484d"],[.34,"sock","#ffffff"],[.44,"sock","#3e7bfa"],[.62,"pants","#3d63a8"],[.82,"shirt","#46a758"]];class kt{constructor(t){d(this,"controls",{left:!1,right:!1,jump:!1});d(this,"paused",!0);d(this,"onWin",()=>{});d(this,"levelIndex",0);d(this,"level",C[0]);d(this,"lines",[]);d(this,"ctx");d(this,"scale",1);d(this,"x",0);d(this,"y",0);d(this,"vx",0);d(this,"vy",0);d(this,"onGround",!1);d(this,"facing",1);d(this,"coyote",0);d(this,"jumpBuffer",0);d(this,"jumpWasDown",!1);d(this,"runCycle",0);d(this,"rollTimer",0);d(this,"phase","running");d(this,"phaseTimer",0);d(this,"cameraX",0);d(this,"time",0);d(this,"falls",0);d(this,"clock",0);d(this,"lastFrame",0);d(this,"leftover",0);this.canvas=t;const e=t.getContext("2d");if(!e)throw new Error("Canvas 2D is not available");this.ctx=e,this.loadLevel(0)}get blocks(){return this.level.blocks}get ice(){return this.level.ice}get isLastLevel(){return this.levelIndex===C.length-1}restartGame(){this.loadLevel(0),this.falls=0}loadLevel(t){this.levelIndex=t,this.level=C[t],this.lines=this.level.clotheslines.map(e=>({line:e,held:null,snappedAt:null})),this.restart()}restart(){this.respawn(),this.time=0,this.phase="running"}resize(t,e){const s=window.devicePixelRatio||1;this.canvas.width=Math.max(1,Math.round(t*s)),this.canvas.height=Math.max(1,Math.round(e*s)),this.scale=this.canvas.width/p,this.draw()}start(){const t=e=>{const s=this.lastFrame?Math.min((e-this.lastFrame)/1e3,.1):0;if(this.lastFrame=e,this.clock+=s,!this.paused)for(this.leftover+=s;this.leftover>=B;)this.update(B),this.leftover-=B;this.draw(),requestAnimationFrame(t)};requestAnimationFrame(t)}respawn(){this.x=this.level.startX,this.y=N(this.blocks,this.level.startX),this.vx=0,this.vy=0,this.onGround=!0,this.facing=1,this.coyote=0,this.jumpBuffer=0,this.rollTimer=0,this.cameraX=0;for(const t of this.lines)t.held=null,t.snappedAt=null}update(t){if(this.phase==="won"){this.vx*=.9,this.runCycle+=t*4,this.phaseTimer-=t,!this.isLastLevel&&this.phaseTimer<=0&&this.loadLevel(this.levelIndex+1);return}if(this.phase==="fell"){this.phaseTimer-=t,this.vy=Math.min(this.vy+Y*t,U),this.y+=this.vy*t,this.phaseTimer<=0&&(this.levelIndex>0?this.loadLevel(0):this.restart());return}this.time+=t;for(const a of this.lines)a.held===null||a.snappedAt!==null||(a.held+=t,a.held>=Wt&&(a.snappedAt=this.clock));const{left:e,right:s,jump:n}=this.controls,o=(s?1:0)-(e?1:0);if(this.onIce())this.vx=Math.min(It,this.vx+At*t),this.facing=1;else{const a=this.onGround?Ct:Rt,h=o*q,f=!this.onGround&&this.vx>q&&o>=0;this.vx<h?this.vx=Math.min(h,this.vx+a*t):this.vx>h&&!f&&(this.vx=Math.max(h,this.vx-a*t)),o!==0&&(this.facing=o)}n&&!this.jumpWasDown&&(this.jumpBuffer=Gt),this.jumpWasDown=n,this.coyote=this.onGround?Xt:Math.max(0,this.coyote-t),this.jumpBuffer=Math.max(0,this.jumpBuffer-t),this.jumpBuffer>0&&this.coyote>0&&(this.vy=-620,this.jumpBuffer=0,this.coyote=0,this.onGround=!1),!n&&this.vy<-620*.45&&(this.vy=-620*.45),this.vy=Math.min(this.vy+Y*t,U);const l=this.onGround,r=this.vy;if(this.moveX(this.vx*t),this.moveY(this.vy*t),this.rollTimer=this.onGround?Math.max(0,this.rollTimer-t):0,!l&&this.onGround&&r>_t&&Math.abs(this.vx)>60&&Math.random()<jt&&(this.rollTimer=V),this.x=Math.max(M/2,Math.min(this.level.end-M/2,this.x)),this.onGround&&Math.abs(this.vx)>10&&(this.runCycle+=t*Math.abs(this.vx)*.055),this.y>Tt){this.phase="fell",this.phaseTimer=Bt,this.falls+=1;return}this.x>=this.level.finishX&&(this.phase="won",this.phaseTimer=Dt,this.isLastLevel&&this.onWin(this.time,this.falls));const c=Math.max(0,Math.min(this.level.end-p,this.x-p*.33));this.cameraX+=(c-this.cameraX)*Math.min(1,t*10)}onIce(){return this.onGround&&this.ice.some(t=>this.y===t.y&&this.x>=t.x&&this.x<=t.x+t.w)}overlaps(t,e,s){return e+M/2>t.x&&e-M/2<t.x+t.w&&s>t.y&&s-K<t.y+t.h}moveX(t){this.x+=t;for(const e of this.blocks)this.overlaps(e,this.x,this.y)&&(t>0?this.x=e.x-M/2:t<0&&(this.x=e.x+e.w+M/2),this.vx=0)}moveY(t){this.y+=t,this.onGround=!1;for(const e of this.blocks)this.overlaps(e,this.x,this.y)&&(t>0?(this.y=e.y,this.onGround=!0):t<0&&(this.y=e.y+e.h+K),this.vy=0);if(!(this.onGround||t<=0))for(const e of this.lines){const{line:s}=e;if(e.snappedAt!==null||this.x<s.x1+2||this.x>s.x2-2)continue;const n=G(s,this.x);this.y>=n&&this.y-t<=n+1.5&&(this.y=n,this.vy=0,this.onGround=!0,e.held===null&&(e.held=0))}}pose(){return this.phase==="won"?"cheer":this.phase==="fell"?"fall":this.onGround?this.rollTimer>0?"roll":this.onIce()?"slide":Math.abs(this.vx)>10?"run":"stand":"jump"}draw(){const t=this.ctx;t.setTransform(this.scale,0,0,this.scale,0,0),this.drawSky(),this.drawFarCity(),t.save(),t.translate(-Math.round(this.cameraX*4)/4,0),this.drawPitFog();for(const n of this.blocks)this.drawBlock(n);for(const n of this.ice)this.drawIce(n);for(const n of this.lines)this.drawClothesline(n);this.drawFinish();const e=this.pose(),s=e==="roll"?1-this.rollTimer/V:this.runCycle;Et(t,this.x,this.y,this.facing,e,s,this.vy),t.restore(),this.drawHud()}drawSky(){const t=this.ctx,e=t.createLinearGradient(0,0,0,y),{scenery:s}=this.level;e.addColorStop(0,s.sky[0]),e.addColorStop(.55,s.sky[1]),e.addColorStop(1,s.sky[2]),t.fillStyle=e,t.fillRect(0,0,p,y),t.fillStyle=s.sun,t.beginPath(),t.arc(360-this.cameraX*.05,92,30,0,Math.PI*2),t.fill()}drawFarCity(){const t=this.ctx,e=[{speed:.2,color:this.level.scenery.farCity[0],base:150,spread:60,width:46},{speed:.4,color:this.level.scenery.farCity[1],base:175,spread:50,width:58}];for(const s of e){t.fillStyle=s.color;const n=this.cameraX*s.speed,o=Math.floor(n/s.width)-1;for(let l=o;l<o+p/s.width+3;l++){const r=s.base-S(l*7.3+s.speed*100+this.levelIndex*31)*s.spread;t.fillRect(l*s.width-n,r,s.width-4,y-r)}}}drawPitFog(){const t=this.ctx,e=t.createLinearGradient(0,y-130,0,y);e.addColorStop(0,"rgba(29, 23, 36, 0)"),e.addColorStop(.6,"rgba(29, 23, 36, 0.85)"),e.addColorStop(1,"rgba(10, 6, 12, 1)"),t.fillStyle=e,t.fillRect(this.cameraX-10,y-130,p+20,130)}drawBlock(t){const e=this.ctx;if(!(t.x>this.cameraX+p||t.x+t.w<this.cameraX))if(t.kind==="roof"){e.fillStyle="#4a3f55",e.fillRect(t.x,t.y,t.w,t.h);for(let s=t.y+22;s<y;s+=26)for(let n=t.x+14;n<t.x+t.w-18;n+=26)e.fillStyle=S(n*.37+s*1.7)>.55?"#ffd27a":"#3a3044",e.fillRect(n,s,10,13);e.fillStyle="#6e6280",e.fillRect(t.x-3,t.y,t.w+6,6),this.drawEdgeStripe(t.x-3,t.y),this.drawEdgeStripe(t.x+t.w-9,t.y)}else if(t.kind==="chimney")e.fillStyle="#8a4b3c",e.fillRect(t.x,t.y,t.w,t.h),e.fillStyle="#6b3a2f",e.fillRect(t.x-2,t.y,t.w+4,5);else if(t.kind==="vent"){e.fillStyle="#8f9aa6",e.fillRect(t.x,t.y,t.w,t.h),e.fillStyle="#6d7782";for(let s=4;s<t.h-2;s+=5)e.fillRect(t.x+4,t.y+s,t.w-8,2)}else e.fillStyle="#7b5a3f",e.fillRect(t.x,t.y,t.w,t.h),e.fillStyle="#5e432e",e.fillRect(t.x,t.y+t.h*.33,t.w,3),e.fillRect(t.x,t.y+t.h*.66,t.w,3)}drawIce(t){const e=this.ctx;if(t.x>this.cameraX+p||t.x+t.w+10<this.cameraX)return;const s=t.x,n=t.x+t.w+3,o=t.y-3,l=t.y+6;for(let a=s+4;a<n-2;a+=3+S(a*.37)*6){const h=(a-s)/(n-s),f=3+S(a*.71)*9*(.4+h),m=e.createLinearGradient(0,l,0,l+f);m.addColorStop(0,"rgba(225, 245, 255, 0.95)"),m.addColorStop(1,"rgba(160, 215, 245, 0.35)"),e.fillStyle=m,e.beginPath(),e.moveTo(a-2,l),e.lineTo(a+2,l),e.lineTo(a+.3,l+f),e.closePath(),e.fill()}const r=e.createLinearGradient(0,o,0,l);r.addColorStop(0,"rgba(240, 252, 255, 0.95)"),r.addColorStop(.35,"rgba(180, 228, 250, 0.8)"),r.addColorStop(1,"rgba(95, 160, 210, 0.75)"),e.fillStyle=r,e.beginPath(),e.moveTo(s+6,o),e.lineTo(n,o),e.lineTo(n,l),e.lineTo(s,l),e.quadraticCurveTo(s-3,o+3,s+6,o),e.fill(),e.fillStyle="rgba(255, 255, 255, 0.9)",e.fillRect(s+5,o,t.w-2,1),e.strokeStyle="rgba(70, 130, 180, 0.55)",e.lineWidth=.6;for(let a=s+12;a<n-10;a+=23){const h=S(a*1.3)*6;e.beginPath(),e.moveTo(a,o+1),e.lineTo(a+3+h*.5,o+4),e.lineTo(a+1+h,l-1),e.moveTo(a+3+h*.5,o+4),e.lineTo(a+8,o+5),e.stroke()}const c=s+this.clock*25%(t.w+40)-20;e.save(),e.beginPath(),e.rect(s,o,n-s,l-o),e.clip(),e.fillStyle="rgba(255, 255, 255, 0.55)",e.beginPath(),e.moveTo(c,l),e.lineTo(c+6,o),e.lineTo(c+10,o),e.lineTo(c+4,l),e.fill(),e.restore(),e.fillStyle="#ffffff";for(let a=s+9;a<n-4;a+=17){const h=Math.sin(this.clock*2.5+S(a)*10);if(h<.6)continue;const f=(h-.6)*6,m=o+1+S(a*2.1)*3;e.fillRect(a-f,m-.4,f*2,.8),e.fillRect(a-.4,m-f,.8,f*2)}this.drawEdgeStripe(t.x+t.w-9,t.y)}drawClothesline(t){const e=this.ctx,{line:s}=t;if(s.x1>this.cameraX+p||s.x2<this.cameraX)return;if(e.fillStyle="#9aa3ad",e.fillRect(s.x1-4,s.y1-2,5,4),e.fillRect(s.x2-1,s.y2-2,5,4),e.strokeStyle="#f2ede4",e.lineWidth=1.4,e.lineCap="round",t.snappedAt===null){const h=t.held===null?0:Math.sin(this.clock*70)*(.4+t.held*2),f=m=>{const v=s.x1+(s.x2-s.x1)*m;return{x:v,y:G(s,v)+h*Math.sin(Math.PI*m)}};e.beginPath();for(let m=0;m<=24;m++){const v=f(m/24);m===0?e.moveTo(v.x,v.y):e.lineTo(v.x,v.y)}e.stroke();for(const[m,v,u]of z){const x=f(m);this.drawClothing(v,u,x.x,x.y)}return}const n=this.clock-t.snappedAt,o=Math.min(1,n*4),l=Math.sin(n*6)*Math.exp(-n*1.5)*8,r=Math.min(70,(s.x2-s.x1)/2),c=[[s.x1,s.y1,1],[s.x2,s.y2,-1]];for(const[h,f,m]of c){const v=h+m*r*(1-o)+l*m,u=f+r*o;e.beginPath(),e.moveTo(h,f),e.quadraticCurveTo(h+m*6,(f+u)/2,v,u),e.stroke()}const a=.5*900*n*n;for(const[h,f,m]of z){const v=s.x1+(s.x2-s.x1)*h+Math.sin(n*5+h*9)*6*n,u=G(s,v)+a;u<y+20&&this.drawClothing(f,m,v,u)}}drawClothing(t,e,s,n){const o=this.ctx;o.fillStyle=e,o.strokeStyle="rgba(29, 23, 36, 0.5)",o.lineWidth=.8,o.beginPath(),t==="shirt"?(o.moveTo(s-6,n),o.lineTo(s+6,n),o.lineTo(s+10,n+5),o.lineTo(s+6,n+6),o.lineTo(s+6,n+15),o.lineTo(s-6,n+15),o.lineTo(s-6,n+6),o.lineTo(s-10,n+5)):t==="pants"?(o.moveTo(s-6,n),o.lineTo(s+6,n),o.lineTo(s+7,n+17),o.lineTo(s+2,n+17),o.lineTo(s,n+6),o.lineTo(s-2,n+17),o.lineTo(s-7,n+17)):(o.moveTo(s-2,n),o.lineTo(s+2,n),o.lineTo(s+2,n+8),o.lineTo(s+5,n+10),o.lineTo(s+4,n+12),o.lineTo(s-2,n+11)),o.closePath(),o.fill(),o.stroke(),o.fillStyle="#c98b4a",o.fillRect(s-.8,n-2.5,1.6,4)}drawEdgeStripe(t,e){const s=this.ctx;s.fillStyle="#ffcc33",s.fillRect(t,e,12,6),s.fillStyle="#2b2233",s.fillRect(t+4,e,4,6)}drawFinish(){const t=this.ctx,e=this.level.finishX,n=N(this.blocks,e)-70,o=Math.sin(this.clock*6)*2;if(this.level.flag==="checkered"){t.fillStyle="#2b2233",t.fillRect(e-2,n,4,70);for(let l=0;l<3;l++)for(let r=0;r<5;r++)t.fillStyle=(l+r)%2===0?"#ffffff":"#1d1724",t.fillRect(e+2+r*6,n+l*6+r/4*o,6,6)}else{t.fillStyle="#f2ede4",t.fillRect(e-2,n-6,4,76),t.fillStyle="#ffcc33",t.beginPath(),t.arc(e,n-7,3.5,0,Math.PI*2),t.fill(),t.fillStyle="#ffb000",t.strokeStyle="#a86a00",t.lineWidth=1,t.beginPath(),t.moveTo(e+2,n),t.quadraticCurveTo(e+18,n-3+o,e+36,n+2+o),t.lineTo(e+36,n+22+o),t.quadraticCurveTo(e+18,n+17+o,e+2,n+22),t.closePath(),t.fill(),t.stroke(),this.drawStar(e+19,n+11+o*.6,6),t.fillStyle="#ffffff";for(let l=0;l<3;l++){const r=Math.sin(this.clock*4+l*2.1);if(r<.3)continue;const c=e+8+l*14,a=n-8+l%2*34,h=r*2.5;t.fillRect(c-h,a-.4,h*2,.8),t.fillRect(c-.4,a-h,.8,h*2)}}t.fillStyle="#ffffff",t.font="bold 10px system-ui, sans-serif",t.textAlign="center",t.fillText(this.level.name.toUpperCase(),e,n-14)}drawStar(t,e,s){const n=this.ctx;n.fillStyle="#fff6d6",n.beginPath();for(let o=0;o<10;o++){const l=o%2===0?s:s*.45,r=-Math.PI/2+o*Math.PI/5;n.lineTo(t+Math.cos(r)*l,e+Math.sin(r)*l)}n.closePath(),n.fill()}drawHud(){const t=this.ctx,e=150,s=p-170,{startX:n,finishX:o}=this.level,l=Math.max(0,Math.min(1,(this.x-n)/(o-n)));t.fillStyle="rgba(43, 34, 51, 0.35)",t.fillRect(e,14,s,6),t.fillStyle="#2b2233",t.fillRect(e,14,s*l,6),t.fillRect(e+s-2,9,3,16),t.fillStyle="#2b2233",t.font="bold 12px system-ui, sans-serif",t.textAlign="right",t.fillText(`${this.level.name}  ${this.time.toFixed(1)} s`,e-10,22),this.phase==="won"&&!this.isLastLevel&&(t.fillStyle="rgba(29, 23, 36, 0.75)",t.fillRect(0,y/2-30,p,60),t.fillStyle="#ffcc33",t.textAlign="center",t.font="bold 22px system-ui, sans-serif",t.fillText(`${this.level.name} complete!`,p/2,y/2-2),t.fillStyle="#ffffff",t.font="12px system-ui, sans-serif",t.fillText(`Get ready for ${C[this.levelIndex+1].name}...`,p/2,y/2+18)),this.phase==="fell"&&(t.fillStyle="rgba(29, 23, 36, 0.75)",t.fillRect(0,y/2-30,p,60),t.fillStyle="#ffcc33",t.textAlign="center",t.font="bold 22px system-ui, sans-serif",t.fillText("Rog fell in the pit!",p/2,y/2-2),t.fillStyle="#ffffff",t.font="12px system-ui, sans-serif",t.fillText(this.levelIndex>0?"All the way back to Level 1!":"Back to the start...",p/2,y/2+18))}}function S(i){const t=Math.sin(i*12.9898)*43758.5453;return t-Math.floor(t)}const Ft=w("#app"),L=w("#stage"),J=w("#screen"),R=w("#game"),P=w("#menu"),Q=w("#menuResult"),W=w("#menuButton"),_=w("#playButton"),tt=w("#restartButton"),j=w("#touch"),Ht=w("#tooSmall");function w(i){const t=document.querySelector(i);if(!t)throw new Error(`Missing required element: ${i}`);return t}const g=new kt(R),Nt=matchMedia("(pointer: coarse)").matches&&!matchMedia("(any-pointer: fine)").matches;let et=!1;function it(){const i=Nt||et;j.hidden=!i,Ft.classList.toggle("touch-mode",i),st()}function k(i,t){const e=w(i),s=()=>{g.controls[t]=!1,e.classList.remove("held")};e.addEventListener("pointerdown",n=>{n.preventDefault(),e.setPointerCapture(n.pointerId),g.controls[t]=!0,e.classList.add("held")}),e.addEventListener("pointerup",s),e.addEventListener("pointercancel",s),e.addEventListener("lostpointercapture",s),e.addEventListener("contextmenu",n=>n.preventDefault())}k("#touchLeft","left");k("#touchRight","right");k("#touchJump","jump");function st(){const i=L.clientWidth,t=L.clientHeight-(j.hidden?0:j.offsetHeight+8),e=Math.max(.1,Math.min(i/p,t/y)),s=Math.floor(p*e),n=Math.floor(y*e);J.style.width=`${s}px`,J.style.height=`${n}px`,g.resize(s,n),Ht.hidden=L.clientWidth>=240&&L.clientHeight>=170}new ResizeObserver(st).observe(L);let F="start";function b(i,t=""){F=i,g.paused=!0,ot(),P.hidden=!1,W.hidden=!0,Q.hidden=t==="",Q.textContent=t,_.textContent=i==="start"?"Play":i==="pause"?"Keep playing":"Play again",tt.hidden=i!=="pause",_.focus()}function H(){P.hidden=!0,W.hidden=!1,g.paused=!1,R.focus()}_.addEventListener("click",()=>{F==="won"&&g.restartGame(),H()});tt.addEventListener("click",()=>{g.restartGame(),H()});W.addEventListener("click",()=>b("pause"));g.onWin=(i,t)=>{const e=t===0?"without falling once!":`and fell in the pit ${t} ${t===1?"time":"times"}.`;setTimeout(()=>b("won",`🏆 Rog beat Level 2 in ${i.toFixed(1)} seconds ${e}`),900)};const nt={ArrowLeft:"left",KeyA:"left",ArrowRight:"right",KeyD:"right",Space:"jump",ArrowUp:"jump",KeyW:"jump"};function ot(){g.controls.left=!1,g.controls.right=!1,g.controls.jump=!1}window.addEventListener("keydown",i=>{if(i.code==="Escape"||i.code==="KeyM"){i.preventDefault(),P.hidden?b("pause"):F!=="won"&&H();return}if(!P.hidden)return;const t=nt[i.code];t&&(i.preventDefault(),g.controls[t]=!0)});window.addEventListener("keyup",i=>{const t=nt[i.code];t&&(g.controls[t]=!1)});window.addEventListener("blur",ot);document.addEventListener("visibilitychange",()=>{document.hidden&&P.hidden&&b("pause")});R.addEventListener("pointerdown",()=>R.focus());ct(rt)&&gt(i=>{et=i.touchButtons===!0,it()});it();b("start");g.start();
</script>
    <style>:root{--ink: #1d1724;--dusk: #2b2233;--sun: #ffb36b;--cream: #fff4dc;--warn: #ffcc33;color-scheme:dark;font:14px/1.4 system-ui,-apple-system,Segoe UI,sans-serif}*{box-sizing:border-box}:where(html,body,body *){-webkit-user-select:none;user-select:none;-webkit-touch-callout:none}html,body,#app{height:100%;margin:0}body{background:var(--ink);color:var(--cream);overflow:hidden}.stage{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;width:100%;height:100%}.screen{position:relative;flex:none}.game{display:block;width:100%;height:100%;outline:none;touch-action:none}button{font:inherit;cursor:pointer}.menu-button{position:absolute;top:6px;left:6px;padding:4px 10px;border:2px solid var(--ink);border-radius:6px;background:var(--cream);color:var(--ink);font-weight:700;font-size:12px}.menu{position:absolute;top:0;right:0;bottom:0;left:0;display:grid;place-items:center;padding:8px;background:#1d1724b8;overflow:auto}.menu[hidden]{display:none}.menu-card{width:min(400px,100%);max-height:100%;overflow:auto;padding:14px 18px;border:3px solid var(--ink);border-radius:10px;background:var(--cream);color:var(--ink);box-shadow:0 6px 0 var(--ink)}.menu-title{font-size:26px;font-weight:900;letter-spacing:2px;text-align:center;font-style:italic}.menu-result{margin-top:4px;text-align:center;font-weight:700;font-size:16px;color:#b0441e}.menu-result[hidden]{display:none}.how p{margin:8px 0}.pit-word{padding:0 4px;border-radius:3px;background:var(--ink);color:var(--warn)}.keys{margin:8px 0;padding:0;list-style:none;font-size:13px}.keys li{margin:4px 0}kbd{display:inline-block;min-width:20px;padding:0 4px;border:1.5px solid var(--ink);border-bottom-width:3px;border-radius:4px;background:#fff;font:700 11px/16px system-ui,sans-serif;text-align:center}.menu-actions{display:flex;gap:8px;justify-content:center;margin-top:10px}.menu-actions button{min-width:100px;min-height:36px;padding:6px 14px;border:2px solid var(--ink);border-radius:8px;background:#fff;color:var(--ink);font-weight:800}.menu-actions button.primary{background:var(--warn)}.menu-actions button[hidden]{display:none}.menu-actions button:focus-visible,.menu-button:focus-visible{outline:3px solid var(--sun);outline-offset:2px}.touch-mode .keys,.touch-keys{display:none}.touch-mode .touch-keys{display:block}.touch{display:flex;justify-content:space-between;align-items:center;width:100%;max-width:900px;padding:0 12px 8px}.touch[hidden]{display:none}.touch-group{display:flex;gap:10px}.touch button{width:64px;height:64px;border:3px solid var(--cream);border-radius:50%;background:#fff4dc26;color:var(--cream);font-size:22px;font-weight:900;touch-action:none}.touch button.touch-jump{width:84px;height:84px;font-size:16px;border-color:var(--warn);color:var(--warn)}.touch button.held{background:#fff4dc66}.too-small{position:absolute;top:0;right:0;bottom:0;left:0;display:grid;place-items:center;padding:8px;text-align:center;background:var(--ink);color:var(--warn);font-weight:700}.too-small[hidden]{display:none}
</style>
    <meta name="napplet-type" content="stick-runner">
    <meta name="napplet-config-schema" content="{&quot;$schema&quot;:&quot;http://json-schema.org/draft-07/schema#&quot;,&quot;$version&quot;:1,&quot;type&quot;:&quot;object&quot;,&quot;additionalProperties&quot;:false,&quot;properties&quot;:{&quot;touchButtons&quot;:{&quot;type&quot;:&quot;boolean&quot;,&quot;title&quot;:&quot;Always show touch buttons&quot;,&quot;description&quot;:&quot;Show the on-screen run and jump buttons even when a keyboard is available.&quot;,&quot;default&quot;:false,&quot;x-napplet-section&quot;:&quot;Game&quot;,&quot;x-napplet-order&quot;:1}}}">
  </head>
  <body>
    <main id="app" class="app">
      <div id="stage" class="stage">
        <div id="screen" class="screen">
          <canvas
            id="game"
            class="game"
            tabindex="0"
            aria-label="Stick Runner. Help Rog the stickman run across the rooftops to the finish flag and jump over the pits."
          ></canvas>
          <button id="menuButton" class="menu-button" type="button" title="Menu (Esc)">☰ Menu</button>
        </div>

        <div id="touch" class="touch" hidden>
          <div class="touch-group">
            <button id="touchLeft" type="button" aria-label="Run left">◀</button>
            <button id="touchRight" type="button" aria-label="Run right">▶</button>
          </div>
          <button id="touchJump" class="touch-jump" type="button" aria-label="Jump">JUMP</button>
        </div>

        <div id="menu" class="menu" role="dialog" aria-modal="true" aria-labelledby="menuTitle">
          <div class="menu-card">
            <div id="menuTitle" class="menu-title">STICK RUNNER</div>
            <div id="menuResult" class="menu-result" hidden></div>
            <div class="how">
              <p>You are <b>Rog</b> the stickman. Help Rog run from roof to roof and reach the flag at the end of <b>Level 1</b>. Then try <b>Level 2</b> and reach the <b>gold flag</b>!</p>
              <p>The gaps between roofs are <b class="pit-word">the pit</b>. If Rog falls in on Level 1, he goes back to the start. On Level 2, he goes <b>all the way back to Level 1</b>!</p>
              <p>Some pits are too wide to jump, but there's a <b>clothesline</b> across them. Land on it and jump again fast: it snaps after half a second!</p>
              <ul class="keys">
                <li><kbd>←</kbd> <kbd>→</kbd> or <kbd>A</kbd> <kbd>D</kbd> run</li>
                <li><kbd>Space</kbd> <kbd>↑</kbd> or <kbd>W</kbd> jump (hold to jump higher)</li>
                <li><kbd>Esc</kbd> or <kbd>M</kbd> open this menu</li>
              </ul>
              <p class="touch-keys">Hold <b>◀</b> <b>▶</b> to run and tap <b>JUMP</b> to jump (hold it to jump higher). Tap <b>☰ Menu</b> to come back here.</p>
            </div>
            <div class="menu-actions">
              <button id="playButton" class="primary" type="button">Play</button>
              <button id="restartButton" type="button" hidden>Restart from Level 1</button>
            </div>
          </div>
        </div>

        <div id="tooSmall" class="too-small" hidden>Make the window bigger to run!</div>
      </div>
    </main>
  </body>
</html>