littlejsengine 1.16.1 → 1.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
1
  // LittleJS Engine - MIT License - Copyright 2021 Frank Force
2
2
  // https://github.com/KilledByAPixel/LittleJS
3
3
 
4
- "use strict";const engineName="LittleJS";const engineVersion="1.16.1";const frameRate=60;const timeDelta=1/frameRate;let engineObjects=[];let engineObjectsCollide=[];let frame=0;let time=0;let timeReal=0;let paused=false;function getPaused(){return paused}function setPaused(isPaused=true){paused=isPaused}let frameTimeLastMS=0,frameTimeBufferMS=0,averageFPS=0;const pluginList=[];class EnginePlugin{constructor(update,render,glContextLost,glContextRestored){this.update=update;this.render=render;this.glContextLost=glContextLost;this.glContextRestored=glContextRestored}}function engineAddPlugin(update,render,glContextLost,glContextRestored){ASSERT(!pluginList.find(p=>p.update===update&&p.render===render&&p.glContextLost===glContextLost&&p.glContextRestored===glContextRestored));const plugin=new EnginePlugin(update,render,glContextLost,glContextRestored);pluginList.push(plugin)}async function engineInit(gameInit,gameUpdate,gameUpdatePost,gameRender,gameRenderPost,imageSources=[],rootElement=document.body){ASSERT(!mainContext,"engine already initialized");ASSERT(isArray(imageSources),"pass in images as array");gameInit||=()=>{};gameUpdate||=()=>{};gameUpdatePost||=()=>{};gameRender||=()=>{};gameRenderPost||=()=>{};function enginePreRender(){mainCanvasSize=vec2(mainCanvas.width,mainCanvas.height);mainContext.imageSmoothingEnabled=!tilesPixelated;glPreRender()}function engineUpdate(frameTimeMS=0){let frameTimeDeltaMS=frameTimeMS-frameTimeLastMS;frameTimeLastMS=frameTimeMS;if(debug||debugWatermark)averageFPS=lerp(averageFPS,1e3/(frameTimeDeltaMS||1),.05);const debugSpeedUp=debug&&keyIsDown("Equal");const debugSpeedDown=debug&&keyIsDown("Minus");if(debug)frameTimeDeltaMS*=debugSpeedUp?10:debugSpeedDown?.1:1;timeReal+=frameTimeDeltaMS/1e3;frameTimeBufferMS+=paused?0:frameTimeDeltaMS;if(!debugSpeedUp)frameTimeBufferMS=min(frameTimeBufferMS,50);let wasUpdated=false;if(paused){wasUpdated=true;updateCanvas();inputUpdate();pluginList.forEach(plugin=>plugin.update?.());for(const o of engineObjects)o.parent||o.updateTransforms();debugUpdate();gameUpdatePost();inputUpdatePost();if(debugVideoCaptureIsActive())renderFrame()}else{let deltaSmooth=0;if(frameTimeBufferMS<0&&frameTimeBufferMS>-9){deltaSmooth=frameTimeBufferMS;frameTimeBufferMS=0}for(;frameTimeBufferMS>=0;frameTimeBufferMS-=1e3/frameRate){time=frame++/frameRate;wasUpdated=true;updateCanvas();inputUpdate();gameUpdate();pluginList.forEach(plugin=>plugin.update?.());engineObjectsUpdate();debugUpdate();gameUpdatePost();inputUpdatePost();if(debugVideoCaptureIsActive())renderFrame()}frameTimeBufferMS+=deltaSmooth}if(!debugVideoCaptureIsActive())renderFrame();requestAnimationFrame(engineUpdate);function renderFrame(){if(headlessMode)return;if(!wasUpdated)updateCanvas();enginePreRender();gameRender();engineObjects.sort((a,b)=>a.renderOrder-b.renderOrder);for(const o of engineObjects)o.destroyed||o.render();gameRenderPost();pluginList.forEach(plugin=>plugin.render?.());inputRender();debugRender();glFlush();debugVideoCaptureUpdate();if(debugWatermark&&!debugVideoCaptureIsActive()){mainContext.textAlign="right";mainContext.textBaseline="top";mainContext.font="1em monospace";mainContext.fillStyle="#000";const text=engineName+" "+"v"+engineVersion+" / "+drawCount+" / "+engineObjects.length+" / "+averageFPS.toFixed(1)+(glEnable?" GL":" 2D");mainContext.fillText(text,mainCanvas.width-3,3);mainContext.fillStyle="#fff";mainContext.fillText(text,mainCanvas.width-2,2)}drawCount=0}}function updateCanvas(){if(headlessMode)return;if(canvasFixedSize.x){mainCanvasSize=canvasFixedSize.copy();const innerAspect=innerWidth/innerHeight;const fixedAspect=canvasFixedSize.x/canvasFixedSize.y;const w=innerAspect<fixedAspect?"100%":"";const h=innerAspect<fixedAspect?"":"100%";mainCanvas.style.width=w;mainCanvas.style.height=h;if(glCanvas){glCanvas.style.width=w;glCanvas.style.height=h}}else{mainCanvasSize.x=min(innerWidth,canvasMaxSize.x);mainCanvasSize.y=min(innerHeight,canvasMaxSize.y);const innerAspect=innerWidth/innerHeight;ASSERT(canvasMinAspect<=canvasMaxAspect);if(canvasMaxAspect&&innerAspect>canvasMaxAspect){const w=mainCanvasSize.y*canvasMaxAspect|0;mainCanvasSize.x=min(w,canvasMaxSize.x)}else if(innerAspect<canvasMinAspect){const h=mainCanvasSize.x/canvasMinAspect|0;mainCanvasSize.y=min(h,canvasMaxSize.y)}}mainCanvas.width=mainCanvasSize.x;mainCanvas.height=mainCanvasSize.y;if(canvasClearColor.a>0&&!glEnable){mainContext.fillStyle=canvasClearColor.toString();mainContext.fillRect(0,0,mainCanvasSize.x,mainCanvasSize.y);mainContext.fillStyle=BLACK.toString()}mainContext.lineJoin="round";mainContext.lineCap="round"}async function startEngine(){await gameInit();engineUpdate()}if(headlessMode)return startEngine();glInit(rootElement);const styleRoot="margin:0;"+"overflow:hidden;"+"background:#000;"+"user-select:none;"+"-webkit-user-select:none;"+"touch-action:none;"+"-webkit-touch-callout:none";rootElement.style.cssText=styleRoot;drawCanvas=mainCanvas=rootElement.appendChild(document.createElement("canvas"));drawContext=mainContext=mainCanvas.getContext("2d");inputInit();audioInit();debugInit();const styleCanvas="position:absolute;"+"top:50%;left:50%;transform:translate(-50%,-50%)";mainCanvas.style.cssText=styleCanvas;if(glCanvas)glCanvas.style.cssText=styleCanvas;setCanvasPixelated(canvasPixelated);updateCanvas();glPreRender();workCanvas=new OffscreenCanvas(64,64);workContext=workCanvas.getContext("2d");workReadCanvas=new OffscreenCanvas(64,64);workReadContext=workReadCanvas.getContext("2d",{willReadFrequently:true});const promises=imageSources.map((src,textureIndex)=>new Promise(resolve=>{ASSERT(isString(src),"imageSources must be an array of strings");const image=new Image;image.onerror=image.onload=()=>{const textureInfo=new TextureInfo(image);textureInfos[textureIndex]=textureInfo;resolve()};image.crossOrigin="anonymous";image.src=src}));if(!imageSources.length){promises.push(new Promise(resolve=>{const textureInfo=new TextureInfo(new Image);textureInfos[0]=textureInfo;resolve()}))}if(showSplashScreen){promises.push(new Promise(resolve=>{let t=0;console.log(`${engineName} Engine v${engineVersion}`);updateSplash();function updateSplash(){inputClear();drawEngineSplashScreen(t+=.01);t>1?resolve():setTimeout(updateSplash,16)}}))}await Promise.all(promises);return startEngine();function drawEngineSplashScreen(t){const x=mainContext;const w=mainCanvas.width=innerWidth;const h=mainCanvas.height=innerHeight;{const p3=percent(t,1,.8);const p4=percent(t,0,.5);const g=x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.7);g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());g.addColorStop(1,hsl(0,0,0,p3).toString());x.save();x.fillStyle=g;x.fillRect(0,0,w,h)}const rect=(X,Y,W,H,C)=>{x.beginPath();x.rect(X,Y,W,C?H*p:H);x.fillStyle=C;C?x.fill():x.stroke()};const line=(X,Y,Z,W)=>{x.beginPath();x.lineTo(X,Y);x.lineTo(Z,W);x.stroke()};const circle=(X,Y,R,A=0,B=2*PI,C,F)=>{const D=(A+B)/2,E=p*(B-A)/2;x.beginPath();F&&x.lineTo(X,Y);x.arc(X,Y,R,D-E,D+E);x.fillStyle=C;C?x.fill():x.stroke()};const color=(c=0,l=0)=>hsl([.98,.3,.57,.14][c%4],.8,[0,.3,.5,.8,.9][l]).toString();const alpha=wave(1,1,t);const p=percent(alpha,.1,.5);x.translate(w/2,h/2);const size=min(6,min(w,h)/99);x.scale(size,size);x.translate(-40,-35);x.lineJoin=x.lineCap="round";x.lineWidth=.1+p*1.9;const p2=percent(alpha,.1,1);x.setLineDash([99*p2,99]);rect(7,16,18,-8,color(2,2));rect(7,8,18,4,color(2,3));rect(25,8,8,8,color(2,1));rect(25,8,-18,8);rect(25,8,8,8);rect(25,16,7,23,color());rect(11,39,14,-23,color(1,1));rect(11,16,14,18,color(1,2));rect(11,16,14,8,color(1,3));rect(25,16,-14,24);rect(15,29,6,-9,color(2,2));circle(15,21,5,0,PI/2,color(2,4),1);rect(21,21,-6,9);rect(37,14,9,6,color(3,2));rect(37,14,4.5,6,color(3,3));rect(37,14,9,6);rect(50,20,10,-8,color(0,1));rect(50,20,6.5,-8,color(0,2));rect(50,20,3.5,-8,color(0,3));rect(50,20,10,-8);circle(55,2,11.4,.5,PI-.5,color(3,3));circle(55,2,11.4,.5,PI/2,color(3,2),1);circle(55,2,11.4,.5,PI-.5);rect(45,7,20,-7,color(0,2));rect(45,-1,20,4,color(0,3));rect(45,-1,20,8);for(let i=5;i--;){circle(60-i*6,30,9.9,0,2*PI,color(i+2,3));circle(60-i*6,30,10,-.5,PI+.5,color(i+2,2));circle(60-i*6,30,10.1,.5,PI-.5,color(i+2,1))}circle(36,30,10,PI/2,PI*3/2);circle(48,30,10,PI/2,PI*3/2);circle(60,30,10);line(36,20,60,20);circle(60,30,4,PI,3*PI,color(3,2));circle(60,30,4,PI,2*PI,color(3,3));circle(60,30,4,PI,3*PI);for(let i=6;i--;){x.beginPath();x.lineTo(53,54);x.lineTo(53,40);x.lineTo(53+(1+i*2.9)*p,40);x.lineTo(53+(4+i*3.5)*p,54);x.fillStyle=color(0,i%2+2);x.fill();i%2&&x.stroke()}rect(6,40,5,5);rect(6,40,5,5,color());rect(15,54,38,-14,color());for(let i=3;i--;)for(let j=2;j--;){circle(15*i+15,47,j?7:1,PI,3*PI,color(i,3));x.stroke();circle(15*i+15,47,j?7:1,0,PI,color(i,2));x.stroke()}line(6,40,68,40);line(77,54,4,54);const s=engineName;x.font="900 16px arial";x.textAlign="center";x.textBaseline="top";x.lineWidth=.1+p*3.9;let w2=0;for(let i=0;i<s.length;++i)w2+=x.measureText(s[i]).width;for(let j=2;j--;)for(let i=0,X=41-w2/2;i<s.length;++i){x.fillStyle=color(i,2);const w=x.measureText(s[i]).width;x[j?"strokeText":"fillText"](s[i],X+w/2,55.5,17*p);X+=w}x.restore()}}function engineObjectsUpdate(){engineObjectsCollide=engineObjects.filter(o=>o.collideSolidObjects);function updateObject(o){if(o.destroyed)return;o.update();for(const child of o.children)updateObject(child)}for(const o of engineObjects){if(o.parent)continue;o.update();o.updatePhysics();for(const child of o.children)updateObject(child);o.updateTransforms()}engineObjects=engineObjects.filter(o=>!o.destroyed)}function engineObjectsDestroy(){for(const o of engineObjects)o.parent||o.destroy();engineObjects=engineObjects.filter(o=>!o.destroyed)}function engineObjectsCollect(pos,size,objects=engineObjects){const collectedObjects=[];if(!pos){for(const o of objects)collectedObjects.push(o)}else if(size instanceof Vector2){for(const o of objects)o.isOverlapping(pos,size)&&collectedObjects.push(o)}else{const sizeSquared=size*size;for(const o of objects)pos.distanceSquared(o.pos)<sizeSquared&&collectedObjects.push(o)}return collectedObjects}function engineObjectsCallback(pos,size,callbackFunction,objects=engineObjects){engineObjectsCollect(pos,size,objects).forEach(o=>callbackFunction(o))}function engineObjectsRaycast(start,end,objects=engineObjects){const hitObjects=[];for(const o of objects){if(o.collideRaycast&&isIntersecting(start,end,o.pos,o.size)){debugRaycast&&debugRect(o.pos,o.size,"#f00");hitObjects.push(o)}}debugRaycast&&debugLine(start,end,hitObjects.length?"#f00":"#00f",.02);return hitObjects}let debugWatermark=0;let debugKey="";const debug=0;const debugOverlay=0;const debugPhysics=0;const debugParticles=0;const debugRaycast=0;const debugGamepads=0;const debugMedals=0;function ASSERT(){}function LOG(){}function debugInit(){}function debugUpdate(){}function debugRender(){}function debugRect(){}function debugPoly(){}function debugCircle(){}function debugPoint(){}function debugLine(){}function debugOverlap(){}function debugText(){}function debugClear(){}function debugScreenshot(){}function debugSaveCanvas(){}function debugSaveText(){}function debugSaveDataURL(){}function debugShowErrors(){}function debugVideoCaptureIsActive(){return false}function debugVideoCaptureStart(){}function debugVideoCaptureStop(){}function debugVideoCaptureUpdate(){}function debugProtectConstant(o){return o}const PI=Math.PI;const abs=Math.abs;const floor=Math.floor;const ceil=Math.ceil;const round=Math.round;const min=Math.min;const max=Math.max;const sign=Math.sign;const hypot=Math.hypot;const log2=Math.log2;const sin=Math.sin;const cos=Math.cos;const tan=Math.tan;const atan2=Math.atan2;function mod(dividend,divisor=1){return(dividend%divisor+divisor)%divisor}function clamp(value,min=0,max=1){return value<min?min:value>max?max:value}function percent(value,valueA,valueB){return(valueB-=valueA)?clamp((value-valueA)/valueB):0}function lerp(valueA,valueB,percent){return valueA+clamp(percent)*(valueB-valueA)}function percentLerp(value,percentA,percentB,lerpA,lerpB){return lerp(lerpA,lerpB,percent(value,percentA,percentB))}function distanceWrap(valueA,valueB,wrapSize=1){const d=(valueA-valueB)%wrapSize;return d*2%wrapSize-d}function lerpWrap(valueA,valueB,percent,wrapSize=1){return valueA+clamp(percent)*distanceWrap(valueB,valueA,wrapSize)}function distanceAngle(angleA,angleB){return distanceWrap(angleA,angleB,2*PI)}function lerpAngle(angleA,angleB,percent){return lerpWrap(angleA,angleB,percent,2*PI)}function smoothStep(percent){return percent*percent*(3-2*percent)}function isPowerOfTwo(value){return!(value&value-1)}function nearestPowerOfTwo(value){return 2**ceil(log2(value))}function isOverlapping(posA,sizeA,posB,sizeB=vec2()){const dx=(posA.x-posB.x)*2;const dy=(posA.y-posB.y)*2;const sx=sizeA.x+sizeB.x;const sy=sizeA.y+sizeB.y;return dx>=-sx&&dx<sx&&dy>=-sy&&dy<sy}function isIntersecting(start,end,pos,size){const boxMin=pos.subtract(size.scale(.5));const boxMax=boxMin.add(size);const delta=end.subtract(start);const a=start.subtract(boxMin);const b=start.subtract(boxMax);const p=[-delta.x,delta.x,-delta.y,delta.y];const q=[a.x,-b.x,a.y,-b.y];let tMin=0,tMax=1;for(let i=4;i--;){if(p[i]){const t=q[i]/p[i];if(p[i]<0){if(t>tMax)return false;tMin=max(t,tMin)}else{if(t<tMin)return false;tMax=min(t,tMax)}}else if(q[i]<0)return false}return true}function wave(frequency=1,amplitude=1,t=time,offset=0){return amplitude/2*(1-cos(offset+t*frequency*2*PI))}function formatTime(t){const sign=t<0?"-":"";t=abs(t)|0;return sign+(t/60|0)+":"+(t%60<10?"0":"")+t%60}async function fetchJSON(url){const response=await fetch(url);if(!response.ok)throw new Error(`Failed to fetch JSON from ${url}: ${response.status} ${response.statusText}`);return response.json()}function isNumber(n){return typeof n==="number"&&!isNaN(n)}function isString(s){return s!==undefined&&s!==null&&typeof s.toString()==="string"}function isArray(a){return Array.isArray(a)}function lineTest(posStart,posEnd,testFunction,normal){ASSERT(isVector2(posStart),"posStart must be a vec2");ASSERT(isVector2(posEnd),"posEnd must be a vec2");ASSERT(typeof testFunction==="function","testFunction must be a function");ASSERT(!normal||isVector2(normal),"normal must be a vec2");const dx=posEnd.x-posStart.x;const dy=posEnd.y-posStart.y;const totalLength=hypot(dx,dy);if(!totalLength)return;const pos=posStart.floor();const dirX=dx/totalLength;const dirY=dy/totalLength;const stepX=sign(dirX);const stepY=sign(dirY);const tDeltaX=dirX?abs(1/dirX):Infinity;const tDeltaY=dirY?abs(1/dirY):Infinity;const nextGridX=stepX>0?pos.x+1:pos.x;const nextGridY=stepY>0?pos.y+1:pos.y;const tMaxX=dirX?(nextGridX-posStart.x)/dirX:Infinity;const tMaxY=dirY?(nextGridY-posStart.y)/dirY:Infinity;let t=0,tX=tMaxX,tY=tMaxY,wasX=tDeltaX<tDeltaY;while(t<totalLength){if(testFunction(pos)){const hitPos=vec2(posStart.x+dirX*t,posStart.y+dirY*t);const e=1e-9;if(wasX){if(stepX<0)hitPos.x-=e}if(stepY<0)hitPos.y-=e;if(normal)wasX?normal.set(-stepX,0):normal.set(0,-stepY);return hitPos}if(wasX=tX<tY){pos.x+=stepX;t=tX;tX+=tDeltaX}else{pos.y+=stepY;t=tY;tY+=tDeltaY}}}function rand(valueA=1,valueB=0){return valueB+Math.random()*(valueA-valueB)}function randInt(valueA,valueB=0){return floor(rand(valueA,valueB))}function randBool(chance=.5){return rand()<chance}function randSign(){return randInt(2)*2-1}function randVec2(length=1){return(new Vector2).setAngle(rand(2*PI),length)}function randInCircle(radius=1,minRadius=0){return radius>0?randVec2(radius*rand(minRadius/radius,1)**.5):new Vector2}function randColor(colorA=new Color,colorB=new Color(0,0,0,1),linear=false){return linear?colorA.lerp(colorB,rand()):new Color(rand(colorA.r,colorB.r),rand(colorA.g,colorB.g),rand(colorA.b,colorB.b),rand(colorA.a,colorB.a))}class RandomGenerator{constructor(seed=123456789){this.seed=seed}float(valueA=1,valueB=0){this.seed^=this.seed<<13;this.seed^=this.seed>>>17;this.seed^=this.seed<<5;return valueB+(valueA-valueB)*((this.seed>>>0)/2**32)}int(valueA,valueB=0){return floor(this.float(valueA,valueB))}bool(chance=.5){return this.float()<chance}sign(){return this.float()>.5?1:-1}floatSign(valueA=1,valueB=0){return this.float(valueA,valueB)*this.sign()}angle(){return this.float(-PI,PI)}vec2(valueA=1,valueB=0){return vec2(this.float(valueA,valueB),this.float(valueA,valueB))}randColor(colorA=new Color,colorB=new Color(0,0,0,1),linear=false){return linear?colorA.lerp(colorB,this.float()):new Color(this.float(colorA.r,colorB.r),this.float(colorA.g,colorB.g),this.float(colorA.b,colorB.b),this.float(colorA.a,colorB.a))}mutateColor(color,amount=.05,alphaAmount=0){ASSERT_NUMBER_VALID(amount);ASSERT_NUMBER_VALID(alphaAmount);return new Color(color.r+this.float(amount,-amount),color.g+this.float(amount,-amount),color.b+this.float(amount,-amount),color.a+this.float(alphaAmount,-alphaAmount)).clamp()}}function vec2(x=0,y){return new Vector2(x,y===undefined?x:y)}function isVector2(v){return v instanceof Vector2&&v.isValid()}function ASSERT_VECTOR2_VALID(v){ASSERT(isVector2(v),"Vector2 is invalid.",v)}function ASSERT_NUMBER_VALID(n){ASSERT(isNumber(n),"Number is invalid.",n)}function ASSERT_VECTOR2_NORMAL(v){ASSERT_VECTOR2_VALID(v);ASSERT(abs(v.lengthSquared()-1)<.01,"Vector2 is not normal.",v)}class Vector2{constructor(x=0,y=0){this.x=x;this.y=y;ASSERT(this.isValid(),"Constructed Vector2 is invalid.",this)}set(x=0,y=0){this.x=x;this.y=y;ASSERT_VECTOR2_VALID(this);return this}setFrom(v){return this.set(v.x,v.y)}copy(){return new Vector2(this.x,this.y)}add(v){return new Vector2(this.x+v.x,this.y+v.y)}subtract(v){return new Vector2(this.x-v.x,this.y-v.y)}multiply(v){return new Vector2(this.x*v.x,this.y*v.y)}divide(v){return new Vector2(this.x/v.x,this.y/v.y)}scale(s){return new Vector2(this.x*s,this.y*s)}length(){return this.lengthSquared()**.5}lengthSquared(){return this.x**2+this.y**2}distance(v){return this.distanceSquared(v)**.5}distanceSquared(v){return(this.x-v.x)**2+(this.y-v.y)**2}normalize(length=1){const l=this.length();return l?this.scale(length/l):new Vector2(0,length)}clampLength(length=1){const l=this.length();return l>length?this.scale(length/l):this.copy()}dot(v){return this.x*v.x+this.y*v.y}cross(v){return this.x*v.y-this.y*v.x}reflect(normal,restitution=1){return this.subtract(normal.scale((1+restitution)*this.dot(normal)))}angle(){return atan2(this.x,this.y)}setAngle(angle=0,length=1){ASSERT_NUMBER_VALID(angle);ASSERT_NUMBER_VALID(length);this.x=length*sin(angle);this.y=length*cos(angle);return this}rotate(angle){ASSERT_NUMBER_VALID(angle);const c=cos(-angle),s=sin(-angle);return new Vector2(this.x*c-this.y*s,this.x*s+this.y*c)}setDirection(direction,length=1){ASSERT_NUMBER_VALID(direction);ASSERT_NUMBER_VALID(length);direction=mod(direction,4);ASSERT(direction===0||direction===1||direction===2||direction===3,"Vector2.setDirection() direction must be an integer between 0 and 3.");this.x=direction%2?direction-1?-length:length:0;this.y=direction%2?0:direction?-length:length;return this}direction(){return abs(this.x)>abs(this.y)?this.x<0?3:1:this.y<0?2:0}abs(){return new Vector2(abs(this.x),abs(this.y))}floor(){return new Vector2(floor(this.x),floor(this.y))}mod(divisor=1){return new Vector2(mod(this.x,divisor),mod(this.y,divisor))}area(){return abs(this.x*this.y)}isZero(){return!this.x&&!this.y}lerp(v,percent){ASSERT_VECTOR2_VALID(v);ASSERT_NUMBER_VALID(percent);const p=clamp(percent);return new Vector2(v.x*p+this.x*(1-p),v.y*p+this.y*(1-p))}arrayCheck(arraySize){return this.x>=0&&this.y>=0&&this.x<arraySize.x&&this.y<arraySize.y}toString(digits=3){ASSERT_NUMBER_VALID(digits);if(this.isValid())return`(${(this.x<0?"":" ")+this.x.toFixed(digits)},${(this.y<0?"":" ")+this.y.toFixed(digits)} )`;else return`(${this.x}, ${this.y})`}isValid(){return isNumber(this.x)&&isNumber(this.y)}}function rgb(r,g,b,a){return new Color(r,g,b,a)}function hsl(h,s,l,a){return(new Color).setHSLA(h,s,l,a)}function isColor(c){return c instanceof Color&&c.isValid()}function ASSERT_COLOR_VALID(c){ASSERT(isColor(c),"Color is invalid.",c)}class Color{constructor(r=1,g=1,b=1,a=1){this.r=r;this.g=g;this.b=b;this.a=a;ASSERT(this.isValid(),"Constructed Color is invalid.",this)}set(r=1,g=1,b=1,a=1){this.r=r;this.g=g;this.b=b;this.a=a;ASSERT_COLOR_VALID(this);return this}setFrom(c){return this.set(c.r,c.g,c.b,c.a)}copy(){return new Color(this.r,this.g,this.b,this.a)}add(c){return new Color(this.r+c.r,this.g+c.g,this.b+c.b,this.a+c.a)}subtract(c){return new Color(this.r-c.r,this.g-c.g,this.b-c.b,this.a-c.a)}multiply(c){return new Color(this.r*c.r,this.g*c.g,this.b*c.b,this.a*c.a)}divide(c){return new Color(this.r/c.r,this.g/c.g,this.b/c.b,this.a/c.a)}scale(scale,alphaScale=scale){return new Color(this.r*scale,this.g*scale,this.b*scale,this.a*alphaScale)}clamp(){return new Color(clamp(this.r),clamp(this.g),clamp(this.b),clamp(this.a))}lerp(c,percent){ASSERT_COLOR_VALID(c);ASSERT_NUMBER_VALID(percent);const p=clamp(percent);return new Color(c.r*p+this.r*(1-p),c.g*p+this.g*(1-p),c.b*p+this.b*(1-p),c.a*p+this.a*(1-p))}setHSLA(h=0,s=0,l=1,a=1){h=mod(h,1);s=clamp(s);l=clamp(l);const q=l<.5?l*(1+s):l+s-l*s,p=2*l-q,f=(p,q,t)=>(t=mod(t,1))*6<1?p+(q-p)*6*t:t*2<1?q:t*3<2?p+(q-p)*(4-t*6):p;this.r=f(p,q,h+1/3);this.g=f(p,q,h);this.b=f(p,q,h-1/3);this.a=a;ASSERT_COLOR_VALID(this);return this}HSLA(){const r=clamp(this.r);const g=clamp(this.g);const b=clamp(this.b);const a=clamp(this.a);const maxC=max(r,g,b);const minC=min(r,g,b);const l=(maxC+minC)/2;let h=0,s=0;if(maxC!==minC){let d=maxC-minC;s=l>.5?d/(2-maxC-minC):d/(maxC+minC);if(r===maxC)h=(g-b)/d+(g<b?6:0);else if(g===maxC)h=(b-r)/d+2;else if(b===maxC)h=(r-g)/d+4}return[h/6,s,l,a]}mutate(amount=.05,alphaAmount=0){ASSERT_NUMBER_VALID(amount);ASSERT_NUMBER_VALID(alphaAmount);return new Color(this.r+rand(amount,-amount),this.g+rand(amount,-amount),this.b+rand(amount,-amount),this.a+rand(alphaAmount,-alphaAmount)).clamp()}toString(useAlpha=true){if(debug&&!this.isValid())return`#000`;const toHex=c=>((c=clamp(c)*255|0)<16?"0":"")+c.toString(16);return"#"+toHex(this.r)+toHex(this.g)+toHex(this.b)+(useAlpha?toHex(this.a):"")}setHex(hex){ASSERT(isString(hex),"Color hex code must be a string");ASSERT(hex[0]==="#","Color hex code must start with #");ASSERT([4,5,7,9].includes(hex.length),"Invalid hex");if(hex.length<6){const fromHex=c=>clamp(parseInt(hex[c],16)/15);this.r=fromHex(1);this.g=fromHex(2);this.b=fromHex(3);this.a=hex.length===5?fromHex(4):1}else{const fromHex=c=>clamp(parseInt(hex.slice(c,c+2),16)/255);this.r=fromHex(1);this.g=fromHex(3);this.b=fromHex(5);this.a=hex.length===9?fromHex(7):1}ASSERT_COLOR_VALID(this);return this}rgbaInt(){const r=clamp(this.r)*255|0;const g=clamp(this.g)*255<<8;const b=clamp(this.b)*255<<16;const a=clamp(this.a)*255<<24;return r+g+b+a}isValid(){return isNumber(this.r)&&isNumber(this.g)&&isNumber(this.b)&&isNumber(this.a)}}const WHITE=debugProtectConstant(rgb());const CLEAR_WHITE=debugProtectConstant(rgb(1,1,1,0));const BLACK=debugProtectConstant(rgb(0,0,0));const CLEAR_BLACK=debugProtectConstant(rgb(0,0,0,0));const GRAY=debugProtectConstant(rgb(.5,.5,.5));const RED=debugProtectConstant(rgb(1,0,0));const ORANGE=debugProtectConstant(rgb(1,.5,0));const YELLOW=debugProtectConstant(rgb(1,1,0));const GREEN=debugProtectConstant(rgb(0,1,0));const CYAN=debugProtectConstant(rgb(0,1,1));const BLUE=debugProtectConstant(rgb(0,0,1));const PURPLE=debugProtectConstant(rgb(.5,0,1));const MAGENTA=debugProtectConstant(rgb(1,0,1));class Timer{constructor(timeLeft,useRealTime=false){ASSERT(timeLeft===undefined||isNumber(timeLeft),"Constructed Timer is invalid.",timeLeft);this.useRealTime=useRealTime;const globalTime=this.getGlobalTime();this.time=timeLeft===undefined?undefined:globalTime+timeLeft;this.setTime=timeLeft}set(timeLeft=0){ASSERT(isNumber(timeLeft),"Timer is invalid.",timeLeft);const globalTime=this.getGlobalTime();this.time=globalTime+timeLeft;this.setTime=timeLeft}setUseRealTime(useRealTime=true){ASSERT(!this.isSet(),"Cannot change global time setting while timer is set.");this.useRealTime=useRealTime}unset(){this.time=undefined}isSet(){return this.time!==undefined}active(){return this.getGlobalTime()<this.time}elapsed(){return this.getGlobalTime()>=this.time}get(){return this.isSet()?this.getGlobalTime()-this.time:0}getPercent(){return this.isSet()?1-percent(this.time-this.getGlobalTime(),0,this.setTime):0}getSetTime(){return this.isSet()?this.setTime:0}getGlobalTime(){return this.useRealTime?timeReal:time}toString(){return this.isSet()?abs(this.get())+" seconds "+(this.get()<0?"before":"after"):"unset"}valueOf(){return this.get()}}let cameraPos=vec2();let cameraAngle=0;let cameraScale=32;let canvasColorTiles=true;let canvasClearColor=CLEAR_BLACK;let canvasMaxSize=vec2(1920,1080);let canvasMinAspect=0;let canvasMaxAspect=0;let canvasFixedSize=vec2();let canvasPixelated=false;let tilesPixelated=true;let fontDefault="arial";let showSplashScreen=false;let headlessMode=false;let glEnable=true;let glCircleSides=32;let tileDefaultSize=vec2(16);let tileDefaultPadding=0;let tileDefaultBleed=0;let enablePhysicsSolver=true;let objectDefaultMass=1;let objectDefaultDamping=1;let objectDefaultAngleDamping=1;let objectDefaultRestitution=0;let objectDefaultFriction=.8;let objectMaxSpeed=1;let gravity=vec2();let particleEmitRateScale=1;let gamepadsEnable=true;let gamepadDirectionEmulateStick=true;let inputWASDEmulateDirection=true;let touchInputEnable=true;let touchGamepadEnable=false;let touchGamepadCenterButton=true;let touchGamepadButtonCount=4;let touchGamepadAnalog=true;let touchGamepadSize=99;let touchGamepadAlpha=.3;let vibrateEnable=true;let soundEnable=true;let soundVolume=.3;let soundDefaultRange=40;let soundDefaultTaper=.7;let medalDisplayTime=5;let medalDisplaySlideTime=.5;let medalDisplaySize=vec2(640,80);let medalsPreventUnlock=false;function setCameraPos(pos){cameraPos=pos.copy()}function setCameraAngle(angle){cameraAngle=angle}function setCameraScale(scale){cameraScale=scale}function setCanvasColorTiles(colorTiles){canvasColorTiles=colorTiles}function setCanvasClearColor(color){canvasClearColor=color.copy()}function setCanvasMaxSize(size){canvasMaxSize=size.copy()}function setCanvasMinAspect(aspect){canvasMinAspect=aspect}function setCanvasMaxAspect(aspect){canvasMaxAspect=aspect}function setCanvasFixedSize(size){canvasFixedSize=size.copy()}function setCanvasPixelated(pixelated){canvasPixelated=pixelated;if(mainCanvas)mainCanvas.style.imageRendering=pixelated?"pixelated":"";if(glCanvas)glCanvas.style.imageRendering=pixelated?"pixelated":""}function setTilesPixelated(pixelated){tilesPixelated=pixelated}function setFontDefault(font){fontDefault=font}function setShowSplashScreen(show){showSplashScreen=show}function setHeadlessMode(headless){headlessMode=headless}function setGLEnable(enable){if(enable&&!glCanBeEnabled){console.warn("Can not enable WebGL if it was disabled on start.");return}glEnable=enable;if(glCanvas)glCanvas.style.display=enable?"":"none"}function setGLCircleSides(sides){glCircleSides=sides}function setTileDefaultSize(size){tileDefaultSize=size.copy()}function setTileDefaultPadding(padding){tileDefaultPadding=padding}function setTileDefaultBleed(bleed){tileDefaultBleed=bleed}function setEnablePhysicsSolver(enable){enablePhysicsSolver=enable}function setObjectDefaultMass(mass){objectDefaultMass=mass}function setObjectDefaultDamping(damp){objectDefaultDamping=damp}function setObjectDefaultAngleDamping(damp){objectDefaultAngleDamping=damp}function setObjectDefaultRestitution(restitution){objectDefaultRestitution=restitution}function setObjectDefaultFriction(friction){objectDefaultFriction=friction}function setObjectMaxSpeed(speed){objectMaxSpeed=speed}function setGravity(newGravity){gravity=newGravity.copy()}function setParticleEmitRateScale(scale){particleEmitRateScale=scale}function setGamepadsEnable(enable){gamepadsEnable=enable}function setGamepadDirectionEmulateStick(enable){gamepadDirectionEmulateStick=enable}function setInputWASDEmulateDirection(enable){inputWASDEmulateDirection=enable}function setTouchInputEnable(enable){touchInputEnable=enable}function setTouchGamepadEnable(enable){touchGamepadEnable=enable}function setTouchGamepadCenterButton(enable){touchGamepadCenterButton=enable}function setTouchGamepadButtonCount(count){touchGamepadButtonCount=count}function setTouchGamepadAnalog(analog){touchGamepadAnalog=analog}function setTouchGamepadSize(size){touchGamepadSize=size}function setTouchGamepadAlpha(alpha){touchGamepadAlpha=alpha}function setVibrateEnable(enable){vibrateEnable=enable}function setSoundEnable(enable){soundEnable=enable}function setSoundVolume(volume){soundVolume=volume;if(soundEnable&&!headlessMode&&audioMasterGain)audioMasterGain.gain.value=volume}function setSoundDefaultRange(range){soundDefaultRange=range}function setSoundDefaultTaper(taper){soundDefaultTaper=taper}function setMedalDisplayTime(time){medalDisplayTime=time}function setMedalDisplaySlideTime(time){medalDisplaySlideTime=time}function setMedalDisplaySize(size){medalDisplaySize=size.copy()}function setMedalsPreventUnlock(preventUnlock){medalsPreventUnlock=preventUnlock}function setDebugWatermark(show){debugWatermark=show}function setDebugKey(key){debugKey=key}class EngineObject{constructor(pos=vec2(),size=vec2(1),tileInfo,angle=0,color=WHITE,renderOrder=0){ASSERT(isVector2(pos),"object pos must be a vec2");ASSERT(isVector2(size),"object size must be a vec2");ASSERT(!tileInfo||tileInfo instanceof TileInfo,"object tileInfo should be a TileInfo or undefined");ASSERT(typeof angle==="number"&&isFinite(angle),"object angle should be a number");ASSERT(isColor(color),"object color should be a valid rgba color");ASSERT(typeof renderOrder==="number","object renderOrder should be a number");this.pos=pos.copy();this.size=size.copy();this.drawSize=undefined;this.tileInfo=tileInfo;this.angle=angle;this.color=color.copy();this.additiveColor=undefined;this.mirror=false;this.mass=objectDefaultMass;this.damping=objectDefaultDamping;this.angleDamping=objectDefaultAngleDamping;this.restitution=objectDefaultRestitution;this.friction=objectDefaultFriction;this.gravityScale=1;this.renderOrder=renderOrder;this.velocity=vec2();this.angleVelocity=0;this.spawnTime=time;this.children=[];this.clampSpeed=true;this.groundObject=undefined;this.parent=undefined;this.localPos=vec2();this.localAngle=0;this.collideTiles=false;this.collideSolidObjects=false;this.isSolid=false;this.collideRaycast=false;engineObjects.push(this)}updateTransforms(){const parent=this.parent;if(parent){const mirror=parent.getMirrorSign();this.pos=this.localPos.multiply(vec2(mirror,1)).rotate(parent.angle).add(parent.pos);this.angle=mirror*this.localAngle+parent.angle}for(const child of this.children)child.updateTransforms()}updatePhysics(){ASSERT(!this.parent);if(this.clampSpeed){this.velocity.x=clamp(this.velocity.x,-objectMaxSpeed,objectMaxSpeed);this.velocity.y=clamp(this.velocity.y,-objectMaxSpeed,objectMaxSpeed)}const oldPos=this.pos.copy();this.velocity.x*=this.damping;this.velocity.y*=this.damping;if(this.mass){this.velocity.x+=gravity.x*this.gravityScale;this.velocity.y+=gravity.y*this.gravityScale}this.pos.x+=this.velocity.x;this.pos.y+=this.velocity.y;this.angle+=this.angleVelocity*=this.angleDamping;ASSERT(this.angleDamping>=0&&this.angleDamping<=1);ASSERT(this.damping>=0&&this.damping<=1);if(!enablePhysicsSolver||!this.mass)return;const wasFalling=this.velocity.y<0&&gravity.y<0||this.velocity.y>0&&gravity.y>0;if(this.groundObject){const friction=max(this.friction,this.groundObject.friction);const groundSpeed=this.groundObject.velocity?this.groundObject.velocity.x:0;this.velocity.x=groundSpeed+(this.velocity.x-groundSpeed)*friction;this.groundObject=undefined}if(this.collideSolidObjects){const epsilon=.001;for(const o of engineObjectsCollide){if(!this.isSolid&&!o.isSolid||o.destroyed||o.parent||o===this)continue;if(!this.isOverlappingObject(o))continue;const collide1=this.collideWithObject(o);const collide2=o.collideWithObject(this);if(!collide1||!collide2)continue;if(isOverlapping(oldPos,this.size,o.pos,o.size)){const deltaPos=oldPos.subtract(o.pos);const length=deltaPos.length();const pushAwayAccel=.001;const velocity=length<.01?randVec2(pushAwayAccel):deltaPos.scale(pushAwayAccel/length);this.velocity=this.velocity.add(velocity);if(o.mass)o.velocity=o.velocity.subtract(velocity);debugPhysics&&debugOverlap(this.pos,this.size,o.pos,o.size,"#f00");continue}const sizeBoth=this.size.add(o.size);const smallStepUp=(oldPos.y-o.pos.y)*2>sizeBoth.y+gravity.y;const isBlockedX=abs(oldPos.y-o.pos.y)*2<sizeBoth.y;const isBlockedY=abs(oldPos.x-o.pos.x)*2<sizeBoth.x;const restitution=max(this.restitution,o.restitution);if(smallStepUp||isBlockedY||!isBlockedX){this.pos.y=o.pos.y+(sizeBoth.y/2+epsilon)*sign(oldPos.y-o.pos.y);if(o.groundObject&&wasFalling||!o.mass){if(wasFalling)this.groundObject=o;this.velocity.y*=-restitution}else if(o.mass){const inelastic=(this.mass*this.velocity.y+o.mass*o.velocity.y)/(this.mass+o.mass);const elastic0=this.velocity.y*(this.mass-o.mass)/(this.mass+o.mass)+o.velocity.y*2*o.mass/(this.mass+o.mass);const elastic1=o.velocity.y*(o.mass-this.mass)/(this.mass+o.mass)+this.velocity.y*2*this.mass/(this.mass+o.mass);this.velocity.y=lerp(inelastic,elastic0,restitution);o.velocity.y=lerp(inelastic,elastic1,restitution)}}if(!smallStepUp&&isBlockedX){this.pos.x=o.pos.x+(sizeBoth.x/2+epsilon)*sign(oldPos.x-o.pos.x);if(o.mass){const inelastic=(this.mass*this.velocity.x+o.mass*o.velocity.x)/(this.mass+o.mass);const elastic0=this.velocity.x*(this.mass-o.mass)/(this.mass+o.mass)+o.velocity.x*2*o.mass/(this.mass+o.mass);const elastic1=o.velocity.x*(o.mass-this.mass)/(this.mass+o.mass)+this.velocity.x*2*this.mass/(this.mass+o.mass);this.velocity.x=lerp(inelastic,elastic0,restitution);o.velocity.x=lerp(inelastic,elastic1,restitution)}else this.velocity.x*=-restitution}debugPhysics&&debugOverlap(this.pos,this.size,o.pos,o.size,"#f0f")}}if(this.collideTiles){const hitLayer=tileCollisionTest(this.pos,this.size,this);if(hitLayer){if(!tileCollisionTest(oldPos,this.size,this)){const blockedLayerY=tileCollisionTest(vec2(oldPos.x,this.pos.y),this.size,this);const blockedLayerX=tileCollisionTest(vec2(this.pos.x,oldPos.y),this.size,this);if(blockedLayerX){const epsilon=.001;const maxMoveUp=.1;const y=floor(oldPos.y-this.size.y/2+1)+this.size.y/2+epsilon;const delta=y-this.pos.y;if(delta<maxMoveUp)if(!tileCollisionTest(vec2(this.pos.x,y),this.size,this)){this.pos.y=y;debugPhysics&&debugRect(this.pos,this.size,"#ff0");return}this.pos.x=oldPos.x;this.velocity.x*=-this.restitution}if(blockedLayerY||!blockedLayerX){const restitution=max(this.restitution,hitLayer.restitution);this.velocity.y*=-restitution;if(wasFalling){const epsilon=1e-4;const offset=this.size.y/2+epsilon;this.pos.y=gravity.y<0?floor(oldPos.y-this.size.y/2)+offset:ceil(oldPos.y+this.size.y/2)-offset;this.groundObject=hitLayer}else{this.pos.y=oldPos.y;this.groundObject=undefined}}debugPhysics&&debugRect(this.pos,this.size,"#f00")}}}}update(){}render(){drawTile(this.pos,this.drawSize||this.size,this.tileInfo,this.color,this.angle,this.mirror,this.additiveColor)}destroy(){if(this.destroyed)return;this.destroyed=1;this.parent&&this.parent.removeChild(this);for(const child of this.children){child.parent=0;child.destroy()}}localToWorld(pos){return this.pos.add(pos.rotate(this.angle))}worldToLocal(pos){return pos.subtract(this.pos).rotate(-this.angle)}localToWorldVector(vec){return vec.rotate(this.angle)}worldToLocalVector(vec){return vec.rotate(-this.angle)}collideWithTile(tileData,pos){return tileData>0}collideWithObject(object){return true}getUp(scale=1){return vec2().setAngle(this.angle,scale)}getRight(scale=1){return vec2().setAngle(this.angle+PI/2,scale)}getAliveTime(){return time-this.spawnTime}getSpeed(){return this.velocity.length()}applyAcceleration(acceleration){if(this.mass)this.velocity=this.velocity.add(acceleration)}applyAngularAcceleration(acceleration){if(this.mass)this.angleVelocity+=acceleration}applyForce(force){if(this.mass)this.applyAcceleration(force.scale(1/this.mass))}getMirrorSign(){return this.mirror?-1:1}addChild(child,localPos=vec2(),localAngle=0){ASSERT(!child.parent&&!this.children.includes(child));ASSERT(child instanceof EngineObject,"child must be an EngineObject");ASSERT(child!==this,"cannot add self as child");this.children.push(child);child.parent=this;child.localPos=localPos.copy();child.localAngle=localAngle;return child}removeChild(child){ASSERT(child.parent===this&&this.children.includes(child));ASSERT(child instanceof EngineObject,"child must be an EngineObject");const index=this.children.indexOf(child);ASSERT(index>=0,"child not found in children array");index>=0&&this.children.splice(index,1);child.parent=0}isOverlappingObject(object){return this.isOverlapping(object.pos,object.size)}isOverlapping(pos,size=vec2()){return isOverlapping(this.pos,this.size,pos,size)}setCollision(collideSolidObjects=true,isSolid=true,collideTiles=true,collideRaycast=true){ASSERT(collideSolidObjects||!isSolid,"solid objects must be set to collide");this.collideSolidObjects=collideSolidObjects;this.isSolid=isSolid;this.collideTiles=collideTiles;this.collideRaycast=collideRaycast}toString(){if(!debug)return;let text="type = "+this.constructor.name;if(this.pos.x||this.pos.y)text+="\npos = "+this.pos;if(this.velocity.x||this.velocity.y)text+="\nvelocity = "+this.velocity;if(this.size.x||this.size.y)text+="\nsize = "+this.size;if(this.angle)text+="\nangle = "+this.angle.toFixed(3);if(this.color)text+="\ncolor = "+this.color;return text}renderDebugInfo(){if(!debug)return;const size=vec2(max(this.size.x,.2),max(this.size.y,.2));const color=rgb(this.collideTiles?1:0,this.collideSolidObjects?1:0,this.isSolid?1:0,.5);drawRect(this.pos,size,color,this.angle);if(this.parent)drawRect(this.pos,size.scale(.8),rgb(1,1,1,.5),this.angle);this.parent&&drawLine(this.pos,this.parent.pos,.1,rgb(1,1,1,.5))}}let mainCanvas;let mainContext;let drawCanvas;let drawContext;let workCanvas;let workContext;let workReadCanvas;let workReadContext;let mainCanvasSize=vec2();let textureInfos=[];let drawCount;function tile(pos=new Vector2,size=tileDefaultSize,textureIndex=0,padding=tileDefaultPadding){if(headlessMode)return new TileInfo;if(typeof size==="number"){ASSERT(size>0);size=new Vector2(size,size)}const tileInfo=new TileInfo(new Vector2,size,textureIndex,padding);const textureInfo=textureInfos[textureIndex];ASSERT(!!textureInfo,"Texture not loaded");const sizePaddedX=size.x+padding*2;const sizePaddedY=size.y+padding*2;if(typeof pos==="number"){const cols=textureInfo.size.x/sizePaddedX|0;ASSERT(cols>0,"Tile size is too big for texture");const posX=pos%cols,posY=pos/cols|0;tileInfo.pos.set(posX*sizePaddedX+padding,posY*sizePaddedY+padding)}else tileInfo.pos.set(pos.x*sizePaddedX+padding,pos.y*sizePaddedY+padding);return tileInfo}class TileInfo{constructor(pos=vec2(),size=tileDefaultSize,textureIndex=0,padding=tileDefaultPadding,bleed=tileDefaultBleed){this.pos=pos.copy();this.size=size.copy();this.textureIndex=textureIndex;this.padding=padding;this.textureInfo=textureInfos[this.textureIndex];this.bleed=bleed}offset(offset){return new TileInfo(this.pos.add(offset),this.size,this.textureIndex,this.padding,this.bleed)}frame(frame){ASSERT(typeof frame==="number");return this.offset(new Vector2(frame*(this.size.x+this.padding*2),0))}setFullImage(textureInfo){this.pos=new Vector2;this.size=textureInfo.size.copy();this.textureInfo=textureInfo;this.bleed=this.padding=0;return this}}class TextureInfo{constructor(image,useWebGL=true){this.image=image;this.size=image?vec2(image.width,image.height):vec2();this.sizeInverse=image?vec2(1/image.width,1/image.height):vec2();this.glTexture=undefined;useWebGL&&this.createWebGLTexture()}createWebGLTexture(){glRegisterTextureInfo(this)}destroyWebGLTexture(){glUnregisterTextureInfo(this)}hasWebGL(){return!!this.glTexture}}function drawTile(pos,size=new Vector2(1),tileInfo,color=WHITE,angle=0,mirror,additiveColor,useWebGL=glEnable,screenSpace,context){ASSERT(isVector2(pos),"pos must be a vec2");ASSERT(isVector2(size),"size must be a vec2");ASSERT(isColor(color),"color is invalid");ASSERT(isNumber(angle),"angle must be a number");ASSERT(!additiveColor||isColor(additiveColor),"additiveColor must be a color");ASSERT(!context||!useWebGL,"context only supported in canvas 2D mode");const textureInfo=tileInfo&&tileInfo.textureInfo;const bleed=tileInfo?.bleed??0;if(useWebGL&&glEnable){ASSERT(!!glContext,"WebGL is not enabled!");if(screenSpace)[pos,size,angle]=screenToWorldTransform(pos,size,angle);if(textureInfo){const sizeInverse=textureInfo.sizeInverse;const x=tileInfo.pos.x*sizeInverse.x;const y=tileInfo.pos.y*sizeInverse.y;const w=tileInfo.size.x*sizeInverse.x;const h=tileInfo.size.y*sizeInverse.y;glSetTexture(textureInfo.glTexture);if(bleed){const bleedX=sizeInverse.x*bleed;const bleedY=sizeInverse.y*bleed;glDraw(pos.x,pos.y,mirror?-size.x:size.x,size.y,angle,x+bleedX,y+bleedY,x-bleedX+w,y-bleedY+h,color.rgbaInt(),additiveColor&&additiveColor.rgbaInt())}else{glDraw(pos.x,pos.y,mirror?-size.x:size.x,size.y,angle,x,y,x+w,y+h,color.rgbaInt(),additiveColor&&additiveColor.rgbaInt())}}else{glDraw(pos.x,pos.y,size.x,size.y,angle,0,0,0,0,0,color.rgbaInt())}}else{++drawCount;size=new Vector2(size.x,-size.y);drawCanvas2D(pos,size,angle,mirror,context=>{if(textureInfo){const x=tileInfo.pos.x,y=tileInfo.pos.y;const w=tileInfo.size.x,h=tileInfo.size.y;drawImageColor(context,textureInfo.image,x,y,w,h,-.5,-.5,1,1,color,additiveColor,bleed)}else{const c=additiveColor?color.add(additiveColor):color;context.fillStyle=c.toString();context.fillRect(-.5,-.5,1,1)}},screenSpace,context)}}function drawRect(pos,size,color,angle,useWebGL,screenSpace,context){drawTile(pos,size,undefined,color,angle,false,undefined,useWebGL,screenSpace,context)}function drawRectGradient(pos,size,colorTop=WHITE,colorBottom=BLACK,angle=0,useWebGL=glEnable,screenSpace=false,context){ASSERT(isVector2(pos),"pos must be a vec2");ASSERT(isVector2(size),"size must be a vec2");ASSERT(isColor(colorTop)&&isColor(colorBottom),"color is invalid");ASSERT(isNumber(angle),"angle must be a number");ASSERT(!context||!useWebGL,"context only supported in canvas 2D mode");if(useWebGL&&glEnable){ASSERT(!!glContext,"WebGL is not enabled!");if(screenSpace){pos=screenToWorld(pos);size=size.scale(1/cameraScale);angle+=cameraAngle}const points=[],colors=[];const halfSizeX=size.x/2,halfSizeY=size.y/2;const colorTopInt=colorTop.rgbaInt();const colorBottomInt=colorBottom.rgbaInt();const c=cos(-angle),s=sin(-angle);for(let i=4;i--;){const x=i&1?halfSizeX:-halfSizeX;const y=i&2?halfSizeY:-halfSizeY;const rx=x*c-y*s;const ry=x*s+y*c;const color=i&2?colorTopInt:colorBottomInt;points.push(vec2(pos.x+rx,pos.y+ry));colors.push(color)}glDrawColoredPoints(points,colors)}else{++drawCount;size=new Vector2(size.x,-size.y);drawCanvas2D(pos,size,angle,false,context=>{const gradient=context.createLinearGradient(0,-.5,0,.5);gradient.addColorStop(0,colorTop.toString());gradient.addColorStop(1,colorBottom.toString());context.fillStyle=gradient;context.fillRect(-.5,-.5,1,1)},screenSpace,context)}}function drawLineList(points,width=.1,color,wrap=false,pos=vec2(),angle=0,useWebGL=glEnable,screenSpace,context){ASSERT(isArray(points),"points must be an array");ASSERT(isNumber(width),"width must be a number");ASSERT(isColor(color),"color is invalid");ASSERT(isVector2(pos),"pos must be a vec2");ASSERT(isNumber(angle),"angle must be a number");ASSERT(!context||!useWebGL,"context only supported in canvas 2D mode");if(useWebGL&&glEnable){ASSERT(!!glContext,"WebGL is not enabled!");let size=vec2(1);if(screenSpace)[pos,size,angle]=screenToWorldTransform(pos,size,angle);glDrawOutlineTransform(points,color.rgbaInt(),width,pos.x,pos.y,size.x,size.y,angle,wrap)}else{++drawCount;drawCanvas2D(pos,vec2(1),angle,false,context=>{context.strokeStyle=color.toString();context.lineWidth=width;context.beginPath();for(let i=0;i<points.length;++i){const point=points[i];if(i)context.lineTo(point.x,point.y);else context.moveTo(point.x,point.y)}if(wrap)context.closePath();context.stroke()},screenSpace,context)}}function drawLine(posA,posB,width=.1,color,pos=vec2(),angle=0,useWebGL,screenSpace,context){const halfDelta=vec2((posB.x-posA.x)/2,(posB.y-posA.y)/2);const size=vec2(width,halfDelta.length()*2);pos=pos.add(posA.add(halfDelta));if(screenSpace)halfDelta.y*=-1;angle+=halfDelta.angle();drawRect(pos,size,color,angle,useWebGL,screenSpace,context)}function drawRegularPoly(pos,size=vec2(1),sides=3,color=WHITE,lineWidth=0,lineColor=BLACK,angle=0,useWebGL=glEnable,screenSpace=false,context){ASSERT(isVector2(size),"size must be a vec2");ASSERT(isNumber(sides),"sides must be a number");const points=[];const sizeX=size.x/2,sizeY=size.y/2;for(let i=sides;i--;){const a=i/sides*PI*2;points.push(vec2(sin(a)*sizeX,cos(a)*sizeY))}drawPoly(points,color,lineWidth,lineColor,pos,angle,useWebGL,screenSpace,context)}function drawPoly(points,color=WHITE,lineWidth=0,lineColor=BLACK,pos=vec2(),angle=0,useWebGL=glEnable,screenSpace=false,context=undefined){ASSERT(isVector2(pos),"pos must be a vec2");ASSERT(isArray(points),"points must be an array");ASSERT(isColor(color)&&isColor(lineColor),"color is invalid");ASSERT(isNumber(lineWidth),"lineWidth must be a number");ASSERT(isNumber(angle),"angle must be a number");ASSERT(!context||!useWebGL,"context only supported in canvas 2D mode");if(useWebGL&&glEnable){ASSERT(!!glContext,"WebGL is not enabled!");let size=vec2(1);if(screenSpace)[pos,size,angle]=screenToWorldTransform(pos,size,angle);glDrawPointsTransform(points,color.rgbaInt(),pos.x,pos.y,size.x,size.y,angle);if(lineWidth>0)glDrawOutlineTransform(points,lineColor.rgbaInt(),lineWidth,pos.x,pos.y,size.x,size.y,angle)}else{drawCanvas2D(pos,vec2(1),angle,false,context=>{context.fillStyle=color.toString();context.beginPath();for(const point of points)context.lineTo(point.x,point.y);context.closePath();context.fill();if(lineWidth){context.strokeStyle=lineColor.toString();context.lineWidth=lineWidth;context.stroke()}},screenSpace,context)}}function drawEllipse(pos,size=vec2(1),color=WHITE,angle=0,lineWidth=0,lineColor=BLACK,useWebGL=glEnable,screenSpace=false,context){ASSERT(isVector2(pos),"pos must be a vec2");ASSERT(isVector2(size),"size must be a vec2");ASSERT(isColor(color)&&isColor(lineColor),"color is invalid");ASSERT(isNumber(angle),"angle must be a number");ASSERT(isNumber(lineWidth),"lineWidth must be a number");ASSERT(lineWidth>=0&&lineWidth<size.x&&lineWidth<size.y,"invalid lineWidth");ASSERT(!context||!useWebGL,"context only supported in canvas 2D mode");if(useWebGL&&glEnable){const sides=glCircleSides;drawRegularPoly(pos,size,sides,color,lineWidth,lineColor,angle,useWebGL,screenSpace,context)}else{drawCanvas2D(pos,vec2(1),angle,false,context=>{context.fillStyle=color.toString();context.beginPath();context.ellipse(0,0,size.x/2,size.y/2,0,0,9);context.fill();if(lineWidth){context.strokeStyle=lineColor.toString();context.lineWidth=lineWidth;context.stroke()}},screenSpace,context)}}function drawCircle(pos,size=1,color=WHITE,lineWidth=0,lineColor=BLACK,useWebGL=glEnable,screenSpace=false,context){ASSERT(isNumber(size),"size must be a number");drawEllipse(pos,vec2(size),color,0,lineWidth,lineColor,useWebGL,screenSpace,context)}function drawCanvas2D(pos,size,angle=0,mirror=false,drawFunction,screenSpace=false,context=drawContext){ASSERT(isVector2(pos),"pos must be a vec2");ASSERT(isVector2(size),"size must be a vec2");ASSERT(isNumber(angle),"angle must be a number");ASSERT(typeof drawFunction==="function","drawFunction must be a function");if(!screenSpace)[pos,size,angle]=worldToScreenTransform(pos,size,angle);context.save();context.translate(pos.x+.5,pos.y+.5);context.rotate(angle);context.scale(mirror?-size.x:size.x,-size.y);drawFunction(context);context.restore()}function drawText(text,pos,size=1,color,lineWidth=0,lineColor,textAlign,font,fontStyle,maxWidth,angle=0,context=drawContext){pos=worldToScreen(pos);size*=cameraScale;lineWidth*=cameraScale;angle-=cameraAngle;angle*=-1;drawTextScreen(text,pos,size,color,lineWidth,lineColor,textAlign,font,fontStyle,maxWidth,angle,context)}function drawTextScreen(text,pos,size=1,color=WHITE,lineWidth=0,lineColor=BLACK,textAlign="center",font=fontDefault,fontStyle="",maxWidth,angle=0,context=mainContext){ASSERT(isString(text),"text must be a string");ASSERT(isVector2(pos),"pos must be a vec2");ASSERT(isNumber(size),"size must be a number");ASSERT(isColor(color),"color must be a color");ASSERT(isNumber(lineWidth),"lineWidth must be a number");ASSERT(isColor(lineColor),"lineColor must be a color");ASSERT(["left","center","right"].includes(textAlign),"align must be left, center, or right");ASSERT(isString(font),"font must be a string");ASSERT(isString(fontStyle),"fontStyle must be a string");ASSERT(isNumber(angle),"angle must be a number");context.fillStyle=color.toString();context.strokeStyle=lineColor.toString();context.lineWidth=lineWidth;context.textAlign=textAlign;context.font=fontStyle+" "+size+"px "+font;context.textBaseline="middle";const lines=(text+"").split("\n");const posY=pos.y-(lines.length-1)*size/2;context.save();context.translate(pos.x,posY);context.rotate(-angle);let yOffset=0;lines.forEach(line=>{lineWidth&&context.strokeText(line,0,yOffset,maxWidth);context.fillText(line,0,yOffset,maxWidth);yOffset+=size});context.restore()}function screenToWorld(screenPos){ASSERT(isVector2(screenPos),"screenPos must be a vec2");let x=(screenPos.x-mainCanvasSize.x/2+.5)/cameraScale;let y=(screenPos.y-mainCanvasSize.y/2+.5)/-cameraScale;if(cameraAngle){const c=cos(-cameraAngle),s=sin(-cameraAngle);const xr=x*c-y*s,yr=x*s+y*c;x=xr;y=yr}return new Vector2(x+cameraPos.x,y+cameraPos.y)}function worldToScreen(worldPos){ASSERT(isVector2(worldPos),"worldPos must be a vec2");let x=worldPos.x-cameraPos.x;let y=worldPos.y-cameraPos.y;if(cameraAngle){const c=cos(cameraAngle),s=sin(cameraAngle);const xr=x*c-y*s,yr=x*s+y*c;x=xr;y=yr}return new Vector2(x*cameraScale+mainCanvasSize.x/2-.5,y*-cameraScale+mainCanvasSize.y/2-.5)}function screenToWorldDelta(screenDelta){ASSERT(isVector2(screenDelta),"screenDelta must be a vec2");let x=screenDelta.x/cameraScale;let y=screenDelta.y/-cameraScale;if(cameraAngle){const c=cos(-cameraAngle),s=sin(-cameraAngle);const xr=x*c-y*s,yr=x*s+y*c;x=xr;y=yr}return new Vector2(x,y)}function worldToScreenDelta(worldDelta){ASSERT(isVector2(worldDelta),"worldDelta must be a vec2");let x=worldDelta.x;let y=worldDelta.y;if(cameraAngle){const c=cos(cameraAngle),s=sin(cameraAngle);const xr=x*c-y*s,yr=x*s+y*c;x=xr;y=yr}return new Vector2(x*cameraScale,y*-cameraScale)}function screenToWorldTransform(screenPos,screenSize,screenAngle=0){ASSERT(isVector2(screenPos),"screenPos must be a vec2");ASSERT(isVector2(screenSize),"screenSize must be a vec2");ASSERT(isNumber(screenAngle),"screenAngle must be a number");return[screenToWorld(screenPos),screenSize.scale(1/cameraScale),screenAngle+cameraAngle]}function worldToScreenTransform(worldPos,worldSize,worldAngle=0){ASSERT(isVector2(worldPos),"worldPos must be a vec2");ASSERT(isVector2(worldSize),"worldSize must be a vec2");ASSERT(isNumber(worldAngle),"worldAngle must be a number");return[worldToScreen(worldPos),worldSize.scale(cameraScale),worldAngle-cameraAngle]}function getCameraSize(){return mainCanvasSize.scale(1/cameraScale)}function isOnScreen(pos,size=0){ASSERT(isVector2(pos),"pos must be a vec2");ASSERT(isVector2(size)||isNumber(size),"size must be a vec2 or number");let x=pos.x-cameraPos.x;let y=pos.y-cameraPos.y;if(cameraAngle){const c=cos(cameraAngle),s=sin(cameraAngle);const xr=x*c-y*s,yr=x*s+y*c;x=xr;y=yr}x*=cameraScale*2;y*=-cameraScale*2;if(size instanceof Vector2)size=size.length();size*=cameraScale;const w=mainCanvasSize.x,h=mainCanvasSize.y;return x+size>-w&&x-size<w&&y+size>-h&&y-size<h}function setBlendMode(additive=false,context){glAdditive=additive;context||=drawContext;context.globalCompositeOperation=additive?"lighter":"source-over"}function combineCanvases(){const w=mainCanvasSize.x,h=mainCanvasSize.y;workCanvas.width=w;workCanvas.height=h;workContext.fillRect(0,0,w,h);glCopyToContext(workContext);workContext.drawImage(mainCanvas,0,0);mainCanvas.width|=0;mainContext.drawImage(workCanvas,0,0)}function drawImageColor(context,image,sx,sy,sWidth,sHeight,dx,dy,dWidth,dHeight,color,additiveColor,bleed=0){function isWhite(c){return c.r>=1&&c.g>=1&&c.b>=1}function isBlack(c){return c.r<=0&&c.g<=0&&c.b<=0&&c.a<=0}const sx2=bleed;const sy2=bleed;sWidth=max(1,sWidth|0);sHeight=max(1,sHeight|0);const sWidth2=sWidth-2*bleed;const sHeight2=sHeight-2*bleed;if(!canvasColorTiles||(additiveColor?isWhite(color.add(additiveColor))&&additiveColor.a<=0:isWhite(color))){context.globalAlpha=color.a;context.drawImage(image,sx+sx2,sy+sy2,sWidth2,sHeight2,dx,dy,dWidth,dHeight);context.globalAlpha=1}else{workReadCanvas.width=sWidth;workReadCanvas.height=sHeight;workReadContext.drawImage(image,sx|0,sy|0,sWidth,sHeight,0,0,sWidth,sHeight);const imageData=workReadContext.getImageData(0,0,sWidth,sHeight);const data=imageData.data;if(additiveColor&&!isBlack(additiveColor)){const colorMultiply=[color.r,color.g,color.b,color.a];const colorAdd=[additiveColor.r*255,additiveColor.g*255,additiveColor.b*255,additiveColor.a*255];for(let i=0;i<data.length;++i)data[i]=data[i]*colorMultiply[i&3]+colorAdd[i&3]|0;workReadContext.putImageData(imageData,0,0);context.drawImage(workReadCanvas,sx2,sy2,sWidth2,sHeight2,dx,dy,dWidth,dHeight)}else{for(let i=0;i<data.length;i+=4){data[i]*=color.r;data[i+1]*=color.g;data[i+2]*=color.b}workReadContext.putImageData(imageData,0,0);context.globalAlpha=color.a;context.drawImage(workReadCanvas,sx2,sy2,sWidth2,sHeight2,dx,dy,dWidth,dHeight);context.globalAlpha=1}}}function isFullscreen(){return!!document.fullscreenElement}function toggleFullscreen(){const rootElement=mainCanvas.parentElement;if(isFullscreen()){if(document.exitFullscreen)document.exitFullscreen()}else if(rootElement.requestFullscreen)rootElement.requestFullscreen()}function setCursor(cursorStyle="auto"){const rootElement=mainCanvas.parentElement;rootElement.style.cursor=cursorStyle}let engineFontImage;class FontImage{constructor(image,tileSize=vec2(8),paddingSize=vec2(0,1)){if(!image&&!engineFontImage){engineFontImage=new Image;engineFontImage.src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAYAQAAAAA9+x6JAAAAAnRSTlMAAHaTzTgAAAGiSURBVHjaZZABhxxBEIUf6ECLBdFY+Q0PMNgf0yCgsSAGZcT9sgIPtBWwIA5wgAPEoHUyJeeSlW+gjK+fegWwtROWpVQEyWh2npdpBmTUFVhb29RINgLIukoXr5LIAvYQ5ve+1FqWEMqNKTX3FAJHyQDRZvmKWubAACcv5z5Gtg2oyCWE+Yk/8JZQX1jTTCpKAFGIgza+dJCNBF2UskRlsgwitHbSV0QLgt9sTPtsRlvJjEr8C/FARWA2bJ/TtJ7lko34dNDn6usJUMzuErP89UUBJbWeozrwLLncXczd508deAjLWipLO4Q5XGPcJvPu92cNDaN0P5G1FL0nSOzddZOrJ6rNhbXGmeDvO3TF7DeJWl4bvaYQTNHCTeuqKZmbjHaSOFes+IX/+IhHrnAkXOAsfn24EM68XieIECoccD4KZLk/odiwzeo2rovYdhvb2HYFgyznJyDpYJdYOmfXgVdJTaUi4xA2uWYNYec9BLeqdl9EsoTw582mSFDX2DxVLbNt9U3YYoeatBad1c2Tj8t2akrjaIGJNywKB/7h75/gN3vCMSaadIUTAAAAAElFTkSuQmCC"}this.image=image||engineFontImage;this.tileSize=tileSize;this.paddingSize=paddingSize}drawText(text,pos,scale=1,center,context=drawContext){this.drawTextScreen(text,worldToScreen(pos).floor(),scale*cameraScale|0,center,context)}drawTextScreen(text,pos,scale=4,center=true,context=mainContext){context.save();const size=this.tileSize;const drawSize=size.add(this.paddingSize).scale(scale);const cols=this.image.width/this.tileSize.x|0;(text+"").split("\n").forEach((line,i)=>{const centerOffset=center?line.length*size.x*scale/2|0:0;for(let j=line.length;j--;){let charCode=line[j].charCodeAt(0);if(charCode<32||charCode>127)charCode=127;const tile=charCode-32;const x=tile%cols;const y=tile/cols|0;const drawPos=pos.add(vec2(j,i).multiply(drawSize));context.drawImage(this.image,x*size.x,y*size.y,size.x,size.y,drawPos.x-centerOffset,drawPos.y,size.x*scale,size.y*scale)}});context.restore()}}let mousePos=vec2();let mousePosScreen=vec2();let mouseDelta=vec2();let mouseDeltaScreen=vec2();let mouseWheel=0;let mouseInWindow=true;let isUsingGamepad=false;let inputPreventDefault=true;let gamepadPrimary=0;function setInputPreventDefault(preventDefault){inputPreventDefault=preventDefault}function inputClearKey(key,device=0,clearDown=true,clearPressed=true,clearReleased=true){if(!inputData[device])return;inputData[device][key]&=~((clearDown?1:0)|(clearPressed?2:0)|(clearReleased?4:0))}function inputClear(){inputData.length=0;inputData[0]=[];touchGamepadButtons.length=0;touchGamepadSticks.length=0;gamepadStickData.length=0;gamepadDpadData.length=0}function keyIsDown(key,device=0){ASSERT(isString(key),"key must be a number or string");ASSERT(device>0||typeof key!=="number"||key<3,"use code string for keyboard");return!!(inputData[device]?.[key]&1)}function keyWasPressed(key,device=0){ASSERT(isString(key),"key must be a number or string");ASSERT(device>0||typeof key!=="number"||key<3,"use code string for keyboard");return!!(inputData[device]?.[key]&2)}function keyWasReleased(key,device=0){ASSERT(isString(key),"key must be a number or string");ASSERT(device>0||typeof key!=="number"||key<3,"use code string for keyboard");return!!(inputData[device]?.[key]&4)}function keyDirection(up="ArrowUp",down="ArrowDown",left="ArrowLeft",right="ArrowRight"){ASSERT(isString(up),"up key must be a string");ASSERT(isString(down),"down key must be a string");ASSERT(isString(left),"left key must be a string");ASSERT(isString(right),"right key must be a string");const k=key=>keyIsDown(key)?1:0;return vec2(k(right)-k(left),k(up)-k(down))}function mouseIsDown(button){ASSERT(isNumber(button),"mouse button must be a number");return keyIsDown(button)}function mouseWasPressed(button){ASSERT(isNumber(button),"mouse button must be a number");return keyWasPressed(button)}function mouseWasReleased(button){ASSERT(isNumber(button),"mouse button must be a number");return keyWasReleased(button)}function gamepadIsDown(button,gamepad=gamepadPrimary){ASSERT(isNumber(button),"button must be a number");ASSERT(isNumber(gamepad),"gamepad must be a number");return keyIsDown(button,gamepad+1)}function gamepadWasPressed(button,gamepad=gamepadPrimary){ASSERT(isNumber(button),"button must be a number");ASSERT(isNumber(gamepad),"gamepad must be a number");return keyWasPressed(button,gamepad+1)}function gamepadWasReleased(button,gamepad=gamepadPrimary){ASSERT(isNumber(button),"button must be a number");ASSERT(isNumber(gamepad),"gamepad must be a number");return keyWasReleased(button,gamepad+1)}function gamepadStick(stick,gamepad=gamepadPrimary){ASSERT(isNumber(stick),"stick must be a number");ASSERT(isNumber(gamepad),"gamepad must be a number");return gamepadStickData[gamepad]?.[stick]??vec2()}function gamepadDpad(gamepad=gamepadPrimary){ASSERT(isNumber(gamepad),"gamepad must be a number");return gamepadDpadData[gamepad]??vec2()}function gamepadConnected(gamepad=gamepadPrimary){ASSERT(isNumber(gamepad),"gamepad must be a number");return!!inputData[gamepad+1]}function gamepadStickCount(gamepad=gamepadPrimary){ASSERT(isNumber(gamepad),"gamepad must be a number");return gamepadStickData[gamepad]?.length??0}const isTouchDevice=!headlessMode&&window.ontouchstart!==undefined;function vibrate(pattern=100){ASSERT(isNumber(pattern)||isArray(pattern),"pattern must be a number or array");vibrateEnable&&!headlessMode&&navigator&&navigator.vibrate&&navigator.vibrate(pattern)}function vibrateStop(){vibrate(0)}function pointerLockRequest(){!isTouchDevice&&mainCanvas.requestPointerLock?.()}function pointerLockExit(){document.exitPointerLock?.()}function pointerLockIsActive(){return document.pointerLockElement===mainCanvas}const inputData=[[]];const gamepadStickData=[],gamepadDpadData=[],gamepadHadInput=[];const touchGamepadTimer=new Timer,touchGamepadButtons=[],touchGamepadSticks=[];function inputInit(){if(headlessMode)return;document.addEventListener("keydown",onKeyDown);document.addEventListener("keyup",onKeyUp);document.addEventListener("mousedown",onMouseDown);document.addEventListener("mouseup",onMouseUp);document.addEventListener("mousemove",onMouseMove);document.addEventListener("mouseleave",onMouseLeave);document.addEventListener("wheel",onMouseWheel);document.addEventListener("contextmenu",onContextMenu);document.addEventListener("blur",onBlur);if(isTouchDevice&&touchInputEnable)touchInputInit();function onKeyDown(e){if(!e.repeat){isUsingGamepad=false;inputData[0][e.code]=3;if(inputWASDEmulateDirection)inputData[0][remapKey(e.code)]=3}const preventDefaultKeys=["ArrowUp","ArrowDown","ArrowLeft","ArrowRight","Space","Tab","Backspace"];if(preventDefaultKeys.includes(e.code))if(inputPreventDefault&&document.hasFocus()&&e.cancelable)e.preventDefault()}function onKeyUp(e){inputData[0][e.code]=inputData[0][e.code]&2|4;if(inputWASDEmulateDirection)inputData[0][remapKey(e.code)]=4}function remapKey(k){return inputWASDEmulateDirection?k==="KeyW"?"ArrowUp":k==="KeyS"?"ArrowDown":k==="KeyA"?"ArrowLeft":k==="KeyD"?"ArrowRight":k:k}function onMouseDown(e){if(isTouchDevice&&touchInputEnable)return;if(soundEnable&&!headlessMode&&audioContext&&!audioIsRunning())audioContext.resume();isUsingGamepad=false;inputData[0][e.button]=3;const mousePosScreenLast=mousePosScreen;mousePosScreen=mouseEventToScreen(vec2(e.x,e.y));mouseDeltaScreen=mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));if(inputPreventDefault&&document.hasFocus()&&e.cancelable)e.preventDefault()}function onMouseUp(e){if(isTouchDevice&&touchInputEnable)return;inputData[0][e.button]=inputData[0][e.button]&2|4}function onMouseMove(e){mouseInWindow=true;const mousePosScreenLast=mousePosScreen;mousePosScreen=mouseEventToScreen(vec2(e.x,e.y));const movement=pointerLockIsActive()?vec2(e.movementX,e.movementY):mousePosScreen.subtract(mousePosScreenLast);mouseDeltaScreen=mouseDeltaScreen.add(movement)}function onMouseLeave(){mouseInWindow=false}function onMouseWheel(e){mouseWheel=e.ctrlKey?0:sign(e.deltaY)}function onContextMenu(e){e.preventDefault()}function onBlur(){inputClear()}function touchInputInit(){document.addEventListener("touchstart",e=>handleTouch(e),{passive:false});document.addEventListener("touchmove",e=>handleTouch(e),{passive:false});document.addEventListener("touchend",e=>handleTouch(e),{passive:false});let wasTouching;function handleTouch(e){if(!touchInputEnable)return;if(touchGamepadEnable)handleTouchGamepad(e);if(soundEnable&&!headlessMode&&audioContext&&!audioIsRunning())audioContext.resume();const touching=e.touches.length;const button=0;if(touching){const pos=vec2(e.touches[0].clientX,e.touches[0].clientY);const mousePosScreenLast=mousePosScreen;mousePosScreen=mouseEventToScreen(pos);if(wasTouching){mouseDeltaScreen=mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));isUsingGamepad=touchGamepadEnable}else inputData[0][button]=3}else if(wasTouching)inputData[0][button]=inputData[0][button]&2|4;wasTouching=touching;if(inputPreventDefault&&document.hasFocus()&&e.cancelable)e.preventDefault();return true}function handleTouchGamepad(e){touchGamepadSticks.length=0;touchGamepadSticks[0]=vec2();touchGamepadSticks[1]=vec2();touchGamepadButtons.length=0;isUsingGamepad=true;const touching=e.touches.length;if(touching){touchGamepadTimer.set();if(touchGamepadCenterButton&&!wasTouching&&paused){touchGamepadButtons[9]=1;return}}if(paused)return;const stickCenter=vec2(touchGamepadSize,mainCanvasSize.y-touchGamepadSize);const buttonCenter=touchGamepadButtonCenter();const startCenter=mainCanvasSize.scale(.5);for(const touch of e.touches){const touchPos=mouseEventToScreen(vec2(touch.clientX,touch.clientY));if(stickCenter.distance(touchPos)<touchGamepadSize){const delta=touchPos.subtract(stickCenter);touchGamepadSticks[0]=delta.scale(2/touchGamepadSize).clampLength()}else if(buttonCenter.distance(touchPos)<touchGamepadSize){if(touchGamepadButtonCount===1){const delta=touchPos.subtract(buttonCenter);touchGamepadSticks[1]=delta.scale(2/touchGamepadSize).clampLength()}let button=buttonCenter.subtract(touchPos).direction();button=mod(button+2,4);if(touchGamepadButtonCount===1)button=0;else if(touchGamepadButtonCount===2){const delta=buttonCenter.subtract(touchPos);button=-delta.x<delta.y?1:0}button=button===3?2:button===2?3:button;if(button<touchGamepadButtonCount)touchGamepadButtons[button]=1}else if(touchGamepadCenterButton&&startCenter.distance(touchPos)<touchGamepadSize){touchGamepadButtons[9]=1}}}}function mouseEventToScreen(mousePos){const rect=mainCanvas.getBoundingClientRect();const px=percent(mousePos.x,rect.left,rect.right);const py=percent(mousePos.y,rect.top,rect.bottom);return vec2(px*mainCanvas.width,py*mainCanvas.height)}}function inputUpdate(){if(headlessMode)return;if(!(touchInputEnable&&isTouchDevice)&&!document.hasFocus())inputClear();mousePos=screenToWorld(mousePosScreen);mouseDelta=screenToWorldDelta(mouseDeltaScreen);gamepadsUpdate();function gamepadsUpdate(){const applyDeadZones=v=>{const min=.3,max=.8;const deadZone=v=>v>min?percent(v,min,max):v<-min?-percent(-v,min,max):0;return vec2(deadZone(v.x),deadZone(-v.y)).clampLength()};if(touchGamepadEnable&&isTouchDevice){if(!touchGamepadTimer.isSet())return;gamepadPrimary=0;const sticks=gamepadStickData[0]??(gamepadStickData[0]=[]);const dpad=gamepadDpadData[0]??(gamepadDpadData[0]=vec2());sticks[0]=vec2();dpad.set();const leftTouchStick=touchGamepadSticks[0]??vec2();if(touchGamepadAnalog)sticks[0]=applyDeadZones(leftTouchStick);else if(leftTouchStick.lengthSquared()>.3){const x=clamp(round(leftTouchStick.x),-1,1);const y=clamp(round(leftTouchStick.y),-1,1);dpad.set(x,-y);sticks[0]=dpad.clampLength()}if(touchGamepadButtonCount===1){const rightTouchStick=touchGamepadSticks[1]??vec2();sticks[1]=applyDeadZones(rightTouchStick)}const data=inputData[1]??(inputData[1]=[]);for(let i=10;i--;){const wasDown=gamepadIsDown(i,0);data[i]=touchGamepadButtons[i]?wasDown?1:3:wasDown?4:0}return}if(!gamepadsEnable||!navigator||!navigator.getGamepads)return;if(!debug&&!document.hasFocus())return;const maxGamepads=8;const gamepads=navigator.getGamepads();const gamepadCount=min(maxGamepads,gamepads.length);for(let i=0;i<gamepadCount;++i){const gamepad=gamepads[i];if(!gamepad){inputData[i+1]=undefined;gamepadStickData[i]=undefined;gamepadDpadData[i]=undefined;gamepadHadInput[i]=undefined;continue}const data=inputData[i+1]??(inputData[i+1]=[]);const sticks=gamepadStickData[i]??(gamepadStickData[i]=[]);const dpad=gamepadDpadData[i]??(gamepadDpadData[i]=vec2());for(let j=0;j<gamepad.axes.length-1;j+=2)sticks[j>>1]=applyDeadZones(vec2(gamepad.axes[j],gamepad.axes[j+1]));let hadInput=false;for(let j=gamepad.buttons.length;j--;){const button=gamepad.buttons[j];const wasDown=gamepadIsDown(j,i);data[j]=button.pressed?wasDown?1:3:wasDown?4:0;if(button.pressed)if(!button.value||button.value>.9)hadInput=true}if(hadInput){gamepadHadInput[i]=true;if(!gamepadHadInput[gamepadPrimary])gamepadPrimary=i;isUsingGamepad||=gamepadPrimary===i}if(gamepad.mapping==="standard"){dpad.set((gamepadIsDown(15,i)&&1)-(gamepadIsDown(14,i)&&1),(gamepadIsDown(12,i)&&1)-(gamepadIsDown(13,i)&&1))}else if(gamepad.axes&&gamepad.axes.length>=2){const x=clamp(round(gamepad.axes[0]),-1,1);const y=clamp(round(gamepad.axes[1]),-1,1);dpad.set(x,-y)}if(gamepadDirectionEmulateStick&&!dpad.isZero())sticks[0]=dpad.clampLength()}touchGamepadEnable&&isUsingGamepad&&touchGamepadTimer.unset()}}function inputUpdatePost(){if(headlessMode)return;for(const deviceInputData of inputData)for(const i in deviceInputData)deviceInputData[i]&=1;mouseWheel=0;mouseDelta=vec2();mouseDeltaScreen=vec2()}function inputRender(){touchGamepadRender();function touchGamepadRender(){if(!touchInputEnable||!isTouchDevice||headlessMode)return;if(!touchGamepadEnable||!touchGamepadTimer.isSet())return;const alpha=percent(touchGamepadTimer.get(),4,3);if(!alpha||paused)return;const context=mainContext;context.save();context.globalAlpha=alpha*touchGamepadAlpha;context.strokeStyle="#fff";context.lineWidth=3;const leftTouchStick=touchGamepadSticks[0]??vec2();context.fillStyle=leftTouchStick.lengthSquared()>0?"#fff":"#000";context.beginPath();const stickCenter=vec2(touchGamepadSize,mainCanvasSize.y-touchGamepadSize);if(touchGamepadAnalog){context.arc(stickCenter.x,stickCenter.y,touchGamepadSize/2,0,9)}else{for(let i=10;--i;){const angle=i*PI/4;context.arc(stickCenter.x,stickCenter.y,touchGamepadSize*.6,angle+PI/8,angle+PI/8);i%2&&context.arc(stickCenter.x,stickCenter.y,touchGamepadSize*.33,angle,angle)}}context.fill();context.stroke();{const buttonCenter=touchGamepadButtonCenter();const buttonSize=touchGamepadButtonCount>1?touchGamepadSize/4:touchGamepadSize/2;for(let i=0;i<touchGamepadButtonCount;i++){const j=mod(i-1,4);let button=touchGamepadButtonCount>2?j:min(j,touchGamepadButtonCount-1);button=button===3?2:button===2?3:button;const pos=touchGamepadButtonCount<2?buttonCenter:buttonCenter.add(vec2().setDirection(j,touchGamepadSize/2));context.fillStyle=touchGamepadButtons[button]?"#fff":"#000";context.beginPath();context.arc(pos.x,pos.y,buttonSize,0,9);context.fill();context.stroke()}}context.restore()}}function touchGamepadButtonCenter(){const center=mainCanvasSize.subtract(vec2(touchGamepadSize));if(touchGamepadButtonCount===2)center.x+=touchGamepadSize/2;return center}let audioContext=new AudioContext;let audioMasterGain;const audioDefaultSampleRate=44100;function audioIsRunning(){return audioContext.state==="running"}function audioInit(){if(!soundEnable||headlessMode)return;audioMasterGain=audioContext.createGain();audioMasterGain.connect(audioContext.destination);audioMasterGain.gain.value=soundVolume}class Sound{constructor(zzfxSound,range=soundDefaultRange,taper=soundDefaultTaper){if(!soundEnable||headlessMode)return;ASSERT(!zzfxSound||isArray(zzfxSound),"zzfxSound is invalid");ASSERT(isNumber(range),"range must be a number");ASSERT(isNumber(taper),"taper must be a number");this.range=range;this.taper=taper;this.randomness=0;this.sampleRate=audioDefaultSampleRate;this.loadedPercent=0;if(zzfxSound){const randomnessIndex=1,defaultRandomness=.05;this.randomness=zzfxSound[randomnessIndex]!==undefined?zzfxSound[randomnessIndex]:defaultRandomness;zzfxSound[randomnessIndex]=0;this.sampleChannels=[zzfxG(...zzfxSound)];this.loadedPercent=1}}play(pos,volume=1,pitch=1,randomnessScale=1,loop=false,paused=false){ASSERT(!pos||isVector2(pos),"pos must be a vec2");ASSERT(isNumber(volume),"volume must be a number");ASSERT(isNumber(pitch),"pitch must be a number");ASSERT(isNumber(randomnessScale),"randomnessScale must be a number");if(!soundEnable||headlessMode)return;if(!this.sampleChannels)return;let pan;if(pos){const range=this.range;if(range){const lengthSquared=cameraPos.distanceSquared(pos);if(lengthSquared>range*range)return;volume*=percent(lengthSquared**.5,range,range*this.taper)}pan=worldToScreen(pos).x*2/mainCanvas.width-1}const rate=pitch+pitch*this.randomness*randomnessScale*rand(-1,1);return new SoundInstance(this,volume,rate,pan,loop,paused)}playMusic(volume=1,loop=true,paused=false){return this.play(undefined,volume,1,0,loop,paused)}playNote(semitoneOffset=0,pos,volume){ASSERT(isNumber(semitoneOffset),"semitoneOffset must be a number");const pitch=getNoteFrequency(semitoneOffset,1);return this.play(pos,volume,pitch,0)}getDuration(){return this.sampleChannels&&this.sampleRate?this.sampleChannels[0].length/this.sampleRate:0}isLoaded(){return this.loadedPercent===1}}class SoundWave extends Sound{constructor(filename,randomness=0,range,taper,onloadCallback){super(undefined,range,taper);if(!soundEnable||headlessMode)return;ASSERT(!filename||isString(filename),"filename must be a string");ASSERT(isNumber(randomness),"randomness must be a number");this.onloadCallback=onloadCallback;this.randomness=randomness;filename&&this.loadSound(filename)}async loadSound(filename){const response=await fetch(filename);if(!response.ok)throw new Error(`Failed to load sound from ${filename}: ${response.status} ${response.statusText}`);const arrayBuffer=await response.arrayBuffer();const audioBuffer=await audioContext.decodeAudioData(arrayBuffer);const channelCount=audioBuffer.numberOfChannels;const samplesPerFrame=1e5;const sampleChannels=[];for(let channel=0;channel<channelCount;channel++){const channelData=audioBuffer.getChannelData(channel);const channelLength=channelData.length;sampleChannels[channel]=new Array(channelLength);let sampleIndex=0;while(sampleIndex<channelLength){await new Promise(resolve=>setTimeout(resolve,0));const endIndex=min(sampleIndex+samplesPerFrame,channelLength);for(;sampleIndex<endIndex;sampleIndex++)sampleChannels[channel][sampleIndex]=channelData[sampleIndex];const samplesTotal=channelCount*channelLength;const samplesProcessed=channel*channelLength+sampleIndex;this.loadedPercent=samplesProcessed/samplesTotal}}this.sampleRate=audioBuffer.sampleRate;this.sampleChannels=sampleChannels;this.loadedPercent=1;if(this.onloadCallback)this.onloadCallback(this)}}class SoundInstance{constructor(sound,volume=1,rate=1,pan=0,loop=false,paused=false){ASSERT(sound instanceof Sound,"SoundInstance requires a valid Sound object");ASSERT(volume>=0,"Sound volume must be positive or zero");ASSERT(rate>=0,"Sound rate must be positive or zero");ASSERT(isNumber(pan),"Sound pan must be a number");this.sound=sound;this.volume=volume;this.rate=rate;this.pan=pan;this.loop=loop;this.pausedTime=0;this.startTime=undefined;this.gainNode=undefined;this.source=undefined;this.onendedCallback=source=>{if(source===this.source)this.source=undefined};if(!paused)this.start()}start(offset=0){ASSERT(offset>=0,"Sound start offset must be positive or zero");if(this.isPlaying())this.stop();this.gainNode=audioContext.createGain();this.source=playSamples(this.sound.sampleChannels,this.volume,this.rate,this.pan,this.loop,this.sound.sampleRate,this.gainNode,offset,this.onendedCallback);if(this.source){this.startTime=audioContext.currentTime-offset;this.pausedTime=undefined}else{this.startTime=undefined;this.pausedTime=0}}setVolume(volume){ASSERT(volume>=0,"Sound volume must be positive or zero");this.volume=volume;if(this.gainNode)this.gainNode.gain.value=volume}stop(fadeTime=0){ASSERT(fadeTime>=0,"Sound fade time must be positive or zero");if(this.isPlaying()){if(fadeTime){const startFade=audioContext.currentTime;const endFade=startFade+fadeTime;this.gainNode.gain.linearRampToValueAtTime(1,startFade);this.gainNode.gain.linearRampToValueAtTime(0,endFade);this.source.stop(endFade)}else this.source.stop()}this.pausedTime=0;this.source=undefined;this.startTime=undefined}pause(){if(this.isPaused())return;this.pausedTime=this.getCurrentTime();this.source.stop();this.source=undefined;this.startTime=undefined}resume(){if(!this.isPaused())return;this.start(this.pausedTime)}isPlaying(){return!!this.source}isPaused(){return!this.isPlaying()}getCurrentTime(){const deltaTime=mod(audioContext.currentTime-this.startTime,this.getDuration());return this.isPlaying()?deltaTime:this.pausedTime}getDuration(){return this.rate?this.sound.getDuration()/this.rate:0}getSource(){return this.source}}function speak(text,language="",volume=1,rate=1,pitch=1){if(!soundEnable||headlessMode)return;if(!speechSynthesis)return;const utterance=new SpeechSynthesisUtterance(text);utterance.lang=language;utterance.volume=2*volume*soundVolume;utterance.rate=rate;utterance.pitch=pitch;speechSynthesis.speak(utterance);return utterance}function speakStop(){speechSynthesis&&speechSynthesis.cancel()}function getNoteFrequency(semitoneOffset,rootFrequency=220){return rootFrequency*2**(semitoneOffset/12)}function playSamples(sampleChannels,volume=1,rate=1,pan=0,loop=false,sampleRate=audioDefaultSampleRate,gainNode,offset=0,onended){if(!soundEnable||headlessMode)return;if(!audioIsRunning()){audioContext.resume();return}const channelCount=sampleChannels.length;const sampleLength=sampleChannels[0].length;const buffer=audioContext.createBuffer(channelCount,sampleLength,sampleRate);const source=audioContext.createBufferSource();sampleChannels.forEach((c,i)=>buffer.getChannelData(i).set(c));source.buffer=buffer;source.playbackRate.value=rate;source.loop=loop;gainNode=gainNode||audioContext.createGain();gainNode.gain.value=volume;gainNode.connect(audioMasterGain);const pannerNode=new StereoPannerNode(audioContext,{pan:clamp(pan,-1,1)});source.connect(pannerNode).connect(gainNode);if(onended)source.addEventListener("ended",()=>onended(source));const startOffset=offset*rate;source.start(0,startOffset);return source}function zzfx(...zzfxSound){return playSamples([zzfxG(...zzfxSound)])}function zzfxG(volume=1,randomness=.05,frequency=220,attack=0,sustain=0,release=.1,shape=0,shapeCurve=1,slide=0,deltaSlide=0,pitchJump=0,pitchJumpTime=0,repeatTime=0,noise=0,modulation=0,bitCrush=0,delay=0,sustainVolume=1,decay=0,tremolo=0,filter=0){let sampleRate=audioDefaultSampleRate,PI2=PI*2,startSlide=slide*=500*PI2/sampleRate/sampleRate,startFrequency=frequency*=(1+rand(randomness,-randomness))*PI2/sampleRate,modOffset=0,repeat=0,crush=0,jump=1,length,b=[],t=0,i=0,s=0,f,quality=2,w=PI2*abs(filter)*2/sampleRate,cosw=cos(w),alpha=sin(w)/2/quality,a0=1+alpha,a1=-2*cosw/a0,a2=(1-alpha)/a0,b0=(1+sign(filter)*cosw)/2/a0,b1=-(sign(filter)+cosw)/a0,b2=b0,x2=0,x1=0,y2=0,y1=0;const minAttack=9;attack=attack*sampleRate||minAttack;decay*=sampleRate;sustain*=sampleRate;release*=sampleRate;delay*=sampleRate;deltaSlide*=500*PI2/sampleRate**3;modulation*=PI2/sampleRate;pitchJump*=PI2/sampleRate;pitchJumpTime*=sampleRate;repeatTime=repeatTime*sampleRate|0;for(length=attack+decay+sustain+release+delay|0;i<length;b[i++]=s*volume){if(!(++crush%(bitCrush*100|0))){s=shape?shape>1?shape>2?shape>3?shape>4?t/PI2%1<shapeCurve/2?1:-1:sin(t**3):max(min(tan(t),1),-1):1-(2*t/PI2%2+2)%2:1-4*abs(round(t/PI2)-t/PI2):sin(t);s=(repeatTime?1-tremolo+tremolo*sin(PI2*i/repeatTime):1)*(shape>4?s:sign(s)*abs(s)**shapeCurve)*(i<attack?i/attack:i<attack+decay?1-(i-attack)/decay*(1-sustainVolume):i<attack+decay+sustain?sustainVolume:i<length-delay?(length-i-delay)/release*sustainVolume:0);s=delay?s/2+(delay>i?0:(i<length-delay?1:(length-i)/delay)*b[i-delay|0]/2/volume):s;if(filter)s=y1=b2*x2+b1*(x2=x1)+b0*(x1=s)-a2*y2-a1*(y2=y1)}f=(frequency+=slide+=deltaSlide)*cos(modulation*modOffset++);t+=f+f*noise*sin(i**5);if(jump&&++jump>pitchJumpTime){frequency+=pitchJump;startFrequency+=pitchJump;jump=0}if(repeatTime&&!(++repeat%repeatTime)){frequency=startFrequency;slide=startSlide;jump||=1}}return b}const tileCollisionLayers=[];function tileCollisionGetData(pos){for(const layer of tileCollisionLayers)if(pos.arrayCheck(layer.size))return layer.getCollisionData(pos);return 0}function tileCollisionTest(pos,size=vec2(),object,solidOnly=true){for(const layer of tileCollisionLayers){if(!solidOnly||layer.isSolid)if(layer.collisionTest(pos,size,object))return layer}}function tileCollisionRaycast(posStart,posEnd,object,normal,solidOnly=true){for(const layer of tileCollisionLayers){if(!solidOnly||layer.isSolid){const hitPos=layer.collisionRaycast(posStart,posEnd,object,normal);if(hitPos)return hitPos}}}function tileLayersLoad(tileMapData,tileInfo=tile(),renderOrder=0,collisionLayer,draw=true){if(!tileMapData){const s=50;tileMapData={};tileMapData.height=tileMapData.width=s;tileMapData.layers=[{}];tileMapData.layers[0].data=new Array(s*s).fill(0)}ASSERT(tileMapData.width&&tileMapData.height);ASSERT(tileMapData.layers&&tileMapData.layers.length);const tileLayers=[];const levelSize=vec2(tileMapData.width,tileMapData.height);const layerCount=tileMapData.layers.length;for(let layerIndex=layerCount;layerIndex--;){const dataLayer=tileMapData.layers[layerIndex];ASSERT(dataLayer.data&&dataLayer.data.length);ASSERT(levelSize.area()===dataLayer.data.length);const layerRenderOrder=renderOrder-(layerCount-1-layerIndex);const tileLayer=new TileCollisionLayer(vec2(),levelSize,tileInfo,layerRenderOrder);tileLayers[layerIndex]=tileLayer;const layerColor=dataLayer.tintcolor?(new Color).setHex(dataLayer.tintcolor):dataLayer.color||WHITE;ASSERT(isColor(layerColor),"layer color is not a color");for(let x=levelSize.x;x--;)for(let y=levelSize.y;y--;){const pos=vec2(x,levelSize.y-1-y);const data=dataLayer.data[x+y*levelSize.x];if(data){const layerData=new TileLayerData(data-1,0,false,layerColor);tileLayer.setData(pos,layerData);if(layerIndex===collisionLayer)tileLayer.setCollisionData(pos,1)}}if(draw)tileLayer.redraw()}return tileLayers}class TileLayerData{constructor(tile,direction=0,mirror=false,color=new Color){this.tile=tile;this.direction=direction;this.mirror=mirror;this.color=color.copy()}clear(){this.tile=this.direction=0;this.mirror=false;this.color=new Color}}class CanvasLayer extends EngineObject{constructor(position,size,angle=0,renderOrder=0,canvasSize=vec2(512)){ASSERT(isVector2(canvasSize),"canvasSize must be a Vector2");super(position,size,undefined,angle,WHITE,renderOrder);this.canvas=headlessMode?undefined:new OffscreenCanvas(canvasSize.x,canvasSize.y);this.context=this.canvas?.getContext("2d");const useWebGL=false;this.textureInfo=new TextureInfo(this.canvas,useWebGL);this.refreshWebGL=false;this.mass=this.gravityScale=this.friction=this.restitution=0}destroy(){if(this.destroyed)return;this.textureInfo.destroyWebGLTexture();super.destroy()}render(){this.draw(this.pos,this.size,this.angle,this.color,this.mirror,this.additiveColor)}draw(pos,size,angle=0,color=WHITE,mirror=false,additiveColor,screenSpace=false,context){const useWebGL=glEnable&&this.textureInfo.hasWebGL();if(useWebGL&&this.refreshWebGL){this.textureInfo.createWebGLTexture();this.refreshWebGL=false}const tileInfo=(new TileInfo).setFullImage(this.textureInfo);drawTile(pos,size,tileInfo,color,angle,mirror,additiveColor,useWebGL,screenSpace,context)}drawCanvas2D(pos,size,angle,mirror,drawFunction){if(!this.context)return;const context=this.context;context.save();pos=pos.subtract(this.pos).multiply(this.tileInfo.size);size=size.multiply(this.tileInfo.size);context.translate(pos.x,this.canvas.height-pos.y);context.rotate(angle);context.scale(mirror?-size.x:size.x,size.y);drawFunction(context);context.restore()}drawTile(pos,size=vec2(1),tileInfo,color=new Color,angle,mirror){this.drawCanvas2D(pos,size,angle,mirror,context=>{const textureInfo=tileInfo&&tileInfo.textureInfo;if(textureInfo){context.globalAlpha=color.a;context.drawImage(textureInfo.image,tileInfo.pos.x,tileInfo.pos.y,tileInfo.size.x,tileInfo.size.y,-.5,-.5,1,1);context.globalAlpha=1}else{context.fillStyle=color.toString();context.fillRect(-.5,-.5,1,1)}})}drawRect(pos,size,color,angle){this.drawTile(pos,size,undefined,color,angle)}useWebGL(enable=true,immediate=false){if(!immediate&&enable&&this.textureInfo.hasWebGL()){this.refreshWebGL=true;return}if(enable)this.textureInfo.createWebGLTexture();else this.textureInfo.destroyWebGLTexture()}}class TileLayer extends CanvasLayer{constructor(position,size,tileInfo=tile(),renderOrder=0){const canvasSize=tileInfo?size.multiply(tileInfo.size):size;super(position,size,0,renderOrder,canvasSize);this.tileInfo=tileInfo;this.data=[];for(let j=this.size.area();j--;)this.data.push(new TileLayerData);if(headlessMode){this.redraw=()=>{};this.render=()=>{};this.redrawStart=()=>{};this.redrawEnd=()=>{};this.drawTileData=()=>{};this.drawCanvas2D=()=>{};this.useWebGL=()=>{}}}setData(layerPos,data,redraw=false){ASSERT(isVector2(layerPos),"layerPos must be a Vector2");ASSERT(data instanceof TileLayerData,"data must be a TileLayerData");if(layerPos.arrayCheck(this.size)){this.data[(layerPos.y|0)*this.size.x+layerPos.x|0]=data;redraw&&this.drawTileData(layerPos)}}getData(layerPos){ASSERT(isVector2(layerPos),"layerPos must be a Vector2");return layerPos.arrayCheck(this.size)&&this.data[(layerPos.y|0)*this.size.x+layerPos.x|0]}render(){ASSERT(drawContext!==this.context,"must call redrawEnd() after drawing tiles!");if(this.refreshWebGL){this.textureInfo.createWebGLTexture();this.refreshWebGL=false}const tileInfo=(new TileInfo).setFullImage(this.textureInfo);const size=this.drawSize||this.size;const pos=this.pos.add(size.scale(.5));const useWebGL=glEnable&&this.textureInfo.hasWebGL();drawTile(pos,size,tileInfo,WHITE,0,false,CLEAR_BLACK,useWebGL)}redraw(){this.redrawStart(true);for(let x=this.size.x;x--;)for(let y=this.size.y;y--;)this.drawTileData(vec2(x,y),false);this.redrawEnd();this.useWebGL()}redrawStart(clear=false){if(!this.context)return;this.savedRenderSettings=[drawCanvas,drawContext,mainCanvasSize,cameraPos,cameraScale,canvasClearColor];drawCanvas=this.canvas;drawContext=this.context;cameraPos=this.size.scale(.5);const tileSize=this.tileInfo?this.tileInfo.size:vec2(1);cameraScale=tileSize.x;canvasClearColor=CLEAR_BLACK;mainCanvasSize=this.size.multiply(tileSize);if(clear){drawCanvas.width=mainCanvasSize.x;drawCanvas.height=mainCanvasSize.y}drawContext.imageSmoothingEnabled=!tilesPixelated;glPreRender()}redrawEnd(){if(!this.context)return;ASSERT(drawContext===this.context,"must call redrawStart() before drawing tiles");glCopyToContext(drawContext);[drawCanvas,drawContext,mainCanvasSize,cameraPos,cameraScale,canvasClearColor]=this.savedRenderSettings}drawTileData(layerPos,clear=true){if(!this.context)return;const s=this.tileInfo.size;if(clear){const pos=layerPos.multiply(s);this.context.clearRect(pos.x,this.canvas.height-pos.y,s.x,-s.y)}const d=this.getData(layerPos);if(d.tile!==undefined){ASSERT(drawContext===this.context,"must call redrawStart() before drawing tiles");const pos=layerPos.add(vec2(.5));const tileInfo=tile(d.tile,s,this.tileInfo.textureIndex,this.tileInfo.padding);drawTile(pos,vec2(1),tileInfo,d.color,d.direction*PI/2,d.mirror)}}}class TileCollisionLayer extends TileLayer{constructor(position,size,tileInfo=tile(),renderOrder=0){super(position,size.floor(),tileInfo,renderOrder);this.collisionData=[];this.initCollision(this.size);tileCollisionLayers.push(this);this.isSolid=true}destroy(){if(this.destroyed)return;const index=tileCollisionLayers.indexOf(this);ASSERT(index>=0,"tile collision layer not found in array");index>=0&&tileCollisionLayers.splice(index,1);super.destroy()}initCollision(size){ASSERT(isVector2(size),"size must be a Vector2");this.size=size.floor();this.collisionData=[];this.collisionData.length=size.area();this.collisionData.fill(0)}setCollisionData(gridPos,data=1){ASSERT(isVector2(gridPos),"gridPos must be a Vector2");const i=(gridPos.y|0)*this.size.x+gridPos.x|0;gridPos.arrayCheck(this.size)&&(this.collisionData[i]=data)}getCollisionData(gridPos){ASSERT(isVector2(gridPos),"gridPos must be a Vector2");const i=(gridPos.y|0)*this.size.x+gridPos.x|0;return gridPos.arrayCheck(this.size)?this.collisionData[i]:0}collisionTest(pos,size=new Vector2,object){ASSERT(isVector2(pos)&&isVector2(size),"pos and size must be Vector2s");ASSERT(!object||object instanceof EngineObject,"object must be an EngineObject");const posX=pos.x-this.pos.x;const posY=pos.y-this.pos.y;const minX=max(posX-size.x/2|0,0);const minY=max(posY-size.y/2|0,0);const maxX=min(posX+size.x/2,this.size.x);const maxY=min(posY+size.y/2,this.size.y);const hitPos=new Vector2;for(let y=minY;y<maxY;++y)for(let x=minX;x<maxX;++x){const tileData=this.collisionData[y*this.size.x+x];if(tileData)if(!object||object.collideWithTile(tileData,hitPos.set(x+this.pos.x,y+this.pos.y)))return true}return false}collisionRaycast(posStart,posEnd,object,normal){ASSERT(isVector2(posStart)&&isVector2(posEnd),"positions must be Vector2s");ASSERT(!object||object instanceof EngineObject,"object must be an EngineObject");const localPos=new Vector2;const collisionTest=pos=>{localPos.set(pos.x-this.pos.x,pos.y-this.pos.y);const tileData=this.getCollisionData(localPos);return tileData&&(!object||object.collideWithTile(tileData,pos))};debugRaycast&&debugLine(posStart,posEnd,"#00f",.02);const hitPos=lineTest(posStart,posEnd,collisionTest,normal);if(hitPos){const tilePos=hitPos.floor().add(vec2(.5));debugRaycast&&debugRect(tilePos,vec2(1),"#f008");debugRaycast&&debugLine(posStart,hitPos,"#f00",.02);debugRaycast&&debugPoint(hitPos,"#0f0");debugRaycast&&normal&&debugLine(hitPos,hitPos.add(normal),"#ff0",.02);return hitPos}}}class ParticleEmitter extends EngineObject{constructor(position,angle,emitSize=0,emitTime=0,emitRate=100,emitConeAngle=PI,tileInfo,colorStartA=WHITE,colorStartB=WHITE,colorEndA=CLEAR_WHITE,colorEndB=CLEAR_WHITE,particleTime=.5,sizeStart=.1,sizeEnd=1,speed=.1,angleSpeed=.05,damping=1,angleDamping=1,gravityScale=0,particleConeAngle=PI,fadeRate=.1,randomness=.2,collideTiles=false,additive=false,randomColorLinear=true,renderOrder=additive?1e9:0,localSpace=false){super(position,vec2(),tileInfo,angle,undefined,renderOrder);this.emitSize=emitSize instanceof Vector2?emitSize.copy():emitSize;this.emitTime=emitTime;this.emitRate=emitRate;this.emitConeAngle=emitConeAngle;this.colorStartA=colorStartA.copy();this.colorStartB=colorStartB.copy();this.colorEndA=colorEndA.copy();this.colorEndB=colorEndB.copy();this.randomColorLinear=randomColorLinear;this.particleTime=particleTime;this.sizeStart=sizeStart;this.sizeEnd=sizeEnd;this.speed=speed;this.angleSpeed=angleSpeed;this.damping=damping;this.angleDamping=angleDamping;this.gravityScale=gravityScale;this.particleConeAngle=particleConeAngle;this.fadeRate=fadeRate;this.randomness=randomness;this.collideTiles=collideTiles;this.additive=additive;this.localSpace=localSpace;this.trailScale=0;this.particleDestroyCallback=undefined;this.particleCreateCallback=undefined;this.emitTimeBuffer=0;this.velocityInheritance=0;this.previousAngle=this.angle;this.previousPos=this.pos.copy()}updatePhysics(){}update(){if(this.velocityInheritance){const p=this.velocityInheritance;this.velocity.x=p*(this.pos.x-this.previousPos.x);this.velocity.y=p*(this.pos.y-this.previousPos.y);this.angleVelocity=p*(this.angle-this.previousAngle);this.previousAngle=this.angle;this.previousPos.x=this.pos.x;this.previousPos.y=this.pos.y}if(!this.emitTime||this.getAliveTime()<=this.emitTime){if(this.emitRate&&particleEmitRateScale){const rate=1/this.emitRate/particleEmitRateScale;for(this.emitTimeBuffer+=timeDelta;this.emitTimeBuffer>0;this.emitTimeBuffer-=rate)this.emitParticle()}}else this.destroy();if(debugParticles){if(typeof this.emitSize==="number")debugCircle(this.pos,this.emitSize/2,"#0f0");else debugRect(this.pos,this.emitSize,"#0f0",0,this.angle)}}emitParticle(){let pos=typeof this.emitSize==="number"?randInCircle(this.emitSize/2):vec2(rand(-.5,.5),rand(-.5,.5)).multiply(this.emitSize).rotate(this.angle);let angle=rand(this.particleConeAngle,-this.particleConeAngle);if(!this.localSpace){pos=this.pos.add(pos);angle+=this.angle}const randomness=this.randomness;const randomizeScale=v=>v+v*rand(randomness,-randomness);const particleTime=randomizeScale(this.particleTime);const sizeStart=randomizeScale(this.sizeStart);const sizeEnd=randomizeScale(this.sizeEnd);const speed=randomizeScale(this.speed);const angleSpeed=randomizeScale(this.angleSpeed)*randSign();const coneAngle=rand(this.emitConeAngle,-this.emitConeAngle);const colorStart=randColor(this.colorStartA,this.colorStartB,this.randomColorLinear);const colorEnd=randColor(this.colorEndA,this.colorEndB,this.randomColorLinear);const velocityAngle=this.localSpace?coneAngle:this.angle+coneAngle;const particle=new Particle(pos,this.tileInfo,angle,colorStart,colorEnd,particleTime,sizeStart,sizeEnd,this.fadeRate,this.additive,this.trailScale,this.localSpace&&this,this.particleDestroyCallback);particle.velocity=vec2().setAngle(velocityAngle,speed);particle.angleVelocity=angleSpeed;if(!this.localSpace&&this.velocityInheritance>0){particle.velocity.x+=this.velocity.x;particle.velocity.y+=this.velocity.y;particle.angleVelocity+=this.angleVelocity}particle.fadeRate=this.fadeRate;particle.damping=this.damping;particle.angleDamping=this.angleDamping;particle.restitution=this.restitution;particle.friction=this.friction;particle.gravityScale=this.gravityScale;particle.collideTiles=this.collideTiles;particle.renderOrder=this.renderOrder;particle.mirror=randBool();this.particleCreateCallback&&this.particleCreateCallback(particle);return particle}render(){}}class Particle extends EngineObject{constructor(position,tileInfo,angle,colorStart,colorEnd,lifeTime,sizeStart,sizeEnd,fadeRate,additive,trailScale,localSpaceEmitter,destroyCallback){super(position,vec2(),tileInfo,angle);this.colorStart=colorStart;this.colorEnd=colorEnd;this.lifeTime=lifeTime;this.sizeStart=sizeStart;this.sizeEnd=sizeEnd;this.fadeRate=fadeRate;this.additive=additive;this.trailScale=trailScale;this.localSpaceEmitter=localSpaceEmitter;this.destroyCallback=destroyCallback;this.clampSpeed=false}update(){if(this.collideTiles||this.collideSolidObjects){const length2=this.velocity.lengthSquared();if(length2>objectMaxSpeed*objectMaxSpeed){const s=objectMaxSpeed/length2**.5;this.velocity.x*=s;this.velocity.y*=s}}if(this.lifeTime>0&&time-this.spawnTime>this.lifeTime){const c=this.colorEnd;this.color.set(c.r,c.g,c.b,c.a);this.size.set(this.sizeEnd,this.sizeEnd);this.destroyCallback&&this.destroyCallback(this);this.destroyed=1}}render(){const p1=this.lifeTime>0?min((time-this.spawnTime)/this.lifeTime,1):1,p2=1-p1;const radius=p2*this.sizeStart+p1*this.sizeEnd;const size=vec2(radius);this.color.r=p2*this.colorStart.r+p1*this.colorEnd.r;this.color.g=p2*this.colorStart.g+p1*this.colorEnd.g;this.color.b=p2*this.colorStart.b+p1*this.colorEnd.b;this.color.a=p2*this.colorStart.a+p1*this.colorEnd.a;const fadeRate=this.fadeRate/2;this.color.a*=p1<fadeRate?p1/fadeRate:p1>1-fadeRate?(1-p1)/fadeRate:1;this.additive&&setBlendMode(true);let pos=this.pos,angle=this.angle;if(this.localSpaceEmitter){const a=this.localSpaceEmitter.angle;const c=cos(a),s=sin(a);pos=this.localSpaceEmitter.pos.add(new Vector2(pos.x*c-pos.y*s,pos.x*s+pos.y*c));angle+=this.localSpaceEmitter.angle}if(this.trailScale){const direction=this.localSpaceEmitter?this.velocity.rotate(-this.localSpaceEmitter.angle):this.velocity;const speed=direction.length();if(speed){const trailLength=speed*this.trailScale;size.y=max(size.x,trailLength);angle=atan2(direction.x,direction.y);drawTile(pos,size,this.tileInfo,this.color,angle,this.mirror)}}else drawTile(pos,size,this.tileInfo,this.color,angle,this.mirror);this.additive&&setBlendMode();debugParticles&&debugRect(pos,size,"#f005",0,angle)}}const medals={};let medalsDisplayQueue=[],medalsSaveName,medalsDisplayTimeLast;function medalsInit(saveName){medalsSaveName=saveName;if(!debugMedals)medalsForEach(medal=>medal.unlocked=!!localStorage[medal.storageKey()]);engineAddPlugin(undefined,medalsRender);function medalsRender(){if(!medalsDisplayQueue.length)return;const medal=medalsDisplayQueue[0];const time=timeReal-medalsDisplayTimeLast;if(!medalsDisplayTimeLast)medalsDisplayTimeLast=timeReal;else if(time>medalDisplayTime){medalsDisplayTimeLast=0;medalsDisplayQueue.shift()}else{const slideOffTime=medalDisplayTime-medalDisplaySlideTime;const hidePercent=time<medalDisplaySlideTime?1-time/medalDisplaySlideTime:time>slideOffTime?(time-slideOffTime)/medalDisplaySlideTime:0;medal.render(hidePercent)}}}function medalsForEach(callback){Object.values(medals).forEach(medal=>callback(medal))}class Medal{constructor(id,name,description="",icon="🏆",src){ASSERT(id>=0&&!medals[id]);this.id=id;this.name=name;this.description=description;this.icon=icon;this.unlocked=false;if(src)(this.image=new Image).src=src;medals[id]=this}unlock(){if(medalsPreventUnlock||this.unlocked)return;ASSERT(medalsSaveName,"save name must be set");localStorage[this.storageKey()]=this.unlocked=true;medalsDisplayQueue.push(this)}render(hidePercent=0){const context=mainContext;const width=min(medalDisplaySize.x,mainCanvas.width);const height=medalDisplaySize.y;const x=mainCanvas.width-width;const y=-height*hidePercent;const backgroundColor=hsl(0,0,.9);context.save();context.beginPath();context.fillStyle=backgroundColor.toString();context.strokeStyle=BLACK.toString();context.lineWidth=3;context.rect(x,y,width,height);context.fill();context.stroke();context.clip();const gap=vec2(.1,.05).scale(height);const medalDisplayIconSize=height-2*gap.x;this.renderIcon(vec2(x+gap.x+medalDisplayIconSize/2,y+height/2),medalDisplayIconSize);const nameSize=height*.5;const descriptionSize=height*.3;const pos=vec2(x+medalDisplayIconSize+2*gap.x,y+gap.y*2+nameSize/2);const textWidth=width-medalDisplayIconSize-3*gap.x;drawTextScreen(this.name,pos,nameSize,BLACK,0,undefined,"left",undefined,undefined,textWidth);pos.y=y+height-gap.y*2-descriptionSize/2;drawTextScreen(this.description,pos,descriptionSize,BLACK,0,undefined,"left",undefined,undefined,textWidth);context.restore()}renderIcon(pos,size){if(this.image)mainContext.drawImage(this.image,pos.x-size/2,pos.y-size/2,size,size);else drawTextScreen(this.icon,pos,size*.7,BLACK)}storageKey(){return medalsSaveName+"_"+this.id}}let glCanvas;let glContext;let glAntialias=true;let glShader,glPolyShader,glPolyMode,glAdditive,glBatchAdditive,glActiveTexture,glArrayBuffer,glGeometryBuffer,glPositionData,glColorData,glBatchCount,glTextureInfos,glCanBeEnabled=true;const gl_ARRAY_BUFFER_SIZE=5e5;const gl_INDICES_PER_INSTANCE=11;const gl_INSTANCE_BYTE_STRIDE=gl_INDICES_PER_INSTANCE*4;const gl_MAX_INSTANCES=gl_ARRAY_BUFFER_SIZE/gl_INSTANCE_BYTE_STRIDE|0;const gl_INDICES_PER_POLY_VERTEX=3;const gl_POLY_VERTEX_BYTE_STRIDE=gl_INDICES_PER_POLY_VERTEX*4;const gl_MAX_POLY_VERTEXES=gl_ARRAY_BUFFER_SIZE/gl_POLY_VERTEX_BYTE_STRIDE|0;function glInit(rootElement){glTextureInfos=new Set;if(!glEnable||headlessMode){glCanBeEnabled=false;return}glCanvas=document.createElement("canvas");glContext=glCanvas.getContext("webgl2",{antialias:glAntialias});if(!glContext){console.warn("WebGL2 not supported, falling back to 2D canvas rendering!");glCanvas=glContext=undefined;glEnable=false;glCanBeEnabled=false;return}rootElement.appendChild(glCanvas);initWebGL();glCanvas.addEventListener("webglcontextlost",e=>{glEnable=false;glCanvas.style.display="none";e.preventDefault();LOG("WebGL context lost! Switching to Canvas2d rendering.");for(const info of glTextureInfos)info.glTexture=undefined;glActiveTexture=undefined;pluginList.forEach(plugin=>plugin.glContextLost?.())});glCanvas.addEventListener("webglcontextrestored",()=>{glEnable=true;glCanvas.style.display="";LOG("WebGL context restored, reinitializing...");initWebGL();for(const info of glTextureInfos)info.glTexture=glCreateTexture(info.image);pluginList.forEach(plugin=>plugin.glContextRestored?.())});function initWebGL(){glShader=glCreateProgram("#version 300 es\n"+"precision highp float;"+"uniform mat4 m;"+"in vec2 g;"+"in vec4 p,u,c,a;"+"in float r;"+"out vec2 v;"+"out vec4 d,e;"+"void main(){"+"vec2 s=(g-.5)*p.zw;"+"gl_Position=m*vec4(p.xy+s*cos(r)-vec2(-s.y,s)*sin(r),1,1);"+"v=mix(u.xw,u.zy,g);"+"d=c;e=a;"+"}","#version 300 es\n"+"precision highp float;"+"uniform sampler2D s;"+"in vec2 v;"+"in vec4 d,e;"+"out vec4 c;"+"void main(){"+"c=texture(s,v)*d+e;"+"}");glPolyShader=glCreateProgram("#version 300 es\n"+"precision highp float;"+"uniform mat4 m;"+"in vec2 p;"+"in vec4 c;"+"out vec4 d;"+"void main(){"+"gl_Position=m*vec4(p,1,1);"+"d=c;"+"}","#version 300 es\n"+"precision highp float;"+"in vec4 d;"+"out vec4 c;"+"void main(){"+"c=d;"+"}");const glInstanceData=new ArrayBuffer(gl_ARRAY_BUFFER_SIZE);glPositionData=new Float32Array(glInstanceData);glColorData=new Uint32Array(glInstanceData);glArrayBuffer=glContext.createBuffer();glGeometryBuffer=glContext.createBuffer();const geometry=new Float32Array([glBatchCount=0,0,1,0,0,1,1,1]);glContext.bindBuffer(glContext.ARRAY_BUFFER,glGeometryBuffer);glContext.bufferData(glContext.ARRAY_BUFFER,geometry,glContext.STATIC_DRAW)}}function glSetInstancedMode(){if(!glPolyMode)return;glFlush();glPolyMode=false;glContext.useProgram(glShader);let offset=0;const initVertexAttribArray=(name,type,typeSize,size)=>{const location=glContext.getAttribLocation(glShader,name);const stride=typeSize&&gl_INSTANCE_BYTE_STRIDE;const divisor=typeSize&&1;const normalize=typeSize===1;glContext.enableVertexAttribArray(location);glContext.vertexAttribPointer(location,size,type,normalize,stride,offset);glContext.vertexAttribDivisor(location,divisor);offset+=size*typeSize};glContext.bindBuffer(glContext.ARRAY_BUFFER,glGeometryBuffer);initVertexAttribArray("g",glContext.FLOAT,0,2);glContext.bindBuffer(glContext.ARRAY_BUFFER,glArrayBuffer);glContext.bufferData(glContext.ARRAY_BUFFER,gl_ARRAY_BUFFER_SIZE,glContext.DYNAMIC_DRAW);initVertexAttribArray("p",glContext.FLOAT,4,4);initVertexAttribArray("u",glContext.FLOAT,4,4);initVertexAttribArray("c",glContext.UNSIGNED_BYTE,1,4);initVertexAttribArray("a",glContext.UNSIGNED_BYTE,1,4);initVertexAttribArray("r",glContext.FLOAT,4,1)}function glSetPolyMode(){if(glPolyMode)return;glFlush();glPolyMode=true;glContext.useProgram(glPolyShader);let offset=0;const initVertexAttribArray=(name,type,typeSize,size)=>{const location=glContext.getAttribLocation(glPolyShader,name);const normalize=typeSize===1;const stride=gl_POLY_VERTEX_BYTE_STRIDE;glContext.enableVertexAttribArray(location);glContext.vertexAttribPointer(location,size,type,normalize,stride,offset);glContext.vertexAttribDivisor(location,0);offset+=size*typeSize};glContext.bindBuffer(glContext.ARRAY_BUFFER,glArrayBuffer);glContext.bufferData(glContext.ARRAY_BUFFER,gl_ARRAY_BUFFER_SIZE,glContext.DYNAMIC_DRAW);initVertexAttribArray("p",glContext.FLOAT,4,2);initVertexAttribArray("c",glContext.UNSIGNED_BYTE,1,4)}function glPreRender(){if(!glEnable||!glContext)return;glClearCanvas();const s=vec2(2*cameraScale).divide(mainCanvasSize);const rotatedCam=cameraPos.rotate(-cameraAngle);const p=vec2(-1).subtract(rotatedCam.multiply(s));const ca=cos(cameraAngle);const sa=sin(cameraAngle);const transform=[s.x*ca,s.y*sa,0,0,-s.x*sa,s.y*ca,0,0,1,1,1,0,p.x,p.y,0,1];const initUniform=(program,uniform,value)=>{glContext.useProgram(program);const location=glContext.getUniformLocation(program,uniform);glContext.uniformMatrix4fv(location,false,value)};initUniform(glPolyShader,"m",transform);initUniform(glShader,"m",transform);glContext.activeTexture(glContext.TEXTURE0);if(textureInfos[0]){glActiveTexture=textureInfos[0].glTexture;glContext.bindTexture(glContext.TEXTURE_2D,glActiveTexture)}glAdditive=glBatchAdditive=false;glPolyMode=true;glSetInstancedMode()}function glClearCanvas(){if(!glContext)return;glCanvas.width=drawCanvas.width;glCanvas.height=drawCanvas.height;glContext.viewport(0,0,glCanvas.width,glCanvas.height);const color=canvasClearColor;if(color.a>0)glContext.clearColor(color.r,color.g,color.b,color.a);glContext.clear(glContext.COLOR_BUFFER_BIT)}function glSetTexture(texture,wrap=false){if(!glContext||texture===glActiveTexture)return;glFlush();glActiveTexture=texture;glContext.bindTexture(glContext.TEXTURE_2D,glActiveTexture);const wrapMode=wrap?glContext.REPEAT:glContext.CLAMP_TO_EDGE;glContext.texParameteri(glContext.TEXTURE_2D,glContext.TEXTURE_WRAP_S,wrapMode);glContext.texParameteri(glContext.TEXTURE_2D,glContext.TEXTURE_WRAP_T,wrapMode)}function glCompileShader(source,type){if(!glContext)return;const shader=glContext.createShader(type);glContext.shaderSource(shader,source);glContext.compileShader(shader);if(debug&&!glContext.getShaderParameter(shader,glContext.COMPILE_STATUS))throw glContext.getShaderInfoLog(shader);return shader}function glCreateProgram(vsSource,fsSource){if(!glContext)return;const program=glContext.createProgram();glContext.attachShader(program,glCompileShader(vsSource,glContext.VERTEX_SHADER));glContext.attachShader(program,glCompileShader(fsSource,glContext.FRAGMENT_SHADER));glContext.linkProgram(program);if(debug&&!glContext.getProgramParameter(program,glContext.LINK_STATUS))throw glContext.getProgramInfoLog(program);return program}function glCreateTexture(image){if(!glContext)return;const texture=glContext.createTexture();let mipMap=false;if(image&&image.width){glSetTextureData(texture,image);glContext.bindTexture(glContext.TEXTURE_2D,texture);mipMap=!tilesPixelated&&isPowerOfTwo(image.width)&&isPowerOfTwo(image.height)}else{const whitePixel=new Uint8Array([255,255,255,255]);glContext.bindTexture(glContext.TEXTURE_2D,texture);glContext.texImage2D(glContext.TEXTURE_2D,0,glContext.RGBA,1,1,0,glContext.RGBA,glContext.UNSIGNED_BYTE,whitePixel)}const magFilter=tilesPixelated?glContext.NEAREST:glContext.LINEAR;const minFilter=mipMap?glContext.LINEAR_MIPMAP_LINEAR:magFilter;glContext.texParameteri(glContext.TEXTURE_2D,glContext.TEXTURE_MAG_FILTER,magFilter);glContext.texParameteri(glContext.TEXTURE_2D,glContext.TEXTURE_MIN_FILTER,minFilter);if(mipMap)glContext.generateMipmap(glContext.TEXTURE_2D);glContext.bindTexture(glContext.TEXTURE_2D,glActiveTexture);return texture}function glDeleteTexture(texture){if(!glContext)return;glContext.deleteTexture(texture)}function glSetTextureData(texture,image){if(!glContext)return;ASSERT(!!image&&image.width>0,"Invalid image data.");glContext.bindTexture(glContext.TEXTURE_2D,texture);glContext.texImage2D(glContext.TEXTURE_2D,0,glContext.RGBA,glContext.RGBA,glContext.UNSIGNED_BYTE,image);glContext.bindTexture(glContext.TEXTURE_2D,glActiveTexture)}function glRegisterTextureInfo(textureInfo){if(headlessMode)return;glTextureInfos.add(textureInfo);if(!glContext)return;if(textureInfo.glTexture)glSetTextureData(textureInfo.glTexture,textureInfo.image);else textureInfo.glTexture=glCreateTexture(textureInfo.image)}function glUnregisterTextureInfo(textureInfo){if(headlessMode)return;glTextureInfos.delete(textureInfo);const glTexture=textureInfo.glTexture;textureInfo.glTexture=undefined;glDeleteTexture(glTexture)}function glFlush(){if(glEnable&&glContext&&glBatchCount){const destBlend=glBatchAdditive?glContext.ONE:glContext.ONE_MINUS_SRC_ALPHA;glContext.blendFuncSeparate(glContext.SRC_ALPHA,destBlend,glContext.ONE,destBlend);glContext.enable(glContext.BLEND);const byteLength=glBatchCount*(glPolyMode?gl_INDICES_PER_POLY_VERTEX:gl_INDICES_PER_INSTANCE);glContext.bufferSubData(glContext.ARRAY_BUFFER,0,glPositionData,0,byteLength);if(glPolyMode)glContext.drawArrays(glContext.TRIANGLE_STRIP,0,glBatchCount);else glContext.drawArraysInstanced(glContext.TRIANGLE_STRIP,0,4,glBatchCount);drawCount+=glBatchCount;glBatchCount=0}glBatchAdditive=glAdditive}function glCopyToContext(context){if(!glEnable||!glContext)return;glFlush();context.drawImage(glCanvas,0,0)}function glSetAntialias(antialias=true){ASSERT(!glCanvas,"must be called before engineInit");glAntialias=antialias}function glDraw(x,y,sizeX,sizeY,angle=0,uv0X=0,uv0Y=0,uv1X=1,uv1Y=1,rgba=-1,rgbaAdditive=0){if(glBatchCount>=gl_MAX_INSTANCES||glBatchAdditive!==glAdditive)glFlush();glSetInstancedMode();let offset=glBatchCount++*gl_INDICES_PER_INSTANCE;glPositionData[offset++]=x;glPositionData[offset++]=y;glPositionData[offset++]=sizeX;glPositionData[offset++]=sizeY;glPositionData[offset++]=uv0X;glPositionData[offset++]=uv0Y;glPositionData[offset++]=uv1X;glPositionData[offset++]=uv1Y;glColorData[offset++]=rgba;glColorData[offset++]=rgbaAdditive;glPositionData[offset++]=angle}function glDrawPointsTransform(points,rgba,x,y,sx,sy,angle,tristrip=true){const pointsOut=[];for(const p of points){const px=p.x*sx;const py=p.y*sy;const sa=sin(-angle);const ca=cos(-angle);pointsOut.push(vec2(x+ca*px-sa*py,y+sa*px+ca*py))}const drawPoints=tristrip?glPolyStrip(pointsOut):pointsOut;glDrawPoints(drawPoints,rgba)}function glDrawOutlineTransform(points,rgba,lineWidth,x,y,sx,sy,angle,wrap=true){const outlinePoints=glMakeOutline(points,lineWidth,wrap);glDrawPointsTransform(outlinePoints,rgba,x,y,sx,sy,angle,false)}function glDrawPoints(points,rgba){if(!glEnable||points.length<3)return;const vertCount=points.length+2;if(glBatchCount+vertCount>=gl_MAX_POLY_VERTEXES||glBatchAdditive!==glAdditive)glFlush();glSetPolyMode();let offset=glBatchCount*gl_INDICES_PER_POLY_VERTEX;for(let i=vertCount;i--;){const j=clamp(i-1,0,vertCount-3);const point=points[j];glPositionData[offset++]=point.x;glPositionData[offset++]=point.y;glColorData[offset++]=rgba}glBatchCount+=vertCount}function glDrawColoredPoints(points,pointColors){if(!glEnable||points.length<3)return;const vertCount=points.length+2;if(glBatchCount+vertCount>=gl_MAX_POLY_VERTEXES||glBatchAdditive!==glAdditive)glFlush();glSetPolyMode();let offset=glBatchCount*gl_INDICES_PER_POLY_VERTEX;for(let i=vertCount;i--;){const j=clamp(i-1,0,vertCount-3);const point=points[j];const color=pointColors[j];glPositionData[offset++]=point.x;glPositionData[offset++]=point.y;glColorData[offset++]=color}glBatchCount+=vertCount}function glMakeOutline(points,width,wrap=true){if(points.length<2)return[];const halfWidth=width/2;const strip=[];const n=points.length;const e=1e-6;const miterLimit=width*100;for(let i=0;i<n;i++){const prev=points[wrap?(i-1+n)%n:max(i-1,0)];const curr=points[i];const next=points[wrap?(i+1)%n:min(i+1,n-1)];const dx1=curr.x-prev.x;const dy1=curr.y-prev.y;const len1=(dx1*dx1+dy1*dy1)**.5;const dx2=next.x-curr.x;const dy2=next.y-curr.y;const len2=(dx2*dx2+dy2*dy2)**.5;if(len1<e&&len2<e)continue;const nx1=len1>e?-dy1/len1:0;const ny1=len1>e?dx1/len1:0;const nx2=len2>e?-dy2/len2:0;const ny2=len2>e?dx2/len2:0;let nx=nx1+nx2;let ny=ny1+ny2;const nlen=(nx*nx+ny*ny)**.5;if(nlen<e){nx=nx1;ny=ny1}else{nx/=nlen;ny/=nlen;const dot=nx1*nx+ny1*ny;if(dot>e){const miterLength=min(1/dot,miterLimit);nx*=miterLength;ny*=miterLength}}const inner=vec2(curr.x-nx*halfWidth,curr.y-ny*halfWidth);const outer=vec2(curr.x+nx*halfWidth,curr.y+ny*halfWidth);strip.push(inner);strip.push(outer)}if(strip.length>1&&wrap){strip.push(strip[0]);strip.push(strip[1])}return strip}function glPolyStrip(points){if(points.length<3)return[];const cross=(a,b,c)=>(b.x-a.x)*(c.y-a.y)-(b.y-a.y)*(c.x-a.x);const signedArea=poly=>{let area=0;for(let i=poly.length;i--;){const j=(i+1)%poly.length;area+=poly[i].cross(poly[j])}return area};if(signedArea(points)<0)points=points.reverse();const e=1e-9;const pointInTriangle=(p,a,b,c)=>{const c1=cross(a,b,p);const c2=cross(b,c,p);const c3=cross(c,a,p);const negative=(c1<-e?1:0)+(c2<-e?1:0)+(c3<-e?1:0);const positive=(c1>e?1:0)+(c2>e?1:0)+(c3>e?1:0);return!(negative&&positive)};const indices=[];for(let i=0;i<points.length;++i)indices[i]=i;const triangles=[];let attempts=0;const maxAttempts=points.length**2+100;while(indices.length>3&&attempts++<maxAttempts){let foundEar=false;for(let i=0;i<indices.length;i++){const i0=indices[(i+indices.length-1)%indices.length];const i1=indices[i];const i2=indices[(i+1)%indices.length];const a=points[i0],b=points[i1],c=points[i2];if(cross(a,b,c)<e)continue;let hasInside=false;for(let j=0;j<indices.length;j++){const k=indices[j];if(k===i0||k===i1||k===i2)continue;const p=points[k];hasInside=pointInTriangle(p,a,b,c);if(hasInside)break}if(hasInside)continue;triangles.push([i0,i1,i2]);indices.splice(i,1);foundEar=true;break}if(!foundEar){let worstIndex=-1,worstValue=Infinity;for(let i=0;i<indices.length;i++){const i0=indices[(i+indices.length-1)%indices.length];const i1=indices[i];const i2=indices[(i+1)%indices.length];const value=abs(cross(points[i0],points[i1],points[i2]));if(value<worstValue){worstValue=value;worstIndex=i}}if(worstIndex<0)break;const i0=indices[(worstIndex+indices.length-1)%indices.length];const i1=indices[worstIndex];const i2=indices[(worstIndex+1)%indices.length];triangles.push([i0,i1,i2]);indices.splice(worstIndex,1)}}if(indices.length===3)triangles.push([indices[0],indices[1],indices[2]]);if(!triangles.length)return[];const strip=[];let[a0,b0,c0]=triangles[0];strip.push(points[a0],points[b0],points[c0]);for(let i=1;i<triangles.length;i++){const[a,b,c]=triangles[i];strip.push(points[c0],points[a]);strip.push(points[a],points[b],points[c]);c0=c}return strip}let newgrounds;class NewgroundsMedal extends Medal{constructor(id,name,description,icon,src){super(id,name,description,icon,src)}unlock(){super.unlock();newgrounds&&newgrounds.unlockMedal(this.id)}}class NewgroundsPlugin{constructor(app_id,cipher,cryptoJS){ASSERT(!newgrounds,"there can only be one newgrounds object");ASSERT(!cipher||cryptoJS,"must provide cryptojs if there is a cipher");newgrounds=this;this.app_id=app_id;this.cipher=cipher;this.cryptoJS=cryptoJS;this.host=location?location.hostname:"";const url=new URL(location.href);this.session_id=url.searchParams.get("ngio_session_id");if(!this.session_id)return;const medalsResult=this.call("Medal.getList");this.medals=medalsResult?medalsResult.result.data["medals"]:[];debugMedals&&LOG(this.medals);for(const newgroundsMedal of this.medals){const medal=medals[newgroundsMedal["id"]];if(medal){medal.image=new Image;medal.image.src=newgroundsMedal["icon"];medal.name=newgroundsMedal["name"];medal.description=newgroundsMedal["description"];medal.unlocked=newgroundsMedal["unlocked"];medal.difficulty=newgroundsMedal["difficulty"];medal.value=newgroundsMedal["value"];if(medal.value)medal.description=medal.description+` (${medal.value})`}}const scoreboardResult=this.call("ScoreBoard.getBoards");this.scoreboards=scoreboardResult?scoreboardResult.result.data.scoreboards:[];debugMedals&&LOG(this.scoreboards);const keepAliveMS=60*1e3;setInterval(()=>this.call("Gateway.ping",0,true),keepAliveMS)}unlockMedal(id){return this.call("Medal.unlock",{id:id},true)}postScore(id,value){return this.call("ScoreBoard.postScore",{id:id,value:value},true)}getScores(id,user,social=0,skip=0,limit=10){return this.call("ScoreBoard.getScores",{id:id,user:user,social:social,skip:skip,limit:limit})}logView(){return this.call("App.logView",{host:this.host},true)}call(component,parameters,async=false){const call={component:component,parameters:parameters};if(this.cipher){const cryptoJS=this.cryptoJS;const aesKey=cryptoJS["enc"]["Base64"]["parse"](this.cipher);const iv=cryptoJS["lib"]["WordArray"]["random"](16);const encrypted=cryptoJS["AES"]["encrypt"](JSON.stringify(call),aesKey,{iv:iv});call["secure"]=cryptoJS["enc"]["Base64"]["stringify"](iv.concat(encrypted["ciphertext"]));call["parameters"]=0}const input={app_id:this.app_id,session_id:this.session_id,call:call};const formData=new FormData;formData.append("input",JSON.stringify(input));const xmlHttp=new XMLHttpRequest;const url="https://newgrounds.io/gateway_v3.php";xmlHttp.open("POST",url,!debugMedals&&async);try{xmlHttp.send(formData)}catch(e){debugMedals&&LOG("newgrounds call failed",e);return}debugMedals&&LOG(xmlHttp.responseText);return xmlHttp.responseText&&JSON.parse(xmlHttp.responseText)}}let postProcess;class PostProcessPlugin{constructor(shaderCode,includeMainCanvas=true){ASSERT(!postProcess,"Post process already initialized");postProcess=this;if(!shaderCode)shaderCode="void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}";this.shader=undefined;this.texture=undefined;initPostProcess();engineAddPlugin(undefined,postProcessRender,postProcessContextLost,postProcessContextRestored);function initPostProcess(){if(headlessMode)return;if(!glEnable){console.warn("PostProcessPlugin: WebGL not enabled!");return}postProcess.texture=glCreateTexture();postProcess.shader=glCreateProgram("#version 300 es\n"+"precision highp float;"+"in vec2 p;"+"void main(){"+"gl_Position=vec4(p+p-1.,1,1);"+"}","#version 300 es\n"+"precision highp float;"+"uniform sampler2D iChannel0;"+"uniform vec3 iResolution;"+"uniform float iTime;"+"out vec4 c;"+"\n"+shaderCode+"\n"+"void main(){"+"mainImage(c,gl_FragCoord.xy);"+"c.a=1.;"+"}")}function postProcessContextLost(){postProcess.shader=undefined;postProcess.texture=undefined;LOG("PostProcessPlugin: WebGL context lost")}function postProcessContextRestored(){initPostProcess();LOG("PostProcessPlugin: WebGL context restored")}function postProcessRender(){if(headlessMode)return;if(!glEnable)return;glFlush();glContext.activeTexture(glContext.TEXTURE0);glContext.bindTexture(glContext.TEXTURE_2D,postProcess.texture);if(includeMainCanvas){workCanvas.width=mainCanvasSize.x;workCanvas.height=mainCanvasSize.y;glCopyToContext(workContext);workContext.drawImage(mainCanvas,0,0);glContext.texImage2D(glContext.TEXTURE_2D,0,glContext.RGBA,glContext.RGBA,glContext.UNSIGNED_BYTE,workCanvas)}glContext.useProgram(postProcess.shader);glContext.bindBuffer(glContext.ARRAY_BUFFER,glGeometryBuffer);glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL,1);glContext.disable(glContext.BLEND);const vertexByteStride=8;const pLocation=glContext.getAttribLocation(postProcess.shader,"p");glContext.enableVertexAttribArray(pLocation);glContext.vertexAttribPointer(pLocation,2,glContext.FLOAT,false,vertexByteStride,0);const uniformLocation=name=>glContext.getUniformLocation(postProcess.shader,name);glContext.uniform1i(uniformLocation("iChannel0"),0);glContext.uniform1f(uniformLocation("iTime"),time);glContext.uniform3f(uniformLocation("iResolution"),mainCanvas.width,mainCanvas.height,1);glContext.drawArrays(glContext.TRIANGLE_STRIP,0,4)}}}class ZzFXMusic extends Sound{constructor(zzfxMusic){super(undefined);if(!soundEnable||headlessMode)return;this.randomness=0;this.sampleChannels=zzfxM(...zzfxMusic);this.sampleRate=audioDefaultSampleRate}playMusic(volume=1,loop=true){return super.play(undefined,volume,1,0,loop)}}function zzfxM(instruments,patterns,sequence,BPM=125){let i,j,k;let instrumentParameters;let note;let sample;let patternChannel;let notFirstBeat;let stop;let instrument;let attenuation;let outSampleOffset;let isSequenceEnd;let sampleOffset=0;let nextSampleOffset;let sampleBuffer=[];let leftChannelBuffer=[];let rightChannelBuffer=[];let channelIndex=0;let panning=0;let hasMore=1;let sampleCache={};let beatLength=audioDefaultSampleRate/BPM*60>>2;for(;hasMore;channelIndex++){sampleBuffer=[hasMore=notFirstBeat=outSampleOffset=0];sequence.forEach((patternIndex,sequenceIndex)=>{patternChannel=patterns[patternIndex][channelIndex]||[0,0,0];hasMore|=patterns[patternIndex][channelIndex]&&1;nextSampleOffset=outSampleOffset+(patterns[patternIndex][0].length-2-(notFirstBeat?0:1))*beatLength;isSequenceEnd=sequenceIndex===sequence.length-1;for(i=2,k=outSampleOffset;i<patternChannel.length+isSequenceEnd;notFirstBeat=++i){note=patternChannel[i];stop=i===patternChannel.length+isSequenceEnd-1&&isSequenceEnd||instrument!==(patternChannel[0]||0)||note|0;for(j=0;j<beatLength&&notFirstBeat;j++>beatLength-99&&stop&&attenuation<1?attenuation+=1/99:0){sample=(1-attenuation)*sampleBuffer[sampleOffset++]/2||0;leftChannelBuffer[k]=(leftChannelBuffer[k]||0)-sample*panning+sample;rightChannelBuffer[k]=(rightChannelBuffer[k++]||0)+sample*panning+sample}if(note){attenuation=note%1;panning=patternChannel[1]||0;if(note|=0){sampleBuffer=sampleCache[[instrument=patternChannel[sampleOffset=0]||0,note]]=sampleCache[[instrument,note]]||(instrumentParameters=[...instruments[instrument]],instrumentParameters[2]=(instrumentParameters[2]||220)*2**(note/12-1),note>0?zzfxG(...instrumentParameters):[])}}}outSampleOffset=nextSampleOffset})}return[leftChannelBuffer,rightChannelBuffer]}let uiSystem;let uiDebug=0;function uiSetDebug(debugMode){uiDebug=typeof debugMode==="boolean"?debugMode?1:0:debugMode}class UISystemPlugin{constructor(context=mainContext){ASSERT(!uiSystem,"UI system already initialized");uiSystem=this;this.activateOnPress=false;this.defaultColor=WHITE;this.defaultLineColor=BLACK;this.defaultTextColor=BLACK;this.defaultButtonColor=hsl(0,0,.7);this.defaultHoverColor=hsl(0,0,.9);this.defaultDisabledColor=hsl(0,0,.3);this.defaultGradientColor=undefined;this.defaultLineWidth=4;this.defaultCornerRadius=0;this.defaultTextFitScale=.8;this.defaultFont=fontDefault;this.defaultSoundPress=undefined;this.defaultSoundRelease=undefined;this.defaultSoundClick=undefined;this.defaultShadowColor=CLEAR_BLACK;this.defaultShadowBlur=5;this.defaultShadowOffset=vec2(5);this.nativeHeight=0;this.navigationObject=undefined;this.navigationTimer=new Timer(undefined,true);this.navigationDelay=.2;this.navigationDirection=1;this.navigationMode=false;this.uiObjects=[];this.uiContext=context;this.activeObject=undefined;this.hoverObject=undefined;this.lastHoverObject=undefined;this.confirmDialog=undefined;engineAddPlugin(uiUpdate,uiRender);function updateTransforms(o){if(!o.parent)return;o.pos.x=o.localPos.x+o.parent.pos.x;o.pos.y=o.localPos.y+o.parent.pos.y}function uiUpdate(){if(uiSystem.activeObject&&!uiSystem.activeObject.visible)uiSystem.activeObject=undefined;uiSystem.lastHoverObject=uiSystem.hoverObject;uiSystem.hoverObject=undefined;if(mouseWasPressed(0)){uiSystem.navigationMode=false;uiSystem.navigationObject=undefined}const navigableObjects=uiSystem.getNavigableObjects();if(!navigableObjects.length)uiSystem.navigationObject=undefined;else{if(!navigableObjects.includes(uiSystem.navigationObject))uiSystem.navigationObject=undefined;if(!isTouchDevice)if(uiSystem.navigationMode&&!uiSystem.navigationObject){uiSystem.navigationObject=navigableObjects.find(o=>o.navigationAutoSelect)}if(!uiSystem.navigationTimer.active()){const direction=sign(uiSystem.getNavigationDirection());if(direction){let newNavigationObject;if(!uiSystem.navigationObject){newNavigationObject=navigableObjects.find(o=>o.navigationAutoSelect);if(!newNavigationObject){const newIndex=direction>0?0:navigableObjects.length-1;newNavigationObject=navigableObjects[newIndex]}}else{const currentIndex=navigableObjects.indexOf(uiSystem.navigationObject);const newIndex=mod(currentIndex+direction,navigableObjects.length);newNavigationObject=navigableObjects[newIndex]}if(uiSystem.navigationObject!==newNavigationObject){uiSystem.navigationMode=true;uiSystem.hoverObject=undefined;uiSystem.navigationObject=newNavigationObject;uiSystem.navigationTimer.set(uiSystem.navigationDelay);newNavigationObject.soundPress&&newNavigationObject.soundPress.play()}}}if(uiSystem.navigationObject)if(uiSystem.getNavigationWasPressed())uiSystem.navigationObject.navigatePressed()}for(let i=uiSystem.uiObjects.length;i--;){const o=uiSystem.uiObjects[i];o.parent||updateObject(o)}uiSystem.uiObjects=uiSystem.uiObjects.filter(o=>!o.destroyed);function updateObject(o){if(!o.visible)return;updateTransforms(o);for(let i=o.children.length;i--;)updateObject(o.children[i]);o.update()}}function uiRender(){const context=uiSystem.uiContext;context.save();if(uiSystem.nativeHeight){const s=mainCanvasSize.y/uiSystem.nativeHeight;context.translate(-s*mainCanvasSize.x/2,0);context.scale(s,s);context.translate(mainCanvasSize.x/2/s,0)}function renderObject(o){if(!o.visible)return;updateTransforms(o);o.render();for(const c of o.children)renderObject(c)}uiSystem.uiObjects.forEach(o=>o.parent||renderObject(o));if(uiDebug>0){function renderDebug(o,visible=true){visible&&=!!o.visible;updateTransforms(o);o.renderDebug(visible);for(const c of o.children)renderDebug(c,visible)}uiSystem.uiObjects.forEach(o=>o.parent||renderDebug(o))}context.restore()}}drawRect(pos,size,color=WHITE,lineWidth=0,lineColor=BLACK,cornerRadius=0,gradientColor,shadowColor=BLACK,shadowBlur=0,shadowOffset=vec2()){ASSERT(isVector2(pos),"pos must be a vec2");ASSERT(isVector2(size),"size must be a vec2");ASSERT(isColor(color),"color must be a color");ASSERT(isNumber(lineWidth),"lineWidth must be a number");ASSERT(isColor(lineColor),"lineColor must be a color");ASSERT(isNumber(cornerRadius),"cornerRadius must be a number");const context=uiSystem.uiContext;if(gradientColor){const g=context.createLinearGradient(pos.x,pos.y-size.y/2,pos.x,pos.y+size.y/2);const c=color.toString();g.addColorStop(0,c);g.addColorStop(.5,gradientColor.toString());g.addColorStop(1,c);context.fillStyle=g}else context.fillStyle=color.toString();if(shadowBlur||shadowOffset.x||shadowOffset.y)if(shadowColor.a>0){context.shadowColor=shadowColor.toString();context.shadowBlur=shadowBlur;context.shadowOffsetX=shadowOffset.x;context.shadowOffsetY=shadowOffset.y}context.beginPath();if(cornerRadius&&context["roundRect"])context["roundRect"](pos.x-size.x/2,pos.y-size.y/2,size.x,size.y,cornerRadius);else context.rect(pos.x-size.x/2,pos.y-size.y/2,size.x,size.y);context.fill();context.shadowColor="#0000";if(lineWidth&&lineColor.a>0){context.strokeStyle=lineColor.toString();context.lineWidth=lineWidth;context.stroke()}}drawLine(posA,posB,lineWidth=uiSystem.defaultLineWidth,lineColor=uiSystem.defaultLineColor){ASSERT(isVector2(posA),"posA must be a vec2");ASSERT(isVector2(posB),"posB must be a vec2");ASSERT(isNumber(lineWidth),"lineWidth must be a number");ASSERT(isColor(lineColor),"lineColor must be a color");const context=uiSystem.uiContext;context.strokeStyle=lineColor.toString();context.lineWidth=lineWidth;context.beginPath();context.lineTo(posA.x,posA.y);context.lineTo(posB.x,posB.y);context.stroke()}drawTile(pos,size,tileInfo,color=uiSystem.defaultColor,angle=0,mirror=false,shadowColor=BLACK,shadowBlur=0,shadowOffset=vec2()){const context=uiSystem.uiContext;if(shadowBlur||shadowOffset.x||shadowOffset.y)if(shadowColor.a>0){context.shadowColor=shadowColor.toString();context.shadowBlur=shadowBlur;context.shadowOffsetX=shadowOffset.x;context.shadowOffsetY=shadowOffset.y}drawTile(pos,size,tileInfo,color,angle,mirror,CLEAR_BLACK,false,true,context);context.shadowColor="#0000"}drawText(text,pos,size,color=uiSystem.defaultColor,lineWidth=uiSystem.defaultLineWidth,lineColor=uiSystem.defaultLineColor,align="center",font=uiSystem.defaultFont,fontStyle="",applyMaxWidth=true,textShadow=undefined,shadowColor=BLACK,shadowBlur=0,shadowOffset=vec2()){const context=uiSystem.uiContext;if(shadowColor.a>0){if(textShadow)drawTextScreen(text,pos.add(textShadow),size.y,shadowColor,lineWidth,lineColor,align,font,fontStyle,applyMaxWidth?size.x:undefined,0,context);if(shadowBlur||shadowOffset.x||shadowOffset.y){context.shadowColor=shadowColor.toString();context.shadowBlur=shadowBlur;context.shadowOffsetX=shadowOffset.x;context.shadowOffsetY=shadowOffset.y}}drawTextScreen(text,pos,size.y,color,lineWidth,lineColor,align,font,fontStyle,applyMaxWidth?size.x:undefined,0,context);context.shadowColor="#0000"}setupDragAndDrop(onDrop,onDragEnter,onDragLeave,onDragOver){function setCallback(callback,listenerType){function listener(e){e.preventDefault();callback&&callback(e)}document.addEventListener(listenerType,listener)}setCallback(onDrop,"drop");setCallback(onDragEnter,"dragenter");setCallback(onDragLeave,"dragleave");setCallback(onDragOver,"dragover")}screenToNative(pos){if(!uiSystem.nativeHeight)return pos;const s=mainCanvasSize.y/uiSystem.nativeHeight;const sInv=1/s;const p=pos.copy();p.x+=s*mainCanvasSize.x/2;p.x*=sInv;p.y*=sInv;p.x-=sInv*mainCanvasSize.x/2;return p}destroyObjects(){for(const o of this.uiObjects)o.parent||o.destroy();this.uiObjects=this.uiObjects.filter(o=>!o.destroyed);this.activeObject=undefined;this.hoverObject=undefined;this.lastHoverObject=undefined}getNavigableObjects(){function getNavigableRecursive(o){if(!o.visible||o.disabled)return;if(o.isInteractive()&&o.navigationIndex!==undefined)objects.push(o);for(let i=o.children.length;i--;)getNavigableRecursive(o.children[i])}let objects=[];for(let i=uiSystem.uiObjects.length;i--;){const o=uiSystem.uiObjects[i];if(uiSystem.confirmDialog&&o!==uiSystem.confirmDialog)continue;o.parent||getNavigableRecursive(o)}objects.sort((a,b)=>a.navigationIndex-b.navigationIndex);return objects}getNavigationDirection(){const vertical=uiSystem.navigationDirection===1;const both=uiSystem.navigationDirection===2;if(isUsingGamepad){const stick=gamepadStick(0,gamepadPrimary);const dpad=gamepadDpad(gamepadPrimary);if(both)return-(stick.y||dpad.y)||(stick.x||dpad.x);return vertical?-(stick.y||dpad.y):stick.x||dpad.x}const up="ArrowUp",down="ArrowDown",left="ArrowLeft",right="ArrowRight";if(both){return keyIsDown(up)||keyIsDown(left)?-1:keyIsDown(down)||keyIsDown(right)?1:0}const back=vertical?up:left;const forward=vertical?down:right;return keyIsDown(back)?-1:keyIsDown(forward)?1:0}getNavigationOtherDirection(){if(uiSystem.navigationDirection===2)return 0;const vertical=uiSystem.navigationDirection===1;if(isUsingGamepad){const stick=gamepadStick(0,gamepadPrimary);const dpad=gamepadDpad(gamepadPrimary);return!vertical?stick.y||dpad.y:stick.x||dpad.x}const back=!vertical?"ArrowUp":"ArrowLeft";const forward=!vertical?"ArrowDown":"ArrowRight";return keyIsDown(back)?-1:keyIsDown(forward)?1:0}getNavigationWasPressed(){return isUsingGamepad?gamepadWasPressed(0,gamepadPrimary):keyWasPressed("Space")||keyWasPressed("Enter")}showConfirmDialog(text="Are you sure?",yesCallback,noCallback,size=vec2(500,250),exitKey="Escape"){ASSERT(!uiSystem.confirmDialog);const savedNavigationDirection=uiSystem.navigationDirection;uiSystem.navigationDirection=2;const confirmMenu=new UIObject(vec2(),size);uiSystem.confirmDialog=confirmMenu;confirmMenu.onRender=()=>{confirmMenu.pos=uiSystem.screenToNative(mainCanvasSize.scale(.5));const backgroundColor=hsl(0,0,0,.7);uiSystem.drawRect(vec2(),vec2(1e9),backgroundColor)};confirmMenu.onUpdate=()=>{if(keyWasPressed(exitKey))closeMenu()};confirmMenu.isMouseOverlapping=()=>true;const gap=50;const textTitle=new UIText(vec2(0,-50),vec2(size.x-gap,70),text);confirmMenu.addChild(textTitle);const buttonYes=new UIButton(vec2(-80,50),vec2(120,70),"Yes");buttonYes.textHeight=40;buttonYes.navigationIndex=1;buttonYes.hoverColor=hsl(0,1,.5);buttonYes.onClick=()=>{closeMenu();yesCallback&&yesCallback()};confirmMenu.addChild(buttonYes);const buttonNo=new UIButton(vec2(80,50),vec2(120,70),"No");buttonNo.textHeight=40;buttonNo.navigationIndex=2;buttonNo.navigationAutoSelect=true;buttonNo.onClick=()=>{closeMenu();noCallback&&noCallback()};confirmMenu.addChild(buttonNo);function closeMenu(){ASSERT(uiSystem.confirmDialog===confirmMenu);confirmMenu.destroy();uiSystem.confirmDialog=undefined;uiSystem.navigationDirection=savedNavigationDirection;inputClear()}}}class UIObject{constructor(pos=vec2(),size=vec2()){ASSERT(isVector2(pos),"ui object pos must be a vec2");ASSERT(isVector2(size),"ui object size must be a vec2");this.localPos=pos.copy();this.pos=pos.copy();this.size=size.copy();this.color=uiSystem.defaultColor.copy();this.activeColor=undefined;this.text=undefined;this.disabledColor=uiSystem.defaultDisabledColor.copy();this.disabled=false;this.textColor=uiSystem.defaultTextColor.copy();this.hoverColor=uiSystem.defaultHoverColor.copy();this.lineColor=uiSystem.defaultLineColor.copy();this.gradientColor=uiSystem.defaultGradientColor?uiSystem.defaultGradientColor.copy():undefined;this.lineWidth=uiSystem.defaultLineWidth;this.cornerRadius=uiSystem.defaultCornerRadius;this.font=uiSystem.defaultFont;this.fontStyle=undefined;this.textWidth=undefined;this.textHeight=undefined;this.textFitScale=uiSystem.defaultTextFitScale;this.textShadow=undefined;this.textLineColor=uiSystem.defaultLineColor.copy();this.textLineWidth=0;this.visible=true;this.children=[];this.parent=undefined;this.extraTouchSize=0;this.soundPress=uiSystem.defaultSoundPress;this.soundRelease=uiSystem.defaultSoundRelease;this.soundClick=uiSystem.defaultSoundClick;this.interactive=false;this.dragActivate=false;this.canBeHover=true;this.shadowColor=uiSystem.defaultShadowColor?.copy();this.shadowBlur=uiSystem.defaultShadowBlur;this.shadowOffset=uiSystem.defaultShadowOffset?.copy();this.navigationIndex=undefined;this.navigationAutoSelect=false;uiSystem.uiObjects.push(this)}addChild(child){ASSERT(!child.parent&&!this.children.includes(child));this.children.push(child);child.parent=this;return child}removeChild(child){ASSERT(child.parent===this&&this.children.includes(child));this.children.splice(this.children.indexOf(child),1);child.parent=undefined}destroy(){if(this.destroyed)return;this.destroyed=1;this.parent&&this.parent.removeChild(this);for(const child of this.children){child.parent=0;child.destroy()}}isMouseOverlapping(){if(!mouseInWindow)return false;const size=!isTouchDevice?this.size:this.size.add(vec2(this.extraTouchSize||0));const pos=uiSystem.screenToNative(mousePosScreen);return isOverlapping(this.pos,size,pos)}update(){this.onUpdate();if(this.disabled&&this==uiSystem.activeObject)uiSystem.activeObject=undefined;const wasHover=uiSystem.lastHoverObject===this;const isActive=this.isActiveObject();const mouseDown=mouseIsDown(0);const mousePress=this.dragActivate?mouseDown:mouseWasPressed(0);if(this.canBeHover)if(!uiSystem.navigationMode)if(mousePress||isActive||!mouseDown&&!isTouchDevice)if(!uiSystem.hoverObject&&this.isMouseOverlapping())uiSystem.hoverObject=this;if(this.isHoverObject()){if(!this.disabled){if(mousePress){if(this.interactive){if(!this.dragActivate||(!wasHover||mouseWasPressed(0)))this.onPress();this.soundPress&&this.soundPress.play();if(uiSystem.activeObject&&!isActive)uiSystem.activeObject.onRelease();uiSystem.activeObject=this;if(uiSystem.activateOnPress){this.onClick();if(!this.soundPress&&this.soundClick)this.soundClick.play()}}}if(!uiSystem.activateOnPress)if(!mouseDown&&this.isActiveObject()&&this.interactive){this.onClick();this.soundClick&&this.soundClick.play()}}mousePress&&inputClearKey(0,0,0,1,0)}if(isActive)if(!mouseDown||this.dragActivate&&!this.isHoverObject()){this.onRelease();this.soundRelease&&this.soundRelease.play();uiSystem.activeObject=undefined}if(this.isHoverObject()!==wasHover)this.isHoverObject()?this.onEnter():this.onLeave()}render(){this.onRender();if(!this.size.x||!this.size.y)return;const isNavigationObject=this.isNavigationObject();const lineColor=isNavigationObject?this.color:this.interactive&&this.isActiveObject()&&!this.disabled?this.color:this.lineColor;const color=isNavigationObject?this.hoverColor:this.disabled?this.disabledColor:this.interactive?this.isHoverObject()?this.hoverColor:this.isActiveObject()?this.activeColor||this.color:this.color:this.color;const lineWidth=this.lineWidth*(isNavigationObject?1.5:1);uiSystem.drawRect(this.pos,this.size,color,lineWidth,lineColor,this.cornerRadius,this.gradientColor,this.shadowColor,this.shadowBlur,this.shadowOffset)}getTextSize(){return vec2(this.textWidth||this.textFitScale*this.size.x,this.textHeight||this.textFitScale*this.size.y)}navigatePressed(){this.onClick();this.soundClick&&this.soundClick.play()}isHoverObject(){return uiSystem.hoverObject===this}isActiveObject(){return uiSystem.activeObject===this}isNavigationObject(){return uiSystem.navigationObject===this}isInteractive(){return this.interactive&&this.visible&&!this.disabled}toString(){if(!debug)return;let text="type = "+this.constructor.name;if(this.text)text+="\ntext = "+this.text;if(this.pos.x||this.pos.y)text+="\npos = "+this.pos;if(this.localPos.x||this.localPos.y)text+="localPos = "+this.localPos;if(this.size.x||this.size.y)text+="\nsize = "+this.size;if(this.color)text+="\ncolor = "+this.color;return text}renderDebug(visible=true){const color=!visible?GREEN:this.isHoverObject()?YELLOW:this.disabled?PURPLE:this.interactive?RED:BLUE;uiSystem.drawRect(this.pos,this.size,CLEAR_BLACK,4,color)}onUpdate(){}onRender(){}onEnter(){}onLeave(){}onPress(){}onRelease(){}onClick(){}onChange(){}}class UIText extends UIObject{constructor(pos,size,text="",align="center",font=uiSystem.defaultFont){super(pos,size);ASSERT(isString(text),"ui text must be a string");ASSERT(["left","center","right"].includes(align),"ui text align must be left, center, or right");ASSERT(isString(font),"ui text font must be a string");this.text=text;this.align=align;this.font=font;this.canBeHover=false;this.color=CLEAR_BLACK;this.shadowColor=CLEAR_BLACK;this.gradientColor=undefined;this.lineWidth=0;this.textFitScale=1}render(){super.render();const textSize=this.getTextSize();uiSystem.drawText(this.text,this.pos,textSize,this.textColor,this.textLineWidth,this.textLineColor,this.align,this.font,this.fontStyle,true,this.textShadow,this.shadowColor,this.shadowBlur,this.shadowOffset)}}class UITile extends UIObject{constructor(pos,size,tileInfo,color=WHITE,angle=0,mirror=false){super(pos,size);ASSERT(tileInfo instanceof TileInfo,"ui tile tileInfo must be a TileInfo");ASSERT(isColor(color),"ui tile color must be a color");ASSERT(isNumber(angle),"ui tile angle must be a number");this.tileInfo=tileInfo;this.angle=angle;this.mirror=mirror;this.color=color.copy();this.shadowColor=CLEAR_BLACK}render(){uiSystem.drawTile(this.pos,this.size,this.tileInfo,this.color,this.angle,this.mirror,this.shadowColor,this.shadowBlur,this.shadowOffset)}}class UIButton extends UIObject{constructor(pos,size,text="",color=uiSystem.defaultButtonColor){super(pos,size);ASSERT(isString(text),"ui button must be a string");ASSERT(isColor(color),"ui button color must be a color");this.textOffset=vec2();this.text=text;this.color=color.copy();this.interactive=true}render(){super.render();const textSize=this.getTextSize();uiSystem.drawText(this.text,this.pos.add(this.textOffset),textSize,this.textColor,this.textLineWidth,this.textLineColor,this.align,this.font,this.fontStyle,true,this.textShadow)}}class UICheckbox extends UIObject{constructor(pos,size,checked=false,text="",color=uiSystem.defaultButtonColor){super(pos,size);ASSERT(isString(text),"ui checkbox must be a string");ASSERT(isColor(color),"ui checkbox color must be a color");this.checked=checked;this.text=text;this.color=color.copy();this.interactive=true}onClick(){this.checked=!this.checked;this.onChange()}render(){super.render();if(this.checked){const p=this.cornerRadius/min(this.size.x,this.size.y)*2;const length=lerp(1,2**.5/2,p)/2;let s=this.size.scale(length);uiSystem.drawLine(this.pos.add(s.multiply(vec2(-1))),this.pos.add(s.multiply(vec2(1))),this.lineWidth,this.lineColor);uiSystem.drawLine(this.pos.add(s.multiply(vec2(-1,1))),this.pos.add(s.multiply(vec2(1,-1))),this.lineWidth,this.lineColor)}const textSize=this.getTextSize();const pos=this.pos.add(vec2(this.size.x,0));uiSystem.drawText(this.text,pos,textSize,this.textColor,this.textLineWidth,this.textLineColor,"left",this.font,this.fontStyle,false,this.textShadow)}}class UIScrollbar extends UIObject{constructor(pos,size,value=.5,text="",color=uiSystem.defaultButtonColor,handleColor=WHITE){super(pos,size);ASSERT(isNumber(value),"ui scrollbar value must be a number");ASSERT(isString(text),"ui scrollbar must be a string");ASSERT(isColor(color),"ui scrollbar color must be a color");ASSERT(isColor(handleColor),"ui scrollbar handleColor must be a color");this.value=value;this.handleColor=handleColor.copy();this.text=text;this.color=color.copy();this.interactive=true}update(){super.update();if(!this.interactive)return;const oldValue=this.value;if(this.isActiveObject()){const isHorizontal=this.size.x>this.size.y;const handleSize=isHorizontal?this.size.y:this.size.x;const barSize=isHorizontal?this.size.x:this.size.y;const centerPos=isHorizontal?this.pos.x:this.pos.y;const handleWidth=barSize-handleSize;const p1=centerPos-handleWidth/2;const p2=centerPos+handleWidth/2;const p=uiSystem.screenToNative(mousePosScreen);this.value=isHorizontal?percent(p.x,p1,p2):percent(p.y,p2,p1)}else if(this.isNavigationObject()){const direction=uiSystem.getNavigationOtherDirection();if(!uiSystem.navigationTimer.active())this.value=clamp(this.value+direction*.01)}this.value===oldValue||this.onChange()}render(){super.render();const isHorizontal=this.size.x>this.size.y;const handleSize=isHorizontal?this.size.y:this.size.x;const barSize=isHorizontal?this.size.x:this.size.y;const centerPos=isHorizontal?this.pos.x:this.pos.y;const handleWidth=barSize-handleSize;const p1=centerPos-handleWidth/2;const p2=centerPos+handleWidth/2;const handlePos=isHorizontal?vec2(lerp(p1,p2,this.value),this.pos.y):vec2(this.pos.x,lerp(p2,p1,this.value));const handleColor=this.disabled?this.disabledColor:this.handleColor;uiSystem.drawRect(handlePos,vec2(handleSize),handleColor,this.lineWidth,this.lineColor,this.cornerRadius,this.gradientColor);const textSize=this.getTextSize();uiSystem.drawText(this.text,this.pos,textSize,this.textColor,this.textLineWidth,this.textLineColor,this.align,this.font,this.fontStyle,true,this.textShadow)}navigatePressed(){this.value=this.value?0:1;this.onRelease();super.navigatePressed()}}class UIVideo extends UIObject{constructor(pos,size,src,autoplay=false,loop=false,volume=1){super(pos,size||vec2());ASSERT(isString(src),"video src must be a string");ASSERT(isNumber(volume),"video volume must be a number");this.color=BLACK;this.cornerRadius=0;this.volume=volume;this.video=document.createElement("video");this.video.loop=loop;this.video.volume=clamp(volume*soundVolume);this.video.muted=!soundEnable;this.video.style.display="none";this.video.src=src;document.body.appendChild(this.video);autoplay&&this.play()}play(){const promise=this.video.play();promise?.catch(()=>{});return promise}pause(){this.video.pause()}stop(){this.video.pause();this.video.currentTime=0}isLoading(){return this.video.readyState<this.video.HAVE_CURRENT_DATA}isPaused(){return this.video.paused}isPlaying(){return!this.isPaused()&&!this.hasEnded()&&!this.isLoading()}hasEnded(){return this.video.ended}setVolume(volume){this.volume=volume;this.video.volume=clamp(volume*soundVolume)}setPlaybackRate(rate){this.video.playbackRate=rate}getCurrentTime(){return this.video.currentTime||0}getDuration(){return this.video.duration||0}getVideoSize(){return vec2(this.video.videoWidth,this.video.videoHeight)}setTime(time){this.video.currentTime=clamp(time,0,this.getDuration())}update(){super.update();this.video.volume=clamp(this.volume*soundVolume)}render(){super.render();if(this.isLoading())return;const context=uiSystem.uiContext;const s=this.size;context.save();context.translate(this.pos.x,this.pos.y);context.drawImage(this.video,-s.x/2,-s.y/2,s.x,s.y);context.restore()}destroy(){if(this.destroyed)return;this.video.pause();this.video.remove();super.destroy()}}let box2d;let box2dDebug=false;function box2dSetDebug(enable){box2dDebug=enable}class Box2dObject extends EngineObject{constructor(pos=vec2(),size=vec2(),tileInfo,angle=0,color,bodyType=box2d.bodyTypeDynamic,renderOrder=0){super(pos,size,tileInfo,angle,color,renderOrder);const bodyDef=new box2d.instance.b2BodyDef;bodyDef.set_type(bodyType);bodyDef.set_position(box2d.vec2dTo(pos));bodyDef.set_angle(-angle);this.body=box2d.world.CreateBody(bodyDef);this.body.object=this;this.lineColor=BLACK;box2d.objects.push(this);this.edgeLists=[];this.edgeLoops=[]}destroy(){if(this.destroyed)return;ASSERT(this.body,"Box2dObject has no body to destroy");box2d.world.DestroyBody(this.body);super.destroy()}updatePhysics(){}render(){if(this.tileInfo)super.render();else this.drawFixtures(this.color,this.lineColor,this.lineWidth)}renderDebugInfo(){const isAsleep=!this.getIsAwake();const isStatic=this.getBodyType()===box2d.bodyTypeStatic;const color=rgb(isAsleep?1:0,isAsleep?1:0,isStatic?1:0,.5);this.drawFixtures(color)}drawFixtures(color=WHITE,lineColor=BLACK,lineWidth=.1,context){this.getFixtureList().forEach(fixture=>{const shape=box2d.castObjectType(fixture.GetShape());if(shape.GetType()!==box2d.instance.b2Shape.e_edge){box2d.drawFixture(fixture,this.pos,this.angle,color,lineColor,lineWidth,context)}});this.edgeLists.forEach(points=>drawLineList(points,lineWidth,lineColor,false,this.pos,this.angle));this.edgeLoops.forEach(points=>drawLineList(points,lineWidth,lineColor,true,this.pos,this.angle))}beginContact(otherObject){}endContact(otherObject){}addShape(shape,density=1,friction=.2,restitution=0,isSensor=false){ASSERT(isNumber(density),"density must be a number");ASSERT(isNumber(friction),"friction must be a number");ASSERT(isNumber(restitution),"restitution must be a number");const fd=new box2d.instance.b2FixtureDef;fd.set_shape(shape);fd.set_density(density);fd.set_friction(friction);fd.set_restitution(restitution);fd.set_isSensor(isSensor);return this.body.CreateFixture(fd)}addBox(size=vec2(1),offset=vec2(),angle=0,density,friction,restitution,isSensor){ASSERT(isVector2(size),"size must be a Vector2");ASSERT(size.x>0&&size.y>0,"size must be positive");ASSERT(isVector2(offset),"offset must be a Vector2");ASSERT(isNumber(angle),"angle must be a number");const shape=new box2d.instance.b2PolygonShape;shape.SetAsBox(size.x/2,size.y/2,box2d.vec2dTo(offset),angle);return this.addShape(shape,density,friction,restitution,isSensor)}addPoly(points,density,friction,restitution,isSensor){ASSERT(isArray(points),"points must be an array");function box2dCreatePolygonShape(points){function box2dCreatePointList(points){const buffer=box2d.instance._malloc(points.length*8);for(let i=0,offset=0;i<points.length;++i){box2d.instance.HEAPF32[buffer+offset>>2]=points[i].x;offset+=4;box2d.instance.HEAPF32[buffer+offset>>2]=points[i].y;offset+=4}return box2d.instance.wrapPointer(buffer,box2d.instance.b2Vec2)}ASSERT(3<=points.length&&points.length<=8);const shape=new box2d.instance.b2PolygonShape;const box2dPoints=box2dCreatePointList(points);shape.Set(box2dPoints,points.length);return shape}const shape=box2dCreatePolygonShape(points);return this.addShape(shape,density,friction,restitution,isSensor)}addRegularPoly(diameter=1,sides=8,density,friction,restitution,isSensor){ASSERT(isNumber(diameter)&&diameter>0,"diameter must be a positive number");ASSERT(isNumber(sides)&&sides>2,"sides must be a positive number greater than 2");const points=[];const radius=diameter/2;for(let i=sides;i--;)points.push(vec2(radius,0).rotate((i+.5)/sides*PI*2));return this.addPoly(points,density,friction,restitution,isSensor)}addRandomPoly(diameter=1,density,friction,restitution,isSensor){ASSERT(isNumber(diameter)&&diameter>0,"diameter must be a positive number");const sides=randInt(3,9);const points=[];const radius=diameter/2;for(let i=sides;i--;)points.push(vec2(rand(radius/2,radius*1.5),0).rotate(i/sides*PI*2));return this.addPoly(points,density,friction,restitution,isSensor)}addCircle(diameter=1,offset=vec2(),density,friction,restitution,isSensor){ASSERT(isNumber(diameter)&&diameter>0,"diameter must be a positive number");ASSERT(isVector2(offset),"offset must be a Vector2");const shape=new box2d.instance.b2CircleShape;shape.set_m_p(box2d.vec2dTo(offset));shape.set_m_radius(diameter/2);return this.addShape(shape,density,friction,restitution,isSensor)}addEdge(point1,point2,density,friction,restitution,isSensor){ASSERT(isVector2(point1),"point1 must be a Vector2");ASSERT(isVector2(point2),"point2 must be a Vector2");const shape=new box2d.instance.b2EdgeShape;shape.Set(box2d.vec2dTo(point1),box2d.vec2dTo(point2));return this.addShape(shape,density,friction,restitution,isSensor)}addEdgeList(points,density,friction,restitution,isSensor){ASSERT(isArray(points),"points must be an array");const fixtures=[],edgePoints=[];for(let i=0;i<points.length-1;++i){const shape=new box2d.instance.b2EdgeShape;points[i-1]&&shape.set_m_vertex0(box2d.vec2dTo(points[i-1]));points[i+0]&&shape.set_m_vertex1(box2d.vec2dTo(points[i+0]));points[i+1]&&shape.set_m_vertex2(box2d.vec2dTo(points[i+1]));points[i+2]&&shape.set_m_vertex3(box2d.vec2dTo(points[i+2]));const f=this.addShape(shape,density,friction,restitution,isSensor);fixtures.push(f);edgePoints.push(points[i].copy())}edgePoints.push(points[points.length-1].copy());this.edgeLists.push(edgePoints);return fixtures}addEdgeLoop(points,density,friction,restitution,isSensor){ASSERT(isArray(points),"points must be an array");const fixtures=[],edgePoints=[];const getPoint=i=>points[mod(i,points.length)];for(let i=0;i<points.length;++i){const shape=new box2d.instance.b2EdgeShape;shape.set_m_vertex0(box2d.vec2dTo(getPoint(i-1)));shape.set_m_vertex1(box2d.vec2dTo(getPoint(i+0)));shape.set_m_vertex2(box2d.vec2dTo(getPoint(i+1)));shape.set_m_vertex3(box2d.vec2dTo(getPoint(i+2)));const f=this.addShape(shape,density,friction,restitution,isSensor);fixtures.push(f);i<points.length&&edgePoints.push(points[i].copy())}this.edgeLoops.push(edgePoints);return fixtures}getCenterOfMass(){return box2d.vec2From(this.body.GetWorldCenter())}getLinearVelocity(){return box2d.vec2From(this.body.GetLinearVelocity())}getAngularVelocity(){return this.body.GetAngularVelocity()}getMass(){return this.body.GetMass()}getInertia(){return this.body.GetInertia()}getIsAwake(){return this.body.IsAwake()}getBodyType(){return this.body.GetType()}getSpeed(){return this.getLinearVelocity().length()}setTransform(pos,angle){this.pos=pos;this.angle=angle;this.body.SetTransform(box2d.vec2dTo(pos),angle)}setPosition(pos){this.setTransform(pos,this.body.GetAngle())}setAngle(angle){this.setTransform(box2d.vec2From(this.body.GetPosition()),-angle)}setLinearVelocity(velocity){this.body.SetLinearVelocity(box2d.vec2dTo(velocity))}setAngularVelocity(angularVelocity){this.body.SetAngularVelocity(angularVelocity)}setLinearDamping(damping){this.body.SetLinearDamping(damping)}setAngularDamping(damping){this.body.SetAngularDamping(damping)}setGravityScale(scale=1){this.body.SetGravityScale(this.gravityScale=scale)}setBullet(isBullet=true){this.body.SetBullet(isBullet)}setAwake(isAwake=true){this.body.SetAwake(isAwake)}setBodyType(type){this.body.SetType(type)}setSleepingAllowed(isAllowed=true){this.body.SetSleepingAllowed(isAllowed)}setFixedRotation(isFixed=true){this.body.SetFixedRotation(isFixed)}setCenterOfMass(center){this.setMassData(center)}setMass(mass){this.setMassData(undefined,mass)}setMomentOfInertia(momentOfInertia){this.setMassData(undefined,undefined,momentOfInertia)}resetMassData(){this.body.ResetMassData()}setMassData(localCenter,mass,momentOfInertia){const data=new box2d.instance.b2MassData;this.body.GetMassData(data);localCenter&&data.set_center(box2d.vec2dTo(localCenter));mass&&data.set_mass(mass);momentOfInertia&&data.set_I(momentOfInertia);this.body.SetMassData(data)}setFilterData(categoryBits=0,ignoreCategoryBits=0,groupIndex=0){this.getFixtureList().forEach(fixture=>{const filter=fixture.GetFilterData();filter.set_categoryBits(categoryBits);filter.set_maskBits(65535&~ignoreCategoryBits);filter.set_groupIndex(groupIndex)})}setSensor(isSensor=true){this.getFixtureList().forEach(f=>f.SetSensor(isSensor))}applyForce(force,pos){pos||=this.getCenterOfMass();this.setAwake();this.body.ApplyForce(box2d.vec2dTo(force),box2d.vec2dTo(pos))}applyAcceleration(acceleration,pos){pos||=this.getCenterOfMass();this.setAwake();this.body.ApplyLinearImpulse(box2d.vec2dTo(acceleration),box2d.vec2dTo(pos))}applyTorque(torque){this.setAwake();this.body.ApplyTorque(torque)}applyAngularAcceleration(acceleration){this.setAwake();this.body.ApplyAngularImpulse(acceleration)}hasFixtures(){return!box2d.isNull(this.body.GetFixtureList())}getFixtureList(){const fixtures=[];for(let fixture=this.body.GetFixtureList();!box2d.isNull(fixture);){fixtures.push(fixture);fixture=fixture.GetNext()}return fixtures}hasJoints(){return!box2d.isNull(this.body.GetJointList())}getJointList(){const joints=[];for(let joint=this.body.GetJointList();!box2d.isNull(joint);){joints.push(joint);joint=joint.get_next()}return joints}}class Box2dRaycastResult{constructor(fixture,point,normal,fraction){this.object=fixture.GetBody().object;this.fixture=fixture;this.point=point;this.normal=normal;this.fraction=fraction}}class Box2dJoint{constructor(jointDef){this.box2dJoint=box2d.castObjectType(box2d.world.CreateJoint(jointDef))}destroy(){box2d.world.DestroyJoint(this.box2dJoint);this.box2dJoint=0}getObjectA(){return this.box2dJoint.GetBodyA().object}getObjectB(){return this.box2dJoint.GetBodyB().object}getAnchorA(){return box2d.vec2From(this.box2dJoint.GetAnchorA())}getAnchorB(){return box2d.vec2From(this.box2dJoint.GetAnchorB())}getReactionForce(time){return box2d.vec2From(this.box2dJoint.GetReactionForce(1/time))}getReactionTorque(time){return this.box2dJoint.GetReactionTorque(1/time)}getCollideConnected(){return this.box2dJoint.getCollideConnected()}isActive(){return this.box2dJoint.IsActive()}}class Box2dTargetJoint extends Box2dJoint{constructor(object,fixedObject,worldPos){object.setAwake();const jointDef=new box2d.instance.b2MouseJointDef;jointDef.set_bodyA(fixedObject.body);jointDef.set_bodyB(object.body);jointDef.set_target(box2d.vec2dTo(worldPos));jointDef.set_maxForce(2e3*object.getMass());super(jointDef)}setTarget(pos){this.box2dJoint.SetTarget(box2d.vec2dTo(pos))}getTarget(){return box2d.vec2From(this.box2dJoint.GetTarget())}setMaxForce(force){this.box2dJoint.SetMaxForce(force)}getMaxForce(){return this.box2dJoint.GetMaxForce()}setFrequency(hz){this.box2dJoint.SetFrequency(hz)}getFrequency(){return this.box2dJoint.GetFrequency()}}class Box2dDistanceJoint extends Box2dJoint{constructor(objectA,objectB,anchorA,anchorB,collide=false){anchorA||=box2d.vec2From(objectA.body.GetPosition());anchorB||=box2d.vec2From(objectB.body.GetPosition());const localAnchorA=objectA.worldToLocal(anchorA);const localAnchorB=objectB.worldToLocal(anchorB);const jointDef=new box2d.instance.b2DistanceJointDef;jointDef.set_bodyA(objectA.body);jointDef.set_bodyB(objectB.body);jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));jointDef.set_length(anchorA.distance(anchorB));jointDef.set_collideConnected(collide);super(jointDef)}getLocalAnchorA(){return box2d.vec2From(this.box2dJoint.GetLocalAnchorA())}getLocalAnchorB(){return box2d.vec2From(this.box2dJoint.GetLocalAnchorB())}setLength(length){this.box2dJoint.SetLength(length)}getLength(){return this.box2dJoint.GetLength()}setFrequency(hz){this.box2dJoint.SetFrequency(hz)}getFrequency(){return this.box2dJoint.GetFrequency()}setDampingRatio(ratio){this.box2dJoint.SetDampingRatio(ratio)}getDampingRatio(){return this.box2dJoint.GetDampingRatio()}}class Box2dPinJoint extends Box2dDistanceJoint{constructor(objectA,objectB,pos=objectA.pos,collide=false){super(objectA,objectB,undefined,pos,collide)}}class Box2dRopeJoint extends Box2dJoint{constructor(objectA,objectB,anchorA,anchorB,extraLength=0,collide=false){anchorA||=box2d.vec2From(objectA.body.GetPosition());anchorB||=box2d.vec2From(objectB.body.GetPosition());const localAnchorA=objectA.worldToLocal(anchorA);const localAnchorB=objectB.worldToLocal(anchorB);const jointDef=new box2d.instance.b2RopeJointDef;jointDef.set_bodyA(objectA.body);jointDef.set_bodyB(objectB.body);jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));jointDef.set_maxLength(anchorA.distance(anchorB)+extraLength);jointDef.set_collideConnected(collide);super(jointDef)}getLocalAnchorA(){return box2d.vec2From(this.box2dJoint.GetLocalAnchorA())}getLocalAnchorB(){return box2d.vec2From(this.box2dJoint.GetLocalAnchorB())}setMaxLength(length){this.box2dJoint.SetMaxLength(length)}getMaxLength(){return this.box2dJoint.GetMaxLength()}}class Box2dRevoluteJoint extends Box2dJoint{constructor(objectA,objectB,anchor,collide=false){anchor||=box2d.vec2From(objectB.body.GetPosition());const localAnchorA=objectA.worldToLocal(anchor);const localAnchorB=objectB.worldToLocal(anchor);const jointDef=new box2d.instance.b2RevoluteJointDef;jointDef.set_bodyA(objectA.body);jointDef.set_bodyB(objectB.body);jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));jointDef.set_referenceAngle(objectA.body.GetAngle()-objectB.body.GetAngle());jointDef.set_collideConnected(collide);super(jointDef)}getLocalAnchorA(){return box2d.vec2From(this.box2dJoint.GetLocalAnchorA())}getLocalAnchorB(){return box2d.vec2From(this.box2dJoint.GetLocalAnchorB())}getReferenceAngle(){return this.box2dJoint.GetReferenceAngle()}getJointAngle(){return this.box2dJoint.GetJointAngle()}getJointSpeed(){return this.box2dJoint.GetJointSpeed()}isLimitEnabled(){return this.box2dJoint.IsLimitEnabled()}enableLimit(enable=true){return this.box2dJoint.enableLimit(enable)}getLowerLimit(){return this.box2dJoint.GetLowerLimit()}getUpperLimit(){return this.box2dJoint.GetUpperLimit()}setLimits(min,max){return this.box2dJoint.SetLimits(min,max)}isMotorEnabled(){return this.box2dJoint.IsMotorEnabled()}enableMotor(enable=true){return this.box2dJoint.EnableMotor(enable)}setMotorSpeed(speed){return this.box2dJoint.SetMotorSpeed(speed)}getMotorSpeed(){return this.box2dJoint.GetMotorSpeed()}setMaxMotorTorque(torque){return this.box2dJoint.SetMaxMotorTorque(torque)}getMaxMotorTorque(){return this.box2dJoint.GetMaxMotorTorque()}getMotorTorque(time){return this.box2dJoint.GetMotorTorque(1/time)}}class Box2dGearJoint extends Box2dJoint{constructor(objectA,objectB,joint1,joint2,ratio=1){const jointDef=new box2d.instance.b2GearJointDef;jointDef.set_bodyA(objectA.body);jointDef.set_bodyB(objectB.body);jointDef.set_joint1(joint1.box2dJoint);jointDef.set_joint2(joint2.box2dJoint);jointDef.set_ratio(ratio);super(jointDef);this.joint1=joint1;this.joint2=joint2}getJoint1(){return this.joint1}getJoint2(){return this.joint2}setRatio(ratio){return this.box2dJoint.SetRatio(ratio)}getRatio(){return this.box2dJoint.GetRatio()}}class Box2dPrismaticJoint extends Box2dJoint{constructor(objectA,objectB,anchor,worldAxis=vec2(0,1),collide=false){anchor||=box2d.vec2From(objectB.body.GetPosition());const localAnchorA=objectA.worldToLocal(anchor);const localAnchorB=objectB.worldToLocal(anchor);const localAxisA=objectB.worldToLocalVector(worldAxis);const jointDef=new box2d.instance.b2PrismaticJointDef;jointDef.set_bodyA(objectA.body);jointDef.set_bodyB(objectB.body);jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));jointDef.set_localAxisA(box2d.vec2dTo(localAxisA));jointDef.set_referenceAngle(objectA.body.GetAngle()-objectB.body.GetAngle());jointDef.set_collideConnected(collide);super(jointDef)}getLocalAnchorA(){return box2d.vec2From(this.box2dJoint.GetLocalAnchorA())}getLocalAnchorB(){return box2d.vec2From(this.box2dJoint.GetLocalAnchorB())}getLocalAxisA(){return box2d.vec2From(this.box2dJoint.GetLocalAxisA())}getReferenceAngle(){return this.box2dJoint.GetReferenceAngle()}getJointTranslation(){return this.box2dJoint.GetJointTranslation()}getJointSpeed(){return this.box2dJoint.GetJointSpeed()}isLimitEnabled(){return this.box2dJoint.IsLimitEnabled()}enableLimit(enable=true){return this.box2dJoint.enableLimit(enable)}getLowerLimit(){return this.box2dJoint.GetLowerLimit()}getUpperLimit(){return this.box2dJoint.GetUpperLimit()}setLimits(min,max){return this.box2dJoint.SetLimits(min,max)}isMotorEnabled(){return this.box2dJoint.IsMotorEnabled()}enableMotor(enable=true){return this.box2dJoint.EnableMotor(enable)}setMotorSpeed(speed){return this.box2dJoint.SetMotorSpeed(speed)}getMotorSpeed(){return this.box2dJoint.GetMotorSpeed()}setMaxMotorForce(force){return this.box2dJoint.SetMaxMotorForce(force)}getMaxMotorForce(){return this.box2dJoint.GetMaxMotorForce()}getMotorForce(time){return this.box2dJoint.GetMotorForce(1/time)}}class Box2dStaticObject extends Box2dObject{constructor(pos,size,tileInfo,angle=0,color,renderOrder=0){const bodyType=box2d.bodyTypeStatic;super(pos,size,tileInfo,angle,color,bodyType,renderOrder)}}class Box2dKiematicObject extends Box2dObject{constructor(pos,size,tileInfo,angle=0,color,renderOrder=0){const bodyType=box2d.bodyTypeKinematic;super(pos,size,tileInfo,angle,color,bodyType,renderOrder)}}class Box2dWheelJoint extends Box2dJoint{constructor(objectA,objectB,anchor,worldAxis=vec2(0,1),collide=false){anchor||=box2d.vec2From(objectB.body.GetPosition());const localAnchorA=objectA.worldToLocal(anchor);const localAnchorB=objectB.worldToLocal(anchor);const localAxisA=objectB.worldToLocalVector(worldAxis);const jointDef=new box2d.instance.b2WheelJointDef;jointDef.set_bodyA(objectA.body);jointDef.set_bodyB(objectB.body);jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));jointDef.set_localAxisA(box2d.vec2dTo(localAxisA));jointDef.set_collideConnected(collide);super(jointDef)}getLocalAnchorA(){return box2d.vec2From(this.box2dJoint.GetLocalAnchorA())}getLocalAnchorB(){return box2d.vec2From(this.box2dJoint.GetLocalAnchorB())}getLocalAxisA(){return box2d.vec2From(this.box2dJoint.GetLocalAxisA())}getJointTranslation(){return this.box2dJoint.GetJointTranslation()}getJointSpeed(){return this.box2dJoint.GetJointSpeed()}isMotorEnabled(){return this.box2dJoint.IsMotorEnabled()}enableMotor(enable=true){return this.box2dJoint.EnableMotor(enable)}setMotorSpeed(speed){return this.box2dJoint.SetMotorSpeed(speed)}getMotorSpeed(){return this.box2dJoint.GetMotorSpeed()}setMaxMotorTorque(torque){return this.box2dJoint.SetMaxMotorTorque(torque)}getMaxMotorTorque(){return this.box2dJoint.GetMaxMotorTorque()}getMotorTorque(time){return this.box2dJoint.GetMotorTorque(1/time)}setSpringFrequencyHz(hz){return this.box2dJoint.SetSpringFrequencyHz(hz)}getSpringFrequencyHz(){return this.box2dJoint.GetSpringFrequencyHz()}setSpringDampingRatio(ratio){return this.box2dJoint.SetSpringDampingRatio(ratio)}getSpringDampingRatio(){return this.box2dJoint.GetSpringDampingRatio()}}class Box2dWeldJoint extends Box2dJoint{constructor(objectA,objectB,anchor,collide=false){anchor||=box2d.vec2From(objectB.body.GetPosition());const localAnchorA=objectA.worldToLocal(anchor);const localAnchorB=objectB.worldToLocal(anchor);const jointDef=new box2d.instance.b2WeldJointDef;jointDef.set_bodyA(objectA.body);jointDef.set_bodyB(objectB.body);jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));jointDef.set_referenceAngle(objectA.body.GetAngle()-objectB.body.GetAngle());jointDef.set_collideConnected(collide);super(jointDef)}getLocalAnchorA(){return box2d.vec2From(this.box2dJoint.GetLocalAnchorA())}getLocalAnchorB(){return box2d.vec2From(this.box2dJoint.GetLocalAnchorB())}getReferenceAngle(){return this.box2dJoint.GetReferenceAngle()}setFrequency(hz){return this.box2dJoint.SetFrequency(hz)}getFrequency(){return this.box2dJoint.GetFrequency()}setSpringDampingRatio(ratio){return this.box2dJoint.SetSpringDampingRatio(ratio)}getSpringDampingRatio(){return this.box2dJoint.GetSpringDampingRatio()}}class Box2dFrictionJoint extends Box2dJoint{constructor(objectA,objectB,anchor,collide=false){anchor||=box2d.vec2From(objectB.body.GetPosition());const localAnchorA=objectA.worldToLocal(anchor);const localAnchorB=objectB.worldToLocal(anchor);const jointDef=new box2d.instance.b2FrictionJointDef;jointDef.set_bodyA(objectA.body);jointDef.set_bodyB(objectB.body);jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));jointDef.set_collideConnected(collide);super(jointDef)}getLocalAnchorA(){return box2d.vec2From(this.box2dJoint.GetLocalAnchorA())}getLocalAnchorB(){return box2d.vec2From(this.box2dJoint.GetLocalAnchorB())}setMaxForce(force){this.box2dJoint.SetMaxForce(force)}getMaxForce(){return this.box2dJoint.GetMaxForce()}setMaxTorque(torque){this.box2dJoint.SetMaxTorque(torque)}getMaxTorque(){return this.box2dJoint.GetMaxTorque()}}class Box2dPulleyJoint extends Box2dJoint{constructor(objectA,objectB,groundAnchorA,groundAnchorB,anchorA,anchorB,ratio=1,collide=false){anchorA||=box2d.vec2From(objectA.body.GetPosition());anchorB||=box2d.vec2From(objectB.body.GetPosition());const localAnchorA=objectA.worldToLocal(anchorA);const localAnchorB=objectB.worldToLocal(anchorB);const jointDef=new box2d.instance.b2PulleyJointDef;jointDef.set_bodyA(objectA.body);jointDef.set_bodyB(objectB.body);jointDef.set_groundAnchorA(box2d.vec2dTo(groundAnchorA));jointDef.set_groundAnchorB(box2d.vec2dTo(groundAnchorB));jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));jointDef.set_ratio(ratio);jointDef.set_lengthA(groundAnchorA.distance(anchorA));jointDef.set_lengthB(groundAnchorB.distance(anchorB));jointDef.set_collideConnected(collide);super(jointDef)}getGroundAnchorA(){return box2d.vec2From(this.box2dJoint.GetGroundAnchorA())}getGroundAnchorB(){return box2d.vec2From(this.box2dJoint.GetGroundAnchorB())}getLengthA(){return this.box2dJoint.GetLengthA()}getLengthB(){return this.box2dJoint.GetLengthB()}getRatio(){return this.box2dJoint.GetRatio()}getCurrentLengthA(){return this.box2dJoint.GetCurrentLengthA()}getCurrentLengthB(){return this.box2dJoint.GetCurrentLengthB()}}class Box2dMotorJoint extends Box2dJoint{constructor(objectA,objectB){const linearOffset=objectA.worldToLocal(box2d.vec2From(objectB.body.GetPosition()));const angularOffset=objectB.body.GetAngle()-objectA.body.GetAngle();const jointDef=new box2d.instance.b2MotorJointDef;jointDef.set_bodyA(objectA.body);jointDef.set_bodyB(objectB.body);jointDef.set_linearOffset(box2d.vec2dTo(linearOffset));jointDef.set_angularOffset(angularOffset);super(jointDef)}setLinearOffset(offset){this.box2dJoint.SetLinearOffset(box2d.vec2dTo(offset))}getLinearOffset(){return box2d.vec2From(this.box2dJoint.GetLinearOffset())}setAngularOffset(offset){this.box2dJoint.SetAngularOffset(offset)}getAngularOffset(){return this.box2dJoint.GetAngularOffset()}setMaxForce(force){this.box2dJoint.SetMaxForce(force)}getMaxForce(){return this.box2dJoint.GetMaxForce()}setMaxTorque(torque){this.box2dJoint.SetMaxTorque(torque)}getMaxTorque(){return this.box2dJoint.GetMaxTorque()}setCorrectionFactor(factor){this.box2dJoint.SetCorrectionFactor(factor)}getCorrectionFactor(){return this.box2dJoint.GetCorrectionFactor()}}class Box2dPlugin{constructor(instance){ASSERT(!box2d,"Box2D already initialized");box2d=this;this.instance=instance;this.world=new box2d.instance.b2World;this.objects=[];this.velocityIterations=8;this.positionIterations=3;this.bodyTypeStatic=instance.b2_staticBody;this.bodyTypeKinematic=instance.b2_kinematicBody;this.bodyTypeDynamic=instance.b2_dynamicBody;const listener=new box2d.instance.JSContactListener;listener.BeginContact=function(contactPtr){const contact=box2d.instance.wrapPointer(contactPtr,box2d.instance.b2Contact);const fixtureA=contact.GetFixtureA();const fixtureB=contact.GetFixtureB();const objectA=fixtureA.GetBody().object;const objectB=fixtureB.GetBody().object;objectA.beginContact(objectB);objectB.beginContact(objectA)};listener.EndContact=function(contactPtr){const contact=box2d.instance.wrapPointer(contactPtr,box2d.instance.b2Contact);const fixtureA=contact.GetFixtureA();const fixtureB=contact.GetFixtureB();const objectA=fixtureA.GetBody().object;const objectB=fixtureB.GetBody().object;objectA.endContact(objectB);objectB.endContact(objectA)};listener.PreSolve=function(){};listener.PostSolve=function(){};box2d.world.SetContactListener(listener)}step(frames=1){box2d.world.SetGravity(box2d.vec2dTo(gravity));for(let i=frames;i--;)box2d.world.Step(timeDelta,this.velocityIterations,this.positionIterations)}raycastAll(start,end){const raycastCallback=new box2d.instance.JSRayCastCallback;raycastCallback.ReportFixture=function(fixturePointer,point,normal,fraction){const fixture=box2d.instance.wrapPointer(fixturePointer,box2d.instance.b2Fixture);point=box2d.vec2FromPointer(point);normal=box2d.vec2FromPointer(normal);raycastResults.push(new Box2dRaycastResult(fixture,point,normal,fraction));return 1};const raycastResults=[];box2d.world.RayCast(raycastCallback,box2d.vec2dTo(start),box2d.vec2dTo(end));debugRaycast&&debugLine(start,end,raycastResults.length?"#f00":"#00f",.02);return raycastResults}raycast(start,end){const raycastResults=box2d.raycastAll(start,end);if(!raycastResults.length)return undefined;return raycastResults.reduce((a,b)=>a.fraction<b.fraction?a:b)}boxCastAll(pos,size){const queryCallback=new box2d.instance.JSQueryCallback;queryCallback.ReportFixture=function(fixturePointer){const fixture=box2d.instance.wrapPointer(fixturePointer,box2d.instance.b2Fixture);const o=fixture.GetBody().object;if(!queryObjects.includes(o))queryObjects.push(o);return true};const aabb=new box2d.instance.b2AABB;aabb.set_lowerBound(box2d.vec2dTo(pos.subtract(size.scale(.5))));aabb.set_upperBound(box2d.vec2dTo(pos.add(size.scale(.5))));let queryObjects=[];box2d.world.QueryAABB(queryCallback,aabb);debugRaycast&&debugRect(pos,size,queryObjects.length?"#f00":"#00f",.02);return queryObjects}boxCast(pos,size){const queryCallback=new box2d.instance.JSQueryCallback;queryCallback.ReportFixture=function(fixturePointer){const fixture=box2d.instance.wrapPointer(fixturePointer,box2d.instance.b2Fixture);queryObject=fixture.GetBody().object;return false};const aabb=new box2d.instance.b2AABB;aabb.set_lowerBound(box2d.vec2dTo(pos.subtract(size.scale(.5))));aabb.set_upperBound(box2d.vec2dTo(pos.add(size.scale(.5))));let queryObject;box2d.world.QueryAABB(queryCallback,aabb);debugRaycast&&debugRect(pos,size,queryObject?"#f00":"#00f",.02);return queryObject}circleCastAll(pos,diameter){const radius2=(diameter/2)**2;const results=box2d.boxCastAll(pos,vec2(diameter));return results.filter(o=>o.pos.distanceSquared(pos)<radius2)}circleCast(pos,diameter){const radius2=(diameter/2)**2;let results=box2d.boxCastAll(pos,vec2(diameter));let bestResult,bestDistance2;for(const result of results){const distance2=result.pos.distanceSquared(pos);if(distance2<radius2&&(!bestResult||distance2<bestDistance2)){bestResult=result;bestDistance2=distance2}}return bestResult}pointCast(pos,dynamicOnly=true){const queryCallback=new box2d.instance.JSQueryCallback;queryCallback.ReportFixture=function(fixturePointer){const fixture=box2d.instance.wrapPointer(fixturePointer,box2d.instance.b2Fixture);if(dynamicOnly&&fixture.GetBody().GetType()!==box2d.instance.b2_dynamicBody)return true;if(!fixture.TestPoint(box2d.vec2dTo(pos)))return true;queryObject=fixture.GetBody().object;return false};const aabb=new box2d.instance.b2AABB;aabb.set_lowerBound(box2d.vec2dTo(pos));aabb.set_upperBound(box2d.vec2dTo(pos));let queryObject;box2d.world.QueryAABB(queryCallback,aabb);debugRaycast&&debugRect(pos,vec2(),queryObject?"#f00":"#00f",.02);return queryObject}drawFixture(fixture,pos,angle,color=WHITE,lineColor=BLACK,lineWidth=.1,context){const shape=box2d.castObjectType(fixture.GetShape());switch(shape.GetType()){case box2d.instance.b2Shape.e_polygon:{let points=[];for(let i=shape.GetVertexCount();i--;)points.push(box2d.vec2From(shape.GetVertex(i)));drawPoly(points,color,lineWidth,lineColor,pos,angle);break}case box2d.instance.b2Shape.e_circle:{const radius=shape.get_m_radius();drawCircle(pos,radius*2,color,lineWidth,lineColor);break}case box2d.instance.b2Shape.e_edge:{const v1=box2d.vec2From(shape.get_m_vertex1());const v2=box2d.vec2From(shape.get_m_vertex2());drawLine(v1,v2,lineWidth,lineColor,pos,angle);break}}}vec2From(v){ASSERT(v instanceof box2d.instance.b2Vec2);return new Vector2(v.get_x(),v.get_y())}vec2FromPointer(vp){const v=box2d.instance.wrapPointer(vp,box2d.instance.b2Vec2);return box2d.vec2From(v)}vec2dTo(v){ASSERT(isVector2(v));return new box2d.instance.b2Vec2(v.x,v.y)}isNull(o){return!box2d.instance.getPointer(o)}castObjectType(o){switch(o.GetType()){case box2d.instance.b2Shape.e_circle:return box2d.instance.castObject(o,box2d.instance.b2CircleShape);case box2d.instance.b2Shape.e_edge:return box2d.instance.castObject(o,box2d.instance.b2EdgeShape);case box2d.instance.b2Shape.e_polygon:return box2d.instance.castObject(o,box2d.instance.b2PolygonShape);case box2d.instance.b2Shape.e_chain:return box2d.instance.castObject(o,box2d.instance.b2ChainShape);case box2d.instance.e_revoluteJoint:return box2d.instance.castObject(o,box2d.instance.b2RevoluteJoint);case box2d.instance.e_prismaticJoint:return box2d.instance.castObject(o,box2d.instance.b2PrismaticJoint);case box2d.instance.e_distanceJoint:return box2d.instance.castObject(o,box2d.instance.b2DistanceJoint);case box2d.instance.e_pulleyJoint:return box2d.instance.castObject(o,box2d.instance.b2PulleyJoint);case box2d.instance.e_mouseJoint:return box2d.instance.castObject(o,box2d.instance.b2MouseJoint);case box2d.instance.e_gearJoint:return box2d.instance.castObject(o,box2d.instance.b2GearJoint);case box2d.instance.e_wheelJoint:return box2d.instance.castObject(o,box2d.instance.b2WheelJoint);case box2d.instance.e_weldJoint:return box2d.instance.castObject(o,box2d.instance.b2WeldJoint);case box2d.instance.e_frictionJoint:return box2d.instance.castObject(o,box2d.instance.b2FrictionJoint);case box2d.instance.e_ropeJoint:return box2d.instance.castObject(o,box2d.instance.b2RopeJoint);case box2d.instance.e_motorJoint:return box2d.instance.castObject(o,box2d.instance.b2MotorJoint)}ASSERT(false,"Unknown box2d object type")}}async function box2dInit(){new Box2dPlugin(await Box2D());setupDebugDraw();engineAddPlugin(box2dUpdate,box2dRender);return box2d;function box2dUpdate(){if(paused)return;box2d.step();box2d.objects=box2d.objects.filter(o=>!o.destroyed);for(const o of box2d.objects){if(o.body){o.pos=box2d.vec2From(o.body.GetPosition());o.angle=-o.body.GetAngle()}}}function box2dRender(){if(box2dDebug||debugPhysics)box2d.world.DrawDebugData()}function setupDebugDraw(){const debugLineWidth=.1;const debugDraw=new box2d.instance.JSDraw;const box2dColor=c=>new Color(c.get_r(),c.get_g(),c.get_b());const box2dColorPointer=c=>box2dColor(box2d.instance.wrapPointer(c,box2d.instance.b2Color));const getDebugColor=color=>box2dColorPointer(color).scale(1,.8);const getPointsList=(vertices,vertexCount)=>{const points=[];for(let i=vertexCount;i--;)points.push(box2d.vec2FromPointer(vertices+i*8));return points};debugDraw.DrawSegment=function(point1,point2,color){color=getDebugColor(color);point1=box2d.vec2FromPointer(point1);point2=box2d.vec2FromPointer(point2);drawLine(point1,point2,debugLineWidth,color,vec2(),0,false)};debugDraw.DrawPolygon=function(vertices,vertexCount,color){color=getDebugColor(color);const points=getPointsList(vertices,vertexCount);drawPoly(points,CLEAR_WHITE,debugLineWidth,color,vec2(),0,false)};debugDraw.DrawSolidPolygon=function(vertices,vertexCount,color){color=getDebugColor(color);const points=getPointsList(vertices,vertexCount);drawPoly(points,color,0,color,vec2(),0,false)};debugDraw.DrawCircle=function(center,radius,color){color=getDebugColor(color);center=box2d.vec2FromPointer(center);drawCircle(center,radius*2,CLEAR_WHITE,debugLineWidth,color,false)};debugDraw.DrawSolidCircle=function(center,radius,axis,color){color=getDebugColor(color);center=box2d.vec2FromPointer(center);axis=box2d.vec2FromPointer(axis).scale(radius);drawCircle(center,radius*2,color,debugLineWidth,color,false);drawLine(vec2(),axis,debugLineWidth,color,center,0,false)};debugDraw.DrawTransform=function(transform){transform=box2d.instance.wrapPointer(transform,box2d.instance.b2Transform);const pos=vec2(transform.get_p());const angle=-transform.get_q().GetAngle();const p1=vec2(1,0),c1=rgb(.75,0,0,.8);const p2=vec2(0,1),c2=rgb(0,.75,0,.8);drawLine(vec2(),p1,debugLineWidth,c1,pos,angle,false);drawLine(vec2(),p2,debugLineWidth,c2,pos,angle,false)};debugDraw.AppendFlags(box2d.instance.b2Draw.e_shapeBit);debugDraw.AppendFlags(box2d.instance.b2Draw.e_jointBit);box2d.world.SetDebugDraw(debugDraw)}}function drawNineSliceScreen(pos,size,startTile,borderSize=32,extraSpace=2,angle=0){drawNineSlice(pos,size,startTile,WHITE,borderSize,BLACK,extraSpace,angle,false,true)}function drawNineSlice(pos,size,startTile,color,borderSize=1,additiveColor,extraSpace=.05,angle=0,useWebGL=glEnable,screenSpace,context){const centerTile=startTile.offset(startTile.size);const centerSize=size.add(vec2(extraSpace-borderSize*2));const cornerSize=vec2(borderSize);const cornerOffset=size.scale(.5).subtract(cornerSize.scale(.5));const flip=screenSpace?-1:1;const rotateAngle=screenSpace?-angle:angle;drawTile(pos,centerSize,centerTile,color,angle,false,additiveColor,useWebGL,screenSpace,context);for(let i=4;i--;){const horizontal=i%2;const sidePos=cornerOffset.multiply(vec2(horizontal?i===1?1:-1:0,horizontal?0:i?-1:1));const sideSize=vec2(horizontal?borderSize:centerSize.x,horizontal?centerSize.y:borderSize);const sideTile=centerTile.offset(startTile.size.multiply(vec2(i===1?1:i===3?-1:0,i===0?-flip:i===2?flip:0)));drawTile(pos.add(sidePos.rotate(rotateAngle)),sideSize,sideTile,color,angle,false,additiveColor,useWebGL,screenSpace,context)}for(let i=4;i--;){const flipX=i>1;const flipY=i&&i<3;const cornerPos=cornerOffset.multiply(vec2(flipX?-1:1,flipY?-1:1));const cornerTile=centerTile.offset(startTile.size.multiply(vec2(flipX?-1:1,flipY?flip:-flip)));drawTile(pos.add(cornerPos.rotate(rotateAngle)),cornerSize,cornerTile,color,angle,false,additiveColor,useWebGL,screenSpace,context)}}function drawThreeSliceScreen(pos,size,startTile,borderSize=32,extraSpace=2,angle=0){drawThreeSlice(pos,size,startTile,WHITE,borderSize,BLACK,extraSpace,angle,false,true)}function drawThreeSlice(pos,size,startTile,color,borderSize=1,additiveColor,extraSpace=.05,angle=0,useWebGL=glEnable,screenSpace,context){const cornerTile=startTile.frame(0);const sideTile=startTile.frame(1);const centerTile=startTile.frame(2);const centerSize=size.add(vec2(extraSpace-borderSize*2));const cornerSize=vec2(borderSize);const cornerOffset=size.scale(.5).subtract(cornerSize.scale(.5));const flip=screenSpace?-1:1;const rotateAngle=screenSpace?-angle:angle;drawTile(pos,centerSize,centerTile,color,angle,false,additiveColor,useWebGL,screenSpace,context);for(let i=4;i--;){const a=angle+i*PI/2;const horizontal=i%2;const sidePos=cornerOffset.multiply(vec2(horizontal?i===1?1:-1:0,horizontal?0:i?-flip:flip));const sideSize=vec2(horizontal?centerSize.y:centerSize.x,borderSize);drawTile(pos.add(sidePos.rotate(rotateAngle)),sideSize,sideTile,color,a,false,additiveColor,useWebGL,screenSpace,context)}for(let i=4;i--;){const a=angle+i*PI/2;const flipX=!i||i>2;const flipY=i>1;const cornerPos=cornerOffset.multiply(vec2(flipX?-1:1,flipY?-flip:flip));drawTile(pos.add(cornerPos.rotate(rotateAngle)),cornerSize,cornerTile,color,a,false,additiveColor,useWebGL,screenSpace,context)}}export{engineName,engineVersion,frameRate,timeDelta,engineObjects,frame,time,timeReal,paused,getPaused,setPaused,engineInit,engineObjectsUpdate,engineObjectsDestroy,engineObjectsCollect,engineObjectsCallback,engineObjectsRaycast,engineAddPlugin,debug,debugOverlay,debugWatermark,ASSERT,LOG,debugRect,debugPoly,debugCircle,debugPoint,debugLine,debugOverlap,debugText,debugClear,debugScreenshot,debugSaveCanvas,debugSaveText,debugSaveDataURL,debugShowErrors,debugVideoCaptureIsActive,debugVideoCaptureStart,debugVideoCaptureStop,cameraPos,cameraAngle,cameraScale,canvasColorTiles,canvasClearColor,canvasMaxSize,canvasMinAspect,canvasMaxAspect,canvasFixedSize,canvasPixelated,tilesPixelated,fontDefault,showSplashScreen,headlessMode,tileDefaultSize,tileDefaultPadding,tileDefaultBleed,enablePhysicsSolver,objectDefaultMass,objectDefaultDamping,objectDefaultAngleDamping,objectDefaultRestitution,objectDefaultFriction,objectMaxSpeed,gravity,particleEmitRateScale,glEnable,gamepadsEnable,gamepadDirectionEmulateStick,inputWASDEmulateDirection,touchGamepadEnable,touchGamepadCenterButton,touchGamepadAnalog,touchGamepadSize,touchGamepadAlpha,vibrateEnable,soundEnable,soundVolume,soundDefaultRange,soundDefaultTaper,medalDisplayTime,medalDisplaySlideTime,medalDisplaySize,setCameraPos,setCameraAngle,setCameraScale,setCanvasColorTiles,setCanvasClearColor,setCanvasMaxSize,setCanvasMinAspect,setCanvasMaxAspect,setCanvasFixedSize,setCanvasPixelated,setTilesPixelated,setFontDefault,setShowSplashScreen,setHeadlessMode,setGLEnable,setTileDefaultSize,setTileDefaultPadding,setTileDefaultBleed,setEnablePhysicsSolver,setObjectDefaultMass,setObjectDefaultDamping,setObjectDefaultAngleDamping,setObjectDefaultRestitution,setObjectDefaultFriction,setObjectMaxSpeed,setGravity,setParticleEmitRateScale,setTouchInputEnable,setGamepadsEnable,setGamepadDirectionEmulateStick,setInputWASDEmulateDirection,setTouchGamepadEnable,setTouchGamepadCenterButton,setTouchGamepadButtonCount,setTouchGamepadAnalog,setTouchGamepadSize,setTouchGamepadAlpha,setVibrateEnable,setSoundEnable,setSoundVolume,setSoundDefaultRange,setSoundDefaultTaper,setMedalDisplayTime,setMedalDisplaySlideTime,setMedalDisplaySize,setMedalsPreventUnlock,setDebugWatermark,setDebugKey,PI,abs,floor,ceil,round,min,max,sign,hypot,log2,sin,cos,tan,atan2,mod,clamp,percent,distanceWrap,lerpWrap,distanceAngle,lerpAngle,lerp,smoothStep,nearestPowerOfTwo,isOverlapping,isIntersecting,wave,formatTime,fetchJSON,rand,randInt,randBool,randSign,randInCircle,randVec2,randColor,RandomGenerator,Vector2,Color,Timer,vec2,rgb,hsl,isColor,isVector2,isNumber,isString,isArray,WHITE,CLEAR_WHITE,BLACK,CLEAR_BLACK,GRAY,RED,ORANGE,YELLOW,GREEN,CYAN,BLUE,PURPLE,MAGENTA,tile,TileInfo,TextureInfo,mainCanvas,mainContext,drawCanvas,drawContext,workCanvas,workContext,workReadCanvas,workReadContext,mainCanvasSize,textureInfos,drawCount,screenToWorld,worldToScreen,screenToWorldDelta,worldToScreenDelta,drawTile,drawRect,drawRectGradient,drawLineList,drawLine,drawPoly,drawEllipse,drawCircle,drawCanvas2D,drawText,drawTextScreen,setBlendMode,combineCanvases,engineFontImage,FontImage,isFullscreen,toggleFullscreen,setCursor,getCameraSize,glCanvas,glContext,glClearCanvas,glSetTexture,glCompileShader,glCreateProgram,glCreateTexture,glDeleteTexture,glSetTextureData,glFlush,glCopyToContext,glSetAntialias,glDraw,glDrawPointsTransform,glDrawOutlineTransform,glDrawPoints,glDrawColoredPoints,glAntialias,glShader,glPolyShader,glPolyMode,glAdditive,glBatchAdditive,glActiveTexture,glArrayBuffer,glGeometryBuffer,glPositionData,glColorData,glBatchCount,keyIsDown,keyWasPressed,keyWasReleased,keyDirection,inputClear,inputClearKey,mouseIsDown,mouseWasPressed,mouseWasReleased,mousePos,mousePosScreen,mouseDelta,mouseDeltaScreen,mouseWheel,mouseInWindow,isUsingGamepad,inputPreventDefault,gamepadPrimary,setInputPreventDefault,gamepadIsDown,gamepadWasPressed,gamepadWasReleased,gamepadStick,gamepadDpad,gamepadConnected,vibrate,vibrateStop,isTouchDevice,pointerLockRequest,pointerLockExit,pointerLockIsActive,audioContext,audioMasterGain,audioDefaultSampleRate,Sound,SoundWave,SoundInstance,speak,speakStop,getNoteFrequency,playSamples,zzfx,zzfxG,EngineObject,tileCollisionLayers,tileCollisionGetData,tileCollisionTest,tileCollisionRaycast,tileLayersLoad,TileLayerData,CanvasLayer,TileLayer,TileCollisionLayer,ParticleEmitter,Particle,medals,medalsPreventUnlock,medalsInit,Medal};export{newgrounds,NewgroundsPlugin,NewgroundsMedal,postProcess,PostProcessPlugin,ZzFXMusic,uiSystem,uiDebug,uiSetDebug,UISystemPlugin,UIObject,UIText,UITile,UIButton,UICheckbox,UIScrollbar,UIVideo,box2d,box2dDebug,box2dSetDebug,box2dInit,Box2dPlugin,Box2dObject,Box2dStaticObject,Box2dKiematicObject,Box2dRaycastResult,Box2dJoint,Box2dTargetJoint,Box2dDistanceJoint,Box2dPinJoint,Box2dRopeJoint,Box2dRevoluteJoint,Box2dGearJoint,Box2dPrismaticJoint,Box2dWheelJoint,Box2dWeldJoint,Box2dFrictionJoint,Box2dPulleyJoint,Box2dMotorJoint,drawNineSlice,drawNineSliceScreen,drawThreeSlice,drawThreeSliceScreen};
4
+ "use strict";const engineName="LittleJS";const engineVersion="1.16.2";const frameRate=60;const timeDelta=1/frameRate;let engineObjects=[];let engineObjectsCollide=[];let frame=0;let time=0;let timeReal=0;let paused=false;function getPaused(){return paused}function setPaused(isPaused=true){paused=isPaused}let frameTimeLastMS=0,frameTimeBufferMS=0,averageFPS=0;const pluginList=[];class EnginePlugin{constructor(update,render,glContextLost,glContextRestored){this.update=update;this.render=render;this.glContextLost=glContextLost;this.glContextRestored=glContextRestored}}function engineAddPlugin(update,render,glContextLost,glContextRestored){ASSERT(!pluginList.find(p=>p.update===update&&p.render===render&&p.glContextLost===glContextLost&&p.glContextRestored===glContextRestored));const plugin=new EnginePlugin(update,render,glContextLost,glContextRestored);pluginList.push(plugin)}async function engineInit(gameInit,gameUpdate,gameUpdatePost,gameRender,gameRenderPost,imageSources=[],rootElement=document.body){ASSERT(!mainContext,"engine already initialized");ASSERT(isArray(imageSources),"pass in images as array");gameInit||=()=>{};gameUpdate||=()=>{};gameUpdatePost||=()=>{};gameRender||=()=>{};gameRenderPost||=()=>{};function enginePreRender(){mainCanvasSize=vec2(mainCanvas.width,mainCanvas.height);mainContext.imageSmoothingEnabled=!tilesPixelated;glPreRender()}function engineUpdate(frameTimeMS=0){let frameTimeDeltaMS=frameTimeMS-frameTimeLastMS;frameTimeLastMS=frameTimeMS;if(debug||debugWatermark)averageFPS=lerp(averageFPS,1e3/(frameTimeDeltaMS||1),.05);const debugSpeedUp=debug&&keyIsDown("Equal");const debugSpeedDown=debug&&keyIsDown("Minus");if(debug)frameTimeDeltaMS*=debugSpeedUp?10:debugSpeedDown?.1:1;timeReal+=frameTimeDeltaMS/1e3;frameTimeBufferMS+=paused?0:frameTimeDeltaMS;if(!debugSpeedUp)frameTimeBufferMS=min(frameTimeBufferMS,50);let wasUpdated=false;if(paused){wasUpdated=true;updateCanvas();inputUpdate();pluginList.forEach(plugin=>plugin.update?.());for(const o of engineObjects)o.parent||o.updateTransforms();debugUpdate();gameUpdatePost();inputUpdatePost();if(debugVideoCaptureIsActive())renderFrame()}else{let deltaSmooth=0;if(frameTimeBufferMS<0&&frameTimeBufferMS>-9){deltaSmooth=frameTimeBufferMS;frameTimeBufferMS=0}for(;frameTimeBufferMS>=0;frameTimeBufferMS-=1e3/frameRate){time=frame++/frameRate;wasUpdated=true;updateCanvas();inputUpdate();gameUpdate();pluginList.forEach(plugin=>plugin.update?.());engineObjectsUpdate();debugUpdate();gameUpdatePost();inputUpdatePost();if(debugVideoCaptureIsActive())renderFrame()}frameTimeBufferMS+=deltaSmooth}if(!debugVideoCaptureIsActive())renderFrame();requestAnimationFrame(engineUpdate);function renderFrame(){if(headlessMode)return;if(!wasUpdated)updateCanvas();enginePreRender();gameRender();engineObjects.sort((a,b)=>a.renderOrder-b.renderOrder);for(const o of engineObjects)o.destroyed||o.render();gameRenderPost();pluginList.forEach(plugin=>plugin.render?.());inputRender();debugRender();glFlush();debugRenderPost();drawCount=0}}function updateCanvas(){if(headlessMode)return;if(canvasFixedSize.x){mainCanvasSize=canvasFixedSize.copy();const innerAspect=innerWidth/innerHeight;const fixedAspect=canvasFixedSize.x/canvasFixedSize.y;const w=innerAspect<fixedAspect?"100%":"";const h=innerAspect<fixedAspect?"":"100%";mainCanvas.style.width=w;mainCanvas.style.height=h;if(glCanvas){glCanvas.style.width=w;glCanvas.style.height=h}}else{mainCanvasSize.x=min(innerWidth,canvasMaxSize.x);mainCanvasSize.y=min(innerHeight,canvasMaxSize.y);const innerAspect=innerWidth/innerHeight;ASSERT(canvasMinAspect<=canvasMaxAspect);if(canvasMaxAspect&&innerAspect>canvasMaxAspect){const w=mainCanvasSize.y*canvasMaxAspect|0;mainCanvasSize.x=min(w,canvasMaxSize.x)}else if(innerAspect<canvasMinAspect){const h=mainCanvasSize.x/canvasMinAspect|0;mainCanvasSize.y=min(h,canvasMaxSize.y)}}mainCanvas.width=mainCanvasSize.x;mainCanvas.height=mainCanvasSize.y;if(canvasClearColor.a>0&&!glEnable){mainContext.fillStyle=canvasClearColor.toString();mainContext.fillRect(0,0,mainCanvasSize.x,mainCanvasSize.y);mainContext.fillStyle=BLACK.toString()}mainContext.lineJoin="round";mainContext.lineCap="round"}if(headlessMode)return startEngine();glInit(rootElement);const styleRoot="margin:0;"+"overflow:hidden;"+"background:#000;"+"user-select:none;"+"-webkit-user-select:none;"+"touch-action:none;"+"-webkit-touch-callout:none";rootElement.style.cssText=styleRoot;mainCanvas=rootElement.appendChild(document.createElement("canvas"));drawContext=mainContext=mainCanvas.getContext("2d");inputInit();audioInit();debugInit();const styleCanvas="position:absolute;"+"top:50%;left:50%;transform:translate(-50%,-50%)";mainCanvas.style.cssText=styleCanvas;if(glCanvas)glCanvas.style.cssText=styleCanvas;setCanvasPixelated(canvasPixelated);updateCanvas();glPreRender();workCanvas=new OffscreenCanvas(64,64);workContext=workCanvas.getContext("2d");workReadCanvas=new OffscreenCanvas(64,64);workReadContext=workReadCanvas.getContext("2d",{willReadFrequently:true});const promises=imageSources.map((src,textureIndex)=>new Promise(resolve=>{ASSERT(isString(src),"imageSources must be an array of strings");const image=new Image;image.onerror=image.onload=()=>{const textureInfo=new TextureInfo(image);textureInfos[textureIndex]=textureInfo;resolve()};image.crossOrigin="anonymous";image.src=src}));if(!imageSources.length){promises.push(new Promise(resolve=>{const textureInfo=new TextureInfo(new Image);textureInfos[0]=textureInfo;resolve()}))}if(showSplashScreen){promises.push(new Promise(resolve=>{let t=0;console.log(`${engineName} Engine v${engineVersion}`);updateSplash();function updateSplash(){inputClear();drawEngineSplashScreen(t+=.01);t>1?resolve():setTimeout(updateSplash,16)}}))}await Promise.all(promises);return startEngine();async function startEngine(){await gameInit();engineUpdate()}function drawEngineSplashScreen(t){const x=mainContext;const w=mainCanvas.width=innerWidth;const h=mainCanvas.height=innerHeight;{const p3=percent(t,1,.8);const p4=percent(t,0,.5);const g=x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.7);g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());g.addColorStop(1,hsl(0,0,0,p3).toString());x.save();x.fillStyle=g;x.fillRect(0,0,w,h)}const rect=(X,Y,W,H,C)=>{x.beginPath();x.rect(X,Y,W,C?H*p:H);x.fillStyle=C;C?x.fill():x.stroke()};const line=(X,Y,Z,W)=>{x.beginPath();x.lineTo(X,Y);x.lineTo(Z,W);x.stroke()};const circle=(X,Y,R,A=0,B=2*PI,C,F)=>{const D=(A+B)/2,E=p*(B-A)/2;x.beginPath();F&&x.lineTo(X,Y);x.arc(X,Y,R,D-E,D+E);x.fillStyle=C;C?x.fill():x.stroke()};const color=(c=0,l=0)=>hsl([.98,.3,.57,.14][c%4],.8,[0,.3,.5,.8,.9][l]).toString();const alpha=wave(1,1,t);const p=percent(alpha,.1,.5);x.translate(w/2,h/2);const size=min(6,min(w,h)/99);x.scale(size,size);x.translate(-40,-35);x.lineJoin=x.lineCap="round";x.lineWidth=.1+p*1.9;const p2=percent(alpha,.1,1);x.setLineDash([99*p2,99]);rect(7,16,18,-8,color(2,2));rect(7,8,18,4,color(2,3));rect(25,8,8,8,color(2,1));rect(25,8,-18,8);rect(25,8,8,8);rect(25,16,7,23,color());rect(11,39,14,-23,color(1,1));rect(11,16,14,18,color(1,2));rect(11,16,14,8,color(1,3));rect(25,16,-14,24);rect(15,29,6,-9,color(2,2));circle(15,21,5,0,PI/2,color(2,4),1);rect(21,21,-6,9);rect(37,14,9,6,color(3,2));rect(37,14,4.5,6,color(3,3));rect(37,14,9,6);rect(50,20,10,-8,color(0,1));rect(50,20,6.5,-8,color(0,2));rect(50,20,3.5,-8,color(0,3));rect(50,20,10,-8);circle(55,2,11.4,.5,PI-.5,color(3,3));circle(55,2,11.4,.5,PI/2,color(3,2),1);circle(55,2,11.4,.5,PI-.5);rect(45,7,20,-7,color(0,2));rect(45,-1,20,4,color(0,3));rect(45,-1,20,8);for(let i=5;i--;){circle(60-i*6,30,9.9,0,2*PI,color(i+2,3));circle(60-i*6,30,10,-.5,PI+.5,color(i+2,2));circle(60-i*6,30,10.1,.5,PI-.5,color(i+2,1))}circle(36,30,10,PI/2,PI*3/2);circle(48,30,10,PI/2,PI*3/2);circle(60,30,10);line(36,20,60,20);circle(60,30,4,PI,3*PI,color(3,2));circle(60,30,4,PI,2*PI,color(3,3));circle(60,30,4,PI,3*PI);for(let i=6;i--;){x.beginPath();x.lineTo(53,54);x.lineTo(53,40);x.lineTo(53+(1+i*2.9)*p,40);x.lineTo(53+(4+i*3.5)*p,54);x.fillStyle=color(0,i%2+2);x.fill();i%2&&x.stroke()}rect(6,40,5,5);rect(6,40,5,5,color());rect(15,54,38,-14,color());for(let i=3;i--;)for(let j=2;j--;){circle(15*i+15,47,j?7:1,PI,3*PI,color(i,3));x.stroke();circle(15*i+15,47,j?7:1,0,PI,color(i,2));x.stroke()}line(6,40,68,40);line(77,54,4,54);const s=engineName;x.font="900 16px arial";x.textAlign="center";x.textBaseline="top";x.lineWidth=.1+p*3.9;let w2=0;for(let i=0;i<s.length;++i)w2+=x.measureText(s[i]).width;for(let j=2;j--;)for(let i=0,X=41-w2/2;i<s.length;++i){x.fillStyle=color(i,2);const w=x.measureText(s[i]).width;x[j?"strokeText":"fillText"](s[i],X+w/2,55.5,17*p);X+=w}x.restore()}}function engineObjectsUpdate(){engineObjectsCollide=engineObjects.filter(o=>o.collideSolidObjects);function updateObject(o){if(o.destroyed)return;o.update();for(const child of o.children)updateObject(child)}for(const o of engineObjects){if(o.parent)continue;o.update();o.updatePhysics();for(const child of o.children)updateObject(child);o.updateTransforms()}engineObjects=engineObjects.filter(o=>!o.destroyed)}function engineObjectsDestroy(){for(const o of engineObjects)o.parent||o.destroy();engineObjects=engineObjects.filter(o=>!o.destroyed)}function engineObjectsCollect(pos,size,objects=engineObjects){const collectedObjects=[];if(!pos){for(const o of objects)collectedObjects.push(o)}else if(size instanceof Vector2){for(const o of objects)o.isOverlapping(pos,size)&&collectedObjects.push(o)}else{const sizeSquared=size*size;for(const o of objects)pos.distanceSquared(o.pos)<sizeSquared&&collectedObjects.push(o)}return collectedObjects}function engineObjectsCallback(pos,size,callbackFunction,objects=engineObjects){engineObjectsCollect(pos,size,objects).forEach(o=>callbackFunction(o))}function engineObjectsRaycast(start,end,objects=engineObjects){const hitObjects=[];for(const o of objects){if(o.collideRaycast&&isIntersecting(start,end,o.pos,o.size)){debugRaycast&&debugRect(o.pos,o.size,"#f00");hitObjects.push(o)}}debugRaycast&&debugLine(start,end,hitObjects.length?"#f00":"#00f",.02);return hitObjects}let debugWatermark=0;let debugKey="";const debug=0;const debugOverlay=0;const debugPhysics=0;const debugParticles=0;const debugRaycast=0;const debugGamepads=0;const debugMedals=0;function ASSERT(){}function LOG(){}function debugInit(){}function debugUpdate(){}function debugRender(){}function debugRenderPost(){}function debugRect(){}function debugPoly(){}function debugCircle(){}function debugPoint(){}function debugLine(){}function debugOverlap(){}function debugText(){}function debugClear(){}function debugScreenshot(){}function debugShowErrors(){}function debugVideoCaptureIsActive(){return false}function debugVideoCaptureStart(){}function debugVideoCaptureStop(){}function debugVideoCaptureUpdate(){}function debugProtectConstant(o){return o}const PI=Math.PI;const abs=Math.abs;const floor=Math.floor;const ceil=Math.ceil;const round=Math.round;const min=Math.min;const max=Math.max;const sign=Math.sign;const hypot=Math.hypot;const log2=Math.log2;const sin=Math.sin;const cos=Math.cos;const tan=Math.tan;const atan2=Math.atan2;function mod(dividend,divisor=1){return(dividend%divisor+divisor)%divisor}function clamp(value,min=0,max=1){return value<min?min:value>max?max:value}function percent(value,valueA,valueB){return(valueB-=valueA)?clamp((value-valueA)/valueB):0}function lerp(valueA,valueB,percent){return valueA+clamp(percent)*(valueB-valueA)}function percentLerp(value,percentA,percentB,lerpA,lerpB){return lerp(lerpA,lerpB,percent(value,percentA,percentB))}function distanceWrap(valueA,valueB,wrapSize=1){const d=(valueA-valueB)%wrapSize;return d*2%wrapSize-d}function lerpWrap(valueA,valueB,percent,wrapSize=1){return valueA+clamp(percent)*distanceWrap(valueB,valueA,wrapSize)}function distanceAngle(angleA,angleB){return distanceWrap(angleA,angleB,2*PI)}function lerpAngle(angleA,angleB,percent){return lerpWrap(angleA,angleB,percent,2*PI)}function smoothStep(percent){return percent*percent*(3-2*percent)}function isPowerOfTwo(value){return!(value&value-1)}function nearestPowerOfTwo(value){return 2**ceil(log2(value))}function isOverlapping(posA,sizeA,posB,sizeB=vec2()){const dx=(posA.x-posB.x)*2;const dy=(posA.y-posB.y)*2;const sx=sizeA.x+sizeB.x;const sy=sizeA.y+sizeB.y;return dx>=-sx&&dx<sx&&dy>=-sy&&dy<sy}function isIntersecting(start,end,pos,size){const boxMin=pos.subtract(size.scale(.5));const boxMax=boxMin.add(size);const delta=end.subtract(start);const a=start.subtract(boxMin);const b=start.subtract(boxMax);const p=[-delta.x,delta.x,-delta.y,delta.y];const q=[a.x,-b.x,a.y,-b.y];let tMin=0,tMax=1;for(let i=4;i--;){if(p[i]){const t=q[i]/p[i];if(p[i]<0){if(t>tMax)return false;tMin=max(t,tMin)}else{if(t<tMin)return false;tMax=min(t,tMax)}}else if(q[i]<0)return false}return true}function wave(frequency=1,amplitude=1,t=time,offset=0){return amplitude/2*(1-cos(offset+t*frequency*2*PI))}function isNumber(n){return typeof n==="number"&&!isNaN(n)}function isString(s){return s!==undefined&&s!==null&&typeof s.toString()==="string"}function isArray(a){return Array.isArray(a)}function lineTest(posStart,posEnd,testFunction,normal){ASSERT(isVector2(posStart),"posStart must be a vec2");ASSERT(isVector2(posEnd),"posEnd must be a vec2");ASSERT(typeof testFunction==="function","testFunction must be a function");ASSERT(!normal||isVector2(normal),"normal must be a vec2");const dx=posEnd.x-posStart.x;const dy=posEnd.y-posStart.y;const totalLength=hypot(dx,dy);if(!totalLength)return;const pos=posStart.floor();const dirX=dx/totalLength;const dirY=dy/totalLength;const stepX=sign(dirX);const stepY=sign(dirY);const tDeltaX=dirX?abs(1/dirX):Infinity;const tDeltaY=dirY?abs(1/dirY):Infinity;const nextGridX=stepX>0?pos.x+1:pos.x;const nextGridY=stepY>0?pos.y+1:pos.y;const tMaxX=dirX?(nextGridX-posStart.x)/dirX:Infinity;const tMaxY=dirY?(nextGridY-posStart.y)/dirY:Infinity;let t=0,tX=tMaxX,tY=tMaxY,wasX=tDeltaX<tDeltaY;while(t<totalLength){if(testFunction(pos)){const hitPos=vec2(posStart.x+dirX*t,posStart.y+dirY*t);const e=1e-9;if(wasX){if(stepX<0)hitPos.x-=e}if(stepY<0)hitPos.y-=e;if(normal)wasX?normal.set(-stepX,0):normal.set(0,-stepY);return hitPos}if(wasX=tX<tY){pos.x+=stepX;t=tX;tX+=tDeltaX}else{pos.y+=stepY;t=tY;tY+=tDeltaY}}}function rand(valueA=1,valueB=0){return valueB+Math.random()*(valueA-valueB)}function randInt(valueA,valueB=0){return floor(rand(valueA,valueB))}function randBool(chance=.5){return rand()<chance}function randSign(){return randInt(2)*2-1}function randVec2(length=1){return(new Vector2).setAngle(rand(2*PI),length)}function randInCircle(radius=1,minRadius=0){return radius>0?randVec2(radius*rand(minRadius/radius,1)**.5):new Vector2}function randColor(colorA=new Color,colorB=new Color(0,0,0,1),linear=false){return linear?colorA.lerp(colorB,rand()):new Color(rand(colorA.r,colorB.r),rand(colorA.g,colorB.g),rand(colorA.b,colorB.b),rand(colorA.a,colorB.a))}class RandomGenerator{constructor(seed=123456789){this.seed=seed}float(valueA=1,valueB=0){this.seed^=this.seed<<13;this.seed^=this.seed>>>17;this.seed^=this.seed<<5;return valueB+(valueA-valueB)*((this.seed>>>0)/2**32)}int(valueA,valueB=0){return floor(this.float(valueA,valueB))}bool(chance=.5){return this.float()<chance}sign(){return this.float()>.5?1:-1}floatSign(valueA=1,valueB=0){return this.float(valueA,valueB)*this.sign()}angle(){return this.float(-PI,PI)}vec2(valueA=1,valueB=0){return vec2(this.float(valueA,valueB),this.float(valueA,valueB))}randColor(colorA=new Color,colorB=new Color(0,0,0,1),linear=false){return linear?colorA.lerp(colorB,this.float()):new Color(this.float(colorA.r,colorB.r),this.float(colorA.g,colorB.g),this.float(colorA.b,colorB.b),this.float(colorA.a,colorB.a))}mutateColor(color,amount=.05,alphaAmount=0){ASSERT_NUMBER_VALID(amount);ASSERT_NUMBER_VALID(alphaAmount);return new Color(color.r+this.float(amount,-amount),color.g+this.float(amount,-amount),color.b+this.float(amount,-amount),color.a+this.float(alphaAmount,-alphaAmount)).clamp()}}function vec2(x=0,y){return new Vector2(x,y===undefined?x:y)}function isVector2(v){return v instanceof Vector2&&v.isValid()}function ASSERT_VECTOR2_VALID(v){ASSERT(isVector2(v),"Vector2 is invalid.",v)}function ASSERT_NUMBER_VALID(n){ASSERT(isNumber(n),"Number is invalid.",n)}function ASSERT_VECTOR2_NORMAL(v){ASSERT_VECTOR2_VALID(v);ASSERT(abs(v.lengthSquared()-1)<.01,"Vector2 is not normal.",v)}class Vector2{constructor(x=0,y=0){this.x=x;this.y=y;ASSERT(this.isValid(),"Constructed Vector2 is invalid.",this)}set(x=0,y=0){this.x=x;this.y=y;ASSERT_VECTOR2_VALID(this);return this}setFrom(v){return this.set(v.x,v.y)}copy(){return new Vector2(this.x,this.y)}add(v){return new Vector2(this.x+v.x,this.y+v.y)}subtract(v){return new Vector2(this.x-v.x,this.y-v.y)}multiply(v){return new Vector2(this.x*v.x,this.y*v.y)}divide(v){return new Vector2(this.x/v.x,this.y/v.y)}scale(s){return new Vector2(this.x*s,this.y*s)}length(){return this.lengthSquared()**.5}lengthSquared(){return this.x**2+this.y**2}distance(v){return this.distanceSquared(v)**.5}distanceSquared(v){return(this.x-v.x)**2+(this.y-v.y)**2}normalize(length=1){const l=this.length();return l?this.scale(length/l):new Vector2(0,length)}clampLength(length=1){const l=this.length();return l>length?this.scale(length/l):this.copy()}dot(v){return this.x*v.x+this.y*v.y}cross(v){return this.x*v.y-this.y*v.x}reflect(normal,restitution=1){return this.subtract(normal.scale((1+restitution)*this.dot(normal)))}angle(){return atan2(this.x,this.y)}setAngle(angle=0,length=1){ASSERT_NUMBER_VALID(angle);ASSERT_NUMBER_VALID(length);this.x=length*sin(angle);this.y=length*cos(angle);return this}rotate(angle){ASSERT_NUMBER_VALID(angle);const c=cos(-angle),s=sin(-angle);return new Vector2(this.x*c-this.y*s,this.x*s+this.y*c)}setDirection(direction,length=1){ASSERT_NUMBER_VALID(direction);ASSERT_NUMBER_VALID(length);direction=mod(direction,4);ASSERT(direction===0||direction===1||direction===2||direction===3,"Vector2.setDirection() direction must be an integer between 0 and 3.");this.x=direction%2?direction-1?-length:length:0;this.y=direction%2?0:direction?-length:length;return this}direction(){return abs(this.x)>abs(this.y)?this.x<0?3:1:this.y<0?2:0}abs(){return new Vector2(abs(this.x),abs(this.y))}floor(){return new Vector2(floor(this.x),floor(this.y))}mod(divisor=1){return new Vector2(mod(this.x,divisor),mod(this.y,divisor))}area(){return abs(this.x*this.y)}isZero(){return!this.x&&!this.y}lerp(v,percent){ASSERT_VECTOR2_VALID(v);ASSERT_NUMBER_VALID(percent);const p=clamp(percent);return new Vector2(v.x*p+this.x*(1-p),v.y*p+this.y*(1-p))}arrayCheck(arraySize){return this.x>=0&&this.y>=0&&this.x<arraySize.x&&this.y<arraySize.y}toString(digits=3){ASSERT_NUMBER_VALID(digits);if(this.isValid())return`(${(this.x<0?"":" ")+this.x.toFixed(digits)},${(this.y<0?"":" ")+this.y.toFixed(digits)} )`;else return`(${this.x}, ${this.y})`}isValid(){return isNumber(this.x)&&isNumber(this.y)}}function rgb(r,g,b,a){return new Color(r,g,b,a)}function hsl(h,s,l,a){return(new Color).setHSLA(h,s,l,a)}function isColor(c){return c instanceof Color&&c.isValid()}function ASSERT_COLOR_VALID(c){ASSERT(isColor(c),"Color is invalid.",c)}class Color{constructor(r=1,g=1,b=1,a=1){this.r=r;this.g=g;this.b=b;this.a=a;ASSERT(this.isValid(),"Constructed Color is invalid.",this)}set(r=1,g=1,b=1,a=1){this.r=r;this.g=g;this.b=b;this.a=a;ASSERT_COLOR_VALID(this);return this}setFrom(c){return this.set(c.r,c.g,c.b,c.a)}copy(){return new Color(this.r,this.g,this.b,this.a)}add(c){return new Color(this.r+c.r,this.g+c.g,this.b+c.b,this.a+c.a)}subtract(c){return new Color(this.r-c.r,this.g-c.g,this.b-c.b,this.a-c.a)}multiply(c){return new Color(this.r*c.r,this.g*c.g,this.b*c.b,this.a*c.a)}divide(c){return new Color(this.r/c.r,this.g/c.g,this.b/c.b,this.a/c.a)}scale(scale,alphaScale=scale){return new Color(this.r*scale,this.g*scale,this.b*scale,this.a*alphaScale)}clamp(){return new Color(clamp(this.r),clamp(this.g),clamp(this.b),clamp(this.a))}lerp(c,percent){ASSERT_COLOR_VALID(c);ASSERT_NUMBER_VALID(percent);const p=clamp(percent);return new Color(c.r*p+this.r*(1-p),c.g*p+this.g*(1-p),c.b*p+this.b*(1-p),c.a*p+this.a*(1-p))}setHSLA(h=0,s=0,l=1,a=1){h=mod(h,1);s=clamp(s);l=clamp(l);const q=l<.5?l*(1+s):l+s-l*s,p=2*l-q,f=(p,q,t)=>(t=mod(t,1))*6<1?p+(q-p)*6*t:t*2<1?q:t*3<2?p+(q-p)*(4-t*6):p;this.r=f(p,q,h+1/3);this.g=f(p,q,h);this.b=f(p,q,h-1/3);this.a=a;ASSERT_COLOR_VALID(this);return this}HSLA(){const r=clamp(this.r);const g=clamp(this.g);const b=clamp(this.b);const a=clamp(this.a);const maxC=max(r,g,b);const minC=min(r,g,b);const l=(maxC+minC)/2;let h=0,s=0;if(maxC!==minC){let d=maxC-minC;s=l>.5?d/(2-maxC-minC):d/(maxC+minC);if(r===maxC)h=(g-b)/d+(g<b?6:0);else if(g===maxC)h=(b-r)/d+2;else if(b===maxC)h=(r-g)/d+4}return[h/6,s,l,a]}mutate(amount=.05,alphaAmount=0){ASSERT_NUMBER_VALID(amount);ASSERT_NUMBER_VALID(alphaAmount);return new Color(this.r+rand(amount,-amount),this.g+rand(amount,-amount),this.b+rand(amount,-amount),this.a+rand(alphaAmount,-alphaAmount)).clamp()}toString(useAlpha=true){if(debug&&!this.isValid())return"#000";const toHex=c=>((c=clamp(c)*255|0)<16?"0":"")+c.toString(16);return"#"+toHex(this.r)+toHex(this.g)+toHex(this.b)+(useAlpha?toHex(this.a):"")}setHex(hex){ASSERT(isString(hex),"Color hex code must be a string");ASSERT(hex[0]==="#","Color hex code must start with #");ASSERT([4,5,7,9].includes(hex.length),"Invalid hex");if(hex.length<6){const fromHex=c=>clamp(parseInt(hex[c],16)/15);this.r=fromHex(1);this.g=fromHex(2);this.b=fromHex(3);this.a=hex.length===5?fromHex(4):1}else{const fromHex=c=>clamp(parseInt(hex.slice(c,c+2),16)/255);this.r=fromHex(1);this.g=fromHex(3);this.b=fromHex(5);this.a=hex.length===9?fromHex(7):1}ASSERT_COLOR_VALID(this);return this}rgbaInt(){const r=clamp(this.r)*255|0;const g=clamp(this.g)*255<<8;const b=clamp(this.b)*255<<16;const a=clamp(this.a)*255<<24;return r+g+b+a}isValid(){return isNumber(this.r)&&isNumber(this.g)&&isNumber(this.b)&&isNumber(this.a)}}const WHITE=debugProtectConstant(rgb());const CLEAR_WHITE=debugProtectConstant(rgb(1,1,1,0));const BLACK=debugProtectConstant(rgb(0,0,0));const CLEAR_BLACK=debugProtectConstant(rgb(0,0,0,0));const GRAY=debugProtectConstant(rgb(.5,.5,.5));const RED=debugProtectConstant(rgb(1,0,0));const ORANGE=debugProtectConstant(rgb(1,.5,0));const YELLOW=debugProtectConstant(rgb(1,1,0));const GREEN=debugProtectConstant(rgb(0,1,0));const CYAN=debugProtectConstant(rgb(0,1,1));const BLUE=debugProtectConstant(rgb(0,0,1));const PURPLE=debugProtectConstant(rgb(.5,0,1));const MAGENTA=debugProtectConstant(rgb(1,0,1));class Timer{constructor(timeLeft,useRealTime=false){ASSERT(timeLeft===undefined||isNumber(timeLeft),"Constructed Timer is invalid.",timeLeft);this.useRealTime=useRealTime;const globalTime=this.getGlobalTime();this.time=timeLeft===undefined?undefined:globalTime+timeLeft;this.setTime=timeLeft}set(timeLeft=0){ASSERT(isNumber(timeLeft),"Timer is invalid.",timeLeft);const globalTime=this.getGlobalTime();this.time=globalTime+timeLeft;this.setTime=timeLeft}setUseRealTime(useRealTime=true){ASSERT(!this.isSet(),"Cannot change global time setting while timer is set.");this.useRealTime=useRealTime}unset(){this.time=undefined}isSet(){return this.time!==undefined}active(){return this.getGlobalTime()<this.time}elapsed(){return this.getGlobalTime()>=this.time}get(){return this.isSet()?this.getGlobalTime()-this.time:0}getPercent(){return this.isSet()?1-percent(this.time-this.getGlobalTime(),0,this.setTime):0}getSetTime(){return this.isSet()?this.setTime:0}getGlobalTime(){return this.useRealTime?timeReal:time}toString(){return this.isSet()?abs(this.get())+" seconds "+(this.get()<0?"before":"after"):"unset"}valueOf(){return this.get()}}function formatTime(t){const sign=t<0?"-":"";t=abs(t)|0;return sign+(t/60|0)+":"+(t%60<10?"0":"")+t%60}async function fetchJSON(url){const response=await fetch(url);if(!response.ok)throw new Error(`Failed to fetch JSON from ${url}: ${response.status} ${response.statusText}`);return response.json()}function saveText(text,filename="text",type="text/plain"){saveDataURL(URL.createObjectURL(new Blob([text],{type:type})),filename)}function saveCanvas(canvas,filename="screenshot",type="image/png"){if(canvas instanceof OffscreenCanvas){const saveCanvas=document.createElement("canvas");saveCanvas.width=canvas.width;saveCanvas.height=canvas.height;saveCanvas.getContext("2d").drawImage(canvas,0,0);saveDataURL(saveCanvas.toDataURL(type),filename)}else saveDataURL(canvas.toDataURL(type),filename)}function saveDataURL(url,filename="download",revokeTime){const link=document.createElement("a");link.download=filename;link.href=url;link.click();if(revokeTime!==undefined)setTimeout(()=>URL.revokeObjectURL(url),revokeTime)}let cameraPos=vec2();let cameraAngle=0;let cameraScale=32;let canvasColorTiles=true;let canvasClearColor=CLEAR_BLACK;let canvasMaxSize=vec2(1920,1080);let canvasMinAspect=0;let canvasMaxAspect=0;let canvasFixedSize=vec2();let canvasPixelated=false;let tilesPixelated=true;let fontDefault="arial";let showSplashScreen=false;let headlessMode=false;let glEnable=true;let glCircleSides=32;let tileDefaultSize=vec2(16);let tileDefaultPadding=0;let tileDefaultBleed=0;let enablePhysicsSolver=true;let objectDefaultMass=1;let objectDefaultDamping=1;let objectDefaultAngleDamping=1;let objectDefaultRestitution=0;let objectDefaultFriction=.8;let objectMaxSpeed=1;let gravity=vec2();let particleEmitRateScale=1;let gamepadsEnable=true;let gamepadDirectionEmulateStick=true;let inputWASDEmulateDirection=true;let touchInputEnable=true;let touchGamepadEnable=false;let touchGamepadCenterButton=true;let touchGamepadButtonCount=4;let touchGamepadAnalog=true;let touchGamepadSize=99;let touchGamepadAlpha=.3;let vibrateEnable=true;let soundEnable=true;let soundVolume=.3;let soundDefaultRange=40;let soundDefaultTaper=.7;let medalDisplayTime=5;let medalDisplaySlideTime=.5;let medalDisplaySize=vec2(640,80);let medalsPreventUnlock=false;function setCameraPos(pos){cameraPos=pos.copy()}function setCameraAngle(angle){cameraAngle=angle}function setCameraScale(scale){cameraScale=scale}function setCanvasColorTiles(colorTiles){canvasColorTiles=colorTiles}function setCanvasClearColor(color){canvasClearColor=color.copy()}function setCanvasMaxSize(size){canvasMaxSize=size.copy()}function setCanvasMinAspect(aspect){canvasMinAspect=aspect}function setCanvasMaxAspect(aspect){canvasMaxAspect=aspect}function setCanvasFixedSize(size){canvasFixedSize=size.copy()}function setCanvasPixelated(pixelated){canvasPixelated=pixelated;if(mainCanvas)mainCanvas.style.imageRendering=pixelated?"pixelated":"";if(glCanvas)glCanvas.style.imageRendering=pixelated?"pixelated":""}function setTilesPixelated(pixelated){tilesPixelated=pixelated}function setFontDefault(font){fontDefault=font}function setShowSplashScreen(show){showSplashScreen=show}function setHeadlessMode(headless){headlessMode=headless}function setGLEnable(enable){if(enable&&!glCanBeEnabled){console.warn("Can not enable WebGL if it was disabled on start.");return}glEnable=enable;if(glCanvas)glCanvas.style.display=enable?"":"none"}function setGLCircleSides(sides){glCircleSides=sides}function setTileDefaultSize(size){tileDefaultSize=size.copy()}function setTileDefaultPadding(padding){tileDefaultPadding=padding}function setTileDefaultBleed(bleed){tileDefaultBleed=bleed}function setEnablePhysicsSolver(enable){enablePhysicsSolver=enable}function setObjectDefaultMass(mass){objectDefaultMass=mass}function setObjectDefaultDamping(damp){objectDefaultDamping=damp}function setObjectDefaultAngleDamping(damp){objectDefaultAngleDamping=damp}function setObjectDefaultRestitution(restitution){objectDefaultRestitution=restitution}function setObjectDefaultFriction(friction){objectDefaultFriction=friction}function setObjectMaxSpeed(speed){objectMaxSpeed=speed}function setGravity(newGravity){gravity=newGravity.copy()}function setParticleEmitRateScale(scale){particleEmitRateScale=scale}function setGamepadsEnable(enable){gamepadsEnable=enable}function setGamepadDirectionEmulateStick(enable){gamepadDirectionEmulateStick=enable}function setInputWASDEmulateDirection(enable){inputWASDEmulateDirection=enable}function setTouchInputEnable(enable){touchInputEnable=enable}function setTouchGamepadEnable(enable){touchGamepadEnable=enable}function setTouchGamepadCenterButton(enable){touchGamepadCenterButton=enable}function setTouchGamepadButtonCount(count){touchGamepadButtonCount=count}function setTouchGamepadAnalog(analog){touchGamepadAnalog=analog}function setTouchGamepadSize(size){touchGamepadSize=size}function setTouchGamepadAlpha(alpha){touchGamepadAlpha=alpha}function setVibrateEnable(enable){vibrateEnable=enable}function setSoundEnable(enable){soundEnable=enable}function setSoundVolume(volume){soundVolume=volume;if(soundEnable&&!headlessMode&&audioMasterGain)audioMasterGain.gain.value=volume}function setSoundDefaultRange(range){soundDefaultRange=range}function setSoundDefaultTaper(taper){soundDefaultTaper=taper}function setMedalDisplayTime(time){medalDisplayTime=time}function setMedalDisplaySlideTime(time){medalDisplaySlideTime=time}function setMedalDisplaySize(size){medalDisplaySize=size.copy()}function setMedalsPreventUnlock(preventUnlock){medalsPreventUnlock=preventUnlock}function setDebugWatermark(show){debugWatermark=show}function setDebugKey(key){debugKey=key}class EngineObject{constructor(pos=vec2(),size=vec2(1),tileInfo,angle=0,color=WHITE,renderOrder=0){ASSERT(isVector2(pos),"object pos must be a vec2");ASSERT(isVector2(size),"object size must be a vec2");ASSERT(!tileInfo||tileInfo instanceof TileInfo,"object tileInfo should be a TileInfo or undefined");ASSERT(typeof angle==="number"&&isFinite(angle),"object angle should be a number");ASSERT(isColor(color),"object color should be a valid rgba color");ASSERT(typeof renderOrder==="number","object renderOrder should be a number");this.pos=pos.copy();this.size=size.copy();this.drawSize=undefined;this.tileInfo=tileInfo;this.angle=angle;this.color=color.copy();this.additiveColor=undefined;this.mirror=false;this.mass=objectDefaultMass;this.damping=objectDefaultDamping;this.angleDamping=objectDefaultAngleDamping;this.restitution=objectDefaultRestitution;this.friction=objectDefaultFriction;this.gravityScale=1;this.renderOrder=renderOrder;this.velocity=vec2();this.angleVelocity=0;this.spawnTime=time;this.children=[];this.clampSpeed=true;this.groundObject=undefined;this.parent=undefined;this.localPos=vec2();this.localAngle=0;this.collideTiles=false;this.collideSolidObjects=false;this.isSolid=false;this.collideRaycast=false;engineObjects.push(this)}updateTransforms(){const parent=this.parent;if(parent){const mirror=parent.getMirrorSign();this.pos=this.localPos.multiply(vec2(mirror,1)).rotate(parent.angle).add(parent.pos);this.angle=mirror*this.localAngle+parent.angle}for(const child of this.children)child.updateTransforms()}updatePhysics(){ASSERT(!this.parent);if(this.clampSpeed){this.velocity.x=clamp(this.velocity.x,-objectMaxSpeed,objectMaxSpeed);this.velocity.y=clamp(this.velocity.y,-objectMaxSpeed,objectMaxSpeed)}const oldPos=this.pos.copy();this.velocity.x*=this.damping;this.velocity.y*=this.damping;if(this.mass){this.velocity.x+=gravity.x*this.gravityScale;this.velocity.y+=gravity.y*this.gravityScale}this.pos.x+=this.velocity.x;this.pos.y+=this.velocity.y;this.angle+=this.angleVelocity*=this.angleDamping;ASSERT(this.angleDamping>=0&&this.angleDamping<=1);ASSERT(this.damping>=0&&this.damping<=1);if(!enablePhysicsSolver||!this.mass)return;const wasFalling=this.velocity.y<0&&gravity.y<0||this.velocity.y>0&&gravity.y>0;if(this.groundObject){const friction=max(this.friction,this.groundObject.friction);const groundSpeed=this.groundObject.velocity?this.groundObject.velocity.x:0;this.velocity.x=groundSpeed+(this.velocity.x-groundSpeed)*friction;this.groundObject=undefined}if(this.collideSolidObjects){const epsilon=.001;for(const o of engineObjectsCollide){if(!this.isSolid&&!o.isSolid||o.destroyed||o.parent||o===this)continue;if(!this.isOverlappingObject(o))continue;const collide1=this.collideWithObject(o);const collide2=o.collideWithObject(this);if(!collide1||!collide2)continue;if(isOverlapping(oldPos,this.size,o.pos,o.size)){const deltaPos=oldPos.subtract(o.pos);const length=deltaPos.length();const pushAwayAccel=.001;const velocity=length<.01?randVec2(pushAwayAccel):deltaPos.scale(pushAwayAccel/length);this.velocity=this.velocity.add(velocity);if(o.mass)o.velocity=o.velocity.subtract(velocity);debugPhysics&&debugOverlap(this.pos,this.size,o.pos,o.size,"#f00");continue}const sizeBoth=this.size.add(o.size);const smallStepUp=(oldPos.y-o.pos.y)*2>sizeBoth.y+gravity.y;const isBlockedX=abs(oldPos.y-o.pos.y)*2<sizeBoth.y;const isBlockedY=abs(oldPos.x-o.pos.x)*2<sizeBoth.x;const restitution=max(this.restitution,o.restitution);if(smallStepUp||isBlockedY||!isBlockedX){this.pos.y=o.pos.y+(sizeBoth.y/2+epsilon)*sign(oldPos.y-o.pos.y);if(o.groundObject&&wasFalling||!o.mass){if(wasFalling)this.groundObject=o;this.velocity.y*=-restitution}else if(o.mass){const inelastic=(this.mass*this.velocity.y+o.mass*o.velocity.y)/(this.mass+o.mass);const elastic0=this.velocity.y*(this.mass-o.mass)/(this.mass+o.mass)+o.velocity.y*2*o.mass/(this.mass+o.mass);const elastic1=o.velocity.y*(o.mass-this.mass)/(this.mass+o.mass)+this.velocity.y*2*this.mass/(this.mass+o.mass);this.velocity.y=lerp(inelastic,elastic0,restitution);o.velocity.y=lerp(inelastic,elastic1,restitution)}}if(!smallStepUp&&isBlockedX){this.pos.x=o.pos.x+(sizeBoth.x/2+epsilon)*sign(oldPos.x-o.pos.x);if(o.mass){const inelastic=(this.mass*this.velocity.x+o.mass*o.velocity.x)/(this.mass+o.mass);const elastic0=this.velocity.x*(this.mass-o.mass)/(this.mass+o.mass)+o.velocity.x*2*o.mass/(this.mass+o.mass);const elastic1=o.velocity.x*(o.mass-this.mass)/(this.mass+o.mass)+this.velocity.x*2*this.mass/(this.mass+o.mass);this.velocity.x=lerp(inelastic,elastic0,restitution);o.velocity.x=lerp(inelastic,elastic1,restitution)}else this.velocity.x*=-restitution}debugPhysics&&debugOverlap(this.pos,this.size,o.pos,o.size,"#f0f")}}if(this.collideTiles){const hitLayer=tileCollisionTest(this.pos,this.size,this);if(hitLayer){if(!tileCollisionTest(oldPos,this.size,this)){const blockedLayerY=tileCollisionTest(vec2(oldPos.x,this.pos.y),this.size,this);const blockedLayerX=tileCollisionTest(vec2(this.pos.x,oldPos.y),this.size,this);if(blockedLayerX){const epsilon=.001;const maxMoveUp=.1;const y=floor(oldPos.y-this.size.y/2+1)+this.size.y/2+epsilon;const delta=y-this.pos.y;if(delta<maxMoveUp)if(!tileCollisionTest(vec2(this.pos.x,y),this.size,this)){this.pos.y=y;debugPhysics&&debugRect(this.pos,this.size,"#ff0");return}this.pos.x=oldPos.x;this.velocity.x*=-this.restitution}if(blockedLayerY||!blockedLayerX){const restitution=max(this.restitution,hitLayer.restitution);this.velocity.y*=-restitution;if(wasFalling){const epsilon=1e-4;const offset=this.size.y/2+epsilon;this.pos.y=gravity.y<0?floor(oldPos.y-this.size.y/2)+offset:ceil(oldPos.y+this.size.y/2)-offset;this.groundObject=hitLayer}else{this.pos.y=oldPos.y;this.groundObject=undefined}}debugPhysics&&debugRect(this.pos,this.size,"#f00")}}}}update(){}render(){drawTile(this.pos,this.drawSize||this.size,this.tileInfo,this.color,this.angle,this.mirror,this.additiveColor)}destroy(){if(this.destroyed)return;this.destroyed=1;this.parent&&this.parent.removeChild(this);for(const child of this.children){child.parent=0;child.destroy()}}localToWorld(pos){return this.pos.add(pos.rotate(this.angle))}worldToLocal(pos){return pos.subtract(this.pos).rotate(-this.angle)}localToWorldVector(vec){return vec.rotate(this.angle)}worldToLocalVector(vec){return vec.rotate(-this.angle)}collideWithTile(tileData,pos){return tileData>0}collideWithObject(object){return true}getUp(scale=1){return vec2().setAngle(this.angle,scale)}getRight(scale=1){return vec2().setAngle(this.angle+PI/2,scale)}getAliveTime(){return time-this.spawnTime}getSpeed(){return this.velocity.length()}applyAcceleration(acceleration){if(this.mass)this.velocity=this.velocity.add(acceleration)}applyAngularAcceleration(acceleration){if(this.mass)this.angleVelocity+=acceleration}applyForce(force){if(this.mass)this.applyAcceleration(force.scale(1/this.mass))}getMirrorSign(){return this.mirror?-1:1}addChild(child,localPos=vec2(),localAngle=0){ASSERT(!child.parent&&!this.children.includes(child));ASSERT(child instanceof EngineObject,"child must be an EngineObject");ASSERT(child!==this,"cannot add self as child");this.children.push(child);child.parent=this;child.localPos=localPos.copy();child.localAngle=localAngle;return child}removeChild(child){ASSERT(child.parent===this&&this.children.includes(child));ASSERT(child instanceof EngineObject,"child must be an EngineObject");const index=this.children.indexOf(child);ASSERT(index>=0,"child not found in children array");index>=0&&this.children.splice(index,1);child.parent=0}isOverlappingObject(object){return this.isOverlapping(object.pos,object.size)}isOverlapping(pos,size=vec2()){return isOverlapping(this.pos,this.size,pos,size)}setCollision(collideSolidObjects=true,isSolid=true,collideTiles=true,collideRaycast=true){ASSERT(collideSolidObjects||!isSolid,"solid objects must be set to collide");this.collideSolidObjects=collideSolidObjects;this.isSolid=isSolid;this.collideTiles=collideTiles;this.collideRaycast=collideRaycast}toString(){if(!debug)return;let text="type = "+this.constructor.name;if(this.pos.x||this.pos.y)text+="\npos = "+this.pos;if(this.velocity.x||this.velocity.y)text+="\nvelocity = "+this.velocity;if(this.size.x||this.size.y)text+="\nsize = "+this.size;if(this.angle)text+="\nangle = "+this.angle.toFixed(3);if(this.color)text+="\ncolor = "+this.color;return text}renderDebugInfo(){if(!debug)return;const size=vec2(max(this.size.x,.2),max(this.size.y,.2));const color=rgb(this.collideTiles?1:0,this.collideSolidObjects?1:0,this.isSolid?1:0,.5);drawRect(this.pos,size,color,this.angle);if(this.parent)drawRect(this.pos,size.scale(.8),rgb(1,1,1,.5),this.angle);this.parent&&drawLine(this.pos,this.parent.pos,.1,rgb(1,1,1,.5))}}let mainCanvas;let mainContext;let drawContext;let workCanvas;let workContext;let workReadCanvas;let workReadContext;let mainCanvasSize=vec2();let textureInfos=[];let drawCount;function tile(pos=new Vector2,size=tileDefaultSize,textureIndex=0,padding=tileDefaultPadding){if(headlessMode)return new TileInfo;if(typeof size==="number"){ASSERT(size>0);size=new Vector2(size,size)}const tileInfo=new TileInfo(new Vector2,size,textureIndex,padding);const textureInfo=textureInfos[textureIndex];ASSERT(!!textureInfo,"Texture not loaded");const sizePaddedX=size.x+padding*2;const sizePaddedY=size.y+padding*2;if(typeof pos==="number"){const cols=textureInfo.size.x/sizePaddedX|0;ASSERT(cols>0,"Tile size is too big for texture");const posX=pos%cols,posY=pos/cols|0;tileInfo.pos.set(posX*sizePaddedX+padding,posY*sizePaddedY+padding)}else tileInfo.pos.set(pos.x*sizePaddedX+padding,pos.y*sizePaddedY+padding);return tileInfo}class TileInfo{constructor(pos=vec2(),size=tileDefaultSize,textureIndex=0,padding=tileDefaultPadding,bleed=tileDefaultBleed){this.pos=pos.copy();this.size=size.copy();this.textureIndex=textureIndex;this.padding=padding;this.textureInfo=textureInfos[this.textureIndex];this.bleed=bleed}offset(offset){return new TileInfo(this.pos.add(offset),this.size,this.textureIndex,this.padding,this.bleed)}frame(frame){ASSERT(typeof frame==="number");return this.offset(new Vector2(frame*(this.size.x+this.padding*2),0))}setFullImage(textureInfo){this.pos=new Vector2;this.size=textureInfo.size.copy();this.textureInfo=textureInfo;this.bleed=this.padding=0;return this}}class TextureInfo{constructor(image,useWebGL=true){this.image=image;this.size=image?vec2(image.width,image.height):vec2();this.sizeInverse=image?vec2(1/image.width,1/image.height):vec2();this.glTexture=undefined;useWebGL&&this.createWebGLTexture()}createWebGLTexture(){glRegisterTextureInfo(this)}destroyWebGLTexture(){glUnregisterTextureInfo(this)}hasWebGL(){return!!this.glTexture}}function drawTile(pos,size=new Vector2(1),tileInfo,color=WHITE,angle=0,mirror,additiveColor,useWebGL=glEnable,screenSpace,context){ASSERT(isVector2(pos),"pos must be a vec2");ASSERT(isVector2(size),"size must be a vec2");ASSERT(isColor(color),"color is invalid");ASSERT(isNumber(angle),"angle must be a number");ASSERT(!additiveColor||isColor(additiveColor),"additiveColor must be a color");ASSERT(!context||!useWebGL,"context only supported in canvas 2D mode");const textureInfo=tileInfo&&tileInfo.textureInfo;const bleed=tileInfo?.bleed??0;if(useWebGL&&glEnable){ASSERT(!!glContext,"WebGL is not enabled!");if(screenSpace)[pos,size,angle]=screenToWorldTransform(pos,size,angle);if(textureInfo){const sizeInverse=textureInfo.sizeInverse;const x=tileInfo.pos.x*sizeInverse.x;const y=tileInfo.pos.y*sizeInverse.y;const w=tileInfo.size.x*sizeInverse.x;const h=tileInfo.size.y*sizeInverse.y;glSetTexture(textureInfo.glTexture);if(bleed){const bleedX=sizeInverse.x*bleed;const bleedY=sizeInverse.y*bleed;glDraw(pos.x,pos.y,mirror?-size.x:size.x,size.y,angle,x+bleedX,y+bleedY,x-bleedX+w,y-bleedY+h,color.rgbaInt(),additiveColor&&additiveColor.rgbaInt())}else{glDraw(pos.x,pos.y,mirror?-size.x:size.x,size.y,angle,x,y,x+w,y+h,color.rgbaInt(),additiveColor&&additiveColor.rgbaInt())}}else{glDraw(pos.x,pos.y,size.x,size.y,angle,0,0,0,0,0,color.rgbaInt())}}else{++drawCount;size=new Vector2(size.x,-size.y);drawCanvas2D(pos,size,angle,mirror,context=>{if(textureInfo){const x=tileInfo.pos.x,y=tileInfo.pos.y;const w=tileInfo.size.x,h=tileInfo.size.y;drawImageColor(context,textureInfo.image,x,y,w,h,-.5,-.5,1,1,color,additiveColor,bleed)}else{const c=additiveColor?color.add(additiveColor):color;context.fillStyle=c.toString();context.fillRect(-.5,-.5,1,1)}},screenSpace,context)}}function drawRect(pos,size,color,angle,useWebGL,screenSpace,context){drawTile(pos,size,undefined,color,angle,false,undefined,useWebGL,screenSpace,context)}function drawRectGradient(pos,size,colorTop=WHITE,colorBottom=BLACK,angle=0,useWebGL=glEnable,screenSpace=false,context){ASSERT(isVector2(pos),"pos must be a vec2");ASSERT(isVector2(size),"size must be a vec2");ASSERT(isColor(colorTop)&&isColor(colorBottom),"color is invalid");ASSERT(isNumber(angle),"angle must be a number");ASSERT(!context||!useWebGL,"context only supported in canvas 2D mode");if(useWebGL&&glEnable){ASSERT(!!glContext,"WebGL is not enabled!");if(screenSpace){pos=screenToWorld(pos);size=size.scale(1/cameraScale);angle+=cameraAngle}const points=[],colors=[];const halfSizeX=size.x/2,halfSizeY=size.y/2;const colorTopInt=colorTop.rgbaInt();const colorBottomInt=colorBottom.rgbaInt();const c=cos(-angle),s=sin(-angle);for(let i=4;i--;){const x=i&1?halfSizeX:-halfSizeX;const y=i&2?halfSizeY:-halfSizeY;const rx=x*c-y*s;const ry=x*s+y*c;const color=i&2?colorTopInt:colorBottomInt;points.push(vec2(pos.x+rx,pos.y+ry));colors.push(color)}glDrawColoredPoints(points,colors)}else{++drawCount;size=new Vector2(size.x,-size.y);drawCanvas2D(pos,size,angle,false,context=>{const gradient=context.createLinearGradient(0,-.5,0,.5);gradient.addColorStop(0,colorTop.toString());gradient.addColorStop(1,colorBottom.toString());context.fillStyle=gradient;context.fillRect(-.5,-.5,1,1)},screenSpace,context)}}function drawLineList(points,width=.1,color,wrap=false,pos=vec2(),angle=0,useWebGL=glEnable,screenSpace,context){ASSERT(isArray(points),"points must be an array");ASSERT(isNumber(width),"width must be a number");ASSERT(isColor(color),"color is invalid");ASSERT(isVector2(pos),"pos must be a vec2");ASSERT(isNumber(angle),"angle must be a number");ASSERT(!context||!useWebGL,"context only supported in canvas 2D mode");if(useWebGL&&glEnable){ASSERT(!!glContext,"WebGL is not enabled!");let size=vec2(1);if(screenSpace)[pos,size,angle]=screenToWorldTransform(pos,size,angle);glDrawOutlineTransform(points,color.rgbaInt(),width,pos.x,pos.y,size.x,size.y,angle,wrap)}else{++drawCount;drawCanvas2D(pos,vec2(1),angle,false,context=>{context.strokeStyle=color.toString();context.lineWidth=width;context.beginPath();for(let i=0;i<points.length;++i){const point=points[i];if(i)context.lineTo(point.x,point.y);else context.moveTo(point.x,point.y)}if(wrap)context.closePath();context.stroke()},screenSpace,context)}}function drawLine(posA,posB,width=.1,color,pos=vec2(),angle=0,useWebGL,screenSpace,context){const halfDelta=vec2((posB.x-posA.x)/2,(posB.y-posA.y)/2);const size=vec2(width,halfDelta.length()*2);pos=pos.add(posA.add(halfDelta));if(screenSpace)halfDelta.y*=-1;angle+=halfDelta.angle();drawRect(pos,size,color,angle,useWebGL,screenSpace,context)}function drawRegularPoly(pos,size=vec2(1),sides=3,color=WHITE,lineWidth=0,lineColor=BLACK,angle=0,useWebGL=glEnable,screenSpace=false,context){ASSERT(isVector2(size),"size must be a vec2");ASSERT(isNumber(sides),"sides must be a number");const points=[];const sizeX=size.x/2,sizeY=size.y/2;for(let i=sides;i--;){const a=i/sides*PI*2;points.push(vec2(sin(a)*sizeX,cos(a)*sizeY))}drawPoly(points,color,lineWidth,lineColor,pos,angle,useWebGL,screenSpace,context)}function drawPoly(points,color=WHITE,lineWidth=0,lineColor=BLACK,pos=vec2(),angle=0,useWebGL=glEnable,screenSpace=false,context=undefined){ASSERT(isVector2(pos),"pos must be a vec2");ASSERT(isArray(points),"points must be an array");ASSERT(isColor(color)&&isColor(lineColor),"color is invalid");ASSERT(isNumber(lineWidth),"lineWidth must be a number");ASSERT(isNumber(angle),"angle must be a number");ASSERT(!context||!useWebGL,"context only supported in canvas 2D mode");if(useWebGL&&glEnable){ASSERT(!!glContext,"WebGL is not enabled!");let size=vec2(1);if(screenSpace)[pos,size,angle]=screenToWorldTransform(pos,size,angle);glDrawPointsTransform(points,color.rgbaInt(),pos.x,pos.y,size.x,size.y,angle);if(lineWidth>0)glDrawOutlineTransform(points,lineColor.rgbaInt(),lineWidth,pos.x,pos.y,size.x,size.y,angle)}else{drawCanvas2D(pos,vec2(1),angle,false,context=>{context.fillStyle=color.toString();context.beginPath();for(const point of points)context.lineTo(point.x,point.y);context.closePath();context.fill();if(lineWidth){context.strokeStyle=lineColor.toString();context.lineWidth=lineWidth;context.stroke()}},screenSpace,context)}}function drawEllipse(pos,size=vec2(1),color=WHITE,angle=0,lineWidth=0,lineColor=BLACK,useWebGL=glEnable,screenSpace=false,context){ASSERT(isVector2(pos),"pos must be a vec2");ASSERT(isVector2(size),"size must be a vec2");ASSERT(isColor(color)&&isColor(lineColor),"color is invalid");ASSERT(isNumber(angle),"angle must be a number");ASSERT(isNumber(lineWidth),"lineWidth must be a number");ASSERT(lineWidth>=0&&lineWidth<size.x&&lineWidth<size.y,"invalid lineWidth");ASSERT(!context||!useWebGL,"context only supported in canvas 2D mode");if(useWebGL&&glEnable){const sides=glCircleSides;drawRegularPoly(pos,size,sides,color,lineWidth,lineColor,angle,useWebGL,screenSpace,context)}else{drawCanvas2D(pos,vec2(1),angle,false,context=>{context.fillStyle=color.toString();context.beginPath();context.ellipse(0,0,size.x/2,size.y/2,0,0,9);context.fill();if(lineWidth){context.strokeStyle=lineColor.toString();context.lineWidth=lineWidth;context.stroke()}},screenSpace,context)}}function drawCircle(pos,size=1,color=WHITE,lineWidth=0,lineColor=BLACK,useWebGL=glEnable,screenSpace=false,context){ASSERT(isNumber(size),"size must be a number");drawEllipse(pos,vec2(size),color,0,lineWidth,lineColor,useWebGL,screenSpace,context)}function drawCanvas2D(pos,size,angle=0,mirror=false,drawFunction,screenSpace=false,context=drawContext){ASSERT(isVector2(pos),"pos must be a vec2");ASSERT(isVector2(size),"size must be a vec2");ASSERT(isNumber(angle),"angle must be a number");ASSERT(typeof drawFunction==="function","drawFunction must be a function");if(!screenSpace){pos=worldToScreen(pos);size=size.scale(cameraScale);angle-=cameraAngle}context.save();context.translate(pos.x+.5,pos.y+.5);context.rotate(angle);context.scale(mirror?-size.x:size.x,-size.y);drawFunction(context);context.restore()}function drawText(text,pos,size=1,color,lineWidth=0,lineColor,textAlign,font,fontStyle,maxWidth,angle=0,context=drawContext){pos=worldToScreen(pos);size*=cameraScale;lineWidth*=cameraScale;angle-=cameraAngle;angle*=-1;drawTextScreen(text,pos,size,color,lineWidth,lineColor,textAlign,font,fontStyle,maxWidth,angle,context)}function drawTextScreen(text,pos,size=1,color=WHITE,lineWidth=0,lineColor=BLACK,textAlign="center",font=fontDefault,fontStyle="",maxWidth,angle=0,context=drawContext){ASSERT(isString(text),"text must be a string");ASSERT(isVector2(pos),"pos must be a vec2");ASSERT(isNumber(size),"size must be a number");ASSERT(isColor(color),"color must be a color");ASSERT(isNumber(lineWidth),"lineWidth must be a number");ASSERT(isColor(lineColor),"lineColor must be a color");ASSERT(["left","center","right"].includes(textAlign),"align must be left, center, or right");ASSERT(isString(font),"font must be a string");ASSERT(isString(fontStyle),"fontStyle must be a string");ASSERT(isNumber(angle),"angle must be a number");context.fillStyle=color.toString();context.strokeStyle=lineColor.toString();context.lineWidth=lineWidth;context.textAlign=textAlign;context.font=fontStyle+" "+size+"px "+font;context.textBaseline="middle";const lines=(text+"").split("\n");const posY=pos.y-(lines.length-1)*size/2;context.save();context.translate(pos.x,posY);context.rotate(-angle);let yOffset=0;lines.forEach(line=>{lineWidth&&context.strokeText(line,0,yOffset,maxWidth);context.fillText(line,0,yOffset,maxWidth);yOffset+=size});context.restore()}function screenToWorld(screenPos){ASSERT(isVector2(screenPos),"screenPos must be a vec2");let x=(screenPos.x-mainCanvasSize.x/2+.5)/cameraScale;let y=(screenPos.y-mainCanvasSize.y/2+.5)/-cameraScale;if(cameraAngle){const c=cos(-cameraAngle),s=sin(-cameraAngle);const xr=x*c-y*s,yr=x*s+y*c;x=xr;y=yr}return new Vector2(x+cameraPos.x,y+cameraPos.y)}function worldToScreen(worldPos){ASSERT(isVector2(worldPos),"worldPos must be a vec2");let x=worldPos.x-cameraPos.x;let y=worldPos.y-cameraPos.y;if(cameraAngle){const c=cos(cameraAngle),s=sin(cameraAngle);const xr=x*c-y*s,yr=x*s+y*c;x=xr;y=yr}return new Vector2(x*cameraScale+mainCanvasSize.x/2-.5,y*-cameraScale+mainCanvasSize.y/2-.5)}function screenToWorldDelta(screenDelta){ASSERT(isVector2(screenDelta),"screenDelta must be a vec2");let x=screenDelta.x/cameraScale;let y=screenDelta.y/-cameraScale;if(cameraAngle){const c=cos(-cameraAngle),s=sin(-cameraAngle);const xr=x*c-y*s,yr=x*s+y*c;x=xr;y=yr}return new Vector2(x,y)}function worldToScreenDelta(worldDelta){ASSERT(isVector2(worldDelta),"worldDelta must be a vec2");let x=worldDelta.x;let y=worldDelta.y;if(cameraAngle){const c=cos(cameraAngle),s=sin(cameraAngle);const xr=x*c-y*s,yr=x*s+y*c;x=xr;y=yr}return new Vector2(x*cameraScale,y*-cameraScale)}function screenToWorldTransform(screenPos,screenSize,screenAngle=0){ASSERT(isVector2(screenPos),"screenPos must be a vec2");ASSERT(isVector2(screenSize),"screenSize must be a vec2");ASSERT(isNumber(screenAngle),"screenAngle must be a number");return[screenToWorld(screenPos),screenSize.scale(1/cameraScale),screenAngle+cameraAngle]}function getCameraSize(){return mainCanvasSize.scale(1/cameraScale)}function isOnScreen(pos,size=0){ASSERT(isVector2(pos),"pos must be a vec2");ASSERT(isVector2(size)||isNumber(size),"size must be a vec2 or number");let x=pos.x-cameraPos.x;let y=pos.y-cameraPos.y;if(cameraAngle){const c=cos(cameraAngle),s=sin(cameraAngle);const xr=x*c-y*s,yr=x*s+y*c;x=xr;y=yr}x*=cameraScale*2;y*=-cameraScale*2;if(size instanceof Vector2)size=size.length();size*=cameraScale;const w=mainCanvasSize.x,h=mainCanvasSize.y;return x+size>-w&&x-size<w&&y+size>-h&&y-size<h}function setBlendMode(additive=false,context){glAdditive=additive;context||=drawContext;context.globalCompositeOperation=additive?"lighter":"source-over"}function combineCanvases(){const w=mainCanvasSize.x,h=mainCanvasSize.y;workCanvas.width=w;workCanvas.height=h;workContext.fillRect(0,0,w,h);glCopyToContext(workContext);workContext.drawImage(mainCanvas,0,0);mainCanvas.width|=0;mainContext.drawImage(workCanvas,0,0)}function drawImageColor(context,image,sx,sy,sWidth,sHeight,dx,dy,dWidth,dHeight,color,additiveColor,bleed=0){function isWhite(c){return c.r>=1&&c.g>=1&&c.b>=1}function isBlack(c){return c.r<=0&&c.g<=0&&c.b<=0&&c.a<=0}const sx2=bleed;const sy2=bleed;sWidth=max(1,sWidth|0);sHeight=max(1,sHeight|0);const sWidth2=sWidth-2*bleed;const sHeight2=sHeight-2*bleed;if(!canvasColorTiles||(additiveColor?isWhite(color.add(additiveColor))&&additiveColor.a<=0:isWhite(color))){context.globalAlpha=color.a;context.drawImage(image,sx+sx2,sy+sy2,sWidth2,sHeight2,dx,dy,dWidth,dHeight);context.globalAlpha=1}else{workReadCanvas.width=sWidth;workReadCanvas.height=sHeight;workReadContext.drawImage(image,sx|0,sy|0,sWidth,sHeight,0,0,sWidth,sHeight);const imageData=workReadContext.getImageData(0,0,sWidth,sHeight);const data=imageData.data;if(additiveColor&&!isBlack(additiveColor)){const colorMultiply=[color.r,color.g,color.b,color.a];const colorAdd=[additiveColor.r*255,additiveColor.g*255,additiveColor.b*255,additiveColor.a*255];for(let i=0;i<data.length;++i)data[i]=data[i]*colorMultiply[i&3]+colorAdd[i&3]|0;workReadContext.putImageData(imageData,0,0);context.drawImage(workReadCanvas,sx2,sy2,sWidth2,sHeight2,dx,dy,dWidth,dHeight)}else{for(let i=0;i<data.length;i+=4){data[i]*=color.r;data[i+1]*=color.g;data[i+2]*=color.b}workReadContext.putImageData(imageData,0,0);context.globalAlpha=color.a;context.drawImage(workReadCanvas,sx2,sy2,sWidth2,sHeight2,dx,dy,dWidth,dHeight);context.globalAlpha=1}}}function isFullscreen(){return!!document.fullscreenElement}function toggleFullscreen(){const rootElement=mainCanvas.parentElement;if(isFullscreen()){if(document.exitFullscreen)document.exitFullscreen()}else if(rootElement.requestFullscreen)rootElement.requestFullscreen()}function setCursor(cursorStyle="auto"){const rootElement=mainCanvas.parentElement;rootElement.style.cursor=cursorStyle}let engineFontImage;class FontImage{constructor(image,tileSize=vec2(8),paddingSize=vec2(0,1)){if(!image&&!engineFontImage){engineFontImage=new Image;engineFontImage.src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAYAQAAAAA9+x6JAAAAAnRSTlMAAHaTzTgAAAGiSURBVHjaZZABhxxBEIUf6ECLBdFY+Q0PMNgf0yCgsSAGZcT9sgIPtBWwIA5wgAPEoHUyJeeSlW+gjK+fegWwtROWpVQEyWh2npdpBmTUFVhb29RINgLIukoXr5LIAvYQ5ve+1FqWEMqNKTX3FAJHyQDRZvmKWubAACcv5z5Gtg2oyCWE+Yk/8JZQX1jTTCpKAFGIgza+dJCNBF2UskRlsgwitHbSV0QLgt9sTPtsRlvJjEr8C/FARWA2bJ/TtJ7lko34dNDn6usJUMzuErP89UUBJbWeozrwLLncXczd508deAjLWipLO4Q5XGPcJvPu92cNDaN0P5G1FL0nSOzddZOrJ6rNhbXGmeDvO3TF7DeJWl4bvaYQTNHCTeuqKZmbjHaSOFes+IX/+IhHrnAkXOAsfn24EM68XieIECoccD4KZLk/odiwzeo2rovYdhvb2HYFgyznJyDpYJdYOmfXgVdJTaUi4xA2uWYNYec9BLeqdl9EsoTw582mSFDX2DxVLbNt9U3YYoeatBad1c2Tj8t2akrjaIGJNywKB/7h75/gN3vCMSaadIUTAAAAAElFTkSuQmCC"}this.image=image||engineFontImage;this.tileSize=tileSize;this.paddingSize=paddingSize}drawText(text,pos,scale=1,center,context=drawContext){this.drawTextScreen(text,worldToScreen(pos).floor(),scale*cameraScale|0,center,context)}drawTextScreen(text,pos,scale=4,center=true,context=drawContext){context.save();const size=this.tileSize;const drawSize=size.add(this.paddingSize).scale(scale);const cols=this.image.width/this.tileSize.x|0;(text+"").split("\n").forEach((line,i)=>{const centerOffset=center?line.length*size.x*scale/2|0:0;for(let j=line.length;j--;){let charCode=line[j].charCodeAt(0);if(charCode<32||charCode>127)charCode=127;const tile=charCode-32;const x=tile%cols;const y=tile/cols|0;const drawPos=pos.add(vec2(j,i).multiply(drawSize));context.drawImage(this.image,x*size.x,y*size.y,size.x,size.y,drawPos.x-centerOffset,drawPos.y,size.x*scale,size.y*scale)}});context.restore()}}let mousePos=vec2();let mousePosScreen=vec2();let mouseDelta=vec2();let mouseDeltaScreen=vec2();let mouseWheel=0;let mouseInWindow=true;let isUsingGamepad=false;let inputPreventDefault=true;let gamepadPrimary=0;function setInputPreventDefault(preventDefault){inputPreventDefault=preventDefault}function inputClearKey(key,device=0,clearDown=true,clearPressed=true,clearReleased=true){if(!inputData[device])return;inputData[device][key]&=~((clearDown?1:0)|(clearPressed?2:0)|(clearReleased?4:0))}function inputClear(){inputData.length=0;inputData[0]=[];touchGamepadButtons.length=0;touchGamepadSticks.length=0;gamepadStickData.length=0;gamepadDpadData.length=0}function keyIsDown(key,device=0){ASSERT(isString(key),"key must be a number or string");ASSERT(device>0||typeof key!=="number"||key<3,"use code string for keyboard");return!!(inputData[device]?.[key]&1)}function keyWasPressed(key,device=0){ASSERT(isString(key),"key must be a number or string");ASSERT(device>0||typeof key!=="number"||key<3,"use code string for keyboard");return!!(inputData[device]?.[key]&2)}function keyWasReleased(key,device=0){ASSERT(isString(key),"key must be a number or string");ASSERT(device>0||typeof key!=="number"||key<3,"use code string for keyboard");return!!(inputData[device]?.[key]&4)}function keyDirection(up="ArrowUp",down="ArrowDown",left="ArrowLeft",right="ArrowRight"){ASSERT(isString(up),"up key must be a string");ASSERT(isString(down),"down key must be a string");ASSERT(isString(left),"left key must be a string");ASSERT(isString(right),"right key must be a string");const k=key=>keyIsDown(key)?1:0;return vec2(k(right)-k(left),k(up)-k(down))}function mouseIsDown(button){ASSERT(isNumber(button),"mouse button must be a number");return keyIsDown(button)}function mouseWasPressed(button){ASSERT(isNumber(button),"mouse button must be a number");return keyWasPressed(button)}function mouseWasReleased(button){ASSERT(isNumber(button),"mouse button must be a number");return keyWasReleased(button)}function gamepadIsDown(button,gamepad=gamepadPrimary){ASSERT(isNumber(button),"button must be a number");ASSERT(isNumber(gamepad),"gamepad must be a number");return keyIsDown(button,gamepad+1)}function gamepadWasPressed(button,gamepad=gamepadPrimary){ASSERT(isNumber(button),"button must be a number");ASSERT(isNumber(gamepad),"gamepad must be a number");return keyWasPressed(button,gamepad+1)}function gamepadWasReleased(button,gamepad=gamepadPrimary){ASSERT(isNumber(button),"button must be a number");ASSERT(isNumber(gamepad),"gamepad must be a number");return keyWasReleased(button,gamepad+1)}function gamepadStick(stick,gamepad=gamepadPrimary){ASSERT(isNumber(stick),"stick must be a number");ASSERT(isNumber(gamepad),"gamepad must be a number");return gamepadStickData[gamepad]?.[stick]??vec2()}function gamepadDpad(gamepad=gamepadPrimary){ASSERT(isNumber(gamepad),"gamepad must be a number");return gamepadDpadData[gamepad]??vec2()}function gamepadConnected(gamepad=gamepadPrimary){ASSERT(isNumber(gamepad),"gamepad must be a number");return!!inputData[gamepad+1]}function gamepadStickCount(gamepad=gamepadPrimary){ASSERT(isNumber(gamepad),"gamepad must be a number");return gamepadStickData[gamepad]?.length??0}const isTouchDevice=!headlessMode&&window.ontouchstart!==undefined;function vibrate(pattern=100){ASSERT(isNumber(pattern)||isArray(pattern),"pattern must be a number or array");vibrateEnable&&!headlessMode&&navigator&&navigator.vibrate&&navigator.vibrate(pattern)}function vibrateStop(){vibrate(0)}function pointerLockRequest(){!isTouchDevice&&mainCanvas.requestPointerLock?.()}function pointerLockExit(){document.exitPointerLock?.()}function pointerLockIsActive(){return document.pointerLockElement===mainCanvas}const inputData=[[]];const gamepadStickData=[],gamepadDpadData=[],gamepadHadInput=[];const touchGamepadTimer=new Timer,touchGamepadButtons=[],touchGamepadSticks=[];function inputInit(){if(headlessMode)return;document.addEventListener("keydown",onKeyDown);document.addEventListener("keyup",onKeyUp);document.addEventListener("mousedown",onMouseDown);document.addEventListener("mouseup",onMouseUp);document.addEventListener("mousemove",onMouseMove);document.addEventListener("mouseleave",onMouseLeave);document.addEventListener("wheel",onMouseWheel);document.addEventListener("contextmenu",onContextMenu);document.addEventListener("blur",onBlur);if(isTouchDevice&&touchInputEnable)touchInputInit();function onKeyDown(e){if(!e.repeat){isUsingGamepad=false;inputData[0][e.code]=3;if(inputWASDEmulateDirection)inputData[0][remapKey(e.code)]=3}const preventDefaultKeys=["ArrowUp","ArrowDown","ArrowLeft","ArrowRight","Space","Tab","Backspace"];if(preventDefaultKeys.includes(e.code))if(inputPreventDefault&&document.hasFocus()&&e.cancelable)e.preventDefault()}function onKeyUp(e){inputData[0][e.code]=inputData[0][e.code]&2|4;if(inputWASDEmulateDirection)inputData[0][remapKey(e.code)]=4}function remapKey(k){return inputWASDEmulateDirection?k==="KeyW"?"ArrowUp":k==="KeyS"?"ArrowDown":k==="KeyA"?"ArrowLeft":k==="KeyD"?"ArrowRight":k:k}function onMouseDown(e){if(isTouchDevice&&touchInputEnable)return;if(soundEnable&&!headlessMode&&audioContext&&!audioIsRunning())audioContext.resume();isUsingGamepad=false;inputData[0][e.button]=3;const mousePosScreenLast=mousePosScreen;mousePosScreen=mouseEventToScreen(vec2(e.x,e.y));mouseDeltaScreen=mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));if(inputPreventDefault&&document.hasFocus()&&e.cancelable)e.preventDefault()}function onMouseUp(e){if(isTouchDevice&&touchInputEnable)return;inputData[0][e.button]=inputData[0][e.button]&2|4}function onMouseMove(e){mouseInWindow=true;const mousePosScreenLast=mousePosScreen;mousePosScreen=mouseEventToScreen(vec2(e.x,e.y));const movement=pointerLockIsActive()?vec2(e.movementX,e.movementY):mousePosScreen.subtract(mousePosScreenLast);mouseDeltaScreen=mouseDeltaScreen.add(movement)}function onMouseLeave(){mouseInWindow=false}function onMouseWheel(e){mouseWheel=e.ctrlKey?0:sign(e.deltaY)}function onContextMenu(e){e.preventDefault()}function onBlur(){inputClear()}function touchInputInit(){document.addEventListener("touchstart",e=>handleTouch(e),{passive:false});document.addEventListener("touchmove",e=>handleTouch(e),{passive:false});document.addEventListener("touchend",e=>handleTouch(e),{passive:false});let wasTouching;function handleTouch(e){if(!touchInputEnable)return;if(touchGamepadEnable)handleTouchGamepad(e);if(soundEnable&&!headlessMode&&audioContext&&!audioIsRunning())audioContext.resume();const touching=e.touches.length;const button=0;if(touching){const pos=vec2(e.touches[0].clientX,e.touches[0].clientY);const mousePosScreenLast=mousePosScreen;mousePosScreen=mouseEventToScreen(pos);if(wasTouching){mouseDeltaScreen=mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));isUsingGamepad=touchGamepadEnable}else inputData[0][button]=3}else if(wasTouching)inputData[0][button]=inputData[0][button]&2|4;wasTouching=touching;if(inputPreventDefault&&document.hasFocus()&&e.cancelable)e.preventDefault();return true}function handleTouchGamepad(e){touchGamepadSticks.length=0;touchGamepadSticks[0]=vec2();touchGamepadSticks[1]=vec2();touchGamepadButtons.length=0;isUsingGamepad=true;const touching=e.touches.length;if(touching){touchGamepadTimer.set();if(touchGamepadCenterButton&&!wasTouching&&paused){touchGamepadButtons[9]=1;return}}if(paused)return;const stickCenter=vec2(touchGamepadSize,mainCanvasSize.y-touchGamepadSize);const buttonCenter=touchGamepadButtonCenter();const startCenter=mainCanvasSize.scale(.5);for(const touch of e.touches){const touchPos=mouseEventToScreen(vec2(touch.clientX,touch.clientY));if(stickCenter.distance(touchPos)<touchGamepadSize){const delta=touchPos.subtract(stickCenter);touchGamepadSticks[0]=delta.scale(2/touchGamepadSize).clampLength()}else if(buttonCenter.distance(touchPos)<touchGamepadSize){if(touchGamepadButtonCount===1){const delta=touchPos.subtract(buttonCenter);touchGamepadSticks[1]=delta.scale(2/touchGamepadSize).clampLength()}let button=buttonCenter.subtract(touchPos).direction();button=mod(button+2,4);if(touchGamepadButtonCount===1)button=0;else if(touchGamepadButtonCount===2){const delta=buttonCenter.subtract(touchPos);button=-delta.x<delta.y?1:0}button=button===3?2:button===2?3:button;if(button<touchGamepadButtonCount)touchGamepadButtons[button]=1}else if(touchGamepadCenterButton&&startCenter.distance(touchPos)<touchGamepadSize){touchGamepadButtons[9]=1}}}}function mouseEventToScreen(mousePos){const rect=mainCanvas.getBoundingClientRect();const px=percent(mousePos.x,rect.left,rect.right);const py=percent(mousePos.y,rect.top,rect.bottom);return vec2(px*mainCanvas.width,py*mainCanvas.height)}}function inputUpdate(){if(headlessMode)return;if(!(touchInputEnable&&isTouchDevice)&&!document.hasFocus())inputClear();mousePos=screenToWorld(mousePosScreen);mouseDelta=screenToWorldDelta(mouseDeltaScreen);gamepadsUpdate();function gamepadsUpdate(){const applyDeadZones=v=>{const min=.3,max=.8;const deadZone=v=>v>min?percent(v,min,max):v<-min?-percent(-v,min,max):0;return vec2(deadZone(v.x),deadZone(-v.y)).clampLength()};if(touchGamepadEnable&&isTouchDevice){if(!touchGamepadTimer.isSet())return;gamepadPrimary=0;const sticks=gamepadStickData[0]??(gamepadStickData[0]=[]);const dpad=gamepadDpadData[0]??(gamepadDpadData[0]=vec2());sticks[0]=vec2();dpad.set();const leftTouchStick=touchGamepadSticks[0]??vec2();if(touchGamepadAnalog)sticks[0]=applyDeadZones(leftTouchStick);else if(leftTouchStick.lengthSquared()>.3){const x=clamp(round(leftTouchStick.x),-1,1);const y=clamp(round(leftTouchStick.y),-1,1);dpad.set(x,-y);sticks[0]=dpad.clampLength()}if(touchGamepadButtonCount===1){const rightTouchStick=touchGamepadSticks[1]??vec2();sticks[1]=applyDeadZones(rightTouchStick)}const data=inputData[1]??(inputData[1]=[]);for(let i=10;i--;){const wasDown=gamepadIsDown(i,0);data[i]=touchGamepadButtons[i]?wasDown?1:3:wasDown?4:0}return}if(!gamepadsEnable||!navigator||!navigator.getGamepads)return;if(!debug&&!document.hasFocus())return;const maxGamepads=8;const gamepads=navigator.getGamepads();const gamepadCount=min(maxGamepads,gamepads.length);for(let i=0;i<gamepadCount;++i){const gamepad=gamepads[i];if(!gamepad){inputData[i+1]=undefined;gamepadStickData[i]=undefined;gamepadDpadData[i]=undefined;gamepadHadInput[i]=undefined;continue}const data=inputData[i+1]??(inputData[i+1]=[]);const sticks=gamepadStickData[i]??(gamepadStickData[i]=[]);const dpad=gamepadDpadData[i]??(gamepadDpadData[i]=vec2());for(let j=0;j<gamepad.axes.length-1;j+=2)sticks[j>>1]=applyDeadZones(vec2(gamepad.axes[j],gamepad.axes[j+1]));let hadInput=false;for(let j=gamepad.buttons.length;j--;){const button=gamepad.buttons[j];const wasDown=gamepadIsDown(j,i);data[j]=button.pressed?wasDown?1:3:wasDown?4:0;if(button.pressed)if(!button.value||button.value>.9)hadInput=true}if(hadInput){gamepadHadInput[i]=true;if(!gamepadHadInput[gamepadPrimary])gamepadPrimary=i;isUsingGamepad||=gamepadPrimary===i}if(gamepad.mapping==="standard"){dpad.set((gamepadIsDown(15,i)&&1)-(gamepadIsDown(14,i)&&1),(gamepadIsDown(12,i)&&1)-(gamepadIsDown(13,i)&&1))}else if(gamepad.axes&&gamepad.axes.length>=2){const x=clamp(round(gamepad.axes[0]),-1,1);const y=clamp(round(gamepad.axes[1]),-1,1);dpad.set(x,-y)}if(gamepadDirectionEmulateStick&&!dpad.isZero())sticks[0]=dpad.clampLength()}touchGamepadEnable&&isUsingGamepad&&touchGamepadTimer.unset()}}function inputUpdatePost(){if(headlessMode)return;for(const deviceInputData of inputData)for(const i in deviceInputData)deviceInputData[i]&=1;mouseWheel=0;mouseDelta=vec2();mouseDeltaScreen=vec2()}function inputRender(){touchGamepadRender();function touchGamepadRender(){if(!touchInputEnable||!isTouchDevice||headlessMode)return;if(!touchGamepadEnable||!touchGamepadTimer.isSet())return;const alpha=percent(touchGamepadTimer.get(),4,3);if(!alpha||paused)return;const context=mainContext;context.save();context.globalAlpha=alpha*touchGamepadAlpha;context.strokeStyle="#fff";context.lineWidth=3;const leftTouchStick=touchGamepadSticks[0]??vec2();context.fillStyle=leftTouchStick.lengthSquared()>0?"#fff":"#000";context.beginPath();const stickCenter=vec2(touchGamepadSize,mainCanvasSize.y-touchGamepadSize);if(touchGamepadAnalog){context.arc(stickCenter.x,stickCenter.y,touchGamepadSize/2,0,9)}else{for(let i=10;--i;){const angle=i*PI/4;context.arc(stickCenter.x,stickCenter.y,touchGamepadSize*.6,angle+PI/8,angle+PI/8);i%2&&context.arc(stickCenter.x,stickCenter.y,touchGamepadSize*.33,angle,angle)}}context.fill();context.stroke();{const buttonCenter=touchGamepadButtonCenter();const buttonSize=touchGamepadButtonCount>1?touchGamepadSize/4:touchGamepadSize/2;for(let i=0;i<touchGamepadButtonCount;i++){const j=mod(i-1,4);let button=touchGamepadButtonCount>2?j:min(j,touchGamepadButtonCount-1);button=button===3?2:button===2?3:button;const pos=touchGamepadButtonCount<2?buttonCenter:buttonCenter.add(vec2().setDirection(j,touchGamepadSize/2));context.fillStyle=touchGamepadButtons[button]?"#fff":"#000";context.beginPath();context.arc(pos.x,pos.y,buttonSize,0,9);context.fill();context.stroke()}}context.restore()}}function touchGamepadButtonCenter(){const center=mainCanvasSize.subtract(vec2(touchGamepadSize));if(touchGamepadButtonCount===2)center.x+=touchGamepadSize/2;return center}let audioContext=new AudioContext;let audioMasterGain;const audioDefaultSampleRate=44100;function audioIsRunning(){return audioContext.state==="running"}function audioInit(){if(!soundEnable||headlessMode)return;audioMasterGain=audioContext.createGain();audioMasterGain.connect(audioContext.destination);audioMasterGain.gain.value=soundVolume}class Sound{constructor(zzfxSound,range=soundDefaultRange,taper=soundDefaultTaper){if(!soundEnable||headlessMode)return;ASSERT(!zzfxSound||isArray(zzfxSound),"zzfxSound is invalid");ASSERT(isNumber(range),"range must be a number");ASSERT(isNumber(taper),"taper must be a number");this.range=range;this.taper=taper;this.randomness=0;this.sampleRate=audioDefaultSampleRate;this.loadedPercent=0;if(zzfxSound){const randomnessIndex=1,defaultRandomness=.05;this.randomness=zzfxSound[randomnessIndex]!==undefined?zzfxSound[randomnessIndex]:defaultRandomness;zzfxSound[randomnessIndex]=0;this.sampleChannels=[zzfxG(...zzfxSound)];this.loadedPercent=1}}play(pos,volume=1,pitch=1,randomnessScale=1,loop=false,paused=false){ASSERT(!pos||isVector2(pos),"pos must be a vec2");ASSERT(isNumber(volume),"volume must be a number");ASSERT(isNumber(pitch),"pitch must be a number");ASSERT(isNumber(randomnessScale),"randomnessScale must be a number");if(!soundEnable||headlessMode)return;if(!this.sampleChannels)return;let pan;if(pos){const range=this.range;if(range){const lengthSquared=cameraPos.distanceSquared(pos);if(lengthSquared>range*range)return;volume*=percent(lengthSquared**.5,range,range*this.taper)}pan=worldToScreen(pos).x*2/mainCanvas.width-1}const rate=pitch+pitch*this.randomness*randomnessScale*rand(-1,1);return new SoundInstance(this,volume,rate,pan,loop,paused)}playMusic(volume=1,loop=true,paused=false){return this.play(undefined,volume,1,0,loop,paused)}playNote(semitoneOffset=0,pos,volume){ASSERT(isNumber(semitoneOffset),"semitoneOffset must be a number");const pitch=getNoteFrequency(semitoneOffset,1);return this.play(pos,volume,pitch,0)}getDuration(){return this.sampleChannels&&this.sampleRate?this.sampleChannels[0].length/this.sampleRate:0}isLoaded(){return this.loadedPercent===1}}class SoundWave extends Sound{constructor(filename,randomness=0,range,taper,onloadCallback){super(undefined,range,taper);if(!soundEnable||headlessMode)return;ASSERT(!filename||isString(filename),"filename must be a string");ASSERT(isNumber(randomness),"randomness must be a number");this.onloadCallback=onloadCallback;this.randomness=randomness;filename&&this.loadSound(filename)}async loadSound(filename){const response=await fetch(filename);if(!response.ok)throw new Error(`Failed to load sound from ${filename}: ${response.status} ${response.statusText}`);const arrayBuffer=await response.arrayBuffer();const audioBuffer=await audioContext.decodeAudioData(arrayBuffer);const channelCount=audioBuffer.numberOfChannels;const samplesPerFrame=1e5;const sampleChannels=[];for(let channel=0;channel<channelCount;channel++){const channelData=audioBuffer.getChannelData(channel);const channelLength=channelData.length;sampleChannels[channel]=new Array(channelLength);let sampleIndex=0;while(sampleIndex<channelLength){await new Promise(resolve=>setTimeout(resolve,0));const endIndex=min(sampleIndex+samplesPerFrame,channelLength);for(;sampleIndex<endIndex;sampleIndex++)sampleChannels[channel][sampleIndex]=channelData[sampleIndex];const samplesTotal=channelCount*channelLength;const samplesProcessed=channel*channelLength+sampleIndex;this.loadedPercent=samplesProcessed/samplesTotal}}this.sampleRate=audioBuffer.sampleRate;this.sampleChannels=sampleChannels;this.loadedPercent=1;if(this.onloadCallback)this.onloadCallback(this)}}class SoundInstance{constructor(sound,volume=1,rate=1,pan=0,loop=false,paused=false){ASSERT(sound instanceof Sound,"SoundInstance requires a valid Sound object");ASSERT(volume>=0,"Sound volume must be positive or zero");ASSERT(rate>=0,"Sound rate must be positive or zero");ASSERT(isNumber(pan),"Sound pan must be a number");this.sound=sound;this.volume=volume;this.rate=rate;this.pan=pan;this.loop=loop;this.pausedTime=0;this.startTime=undefined;this.gainNode=undefined;this.source=undefined;this.onendedCallback=source=>{if(source===this.source)this.source=undefined};if(!paused)this.start()}start(offset=0){ASSERT(offset>=0,"Sound start offset must be positive or zero");if(this.isPlaying())this.stop();this.gainNode=audioContext.createGain();this.source=playSamples(this.sound.sampleChannels,this.volume,this.rate,this.pan,this.loop,this.sound.sampleRate,this.gainNode,offset,this.onendedCallback);if(this.source){this.startTime=audioContext.currentTime-offset;this.pausedTime=undefined}else{this.startTime=undefined;this.pausedTime=0}}setVolume(volume){ASSERT(volume>=0,"Sound volume must be positive or zero");this.volume=volume;if(this.gainNode)this.gainNode.gain.value=volume}stop(fadeTime=0){ASSERT(fadeTime>=0,"Sound fade time must be positive or zero");if(this.isPlaying()){if(fadeTime){const startFade=audioContext.currentTime;const endFade=startFade+fadeTime;this.gainNode.gain.linearRampToValueAtTime(1,startFade);this.gainNode.gain.linearRampToValueAtTime(0,endFade);this.source.stop(endFade)}else this.source.stop()}this.pausedTime=0;this.source=undefined;this.startTime=undefined}pause(){if(this.isPaused())return;this.pausedTime=this.getCurrentTime();this.source.stop();this.source=undefined;this.startTime=undefined}resume(){if(!this.isPaused())return;this.start(this.pausedTime)}isPlaying(){return!!this.source}isPaused(){return!this.isPlaying()}getCurrentTime(){const deltaTime=mod(audioContext.currentTime-this.startTime,this.getDuration());return this.isPlaying()?deltaTime:this.pausedTime}getDuration(){return this.rate?this.sound.getDuration()/this.rate:0}getSource(){return this.source}}function speak(text,language="",volume=1,rate=1,pitch=1){if(!soundEnable||headlessMode)return;if(!speechSynthesis)return;const utterance=new SpeechSynthesisUtterance(text);utterance.lang=language;utterance.volume=2*volume*soundVolume;utterance.rate=rate;utterance.pitch=pitch;speechSynthesis.speak(utterance);return utterance}function speakStop(){speechSynthesis&&speechSynthesis.cancel()}function getNoteFrequency(semitoneOffset,rootFrequency=220){return rootFrequency*2**(semitoneOffset/12)}function playSamples(sampleChannels,volume=1,rate=1,pan=0,loop=false,sampleRate=audioDefaultSampleRate,gainNode,offset=0,onended){if(!soundEnable||headlessMode)return;if(!audioIsRunning()){audioContext.resume();return}const channelCount=sampleChannels.length;const sampleLength=sampleChannels[0].length;const buffer=audioContext.createBuffer(channelCount,sampleLength,sampleRate);const source=audioContext.createBufferSource();sampleChannels.forEach((c,i)=>buffer.getChannelData(i).set(c));source.buffer=buffer;source.playbackRate.value=rate;source.loop=loop;gainNode=gainNode||audioContext.createGain();gainNode.gain.value=volume;gainNode.connect(audioMasterGain);const pannerNode=new StereoPannerNode(audioContext,{pan:clamp(pan,-1,1)});source.connect(pannerNode).connect(gainNode);if(onended)source.addEventListener("ended",()=>onended(source));const startOffset=offset*rate;source.start(0,startOffset);return source}function zzfx(...zzfxSound){return playSamples([zzfxG(...zzfxSound)])}function zzfxG(volume=1,randomness=.05,frequency=220,attack=0,sustain=0,release=.1,shape=0,shapeCurve=1,slide=0,deltaSlide=0,pitchJump=0,pitchJumpTime=0,repeatTime=0,noise=0,modulation=0,bitCrush=0,delay=0,sustainVolume=1,decay=0,tremolo=0,filter=0){let sampleRate=audioDefaultSampleRate,PI2=PI*2,startSlide=slide*=500*PI2/sampleRate/sampleRate,startFrequency=frequency*=(1+rand(randomness,-randomness))*PI2/sampleRate,modOffset=0,repeat=0,crush=0,jump=1,length,b=[],t=0,i=0,s=0,f,quality=2,w=PI2*abs(filter)*2/sampleRate,cosw=cos(w),alpha=sin(w)/2/quality,a0=1+alpha,a1=-2*cosw/a0,a2=(1-alpha)/a0,b0=(1+sign(filter)*cosw)/2/a0,b1=-(sign(filter)+cosw)/a0,b2=b0,x2=0,x1=0,y2=0,y1=0;const minAttack=9;attack=attack*sampleRate||minAttack;decay*=sampleRate;sustain*=sampleRate;release*=sampleRate;delay*=sampleRate;deltaSlide*=500*PI2/sampleRate**3;modulation*=PI2/sampleRate;pitchJump*=PI2/sampleRate;pitchJumpTime*=sampleRate;repeatTime=repeatTime*sampleRate|0;for(length=attack+decay+sustain+release+delay|0;i<length;b[i++]=s*volume){if(!(++crush%(bitCrush*100|0))){s=shape?shape>1?shape>2?shape>3?shape>4?t/PI2%1<shapeCurve/2?1:-1:sin(t**3):max(min(tan(t),1),-1):1-(2*t/PI2%2+2)%2:1-4*abs(round(t/PI2)-t/PI2):sin(t);s=(repeatTime?1-tremolo+tremolo*sin(PI2*i/repeatTime):1)*(shape>4?s:sign(s)*abs(s)**shapeCurve)*(i<attack?i/attack:i<attack+decay?1-(i-attack)/decay*(1-sustainVolume):i<attack+decay+sustain?sustainVolume:i<length-delay?(length-i-delay)/release*sustainVolume:0);s=delay?s/2+(delay>i?0:(i<length-delay?1:(length-i)/delay)*b[i-delay|0]/2/volume):s;if(filter)s=y1=b2*x2+b1*(x2=x1)+b0*(x1=s)-a2*y2-a1*(y2=y1)}f=(frequency+=slide+=deltaSlide)*cos(modulation*modOffset++);t+=f+f*noise*sin(i**5);if(jump&&++jump>pitchJumpTime){frequency+=pitchJump;startFrequency+=pitchJump;jump=0}if(repeatTime&&!(++repeat%repeatTime)){frequency=startFrequency;slide=startSlide;jump||=1}}return b}const tileCollisionLayers=[];function tileCollisionGetData(pos){for(const layer of tileCollisionLayers)if(pos.arrayCheck(layer.size))return layer.getCollisionData(pos);return 0}function tileCollisionTest(pos,size=vec2(),object,solidOnly=true){for(const layer of tileCollisionLayers){if(!solidOnly||layer.isSolid)if(layer.collisionTest(pos,size,object))return layer}}function tileCollisionRaycast(posStart,posEnd,object,normal,solidOnly=true){for(const layer of tileCollisionLayers){if(!solidOnly||layer.isSolid){const hitPos=layer.collisionRaycast(posStart,posEnd,object,normal);if(hitPos)return hitPos}}}function tileLayersLoad(tileMapData,tileInfo=tile(),renderOrder=0,collisionLayer,draw=true){if(!tileMapData){const s=50;tileMapData={};tileMapData.height=tileMapData.width=s;tileMapData.layers=[{}];tileMapData.layers[0].data=new Array(s*s).fill(0)}ASSERT(tileMapData.width&&tileMapData.height);ASSERT(tileMapData.layers&&tileMapData.layers.length);const tileLayers=[];const levelSize=vec2(tileMapData.width,tileMapData.height);const layerCount=tileMapData.layers.length;for(let layerIndex=layerCount;layerIndex--;){const dataLayer=tileMapData.layers[layerIndex];ASSERT(dataLayer.data&&dataLayer.data.length);ASSERT(levelSize.area()===dataLayer.data.length);const layerRenderOrder=renderOrder-(layerCount-1-layerIndex);const tileLayer=new TileCollisionLayer(vec2(),levelSize,tileInfo,layerRenderOrder);tileLayers[layerIndex]=tileLayer;const layerColor=dataLayer.tintcolor?(new Color).setHex(dataLayer.tintcolor):dataLayer.color||WHITE;ASSERT(isColor(layerColor),"layer color is not a color");for(let x=levelSize.x;x--;)for(let y=levelSize.y;y--;){const pos=vec2(x,levelSize.y-1-y);const data=dataLayer.data[x+y*levelSize.x];if(data){const layerData=new TileLayerData(data-1,0,false,layerColor);tileLayer.setData(pos,layerData);if(layerIndex===collisionLayer)tileLayer.setCollisionData(pos,1)}}if(draw)tileLayer.redraw()}return tileLayers}class TileLayerData{constructor(tile,direction=0,mirror=false,color=new Color){this.tile=tile;this.direction=direction;this.mirror=mirror;this.color=color.copy()}clear(){this.tile=this.direction=0;this.mirror=false;this.color=new Color}}class CanvasLayer extends EngineObject{constructor(position,size,angle=0,renderOrder=0,canvasSize=vec2(512)){ASSERT(isVector2(canvasSize),"canvasSize must be a Vector2");super(position,size,undefined,angle,WHITE,renderOrder);this.canvas=headlessMode?undefined:new OffscreenCanvas(canvasSize.x,canvasSize.y);this.context=this.canvas?.getContext("2d");const useWebGL=false;this.textureInfo=new TextureInfo(this.canvas,useWebGL);this.refreshWebGL=false;this.mass=this.gravityScale=this.friction=this.restitution=0}destroy(){if(this.destroyed)return;this.textureInfo.destroyWebGLTexture();super.destroy()}render(){this.draw(this.pos,this.size,this.angle,this.color,this.mirror,this.additiveColor)}draw(pos,size,angle=0,color=WHITE,mirror=false,additiveColor,screenSpace=false,context){const useWebGL=glEnable&&this.textureInfo.hasWebGL();if(useWebGL&&this.refreshWebGL){this.textureInfo.createWebGLTexture();this.refreshWebGL=false}const tileInfo=(new TileInfo).setFullImage(this.textureInfo);drawTile(pos,size,tileInfo,color,angle,mirror,additiveColor,useWebGL,screenSpace,context)}drawCanvas2D(pos,size,angle,mirror,drawFunction){if(!this.context)return;const context=this.context;context.save();pos=pos.subtract(this.pos).multiply(this.tileInfo.size);size=size.multiply(this.tileInfo.size);context.translate(pos.x,this.canvas.height-pos.y);context.rotate(angle);context.scale(mirror?-size.x:size.x,size.y);drawFunction(context);context.restore()}drawTile(pos,size=vec2(1),tileInfo,color=new Color,angle,mirror){this.drawCanvas2D(pos,size,angle,mirror,context=>{const textureInfo=tileInfo&&tileInfo.textureInfo;if(textureInfo){context.globalAlpha=color.a;context.drawImage(textureInfo.image,tileInfo.pos.x,tileInfo.pos.y,tileInfo.size.x,tileInfo.size.y,-.5,-.5,1,1);context.globalAlpha=1}else{context.fillStyle=color.toString();context.fillRect(-.5,-.5,1,1)}})}drawRect(pos,size,color,angle){this.drawTile(pos,size,undefined,color,angle)}useWebGL(enable=true,immediate=false){if(!immediate&&enable&&this.textureInfo.hasWebGL()){this.refreshWebGL=true;return}if(enable)this.textureInfo.createWebGLTexture();else this.textureInfo.destroyWebGLTexture()}}class TileLayer extends CanvasLayer{constructor(position,size,tileInfo=tile(),renderOrder=0){const canvasSize=tileInfo?size.multiply(tileInfo.size):size;super(position,size,0,renderOrder,canvasSize);this.tileInfo=tileInfo;this.data=[];for(let j=this.size.area();j--;)this.data.push(new TileLayerData);if(headlessMode){this.redraw=()=>{};this.render=()=>{};this.redrawStart=()=>{};this.redrawEnd=()=>{};this.drawTileData=()=>{};this.drawCanvas2D=()=>{};this.useWebGL=()=>{}}}setData(layerPos,data,redraw=false){ASSERT(isVector2(layerPos),"layerPos must be a Vector2");ASSERT(data instanceof TileLayerData,"data must be a TileLayerData");if(layerPos.arrayCheck(this.size)){this.data[(layerPos.y|0)*this.size.x+layerPos.x|0]=data;redraw&&this.drawTileData(layerPos)}}getData(layerPos){ASSERT(isVector2(layerPos),"layerPos must be a Vector2");return layerPos.arrayCheck(this.size)&&this.data[(layerPos.y|0)*this.size.x+layerPos.x|0]}render(){ASSERT(drawContext!==this.context,"must call redrawEnd() after drawing tiles!");if(this.refreshWebGL){this.textureInfo.createWebGLTexture();this.refreshWebGL=false}const tileInfo=(new TileInfo).setFullImage(this.textureInfo);const size=this.drawSize||this.size;const pos=this.pos.add(size.scale(.5));const useWebGL=glEnable&&this.textureInfo.hasWebGL();drawTile(pos,size,tileInfo,WHITE,0,false,CLEAR_BLACK,useWebGL)}redraw(){this.redrawStart(true);for(let x=this.size.x;x--;)for(let y=this.size.y;y--;)this.drawTileData(vec2(x,y),false);this.redrawEnd();this.useWebGL()}redrawStart(clear=false){if(!this.context)return;this.savedRenderSettings=[drawContext,mainCanvasSize,cameraPos,cameraScale,canvasClearColor];drawContext=this.context;cameraPos=this.size.scale(.5);const tileSize=this.tileInfo?this.tileInfo.size:vec2(1);cameraScale=tileSize.x;canvasClearColor=CLEAR_BLACK;mainCanvasSize=this.size.multiply(tileSize);if(clear){this.canvas.width=mainCanvasSize.x;this.canvas.height=mainCanvasSize.y}drawContext.imageSmoothingEnabled=!tilesPixelated;glPreRender()}redrawEnd(){if(!this.context)return;ASSERT(drawContext===this.context,"must call redrawStart() before drawing tiles");glCopyToContext(drawContext);[drawContext,mainCanvasSize,cameraPos,cameraScale,canvasClearColor]=this.savedRenderSettings}drawTileData(layerPos,clear=true){if(!this.context)return;const s=this.tileInfo.size;if(clear){const pos=layerPos.multiply(s);this.context.clearRect(pos.x,this.canvas.height-pos.y,s.x,-s.y)}const d=this.getData(layerPos);if(d.tile!==undefined){ASSERT(drawContext===this.context,"must call redrawStart() before drawing tiles");const pos=layerPos.add(vec2(.5));const tileInfo=tile(d.tile,s,this.tileInfo.textureIndex,this.tileInfo.padding);drawTile(pos,vec2(1),tileInfo,d.color,d.direction*PI/2,d.mirror)}}}class TileCollisionLayer extends TileLayer{constructor(position,size,tileInfo=tile(),renderOrder=0){super(position,size.floor(),tileInfo,renderOrder);this.collisionData=[];this.initCollision(this.size);tileCollisionLayers.push(this);this.isSolid=true}destroy(){if(this.destroyed)return;const index=tileCollisionLayers.indexOf(this);ASSERT(index>=0,"tile collision layer not found in array");index>=0&&tileCollisionLayers.splice(index,1);super.destroy()}initCollision(size){ASSERT(isVector2(size),"size must be a Vector2");this.size=size.floor();this.collisionData=[];this.collisionData.length=size.area();this.collisionData.fill(0)}setCollisionData(gridPos,data=1){ASSERT(isVector2(gridPos),"gridPos must be a Vector2");const i=(gridPos.y|0)*this.size.x+gridPos.x|0;gridPos.arrayCheck(this.size)&&(this.collisionData[i]=data)}getCollisionData(gridPos){ASSERT(isVector2(gridPos),"gridPos must be a Vector2");const i=(gridPos.y|0)*this.size.x+gridPos.x|0;return gridPos.arrayCheck(this.size)?this.collisionData[i]:0}collisionTest(pos,size=new Vector2,object){ASSERT(isVector2(pos)&&isVector2(size),"pos and size must be Vector2s");ASSERT(!object||object instanceof EngineObject,"object must be an EngineObject");const posX=pos.x-this.pos.x;const posY=pos.y-this.pos.y;const minX=max(posX-size.x/2|0,0);const minY=max(posY-size.y/2|0,0);const maxX=min(posX+size.x/2,this.size.x);const maxY=min(posY+size.y/2,this.size.y);const hitPos=new Vector2;for(let y=minY;y<maxY;++y)for(let x=minX;x<maxX;++x){const tileData=this.collisionData[y*this.size.x+x];if(tileData)if(!object||object.collideWithTile(tileData,hitPos.set(x+this.pos.x,y+this.pos.y)))return true}return false}collisionRaycast(posStart,posEnd,object,normal){ASSERT(isVector2(posStart)&&isVector2(posEnd),"positions must be Vector2s");ASSERT(!object||object instanceof EngineObject,"object must be an EngineObject");const localPos=new Vector2;const collisionTest=pos=>{localPos.set(pos.x-this.pos.x,pos.y-this.pos.y);const tileData=this.getCollisionData(localPos);return tileData&&(!object||object.collideWithTile(tileData,pos))};debugRaycast&&debugLine(posStart,posEnd,"#00f",.02);const hitPos=lineTest(posStart,posEnd,collisionTest,normal);if(hitPos){const tilePos=hitPos.floor().add(vec2(.5));debugRaycast&&debugRect(tilePos,vec2(1),"#f008");debugRaycast&&debugLine(posStart,hitPos,"#f00",.02);debugRaycast&&debugPoint(hitPos,"#0f0");debugRaycast&&normal&&debugLine(hitPos,hitPos.add(normal),"#ff0",.02);return hitPos}}}class ParticleEmitter extends EngineObject{constructor(position,angle,emitSize=0,emitTime=0,emitRate=100,emitConeAngle=PI,tileInfo,colorStartA=WHITE,colorStartB=WHITE,colorEndA=CLEAR_WHITE,colorEndB=CLEAR_WHITE,particleTime=.5,sizeStart=.1,sizeEnd=1,speed=.1,angleSpeed=.05,damping=1,angleDamping=1,gravityScale=0,particleConeAngle=PI,fadeRate=.1,randomness=.2,collideTiles=false,additive=false,randomColorLinear=true,renderOrder=additive?1e9:0,localSpace=false){super(position,vec2(),tileInfo,angle,undefined,renderOrder);this.emitSize=emitSize instanceof Vector2?emitSize.copy():emitSize;this.emitTime=emitTime;this.emitRate=emitRate;this.emitConeAngle=emitConeAngle;this.colorStartA=colorStartA.copy();this.colorStartB=colorStartB.copy();this.colorEndA=colorEndA.copy();this.colorEndB=colorEndB.copy();this.randomColorLinear=randomColorLinear;this.particleTime=particleTime;this.sizeStart=sizeStart;this.sizeEnd=sizeEnd;this.speed=speed;this.angleSpeed=angleSpeed;this.damping=damping;this.angleDamping=angleDamping;this.gravityScale=gravityScale;this.particleConeAngle=particleConeAngle;this.fadeRate=fadeRate;this.randomness=randomness;this.collideTiles=collideTiles;this.additive=additive;this.localSpace=localSpace;this.trailScale=0;this.particleDestroyCallback=undefined;this.particleCreateCallback=undefined;this.emitTimeBuffer=0;this.velocityInheritance=0;this.previousAngle=this.angle;this.previousPos=this.pos.copy()}updatePhysics(){}update(){if(this.velocityInheritance){const p=this.velocityInheritance;this.velocity.x=p*(this.pos.x-this.previousPos.x);this.velocity.y=p*(this.pos.y-this.previousPos.y);this.angleVelocity=p*(this.angle-this.previousAngle);this.previousAngle=this.angle;this.previousPos.x=this.pos.x;this.previousPos.y=this.pos.y}if(!this.emitTime||this.getAliveTime()<=this.emitTime){if(this.emitRate&&particleEmitRateScale){const rate=1/this.emitRate/particleEmitRateScale;for(this.emitTimeBuffer+=timeDelta;this.emitTimeBuffer>0;this.emitTimeBuffer-=rate)this.emitParticle()}}else this.destroy();if(debugParticles){if(typeof this.emitSize==="number")debugCircle(this.pos,this.emitSize/2,"#0f0");else debugRect(this.pos,this.emitSize,"#0f0",0,this.angle)}}emitParticle(){let pos=typeof this.emitSize==="number"?randInCircle(this.emitSize/2):vec2(rand(-.5,.5),rand(-.5,.5)).multiply(this.emitSize).rotate(this.angle);let angle=rand(this.particleConeAngle,-this.particleConeAngle);if(!this.localSpace){pos=this.pos.add(pos);angle+=this.angle}const randomness=this.randomness;const randomizeScale=v=>v+v*rand(randomness,-randomness);const particleTime=randomizeScale(this.particleTime);const sizeStart=randomizeScale(this.sizeStart);const sizeEnd=randomizeScale(this.sizeEnd);const speed=randomizeScale(this.speed);const angleSpeed=randomizeScale(this.angleSpeed)*randSign();const coneAngle=rand(this.emitConeAngle,-this.emitConeAngle);const colorStart=randColor(this.colorStartA,this.colorStartB,this.randomColorLinear);const colorEnd=randColor(this.colorEndA,this.colorEndB,this.randomColorLinear);const velocityAngle=this.localSpace?coneAngle:this.angle+coneAngle;const particle=new Particle(pos,this.tileInfo,angle,colorStart,colorEnd,particleTime,sizeStart,sizeEnd,this.fadeRate,this.additive,this.trailScale,this.localSpace&&this,this.particleDestroyCallback);particle.velocity=vec2().setAngle(velocityAngle,speed);particle.angleVelocity=angleSpeed;if(!this.localSpace&&this.velocityInheritance>0){particle.velocity.x+=this.velocity.x;particle.velocity.y+=this.velocity.y;particle.angleVelocity+=this.angleVelocity}particle.fadeRate=this.fadeRate;particle.damping=this.damping;particle.angleDamping=this.angleDamping;particle.restitution=this.restitution;particle.friction=this.friction;particle.gravityScale=this.gravityScale;particle.collideTiles=this.collideTiles;particle.renderOrder=this.renderOrder;particle.mirror=randBool();this.particleCreateCallback&&this.particleCreateCallback(particle);return particle}render(){}}class Particle extends EngineObject{constructor(position,tileInfo,angle,colorStart,colorEnd,lifeTime,sizeStart,sizeEnd,fadeRate,additive,trailScale,localSpaceEmitter,destroyCallback){super(position,vec2(),tileInfo,angle);this.colorStart=colorStart;this.colorEnd=colorEnd;this.lifeTime=lifeTime;this.sizeStart=sizeStart;this.sizeEnd=sizeEnd;this.fadeRate=fadeRate;this.additive=additive;this.trailScale=trailScale;this.localSpaceEmitter=localSpaceEmitter;this.destroyCallback=destroyCallback;this.clampSpeed=false}update(){if(this.collideTiles||this.collideSolidObjects){const length2=this.velocity.lengthSquared();if(length2>objectMaxSpeed*objectMaxSpeed){const s=objectMaxSpeed/length2**.5;this.velocity.x*=s;this.velocity.y*=s}}if(this.lifeTime>0&&time-this.spawnTime>this.lifeTime){const c=this.colorEnd;this.color.set(c.r,c.g,c.b,c.a);this.size.set(this.sizeEnd,this.sizeEnd);this.destroyCallback&&this.destroyCallback(this);this.destroyed=1}}render(){const p1=this.lifeTime>0?min((time-this.spawnTime)/this.lifeTime,1):1,p2=1-p1;const radius=p2*this.sizeStart+p1*this.sizeEnd;const size=vec2(radius);this.color.r=p2*this.colorStart.r+p1*this.colorEnd.r;this.color.g=p2*this.colorStart.g+p1*this.colorEnd.g;this.color.b=p2*this.colorStart.b+p1*this.colorEnd.b;this.color.a=p2*this.colorStart.a+p1*this.colorEnd.a;const fadeRate=this.fadeRate/2;this.color.a*=p1<fadeRate?p1/fadeRate:p1>1-fadeRate?(1-p1)/fadeRate:1;this.additive&&setBlendMode(true);let pos=this.pos,angle=this.angle;if(this.localSpaceEmitter){const a=this.localSpaceEmitter.angle;const c=cos(a),s=sin(a);pos=this.localSpaceEmitter.pos.add(new Vector2(pos.x*c-pos.y*s,pos.x*s+pos.y*c));angle+=this.localSpaceEmitter.angle}if(this.trailScale){const direction=this.localSpaceEmitter?this.velocity.rotate(-this.localSpaceEmitter.angle):this.velocity;const speed=direction.length();if(speed){const trailLength=speed*this.trailScale;size.y=max(size.x,trailLength);angle=atan2(direction.x,direction.y);drawTile(pos,size,this.tileInfo,this.color,angle,this.mirror)}}else drawTile(pos,size,this.tileInfo,this.color,angle,this.mirror);this.additive&&setBlendMode();debugParticles&&debugRect(pos,size,"#f005",0,angle)}}const medals={};let medalsDisplayQueue=[],medalsSaveName,medalsDisplayTimeLast;function medalsInit(saveName){medalsSaveName=saveName;if(!debugMedals)medalsForEach(medal=>medal.unlocked=!!localStorage[medal.storageKey()]);engineAddPlugin(undefined,medalsRender);function medalsRender(){if(!medalsDisplayQueue.length)return;const medal=medalsDisplayQueue[0];const time=timeReal-medalsDisplayTimeLast;if(!medalsDisplayTimeLast)medalsDisplayTimeLast=timeReal;else if(time>medalDisplayTime){medalsDisplayTimeLast=0;medalsDisplayQueue.shift()}else{const slideOffTime=medalDisplayTime-medalDisplaySlideTime;const hidePercent=time<medalDisplaySlideTime?1-time/medalDisplaySlideTime:time>slideOffTime?(time-slideOffTime)/medalDisplaySlideTime:0;medal.render(hidePercent)}}}function medalsForEach(callback){Object.values(medals).forEach(medal=>callback(medal))}class Medal{constructor(id,name,description="",icon="🏆",src){ASSERT(id>=0&&!medals[id]);this.id=id;this.name=name;this.description=description;this.icon=icon;this.unlocked=false;if(src)(this.image=new Image).src=src;medals[id]=this}unlock(){if(medalsPreventUnlock||this.unlocked)return;ASSERT(medalsSaveName,"save name must be set");localStorage[this.storageKey()]=this.unlocked=true;medalsDisplayQueue.push(this)}render(hidePercent=0){const context=mainContext;const width=min(medalDisplaySize.x,mainCanvas.width);const height=medalDisplaySize.y;const x=mainCanvas.width-width;const y=-height*hidePercent;const backgroundColor=hsl(0,0,.9);context.save();context.beginPath();context.fillStyle=backgroundColor.toString();context.strokeStyle=BLACK.toString();context.lineWidth=3;context.rect(x,y,width,height);context.fill();context.stroke();context.clip();const gap=vec2(.1,.05).scale(height);const medalDisplayIconSize=height-2*gap.x;this.renderIcon(vec2(x+gap.x+medalDisplayIconSize/2,y+height/2),medalDisplayIconSize);const nameSize=height*.5;const descriptionSize=height*.3;const pos=vec2(x+medalDisplayIconSize+2*gap.x,y+gap.y*2+nameSize/2);const textWidth=width-medalDisplayIconSize-3*gap.x;drawTextScreen(this.name,pos,nameSize,BLACK,0,undefined,"left",undefined,undefined,textWidth);pos.y=y+height-gap.y*2-descriptionSize/2;drawTextScreen(this.description,pos,descriptionSize,BLACK,0,undefined,"left",undefined,undefined,textWidth);context.restore()}renderIcon(pos,size){if(this.image)mainContext.drawImage(this.image,pos.x-size/2,pos.y-size/2,size,size);else drawTextScreen(this.icon,pos,size*.7,BLACK)}storageKey(){return medalsSaveName+"_"+this.id}}let glCanvas;let glContext;let glAntialias=true;let glShader,glPolyShader,glPolyMode,glAdditive,glBatchAdditive,glActiveTexture,glArrayBuffer,glGeometryBuffer,glPositionData,glColorData,glBatchCount,glTextureInfos,glCanBeEnabled=true;const gl_ARRAY_BUFFER_SIZE=5e5;const gl_INDICES_PER_INSTANCE=11;const gl_INSTANCE_BYTE_STRIDE=gl_INDICES_PER_INSTANCE*4;const gl_MAX_INSTANCES=gl_ARRAY_BUFFER_SIZE/gl_INSTANCE_BYTE_STRIDE|0;const gl_INDICES_PER_POLY_VERTEX=3;const gl_POLY_VERTEX_BYTE_STRIDE=gl_INDICES_PER_POLY_VERTEX*4;const gl_MAX_POLY_VERTEXES=gl_ARRAY_BUFFER_SIZE/gl_POLY_VERTEX_BYTE_STRIDE|0;function glInit(rootElement){glTextureInfos=new Set;if(!glEnable||headlessMode){glCanBeEnabled=false;return}glCanvas=document.createElement("canvas");glContext=glCanvas.getContext("webgl2",{antialias:glAntialias});if(!glContext){console.warn("WebGL2 not supported, falling back to 2D canvas rendering!");glCanvas=glContext=undefined;glEnable=false;glCanBeEnabled=false;return}rootElement.appendChild(glCanvas);initWebGL();glCanvas.addEventListener("webglcontextlost",e=>{glEnable=false;glCanvas.style.display="none";e.preventDefault();LOG("WebGL context lost! Switching to Canvas2d rendering.");for(const info of glTextureInfos)info.glTexture=undefined;glActiveTexture=undefined;pluginList.forEach(plugin=>plugin.glContextLost?.())});glCanvas.addEventListener("webglcontextrestored",()=>{glEnable=true;glCanvas.style.display="";LOG("WebGL context restored, reinitializing...");initWebGL();for(const info of glTextureInfos)info.glTexture=glCreateTexture(info.image);pluginList.forEach(plugin=>plugin.glContextRestored?.())});function initWebGL(){glShader=glCreateProgram("#version 300 es\n"+"precision highp float;"+"uniform mat4 m;"+"in vec2 g;"+"in vec4 p,u,c,a;"+"in float r;"+"out vec2 v;"+"out vec4 d,e;"+"void main(){"+"vec2 s=(g-.5)*p.zw;"+"gl_Position=m*vec4(p.xy+s*cos(r)-vec2(-s.y,s)*sin(r),1,1);"+"v=mix(u.xw,u.zy,g);"+"d=c;e=a;"+"}","#version 300 es\n"+"precision highp float;"+"uniform sampler2D s;"+"in vec2 v;"+"in vec4 d,e;"+"out vec4 c;"+"void main(){"+"c=texture(s,v)*d+e;"+"}");glPolyShader=glCreateProgram("#version 300 es\n"+"precision highp float;"+"uniform mat4 m;"+"in vec2 p;"+"in vec4 c;"+"out vec4 d;"+"void main(){"+"gl_Position=m*vec4(p,1,1);"+"d=c;"+"}","#version 300 es\n"+"precision highp float;"+"in vec4 d;"+"out vec4 c;"+"void main(){"+"c=d;"+"}");const glInstanceData=new ArrayBuffer(gl_ARRAY_BUFFER_SIZE);glPositionData=new Float32Array(glInstanceData);glColorData=new Uint32Array(glInstanceData);glArrayBuffer=glContext.createBuffer();glGeometryBuffer=glContext.createBuffer();const geometry=new Float32Array([glBatchCount=0,0,1,0,0,1,1,1]);glContext.bindBuffer(glContext.ARRAY_BUFFER,glGeometryBuffer);glContext.bufferData(glContext.ARRAY_BUFFER,geometry,glContext.STATIC_DRAW)}}function glSetInstancedMode(){if(!glPolyMode)return;glFlush();glPolyMode=false;glContext.useProgram(glShader);let offset=0;const initVertexAttribArray=(name,type,typeSize,size)=>{const location=glContext.getAttribLocation(glShader,name);const stride=typeSize&&gl_INSTANCE_BYTE_STRIDE;const divisor=typeSize&&1;const normalize=typeSize===1;glContext.enableVertexAttribArray(location);glContext.vertexAttribPointer(location,size,type,normalize,stride,offset);glContext.vertexAttribDivisor(location,divisor);offset+=size*typeSize};glContext.bindBuffer(glContext.ARRAY_BUFFER,glGeometryBuffer);initVertexAttribArray("g",glContext.FLOAT,0,2);glContext.bindBuffer(glContext.ARRAY_BUFFER,glArrayBuffer);glContext.bufferData(glContext.ARRAY_BUFFER,gl_ARRAY_BUFFER_SIZE,glContext.DYNAMIC_DRAW);initVertexAttribArray("p",glContext.FLOAT,4,4);initVertexAttribArray("u",glContext.FLOAT,4,4);initVertexAttribArray("c",glContext.UNSIGNED_BYTE,1,4);initVertexAttribArray("a",glContext.UNSIGNED_BYTE,1,4);initVertexAttribArray("r",glContext.FLOAT,4,1)}function glSetPolyMode(){if(glPolyMode)return;glFlush();glPolyMode=true;glContext.useProgram(glPolyShader);let offset=0;const initVertexAttribArray=(name,type,typeSize,size)=>{const location=glContext.getAttribLocation(glPolyShader,name);const normalize=typeSize===1;const stride=gl_POLY_VERTEX_BYTE_STRIDE;glContext.enableVertexAttribArray(location);glContext.vertexAttribPointer(location,size,type,normalize,stride,offset);glContext.vertexAttribDivisor(location,0);offset+=size*typeSize};glContext.bindBuffer(glContext.ARRAY_BUFFER,glArrayBuffer);glContext.bufferData(glContext.ARRAY_BUFFER,gl_ARRAY_BUFFER_SIZE,glContext.DYNAMIC_DRAW);initVertexAttribArray("p",glContext.FLOAT,4,2);initVertexAttribArray("c",glContext.UNSIGNED_BYTE,1,4)}function glPreRender(){if(!glEnable||!glContext)return;glClearCanvas();const s=vec2(2*cameraScale).divide(mainCanvasSize);const rotatedCam=cameraPos.rotate(-cameraAngle);const p=vec2(-1).subtract(rotatedCam.multiply(s));const ca=cos(cameraAngle);const sa=sin(cameraAngle);const transform=[s.x*ca,s.y*sa,0,0,-s.x*sa,s.y*ca,0,0,1,1,1,0,p.x,p.y,0,1];const initUniform=(program,uniform,value)=>{glContext.useProgram(program);const location=glContext.getUniformLocation(program,uniform);glContext.uniformMatrix4fv(location,false,value)};initUniform(glPolyShader,"m",transform);initUniform(glShader,"m",transform);glContext.activeTexture(glContext.TEXTURE0);if(textureInfos[0]){glActiveTexture=textureInfos[0].glTexture;glContext.bindTexture(glContext.TEXTURE_2D,glActiveTexture)}glAdditive=glBatchAdditive=false;glPolyMode=true;glSetInstancedMode()}function glClearCanvas(){if(!glContext)return;glCanvas.width=mainCanvasSize.x;glCanvas.height=mainCanvasSize.y;glContext.viewport(0,0,glCanvas.width,glCanvas.height);const color=canvasClearColor;if(color.a>0)glContext.clearColor(color.r,color.g,color.b,color.a);glContext.clear(glContext.COLOR_BUFFER_BIT)}function glSetTexture(texture,wrap=false){if(!glContext||texture===glActiveTexture)return;glFlush();glActiveTexture=texture;glContext.bindTexture(glContext.TEXTURE_2D,glActiveTexture);const wrapMode=wrap?glContext.REPEAT:glContext.CLAMP_TO_EDGE;glContext.texParameteri(glContext.TEXTURE_2D,glContext.TEXTURE_WRAP_S,wrapMode);glContext.texParameteri(glContext.TEXTURE_2D,glContext.TEXTURE_WRAP_T,wrapMode)}function glCompileShader(source,type){if(!glContext)return;const shader=glContext.createShader(type);glContext.shaderSource(shader,source);glContext.compileShader(shader);if(debug&&!glContext.getShaderParameter(shader,glContext.COMPILE_STATUS))throw glContext.getShaderInfoLog(shader);return shader}function glCreateProgram(vsSource,fsSource){if(!glContext)return;const program=glContext.createProgram();glContext.attachShader(program,glCompileShader(vsSource,glContext.VERTEX_SHADER));glContext.attachShader(program,glCompileShader(fsSource,glContext.FRAGMENT_SHADER));glContext.linkProgram(program);if(debug&&!glContext.getProgramParameter(program,glContext.LINK_STATUS))throw glContext.getProgramInfoLog(program);return program}function glCreateTexture(image){if(!glContext)return;const texture=glContext.createTexture();let mipMap=false;if(image&&image.width){glSetTextureData(texture,image);glContext.bindTexture(glContext.TEXTURE_2D,texture);mipMap=!tilesPixelated&&isPowerOfTwo(image.width)&&isPowerOfTwo(image.height)}else{const whitePixel=new Uint8Array([255,255,255,255]);glContext.bindTexture(glContext.TEXTURE_2D,texture);glContext.texImage2D(glContext.TEXTURE_2D,0,glContext.RGBA,1,1,0,glContext.RGBA,glContext.UNSIGNED_BYTE,whitePixel)}const magFilter=tilesPixelated?glContext.NEAREST:glContext.LINEAR;const minFilter=mipMap?glContext.LINEAR_MIPMAP_LINEAR:magFilter;glContext.texParameteri(glContext.TEXTURE_2D,glContext.TEXTURE_MAG_FILTER,magFilter);glContext.texParameteri(glContext.TEXTURE_2D,glContext.TEXTURE_MIN_FILTER,minFilter);if(mipMap)glContext.generateMipmap(glContext.TEXTURE_2D);glContext.bindTexture(glContext.TEXTURE_2D,glActiveTexture);return texture}function glDeleteTexture(texture){if(!glContext)return;glContext.deleteTexture(texture)}function glSetTextureData(texture,image){if(!glContext)return;ASSERT(!!image&&image.width>0,"Invalid image data.");glContext.bindTexture(glContext.TEXTURE_2D,texture);glContext.texImage2D(glContext.TEXTURE_2D,0,glContext.RGBA,glContext.RGBA,glContext.UNSIGNED_BYTE,image);glContext.bindTexture(glContext.TEXTURE_2D,glActiveTexture)}function glRegisterTextureInfo(textureInfo){if(headlessMode)return;glTextureInfos.add(textureInfo);if(!glContext)return;if(textureInfo.glTexture)glSetTextureData(textureInfo.glTexture,textureInfo.image);else textureInfo.glTexture=glCreateTexture(textureInfo.image)}function glUnregisterTextureInfo(textureInfo){if(headlessMode)return;glTextureInfos.delete(textureInfo);const glTexture=textureInfo.glTexture;textureInfo.glTexture=undefined;glDeleteTexture(glTexture)}function glFlush(){if(glEnable&&glContext&&glBatchCount){const destBlend=glBatchAdditive?glContext.ONE:glContext.ONE_MINUS_SRC_ALPHA;glContext.blendFuncSeparate(glContext.SRC_ALPHA,destBlend,glContext.ONE,destBlend);glContext.enable(glContext.BLEND);const byteLength=glBatchCount*(glPolyMode?gl_INDICES_PER_POLY_VERTEX:gl_INDICES_PER_INSTANCE);glContext.bufferSubData(glContext.ARRAY_BUFFER,0,glPositionData,0,byteLength);if(glPolyMode)glContext.drawArrays(glContext.TRIANGLE_STRIP,0,glBatchCount);else glContext.drawArraysInstanced(glContext.TRIANGLE_STRIP,0,4,glBatchCount);drawCount+=glBatchCount;glBatchCount=0}glBatchAdditive=glAdditive}function glCopyToContext(context){if(!glEnable||!glContext)return;glFlush();context.drawImage(glCanvas,0,0)}function glSetAntialias(antialias=true){ASSERT(!glCanvas,"must be called before engineInit");glAntialias=antialias}function glDraw(x,y,sizeX,sizeY,angle=0,uv0X=0,uv0Y=0,uv1X=1,uv1Y=1,rgba=-1,rgbaAdditive=0){if(glBatchCount>=gl_MAX_INSTANCES||glBatchAdditive!==glAdditive)glFlush();glSetInstancedMode();let offset=glBatchCount++*gl_INDICES_PER_INSTANCE;glPositionData[offset++]=x;glPositionData[offset++]=y;glPositionData[offset++]=sizeX;glPositionData[offset++]=sizeY;glPositionData[offset++]=uv0X;glPositionData[offset++]=uv0Y;glPositionData[offset++]=uv1X;glPositionData[offset++]=uv1Y;glColorData[offset++]=rgba;glColorData[offset++]=rgbaAdditive;glPositionData[offset++]=angle}function glDrawPointsTransform(points,rgba,x,y,sx,sy,angle,tristrip=true){const pointsOut=[];for(const p of points){const px=p.x*sx;const py=p.y*sy;const sa=sin(-angle);const ca=cos(-angle);pointsOut.push(vec2(x+ca*px-sa*py,y+sa*px+ca*py))}const drawPoints=tristrip?glPolyStrip(pointsOut):pointsOut;glDrawPoints(drawPoints,rgba)}function glDrawOutlineTransform(points,rgba,lineWidth,x,y,sx,sy,angle,wrap=true){const outlinePoints=glMakeOutline(points,lineWidth,wrap);glDrawPointsTransform(outlinePoints,rgba,x,y,sx,sy,angle,false)}function glDrawPoints(points,rgba){if(!glEnable||points.length<3)return;const vertCount=points.length+2;if(glBatchCount+vertCount>=gl_MAX_POLY_VERTEXES||glBatchAdditive!==glAdditive)glFlush();glSetPolyMode();let offset=glBatchCount*gl_INDICES_PER_POLY_VERTEX;for(let i=vertCount;i--;){const j=clamp(i-1,0,vertCount-3);const point=points[j];glPositionData[offset++]=point.x;glPositionData[offset++]=point.y;glColorData[offset++]=rgba}glBatchCount+=vertCount}function glDrawColoredPoints(points,pointColors){if(!glEnable||points.length<3)return;const vertCount=points.length+2;if(glBatchCount+vertCount>=gl_MAX_POLY_VERTEXES||glBatchAdditive!==glAdditive)glFlush();glSetPolyMode();let offset=glBatchCount*gl_INDICES_PER_POLY_VERTEX;for(let i=vertCount;i--;){const j=clamp(i-1,0,vertCount-3);const point=points[j];const color=pointColors[j];glPositionData[offset++]=point.x;glPositionData[offset++]=point.y;glColorData[offset++]=color}glBatchCount+=vertCount}function glMakeOutline(points,width,wrap=true){if(points.length<2)return[];const halfWidth=width/2;const strip=[];const n=points.length;const e=1e-6;const miterLimit=width*100;for(let i=0;i<n;i++){const prev=points[wrap?(i-1+n)%n:max(i-1,0)];const curr=points[i];const next=points[wrap?(i+1)%n:min(i+1,n-1)];const dx1=curr.x-prev.x;const dy1=curr.y-prev.y;const len1=(dx1*dx1+dy1*dy1)**.5;const dx2=next.x-curr.x;const dy2=next.y-curr.y;const len2=(dx2*dx2+dy2*dy2)**.5;if(len1<e&&len2<e)continue;const nx1=len1>e?-dy1/len1:0;const ny1=len1>e?dx1/len1:0;const nx2=len2>e?-dy2/len2:0;const ny2=len2>e?dx2/len2:0;let nx=nx1+nx2;let ny=ny1+ny2;const nlen=(nx*nx+ny*ny)**.5;if(nlen<e){nx=nx1;ny=ny1}else{nx/=nlen;ny/=nlen;const dot=nx1*nx+ny1*ny;if(dot>e){const miterLength=min(1/dot,miterLimit);nx*=miterLength;ny*=miterLength}}const inner=vec2(curr.x-nx*halfWidth,curr.y-ny*halfWidth);const outer=vec2(curr.x+nx*halfWidth,curr.y+ny*halfWidth);strip.push(inner);strip.push(outer)}if(strip.length>1&&wrap){strip.push(strip[0]);strip.push(strip[1])}return strip}function glPolyStrip(points){if(points.length<3)return[];const cross=(a,b,c)=>(b.x-a.x)*(c.y-a.y)-(b.y-a.y)*(c.x-a.x);const signedArea=poly=>{let area=0;for(let i=poly.length;i--;){const j=(i+1)%poly.length;area+=poly[i].cross(poly[j])}return area};if(signedArea(points)<0)points=points.reverse();const e=1e-9;const pointInTriangle=(p,a,b,c)=>{const c1=cross(a,b,p);const c2=cross(b,c,p);const c3=cross(c,a,p);const negative=(c1<-e?1:0)+(c2<-e?1:0)+(c3<-e?1:0);const positive=(c1>e?1:0)+(c2>e?1:0)+(c3>e?1:0);return!(negative&&positive)};const indices=[];for(let i=0;i<points.length;++i)indices[i]=i;const triangles=[];let attempts=0;const maxAttempts=points.length**2+100;while(indices.length>3&&attempts++<maxAttempts){let foundEar=false;for(let i=0;i<indices.length;i++){const i0=indices[(i+indices.length-1)%indices.length];const i1=indices[i];const i2=indices[(i+1)%indices.length];const a=points[i0],b=points[i1],c=points[i2];if(cross(a,b,c)<e)continue;let hasInside=false;for(let j=0;j<indices.length;j++){const k=indices[j];if(k===i0||k===i1||k===i2)continue;const p=points[k];hasInside=pointInTriangle(p,a,b,c);if(hasInside)break}if(hasInside)continue;triangles.push([i0,i1,i2]);indices.splice(i,1);foundEar=true;break}if(!foundEar){let worstIndex=-1,worstValue=Infinity;for(let i=0;i<indices.length;i++){const i0=indices[(i+indices.length-1)%indices.length];const i1=indices[i];const i2=indices[(i+1)%indices.length];const value=abs(cross(points[i0],points[i1],points[i2]));if(value<worstValue){worstValue=value;worstIndex=i}}if(worstIndex<0)break;const i0=indices[(worstIndex+indices.length-1)%indices.length];const i1=indices[worstIndex];const i2=indices[(worstIndex+1)%indices.length];triangles.push([i0,i1,i2]);indices.splice(worstIndex,1)}}if(indices.length===3)triangles.push([indices[0],indices[1],indices[2]]);if(!triangles.length)return[];const strip=[];let[a0,b0,c0]=triangles[0];strip.push(points[a0],points[b0],points[c0]);for(let i=1;i<triangles.length;i++){const[a,b,c]=triangles[i];strip.push(points[c0],points[a]);strip.push(points[a],points[b],points[c]);c0=c}return strip}let newgrounds;class NewgroundsMedal extends Medal{constructor(id,name,description,icon,src){super(id,name,description,icon,src)}unlock(){super.unlock();newgrounds&&newgrounds.unlockMedal(this.id)}}class NewgroundsPlugin{constructor(app_id,cipher,cryptoJS){ASSERT(!newgrounds,"there can only be one newgrounds object");ASSERT(!cipher||cryptoJS,"must provide cryptojs if there is a cipher");newgrounds=this;this.app_id=app_id;this.cipher=cipher;this.cryptoJS=cryptoJS;this.host=location?location.hostname:"";const url=new URL(location.href);this.session_id=url.searchParams.get("ngio_session_id");if(!this.session_id)return;const medalsResult=this.call("Medal.getList");this.medals=medalsResult?medalsResult.result.data["medals"]:[];debugMedals&&LOG(this.medals);for(const newgroundsMedal of this.medals){const medal=medals[newgroundsMedal["id"]];if(medal){medal.image=new Image;medal.image.src=newgroundsMedal["icon"];medal.name=newgroundsMedal["name"];medal.description=newgroundsMedal["description"];medal.unlocked=newgroundsMedal["unlocked"];medal.difficulty=newgroundsMedal["difficulty"];medal.value=newgroundsMedal["value"];if(medal.value)medal.description=medal.description+` (${medal.value})`}}const scoreboardResult=this.call("ScoreBoard.getBoards");this.scoreboards=scoreboardResult?scoreboardResult.result.data.scoreboards:[];debugMedals&&LOG(this.scoreboards);const keepAliveMS=60*1e3;setInterval(()=>this.call("Gateway.ping",0,true),keepAliveMS)}unlockMedal(id){return this.call("Medal.unlock",{id:id},true)}postScore(id,value){return this.call("ScoreBoard.postScore",{id:id,value:value},true)}getScores(id,user,social=0,skip=0,limit=10){return this.call("ScoreBoard.getScores",{id:id,user:user,social:social,skip:skip,limit:limit})}logView(){return this.call("App.logView",{host:this.host},true)}call(component,parameters,async=false){const call={component:component,parameters:parameters};if(this.cipher){const cryptoJS=this.cryptoJS;const aesKey=cryptoJS["enc"]["Base64"]["parse"](this.cipher);const iv=cryptoJS["lib"]["WordArray"]["random"](16);const encrypted=cryptoJS["AES"]["encrypt"](JSON.stringify(call),aesKey,{iv:iv});call["secure"]=cryptoJS["enc"]["Base64"]["stringify"](iv.concat(encrypted["ciphertext"]));call["parameters"]=0}const input={app_id:this.app_id,session_id:this.session_id,call:call};const formData=new FormData;formData.append("input",JSON.stringify(input));const xmlHttp=new XMLHttpRequest;const url="https://newgrounds.io/gateway_v3.php";xmlHttp.open("POST",url,!debugMedals&&async);try{xmlHttp.send(formData)}catch(e){debugMedals&&LOG("newgrounds call failed",e);return}debugMedals&&LOG(xmlHttp.responseText);return xmlHttp.responseText&&JSON.parse(xmlHttp.responseText)}}let postProcess;class PostProcessPlugin{constructor(shaderCode,includeMainCanvas=true){ASSERT(!postProcess,"Post process already initialized");postProcess=this;if(!shaderCode)shaderCode="void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}";this.shader=undefined;this.texture=undefined;initPostProcess();engineAddPlugin(undefined,postProcessRender,postProcessContextLost,postProcessContextRestored);function initPostProcess(){if(headlessMode)return;if(!glEnable){console.warn("PostProcessPlugin: WebGL not enabled!");return}postProcess.texture=glCreateTexture();postProcess.shader=glCreateProgram("#version 300 es\n"+"precision highp float;"+"in vec2 p;"+"void main(){"+"gl_Position=vec4(p+p-1.,1,1);"+"}","#version 300 es\n"+"precision highp float;"+"uniform sampler2D iChannel0;"+"uniform vec3 iResolution;"+"uniform float iTime;"+"out vec4 c;"+"\n"+shaderCode+"\n"+"void main(){"+"mainImage(c,gl_FragCoord.xy);"+"c.a=1.;"+"}")}function postProcessContextLost(){postProcess.shader=undefined;postProcess.texture=undefined;LOG("PostProcessPlugin: WebGL context lost")}function postProcessContextRestored(){initPostProcess();LOG("PostProcessPlugin: WebGL context restored")}function postProcessRender(){if(headlessMode)return;if(!glEnable)return;glFlush();glContext.activeTexture(glContext.TEXTURE0);glContext.bindTexture(glContext.TEXTURE_2D,postProcess.texture);if(includeMainCanvas){workCanvas.width=mainCanvasSize.x;workCanvas.height=mainCanvasSize.y;glCopyToContext(workContext);workContext.drawImage(mainCanvas,0,0);glContext.texImage2D(glContext.TEXTURE_2D,0,glContext.RGBA,glContext.RGBA,glContext.UNSIGNED_BYTE,workCanvas)}glContext.useProgram(postProcess.shader);glContext.bindBuffer(glContext.ARRAY_BUFFER,glGeometryBuffer);glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL,1);glContext.disable(glContext.BLEND);const vertexByteStride=8;const pLocation=glContext.getAttribLocation(postProcess.shader,"p");glContext.enableVertexAttribArray(pLocation);glContext.vertexAttribPointer(pLocation,2,glContext.FLOAT,false,vertexByteStride,0);const uniformLocation=name=>glContext.getUniformLocation(postProcess.shader,name);glContext.uniform1i(uniformLocation("iChannel0"),0);glContext.uniform1f(uniformLocation("iTime"),time);glContext.uniform3f(uniformLocation("iResolution"),mainCanvas.width,mainCanvas.height,1);glContext.drawArrays(glContext.TRIANGLE_STRIP,0,4)}}}class ZzFXMusic extends Sound{constructor(zzfxMusic){super(undefined);if(!soundEnable||headlessMode)return;this.randomness=0;this.sampleChannels=zzfxM(...zzfxMusic);this.sampleRate=audioDefaultSampleRate}playMusic(volume=1,loop=true){return super.play(undefined,volume,1,0,loop)}}function zzfxM(instruments,patterns,sequence,BPM=125){let i,j,k;let instrumentParameters;let note;let sample;let patternChannel;let notFirstBeat;let stop;let instrument;let attenuation;let outSampleOffset;let isSequenceEnd;let sampleOffset=0;let nextSampleOffset;let sampleBuffer=[];let leftChannelBuffer=[];let rightChannelBuffer=[];let channelIndex=0;let panning=0;let hasMore=1;let sampleCache={};let beatLength=audioDefaultSampleRate/BPM*60>>2;for(;hasMore;channelIndex++){sampleBuffer=[hasMore=notFirstBeat=outSampleOffset=0];sequence.forEach((patternIndex,sequenceIndex)=>{patternChannel=patterns[patternIndex][channelIndex]||[0,0,0];hasMore|=patterns[patternIndex][channelIndex]&&1;nextSampleOffset=outSampleOffset+(patterns[patternIndex][0].length-2-(notFirstBeat?0:1))*beatLength;isSequenceEnd=sequenceIndex===sequence.length-1;for(i=2,k=outSampleOffset;i<patternChannel.length+isSequenceEnd;notFirstBeat=++i){note=patternChannel[i];stop=i===patternChannel.length+isSequenceEnd-1&&isSequenceEnd||instrument!==(patternChannel[0]||0)||note|0;for(j=0;j<beatLength&&notFirstBeat;j++>beatLength-99&&stop&&attenuation<1?attenuation+=1/99:0){sample=(1-attenuation)*sampleBuffer[sampleOffset++]/2||0;leftChannelBuffer[k]=(leftChannelBuffer[k]||0)-sample*panning+sample;rightChannelBuffer[k]=(rightChannelBuffer[k++]||0)+sample*panning+sample}if(note){attenuation=note%1;panning=patternChannel[1]||0;if(note|=0){sampleBuffer=sampleCache[[instrument=patternChannel[sampleOffset=0]||0,note]]=sampleCache[[instrument,note]]||(instrumentParameters=[...instruments[instrument]],instrumentParameters[2]=(instrumentParameters[2]||220)*2**(note/12-1),note>0?zzfxG(...instrumentParameters):[])}}}outSampleOffset=nextSampleOffset})}return[leftChannelBuffer,rightChannelBuffer]}let uiSystem;let uiDebug=0;function uiSetDebug(debugMode){uiDebug=typeof debugMode==="boolean"?debugMode?1:0:debugMode}class UISystemPlugin{constructor(context=mainContext){ASSERT(!uiSystem,"UI system already initialized");uiSystem=this;this.activateOnPress=false;this.defaultColor=WHITE;this.defaultLineColor=BLACK;this.defaultTextColor=BLACK;this.defaultButtonColor=hsl(0,0,.7);this.defaultHoverColor=hsl(0,0,.9);this.defaultDisabledColor=hsl(0,0,.3);this.defaultGradientColor=undefined;this.defaultLineWidth=4;this.defaultCornerRadius=0;this.defaultTextFitScale=.8;this.defaultFont=fontDefault;this.defaultSoundPress=undefined;this.defaultSoundRelease=undefined;this.defaultSoundClick=undefined;this.defaultShadowColor=CLEAR_BLACK;this.defaultShadowBlur=5;this.defaultShadowOffset=vec2(5);this.nativeHeight=0;this.navigationObject=undefined;this.navigationTimer=new Timer(undefined,true);this.navigationDelay=.2;this.navigationDirection=1;this.navigationMode=false;this.uiObjects=[];this.uiContext=context;this.activeObject=undefined;this.hoverObject=undefined;this.lastHoverObject=undefined;this.confirmDialog=undefined;engineAddPlugin(uiUpdate,uiRender);function updateTransforms(o){if(!o.parent)return;o.pos.x=o.localPos.x+o.parent.pos.x;o.pos.y=o.localPos.y+o.parent.pos.y}function uiUpdate(){if(uiSystem.activeObject&&!uiSystem.activeObject.visible)uiSystem.activeObject=undefined;uiSystem.lastHoverObject=uiSystem.hoverObject;uiSystem.hoverObject=undefined;if(mouseWasPressed(0)){uiSystem.navigationMode=false;uiSystem.navigationObject=undefined}const navigableObjects=uiSystem.getNavigableObjects();if(!navigableObjects.length)uiSystem.navigationObject=undefined;else{if(!navigableObjects.includes(uiSystem.navigationObject))uiSystem.navigationObject=undefined;if(!isTouchDevice)if(uiSystem.navigationMode&&!uiSystem.navigationObject){uiSystem.navigationObject=navigableObjects.find(o=>o.navigationAutoSelect)}if(!uiSystem.navigationTimer.active()){const direction=sign(uiSystem.getNavigationDirection());if(direction){let newNavigationObject;if(!uiSystem.navigationObject){newNavigationObject=navigableObjects.find(o=>o.navigationAutoSelect);if(!newNavigationObject){const newIndex=direction>0?0:navigableObjects.length-1;newNavigationObject=navigableObjects[newIndex]}}else{const currentIndex=navigableObjects.indexOf(uiSystem.navigationObject);const newIndex=mod(currentIndex+direction,navigableObjects.length);newNavigationObject=navigableObjects[newIndex]}if(uiSystem.navigationObject!==newNavigationObject){uiSystem.navigationMode=true;uiSystem.hoverObject=undefined;uiSystem.navigationObject=newNavigationObject;uiSystem.navigationTimer.set(uiSystem.navigationDelay);newNavigationObject.soundPress&&newNavigationObject.soundPress.play()}}}if(uiSystem.navigationObject)if(uiSystem.getNavigationWasPressed())uiSystem.navigationObject.navigatePressed()}for(let i=uiSystem.uiObjects.length;i--;){const o=uiSystem.uiObjects[i];o.parent||updateObject(o)}uiSystem.uiObjects=uiSystem.uiObjects.filter(o=>!o.destroyed);function updateObject(o){if(!o.visible)return;updateTransforms(o);for(let i=o.children.length;i--;)updateObject(o.children[i]);o.update()}}function uiRender(){const context=uiSystem.uiContext;context.save();if(uiSystem.nativeHeight){const s=mainCanvasSize.y/uiSystem.nativeHeight;context.translate(-s*mainCanvasSize.x/2,0);context.scale(s,s);context.translate(mainCanvasSize.x/2/s,0)}function renderObject(o){if(!o.visible)return;updateTransforms(o);o.render();for(const c of o.children)renderObject(c)}uiSystem.uiObjects.forEach(o=>o.parent||renderObject(o));if(uiDebug>0){function renderDebug(o,visible=true){visible&&=!!o.visible;updateTransforms(o);o.renderDebug(visible);for(const c of o.children)renderDebug(c,visible)}uiSystem.uiObjects.forEach(o=>o.parent||renderDebug(o))}context.restore()}}drawRect(pos,size,color=WHITE,lineWidth=0,lineColor=BLACK,cornerRadius=0,gradientColor,shadowColor=BLACK,shadowBlur=0,shadowOffset=vec2()){ASSERT(isVector2(pos),"pos must be a vec2");ASSERT(isVector2(size),"size must be a vec2");ASSERT(isColor(color),"color must be a color");ASSERT(isNumber(lineWidth),"lineWidth must be a number");ASSERT(isColor(lineColor),"lineColor must be a color");ASSERT(isNumber(cornerRadius),"cornerRadius must be a number");const context=uiSystem.uiContext;if(gradientColor){const g=context.createLinearGradient(pos.x,pos.y-size.y/2,pos.x,pos.y+size.y/2);const c=color.toString();g.addColorStop(0,c);g.addColorStop(.5,gradientColor.toString());g.addColorStop(1,c);context.fillStyle=g}else context.fillStyle=color.toString();if(shadowBlur||shadowOffset.x||shadowOffset.y)if(shadowColor.a>0){context.shadowColor=shadowColor.toString();context.shadowBlur=shadowBlur;context.shadowOffsetX=shadowOffset.x;context.shadowOffsetY=shadowOffset.y}context.beginPath();if(cornerRadius&&context["roundRect"])context["roundRect"](pos.x-size.x/2,pos.y-size.y/2,size.x,size.y,cornerRadius);else context.rect(pos.x-size.x/2,pos.y-size.y/2,size.x,size.y);context.fill();context.shadowColor="#0000";if(lineWidth&&lineColor.a>0){context.strokeStyle=lineColor.toString();context.lineWidth=lineWidth;context.stroke()}}drawLine(posA,posB,lineWidth=uiSystem.defaultLineWidth,lineColor=uiSystem.defaultLineColor){ASSERT(isVector2(posA),"posA must be a vec2");ASSERT(isVector2(posB),"posB must be a vec2");ASSERT(isNumber(lineWidth),"lineWidth must be a number");ASSERT(isColor(lineColor),"lineColor must be a color");const context=uiSystem.uiContext;context.strokeStyle=lineColor.toString();context.lineWidth=lineWidth;context.beginPath();context.lineTo(posA.x,posA.y);context.lineTo(posB.x,posB.y);context.stroke()}drawTile(pos,size,tileInfo,color=uiSystem.defaultColor,angle=0,mirror=false,shadowColor=BLACK,shadowBlur=0,shadowOffset=vec2()){const context=uiSystem.uiContext;if(shadowBlur||shadowOffset.x||shadowOffset.y)if(shadowColor.a>0){context.shadowColor=shadowColor.toString();context.shadowBlur=shadowBlur;context.shadowOffsetX=shadowOffset.x;context.shadowOffsetY=shadowOffset.y}drawTile(pos,size,tileInfo,color,angle,mirror,CLEAR_BLACK,false,true,context);context.shadowColor="#0000"}drawText(text,pos,size,color=uiSystem.defaultColor,lineWidth=uiSystem.defaultLineWidth,lineColor=uiSystem.defaultLineColor,align="center",font=uiSystem.defaultFont,fontStyle="",applyMaxWidth=true,textShadow=undefined,shadowColor=BLACK,shadowBlur=0,shadowOffset=vec2()){const context=uiSystem.uiContext;if(shadowColor.a>0){if(textShadow)drawTextScreen(text,pos.add(textShadow),size.y,shadowColor,lineWidth,lineColor,align,font,fontStyle,applyMaxWidth?size.x:undefined,0,context);if(shadowBlur||shadowOffset.x||shadowOffset.y){context.shadowColor=shadowColor.toString();context.shadowBlur=shadowBlur;context.shadowOffsetX=shadowOffset.x;context.shadowOffsetY=shadowOffset.y}}drawTextScreen(text,pos,size.y,color,lineWidth,lineColor,align,font,fontStyle,applyMaxWidth?size.x:undefined,0,context);context.shadowColor="#0000"}setupDragAndDrop(onDrop,onDragEnter,onDragLeave,onDragOver){function setCallback(callback,listenerType){function listener(e){e.preventDefault();callback&&callback(e)}document.addEventListener(listenerType,listener)}setCallback(onDrop,"drop");setCallback(onDragEnter,"dragenter");setCallback(onDragLeave,"dragleave");setCallback(onDragOver,"dragover")}screenToNative(pos){if(!uiSystem.nativeHeight)return pos;const s=mainCanvasSize.y/uiSystem.nativeHeight;const sInv=1/s;const p=pos.copy();p.x+=s*mainCanvasSize.x/2;p.x*=sInv;p.y*=sInv;p.x-=sInv*mainCanvasSize.x/2;return p}destroyObjects(){for(const o of this.uiObjects)o.parent||o.destroy();this.uiObjects=this.uiObjects.filter(o=>!o.destroyed);this.activeObject=undefined;this.hoverObject=undefined;this.lastHoverObject=undefined}getNavigableObjects(){function getNavigableRecursive(o){if(!o.visible||o.disabled)return;if(o.isInteractive()&&o.navigationIndex!==undefined)objects.push(o);for(let i=o.children.length;i--;)getNavigableRecursive(o.children[i])}let objects=[];for(let i=uiSystem.uiObjects.length;i--;){const o=uiSystem.uiObjects[i];if(uiSystem.confirmDialog&&o!==uiSystem.confirmDialog)continue;o.parent||getNavigableRecursive(o)}objects.sort((a,b)=>a.navigationIndex-b.navigationIndex);return objects}getNavigationDirection(){const vertical=uiSystem.navigationDirection===1;const both=uiSystem.navigationDirection===2;if(isUsingGamepad){const stick=gamepadStick(0,gamepadPrimary);const dpad=gamepadDpad(gamepadPrimary);if(both)return-(stick.y||dpad.y)||(stick.x||dpad.x);return vertical?-(stick.y||dpad.y):stick.x||dpad.x}const up="ArrowUp",down="ArrowDown",left="ArrowLeft",right="ArrowRight";if(both){return keyIsDown(up)||keyIsDown(left)?-1:keyIsDown(down)||keyIsDown(right)?1:0}const back=vertical?up:left;const forward=vertical?down:right;return keyIsDown(back)?-1:keyIsDown(forward)?1:0}getNavigationOtherDirection(){if(uiSystem.navigationDirection===2)return 0;const vertical=uiSystem.navigationDirection===1;if(isUsingGamepad){const stick=gamepadStick(0,gamepadPrimary);const dpad=gamepadDpad(gamepadPrimary);return!vertical?stick.y||dpad.y:stick.x||dpad.x}const back=!vertical?"ArrowUp":"ArrowLeft";const forward=!vertical?"ArrowDown":"ArrowRight";return keyIsDown(back)?-1:keyIsDown(forward)?1:0}getNavigationWasPressed(){return isUsingGamepad?gamepadWasPressed(0,gamepadPrimary):keyWasPressed("Space")||keyWasPressed("Enter")}showConfirmDialog(text="Are you sure?",yesCallback,noCallback,size=vec2(500,250),exitKey="Escape"){ASSERT(!uiSystem.confirmDialog);const savedNavigationDirection=uiSystem.navigationDirection;uiSystem.navigationDirection=2;const confirmMenu=new UIObject(vec2(),size);uiSystem.confirmDialog=confirmMenu;confirmMenu.onRender=()=>{confirmMenu.pos=uiSystem.screenToNative(mainCanvasSize.scale(.5));const backgroundColor=hsl(0,0,0,.7);uiSystem.drawRect(vec2(),vec2(1e9),backgroundColor)};confirmMenu.onUpdate=()=>{if(keyWasPressed(exitKey))closeMenu()};confirmMenu.isMouseOverlapping=()=>true;const gap=50;const textTitle=new UIText(vec2(0,-50),vec2(size.x-gap,70),text);confirmMenu.addChild(textTitle);const buttonYes=new UIButton(vec2(-80,50),vec2(120,70),"Yes");buttonYes.textHeight=40;buttonYes.navigationIndex=1;buttonYes.hoverColor=hsl(0,1,.5);buttonYes.onClick=()=>{closeMenu();yesCallback&&yesCallback()};confirmMenu.addChild(buttonYes);const buttonNo=new UIButton(vec2(80,50),vec2(120,70),"No");buttonNo.textHeight=40;buttonNo.navigationIndex=2;buttonNo.navigationAutoSelect=true;buttonNo.onClick=()=>{closeMenu();noCallback&&noCallback()};confirmMenu.addChild(buttonNo);function closeMenu(){ASSERT(uiSystem.confirmDialog===confirmMenu);confirmMenu.destroy();uiSystem.confirmDialog=undefined;uiSystem.navigationDirection=savedNavigationDirection;inputClear()}}}class UIObject{constructor(pos=vec2(),size=vec2()){ASSERT(isVector2(pos),"ui object pos must be a vec2");ASSERT(isVector2(size),"ui object size must be a vec2");this.localPos=pos.copy();this.pos=pos.copy();this.size=size.copy();this.color=uiSystem.defaultColor.copy();this.activeColor=undefined;this.text=undefined;this.disabledColor=uiSystem.defaultDisabledColor.copy();this.disabled=false;this.textColor=uiSystem.defaultTextColor.copy();this.hoverColor=uiSystem.defaultHoverColor.copy();this.lineColor=uiSystem.defaultLineColor.copy();this.gradientColor=uiSystem.defaultGradientColor?uiSystem.defaultGradientColor.copy():undefined;this.lineWidth=uiSystem.defaultLineWidth;this.cornerRadius=uiSystem.defaultCornerRadius;this.font=uiSystem.defaultFont;this.fontStyle=undefined;this.textWidth=undefined;this.textHeight=undefined;this.textFitScale=uiSystem.defaultTextFitScale;this.textShadow=undefined;this.textLineColor=uiSystem.defaultLineColor.copy();this.textLineWidth=0;this.visible=true;this.children=[];this.parent=undefined;this.extraTouchSize=0;this.soundPress=uiSystem.defaultSoundPress;this.soundRelease=uiSystem.defaultSoundRelease;this.soundClick=uiSystem.defaultSoundClick;this.interactive=false;this.dragActivate=false;this.canBeHover=true;this.shadowColor=uiSystem.defaultShadowColor?.copy();this.shadowBlur=uiSystem.defaultShadowBlur;this.shadowOffset=uiSystem.defaultShadowOffset?.copy();this.navigationIndex=undefined;this.navigationAutoSelect=false;uiSystem.uiObjects.push(this)}addChild(child){ASSERT(!child.parent&&!this.children.includes(child));this.children.push(child);child.parent=this;return child}removeChild(child){ASSERT(child.parent===this&&this.children.includes(child));this.children.splice(this.children.indexOf(child),1);child.parent=undefined}destroy(){if(this.destroyed)return;this.destroyed=1;this.parent&&this.parent.removeChild(this);for(const child of this.children){child.parent=0;child.destroy()}}isMouseOverlapping(){if(!mouseInWindow)return false;const size=!isTouchDevice?this.size:this.size.add(vec2(this.extraTouchSize||0));const pos=uiSystem.screenToNative(mousePosScreen);return isOverlapping(this.pos,size,pos)}update(){this.onUpdate();if(this.disabled&&this===uiSystem.activeObject)uiSystem.activeObject=undefined;const wasHover=uiSystem.lastHoverObject===this;const isActive=this.isActiveObject();const mouseDown=mouseIsDown(0);const mousePress=this.dragActivate?mouseDown:mouseWasPressed(0);if(this.canBeHover)if(!uiSystem.navigationMode)if(mousePress||isActive||!mouseDown&&!isTouchDevice)if(!uiSystem.hoverObject&&this.isMouseOverlapping())uiSystem.hoverObject=this;if(this.isHoverObject()){if(!this.disabled){if(mousePress){if(this.interactive){if(!this.dragActivate||(!wasHover||mouseWasPressed(0)))this.onPress();this.soundPress&&this.soundPress.play();if(uiSystem.activeObject&&!isActive)uiSystem.activeObject.onRelease();uiSystem.activeObject=this;if(uiSystem.activateOnPress){this.onClick();if(!this.soundPress&&this.soundClick)this.soundClick.play()}}}if(!uiSystem.activateOnPress)if(!mouseDown&&this.isActiveObject()&&this.interactive){this.onClick();this.soundClick&&this.soundClick.play()}}mousePress&&inputClearKey(0,0,0,1,0)}if(isActive)if(!mouseDown||this.dragActivate&&!this.isHoverObject()){this.onRelease();this.soundRelease&&this.soundRelease.play();uiSystem.activeObject=undefined}if(this.isHoverObject()!==wasHover)this.isHoverObject()?this.onEnter():this.onLeave()}render(){this.onRender();if(!this.size.x||!this.size.y)return;const isNavigationObject=this.isNavigationObject();const lineColor=isNavigationObject?this.color:this.interactive&&this.isActiveObject()&&!this.disabled?this.color:this.lineColor;const color=isNavigationObject?this.hoverColor:this.disabled?this.disabledColor:this.interactive?this.isHoverObject()?this.hoverColor:this.isActiveObject()?this.activeColor||this.color:this.color:this.color;const lineWidth=this.lineWidth*(isNavigationObject?1.5:1);uiSystem.drawRect(this.pos,this.size,color,lineWidth,lineColor,this.cornerRadius,this.gradientColor,this.shadowColor,this.shadowBlur,this.shadowOffset)}getTextSize(){return vec2(this.textWidth||this.textFitScale*this.size.x,this.textHeight||this.textFitScale*this.size.y)}navigatePressed(){this.onClick();this.soundClick&&this.soundClick.play()}isHoverObject(){return uiSystem.hoverObject===this}isActiveObject(){return uiSystem.activeObject===this}isNavigationObject(){return uiSystem.navigationObject===this}isInteractive(){return this.interactive&&this.visible&&!this.disabled}toString(){if(!debug)return;let text="type = "+this.constructor.name;if(this.text)text+="\ntext = "+this.text;if(this.pos.x||this.pos.y)text+="\npos = "+this.pos;if(this.localPos.x||this.localPos.y)text+="localPos = "+this.localPos;if(this.size.x||this.size.y)text+="\nsize = "+this.size;if(this.color)text+="\ncolor = "+this.color;return text}renderDebug(visible=true){const color=!visible?GREEN:this.isHoverObject()?YELLOW:this.disabled?PURPLE:this.interactive?RED:BLUE;uiSystem.drawRect(this.pos,this.size,CLEAR_BLACK,4,color)}onUpdate(){}onRender(){}onEnter(){}onLeave(){}onPress(){}onRelease(){}onClick(){}onChange(){}}class UIText extends UIObject{constructor(pos,size,text="",align="center",font=uiSystem.defaultFont){super(pos,size);ASSERT(isString(text),"ui text must be a string");ASSERT(["left","center","right"].includes(align),"ui text align must be left, center, or right");ASSERT(isString(font),"ui text font must be a string");this.text=text;this.align=align;this.font=font;this.canBeHover=false;this.color=CLEAR_BLACK;this.shadowColor=CLEAR_BLACK;this.gradientColor=undefined;this.lineWidth=0;this.textFitScale=1}render(){super.render();const textSize=this.getTextSize();uiSystem.drawText(this.text,this.pos,textSize,this.textColor,this.textLineWidth,this.textLineColor,this.align,this.font,this.fontStyle,true,this.textShadow,this.shadowColor,this.shadowBlur,this.shadowOffset)}}class UITile extends UIObject{constructor(pos,size,tileInfo,color=WHITE,angle=0,mirror=false){super(pos,size);ASSERT(tileInfo instanceof TileInfo,"ui tile tileInfo must be a TileInfo");ASSERT(isColor(color),"ui tile color must be a color");ASSERT(isNumber(angle),"ui tile angle must be a number");this.tileInfo=tileInfo;this.angle=angle;this.mirror=mirror;this.color=color.copy();this.shadowColor=CLEAR_BLACK}render(){uiSystem.drawTile(this.pos,this.size,this.tileInfo,this.color,this.angle,this.mirror,this.shadowColor,this.shadowBlur,this.shadowOffset)}}class UIButton extends UIObject{constructor(pos,size,text="",color=uiSystem.defaultButtonColor){super(pos,size);ASSERT(isString(text),"ui button must be a string");ASSERT(isColor(color),"ui button color must be a color");this.textOffset=vec2();this.text=text;this.color=color.copy();this.interactive=true}render(){super.render();const textSize=this.getTextSize();uiSystem.drawText(this.text,this.pos.add(this.textOffset),textSize,this.textColor,this.textLineWidth,this.textLineColor,this.align,this.font,this.fontStyle,true,this.textShadow)}}class UICheckbox extends UIObject{constructor(pos,size,checked=false,text="",color=uiSystem.defaultButtonColor){super(pos,size);ASSERT(isString(text),"ui checkbox must be a string");ASSERT(isColor(color),"ui checkbox color must be a color");this.checked=checked;this.text=text;this.color=color.copy();this.interactive=true}onClick(){this.checked=!this.checked;this.onChange()}render(){super.render();if(this.checked){const p=this.cornerRadius/min(this.size.x,this.size.y)*2;const length=lerp(1,2**.5/2,p)/2;let s=this.size.scale(length);uiSystem.drawLine(this.pos.add(s.multiply(vec2(-1))),this.pos.add(s.multiply(vec2(1))),this.lineWidth,this.lineColor);uiSystem.drawLine(this.pos.add(s.multiply(vec2(-1,1))),this.pos.add(s.multiply(vec2(1,-1))),this.lineWidth,this.lineColor)}const textSize=this.getTextSize();const pos=this.pos.add(vec2(this.size.x,0));uiSystem.drawText(this.text,pos,textSize,this.textColor,this.textLineWidth,this.textLineColor,"left",this.font,this.fontStyle,false,this.textShadow)}}class UIScrollbar extends UIObject{constructor(pos,size,value=.5,text="",color=uiSystem.defaultButtonColor,handleColor=WHITE){super(pos,size);ASSERT(isNumber(value),"ui scrollbar value must be a number");ASSERT(isString(text),"ui scrollbar must be a string");ASSERT(isColor(color),"ui scrollbar color must be a color");ASSERT(isColor(handleColor),"ui scrollbar handleColor must be a color");this.value=value;this.handleColor=handleColor.copy();this.text=text;this.color=color.copy();this.interactive=true}update(){super.update();if(!this.interactive)return;const oldValue=this.value;if(this.isActiveObject()){const isHorizontal=this.size.x>this.size.y;const handleSize=isHorizontal?this.size.y:this.size.x;const barSize=isHorizontal?this.size.x:this.size.y;const centerPos=isHorizontal?this.pos.x:this.pos.y;const handleWidth=barSize-handleSize;const p1=centerPos-handleWidth/2;const p2=centerPos+handleWidth/2;const p=uiSystem.screenToNative(mousePosScreen);this.value=isHorizontal?percent(p.x,p1,p2):percent(p.y,p2,p1)}else if(this.isNavigationObject()){const direction=uiSystem.getNavigationOtherDirection();if(!uiSystem.navigationTimer.active())this.value=clamp(this.value+direction*.01)}this.value===oldValue||this.onChange()}render(){super.render();const isHorizontal=this.size.x>this.size.y;const handleSize=isHorizontal?this.size.y:this.size.x;const barSize=isHorizontal?this.size.x:this.size.y;const centerPos=isHorizontal?this.pos.x:this.pos.y;const handleWidth=barSize-handleSize;const p1=centerPos-handleWidth/2;const p2=centerPos+handleWidth/2;const handlePos=isHorizontal?vec2(lerp(p1,p2,this.value),this.pos.y):vec2(this.pos.x,lerp(p2,p1,this.value));const handleColor=this.disabled?this.disabledColor:this.handleColor;uiSystem.drawRect(handlePos,vec2(handleSize),handleColor,this.lineWidth,this.lineColor,this.cornerRadius,this.gradientColor);const textSize=this.getTextSize();uiSystem.drawText(this.text,this.pos,textSize,this.textColor,this.textLineWidth,this.textLineColor,this.align,this.font,this.fontStyle,true,this.textShadow)}navigatePressed(){this.value=this.value?0:1;this.onRelease();super.navigatePressed()}}class UIVideo extends UIObject{constructor(pos,size,src,autoplay=false,loop=false,volume=1){super(pos,size||vec2());ASSERT(isString(src),"video src must be a string");ASSERT(isNumber(volume),"video volume must be a number");this.color=BLACK;this.cornerRadius=0;this.volume=volume;this.video=document.createElement("video");this.video.loop=loop;this.video.volume=clamp(volume*soundVolume);this.video.muted=!soundEnable;this.video.style.display="none";this.video.src=src;document.body.appendChild(this.video);autoplay&&this.play()}play(){const promise=this.video.play();promise?.catch(()=>{});return promise}pause(){this.video.pause()}stop(){this.video.pause();this.video.currentTime=0}isLoading(){return this.video.readyState<this.video.HAVE_CURRENT_DATA}isPaused(){return this.video.paused}isPlaying(){return!this.isPaused()&&!this.hasEnded()&&!this.isLoading()}hasEnded(){return this.video.ended}setVolume(volume){this.volume=volume;this.video.volume=clamp(volume*soundVolume)}setPlaybackRate(rate){this.video.playbackRate=rate}getCurrentTime(){return this.video.currentTime||0}getDuration(){return this.video.duration||0}getVideoSize(){return vec2(this.video.videoWidth,this.video.videoHeight)}setTime(time){this.video.currentTime=clamp(time,0,this.getDuration())}update(){super.update();this.video.volume=clamp(this.volume*soundVolume)}render(){super.render();if(this.isLoading())return;const context=uiSystem.uiContext;const s=this.size;context.save();context.translate(this.pos.x,this.pos.y);context.drawImage(this.video,-s.x/2,-s.y/2,s.x,s.y);context.restore()}destroy(){if(this.destroyed)return;this.video.pause();this.video.remove();super.destroy()}}let box2d;let box2dDebug=false;function box2dSetDebug(enable){box2dDebug=enable}class Box2dObject extends EngineObject{constructor(pos=vec2(),size=vec2(),tileInfo,angle=0,color,bodyType=box2d.bodyTypeDynamic,renderOrder=0){super(pos,size,tileInfo,angle,color,renderOrder);const bodyDef=new box2d.instance.b2BodyDef;bodyDef.set_type(bodyType);bodyDef.set_position(box2d.vec2dTo(pos));bodyDef.set_angle(-angle);this.body=box2d.world.CreateBody(bodyDef);this.body.object=this;this.lineColor=BLACK;box2d.objects.push(this);this.edgeLists=[];this.edgeLoops=[]}destroy(){if(this.destroyed)return;ASSERT(this.body,"Box2dObject has no body to destroy");box2d.world.DestroyBody(this.body);super.destroy()}updatePhysics(){}render(){if(this.tileInfo)super.render();else this.drawFixtures(this.color,this.lineColor,this.lineWidth)}renderDebugInfo(){const isAsleep=!this.getIsAwake();const isStatic=this.getBodyType()===box2d.bodyTypeStatic;const color=rgb(isAsleep?1:0,isAsleep?1:0,isStatic?1:0,.5);this.drawFixtures(color)}drawFixtures(color=WHITE,lineColor=BLACK,lineWidth=.1,context){this.getFixtureList().forEach(fixture=>{const shape=box2d.castObjectType(fixture.GetShape());if(shape.GetType()!==box2d.instance.b2Shape.e_edge){box2d.drawFixture(fixture,this.pos,this.angle,color,lineColor,lineWidth,context)}});this.edgeLists.forEach(points=>drawLineList(points,lineWidth,lineColor,false,this.pos,this.angle));this.edgeLoops.forEach(points=>drawLineList(points,lineWidth,lineColor,true,this.pos,this.angle))}beginContact(otherObject){}endContact(otherObject){}addShape(shape,density=1,friction=.2,restitution=0,isSensor=false){ASSERT(isNumber(density),"density must be a number");ASSERT(isNumber(friction),"friction must be a number");ASSERT(isNumber(restitution),"restitution must be a number");const fd=new box2d.instance.b2FixtureDef;fd.set_shape(shape);fd.set_density(density);fd.set_friction(friction);fd.set_restitution(restitution);fd.set_isSensor(isSensor);return this.body.CreateFixture(fd)}addBox(size=vec2(1),offset=vec2(),angle=0,density,friction,restitution,isSensor){ASSERT(isVector2(size),"size must be a Vector2");ASSERT(size.x>0&&size.y>0,"size must be positive");ASSERT(isVector2(offset),"offset must be a Vector2");ASSERT(isNumber(angle),"angle must be a number");const shape=new box2d.instance.b2PolygonShape;shape.SetAsBox(size.x/2,size.y/2,box2d.vec2dTo(offset),angle);return this.addShape(shape,density,friction,restitution,isSensor)}addPoly(points,density,friction,restitution,isSensor){ASSERT(isArray(points),"points must be an array");function box2dCreatePolygonShape(points){function box2dCreatePointList(points){const buffer=box2d.instance._malloc(points.length*8);for(let i=0,offset=0;i<points.length;++i){box2d.instance.HEAPF32[buffer+offset>>2]=points[i].x;offset+=4;box2d.instance.HEAPF32[buffer+offset>>2]=points[i].y;offset+=4}return box2d.instance.wrapPointer(buffer,box2d.instance.b2Vec2)}ASSERT(3<=points.length&&points.length<=8);const shape=new box2d.instance.b2PolygonShape;const box2dPoints=box2dCreatePointList(points);shape.Set(box2dPoints,points.length);return shape}const shape=box2dCreatePolygonShape(points);return this.addShape(shape,density,friction,restitution,isSensor)}addRegularPoly(diameter=1,sides=8,density,friction,restitution,isSensor){ASSERT(isNumber(diameter)&&diameter>0,"diameter must be a positive number");ASSERT(isNumber(sides)&&sides>2,"sides must be a positive number greater than 2");const points=[];const radius=diameter/2;for(let i=sides;i--;)points.push(vec2(radius,0).rotate((i+.5)/sides*PI*2));return this.addPoly(points,density,friction,restitution,isSensor)}addRandomPoly(diameter=1,density,friction,restitution,isSensor){ASSERT(isNumber(diameter)&&diameter>0,"diameter must be a positive number");const sides=randInt(3,9);const points=[];const radius=diameter/2;for(let i=sides;i--;)points.push(vec2(rand(radius/2,radius*1.5),0).rotate(i/sides*PI*2));return this.addPoly(points,density,friction,restitution,isSensor)}addCircle(diameter=1,offset=vec2(),density,friction,restitution,isSensor){ASSERT(isNumber(diameter)&&diameter>0,"diameter must be a positive number");ASSERT(isVector2(offset),"offset must be a Vector2");const shape=new box2d.instance.b2CircleShape;shape.set_m_p(box2d.vec2dTo(offset));shape.set_m_radius(diameter/2);return this.addShape(shape,density,friction,restitution,isSensor)}addEdge(point1,point2,density,friction,restitution,isSensor){ASSERT(isVector2(point1),"point1 must be a Vector2");ASSERT(isVector2(point2),"point2 must be a Vector2");const shape=new box2d.instance.b2EdgeShape;shape.Set(box2d.vec2dTo(point1),box2d.vec2dTo(point2));return this.addShape(shape,density,friction,restitution,isSensor)}addEdgeList(points,density,friction,restitution,isSensor){ASSERT(isArray(points),"points must be an array");const fixtures=[],edgePoints=[];for(let i=0;i<points.length-1;++i){const shape=new box2d.instance.b2EdgeShape;points[i-1]&&shape.set_m_vertex0(box2d.vec2dTo(points[i-1]));points[i+0]&&shape.set_m_vertex1(box2d.vec2dTo(points[i+0]));points[i+1]&&shape.set_m_vertex2(box2d.vec2dTo(points[i+1]));points[i+2]&&shape.set_m_vertex3(box2d.vec2dTo(points[i+2]));const f=this.addShape(shape,density,friction,restitution,isSensor);fixtures.push(f);edgePoints.push(points[i].copy())}edgePoints.push(points[points.length-1].copy());this.edgeLists.push(edgePoints);return fixtures}addEdgeLoop(points,density,friction,restitution,isSensor){ASSERT(isArray(points),"points must be an array");const fixtures=[],edgePoints=[];const getPoint=i=>points[mod(i,points.length)];for(let i=0;i<points.length;++i){const shape=new box2d.instance.b2EdgeShape;shape.set_m_vertex0(box2d.vec2dTo(getPoint(i-1)));shape.set_m_vertex1(box2d.vec2dTo(getPoint(i+0)));shape.set_m_vertex2(box2d.vec2dTo(getPoint(i+1)));shape.set_m_vertex3(box2d.vec2dTo(getPoint(i+2)));const f=this.addShape(shape,density,friction,restitution,isSensor);fixtures.push(f);i<points.length&&edgePoints.push(points[i].copy())}this.edgeLoops.push(edgePoints);return fixtures}getCenterOfMass(){return box2d.vec2From(this.body.GetWorldCenter())}getLinearVelocity(){return box2d.vec2From(this.body.GetLinearVelocity())}getAngularVelocity(){return this.body.GetAngularVelocity()}getMass(){return this.body.GetMass()}getInertia(){return this.body.GetInertia()}getIsAwake(){return this.body.IsAwake()}getBodyType(){return this.body.GetType()}getSpeed(){return this.getLinearVelocity().length()}setTransform(pos,angle){this.pos=pos;this.angle=angle;this.body.SetTransform(box2d.vec2dTo(pos),angle)}setPosition(pos){this.setTransform(pos,this.body.GetAngle())}setAngle(angle){this.setTransform(box2d.vec2From(this.body.GetPosition()),-angle)}setLinearVelocity(velocity){this.body.SetLinearVelocity(box2d.vec2dTo(velocity))}setAngularVelocity(angularVelocity){this.body.SetAngularVelocity(angularVelocity)}setLinearDamping(damping){this.body.SetLinearDamping(damping)}setAngularDamping(damping){this.body.SetAngularDamping(damping)}setGravityScale(scale=1){this.body.SetGravityScale(this.gravityScale=scale)}setBullet(isBullet=true){this.body.SetBullet(isBullet)}setAwake(isAwake=true){this.body.SetAwake(isAwake)}setBodyType(type){this.body.SetType(type)}setSleepingAllowed(isAllowed=true){this.body.SetSleepingAllowed(isAllowed)}setFixedRotation(isFixed=true){this.body.SetFixedRotation(isFixed)}setCenterOfMass(center){this.setMassData(center)}setMass(mass){this.setMassData(undefined,mass)}setMomentOfInertia(momentOfInertia){this.setMassData(undefined,undefined,momentOfInertia)}resetMassData(){this.body.ResetMassData()}setMassData(localCenter,mass,momentOfInertia){const data=new box2d.instance.b2MassData;this.body.GetMassData(data);localCenter&&data.set_center(box2d.vec2dTo(localCenter));mass&&data.set_mass(mass);momentOfInertia&&data.set_I(momentOfInertia);this.body.SetMassData(data)}setFilterData(categoryBits=0,ignoreCategoryBits=0,groupIndex=0){this.getFixtureList().forEach(fixture=>{const filter=fixture.GetFilterData();filter.set_categoryBits(categoryBits);filter.set_maskBits(65535&~ignoreCategoryBits);filter.set_groupIndex(groupIndex)})}setSensor(isSensor=true){this.getFixtureList().forEach(f=>f.SetSensor(isSensor))}applyForce(force,pos){pos||=this.getCenterOfMass();this.setAwake();this.body.ApplyForce(box2d.vec2dTo(force),box2d.vec2dTo(pos))}applyAcceleration(acceleration,pos){pos||=this.getCenterOfMass();this.setAwake();this.body.ApplyLinearImpulse(box2d.vec2dTo(acceleration),box2d.vec2dTo(pos))}applyTorque(torque){this.setAwake();this.body.ApplyTorque(torque)}applyAngularAcceleration(acceleration){this.setAwake();this.body.ApplyAngularImpulse(acceleration)}hasFixtures(){return!box2d.isNull(this.body.GetFixtureList())}getFixtureList(){const fixtures=[];for(let fixture=this.body.GetFixtureList();!box2d.isNull(fixture);){fixtures.push(fixture);fixture=fixture.GetNext()}return fixtures}hasJoints(){return!box2d.isNull(this.body.GetJointList())}getJointList(){const joints=[];for(let joint=this.body.GetJointList();!box2d.isNull(joint);){joints.push(joint);joint=joint.get_next()}return joints}}class Box2dRaycastResult{constructor(fixture,point,normal,fraction){this.object=fixture.GetBody().object;this.fixture=fixture;this.point=point;this.normal=normal;this.fraction=fraction}}class Box2dJoint{constructor(jointDef){this.box2dJoint=box2d.castObjectType(box2d.world.CreateJoint(jointDef))}destroy(){box2d.world.DestroyJoint(this.box2dJoint);this.box2dJoint=0}getObjectA(){return this.box2dJoint.GetBodyA().object}getObjectB(){return this.box2dJoint.GetBodyB().object}getAnchorA(){return box2d.vec2From(this.box2dJoint.GetAnchorA())}getAnchorB(){return box2d.vec2From(this.box2dJoint.GetAnchorB())}getReactionForce(time){return box2d.vec2From(this.box2dJoint.GetReactionForce(1/time))}getReactionTorque(time){return this.box2dJoint.GetReactionTorque(1/time)}getCollideConnected(){return this.box2dJoint.getCollideConnected()}isActive(){return this.box2dJoint.IsActive()}}class Box2dTargetJoint extends Box2dJoint{constructor(object,fixedObject,worldPos){object.setAwake();const jointDef=new box2d.instance.b2MouseJointDef;jointDef.set_bodyA(fixedObject.body);jointDef.set_bodyB(object.body);jointDef.set_target(box2d.vec2dTo(worldPos));jointDef.set_maxForce(2e3*object.getMass());super(jointDef)}setTarget(pos){this.box2dJoint.SetTarget(box2d.vec2dTo(pos))}getTarget(){return box2d.vec2From(this.box2dJoint.GetTarget())}setMaxForce(force){this.box2dJoint.SetMaxForce(force)}getMaxForce(){return this.box2dJoint.GetMaxForce()}setFrequency(hz){this.box2dJoint.SetFrequency(hz)}getFrequency(){return this.box2dJoint.GetFrequency()}}class Box2dDistanceJoint extends Box2dJoint{constructor(objectA,objectB,anchorA,anchorB,collide=false){anchorA||=box2d.vec2From(objectA.body.GetPosition());anchorB||=box2d.vec2From(objectB.body.GetPosition());const localAnchorA=objectA.worldToLocal(anchorA);const localAnchorB=objectB.worldToLocal(anchorB);const jointDef=new box2d.instance.b2DistanceJointDef;jointDef.set_bodyA(objectA.body);jointDef.set_bodyB(objectB.body);jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));jointDef.set_length(anchorA.distance(anchorB));jointDef.set_collideConnected(collide);super(jointDef)}getLocalAnchorA(){return box2d.vec2From(this.box2dJoint.GetLocalAnchorA())}getLocalAnchorB(){return box2d.vec2From(this.box2dJoint.GetLocalAnchorB())}setLength(length){this.box2dJoint.SetLength(length)}getLength(){return this.box2dJoint.GetLength()}setFrequency(hz){this.box2dJoint.SetFrequency(hz)}getFrequency(){return this.box2dJoint.GetFrequency()}setDampingRatio(ratio){this.box2dJoint.SetDampingRatio(ratio)}getDampingRatio(){return this.box2dJoint.GetDampingRatio()}}class Box2dPinJoint extends Box2dDistanceJoint{constructor(objectA,objectB,pos=objectA.pos,collide=false){super(objectA,objectB,undefined,pos,collide)}}class Box2dRopeJoint extends Box2dJoint{constructor(objectA,objectB,anchorA,anchorB,extraLength=0,collide=false){anchorA||=box2d.vec2From(objectA.body.GetPosition());anchorB||=box2d.vec2From(objectB.body.GetPosition());const localAnchorA=objectA.worldToLocal(anchorA);const localAnchorB=objectB.worldToLocal(anchorB);const jointDef=new box2d.instance.b2RopeJointDef;jointDef.set_bodyA(objectA.body);jointDef.set_bodyB(objectB.body);jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));jointDef.set_maxLength(anchorA.distance(anchorB)+extraLength);jointDef.set_collideConnected(collide);super(jointDef)}getLocalAnchorA(){return box2d.vec2From(this.box2dJoint.GetLocalAnchorA())}getLocalAnchorB(){return box2d.vec2From(this.box2dJoint.GetLocalAnchorB())}setMaxLength(length){this.box2dJoint.SetMaxLength(length)}getMaxLength(){return this.box2dJoint.GetMaxLength()}}class Box2dRevoluteJoint extends Box2dJoint{constructor(objectA,objectB,anchor,collide=false){anchor||=box2d.vec2From(objectB.body.GetPosition());const localAnchorA=objectA.worldToLocal(anchor);const localAnchorB=objectB.worldToLocal(anchor);const jointDef=new box2d.instance.b2RevoluteJointDef;jointDef.set_bodyA(objectA.body);jointDef.set_bodyB(objectB.body);jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));jointDef.set_referenceAngle(objectA.body.GetAngle()-objectB.body.GetAngle());jointDef.set_collideConnected(collide);super(jointDef)}getLocalAnchorA(){return box2d.vec2From(this.box2dJoint.GetLocalAnchorA())}getLocalAnchorB(){return box2d.vec2From(this.box2dJoint.GetLocalAnchorB())}getReferenceAngle(){return this.box2dJoint.GetReferenceAngle()}getJointAngle(){return this.box2dJoint.GetJointAngle()}getJointSpeed(){return this.box2dJoint.GetJointSpeed()}isLimitEnabled(){return this.box2dJoint.IsLimitEnabled()}enableLimit(enable=true){return this.box2dJoint.enableLimit(enable)}getLowerLimit(){return this.box2dJoint.GetLowerLimit()}getUpperLimit(){return this.box2dJoint.GetUpperLimit()}setLimits(min,max){return this.box2dJoint.SetLimits(min,max)}isMotorEnabled(){return this.box2dJoint.IsMotorEnabled()}enableMotor(enable=true){return this.box2dJoint.EnableMotor(enable)}setMotorSpeed(speed){return this.box2dJoint.SetMotorSpeed(speed)}getMotorSpeed(){return this.box2dJoint.GetMotorSpeed()}setMaxMotorTorque(torque){return this.box2dJoint.SetMaxMotorTorque(torque)}getMaxMotorTorque(){return this.box2dJoint.GetMaxMotorTorque()}getMotorTorque(time){return this.box2dJoint.GetMotorTorque(1/time)}}class Box2dGearJoint extends Box2dJoint{constructor(objectA,objectB,joint1,joint2,ratio=1){const jointDef=new box2d.instance.b2GearJointDef;jointDef.set_bodyA(objectA.body);jointDef.set_bodyB(objectB.body);jointDef.set_joint1(joint1.box2dJoint);jointDef.set_joint2(joint2.box2dJoint);jointDef.set_ratio(ratio);super(jointDef);this.joint1=joint1;this.joint2=joint2}getJoint1(){return this.joint1}getJoint2(){return this.joint2}setRatio(ratio){return this.box2dJoint.SetRatio(ratio)}getRatio(){return this.box2dJoint.GetRatio()}}class Box2dPrismaticJoint extends Box2dJoint{constructor(objectA,objectB,anchor,worldAxis=vec2(0,1),collide=false){anchor||=box2d.vec2From(objectB.body.GetPosition());const localAnchorA=objectA.worldToLocal(anchor);const localAnchorB=objectB.worldToLocal(anchor);const localAxisA=objectB.worldToLocalVector(worldAxis);const jointDef=new box2d.instance.b2PrismaticJointDef;jointDef.set_bodyA(objectA.body);jointDef.set_bodyB(objectB.body);jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));jointDef.set_localAxisA(box2d.vec2dTo(localAxisA));jointDef.set_referenceAngle(objectA.body.GetAngle()-objectB.body.GetAngle());jointDef.set_collideConnected(collide);super(jointDef)}getLocalAnchorA(){return box2d.vec2From(this.box2dJoint.GetLocalAnchorA())}getLocalAnchorB(){return box2d.vec2From(this.box2dJoint.GetLocalAnchorB())}getLocalAxisA(){return box2d.vec2From(this.box2dJoint.GetLocalAxisA())}getReferenceAngle(){return this.box2dJoint.GetReferenceAngle()}getJointTranslation(){return this.box2dJoint.GetJointTranslation()}getJointSpeed(){return this.box2dJoint.GetJointSpeed()}isLimitEnabled(){return this.box2dJoint.IsLimitEnabled()}enableLimit(enable=true){return this.box2dJoint.enableLimit(enable)}getLowerLimit(){return this.box2dJoint.GetLowerLimit()}getUpperLimit(){return this.box2dJoint.GetUpperLimit()}setLimits(min,max){return this.box2dJoint.SetLimits(min,max)}isMotorEnabled(){return this.box2dJoint.IsMotorEnabled()}enableMotor(enable=true){return this.box2dJoint.EnableMotor(enable)}setMotorSpeed(speed){return this.box2dJoint.SetMotorSpeed(speed)}getMotorSpeed(){return this.box2dJoint.GetMotorSpeed()}setMaxMotorForce(force){return this.box2dJoint.SetMaxMotorForce(force)}getMaxMotorForce(){return this.box2dJoint.GetMaxMotorForce()}getMotorForce(time){return this.box2dJoint.GetMotorForce(1/time)}}class Box2dStaticObject extends Box2dObject{constructor(pos,size,tileInfo,angle=0,color,renderOrder=0){const bodyType=box2d.bodyTypeStatic;super(pos,size,tileInfo,angle,color,bodyType,renderOrder)}}class Box2dKiematicObject extends Box2dObject{constructor(pos,size,tileInfo,angle=0,color,renderOrder=0){const bodyType=box2d.bodyTypeKinematic;super(pos,size,tileInfo,angle,color,bodyType,renderOrder)}}class Box2dWheelJoint extends Box2dJoint{constructor(objectA,objectB,anchor,worldAxis=vec2(0,1),collide=false){anchor||=box2d.vec2From(objectB.body.GetPosition());const localAnchorA=objectA.worldToLocal(anchor);const localAnchorB=objectB.worldToLocal(anchor);const localAxisA=objectB.worldToLocalVector(worldAxis);const jointDef=new box2d.instance.b2WheelJointDef;jointDef.set_bodyA(objectA.body);jointDef.set_bodyB(objectB.body);jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));jointDef.set_localAxisA(box2d.vec2dTo(localAxisA));jointDef.set_collideConnected(collide);super(jointDef)}getLocalAnchorA(){return box2d.vec2From(this.box2dJoint.GetLocalAnchorA())}getLocalAnchorB(){return box2d.vec2From(this.box2dJoint.GetLocalAnchorB())}getLocalAxisA(){return box2d.vec2From(this.box2dJoint.GetLocalAxisA())}getJointTranslation(){return this.box2dJoint.GetJointTranslation()}getJointSpeed(){return this.box2dJoint.GetJointSpeed()}isMotorEnabled(){return this.box2dJoint.IsMotorEnabled()}enableMotor(enable=true){return this.box2dJoint.EnableMotor(enable)}setMotorSpeed(speed){return this.box2dJoint.SetMotorSpeed(speed)}getMotorSpeed(){return this.box2dJoint.GetMotorSpeed()}setMaxMotorTorque(torque){return this.box2dJoint.SetMaxMotorTorque(torque)}getMaxMotorTorque(){return this.box2dJoint.GetMaxMotorTorque()}getMotorTorque(time){return this.box2dJoint.GetMotorTorque(1/time)}setSpringFrequencyHz(hz){return this.box2dJoint.SetSpringFrequencyHz(hz)}getSpringFrequencyHz(){return this.box2dJoint.GetSpringFrequencyHz()}setSpringDampingRatio(ratio){return this.box2dJoint.SetSpringDampingRatio(ratio)}getSpringDampingRatio(){return this.box2dJoint.GetSpringDampingRatio()}}class Box2dWeldJoint extends Box2dJoint{constructor(objectA,objectB,anchor,collide=false){anchor||=box2d.vec2From(objectB.body.GetPosition());const localAnchorA=objectA.worldToLocal(anchor);const localAnchorB=objectB.worldToLocal(anchor);const jointDef=new box2d.instance.b2WeldJointDef;jointDef.set_bodyA(objectA.body);jointDef.set_bodyB(objectB.body);jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));jointDef.set_referenceAngle(objectA.body.GetAngle()-objectB.body.GetAngle());jointDef.set_collideConnected(collide);super(jointDef)}getLocalAnchorA(){return box2d.vec2From(this.box2dJoint.GetLocalAnchorA())}getLocalAnchorB(){return box2d.vec2From(this.box2dJoint.GetLocalAnchorB())}getReferenceAngle(){return this.box2dJoint.GetReferenceAngle()}setFrequency(hz){return this.box2dJoint.SetFrequency(hz)}getFrequency(){return this.box2dJoint.GetFrequency()}setSpringDampingRatio(ratio){return this.box2dJoint.SetSpringDampingRatio(ratio)}getSpringDampingRatio(){return this.box2dJoint.GetSpringDampingRatio()}}class Box2dFrictionJoint extends Box2dJoint{constructor(objectA,objectB,anchor,collide=false){anchor||=box2d.vec2From(objectB.body.GetPosition());const localAnchorA=objectA.worldToLocal(anchor);const localAnchorB=objectB.worldToLocal(anchor);const jointDef=new box2d.instance.b2FrictionJointDef;jointDef.set_bodyA(objectA.body);jointDef.set_bodyB(objectB.body);jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));jointDef.set_collideConnected(collide);super(jointDef)}getLocalAnchorA(){return box2d.vec2From(this.box2dJoint.GetLocalAnchorA())}getLocalAnchorB(){return box2d.vec2From(this.box2dJoint.GetLocalAnchorB())}setMaxForce(force){this.box2dJoint.SetMaxForce(force)}getMaxForce(){return this.box2dJoint.GetMaxForce()}setMaxTorque(torque){this.box2dJoint.SetMaxTorque(torque)}getMaxTorque(){return this.box2dJoint.GetMaxTorque()}}class Box2dPulleyJoint extends Box2dJoint{constructor(objectA,objectB,groundAnchorA,groundAnchorB,anchorA,anchorB,ratio=1,collide=false){anchorA||=box2d.vec2From(objectA.body.GetPosition());anchorB||=box2d.vec2From(objectB.body.GetPosition());const localAnchorA=objectA.worldToLocal(anchorA);const localAnchorB=objectB.worldToLocal(anchorB);const jointDef=new box2d.instance.b2PulleyJointDef;jointDef.set_bodyA(objectA.body);jointDef.set_bodyB(objectB.body);jointDef.set_groundAnchorA(box2d.vec2dTo(groundAnchorA));jointDef.set_groundAnchorB(box2d.vec2dTo(groundAnchorB));jointDef.set_localAnchorA(box2d.vec2dTo(localAnchorA));jointDef.set_localAnchorB(box2d.vec2dTo(localAnchorB));jointDef.set_ratio(ratio);jointDef.set_lengthA(groundAnchorA.distance(anchorA));jointDef.set_lengthB(groundAnchorB.distance(anchorB));jointDef.set_collideConnected(collide);super(jointDef)}getGroundAnchorA(){return box2d.vec2From(this.box2dJoint.GetGroundAnchorA())}getGroundAnchorB(){return box2d.vec2From(this.box2dJoint.GetGroundAnchorB())}getLengthA(){return this.box2dJoint.GetLengthA()}getLengthB(){return this.box2dJoint.GetLengthB()}getRatio(){return this.box2dJoint.GetRatio()}getCurrentLengthA(){return this.box2dJoint.GetCurrentLengthA()}getCurrentLengthB(){return this.box2dJoint.GetCurrentLengthB()}}class Box2dMotorJoint extends Box2dJoint{constructor(objectA,objectB){const linearOffset=objectA.worldToLocal(box2d.vec2From(objectB.body.GetPosition()));const angularOffset=objectB.body.GetAngle()-objectA.body.GetAngle();const jointDef=new box2d.instance.b2MotorJointDef;jointDef.set_bodyA(objectA.body);jointDef.set_bodyB(objectB.body);jointDef.set_linearOffset(box2d.vec2dTo(linearOffset));jointDef.set_angularOffset(angularOffset);super(jointDef)}setLinearOffset(offset){this.box2dJoint.SetLinearOffset(box2d.vec2dTo(offset))}getLinearOffset(){return box2d.vec2From(this.box2dJoint.GetLinearOffset())}setAngularOffset(offset){this.box2dJoint.SetAngularOffset(offset)}getAngularOffset(){return this.box2dJoint.GetAngularOffset()}setMaxForce(force){this.box2dJoint.SetMaxForce(force)}getMaxForce(){return this.box2dJoint.GetMaxForce()}setMaxTorque(torque){this.box2dJoint.SetMaxTorque(torque)}getMaxTorque(){return this.box2dJoint.GetMaxTorque()}setCorrectionFactor(factor){this.box2dJoint.SetCorrectionFactor(factor)}getCorrectionFactor(){return this.box2dJoint.GetCorrectionFactor()}}class Box2dPlugin{constructor(instance){ASSERT(!box2d,"Box2D already initialized");box2d=this;this.instance=instance;this.world=new box2d.instance.b2World;this.objects=[];this.velocityIterations=8;this.positionIterations=3;this.bodyTypeStatic=instance.b2_staticBody;this.bodyTypeKinematic=instance.b2_kinematicBody;this.bodyTypeDynamic=instance.b2_dynamicBody;const listener=new box2d.instance.JSContactListener;listener.BeginContact=function(contactPtr){const contact=box2d.instance.wrapPointer(contactPtr,box2d.instance.b2Contact);const fixtureA=contact.GetFixtureA();const fixtureB=contact.GetFixtureB();const objectA=fixtureA.GetBody().object;const objectB=fixtureB.GetBody().object;objectA.beginContact(objectB);objectB.beginContact(objectA)};listener.EndContact=function(contactPtr){const contact=box2d.instance.wrapPointer(contactPtr,box2d.instance.b2Contact);const fixtureA=contact.GetFixtureA();const fixtureB=contact.GetFixtureB();const objectA=fixtureA.GetBody().object;const objectB=fixtureB.GetBody().object;objectA.endContact(objectB);objectB.endContact(objectA)};listener.PreSolve=function(){};listener.PostSolve=function(){};box2d.world.SetContactListener(listener)}step(frames=1){box2d.world.SetGravity(box2d.vec2dTo(gravity));for(let i=frames;i--;)box2d.world.Step(timeDelta,this.velocityIterations,this.positionIterations)}raycastAll(start,end){const raycastCallback=new box2d.instance.JSRayCastCallback;raycastCallback.ReportFixture=function(fixturePointer,point,normal,fraction){const fixture=box2d.instance.wrapPointer(fixturePointer,box2d.instance.b2Fixture);point=box2d.vec2FromPointer(point);normal=box2d.vec2FromPointer(normal);raycastResults.push(new Box2dRaycastResult(fixture,point,normal,fraction));return 1};const raycastResults=[];box2d.world.RayCast(raycastCallback,box2d.vec2dTo(start),box2d.vec2dTo(end));debugRaycast&&debugLine(start,end,raycastResults.length?"#f00":"#00f",.02);return raycastResults}raycast(start,end){const raycastResults=box2d.raycastAll(start,end);if(!raycastResults.length)return undefined;return raycastResults.reduce((a,b)=>a.fraction<b.fraction?a:b)}boxCastAll(pos,size){const queryCallback=new box2d.instance.JSQueryCallback;queryCallback.ReportFixture=function(fixturePointer){const fixture=box2d.instance.wrapPointer(fixturePointer,box2d.instance.b2Fixture);const o=fixture.GetBody().object;if(!queryObjects.includes(o))queryObjects.push(o);return true};const aabb=new box2d.instance.b2AABB;aabb.set_lowerBound(box2d.vec2dTo(pos.subtract(size.scale(.5))));aabb.set_upperBound(box2d.vec2dTo(pos.add(size.scale(.5))));let queryObjects=[];box2d.world.QueryAABB(queryCallback,aabb);debugRaycast&&debugRect(pos,size,queryObjects.length?"#f00":"#00f",.02);return queryObjects}boxCast(pos,size){const queryCallback=new box2d.instance.JSQueryCallback;queryCallback.ReportFixture=function(fixturePointer){const fixture=box2d.instance.wrapPointer(fixturePointer,box2d.instance.b2Fixture);queryObject=fixture.GetBody().object;return false};const aabb=new box2d.instance.b2AABB;aabb.set_lowerBound(box2d.vec2dTo(pos.subtract(size.scale(.5))));aabb.set_upperBound(box2d.vec2dTo(pos.add(size.scale(.5))));let queryObject;box2d.world.QueryAABB(queryCallback,aabb);debugRaycast&&debugRect(pos,size,queryObject?"#f00":"#00f",.02);return queryObject}circleCastAll(pos,diameter){const radius2=(diameter/2)**2;const results=box2d.boxCastAll(pos,vec2(diameter));return results.filter(o=>o.pos.distanceSquared(pos)<radius2)}circleCast(pos,diameter){const radius2=(diameter/2)**2;let results=box2d.boxCastAll(pos,vec2(diameter));let bestResult,bestDistance2;for(const result of results){const distance2=result.pos.distanceSquared(pos);if(distance2<radius2&&(!bestResult||distance2<bestDistance2)){bestResult=result;bestDistance2=distance2}}return bestResult}pointCast(pos,dynamicOnly=true){const queryCallback=new box2d.instance.JSQueryCallback;queryCallback.ReportFixture=function(fixturePointer){const fixture=box2d.instance.wrapPointer(fixturePointer,box2d.instance.b2Fixture);if(dynamicOnly&&fixture.GetBody().GetType()!==box2d.instance.b2_dynamicBody)return true;if(!fixture.TestPoint(box2d.vec2dTo(pos)))return true;queryObject=fixture.GetBody().object;return false};const aabb=new box2d.instance.b2AABB;aabb.set_lowerBound(box2d.vec2dTo(pos));aabb.set_upperBound(box2d.vec2dTo(pos));let queryObject;box2d.world.QueryAABB(queryCallback,aabb);debugRaycast&&debugRect(pos,vec2(),queryObject?"#f00":"#00f",.02);return queryObject}drawFixture(fixture,pos,angle,color=WHITE,lineColor=BLACK,lineWidth=.1,context){const shape=box2d.castObjectType(fixture.GetShape());switch(shape.GetType()){case box2d.instance.b2Shape.e_polygon:{let points=[];for(let i=shape.GetVertexCount();i--;)points.push(box2d.vec2From(shape.GetVertex(i)));drawPoly(points,color,lineWidth,lineColor,pos,angle);break}case box2d.instance.b2Shape.e_circle:{const radius=shape.get_m_radius();drawCircle(pos,radius*2,color,lineWidth,lineColor);break}case box2d.instance.b2Shape.e_edge:{const v1=box2d.vec2From(shape.get_m_vertex1());const v2=box2d.vec2From(shape.get_m_vertex2());drawLine(v1,v2,lineWidth,lineColor,pos,angle);break}}}vec2From(v){ASSERT(v instanceof box2d.instance.b2Vec2);return new Vector2(v.get_x(),v.get_y())}vec2FromPointer(vp){const v=box2d.instance.wrapPointer(vp,box2d.instance.b2Vec2);return box2d.vec2From(v)}vec2dTo(v){ASSERT(isVector2(v));return new box2d.instance.b2Vec2(v.x,v.y)}isNull(o){return!box2d.instance.getPointer(o)}castObjectType(o){switch(o.GetType()){case box2d.instance.b2Shape.e_circle:return box2d.instance.castObject(o,box2d.instance.b2CircleShape);case box2d.instance.b2Shape.e_edge:return box2d.instance.castObject(o,box2d.instance.b2EdgeShape);case box2d.instance.b2Shape.e_polygon:return box2d.instance.castObject(o,box2d.instance.b2PolygonShape);case box2d.instance.b2Shape.e_chain:return box2d.instance.castObject(o,box2d.instance.b2ChainShape);case box2d.instance.e_revoluteJoint:return box2d.instance.castObject(o,box2d.instance.b2RevoluteJoint);case box2d.instance.e_prismaticJoint:return box2d.instance.castObject(o,box2d.instance.b2PrismaticJoint);case box2d.instance.e_distanceJoint:return box2d.instance.castObject(o,box2d.instance.b2DistanceJoint);case box2d.instance.e_pulleyJoint:return box2d.instance.castObject(o,box2d.instance.b2PulleyJoint);case box2d.instance.e_mouseJoint:return box2d.instance.castObject(o,box2d.instance.b2MouseJoint);case box2d.instance.e_gearJoint:return box2d.instance.castObject(o,box2d.instance.b2GearJoint);case box2d.instance.e_wheelJoint:return box2d.instance.castObject(o,box2d.instance.b2WheelJoint);case box2d.instance.e_weldJoint:return box2d.instance.castObject(o,box2d.instance.b2WeldJoint);case box2d.instance.e_frictionJoint:return box2d.instance.castObject(o,box2d.instance.b2FrictionJoint);case box2d.instance.e_ropeJoint:return box2d.instance.castObject(o,box2d.instance.b2RopeJoint);case box2d.instance.e_motorJoint:return box2d.instance.castObject(o,box2d.instance.b2MotorJoint)}ASSERT(false,"Unknown box2d object type")}}async function box2dInit(){new Box2dPlugin(await Box2D());setupDebugDraw();engineAddPlugin(box2dUpdate,box2dRender);return box2d;function box2dUpdate(){if(paused)return;box2d.step();box2d.objects=box2d.objects.filter(o=>!o.destroyed);for(const o of box2d.objects){if(o.body){o.pos=box2d.vec2From(o.body.GetPosition());o.angle=-o.body.GetAngle()}}}function box2dRender(){if(box2dDebug||debugPhysics)box2d.world.DrawDebugData()}function setupDebugDraw(){const debugLineWidth=.1;const debugDraw=new box2d.instance.JSDraw;const box2dColor=c=>new Color(c.get_r(),c.get_g(),c.get_b());const box2dColorPointer=c=>box2dColor(box2d.instance.wrapPointer(c,box2d.instance.b2Color));const getDebugColor=color=>box2dColorPointer(color).scale(1,.8);const getPointsList=(vertices,vertexCount)=>{const points=[];for(let i=vertexCount;i--;)points.push(box2d.vec2FromPointer(vertices+i*8));return points};debugDraw.DrawSegment=function(point1,point2,color){color=getDebugColor(color);point1=box2d.vec2FromPointer(point1);point2=box2d.vec2FromPointer(point2);drawLine(point1,point2,debugLineWidth,color,vec2(),0,false)};debugDraw.DrawPolygon=function(vertices,vertexCount,color){color=getDebugColor(color);const points=getPointsList(vertices,vertexCount);drawPoly(points,CLEAR_WHITE,debugLineWidth,color,vec2(),0,false)};debugDraw.DrawSolidPolygon=function(vertices,vertexCount,color){color=getDebugColor(color);const points=getPointsList(vertices,vertexCount);drawPoly(points,color,0,color,vec2(),0,false)};debugDraw.DrawCircle=function(center,radius,color){color=getDebugColor(color);center=box2d.vec2FromPointer(center);drawCircle(center,radius*2,CLEAR_WHITE,debugLineWidth,color,false)};debugDraw.DrawSolidCircle=function(center,radius,axis,color){color=getDebugColor(color);center=box2d.vec2FromPointer(center);axis=box2d.vec2FromPointer(axis).scale(radius);drawCircle(center,radius*2,color,debugLineWidth,color,false);drawLine(vec2(),axis,debugLineWidth,color,center,0,false)};debugDraw.DrawTransform=function(transform){transform=box2d.instance.wrapPointer(transform,box2d.instance.b2Transform);const pos=vec2(transform.get_p());const angle=-transform.get_q().GetAngle();const p1=vec2(1,0),c1=rgb(.75,0,0,.8);const p2=vec2(0,1),c2=rgb(0,.75,0,.8);drawLine(vec2(),p1,debugLineWidth,c1,pos,angle,false);drawLine(vec2(),p2,debugLineWidth,c2,pos,angle,false)};debugDraw.AppendFlags(box2d.instance.b2Draw.e_shapeBit);debugDraw.AppendFlags(box2d.instance.b2Draw.e_jointBit);box2d.world.SetDebugDraw(debugDraw)}}function drawNineSliceScreen(pos,size,startTile,borderSize=32,extraSpace=2,angle=0){drawNineSlice(pos,size,startTile,WHITE,borderSize,BLACK,extraSpace,angle,false,true)}function drawNineSlice(pos,size,startTile,color,borderSize=1,additiveColor,extraSpace=.05,angle=0,useWebGL=glEnable,screenSpace,context){const centerTile=startTile.offset(startTile.size);const centerSize=size.add(vec2(extraSpace-borderSize*2));const cornerSize=vec2(borderSize);const cornerOffset=size.scale(.5).subtract(cornerSize.scale(.5));const flip=screenSpace?-1:1;const rotateAngle=screenSpace?-angle:angle;drawTile(pos,centerSize,centerTile,color,angle,false,additiveColor,useWebGL,screenSpace,context);for(let i=4;i--;){const horizontal=i%2;const sidePos=cornerOffset.multiply(vec2(horizontal?i===1?1:-1:0,horizontal?0:i?-1:1));const sideSize=vec2(horizontal?borderSize:centerSize.x,horizontal?centerSize.y:borderSize);const sideTile=centerTile.offset(startTile.size.multiply(vec2(i===1?1:i===3?-1:0,i===0?-flip:i===2?flip:0)));drawTile(pos.add(sidePos.rotate(rotateAngle)),sideSize,sideTile,color,angle,false,additiveColor,useWebGL,screenSpace,context)}for(let i=4;i--;){const flipX=i>1;const flipY=i&&i<3;const cornerPos=cornerOffset.multiply(vec2(flipX?-1:1,flipY?-1:1));const cornerTile=centerTile.offset(startTile.size.multiply(vec2(flipX?-1:1,flipY?flip:-flip)));drawTile(pos.add(cornerPos.rotate(rotateAngle)),cornerSize,cornerTile,color,angle,false,additiveColor,useWebGL,screenSpace,context)}}function drawThreeSliceScreen(pos,size,startTile,borderSize=32,extraSpace=2,angle=0){drawThreeSlice(pos,size,startTile,WHITE,borderSize,BLACK,extraSpace,angle,false,true)}function drawThreeSlice(pos,size,startTile,color,borderSize=1,additiveColor,extraSpace=.05,angle=0,useWebGL=glEnable,screenSpace,context){const cornerTile=startTile.frame(0);const sideTile=startTile.frame(1);const centerTile=startTile.frame(2);const centerSize=size.add(vec2(extraSpace-borderSize*2));const cornerSize=vec2(borderSize);const cornerOffset=size.scale(.5).subtract(cornerSize.scale(.5));const flip=screenSpace?-1:1;const rotateAngle=screenSpace?-angle:angle;drawTile(pos,centerSize,centerTile,color,angle,false,additiveColor,useWebGL,screenSpace,context);for(let i=4;i--;){const a=angle+i*PI/2;const horizontal=i%2;const sidePos=cornerOffset.multiply(vec2(horizontal?i===1?1:-1:0,horizontal?0:i?-flip:flip));const sideSize=vec2(horizontal?centerSize.y:centerSize.x,borderSize);drawTile(pos.add(sidePos.rotate(rotateAngle)),sideSize,sideTile,color,a,false,additiveColor,useWebGL,screenSpace,context)}for(let i=4;i--;){const a=angle+i*PI/2;const flipX=!i||i>2;const flipY=i>1;const cornerPos=cornerOffset.multiply(vec2(flipX?-1:1,flipY?-flip:flip));drawTile(pos.add(cornerPos.rotate(rotateAngle)),cornerSize,cornerTile,color,a,false,additiveColor,useWebGL,screenSpace,context)}}export{engineName,engineVersion,frameRate,timeDelta,engineObjects,frame,time,timeReal,paused,getPaused,setPaused,engineInit,engineObjectsUpdate,engineObjectsDestroy,engineObjectsCollect,engineObjectsCallback,engineObjectsRaycast,engineAddPlugin,debug,debugOverlay,debugWatermark,ASSERT,LOG,debugRect,debugPoly,debugCircle,debugPoint,debugLine,debugOverlap,debugText,debugClear,debugScreenshot,debugShowErrors,debugVideoCaptureStart,debugVideoCaptureStop,debugVideoCaptureIsActive,cameraPos,cameraAngle,cameraScale,canvasColorTiles,canvasClearColor,canvasMaxSize,canvasMinAspect,canvasMaxAspect,canvasFixedSize,canvasPixelated,tilesPixelated,fontDefault,showSplashScreen,headlessMode,tileDefaultSize,tileDefaultPadding,tileDefaultBleed,enablePhysicsSolver,objectDefaultMass,objectDefaultDamping,objectDefaultAngleDamping,objectDefaultRestitution,objectDefaultFriction,objectMaxSpeed,gravity,particleEmitRateScale,glEnable,gamepadsEnable,gamepadDirectionEmulateStick,inputWASDEmulateDirection,touchGamepadEnable,touchGamepadCenterButton,touchGamepadAnalog,touchGamepadSize,touchGamepadAlpha,vibrateEnable,soundEnable,soundVolume,soundDefaultRange,soundDefaultTaper,medalDisplayTime,medalDisplaySlideTime,medalDisplaySize,setCameraPos,setCameraAngle,setCameraScale,setCanvasColorTiles,setCanvasClearColor,setCanvasMaxSize,setCanvasMinAspect,setCanvasMaxAspect,setCanvasFixedSize,setCanvasPixelated,setTilesPixelated,setFontDefault,setShowSplashScreen,setHeadlessMode,setGLEnable,setTileDefaultSize,setTileDefaultPadding,setTileDefaultBleed,setEnablePhysicsSolver,setObjectDefaultMass,setObjectDefaultDamping,setObjectDefaultAngleDamping,setObjectDefaultRestitution,setObjectDefaultFriction,setObjectMaxSpeed,setGravity,setParticleEmitRateScale,setTouchInputEnable,setGamepadsEnable,setGamepadDirectionEmulateStick,setInputWASDEmulateDirection,setTouchGamepadEnable,setTouchGamepadCenterButton,setTouchGamepadButtonCount,setTouchGamepadAnalog,setTouchGamepadSize,setTouchGamepadAlpha,setVibrateEnable,setSoundEnable,setSoundVolume,setSoundDefaultRange,setSoundDefaultTaper,setMedalDisplayTime,setMedalDisplaySlideTime,setMedalDisplaySize,setMedalsPreventUnlock,setDebugWatermark,setDebugKey,PI,abs,floor,ceil,round,min,max,sign,hypot,log2,sin,cos,tan,atan2,mod,clamp,percent,distanceWrap,lerpWrap,distanceAngle,lerpAngle,lerp,smoothStep,nearestPowerOfTwo,isOverlapping,isIntersecting,wave,formatTime,fetchJSON,saveText,saveCanvas,saveDataURL,rand,randInt,randBool,randSign,randInCircle,randVec2,randColor,RandomGenerator,Vector2,Color,Timer,vec2,rgb,hsl,isColor,isVector2,isNumber,isString,isArray,WHITE,CLEAR_WHITE,BLACK,CLEAR_BLACK,GRAY,RED,ORANGE,YELLOW,GREEN,CYAN,BLUE,PURPLE,MAGENTA,tile,TileInfo,TextureInfo,mainCanvas,mainContext,drawContext,workCanvas,workContext,workReadCanvas,workReadContext,mainCanvasSize,textureInfos,drawCount,screenToWorld,worldToScreen,screenToWorldDelta,worldToScreenDelta,screenToWorldTransform,drawTile,drawRect,drawRectGradient,drawLineList,drawLine,drawPoly,drawEllipse,drawCircle,drawCanvas2D,drawText,drawTextScreen,setBlendMode,combineCanvases,engineFontImage,FontImage,isFullscreen,toggleFullscreen,setCursor,getCameraSize,glCanvas,glContext,glClearCanvas,glSetTexture,glCompileShader,glCreateProgram,glCreateTexture,glDeleteTexture,glSetTextureData,glFlush,glCopyToContext,glSetAntialias,glDraw,glDrawPointsTransform,glDrawOutlineTransform,glDrawPoints,glDrawColoredPoints,glAntialias,glShader,glPolyShader,glPolyMode,glAdditive,glBatchAdditive,glActiveTexture,glArrayBuffer,glGeometryBuffer,glPositionData,glColorData,glBatchCount,keyIsDown,keyWasPressed,keyWasReleased,keyDirection,inputClear,inputClearKey,mouseIsDown,mouseWasPressed,mouseWasReleased,mousePos,mousePosScreen,mouseDelta,mouseDeltaScreen,mouseWheel,mouseInWindow,isUsingGamepad,inputPreventDefault,gamepadPrimary,setInputPreventDefault,gamepadIsDown,gamepadWasPressed,gamepadWasReleased,gamepadStick,gamepadDpad,gamepadConnected,vibrate,vibrateStop,isTouchDevice,pointerLockRequest,pointerLockExit,pointerLockIsActive,audioContext,audioMasterGain,audioDefaultSampleRate,Sound,SoundWave,SoundInstance,speak,speakStop,getNoteFrequency,playSamples,zzfx,zzfxG,EngineObject,tileCollisionLayers,tileCollisionGetData,tileCollisionTest,tileCollisionRaycast,tileLayersLoad,TileLayerData,CanvasLayer,TileLayer,TileCollisionLayer,ParticleEmitter,Particle,medals,medalsPreventUnlock,medalsInit,Medal};export{newgrounds,NewgroundsPlugin,NewgroundsMedal,postProcess,PostProcessPlugin,ZzFXMusic,uiSystem,uiDebug,uiSetDebug,UISystemPlugin,UIObject,UIText,UITile,UIButton,UICheckbox,UIScrollbar,UIVideo,box2d,box2dDebug,box2dSetDebug,box2dInit,Box2dPlugin,Box2dObject,Box2dStaticObject,Box2dKiematicObject,Box2dRaycastResult,Box2dJoint,Box2dTargetJoint,Box2dDistanceJoint,Box2dPinJoint,Box2dRopeJoint,Box2dRevoluteJoint,Box2dGearJoint,Box2dPrismaticJoint,Box2dWheelJoint,Box2dWeldJoint,Box2dFrictionJoint,Box2dPulleyJoint,Box2dMotorJoint,drawNineSlice,drawNineSliceScreen,drawThreeSlice,drawThreeSliceScreen};