SOURCE / PINNED RELEASE
Made of little things.
super-platformer
- Release
- b61593abc2c6…
- Author-recorded commit
- bbd35ca510f6…
- License
- Check the original source
- Author’s source reference
- nostr://npub16uawymqtmdg4j6zcl6snt5apzfp4ek2g8lpasnqa75tspdpglcqsgpatvc/wss%3A%2F%2Fgit.napplet.soy%2F/n-01288b1ab38
The hash-verified playable HTML, which may contain bundled or minified code. This view does not execute it.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Super Platformer</title>
<script type="module" crossorigin>var pe=Object.defineProperty;var ce=(o,e,t)=>e in o?pe(o,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):o[e]=t;var p=(o,e,t)=>ce(o,typeof e!="symbol"?e+"":e,t);function de(){const o=new Map;function e(i,l){if(o.has(i))throw new Error(`NAP domain "${i}" is already registered`);o.set(i,l)}function t(i){const l=i.type.indexOf(".");if(l<=0)return!1;const a=i.type.slice(0,l),r=o.get(a);return r?(r(i),!0):!1}function s(){return Array.from(o.keys())}return{registerNap:e,dispatch:t,getRegisteredDomains:s}}var me=de(),ee=me.registerNap,O="storage";ee(O,o=>{});var te="theme";function se(){var e;const o=window;if(!((e=o.napplet)!=null&&e.theme))throw new Error("window.napplet.theme is unavailable -- runtime did not inject this domain");return o.napplet.theme}function ye(){return se().get()}function ve(o){return se().onChanged(o)}ee(te,o=>{});function ge(){const o=window;if(!o.napplet)throw new Error("window.napplet is unavailable -- runtime did not inject the napplet namespace");return o.napplet}function T(o){const t=ge()[o];if(!t)throw new Error(`window.napplet.${String(o)} is unavailable -- runtime did not inject this domain`);return t}var ie={getItem(o){return T("storage").getItem(o)},setItem(o,e){return T("storage").setItem(o,e)},removeItem(o){return T("storage").removeItem(o)},keys(){return T("storage").keys()},instance:{getItem(o){return T("storage").instance.getItem(o)},setItem(o,e){return T("storage").instance.setItem(o,e)},removeItem(o){return T("storage").instance.removeItem(o)},keys(){return T("storage").instance.keys()}}},we={get(){return T("config").get()},subscribe(o){return T("config").subscribe(o)},openSettings(o){T("config").openSettings(o)},registerSchema(o,e){return T("config").registerSchema(o,e)},onSchemaError(o){return T("config").onSchemaError(o)},get schema(){return T("config").schema}};function be(o,e){return!!(o!=null&&o[e])}function G(o){return be(window.napplet,o)}const A={textSize:13,controlSize:30,touchControls:!1,sound:!0},D=new Set;function Se(o){A.textSize=typeof o.textSize=="number"?o.textSize:13,A.controlSize=typeof o.controlSize=="number"?o.controlSize:30,A.touchControls=o.touchControls===!0,A.sound=o.sound!==!1;const e=document.documentElement;e.style.fontSize=`${A.textSize}px`,e.style.setProperty("--control-h",`${A.controlSize}px`),document.body.classList.toggle("force-touch",A.touchControls);for(const t of D)t(A)}function Me(o){return D.add(o),o(A),()=>{D.delete(o)}}if(G("config"))try{const o=we.subscribe(e=>Se(e));window.addEventListener("pagehide",()=>o.close(),{once:!0})}catch{}class Te{constructor(){p(this,"ctx",null);p(this,"master",null);p(this,"enabled",!0)}ensure(){if(!this.enabled)return null;try{if(!this.ctx){const e=window.AudioContext??window.webkitAudioContext;if(!e)return null;this.ctx=new e,this.master=this.ctx.createGain(),this.master.gain.value=.5,this.master.connect(this.ctx.destination)}return this.ctx.state==="suspended"&&this.ctx.resume(),this.ctx}catch{return null}}resume(){this.ensure()}setEnabled(e){this.enabled=e,e&&this.ensure()}blip(e,t,s,i,l=0,a){const r=this.ensure();if(!r||!this.master)return;const u=r.currentTime+l,c=r.createOscillator(),h=r.createGain();c.type=s,c.frequency.setValueAtTime(e,u),a&&c.frequency.exponentialRampToValueAtTime(a,u+t),h.gain.setValueAtTime(1e-4,u),h.gain.exponentialRampToValueAtTime(i,u+.01),h.gain.exponentialRampToValueAtTime(1e-4,u+t),c.connect(h),h.connect(this.master),c.start(u),c.stop(u+t+.03)}jump(){this.blip(360,.14,"square",.045,0,640)}coin(){this.blip(990,.07,"sine",.05),this.blip(1480,.09,"sine",.045,.06)}stomp(){this.blip(210,.12,"sawtooth",.06,0,90)}hurt(){this.blip(420,.35,"triangle",.07,0,70)}power(){[523,659,784,1047].forEach((e,t)=>this.blip(e,.1,"square",.045,t*.07))}fire(){this.blip(900,.09,"square",.04,0,320)}checkpoint(){this.blip(660,.1,"sine",.055),this.blip(880,.13,"sine",.05,.09)}bossHit(){this.blip(150,.18,"sawtooth",.07,0,70)}bossDown(){[200,150,110,80].forEach((e,t)=>this.blip(e,.2,"sawtooth",.07,t*.12))}complete(){[523,659,784,1046].forEach((e,t)=>this.blip(e,.14,"triangle",.055,t*.12))}}const S=new Te,n=24,Pe=9,ke=14,j=1/120,w=15,y=21,V=1250,J=900,Ce=145,Ee=1300,Ie=1700,Be=1e3,Ae=.1,Le=.12,He=1.55,Re=360,je=.22,Fe=92,I=[{name:"Meadow",rows:[" "," "," "," "," "," "," "," "," "," oow f ooo f ooj "," P === ? w b ===? ^ ? ^ === c G ","#################### ################## #####################"],hazards:[{x:8,y:10},{x:57,y:10}]},{name:"Cavern",rows:[" "," "," "," "," "," "," "," "," "," ooo f oow f ooj f oob ooo "," P ===b ? ^ === ^ j===? ? ^=== w === c G ","############## ########### ########## ########### ############"],hazards:[{x:4,y:10},{x:51,y:10},{x:59,y:10}]},{name:"Spires",rows:[" "," "," "," "," "," "," f f "," "," oow ooj oob f "," === ooo === ooo === ooo "," P ===? w=== ? ^ ===? ^=== c ^=== j === G ","################## ################## #######################"],hazards:[{x:4,y:10},{x:50,y:10}],movers:[{x:20,y:8,w:2,axis:"y",range:3,speed:1.8,phase:0}]},{name:"Foundry",rows:[" "," "," "," "," "," "," "," "," f f f "," ooo oow ooj oob ooo "," P === ^ === ^ j ===w c ^ ^=== b === w G ","################ ############# ############## #####################"],hazards:[{x:4,y:10},{x:20,y:10},{x:37,y:10},{x:63,y:10}],movers:[{x:26,y:9,w:2,axis:"x",range:3,speed:2.2,phase:0}]},{name:"Ruins",rows:[" "," "," "," "," "," "," "," f f "," oow ooo oob ooj "," === ooj === === oow === oob f ooo "," P === === ^ ===c b=== ===? ^ j=== ^ === B === w G ","########################## ################ ##########################"],hazards:[{x:8,y:10},{x:36,y:10}]},{name:"Gauntlet",rows:[" "," "," "," "," "," "," "," f f "," f oob f oow f "," oow === ooj === oob ooo "," P ===j ^ b===b ^===w w===c ^===? ^ B===c G ","############## ########### ############ ########### ########## #########"],hazards:[{x:4,y:10}]}],E={walker:{w:18,h:18,speed:55,hp:1,coins:1,color:"#e5484d",shade:"#b4232c"},jumper:{w:18,h:18,speed:28,hp:1,coins:1,color:"#a259ff",shade:"#7b34d6"},flyer:{w:20,h:15,speed:70,hp:1,coins:1,color:"#2fb344",shade:"#1f8a33"},brute:{w:26,h:26,speed:36,hp:2,coins:2,color:"#8e1f2b",shade:"#661520"},boss:{w:44,h:44,speed:34,hp:5,coins:10,color:"#7a1f2b",shade:"#4d1119"}},$e={skyTop:"#eef3f8",skyBottom:"#dbe6f2",hillFar:"#d3dde9",hillNear:"#bccbdc",solid:"#8a97a8",solidTop:"#a9b5c5",spike:"#e5484d",coin:"#f5b301",coinShine:"#ffe08a",player:"#2563eb",playerShine:"#7ba4f6",eye:"#ffffff",pupil:"#17212b",goal:"#2563eb",goalFlag:"#7ba4f6",mover:"#c98a3a",moverTop:"#e0aa5c",saw:"#8b93a3",sawShine:"#c9d1de",crate:"#c98a3a",crateDark:"#8a5a22"};function Y(o){const e=o.trim();if(!e)return null;const t=/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(e);if(t){const i=t[1],l=i.length===3?i.split("").map(a=>a+a).join(""):i;return[Number.parseInt(l.slice(0,2),16),Number.parseInt(l.slice(2,4),16),Number.parseInt(l.slice(4,6),16)]}const s=/^rgba?\(([^)]+)\)$/i.exec(e);if(s){const i=s[1].split(/[\s,/]+/).filter(Boolean).map(Number);if(i.length>=3&&i.slice(0,3).every(l=>Number.isFinite(l)))return[i[0],i[1],i[2]]}return null}function B(o,e,t){return[Math.round(o[0]+(e[0]-o[0])*t),Math.round(o[1]+(e[1]-o[1])*t),Math.round(o[2]+(e[2]-o[2])*t)]}function P(o){return`rgb(${o[0]} ${o[1]} ${o[2]})`}function Ge(o){return(.2126*o[0]+.7152*o[1]+.0722*o[2])/255}function Xe(o,e,t){const s=Y(o)??[244,246,248],i=Y(e)??[23,33,43],l=Y(t)??[37,99,235],a=Ge(s)<.5;return{skyTop:P(s),skyBottom:P(B(s,l,a?.08:.12)),hillFar:P(B(s,i,a?.12:.06)),hillNear:P(B(s,i,a?.22:.12)),solid:P(B(s,i,a?.34:.24)),solidTop:P(B(s,i,a?.5:.36)),spike:"#e5484d",coin:"#f5b301",coinShine:"#ffe08a",player:P(l),playerShine:P(B(l,[255,255,255],.38)),eye:"#ffffff",pupil:P(B(l,i,.55)),goal:P(l),goalFlag:P(B(l,[255,255,255],.25)),mover:"#c98a3a",moverTop:"#e0aa5c",saw:P(B(s,i,a?.72:.62)),sawShine:P(B(s,i,a?.95:.85)),crate:"#c98a3a",crateDark:"#8a5a22"}}const K=["magnet","fire","shield","doubleJump","speed"];function N(o){const e=o.rows.length,t=Math.max(...o.rows.map(d=>d.length)),s=new Uint8Array(t*e),i=[],l=[],a=[],r=[];let u={x:1,y:e-2},c={x:t-2,y:e-2};const h={w:"walker",j:"jumper",f:"flyer",b:"brute",B:"boss"};for(let d=0;d<e;d+=1){const g=o.rows[d];for(let v=0;v<t;v+=1){const C=g[v]??" ",q=d*t+v;C==="#"||C==="="?s[q]=1:C==="^"?s[q]=2:C==="o"?i.push({col:v,row:d,x:(v+.5)*n,y:(d+.5)*n,taken:!1}):C==="?"?l.push({col:v,row:d,broken:!1,power:K[(v*3+d)%K.length],wobble:0}):C==="c"?a.push({col:v,row:d,active:!1}):C==="P"?u={x:v,y:d}:C==="G"?c={x:v,y:d}:h[C]&&r.push({type:h[C],col:v,row:d})}}return{name:o.name,cols:t,rows:e,tiles:s,coins:i,crates:l,checkpoints:a,enemies:r,movers:o.movers??[],hazards:o.hazards??[],spawn:u,goal:c}}function U(o,e,t){return o<e?Math.min(o+t,e):o>e?Math.max(o-t,e):o}function m(o,e,t,s,i,l){const a=Math.max(0,Math.min(l,s/2,i/2));o.beginPath(),o.moveTo(e+a,t),o.arcTo(e+s,t,e+s,t+i,a),o.arcTo(e+s,t+i,e,t+i,a),o.arcTo(e,t+i,e,t,a),o.arcTo(e,t,e+s,t,a),o.closePath()}class Ye{constructor(e,t){p(this,"canvas");p(this,"ctx");p(this,"callbacks");p(this,"level");p(this,"levelIndex",0);p(this,"status","ready");p(this,"player",{x:0,y:0,vx:0,vy:0,w,h:y,onGround:!1,facing:1,stretch:0,walk:0});p(this,"enemies",[]);p(this,"movers",[]);p(this,"ridingMover",null);p(this,"saws",[]);p(this,"shots",[]);p(this,"pops",[]);p(this,"coinTotal",0);p(this,"power",null);p(this,"extraJump",!1);p(this,"fireCooldown",0);p(this,"respawnPoint",null);p(this,"lastSafe",null);p(this,"input",{left:!1,right:!1,jump:!1,fire:!1});p(this,"coyote",0);p(this,"jumpBuffer",0);p(this,"dead",!1);p(this,"deadTimer",0);p(this,"grace",0);p(this,"shake",0);p(this,"levelTime",0);p(this,"totalTime",0);p(this,"elapsed",0);p(this,"simTime",0);p(this,"deaths",0);p(this,"hearts",3);p(this,"coinsCollected",0);p(this,"palette",$e);p(this,"scale",1);p(this,"viewW",0);p(this,"viewH",0);p(this,"camX",0);p(this,"camY",0);p(this,"cssW",0);p(this,"cssH",0);p(this,"dpr",1);p(this,"raf",0);p(this,"last",0);p(this,"accumulator",0);p(this,"lastHudKey","");p(this,"destroyed",!1);p(this,"frame",e=>{if(this.destroyed)return;this.raf=requestAnimationFrame(this.frame);const t=Math.min(.05,Math.max(0,(e-this.last)/1e3));if(this.last=e,this.elapsed+=t,this.status==="playing")for(this.accumulator+=t;this.accumulator>=j;)this.step(j),this.dead||(this.levelTime+=j),this.accumulator-=j;else this.accumulator=0;this.shake>0&&(this.shake=Math.max(0,this.shake-t*26)),this.updateCamera(),this.render(),this.emitHud(!1)});this.canvas=e;const s=e.getContext("2d");if(!s)throw new Error("2D canvas context unavailable");this.ctx=s,this.callbacks=t,this.level=N(I[0]),this.respawnPoint=null,this.resetEntities(),this.refreshPalette(),this.resize(),this.last=performance.now(),this.raf=requestAnimationFrame(this.frame)}press(e){e==="jump"?(this.input.jump=!0,this.jumpBuffer=Le):this.input[e]=!0}release(e){this.input[e]=!1}releaseAll(){this.input.left=!1,this.input.right=!1,this.input.jump=!1,this.input.fire=!1}primary(){switch(this.status){case"ready":this.startRun();break;case"paused":this.setStatus("playing");break;case"levelComplete":this.advance();break;case"allComplete":this.startRun();break;case"gameOver":this.retryLevel();break}}togglePause(){this.status==="playing"?this.setStatus("paused"):this.status==="paused"&&this.setStatus("playing")}restartLevel(){if(this.status==="gameOver"){this.retryLevel();return}if(this.status==="allComplete"){this.startRun();return}this.resetLevelState(),this.setStatus("playing")}startRun(){this.totalTime=0,this.deaths=0,this.hearts=3,this.loadLevel(0),this.setStatus("playing")}resize(){const e=this.canvas.clientWidth,t=this.canvas.clientHeight;if(e<=0||t<=0)return;this.dpr=Math.min(window.devicePixelRatio||1,3),this.cssW=e,this.cssH=t;const s=Math.round(e*this.dpr),i=Math.round(t*this.dpr);this.canvas.width!==s&&(this.canvas.width=s),this.canvas.height!==i&&(this.canvas.height=i),this.scale=Math.min(t/(Pe*n),e/(ke*n)),this.viewW=e/this.scale,this.viewH=t/this.scale,this.updateCamera()}refreshPalette(){const e=getComputedStyle(document.documentElement);this.palette=Xe(e.getPropertyValue("--bg"),e.getPropertyValue("--fg"),e.getPropertyValue("--primary"))}destroy(){this.destroyed=!0,cancelAnimationFrame(this.raf)}loadLevel(e){this.levelIndex=e,this.hearts=3,this.level=N(I[e]),this.respawnPoint=null,this.lastSafe=null,this.resetEntities()}resetLevelState(){this.level=N(I[this.levelIndex]),this.respawnPoint=null,this.lastSafe=null,this.resetEntities()}retryLevel(){this.hearts=3,this.resetLevelState(),this.setStatus("playing")}respawnPlayer(){this.resetPlayer();const e=this.respawnPoint??this.lastSafe;e&&(this.player.x=e.x,this.player.y=e.y),this.grace=1.5}resetEntities(){for(const e of this.level.coins)e.taken=!1,e.x=(e.col+.5)*n,e.y=(e.row+.5)*n;for(const e of this.level.crates)e.broken=!1;this.enemies=this.level.enemies.map(e=>this.createEnemy(e)),this.movers=this.level.movers.map(e=>this.createMover(e)),this.saws=this.level.hazards.map(e=>this.createSaw(e)),this.shots=[],this.power=null,this.extraJump=!1,this.fireCooldown=0,this.ridingMover=null,this.coinsCollected=0,this.levelTime=0,this.dead=!1,this.deadTimer=0,this.pops=[],this.coinTotal=this.level.coins.length+this.enemies.reduce((e,t)=>e+t.coinValue,0),this.resetPlayer(),this.respawnPoint&&(this.player.x=this.respawnPoint.x,this.player.y=this.respawnPoint.y)}createEnemy(e){const t=E[e.type],i=e.type!=="flyer"?(e.row+1)*n-t.h:e.row*n+(n-t.h)/2;return{type:e.type,x:e.col*n+(n-t.w)/2,y:i,vx:0,vy:0,w:t.w,h:t.h,onGround:!1,dir:-1,hp:t.hp,maxHp:t.hp,coinValue:t.coins,alive:!0,squash:0,hitFlash:0,baseY:i,originX:e.col*n+(n-t.w)/2,timer:.6+e.col%3*.25,bob:e.col%4*.5}}createMover(e){const t=(e.h??.55)*n,s=e.x*n,i=e.y*n;return{x:s,y:i,w:e.w*n,h:t,axis:e.axis,originX:s,originY:i,range:e.range*n,speed:e.speed,phase:e.phase??0,dx:0,dy:0,prevX:s,prevY:i}}createSaw(e){const t=(e.x+.5)*n,s=(e.y+.5)*n;return{x:t,y:s,r:(e.r??.6)*n,axis:e.axis??null,originX:t,originY:s,range:(e.range??0)*n,speed:e.speed??0,phase:e.phase??0,angle:0}}resetPlayer(){const e=this.player;e.x=this.level.spawn.x*n+(n-w)/2,e.y=(this.level.spawn.y+1)*n-y,e.vx=0,e.vy=0,e.onGround=!1,e.facing=1,e.stretch=0,e.walk=0,this.coyote=0,this.jumpBuffer=0,this.dead=!1,this.deadTimer=0,this.ridingMover=null,this.power=null,this.extraJump=!1,this.fireCooldown=0,this.shots=[]}advance(){if(this.levelIndex>=I.length-1){this.startRun();return}this.loadLevel(this.levelIndex+1),this.setStatus("playing")}setStatus(e){this.status=e,this.callbacks.onStatusChange(e,this.completionInfo()),this.emitHud(!0)}completionInfo(){return{levelIndex:this.levelIndex,levelCount:I.length,levelName:this.level.name,levelTime:this.levelTime,totalTime:this.totalTime+this.levelTime,isLast:this.levelIndex>=I.length-1,deaths:this.deaths}}completeLevel(){this.status==="playing"&&(this.totalTime+=this.levelTime,S.complete(),this.setStatus(this.levelIndex>=I.length-1?"allComplete":"levelComplete"))}die(e=!1){this.dead||this.invulnerable()&&!e||(this.dead=!0,this.deadTimer=.6,this.deaths+=1,this.hearts=Math.max(0,this.hearts-1),this.shake=9,this.player.vy=0,this.player.vx=0,S.hurt(),this.emitHud(!0))}spawnPop(e,t,s,i){this.pops.push({x:e,y:t,vy:-46,life:.8,max:.8,text:s,color:i})}step(e){var h,d,g;if(this.simTime+=e,this.dead){this.deadTimer-=e,this.updateMovers(),this.updateSaws(e),this.updateShots(e),this.updatePops(e),this.deadTimer<=0&&(this.hearts<=0?(this.dead=!1,this.setStatus("gameOver")):this.respawnPlayer());return}this.updateMovers(),this.ridingMover&&this.player.onGround&&(this.player.x+=this.ridingMover.dx),this.ridingMover=null,this.grace=Math.max(0,this.grace-e);const t=this.player,s=(this.input.right?1:0)-(this.input.left?1:0);s!==0&&(t.facing=s),this.power&&(this.power.timeLeft-=e,this.power.timeLeft<=0&&(this.power=null,this.emitHud(!0)));const i=((h=this.power)==null?void 0:h.type)==="speed"?He:1,l=Ce*i,a=(t.onGround?Ee:Be)*i;s!==0?t.vx=U(t.vx,s*l,a*e):t.vx=U(t.vx,0,Ie*e),t.onGround?(this.coyote=Ae,this.extraJump=!0):this.coyote=Math.max(0,this.coyote-e),this.jumpBuffer=Math.max(0,this.jumpBuffer-e),this.jumpBuffer>0&&(this.coyote>0?(t.vy=-520,t.onGround=!1,this.jumpBuffer=0,this.coyote=0,S.jump()):((d=this.power)==null?void 0:d.type)==="doubleJump"&&this.extraJump&&(t.vy=-520*.92,this.extraJump=!1,this.jumpBuffer=0,this.spawnPop(t.x+w/2,t.y+y,"hop!",this.palette.playerShine),S.jump())),!this.input.jump&&t.vy<-200&&(t.vy=-200),this.fireCooldown=Math.max(0,this.fireCooldown-e),this.input.fire&&((g=this.power)==null?void 0:g.type)==="fire"&&this.fireCooldown<=0&&(this.fireCooldown=je,this.shots.push({x:t.x+w/2+t.facing*10,y:t.y+y*.42,vx:t.facing*Re,vy:0,life:.9}),S.fire()),t.vy=Math.min(t.vy+V*e,J);const r=t.vy>0,u=t.y+y;if(t.onGround=!1,this.moveBodyX(t,e),this.moveBodyY(t,e),!t.onGround&&r){for(const v of this.movers)if(t.x+w>v.x&&t.x<v.x+v.w){const C=t.y+y;if(u<=v.y+3&&C>=v.y){t.y=v.y-y,t.vy=0,t.onGround=!0,this.ridingMover=v;break}}}this.ridingMover&&(t.y=this.ridingMover.y-y),t.onGround&&(t.walk+=Math.abs(t.vx)*e*.22,this.lastSafe={x:t.x,y:t.y});const c=t.onGround?0:Math.max(-.08,Math.min(.12,t.vy/2600));t.stretch+=(c-t.stretch)*Math.min(1,e*16),this.breakCrates(),this.checkCheckpoints(),this.applyMagnet(e),this.updateSaws(e),this.updateShots(e),this.updateEnemies(e),this.collectCoins(),this.checkEnemies(),this.checkSpikes(),this.checkSaws(),this.checkGoal(),this.updatePops(e),t.y>this.level.rows*n+80&&this.die(!0)}checkCheckpoints(){const e=this.player;for(const t of this.level.checkpoints){if(t.active)continue;const s=t.col*n+4,i=t.row*n+4,l=n-8,a=n-4;e.x<s+l&&e.x+w>s&&e.y<i+a&&e.y+y>i&&(t.active=!0,this.respawnPoint={x:t.col*n+(n-w)/2,y:(t.row+1)*n-y},this.spawnPop((t.col+.5)*n,t.row*n,"Checkpoint!",this.palette.goalFlag),S.checkpoint(),this.emitHud(!0))}}invulnerable(){var e;return((e=this.power)==null?void 0:e.type)==="shield"||this.grace>0}grantPower(e,t,s){const i={magnet:9,fire:9,shield:8,doubleJump:12,speed:8},l={magnet:"Coin magnet!",fire:"Fire shot!",shield:"Shield!",doubleJump:"Double jump!",speed:"Speed!"};this.power={type:e,timeLeft:i[e]},this.extraJump=!0,this.fireCooldown=0,this.shake=Math.max(this.shake,3),this.spawnPop((t+.5)*n,(s+.4)*n,l[e],this.palette.coinShine),S.power(),this.emitHud(!0)}breakCrates(){const e=this.player,t=3,s=Math.floor((e.x-t)/n),i=Math.floor((e.x+w+t-.01)/n),l=Math.floor((e.y-t)/n),a=Math.floor((e.y+y+t-.01)/n);for(const r of this.level.crates)r.broken||r.col<s||r.col>i||r.row<l||r.row>a||(r.broken=!0,this.grantPower(r.power,r.col,r.row))}applyMagnet(e){var i;if(((i=this.power)==null?void 0:i.type)!=="magnet")return;const t=this.player.x+w/2,s=this.player.y+y/2;for(const l of this.level.coins){if(l.taken)continue;const a=t-l.x,r=s-l.y,u=Math.hypot(a,r);if(u<Fe&&u>.5){const c=Math.min(280*e,u);l.x+=a/u*c,l.y+=r/u*c}}}updateSaws(e){for(const t of this.saws)if(t.angle+=e*9,t.axis&&t.range>0&&t.speed>0){const s=t.range*2/t.speed,i=(this.simTime/s+t.phase)%1,a=(i<.5?i*2:(1-i)*2)*t.range;t.axis==="x"?(t.x=t.originX+a,t.y=t.originY):(t.y=t.originY+a,t.x=t.originX)}}updateShots(e){for(const t of this.shots)if(t.x+=t.vx*e,t.y+=t.vy*e,t.life-=e,!(t.life<=0)){if(this.solidAt(Math.floor(t.x/n),Math.floor(t.y/n))){t.life=0;continue}for(const s of this.enemies)if(s.alive&&t.x>s.x&&t.x<s.x+s.w&&t.y>s.y&&t.y<s.y+s.h){s.hp-=1,s.hitFlash=.18,t.life=0,s.hp<=0&&(s.alive=!1,s.squash=.3,this.coinsCollected+=s.coinValue,this.spawnPop(s.x+s.w/2,s.y,`+${s.coinValue}`,this.palette.coinShine),this.emitHud(!0));break}}this.shots=this.shots.filter(t=>t.life>0)}checkSaws(){if(this.invulnerable())return;const e=this.player;for(const t of this.saws){const s=Math.max(e.x,Math.min(t.x,e.x+w)),i=Math.max(e.y,Math.min(t.y,e.y+y)),l=t.x-s,a=t.y-i,r=Math.max(2,t.r-1);if(l*l+a*a<r*r){this.die();return}}}solidAt(e,t){return e<0||e>=this.level.cols?!0:t<0||t>=this.level.rows?!1:this.level.tiles[t*this.level.cols+e]===1}moveBodyX(e,t){e.x+=e.vx*t;const s=Math.floor(e.y/n),i=Math.floor((e.y+e.h-.01)/n);if(e.vx>0){const l=Math.floor((e.x+e.w-.01)/n);for(let a=s;a<=i;a+=1)if(this.solidAt(l,a)){e.x=l*n-e.w,e.vx=0;break}}else if(e.vx<0){const l=Math.floor(e.x/n);for(let a=s;a<=i;a+=1)if(this.solidAt(l,a)){e.x=(l+1)*n,e.vx=0;break}}}moveBodyY(e,t){e.y+=e.vy*t;const s=Math.floor(e.x/n),i=Math.floor((e.x+e.w-.01)/n);if(e.vy>0){const l=Math.floor((e.y+e.h-.01)/n);for(let a=s;a<=i;a+=1)if(this.solidAt(a,l)){e.y=l*n-e.h,e.vy=0,e.onGround=!0;break}}else if(e.vy<0){const l=Math.floor(e.y/n);for(let a=s;a<=i;a+=1)if(this.solidAt(a,l)){e.y=(l+1)*n,e.vy=0;break}}}updateMovers(){for(const e of this.movers){e.prevX=e.x,e.prevY=e.y;const t=e.range*2/e.speed,s=(this.simTime/t+e.phase)%1,l=(s<.5?s*2:(1-s)*2)*e.range;e.axis==="x"?(e.x=e.originX+l,e.y=e.originY):(e.y=e.originY+l,e.x=e.originX),e.dx=e.x-e.prevX,e.dy=e.y-e.prevY}}updateEnemies(e){for(const t of this.enemies){if(!t.alive){t.squash>0&&(t.squash=Math.max(0,t.squash-e));continue}if(t.hitFlash>0&&(t.hitFlash=Math.max(0,t.hitFlash-e)),t.type==="flyer"){t.bob+=e,t.x+=t.dir*E.flyer.speed*e;const a=t.dir>0?t.x+t.w+2:t.x-2,r=Math.floor((t.y+t.h/2)/n);this.solidAt(Math.floor(a/n),r)&&(t.dir*=-1);const u=2*n,c=Math.max(n,t.originX-u),h=Math.min(this.level.cols*n-t.w-n,t.originX+u);t.x<c?(t.x=c,t.dir=1):t.x>h&&(t.x=h,t.dir=-1),t.y=t.baseY+Math.sin(t.bob*2.4)*7,t.onGround=!1;continue}const s=t.hp<t.maxHp?1.7:1,i=t.type==="boss"?E.boss.speed:t.type==="brute"?E.brute.speed*s:t.type==="jumper"?E.jumper.speed:E.walker.speed;t.vy=Math.min(t.vy+V*e,J),t.vx=t.dir*i,(t.type==="jumper"||t.type==="boss")&&t.onGround&&(t.timer-=e,t.timer<=0&&(t.vy=t.type==="boss"?-470:-430,t.timer=t.type==="boss"?2.4:1.5)),t.onGround=!1;const l=t.vx;this.moveBodyX(t,e),l!==0&&t.vx===0&&(t.dir*=-1),this.moveBodyY(t,e),this.avoidLedge(t),t.y>this.level.rows*n+100&&(t.alive=!1)}}avoidLedge(e){if(!e.onGround)return;const t=e.dir>0?e.x+e.w+1:e.x-1,s=Math.floor(t/n),i=Math.floor((e.y+e.h+1)/n);this.solidAt(s,i)||(e.dir*=-1)}collectCoins(){const e=this.player.x+w/2,t=this.player.y+y/2;for(const s of this.level.coins)s.taken||Math.abs(e-s.x)<12&&Math.abs(t-s.y)<13&&(s.taken=!0,this.coinsCollected+=1,this.spawnPop(s.x,s.y,"+1",this.palette.coinShine),S.coin(),this.emitHud(!0))}killEnemy(e,t){e.alive=!1,e.squash=.3,this.coinsCollected+=e.coinValue,this.spawnPop(e.x+e.w/2,e.y+e.h/2,`+${e.coinValue}`,this.palette.coinShine),this.shake=Math.max(this.shake,e.type==="boss"?9:4),e.type==="boss"?(S.bossDown(),this.spawnPop(e.x+e.w/2,e.y-12,"Boss defeated!",this.palette.spike)):S.stomp(),t&&(this.player.vy=-380,this.player.onGround=!1),this.emitHud(!0)}checkEnemies(){const e=this.player;for(const t of this.enemies){if(!t.alive||e.x>=t.x+t.w||e.x+w<=t.x||e.y>=t.y+t.h||e.y+y<=t.y)continue;if(!(e.vy>0&&e.y+y<t.y+t.h*.6)){if(this.invulnerable()){this.killEnemy(t,!0);continue}this.die();return}t.hp-=1,t.hitFlash=.18,e.vy=-380,e.onGround=!1,t.hp<=0?this.killEnemy(t,!1):(e.y=t.y-y,t.type==="boss"?S.bossHit():S.stomp())}}checkSpikes(){if(this.invulnerable())return;const e=this.player,t=Math.floor(e.x/n),s=Math.floor((e.x+w-.01)/n),i=Math.floor(e.y/n),l=Math.floor((e.y+y-.01)/n);for(let a=i;a<=l;a+=1)for(let r=t;r<=s;r+=1){if(a<0||a>=this.level.rows||r<0||r>=this.level.cols||this.level.tiles[a*this.level.cols+r]!==2)continue;const u=r*n+5,c=a*n+9,h=n-10,d=n-9;if(e.x<u+h&&e.x+w>u&&e.y<c+d&&e.y+y>c){this.die();return}}}checkGoal(){const e=this.level.goal,t=e.x*n+3,s=e.y*n+2,i=n-6,l=n-2,a=this.player;a.x<t+i&&a.x+w>t&&a.y<s+l&&a.y+y>s&&this.completeLevel()}updatePops(e){for(const t of this.pops)t.life-=e,t.y+=t.vy*e,t.vy+=130*e;this.pops=this.pops.filter(t=>t.life>0)}updateCamera(){const e=this.level.cols*n,t=this.level.rows*n,s=this.player.x+w/2-this.viewW/2;if(e<=this.viewW?this.camX=(e-this.viewW)/2:this.camX=Math.max(0,Math.min(e-this.viewW,s)),t<=this.viewH)this.camY=t-this.viewH;else{const i=this.player.y+y/2-this.viewH/2;this.camY=Math.max(0,Math.min(t-this.viewH,i))}}render(){var d;const e=this.ctx,t=this.palette,{cssW:s,cssH:i,dpr:l,scale:a}=this;e.setTransform(l,0,0,l,0,0);const r=e.createLinearGradient(0,0,0,i);r.addColorStop(0,t.skyTop),r.addColorStop(1,t.skyBottom),e.fillStyle=r,e.fillRect(0,0,s,i);let u=0,c=0;this.shake>0&&(u=(Math.random()-.5)*this.shake,c=(Math.random()-.5)*this.shake);const h=((this.level.rows-1)*n-this.camY)*a;this.drawHills(e,s,i,h,.35,30,t.hillFar),this.drawHills(e,s,i,h+6,.6,18,t.hillNear),e.setTransform(l*a,0,0,l*a,(-this.camX*a+u)*l,(-this.camY*a+c)*l),this.drawTiles(e),this.drawMovers(e),this.drawSaws(e),this.drawCoins(e),this.drawCrates(e),this.drawCheckpoints(e),this.drawGoal(e);for(const g of this.enemies)(g.alive||g.squash>0)&&this.drawEnemy(e,g);this.drawShots(e),this.dead||this.drawPlayer(e),((d=this.power)==null?void 0:d.type)==="shield"&&this.drawShield(e),this.drawPops(e),e.setTransform(l,0,0,l,0,0)}drawHills(e,t,s,i,l,a,r){e.fillStyle=r,e.beginPath();const u=-this.camX*this.scale*l;e.moveTo(0,s);for(let c=0;c<=t+8;c+=8){const h=(c-u)/150,d=i-(a+a*Math.sin(h));e.lineTo(c,d)}e.lineTo(t,s),e.closePath(),e.fill()}drawTiles(e){const{cols:t,rows:s}=this.level,i=Math.max(0,Math.floor(this.camX/n)-1),l=Math.min(t-1,Math.ceil((this.camX+this.viewW)/n)+1),a=Math.max(0,Math.floor(this.camY/n)-1),r=Math.min(s-1,Math.ceil((this.camY+this.viewH)/n)+1);for(let u=a;u<=r;u+=1)for(let c=i;c<=l;c+=1){const h=this.level.tiles[u*t+c],d=c*n,g=u*n;h===1?(e.fillStyle=this.palette.solid,e.fillRect(d,g,n,n),(u===0||this.level.tiles[(u-1)*t+c]!==1)&&(e.fillStyle=this.palette.solidTop,e.fillRect(d,g,n,4))):h===2&&this.drawSpike(e,d,g)}}drawSpike(e,t,s){e.fillStyle=this.palette.spike;const i=s+n;for(const[l,a]of[[t+2,t+n/2],[t+n/2,t+n-2]])e.beginPath(),e.moveTo(l,i),e.lineTo((l+a)/2,s+4),e.lineTo(a,i),e.closePath(),e.fill();e.fillStyle="rgba(0, 0, 0, 0.18)",e.fillRect(t+2,i-3,n-4,3)}drawMovers(e){for(const t of this.movers){e.fillStyle="rgba(0, 0, 0, 0.16)",e.fillRect(t.x+2,t.y+t.h,t.w-4,3),e.fillStyle=this.palette.mover,m(e,t.x,t.y,t.w,t.h,4),e.fill(),e.fillStyle=this.palette.moverTop,m(e,t.x+2,t.y+2,t.w-4,t.h*.45,3),e.fill(),e.fillStyle="rgba(0, 0, 0, 0.28)";for(let a=1;a<Math.round(t.w/n);a+=1)e.beginPath(),e.arc(t.x+a*n,t.y+t.h/2,1.6,0,Math.PI*2),e.fill();const s=t.x+t.w/2,i=t.y+t.h/2,l=.6+.4*Math.sin(this.elapsed*5);if(e.globalAlpha=l,e.fillStyle="#ffffff",e.beginPath(),t.axis==="x"){const a=Math.sign(t.x-t.prevX)||1;e.moveTo(s+a*5,i),e.lineTo(s-a*3,i-3.5),e.lineTo(s-a*3,i+3.5)}else{const a=Math.sign(t.y-t.prevY)||1;e.moveTo(s,i+a*4),e.lineTo(s-3.5,i-a*3),e.lineTo(s+3.5,i-a*3)}e.closePath(),e.fill(),e.globalAlpha=1}}drawCoins(e){for(const t of this.level.coins){if(t.taken)continue;const s=Math.sin(this.elapsed*4+t.col)*1.6,i=t.x,l=t.y+s,a=5.5;e.fillStyle=this.palette.coin,e.beginPath(),e.arc(i,l,a,0,Math.PI*2),e.fill(),e.fillStyle=this.palette.coinShine,e.beginPath(),e.arc(i-1.4,l-1.4,a*.42,0,Math.PI*2),e.fill()}}drawCheckpoints(e){for(const t of this.level.checkpoints){const s=(t.col+.5)*n,i=t.row*n,l=(t.row+1)*n;e.fillStyle=t.active?this.palette.goalFlag:this.palette.mover,e.fillRect(s-1.5,i+3,3,n-3),e.fillStyle=t.active?this.palette.goal:this.palette.moverTop;const a=t.active?Math.sin(this.elapsed*6)*1.2:0;e.beginPath(),e.moveTo(s+1.5,i+4),e.lineTo(s+11,i+8+a),e.lineTo(s+1.5,i+12),e.closePath(),e.fill(),e.fillStyle=t.active?this.palette.goalFlag:this.palette.mover,e.fillRect(s-5,l-3,10,3)}}drawSaws(e){for(const t of this.saws){e.save(),e.translate(t.x,t.y),e.rotate(t.angle);const s=8;e.fillStyle=this.palette.saw,e.beginPath();for(let i=0;i<s;i+=1){const l=i/s*Math.PI*2,a=(i+.5)/s*Math.PI*2;e.lineTo(Math.cos(l)*t.r,Math.sin(l)*t.r),e.lineTo(Math.cos(a)*t.r*.72,Math.sin(a)*t.r*.72)}e.closePath(),e.fill(),e.fillStyle=this.palette.sawShine,e.beginPath(),e.arc(0,0,t.r*.42,0,Math.PI*2),e.fill(),e.fillStyle=this.palette.saw,e.beginPath(),e.arc(0,0,t.r*.16,0,Math.PI*2),e.fill(),e.restore()}}drawCrates(e){for(const t of this.level.crates){if(t.broken)continue;const s=t.col*n,i=t.row*n,l=2,a=n-l*2,r=.5+.5*Math.sin(this.elapsed*4+t.col);e.fillStyle=this.palette.crateDark,m(e,s+l,i+l,a,a,4),e.fill(),e.fillStyle=this.palette.crate,m(e,s+l+2,i+l+2,a-4,a-4,3),e.fill(),e.strokeStyle=this.palette.crateDark,e.lineWidth=2,e.beginPath(),e.moveTo(s+l+2,i+n/2),e.lineTo(s+n-l-2,i+n/2),e.moveTo(s+n/2,i+l+2),e.lineTo(s+n/2,i+n-l-2),e.stroke(),e.globalAlpha=.55+r*.45,e.fillStyle="#ffffff",e.font="bold 12px ui-sans-serif, system-ui, sans-serif",e.textAlign="center",e.textBaseline="middle",e.fillText("?",s+n/2,i+n/2+1),e.globalAlpha=1,e.textAlign="start",e.textBaseline="alphabetic"}}drawShots(e){for(const t of this.shots)e.fillStyle="#ff8a3d",e.beginPath(),e.arc(t.x,t.y,5,0,Math.PI*2),e.fill(),e.fillStyle="#ffe08a",e.beginPath(),e.arc(t.x-1,t.y-1,5*.5,0,Math.PI*2),e.fill(),e.globalAlpha=.5,e.fillStyle="#ff5a1f",e.beginPath(),e.arc(t.x-Math.sign(t.vx)*6,t.y,5*.7,0,Math.PI*2),e.fill(),e.globalAlpha=1}drawShield(e){const t=this.player,s=t.x+w/2,i=t.y+y/2,l=1+Math.sin(this.elapsed*8)*.08;e.strokeStyle=this.palette.playerShine,e.lineWidth=2,e.globalAlpha=.75,e.beginPath(),e.arc(s,i,w*.95*l,0,Math.PI*2),e.stroke(),e.globalAlpha=1}drawGoal(e){const t=this.level.goal,s=(t.x+.5)*n,i=t.y*n,l=(t.y+1)*n;e.fillStyle=this.palette.goal,e.fillRect(s-1.5,i+2,3,n-2),e.fillStyle=this.palette.goalFlag;const a=Math.sin(this.elapsed*5)*1.5;e.beginPath(),e.moveTo(s+1.5,i+3),e.lineTo(s+13,i+7+a),e.lineTo(s+1.5,i+12),e.closePath(),e.fill(),e.fillStyle=this.palette.goal,e.fillRect(s-6,l-3,12,3)}drawPlayer(e){const t=this.player,s=w,i=y,l=t.x+s/2,a=t.y+i,r=1+t.stretch,u=1-t.stretch*.7,c=Math.sin(t.walk)*(t.onGround?1:0);e.save(),e.translate(l,a),e.scale(u,r),e.translate(0,-i/2),e.fillStyle="rgba(0, 0, 0, 0.16)",m(e,-s/2-3+c*2,i/2-3,6,3,1.5),e.fill(),m(e,s/2-3-c*2,i/2-3,6,3,1.5),e.fill(),e.fillStyle=this.palette.player,m(e,-s/2,-i/2,s,i,5),e.fill(),e.fillStyle=this.palette.playerShine,m(e,-s/2+2.5,-i/2+2.5,s*.42,i*.5,3),e.fill();const h=-i/2+7,d=t.facing*1.4;for(const g of[-3.6,3.6])e.fillStyle=this.palette.eye,e.beginPath(),e.arc(g,h,3.1,0,Math.PI*2),e.fill(),e.fillStyle=this.palette.pupil,e.beginPath(),e.arc(g+d,h+.4,1.6,0,Math.PI*2),e.fill();e.restore()}drawEnemy(e,t){const s=t.w,i=t.h,l=t.x+s/2,a=t.y+i,r=!t.alive,u=r?1-t.squash/.3:0,c=r?1-u*.85:1,h=r?Math.max(0,1-u):1;e.save(),e.globalAlpha=h,e.translate(l,a),e.scale(1,c),e.translate(0,-i/2),e.fillStyle="rgba(0, 0, 0, 0.16)",e.beginPath(),e.ellipse(0,i/2-1,s*.5,2.6,0,0,Math.PI*2),e.fill(),t.type==="flyer"?this.drawFlyer(e,t,s,i):t.type==="jumper"?this.drawJumper(e,t,s,i):t.type==="brute"?this.drawBrute(e,t,s,i):t.type==="boss"?this.drawBoss(e,t,s,i):this.drawWalker(e,t,s,i),t.hitFlash>0&&(e.globalAlpha=h*Math.min(1,t.hitFlash/.18)*.75,e.fillStyle="#ffffff",m(e,-s/2,-i/2,s,i,6),e.fill(),e.globalAlpha=h),e.restore()}drawEvilFace(e,t,s,i){const l=-s*.12,a=t*.22,r=Math.max(2.6,t*.16);for(const u of[-1,1])e.fillStyle=this.palette.eye,e.beginPath(),e.arc(u*a,l,r,0,Math.PI*2),e.fill(),e.fillStyle=this.palette.pupil,e.beginPath(),e.arc(u*a+i,l+r*.15,r*.52,0,Math.PI*2),e.fill();e.strokeStyle=this.palette.pupil,e.lineWidth=Math.max(1.6,t*.1),e.lineCap="round";for(const u of[-1,1])e.beginPath(),e.moveTo(u*a-u*r,l-r*1.1),e.lineTo(u*a+u*r*.7,l-r*1.7),e.stroke();e.beginPath(),e.arc(0,s*.22,t*.2,Math.PI*1.15,Math.PI*1.85),e.stroke()}drawWalker(e,t,s,i){const l=E.walker,a=Math.sin(this.elapsed*9+t.x*.1)*1.4;e.fillStyle=l.shade,m(e,-s*.42+a,i/2-4,s*.34,4,2),e.fill(),m(e,s*.08-a,i/2-4,s*.34,4,2),e.fill(),e.fillStyle=l.color,m(e,-s/2,-i/2,s,i,6),e.fill(),e.fillStyle=l.color;for(const r of[-1,1])e.beginPath(),e.moveTo(r*s*.34,-i/2+1),e.lineTo(r*s*.5,-i/2-4),e.lineTo(r*s*.14,-i/2+1),e.closePath(),e.fill();e.fillStyle="rgba(255, 255, 255, 0.22)",m(e,-s/2+2,-i/2+2,s*.36,i*.4,3),e.fill(),this.drawEvilFace(e,s,i,t.dir*1.2)}drawJumper(e,t,s,i){const l=E.jumper,a=t.onGround?1:.92;e.fillStyle=l.shade;for(let r=0;r<3;r+=1)e.beginPath(),e.ellipse(0,i/2-1-r*2.2,s*.32,1.6,0,0,Math.PI*2),e.fill();e.save(),e.scale(1,a),e.fillStyle=l.color,m(e,-s/2,-i/2-2,s,i+2,7),e.fill(),e.fillStyle="rgba(255, 255, 255, 0.24)",m(e,-s/2+2,-i/2,s*.36,i*.4,3),e.fill(),e.strokeStyle=l.shade,e.lineWidth=1.6,e.beginPath(),e.moveTo(0,-i/2-2),e.lineTo(0,-i/2-6),e.stroke(),e.fillStyle=l.shade,e.beginPath(),e.arc(0,-i/2-7,2.4,0,Math.PI*2),e.fill(),this.drawEvilFace(e,s,i+2,t.dir*1.2),e.restore()}drawFlyer(e,t,s,i){const l=E.flyer,a=Math.sin(this.elapsed*16+t.bob)*.5;e.fillStyle="rgba(255, 255, 255, 0.75)";for(const r of[-1,1])e.save(),e.translate(r*s*.4,-i*.1),e.rotate(r*(.5+a)),e.beginPath(),e.ellipse(0,0,s*.34,i*.2,0,0,Math.PI*2),e.fill(),e.restore();e.fillStyle=l.color,m(e,-s/2,-i/2,s,i,7),e.fill(),e.fillStyle="rgba(255, 255, 255, 0.24)",m(e,-s/2+2,-i/2+2,s*.34,i*.4,3),e.fill(),e.strokeStyle=l.shade,e.lineWidth=1.4;for(const r of[-1,1])e.beginPath(),e.moveTo(r*s*.2,-i/2),e.lineTo(r*s*.3,-i/2-5),e.stroke(),e.fillStyle=l.shade,e.beginPath(),e.arc(r*s*.3,-i/2-6,1.8,0,Math.PI*2),e.fill();this.drawEvilFace(e,s,i,t.dir*1.2)}drawBrute(e,t,s,i){const l=E.brute,a=t.hp<t.maxHp;e.fillStyle=l.shade,m(e,-s*.46,i/2-5,s*.38,5,2),e.fill(),m(e,s*.08,i/2-5,s*.38,5,2),e.fill(),e.fillStyle=l.color,m(e,-s/2,-i/2,s,i,8),e.fill(),e.fillStyle=l.shade;for(const r of[-1,1])e.beginPath(),e.moveTo(r*s*.32,-i/2+1),e.lineTo(r*s*.52,-i/2-7),e.lineTo(r*s*.1,-i/2+1),e.closePath(),e.fill();e.fillStyle="rgba(255, 255, 255, 0.2)",m(e,-s/2+3,-i/2+3,s*.34,i*.38,3),e.fill(),this.drawEvilFace(e,s,i,t.dir*1.2),e.fillStyle="#ffffff";for(const r of[-1,1])e.beginPath(),e.moveTo(r*s*.16,i*.3),e.lineTo(r*s*.06,i*.3),e.lineTo(r*s*.11,i*.14),e.closePath(),e.fill();a&&(e.fillStyle="#ffd166",e.beginPath(),e.arc(0,-i*.34,2,0,Math.PI*2),e.fill())}drawBoss(e,t,s,i){const l=E.boss,a=t.hp<t.maxHp;e.fillStyle=l.shade;for(const h of[-1,1])m(e,h*s*.5-(h>0?0:6),-i*.15,6,i*.4,3),e.fill();m(e,-s*.44,i/2-6,s*.36,6,3),e.fill(),m(e,s*.08,i/2-6,s*.36,6,3),e.fill(),e.fillStyle=l.color,m(e,-s/2,-i/2,s,i,10),e.fill(),e.fillStyle=l.shade;for(const h of[-1,1])e.beginPath(),e.moveTo(h*s*.3,-i/2+1),e.lineTo(h*s*.6,-i/2-12),e.lineTo(h*s*.06,-i/2+1),e.closePath(),e.fill();a&&(e.fillStyle="#ffd166",e.beginPath(),e.moveTo(-s*.22,-i/2+2),e.lineTo(-s*.14,-i/2-8),e.lineTo(-s*.05,-i/2-1),e.lineTo(0,-i/2-10),e.lineTo(s*.05,-i/2-1),e.lineTo(s*.14,-i/2-8),e.lineTo(s*.22,-i/2+2),e.closePath(),e.fill()),e.fillStyle="rgba(255, 255, 255, 0.18)",m(e,-s/2+4,-i/2+4,s*.32,i*.34,4),e.fill(),this.drawEvilFace(e,s,i,t.dir*1.2),e.fillStyle="#ffffff";for(const h of[-1,1])for(let d=0;d<2;d+=1){const g=h*(s*.2)-h*d*s*.1;e.beginPath(),e.moveTo(g,i*.32),e.lineTo(g-h*s*.08,i*.32),e.lineTo(g-h*s*.04,i*.14),e.closePath(),e.fill()}const r=5,u=t.maxHp,c=-((u-1)*r)/2;for(let h=0;h<u;h+=1)e.fillStyle=h<t.hp?"#ffd166":"rgba(0, 0, 0, 0.35)",e.fillRect(c+h*r-2,-i/2-18,4,4)}drawPops(e){e.textAlign="center",e.textBaseline="middle";for(const t of this.pops){const s=Math.max(0,t.life/t.max);e.globalAlpha=s,e.fillStyle=t.color,e.font="bold 11px ui-sans-serif, system-ui, sans-serif",e.fillText(t.text,t.x,t.y)}e.globalAlpha=1,e.textAlign="start",e.textBaseline="alphabetic"}emitHud(e){var a;const t=((a=this.power)==null?void 0:a.type)??null,s=this.power?Math.max(0,this.power.timeLeft):0,i={levelIndex:this.levelIndex,levelCount:I.length,levelName:this.level.name,coins:this.coinsCollected,coinTotal:this.coinTotal,time:this.levelTime,deaths:this.deaths,hearts:this.hearts,status:this.status,power:t,powerTime:s},l=`${i.levelIndex}|${i.coins}|${i.coinTotal}|${i.time.toFixed(1)}|${i.deaths}|${i.hearts}|${i.status}|${t}|${s.toFixed(1)}`;!e&&l===this.lastHudKey||(this.lastHudKey=l,this.callbacks.onHud(i))}}class Ne{constructor(e){p(this,"sources",new Map);this.target=e}set(e,t){const s=new Set([...this.sources.values()].flatMap(a=>[...a])),i=new Set(t);i.size?this.sources.set(e,i):this.sources.delete(e);const l=new Set([...this.sources.values()].flatMap(a=>[...a]));for(const a of s)l.has(a)||this.target.release(a);for(const a of l)s.has(a)||this.target.press(a)}clear(){for(const e of this.sources.keys())this.set(e,[])}}class We{constructor(e,t){p(this,"previous",new Set);p(this,"activeIndex",null);p(this,"armed",!1);this.input=e,this.commands=t}reset(){this.input.set("controller",[]),this.previous.clear(),this.armed=!1,this.activeIndex=null}update(e){const t=e.filter(h=>!!(h!=null&&h.connected)&&h.mapping==="standard"),s=t.find(h=>h.index===this.activeIndex)??t[0];if(!s)return this.reset(),e.some(h=>h==null?void 0:h.connected)?"unsupported":"waiting";s.index!==this.activeIndex&&(this.reset(),this.activeIndex=s.index);const i=new Set(s.buttons.flatMap((h,d)=>h.pressed?[d]:[])),l=s.axes[0]??0;if(!this.armed)return this.armed=i.size===0&&Math.abs(l)<.25,this.previous=i,"connected";const a=h=>i.has(h)&&!this.previous.has(h),r=this.commands.playing();let u=!1;a(9)?(r?this.commands.pause():this.commands.primary(),u=!0):!r&&a(0)?(this.commands.primary(),u=!0):!r&&a(3)&&(this.commands.restart(),u=!0);const c=[];return r&&!u&&((i.has(14)||l<-.25)&&c.push("left"),(i.has(15)||l>.25)&&c.push("right"),i.has(0)&&c.push("jump"),i.has(2)&&c.push("fire")),this.input.set("controller",c),u&&(this.armed=!1),this.previous=i,"connected"}}const oe="super-platformer/best-v1",f={stage:b("#stage"),canvas:b("#game"),overlay:b("#overlay"),overlayTitle:b("#overlayTitle"),overlayText:b("#overlayText"),overlayHint:b("#overlayHint"),primaryButton:b("#primaryButton"),secondaryButton:b("#secondaryButton"),hudLevel:b("#hudLevel"),hudCoins:b("#hudCoins"),hudTime:b("#hudTime"),hudBest:b("#hudBest"),hudHearts:b("#hudHearts"),hudPower:b("#hudPower"),hudPowerWrap:b("#hudPowerWrap"),soundButton:b("#soundButton"),pauseButton:b("#pauseButton"),restartButton:b("#restartButton"),touchButtons:Array.from(document.querySelectorAll("#touchControls button[data-action]"))},De={magnet:"Magnet",fire:"Fire",shield:"Shield",doubleJump:"Double jump",speed:"Speed"},k={levels:I.map(()=>null),total:null};let F=null,X="ready",$=null;function b(o){const e=document.querySelector(o);if(!e)throw new Error(`Missing required element: ${o}`);return e}function R(o){if(o>=60){const e=Math.floor(o/60),t=o-e*60;return`${e}:${t.toFixed(1).padStart(4,"0")}`}return o.toFixed(1)}async function ze(){if(G(O))try{const o=await ie.getItem(oe);if(!o)return;const e=JSON.parse(o);Array.isArray(e.levels)&&e.levels.forEach((t,s)=>{s<k.levels.length&&typeof t=="number"&&Number.isFinite(t)&&(k.levels[s]=t)}),typeof e.total=="number"&&Number.isFinite(e.total)&&(k.total=e.total),ae()}catch{}}async function x(){if(G(O))try{await ie.setItem(oe,JSON.stringify(k))}catch{}}function ae(o=(F==null?void 0:F.levelIndex)??0){const e=k.levels[o];f.hudBest.textContent=e==null?"--":R(e)}function Oe(o){F=o,X=o.status,f.hudLevel.textContent=`${o.levelIndex+1}/${o.levelCount}`,f.hudCoins.textContent=`${o.coins}/${o.coinTotal}`,f.hudTime.textContent=R(o.time),ae(o.levelIndex),document.body.dataset.status=o.status;const e="♥".repeat(Math.max(0,o.hearts)),t="♡".repeat(Math.max(0,3-o.hearts));f.hudHearts.textContent=e+t,f.hudHearts.classList.toggle("low",o.hearts<=1),o.power?(f.hudPowerWrap.hidden=!1,f.hudPower.textContent=`${De[o.power]??o.power} ${Math.ceil(o.powerTime)}`):f.hudPowerWrap.hidden=!0;const s=o.status==="paused";f.pauseButton.textContent=s?"▶":"❚❚",f.pauseButton.setAttribute("aria-label",s?"Resume":"Pause"),f.pauseButton.title=s?"Resume (P)":"Pause (P)",f.pauseButton.disabled=o.status!=="playing"&&!s,f.restartButton.disabled=o.status==="ready"}function le(o,e){const t=f.overlay;switch(o){case"playing":t.hidden=!0;return;case"ready":t.hidden=!1,f.overlayTitle.textContent="Ready",f.overlayText.textContent=`${I.length} short levels. Grab the coins, reach the flag.`,f.primaryButton.textContent="Play",f.secondaryButton.hidden=!0,f.overlayHint.hidden=!1;break;case"paused":t.hidden=!1,f.overlayTitle.textContent="Paused",f.overlayText.textContent=`${e.levelName} · ${R(e.levelTime)} · ${e.deaths} falls`,f.primaryButton.textContent="Resume",f.secondaryButton.hidden=!1,f.secondaryButton.textContent="Restart level",f.overlayHint.hidden=!1;break;case"levelComplete":{t.hidden=!1;const s=k.levels[e.levelIndex],i=s!=null&&Math.abs(s-e.levelTime)<.001;f.overlayTitle.textContent="Level complete",f.overlayText.textContent=`${e.levelName} · ${R(e.levelTime)}${i?" · new best!":""}`,f.primaryButton.textContent="Next level",f.secondaryButton.hidden=!1,f.secondaryButton.textContent="Replay",f.overlayHint.hidden=!0;break}case"allComplete":{t.hidden=!1;const s=k.total!=null&&Math.abs(k.total-e.totalTime)<.001;f.overlayTitle.textContent="All levels complete",f.overlayText.textContent=`Total ${R(e.totalTime)}${s?" · new best!":""}`,f.primaryButton.textContent="Play again",f.secondaryButton.hidden=!0,f.overlayHint.hidden=!0;break}case"gameOver":{t.hidden=!1,f.overlayTitle.textContent="Game over",f.overlayText.textContent=`Out of hearts on ${e.levelName}.`,f.primaryButton.textContent="Try again",f.secondaryButton.hidden=!0,f.overlayHint.hidden=!0;break}}}function _e(o,e){if(X=o,o==="levelComplete"||o==="allComplete"){const t=k.levels[e.levelIndex];(t==null||e.levelTime<t)&&(k.levels[e.levelIndex]=e.levelTime,x()),o==="allComplete"&&(k.total==null||e.totalTime<k.total)&&(k.total=e.totalTime,x())}le(o,e)}const M=new Ye(f.canvas,{onHud:Oe,onStatusChange:(o,e)=>_e(o,e)}),qe={levelIndex:0,levelName:I[0].name,levelTime:0,totalTime:0,deaths:0};le("ready",qe);function Q(o){const e=document.documentElement.style;e.setProperty("--bg",o.colors.background),e.setProperty("--fg",o.colors.text),e.setProperty("--primary",o.colors.primary),M.refreshPalette()}function Ve(){if(G(te))try{ye().then(Q).catch(()=>{}),$=ve(Q)}catch{}}const H=new Ne(M),ne={ArrowLeft:"left",KeyA:"left",ArrowRight:"right",KeyD:"right",ArrowUp:"jump",KeyW:"jump",Space:"jump",KeyX:"fire",KeyJ:"fire"};function L(){f.canvas.focus({preventScroll:!0})}window.addEventListener("keydown",o=>{if(o.code==="KeyP"||o.code==="Escape"){o.repeat||M.togglePause(),o.preventDefault();return}if(o.code==="KeyR"){o.repeat||M.restartLevel(),o.preventDefault();return}if(!f.overlay.hidden&&o.code==="Enter"){o.preventDefault(),M.primary(),L();return}if(!f.overlay.hidden&&o.code==="Space"&&X!=="paused"){o.preventDefault(),M.primary(),L();return}const e=ne[o.code];e&&(o.preventDefault(),!o.repeat&&H.set(`key:${o.code}`,[e]))});window.addEventListener("keyup",o=>{ne[o.code]&&(o.preventDefault(),H.set(`key:${o.code}`,[]))});const z=new We(H,{playing:()=>X==="playing",primary:()=>M.primary(),pause:()=>M.togglePause(),restart:()=>M.restartLevel()}),Je="← → move · Space jump · X fire · P pause",Ke={waiting:"Controller: press a button to connect.",connected:"Controller: stick / D-pad move · A/× jump / confirm · X/□ fire · Start pause · Y/△ retry in menus",unsupported:"Controller mapping unsupported. Use keyboard or touch.",unavailable:"Controller unavailable in this browser or host. Use keyboard or touch."};let Z=null,re=0,W=typeof navigator.getGamepads!="function";function he(){let o=W?"unavailable":"waiting";if(!W&&!document.hidden&&document.hasFocus())try{o=z.update(navigator.getGamepads())}catch{z.reset(),W=!0,o="unavailable"}o!==Z&&(Z=o,f.overlayHint.textContent=`${Je} · ${Ke[o]}`),re=requestAnimationFrame(he)}function _(){H.clear(),z.reset(),M.releaseAll();for(const o of f.touchButtons)o.classList.remove("active")}window.addEventListener("blur",_);document.addEventListener("visibilitychange",()=>{document.hidden&&_()});he();for(const o of f.touchButtons){const e=o.dataset.action;if(!e)continue;const t=i=>{i.preventDefault(),o.setPointerCapture(i.pointerId),o.classList.add("active"),H.set(`touch:${i.pointerId}`,[e])},s=i=>{o.hasPointerCapture(i.pointerId)&&o.releasePointerCapture(i.pointerId),o.classList.remove("active"),H.set(`touch:${i.pointerId}`,[])};o.addEventListener("pointerdown",t),o.addEventListener("pointerup",s),o.addEventListener("pointercancel",s),o.addEventListener("lostpointercapture",s)}f.stage.addEventListener("pointerdown",()=>{S.resume(),L()});f.primaryButton.addEventListener("click",()=>{S.resume(),M.primary(),L()});f.secondaryButton.addEventListener("click",()=>{M.restartLevel(),L()});f.pauseButton.addEventListener("click",()=>{M.togglePause(),L()});f.restartButton.addEventListener("click",()=>{M.restartLevel(),L()});function fe(){const o=S.enabled;f.soundButton.textContent=o?"♪":"♪̸",f.soundButton.setAttribute("aria-label",o?"Mute sound":"Unmute sound"),f.soundButton.title=o?"Sound on":"Sound off"}f.soundButton.addEventListener("click",()=>{S.setEnabled(!S.enabled),fe(),L()});Me(o=>{S.setEnabled(o.sound),fe()});const ue=new ResizeObserver(()=>M.resize());ue.observe(f.stage);window.addEventListener("resize",()=>M.resize());window.addEventListener("pagehide",()=>{$==null||$.close(),ue.disconnect(),cancelAnimationFrame(re),_(),M.destroy()},{once:!0});ze();Ve();M.resize();
</script>
<style>:root{--bg: #f4f6f8;--fg: #17212b;--primary: #2563eb;--surface: color-mix(in srgb, var(--bg) 88%, var(--fg));--surface-2: color-mix(in srgb, var(--bg) 76%, var(--fg));--border: color-mix(in srgb, var(--fg) 16%, transparent);--muted: color-mix(in srgb, var(--fg) 58%, var(--bg));--space-1: 4px;--space-2: 8px;--space-3: 12px;--control-h: 30px;--radius: 6px;--napplet-text-selection: none;color-scheme:light dark;font:13px/1.4 Inter,ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif}*{box-sizing:border-box}:where(html,body,body *){-webkit-user-select:var(--napplet-text-selection, none);user-select:var(--napplet-text-selection, none)}html,body,#app{height:100%;margin:0;background:var(--bg);color:var(--fg)}button{font:inherit;color:inherit}[hidden]{display:none!important}.app{container-type:inline-size;position:relative;overflow:hidden}.stage{position:absolute;top:0;right:0;bottom:0;left:0}#game{position:absolute;top:0;right:0;bottom:0;left:0;display:block;width:100%;height:100%;outline:none;touch-action:none}.hud{position:absolute;inset:0 0 auto 0;display:flex;align-items:center;gap:var(--space-2);padding:var(--space-1) var(--space-2);background:linear-gradient(to bottom,color-mix(in srgb,var(--bg) 72%,transparent),transparent);pointer-events:none;font-variant-numeric:tabular-nums}.hud-stats{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-1) var(--space-2);min-width:0}.stat{display:inline-flex;align-items:baseline;gap:var(--space-1);padding:2px var(--space-2);border:1px solid var(--border);border-radius:999px;background:color-mix(in srgb,var(--bg) 62%,transparent);white-space:nowrap}.stat-key{display:none;color:var(--muted);font-size:9px;font-weight:700;letter-spacing:.06em;text-transform:uppercase}.stat-val{font-size:12px;font-weight:700}.stat-best{display:none}.stat-power{display:inline-flex;border-color:color-mix(in srgb,var(--primary) 55%,transparent);background:color-mix(in srgb,var(--primary) 20%,transparent);color:var(--primary)}.stat-hearts{padding-inline:var(--space-1);border-color:color-mix(in srgb,#e5484d 45%,transparent);background:color-mix(in srgb,#e5484d 14%,transparent)}.hearts{color:#e5484d;letter-spacing:1px;font-size:14px}.hearts.low{animation:heart-pulse .8s ease-in-out infinite}@keyframes heart-pulse{0%,to{opacity:1}50%{opacity:.4}}.hud-actions{display:flex;gap:var(--space-1);margin-left:auto;pointer-events:auto}.hud-actions button{display:inline-grid;place-items:center;width:var(--control-h);height:var(--control-h);padding:0;border:1px solid var(--border);border-radius:var(--radius);background:color-mix(in srgb,var(--bg) 70%,transparent);cursor:pointer;font-size:12px}.hud-actions button:hover:not(:disabled){border-color:var(--primary)}.hud-actions button:disabled{opacity:.4;cursor:not-allowed}.touch{position:absolute;inset:auto 0 0 0;display:none;align-items:flex-end;justify-content:space-between;gap:var(--space-2);padding:var(--space-2);pointer-events:none}.touch-move{display:flex;gap:var(--space-2)}.touch-actions{display:flex;align-items:flex-end;gap:var(--space-2)}.touch button{min-width:56px;height:56px;padding:0;border:1px solid var(--border);border-radius:12px;background:color-mix(in srgb,var(--bg) 55%,transparent);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);font-size:20px;font-weight:700;pointer-events:auto;touch-action:none}.touch button.active{border-color:var(--primary);background:color-mix(in srgb,var(--primary) 38%,transparent)}.touch-jump{min-width:76px;font-size:14px}.touch-fire{min-width:64px;border-color:color-mix(in srgb,#ff8a3d 55%,transparent);color:#b3530f;font-size:13px}@media(pointer:coarse),(hover:none){.touch{display:flex}}body.force-touch .touch{display:flex}.overlay{position:absolute;top:0;right:0;bottom:0;left:0;display:grid;place-items:center;padding:var(--space-3);background:color-mix(in srgb,var(--bg) 66%,transparent);-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px)}.overlay-card{display:grid;gap:var(--space-2);width:min(280px,100%);padding:var(--space-3);border:1px solid var(--border);border-radius:10px;background:var(--surface);text-align:center}.overlay-title{margin:0;font-size:15px;font-weight:700}.overlay-text{margin:0;color:var(--muted);font-size:12px}.overlay-actions{display:flex;justify-content:center;gap:var(--space-2)}.overlay-actions button{min-height:var(--control-h);padding:0 var(--space-3);border:1px solid var(--border);border-radius:var(--radius);background:var(--surface-2);cursor:pointer}.overlay-actions button:hover{border-color:var(--primary)}.overlay-actions .primary{border-color:var(--primary);background:var(--primary);color:#fff}.overlay-hint{margin:0;color:var(--muted);font-size:11px}button:focus-visible,#game:focus-visible{outline:2px solid var(--primary);outline-offset:1px}@container (min-width: 260px){.stat-key{display:inline}}@container (min-width: 480px){.stat-best{display:inline-flex}}@container (min-width: 900px){.hud{padding:var(--space-2) var(--space-3)}.hud-stats{gap:var(--space-2)}}@media(max-height:240px){.overlay-hint{display:none}.overlay-card{padding:var(--space-2)}}
</style>
<meta name="napplet-type" content="super-platformer">
<meta name="napplet-config-schema" content="{"$schema":"http://json-schema.org/draft-07/schema#","$version":1,"type":"object","additionalProperties":false,"properties":{"textSize":{"type":"integer","title":"Text size","description":"Base text size in pixels for the HUD and overlays.","minimum":10,"maximum":20,"default":13,"x-napplet-section":"Appearance","x-napplet-order":1},"controlSize":{"type":"integer","title":"Control height","description":"Height of the HUD and overlay buttons, in pixels.","minimum":24,"maximum":44,"default":30,"x-napplet-section":"Appearance","x-napplet-order":2},"touchControls":{"type":"boolean","title":"Always show on-screen controls","description":"Show the touch buttons even when a keyboard is available.","default":false,"x-napplet-section":"Controls"},"sound":{"type":"boolean","title":"Sound effects","description":"Play synthesized jump, coin, power-up and hit sounds.","default":true,"x-napplet-section":"Audio"}}}">
</head>
<body>
<!--
A napplet is an applet framed by a runtime: the runtime already shows this
napplet's name, so there is no title header here. The frame is the game
surface from the first pixel and adapts to any frame size.
-->
<main id="app" class="app">
<div id="stage" class="stage">
<canvas
id="game"
tabindex="0"
aria-label="Platformer. Arrow keys or A and D to move, Space or W to jump."
></canvas>
<div class="hud">
<div class="hud-stats">
<span class="stat stat-hearts">
<span id="hudHearts" class="stat-val hearts" aria-label="Lives">♥♥♥</span>
</span>
<span class="stat">
<span class="stat-key">Level</span>
<span id="hudLevel" class="stat-val">1/3</span>
</span>
<span class="stat">
<span class="stat-key">Coins</span>
<span id="hudCoins" class="stat-val">0/0</span>
</span>
<span class="stat">
<span class="stat-key">Time</span>
<span id="hudTime" class="stat-val">0.0</span>
</span>
<span class="stat stat-best">
<span class="stat-key">Best</span>
<span id="hudBest" class="stat-val">--</span>
</span>
<span id="hudPowerWrap" class="stat stat-power" hidden>
<span aria-hidden="true">★</span>
<span id="hudPower" class="stat-val">Power</span>
</span>
</div>
<div class="hud-actions">
<button id="soundButton" type="button" aria-label="Mute sound" title="Sound">♪</button>
<button id="pauseButton" type="button" aria-label="Pause" title="Pause (P)">❚❚</button>
<button id="restartButton" type="button" aria-label="Restart level" title="Restart (R)">↻</button>
</div>
</div>
<div id="touchControls" class="touch">
<div class="touch-move">
<button type="button" data-action="left" aria-label="Move left">◀</button>
<button type="button" data-action="right" aria-label="Move right">▶</button>
</div>
<div class="touch-actions">
<button type="button" data-action="fire" class="touch-fire" aria-label="Shoot">Fire</button>
<button type="button" data-action="jump" class="touch-jump" aria-label="Jump">Jump</button>
</div>
</div>
<div id="overlay" class="overlay" hidden>
<div class="overlay-card">
<p id="overlayTitle" class="overlay-title">Ready</p>
<p id="overlayText" class="overlay-text"></p>
<div class="overlay-actions">
<button id="primaryButton" type="button" class="primary">Play</button>
<button id="secondaryButton" type="button" hidden>Replay</button>
</div>
<p id="overlayHint" class="overlay-hint">← → move · Space jump · X fire · P pause</p>
</div>
</div>
</div>
</main>
</body>
</html>
