littlejsengine
Version:
LittleJS - Tiny and Fast HTML5 Game Engine
4 lines (3 loc) • 138 kB
JavaScript
// LittleJS Engine - MIT License - Copyright 2021 Frank Force
// https://github.com/KilledByAPixel/LittleJS
const engineName="LittleJS",engineVersion="1.17.11",frameRate=60,timeDelta=1/frameRate;let engineObjects=[],engineObjectsCollide=[],frame=0,time=0,timeReal=0,paused=!1;function getPaused(){return paused}function setPaused(a=!0){paused=a}let frameTimeLastMS=0,frameTimeBufferMS=0,averageFPS=0,showEngineVersion=!0;const pluginList=[];class EnginePlugin{constructor(a,b,c,d){this.update=a;this.render=b;this.glContextLost=c;this.glContextRestored=d}}function engineAddPlugin(a,b,c,d){ASSERT(!pluginList.find(f=>f.update===a&&f.render===b&&f.glContextLost===c&&f.glContextRestored===d));const e=new EnginePlugin(a,b,c,d);pluginList.push(e)}async function engineInit(a,b,c,d,e,f=[],g=document.body){function h(m=0){function n(){if(!headlessMode){r||k();mainCanvasSize=vec2(mainCanvas.width,mainCanvas.height);mainContext.imageSmoothingEnabled=!tilesPixelated;glPreRender();d();engineObjects.sort((t,u)=>t.renderOrder-u.renderOrder);for(const t of engineObjects)t.destroyed||t.render();e();pluginList.forEach(t=>t.render?.());inputRender();debugRender();glFlush();debugRenderPost();drawCount=0}}var p=m-frameTimeLastMS;frameTimeLastMS=m;if(debug||debugWatermark)averageFPS=lerp(averageFPS,1e3/(p||1),.05);m=debug&&keyIsDown("Equal");const q=debug&&keyIsDown("Minus");debug&&(p*=m?10:q?.1:1);timeReal+=p/1e3;frameTimeBufferMS+=paused?0:p;m||(frameTimeBufferMS=min(frameTimeBufferMS,50));let r=!1;if(paused){r=!0;k();inputUpdate();pluginList.forEach(t=>t.update?.());for(const t of engineObjects)t.parent||t.updateTransforms();debugUpdate();c();inputUpdatePost();debugVideoCaptureIsActive()&&n()}else{p=0;0>frameTimeBufferMS&&-9<frameTimeBufferMS&&(p=frameTimeBufferMS,frameTimeBufferMS=0);for(;0<=frameTimeBufferMS;frameTimeBufferMS-=1e3/frameRate)time=frame++/frameRate,r=!0,k(),inputUpdate(),b(),pluginList.forEach(t=>t.update?.()),engineObjectsUpdate(),debugUpdate(),c(),inputUpdatePost(),debugVideoCaptureIsActive()&&n();frameTimeBufferMS+=p}debugVideoCaptureIsActive()||n();requestAnimationFrame(h)}function k(){if(!headlessMode){if(canvasFixedSize.x){mainCanvasSize=canvasFixedSize.copy();var m=innerWidth/innerHeight;const p=canvasFixedSize.x/canvasFixedSize.y;var n=m<p?"100%":"";m=m<p?"":"100%";mainCanvas.style.width=n;mainCanvas.style.height=m;glCanvas&&(glCanvas.style.width=n,glCanvas.style.height=m)}else mainCanvasSize.x=min(innerWidth,canvasMaxSize.x),mainCanvasSize.y=min(innerHeight,canvasMaxSize.y),n=innerWidth/innerHeight,ASSERT(canvasMinAspect<=canvasMaxAspect),canvasMaxAspect&&n>canvasMaxAspect?mainCanvasSize.x=min(mainCanvasSize.y*canvasMaxAspect|0,canvasMaxSize.x):n<canvasMinAspect&&(mainCanvasSize.y=min(mainCanvasSize.x/canvasMinAspect|0,canvasMaxSize.y));mainCanvas.width=mainCanvasSize.x;mainCanvas.height=mainCanvasSize.y;0<canvasClearColor.a&&!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 l(){await a();h()}showEngineVersion&&console.log(`${engineName} Engine v${engineVersion}`);ASSERT(!mainContext,"engine already initialized");ASSERT(isArray(f),"pass in images as array");a||=()=>{};b||=()=>{};c||=()=>{};d||=()=>{};e||=()=>{};if(headlessMode)return l();glInit(g);g.style.cssText="margin:0;overflow:hidden;background:#000;user-select:none;-webkit-user-select:none;touch-action:none;-webkit-touch-callout:none";mainCanvas=g.appendChild(document.createElement("canvas"));drawContext=mainContext=mainCanvas.getContext("2d");inputInit();audioInit();debugInit();mainCanvas.style.cssText="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)";glCanvas&&(glCanvas.style.cssText="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)");setCanvasPixelated(canvasPixelated);k();glPreRender();workCanvas=new OffscreenCanvas(64,64);workContext=workCanvas.getContext("2d");workReadCanvas=new OffscreenCanvas(64,64);workReadContext=workReadCanvas.getContext("2d",{willReadFrequently:!0});g=f.map((m,n)=>loadTexture(n,m));f.length||g.push(loadTexture(0));g.push(fontImageInit());showSplashScreen&&g.push(new Promise(m=>{function n(){inputClear();drawEngineLogo(p+=.01);1<p?m():setTimeout(n,16)}let p=0;n()}));await Promise.all(g);return l()}function engineObjectsUpdate(){function a(b){if(!b.destroyed){b.update();for(const c of b.children)a(c)}}engineObjectsCollide=engineObjects.filter(b=>b.collideSolidObjects);for(const b of engineObjects)if(!b.parent&&!b.destroyed){b.update();b.updatePhysics();for(const c of b.children)a(c);b.updateTransforms()}engineObjects=engineObjects.filter(b=>!b.destroyed)}function engineObjectsDestroy(a=!0){for(const b of engineObjects)b.parent||b.destroy(a);engineObjects=engineObjects.filter(b=>!b.destroyed)}function engineObjectsCollect(a,b,c=engineObjects){const d=[];if(a)if(b instanceof Vector2)for(const e of c)e.isOverlapping(a,b)&&d.push(e);else{b*=b;for(const e of c)a.distanceSquared(e.pos)<b&&d.push(e)}else for(const e of c)d.push(e);return d}function engineObjectsCallback(a,b,c,d=engineObjects){engineObjectsCollect(a,b,d).forEach(e=>c(e))}function engineObjectsRaycast(a,b,c=engineObjects){const d=[];for(const e of c)e.collideRaycast&&isIntersecting(a,b,e.pos,e.size)&&(debugRaycast&&debugRect(e.pos,e.size,"#f00"),d.push(e));debugRaycast&&debugLine(a,b,d.length?"#f00":"#00f",.02);return d}function drawEngineLogo(a){const b=mainContext;var c=mainCanvas.width=innerWidth,d=mainCanvas.height=innerHeight,e=percent(a,1,.8),f=percent(a,0,.5),g=b.createRadialGradient(c/2,d/2,0,c/2,d/2,.6*hypot(c,d));g.addColorStop(0,hsl(0,0,lerp(0,e/2,f),e).toString());g.addColorStop(1,hsl(0,0,0,e).toString());b.save();b.fillStyle=g;b.fillRect(0,0,c,d);const h=(l,m,n,p,q,r=1)=>{0<=q?(l=b.fillStyle=b.createLinearGradient(l,m,n,p),l.addColorStop(0,`hsl(${360*[.95,.56,.13][q%3]} 99%${75}%`),l.addColorStop(1,`hsl(${360*[.95,.56,.13][q%3]} 99%${50}%`)):b.fillStyle="#000";-1<=q?(b.fill(),r&&b.stroke()):b.stroke()};f=(l,m,n,p=0,q=2*PI,r,t)=>{b.beginPath();b.arc(l,m,n,k*p,k*q);h(l,m-n,l,m+n,r,t)};g=(l,m,n,p,q)=>{b.beginPath();b.rect(l,m,n,p*k);h(l,m+p,l+n,m,q)};e=(l,m,n,p)=>{b.beginPath();for(const q of l)b.lineTo(q.x,q.y);b.closePath();h(0,n,0,n+p,m)};a=wave(1,1,a);const k=percent(a,.1,.5);a=min(6,min(c,d)/99);b.translate(c/2,d/2);b.scale(a,a);b.translate(-40,-35);1>k&&b.setLineDash([99*k,99]);b.lineJoin=b.lineCap="round";b.lineWidth=.1+1.9*k;b.font="900 15.5px arial";b.lineWidth=.1+3.9*k;b.textAlign="center";b.textBaseline="top";g(11,55,59,8*k,-1);b.beginPath();c=0;for(d=0;8>d;++d)c+=b.measureText("LittleJS"[d]).width;for(d=2;d--;)for(let l=0,m=40-c/2;8>l;++l){a=b.measureText("LittleJS"[l]).width;const n=m+a/2;h(n,54,n+2,67,5<l?1:0);b[d?"strokeText":"fillText"]("LittleJS"[l],n,54.5,17*k);m+=a}b.lineWidth=.1+1.9*k;g(3,54,73,0);g(7,15,26,-7,0);g(25,15,8,25,-1);g(10,40,15,-25,1);g(14,21,7,9,2);g(38,20,6,-6,2);g(49,20,10,-6,0);c=[vec2(44,8),vec2(64,8),vec2(59,8+6*k),vec2(49,8+6*k)];e(c,2,8,6*k);g(44,8,20,-7,0);for(c=5;c--;)f(59-6*c*k,30,10,0,2*PI,1,0);f(59,30,4,0,7,2);g(35,20,24,0);f(59,30,10);f(47,30,10,PI/2,3*PI/2);f(35,30,10,PI/2,3*PI/2);g(7,40,13,7,-1);g(17,40,43,14,-1);for(g=3;g--;)for(c=2;c--;)f(17+15*g,47,c?7:1,0,2*PI,2);for(f=2;f--;)g=53+6*k*f,g=[vec2(g+7,54),vec2(g,40),vec2(g+6*k,40),vec2(g+7+6*k,54)],e(g,0,40,14);b.restore()}let debugWatermark=0,debugKey="";const debug=0,debugOverlay=0,debugPhysics=0,debugParticles=0,debugRaycast=0,debugGamepads=0,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!1}function debugVideoCaptureStart(){}function debugVideoCaptureStop(){}function debugVideoCaptureUpdate(){}function debugProtectConstant(a){return a}const PI=Math.PI,abs=Math.abs,floor=Math.floor,ceil=Math.ceil,round=Math.round,min=Math.min,max=Math.max,sign=Math.sign,hypot=Math.hypot,log2=Math.log2,sin=Math.sin,cos=Math.cos,tan=Math.tan,atan2=Math.atan2;function mod(a,b=1){return(a%b+b)%b}function clamp(a,b=0,c=1){return a<b?b:a>c?c:a}function percent(a,b,c){return(c-=b)?clamp((a-b)/c):0}function lerp(a,b,c){return a+clamp(c)*(b-a)}function percentLerp(a,b,c,d,e){return lerp(d,e,percent(a,b,c))}function distanceWrap(a,b,c=1){a=(a-b)%c;return 2*a%c-a}function lerpWrap(a,b,c,d=1){return a+clamp(c)*distanceWrap(b,a,d)}function distanceAngle(a,b){return distanceWrap(a,b,2*PI)}function lerpAngle(a,b,c){return lerpWrap(a,b,c,2*PI)}function smoothStep(a){return a*a*(3-2*a)}function isPowerOfTwo(a){return!(a&a-1)}function nearestPowerOfTwo(a){return 2**ceil(log2(a))}function isOverlapping(a,b,c,d=vec2()){const e=2*(a.x-c.x);a=2*(a.y-c.y);c=b.x+d.x;b=b.y+d.y;return e>=-c&&e<c&&a>=-b&&a<b}function isIntersecting(a,b,c,d){c=c.subtract(d.scale(.5));d=c.add(d);b=b.subtract(a);c=a.subtract(c);d=a.subtract(d);a=[-b.x,b.x,-b.y,b.y];b=[c.x,-d.x,c.y,-d.y];c=0;d=1;for(let e=4;e--;)if(a[e]){const f=b[e]/a[e];if(0>a[e]){if(f>d)return!1;c=max(f,c)}else{if(f<c)return!1;d=min(f,d)}}else if(0>b[e])return!1;return!0}function wave(a=1,b=1,c=time,d=0){return b/2*(1-cos(d+c*a*2*PI))}function isNumber(a){return"number"===typeof a&&!isNaN(a)}function isString(a){return null!=a&&"string"===typeof a?.toString()}function isArray(a){return Array.isArray(a)}function lineTest(a,b,c,d){ASSERT(isVector2(a),"posStart must be a vec2");ASSERT(isVector2(b),"posEnd must be a vec2");ASSERT("function"===typeof c,"testFunction must be a function");ASSERT(!d||isVector2(d),"normal must be a vec2");var e=b.x-a.x,f=b.y-a.y;if(b=hypot(e,f)){var g=a.floor();e/=b;var h=f/b;f=sign(e);var k=sign(h),l=e?abs(1/e):Infinity,m=h?abs(1/h):Infinity,n=0<f?g.x+1:g.x,p=0<k?g.y+1:g.y,q=0;n=e?(n-a.x)/e:Infinity;var r=h?(p-a.y)/h:Infinity;for(p=l<m;q<b;){if(c(g))return a=vec2(a.x+e*q,a.y+h*q),p?0>f&&(a.x-=1e-9):0>k&&(a.y-=1e-9),d&&(p?d.set(-f,0):d.set(0,-k)),a;(p=n<r)?(g.x+=f,q=n,n+=l):(g.y+=k,q=r,r+=m)}}}function rand(a=1,b=0){return b+Math.random()*(a-b)}function randInt(a,b=0){return floor(rand(a,b))}function randBool(a=.5){return rand()<a}function randSign(){return 2*randInt(2)-1}function randVec2(a=1){return(new Vector2).setAngle(rand(2*PI),a)}function randInCircle(a=1,b=0){return 0<a?randVec2(a*rand(b/a,1)**.5):new Vector2}function randColor(a=new Color,b=new Color(0,0,0,1),c=!1){return c?a.lerp(b,rand()):new Color(rand(a.r,b.r),rand(a.g,b.g),rand(a.b,b.b),rand(a.a,b.a))}class RandomGenerator{constructor(a=123456789){this.seed=a}float(a=1,b=0){this.seed^=this.seed<<13;this.seed^=this.seed>>>17;this.seed^=this.seed<<5;return b+(this.seed>>>0)/2**32*(a-b)}int(a,b=0){return floor(this.float(a,b))}bool(a=.5){return this.float()<a}sign(){return.5<this.float()?1:-1}floatSign(a=1,b=0){return this.float(a,b)*this.sign()}angle(){return this.float(-PI,PI)}vec2(a=1,b=0){return vec2(this.float(a,b),this.float(a,b))}randColor(a=new Color,b=new Color(0,0,0,1),c=!1){return c?a.lerp(b,this.float()):new Color(this.float(a.r,b.r),this.float(a.g,b.g),this.float(a.b,b.b),this.float(a.a,b.a))}mutateColor(a,b=.05,c=0){ASSERT_NUMBER_VALID(b);ASSERT_NUMBER_VALID(c);return new Color(a.r+this.float(b,-b),a.g+this.float(b,-b),a.b+this.float(b,-b),a.a+this.float(c,-c)).clamp()}}function vec2(a=0,b){return new Vector2(a,b??a)}function isVector2(a){return a instanceof Vector2&&a.isValid()}function ASSERT_VECTOR2_VALID(a){ASSERT(isVector2(a),"Vector2 is invalid.",a)}function ASSERT_NUMBER_VALID(a){ASSERT(isNumber(a),"Number is invalid.",a)}function ASSERT_VECTOR2_NORMAL(a){ASSERT_VECTOR2_VALID(a);ASSERT(.01>abs(a.lengthSquared()-1),"Vector2 is not normal.",a)}class Vector2{constructor(a=0,b=0){this.x=a;this.y=b;ASSERT(this.isValid(),"Constructed Vector2 is invalid.",this)}set(a=0,b=0){this.x=a;this.y=b;ASSERT_VECTOR2_VALID(this);return this}setFrom(a){return this.set(a.x,a.y)}copy(){return new Vector2(this.x,this.y)}add(a){return new Vector2(this.x+a.x,this.y+a.y)}subtract(a){return new Vector2(this.x-a.x,this.y-a.y)}multiply(a){return new Vector2(this.x*a.x,this.y*a.y)}divide(a){return new Vector2(this.x/a.x,this.y/a.y)}scale(a){return new Vector2(this.x*a,this.y*a)}length(){return this.lengthSquared()**.5}lengthSquared(){return this.x**2+this.y**2}distance(a){return this.distanceSquared(a)**.5}distanceSquared(a){return(this.x-a.x)**2+(this.y-a.y)**2}normalize(a=1){const b=this.length();return b?this.scale(a/b):new Vector2(0,a)}clampLength(a=1){const b=this.length();return b>a?this.scale(a/b):this.copy()}dot(a){return this.x*a.x+this.y*a.y}cross(a){return this.x*a.y-this.y*a.x}reflect(a,b=1){return this.subtract(a.scale((1+b)*this.dot(a)))}angle(){return atan2(this.x,this.y)}setAngle(a=0,b=1){ASSERT_NUMBER_VALID(a);ASSERT_NUMBER_VALID(b);this.x=b*sin(a);this.y=b*cos(a);return this}rotate(a){ASSERT_NUMBER_VALID(a);const b=cos(-a);a=sin(-a);return new Vector2(this.x*b-this.y*a,this.x*a+this.y*b)}setDirection(a,b=1){ASSERT_NUMBER_VALID(a);ASSERT_NUMBER_VALID(b);a=mod(a,4);ASSERT(0===a||1===a||2===a||3===a,"Vector2.setDirection() direction must be an integer between 0 and 3.");this.x=a%2?a-1?-b:b:0;this.y=a%2?0:a?-b:b;return this}direction(){return abs(this.x)>abs(this.y)?0>this.x?3:1:0>this.y?2:0}abs(){return new Vector2(abs(this.x),abs(this.y))}floor(){return new Vector2(floor(this.x),floor(this.y))}mod(a=1){return new Vector2(mod(this.x,a),mod(this.y,a))}area(){return abs(this.x*this.y)}lerp(a,b){ASSERT_VECTOR2_VALID(a);ASSERT_NUMBER_VALID(b);b=clamp(b);return new Vector2(a.x*b+this.x*(1-b),a.y*b+this.y*(1-b))}arrayCheck(a){return 0<=this.x&&0<=this.y&&this.x<a.x&&this.y<a.y}toString(a=3){ASSERT_NUMBER_VALID(a);return this.isValid()?`(${(0>this.x?"":" ")+this.x.toFixed(a)},${(0>this.y?"":" ")+this.y.toFixed(a)} )`:`(${this.x}, ${this.y})`}isValid(){return isNumber(this.x)&&isNumber(this.y)}}function rgb(a,b,c,d){return new Color(a,b,c,d)}function hsl(a,b,c,d){return(new Color).setHSLA(a,b,c,d)}function isColor(a){return a instanceof Color&&a.isValid()}function ASSERT_COLOR_VALID(a){ASSERT(isColor(a),"Color is invalid.",a)}class Color{constructor(a=1,b=1,c=1,d=1){this.r=a;this.g=b;this.b=c;this.a=d;ASSERT(this.isValid(),"Constructed Color is invalid.",this)}set(a=1,b=1,c=1,d=1){this.r=a;this.g=b;this.b=c;this.a=d;ASSERT_COLOR_VALID(this);return this}setFrom(a){return this.set(a.r,a.g,a.b,a.a)}copy(){return new Color(this.r,this.g,this.b,this.a)}add(a){return new Color(this.r+a.r,this.g+a.g,this.b+a.b,this.a+a.a)}subtract(a){return new Color(this.r-a.r,this.g-a.g,this.b-a.b,this.a-a.a)}multiply(a){return new Color(this.r*a.r,this.g*a.g,this.b*a.b,this.a*a.a)}divide(a){return new Color(this.r/a.r,this.g/a.g,this.b/a.b,this.a/a.a)}scale(a,b=a){return new Color(this.r*a,this.g*a,this.b*a,this.a*b)}clamp(){return new Color(clamp(this.r),clamp(this.g),clamp(this.b),clamp(this.a))}lerp(a,b){ASSERT_COLOR_VALID(a);ASSERT_NUMBER_VALID(b);b=clamp(b);return new Color(a.r*b+this.r*(1-b),a.g*b+this.g*(1-b),a.b*b+this.b*(1-b),a.a*b+this.a*(1-b))}setHSLA(a=0,b=0,c=1,d=1){a=mod(a,1);b=clamp(b);c=clamp(c);b=.5>c?c*(1+b):c+b-c*b;c=2*c-b;const e=(f,g,h)=>1>6*(h=mod(h,1))?f+6*(g-f)*h:1>2*h?g:2>3*h?f+(g-f)*(4-6*h):f;this.r=e(c,b,a+1/3);this.g=e(c,b,a);this.b=e(c,b,a-1/3);this.a=d;ASSERT_COLOR_VALID(this);return this}HSLA(){const a=clamp(this.r),b=clamp(this.g),c=clamp(this.b),d=clamp(this.a),e=max(a,b,c),f=min(a,b,c),g=(e+f)/2;let h=0,k=0;if(e!==f){let l=e-f;k=.5<g?l/(2-e-f):l/(e+f);a===e?h=(b-c)/l+(b<c?6:0):b===e?h=(c-a)/l+2:c===e&&(h=(a-b)/l+4)}return[h/6,k,g,d]}mutate(a=.05,b=0){ASSERT_NUMBER_VALID(a);ASSERT_NUMBER_VALID(b);return new Color(this.r+rand(a,-a),this.g+rand(a,-a),this.b+rand(a,-a),this.a+rand(b,-b)).clamp()}toString(a=!0){if(debug&&!this.isValid())return"#000";const b=c=>(16>(c=255*clamp(c)|0)?"0":"")+c.toString(16);return"#"+b(this.r)+b(this.g)+b(this.b)+(a?b(this.a):"")}setHex(a){ASSERT(isString(a),"Color hex code must be a string");ASSERT("#"===a[0],"Color hex code must start with #");ASSERT([4,5,7,9].includes(a.length),"Invalid hex");6>a.length?(this.r=clamp(parseInt(a[1],16)/15),this.g=clamp(parseInt(a[2],16)/15),this.b=clamp(parseInt(a[3],16)/15),this.a=5===a.length?clamp(parseInt(a[4],16)/15):1):(this.r=clamp(parseInt(a.slice(1,3),16)/255),this.g=clamp(parseInt(a.slice(3,5),16)/255),this.b=clamp(parseInt(a.slice(5,7),16)/255),this.a=9===a.length?clamp(parseInt(a.slice(7,9),16)/255):1);ASSERT_COLOR_VALID(this);return this}rgbaInt(){const a=255*clamp(this.r)|0,b=255*clamp(this.g)<<8,c=255*clamp(this.b)<<16,d=255*clamp(this.a)<<24;return a+b+c+d}isValid(){return isNumber(this.r)&&isNumber(this.g)&&isNumber(this.b)&&isNumber(this.a)}}const WHITE=debugProtectConstant(rgb()),CLEAR_WHITE=debugProtectConstant(rgb(1,1,1,0)),BLACK=debugProtectConstant(rgb(0,0,0)),CLEAR_BLACK=debugProtectConstant(rgb(0,0,0,0)),GRAY=debugProtectConstant(rgb(.5,.5,.5)),RED=debugProtectConstant(rgb(1,0,0)),ORANGE=debugProtectConstant(rgb(1,.5,0)),YELLOW=debugProtectConstant(rgb(1,1,0)),GREEN=debugProtectConstant(rgb(0,1,0)),CYAN=debugProtectConstant(rgb(0,1,1)),BLUE=debugProtectConstant(rgb(0,0,1)),PURPLE=debugProtectConstant(rgb(.5,0,1)),MAGENTA=debugProtectConstant(rgb(1,0,1));class Timer{constructor(a,b=!1){ASSERT(void 0===a||isNumber(a),"Constructed Timer is invalid.",a);this.useRealTime=b;b=this.getGlobalTime();this.time=void 0===a?void 0:b+a;this.setTime=a}set(a=0){ASSERT(isNumber(a),"Timer is invalid.",a);this.time=this.getGlobalTime()+a;this.setTime=a}setUseRealTime(a=!0){ASSERT(!this.isSet(),"Cannot change global time setting while timer is set.");this.useRealTime=a}unset(){this.time=void 0}isSet(){return void 0!==this.time}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 "+(0>this.get()?"before":"after"):"unset"}valueOf(){return this.get()}}function formatTime(a){const b=0>a?"-":"";a=abs(a)|0;return b+(a/60|0)+":"+(10>a%60?"0":"")+a%60}async function fetchJSON(a){const b=await fetch(a);if(!b.ok)throw Error(`Failed to fetch JSON from ${a}: ${b.status} ${b.statusText}`);return b.json()}function saveText(a,b="text",c="text/plain"){saveDataURL(URL.createObjectURL(new Blob([a],{type:c})),b)}function saveCanvas(a,b="screenshot",c="image/png"){if(a instanceof OffscreenCanvas){const d=document.createElement("canvas");d.width=a.width;d.height=a.height;d.getContext("2d").drawImage(a,0,0);saveDataURL(d.toDataURL(c),b)}else saveDataURL(a.toDataURL(c),b)}function saveDataURL(a,b="download",c){ASSERT(isString(a),"saveDataURL requires url string");ASSERT(isString(b),"saveDataURL requires filename string");const d=document.createElement("a");d.download=b;d.href=a;d.click();void 0!==c&&setTimeout(()=>URL.revokeObjectURL(a),c)}function shareURL(a,b,c){ASSERT(isString(a),"shareURL requires title string");ASSERT(isString(b),"shareURL requires url string");navigator.share?.({title:a,url:b}).then(()=>c?.())}function readSaveData(a,b){ASSERT(isString(a),"loadData requires saveName string");a=(a=localStorage[a])?JSON.parse(a):{};return{...b,...a}}function writeSaveData(a,b){ASSERT(isString(a),"saveData requires saveName string");localStorage[a]=JSON.stringify(b)}let cameraPos=vec2(),cameraAngle=0,cameraScale=32,canvasColorTiles=!0,canvasClearColor=CLEAR_BLACK,canvasMaxSize=vec2(1920,1080),canvasMinAspect=0,canvasMaxAspect=0,canvasFixedSize=vec2(),canvasPixelated=!1,tilesPixelated=!0,fontDefault="arial",showSplashScreen=!1,headlessMode=!1,glEnable=!0,glCircleSides=32,tileDefaultSize=vec2(16),tileDefaultPadding=0,tileDefaultBleed=0,enablePhysicsSolver=!0,objectDefaultMass=1,objectDefaultDamping=1,objectDefaultAngleDamping=1,objectDefaultRestitution=0,objectDefaultFriction=.8,objectMaxSpeed=1,gravity=vec2(),particleEmitRateScale=1,gamepadsEnable=!0,gamepadDirectionEmulateStick=!0,inputWASDEmulateDirection=!0,touchInputEnable=!0,touchGamepadEnable=!1,touchGamepadCenterButton=!0,touchGamepadButtonCount=4,touchGamepadAnalog=!0,touchGamepadSize=99,touchGamepadAlpha=.3,vibrateEnable=!0,soundEnable=!0,soundVolume=.3,soundDefaultRange=40,soundDefaultTaper=.7,medalDisplayTime=5,medalDisplaySlideTime=.5,medalDisplaySize=vec2(640,80),medalsPreventUnlock=!1;function setCameraPos(a){cameraPos=a.copy()}function setCameraAngle(a){cameraAngle=a}function setCameraScale(a){cameraScale=a}function setCanvasColorTiles(a){canvasColorTiles=a}function setCanvasClearColor(a){canvasClearColor=a.copy()}function setCanvasMaxSize(a){canvasMaxSize=a.copy()}function setCanvasMinAspect(a){canvasMinAspect=a}function setCanvasMaxAspect(a){canvasMaxAspect=a}function setCanvasFixedSize(a){canvasFixedSize=a.copy()}function setCanvasPixelated(a){canvasPixelated=a;mainCanvas&&(mainCanvas.style.imageRendering=a?"pixelated":"");glCanvas&&(glCanvas.style.imageRendering=a?"pixelated":"")}function setTilesPixelated(a){tilesPixelated=a}function setFontDefault(a){fontDefault=a}function setShowSplashScreen(a){showSplashScreen=a}function setHeadlessMode(a){headlessMode=a}function setGLEnable(a){a&&!glCanBeEnabled?console.warn("Can not enable WebGL if it was disabled on start."):(glEnable=a,glCanvas&&(glCanvas.style.display=a?"":"none"))}function setGLCircleSides(a){glCircleSides=a}function setTileDefaultSize(a){tileDefaultSize=a.copy()}function setTileDefaultPadding(a){tileDefaultPadding=a}function setTileDefaultBleed(a){tileDefaultBleed=a}function setEnablePhysicsSolver(a){enablePhysicsSolver=a}function setObjectDefaultMass(a){objectDefaultMass=a}function setObjectDefaultDamping(a){objectDefaultDamping=a}function setObjectDefaultAngleDamping(a){objectDefaultAngleDamping=a}function setObjectDefaultRestitution(a){objectDefaultRestitution=a}function setObjectDefaultFriction(a){objectDefaultFriction=a}function setObjectMaxSpeed(a){objectMaxSpeed=a}function setGravity(a){gravity=a.copy()}function setParticleEmitRateScale(a){particleEmitRateScale=a}function setGamepadsEnable(a){gamepadsEnable=a}function setGamepadDirectionEmulateStick(a){gamepadDirectionEmulateStick=a}function setInputWASDEmulateDirection(a){inputWASDEmulateDirection=a}function setTouchInputEnable(a){touchInputEnable=a}function setTouchGamepadEnable(a){touchGamepadEnable=a}function setTouchGamepadCenterButton(a){touchGamepadCenterButton=a}function setTouchGamepadButtonCount(a){touchGamepadButtonCount=a}function setTouchGamepadAnalog(a){touchGamepadAnalog=a}function setTouchGamepadSize(a){touchGamepadSize=a}function setTouchGamepadAlpha(a){touchGamepadAlpha=a}function setVibrateEnable(a){vibrateEnable=a}function setSoundEnable(a){soundEnable=a}function setSoundVolume(a){soundVolume=a;soundEnable&&!headlessMode&&audioMasterGain&&(audioMasterGain.gain.value=a)}function setSoundDefaultRange(a){soundDefaultRange=a}function setSoundDefaultTaper(a){soundDefaultTaper=a}function setMedalDisplayTime(a){medalDisplayTime=a}function setMedalDisplaySlideTime(a){medalDisplaySlideTime=a}function setMedalDisplaySize(a){medalDisplaySize=a.copy()}function setMedalsPreventUnlock(a){medalsPreventUnlock=a}function setDebugWatermark(a){debugWatermark=a}function setDebugKey(a){debugKey=a}class EngineObject{constructor(a=vec2(),b=vec2(1),c,d=0,e=WHITE,f=0){ASSERT(isVector2(a),"object pos must be a vec2");ASSERT(isVector2(b),"object size must be a vec2");ASSERT(!c||c instanceof TileInfo,"object tileInfo should be a TileInfo or undefined");ASSERT("number"===typeof d&&isFinite(d),"object angle should be a number");ASSERT(isColor(e),"object color should be a valid rgba color");ASSERT("number"===typeof f,"object renderOrder should be a number");this.pos=a.copy();this.size=b.copy();this.drawSize=void 0;this.tileInfo=c;this.angle=d;this.color=e.copy();this.additiveColor=void 0;this.destroyed=this.mirror=!1;this.mass=objectDefaultMass;this.damping=objectDefaultDamping;this.angleDamping=objectDefaultAngleDamping;this.restitution=objectDefaultRestitution;this.friction=objectDefaultFriction;this.gravityScale=1;this.renderOrder=f;this.velocity=vec2();this.angleVelocity=0;this.spawnTime=time;this.children=[];this.clampSpeed=!0;this.parent=this.groundObject=void 0;this.localPos=vec2();this.localAngle=0;this.collideRaycast=this.isSolid=this.collideSolidObjects=this.collideTiles=!1;engineObjects.push(this)}updateTransforms(){const a=this.parent;if(a){const b=a.getMirrorSign();this.pos=this.localPos.multiply(vec2(b,1)).rotate(a.angle).add(a.pos);this.angle=b*this.localAngle+a.angle}for(const b of this.children)b.updateTransforms()}updatePhysics(){ASSERT(!this.parent);this.clampSpeed&&(this.velocity.x=clamp(this.velocity.x,-objectMaxSpeed,objectMaxSpeed),this.velocity.y=clamp(this.velocity.y,-objectMaxSpeed,objectMaxSpeed));const a=this.pos.copy();this.velocity.x*=this.damping;this.velocity.y*=this.damping;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(0<=this.angleDamping&&1>=this.angleDamping);ASSERT(0<=this.damping&&1>=this.damping);if(enablePhysicsSolver&&this.mass){var b=0>this.velocity.y&&0>gravity.y||0<this.velocity.y&&0<gravity.y;if(this.groundObject){var c=max(this.friction,this.groundObject.friction),d=this.groundObject.velocity.x;this.velocity.x=d+(this.velocity.x-d)*c;this.groundObject=void 0}if(this.collideSolidObjects)for(var e of engineObjectsCollide)if(!e.destroyed&&!e.parent&&e!==this&&(this.isSolid||e.isSolid)&&this.isOverlappingObject(e)&&(c=this.collideWithObject(e),d=e.collideWithObject(this),c&&d))if(isOverlapping(a,this.size,e.pos,e.size))c=a.subtract(e.pos),d=c.length(),c=.01>d?randVec2(.001):c.scale(.001/d),this.velocity=this.velocity.add(c),e.mass&&(e.velocity=e.velocity.subtract(c)),debugPhysics&&debugOverlap(this.pos,this.size,e.pos,e.size,"#f00");else{d=this.size.add(e.size);var f=2*(a.y-e.pos.y)>d.y+gravity.y,g=2*abs(a.y-e.pos.y)<d.y,h=2*abs(a.x-e.pos.x)<d.x;c=max(this.restitution,e.restitution);if(f||h||!g)if(this.pos.y=e.pos.y+(d.y/2+.001)*sign(a.y-e.pos.y),e.groundObject&&b||!e.mass)b&&(this.groundObject=e),this.velocity.y*=-c;else if(e.mass){h=(this.mass*this.velocity.y+e.mass*e.velocity.y)/(this.mass+e.mass);const k=e.velocity.y*(e.mass-this.mass)/(this.mass+e.mass)+2*this.velocity.y*this.mass/(this.mass+e.mass);this.velocity.y=lerp(h,this.velocity.y*(this.mass-e.mass)/(this.mass+e.mass)+2*e.velocity.y*e.mass/(this.mass+e.mass),c);e.velocity.y=lerp(h,k,c)}!f&&g&&(this.pos.x=e.pos.x+(d.x/2+.001)*sign(a.x-e.pos.x),e.mass?(d=(this.mass*this.velocity.x+e.mass*e.velocity.x)/(this.mass+e.mass),f=e.velocity.x*(e.mass-this.mass)/(this.mass+e.mass)+2*this.velocity.x*this.mass/(this.mass+e.mass),this.velocity.x=lerp(d,this.velocity.x*(this.mass-e.mass)/(this.mass+e.mass)+2*e.velocity.x*e.mass/(this.mass+e.mass),c),e.velocity.x=lerp(d,f,c)):this.velocity.x*=-c);debugPhysics&&debugOverlap(this.pos,this.size,e.pos,e.size,"#f0f")}if(this.collideTiles&&(e=tileCollisionTest(this.pos,this.size,this))&&!tileCollisionTest(a,this.size,this)){d=tileCollisionTest(vec2(this.pos.x,a.y),this.size,this);f=tileCollisionTest(vec2(a.x,this.pos.y),this.size,this);c=max(this.restitution,e.restitution);if(d){g=floor(a.y-this.size.y/2+1)+this.size.y/2+.001;if(.1>g-this.pos.y&&!tileCollisionTest(vec2(this.pos.x,g),this.size,this)){this.pos.y=g;debugPhysics&&debugRect(this.pos,this.size,"#ff0");return}this.pos.x=a.x;this.velocity.x*=-c}if(f||!d)b?(b=this.size.y/2+1e-4,this.pos.y=0>gravity.y?floor(a.y-this.size.y/2)+b:ceil(a.y+this.size.y/2)-b,this.groundObject=e):(this.pos.y=a.y,this.groundObject=void 0),this.velocity.y*=-c;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(a=!1){if(!this.destroyed){this.destroyed=!0;this.parent?.removeChild(this);for(const b of this.children)b.parent=void 0,b.destroy(a)}}localToWorld(a){return this.pos.add(a.rotate(this.angle))}worldToLocal(a){return a.subtract(this.pos).rotate(-this.angle)}localToWorldVector(a){return a.rotate(this.angle)}worldToLocalVector(a){return a.rotate(-this.angle)}collideWithTile(a,b){return 0<a}collideWithObject(a){return!0}getUp(a=1){return vec2().setAngle(this.angle,a)}getRight(a=1){return vec2().setAngle(this.angle+PI/2,a)}getAliveTime(){return time-this.spawnTime}getSpeed(){return this.velocity.length()}applyAcceleration(a){this.mass&&(this.velocity=this.velocity.add(a))}applyAngularAcceleration(a){this.mass&&(this.angleVelocity+=a)}applyForce(a){this.mass&&this.applyAcceleration(a.scale(1/this.mass))}getMirrorSign(){return this.mirror?-1:1}addChild(a,b=vec2(),c=0){ASSERT(!a.parent&&!this.children.includes(a));ASSERT(a instanceof EngineObject,"child must be an EngineObject");ASSERT(a!==this,"cannot add self as child");this.children.push(a);a.parent=this;a.localPos=b.copy();a.localAngle=c;return a}removeChild(a){ASSERT(a.parent===this&&this.children.includes(a));ASSERT(a instanceof EngineObject,"child must be an EngineObject");const b=this.children.indexOf(a);ASSERT(0<=b,"child not found in children array");0<=b&&this.children.splice(b,1);a.parent=void 0}isOverlappingObject(a){return this.isOverlapping(a.pos,a.size)}isOverlapping(a,b=vec2()){return isOverlapping(this.pos,this.size,a,b)}setCollision(a=!0,b=!0,c=!0,d=!0){ASSERT(a||!b,"solid objects must be set to collide");this.collideSolidObjects=a;this.isSolid=b;this.collideTiles=c;this.collideRaycast=d}toString(){if(debug){var a="type = "+this.constructor.name;if(this.pos.x||this.pos.y)a+="\npos = "+this.pos;if(this.velocity.x||this.velocity.y)a+="\nvelocity = "+this.velocity;if(this.size.x||this.size.y)a+="\nsize = "+this.size;this.angle&&(a+="\nangle = "+this.angle.toFixed(3));this.color&&(a+="\ncolor = "+this.color);return a}}renderDebugInfo(){if(debug){var a=vec2(max(this.size.x,.2),max(this.size.y,.2)),b=rgb(this.collideTiles?1:0,this.collideSolidObjects?1:0,this.isSolid?1:0,.5);drawRect(this.pos,a,b,this.angle);this.parent&&drawRect(this.pos,a.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,mainContext,drawContext,workCanvas,workContext,workReadCanvas,workReadContext,mainCanvasSize=vec2(),textureInfos=[],drawCount;function tile(a=new Vector2,b=tileDefaultSize,c=0,d=tileDefaultPadding,e=tileDefaultBleed){ASSERT(isVector2(a)||"number"===typeof a,"index must be a vec2 or number");ASSERT(isVector2(b)||"number"===typeof b,"size must be a vec2 or number");ASSERT(isNumber(c)||c instanceof TextureInfo,"texture must be a number or TextureInfo");ASSERT(isNumber(d),"padding must be a number");if(headlessMode)return new TileInfo;"number"===typeof b&&(ASSERT(0<b),b=new Vector2(b,b));c="number"===typeof c?textureInfos[c]:c;const f=b.x+2*d,g=b.y+2*d;let h;if("number"===typeof a){const k=c.size.x/f|0;h=a%k;a=a/k|0}else h=a.x,a=a.y;a=new Vector2(h*f+d,a*g+d);return new TileInfo(a,b,c,d,e)}class TileInfo{constructor(a=vec2(),b=tileDefaultSize,c=textureInfos[0],d=tileDefaultPadding,e=tileDefaultBleed){this.pos=a.copy();this.size=b.copy();this.padding=d;this.textureInfo=c;this.bleed=e}offset(a){return new TileInfo(this.pos.add(a),this.size,this.textureInfo,this.padding,this.bleed)}frame(a){ASSERT("number"===typeof a);a*=this.size.x+2*this.padding;ASSERT(a<this.textureInfo.size.x,"frame extends beyond texture width!");return this.offset(new Vector2(a))}setFullImage(a=this.textureInfo){this.textureInfo=a;this.pos=new Vector2;this.size=a.size.copy();this.bleed=this.padding=0;return this}tile(a){return tile(a,this.size,this.textureInfo,this.padding,this.bleed)}}class TextureInfo{constructor(a,b=!0){this.size=(this.image=a)?vec2(a.width,a.height):vec2();this.sizeInverse=a?vec2(1/a.width,1/a.height):vec2();this.glTexture=void 0;b&&this.createWebGLTexture()}createWebGLTexture(){glRegisterTextureInfo(this)}destroyWebGLTexture(){glUnregisterTextureInfo(this)}hasWebGL(){return!!this.glTexture}}function drawTile(a,b=vec2(1),c,d=WHITE,e=0,f,g,h=glEnable,k,l){ASSERT(isVector2(a),"pos must be a vec2");ASSERT(isVector2(b),"size must be a vec2");ASSERT(isColor(d),"color is invalid");ASSERT(isNumber(e),"angle must be a number");ASSERT(!g||isColor(g),"additiveColor must be a color");ASSERT(!l||!h,"context only supported in canvas 2D mode");const m=c?.textureInfo,n=c?.bleed??0;if(h&&glEnable)if(ASSERT(!!glContext,"WebGL is not enabled!"),k&&([a,b,e]=screenToWorldTransform(a,b,e)),m){var p=m.sizeInverse;h=c.pos.x*p.x;k=c.pos.y*p.y;l=c.size.x*p.x;const q=c.size.y*p.y;glSetTexture(m.glTexture);if(n){const r=p.x*n;p=p.y*n;glDraw(a.x,a.y,f?-b.x:b.x,b.y,e,h+r,k+p,h-r+l,k-p+q,d.rgbaInt(),g&&g.rgbaInt())}else glDraw(a.x,a.y,f?-b.x:b.x,b.y,e,h,k,h+l,k+q,d.rgbaInt(),g&&g.rgbaInt())}else glDraw(a.x,a.y,b.x,b.y,e,0,0,0,0,0,d.rgbaInt());else++drawCount,b=new Vector2(b.x,-b.y),drawCanvas2D(a,b,e,f,q=>{if(m)drawImageColor(q,m.image,c.pos.x,c.pos.y,c.size.x,c.size.y,-.5,-.5,1,1,d,g,n);else{const r=g?d.add(g):d;q.fillStyle=r.toString();q.fillRect(-.5,-.5,1,1)}},k,l)}function drawRect(a,b,c,d,e,f,g){drawTile(a,b,void 0,c,d,!1,void 0,e,f,g)}function drawRectGradient(a,b,c=WHITE,d=BLACK,e=0,f=glEnable,g=!1,h){ASSERT(isVector2(a),"pos must be a vec2");ASSERT(isVector2(b),"size must be a vec2");ASSERT(isColor(c)&&isColor(d),"color is invalid");ASSERT(isNumber(e),"angle must be a number");ASSERT(!h||!f,"context only supported in canvas 2D mode");if(f&&glEnable){ASSERT(!!glContext,"WebGL is not enabled!");g&&(a=screenToWorld(a),b=b.scale(1/cameraScale),e+=cameraAngle);f=[];g=[];h=b.x/2;b=b.y/2;const k=c.rgbaInt(),l=d.rgbaInt(),m=cos(-e);e=sin(-e);for(let n=4;n--;){const p=n&1?h:-h,q=n&2?b:-b,r=n&2?k:l;f.push(vec2(a.x+(p*m-q*e),a.y+(p*e+q*m)));g.push(r)}glDrawColoredPoints(f,g)}else++drawCount,b=new Vector2(b.x,-b.y),drawCanvas2D(a,b,e,!1,k=>{const l=k.createLinearGradient(0,-.5,0,.5);l.addColorStop(0,c.toString());l.addColorStop(1,d.toString());k.fillStyle=l;k.fillRect(-.5,-.5,1,1)},g,h)}function drawLineList(a,b=.1,c,d=!1,e=vec2(),f=0,g=glEnable,h,k){ASSERT(isArray(a),"points must be an array");ASSERT(isNumber(b),"width must be a number");ASSERT(isColor(c),"color is invalid");ASSERT(isVector2(e),"pos must be a vec2");ASSERT(isNumber(f),"angle must be a number");ASSERT(!k||!g,"context only supported in canvas 2D mode");g&&glEnable?(ASSERT(!!glContext,"WebGL is not enabled!"),g=vec2(1),h&&([e,g,f]=screenToWorldTransform(e,g,f)),glDrawOutlineTransform(a,c.rgbaInt(),b,e.x,e.y,g.x,g.y,f,d)):(++drawCount,drawCanvas2D(e,vec2(1),f,!1,l=>{l.strokeStyle=c.toString();l.lineWidth=b;l.beginPath();for(let m=0;m<a.length;++m){const n=a[m];l.lineTo(n.x,n.y)}d&&l.closePath();l.stroke()},h,k))}function drawLine(a,b,c=.1,d,e=vec2(),f=0,g,h,k){b=vec2((b.x-a.x)/2,(b.y-a.y)/2);c=vec2(c,2*b.length());e=e.add(a.add(b));h&&(b.y*=-1);f+=b.angle();drawRect(e,c,d,f,g,h,k)}function drawRegularPoly(a,b=vec2(1),c=3,d=WHITE,e=0,f=BLACK,g=0,h=glEnable,k=!1,l){ASSERT(isVector2(b),"size must be a vec2");ASSERT(isNumber(c),"sides must be a number");const m=[],n=b.x/2;b=b.y/2;for(let p=c;p--;){const q=p/c*PI*2;m.push(vec2(sin(q)*n,cos(q)*b))}drawPoly(m,d,e,f,a,g,h,k,l)}function drawPoly(a,b=WHITE,c=0,d=BLACK,e=vec2(),f=0,g=glEnable,h=!1,k){ASSERT(isVector2(e),"pos must be a vec2");ASSERT(isArray(a),"points must be an array");ASSERT(isColor(b)&&isColor(d),"color is invalid");ASSERT(isNumber(c),"lineWidth must be a number");ASSERT(isNumber(f),"angle must be a number");ASSERT(!k||!g,"context only supported in canvas 2D mode");g&&glEnable?(ASSERT(!!glContext,"WebGL is not enabled!"),g=vec2(1),h&&([e,g,f]=screenToWorldTransform(e,g,f)),glDrawPointsTransform(a,b.rgbaInt(),e.x,e.y,g.x,g.y,f),0<c&&glDrawOutlineTransform(a,d.rgbaInt(),c,e.x,e.y,g.x,g.y,f)):drawCanvas2D(e,vec2(1),f,!1,l=>{l.fillStyle=b.toString();l.beginPath();for(const m of a)l.lineTo(m.x,m.y);l.closePath();l.fill();c&&(l.strokeStyle=d.toString(),l.lineWidth=c,l.stroke())},h,k)}function drawEllipse(a,b=vec2(1),c=WHITE,d=0,e=0,f=BLACK,g=glEnable,h=!1,k){ASSERT(isVector2(a),"pos must be a vec2");ASSERT(isVector2(b),"size must be a vec2");ASSERT(isColor(c)&&isColor(f),"color is invalid");ASSERT(isNumber(d),"angle must be a number");ASSERT(isNumber(e),"lineWidth must be a number");ASSERT(0<=e,"lineWidth must be a positive value or 0");ASSERT(!k||!g,"context only supported in canvas 2D mode");e=clamp(e,0,Math.min(b.x,b.y));g&&glEnable?drawRegularPoly(a,b,glCircleSides,c,e,f,d,g,h,k):drawCanvas2D(a,vec2(1),d,!1,l=>{l.fillStyle=c.toString();l.beginPath();l.ellipse(0,0,b.x/2,b.y/2,0,0,9);l.fill();e&&(l.strokeStyle=f.toString(),l.lineWidth=e,l.stroke())},h,k)}function drawCircle(a,b=1,c=WHITE,d=0,e=BLACK,f=glEnable,g=!1,h){ASSERT(isNumber(b),"size must be a number");drawEllipse(a,vec2(b),c,0,d,e,f,g,h)}function drawCanvas2D(a,b,c=0,d=!1,e,f=!1,g=drawContext){ASSERT(isVector2(a),"pos must be a vec2");ASSERT(isVector2(b),"size must be a vec2");ASSERT(isNumber(c),"angle must be a number");ASSERT("function"===typeof e,"drawFunction must be a function");f||(a=worldToScreen(a),b=b.scale(cameraScale),c-=cameraAngle);g.save();g.translate(a.x+.5,a.y+.5);g.rotate(c);g.scale(d?-b.x:b.x,-b.y);e(g);g.restore()}function drawText(a,b,c=1,d,e=0,f,g,h,k,l,m=0,n=drawContext){b=worldToScreen(b);c*=cameraScale;e*=cameraScale;m-=cameraAngle;drawTextScreen(a,b,c,d,e,f,g,h,k,l,-1*m,n)}function drawTextScreen(a,b,c,d=WHITE,e=0,f=BLACK,g="center",h=fontDefault,k="",l,m=0,n=drawContext){ASSERT(isString(a),"text must be a string");ASSERT(isVector2(b),"pos must be a vec2");ASSERT(isNumber(c),"size must be a number");ASSERT(isColor(d),"color must be a color");ASSERT(isNumber(e),"lineWidth must be a number");ASSERT(isColor(f),"lineColor must be a color");ASSERT(["left","center","right"].includes(g),"align must be left, center, or right");ASSERT(isString(h),"font must be a string");ASSERT(isString(k),"fontStyle must be a string");ASSERT(isNumber(m),"angle must be a number");n.fillStyle=d.toString();n.strokeStyle=f.toString();n.lineWidth=e;n.textAlign=g;n.font=k+" "+c+"px "+h;n.textBaseline="middle";a=(a+"").split("\n");d=b.y-(a.length-1)*c/2;n.save();n.translate(b.x,d);n.rotate(-m);let p=0;a.forEach(q=>{e&&n.strokeText(q,0,p,l);n.fillText(q,0,p,l);p+=c});n.restore()}async function loadTexture(a,b){ASSERT(isNumber(a),"textureIndex must be a number");ASSERT(!textureInfos[a],"textureIndex is already loaded!");ASSERT(!b||isString(b),"image src must be a string");const c=new Image;b&&await new Promise(d=>{c.onerror=c.onload=d;c.crossOrigin="anonymous";c.src=b});textureInfos[a]=new TextureInfo(c)}function screenToWorld(a){ASSERT(isVector2(a),"screenPos must be a vec2");let b=(a.x-mainCanvasSize.x/2+.5)/cameraScale;a=(a.y-mainCanvasSize.y/2+.5)/-cameraScale;if(cameraAngle){const c=cos(-cameraAngle),d=sin(-cameraAngle),e=b*d+a*c;b=b*c-a*d;a=e}return new Vector2(b+cameraPos.x,a+cameraPos.y)}function worldToScreen(a){ASSERT(isVector2(a),"worldPos must be a vec2");let b=a.x-cameraPos.x;a=a.y-cameraPos.y;if(cameraAngle){const c=cos(cameraAngle),d=sin(cameraAngle),e=b*d+a*c;b=b*c-a*d;a=e}return new Vector2(b*cameraScale+mainCanvasSize.x/2-.5,a*-cameraScale+mainCanvasSize.y/2-.5)}function screenToWorldDelta(a){ASSERT(isVector2(a),"screenDelta must be a vec2");let b=a.x/cameraScale;a=a.y/-cameraScale;if(cameraAngle){const c=cos(-cameraAngle),d=sin(-cameraAngle),e=b*d+a*c;b=b*c-a*d;a=e}return new Vector2(b,a)}function worldToScreenDelta(a){ASSERT(isVector2(a),"worldDelta must be a vec2");let b=a.x;a=a.y;if(cameraAngle){const c=cos(cameraAngle),d=sin(cameraAngle),e=b*d+a*c;b=b*c-a*d;a=e}return new Vector2(b*cameraScale,a*-cameraScale)}function screenToWorldTransform(a,b,c=0){ASSERT(isVector2(a),"screenPos must be a vec2");ASSERT(isVector2(b),"screenSize must be a vec2");ASSERT(isNumber(c),"screenAngle must be a number");return[screenToWorld(a),b.scale(1/cameraScale),c+cameraAngle]}function getCameraSize(){return mainCanvasSize.scale(1/cameraScale)}function isOnScreen(a,b=0){ASSERT(isVector2(a),"pos must be a vec2");ASSERT(isVector2(b)||isNumber(b),"size must be a vec2 or number");let c=a.x-cameraPos.x;a=a.y-cameraPos.y;if(cameraAngle){var d=cos(cameraAngle),e=sin(cameraAngle);const f=c*e+a*d;c=c*d-a*e;a=f}c*=2*cameraScale;a*=2*-cameraScale;b instanceof Vector2&&(b=b.length());b*=cameraScale;d=mainCanvasSize.x;e=mainCanvasSize.y;return c+b>-d&&c-b<d&&a+b>-e&&a-b<e}function setBlendMode(a=!1,b=drawContext){glAdditive=a;b.globalCompositeOperation=a?"lighter":"source-over"}function combineCanvases(){const a=mainCanvasSize.x,b=mainCanvasSize.y;workCanvas.width=a;workCanvas.height=b;workContext.fillRect(0,0,a,b);glCopyToContext(workContext);workContext.drawImage(mainCanvas,0,0);mainContext.drawImage(workCanvas,0,0)}function drawImageColor(a,b,c,d,e,f,g,h,k,l,m,n,p=0){function q(u){return 1<=u.r&&1<=u.g&&1<=u.b}e=max(1,e|0);f=max(1,f|0);const r=e-2*p,t=f-2*p;if(!canvasColorTiles||(n?q(m.add(n))&&0>=n.a:q(m)))a.globalAlpha=m.a,a.drawImage(b,c+p,d+p,r,t,g,h,k,l),a.globalAlpha=1;else if(workReadCanvas.width=e,workReadCanvas.height=f,workReadContext.drawImage(b,c|0,d|0,e,f,0,0,e,f),b=workReadContext.getImageData(0,0,e,f),c=b.data,!n||0>=n.r&&0>=n.g&&0>=n.b&&0>=n.a){for(n=0;n<c.length;n+=4)c[n]*=m.r,c[n+1]*=m.g,c[n+2]*=m.b;workReadContext.putImageData(b,0,0);a.globalAlpha=m.a;a.drawImage(workReadCanvas,p,p,r,t,g,h,k,l);a.globalAlpha=1}else{m=[m.r,m.g,m.b,m.a];n=[255*n.r,255*n.g,255*n.b,255*n.a];for(d=0;d<c.length;++d)c[d]=c[d]*m[d&3]+n[d&3]|0;workReadContext.putImageData(b,0,0);a.drawImage(workReadCanvas,p,p,r,t,g,h,k,l)}}function isFullscreen(){return!!document.fullscreenElement}function toggleFullscreen(){const a=mainCanvas.parentElement;isFullscreen()?document.exitFullscreen&&document.exitFullscreen():a.requestFullscreen&&a.requestFullscreen()}function setCursor(a="auto"){mainCanvas.parentElement.style.cursor=a}let engineFontImage;class FontImage{constructor(a){ASSERT(!!a,"tileInfo is required for FontImage");this.tileInfo=a.frame(0)}drawText(a,b,c=1,d,e,f,g){ASSERT(isVector2(c)||"number"===typeof c,"size must be a vec2 or number");"number"===typeof c?(ASSERT(0<c),c*=cameraScale,c=new Vector2(c,c)):c=c.scale(cameraScale);this.drawTextScreen(a,worldToScreen(b),c,d,e,f,g)}drawTextScreen(a,b,c,d=!0,e=WHITE,f=glEnable,g){ASSERT(isString(a),"text must be a string");ASSERT(isVector2(b),"pos must be a vec2");ASSERT(isVector2(c)||"number"===typeof c,"size must be a vec2 or number");ASSERT(isColor(e),"color must be a color");c="number"===typeof c?new Vector2(c,c):c;const h=new Vector2,k=this.tileInfo,l=k.padding,m=k.size.x+2*l,n=k.size.y+2*l,p=k.textureInfo.size.x/m|0;(a+"").split("\n").forEach((q,r)=>{const t=d?(q.length-1)*c.x/2:0;for(let w=q.length;w--;){var u=q.charCodeAt(w);u=32>u||127<u?95:u-32;const x=u/p|0;k.pos.x=u%p*m+l;k.pos.y=x*n+l;h.x=b.x+w*c.x-t|0;h.y=b.y+r*c.y|0;drawTile(h,c,k,e,0,!1,void 0,f,!0,g)}})}}async function fontImageInit(){const a=new Image;await new Promise(e=>{a.onerror=a.onload=e;a.crossOrigin="anonymous";a.src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAAeAQMAAABnrVXaAAAABlBMVEUAAAD///+l2Z/dAAAAAXRSTlMAQObYZgAAAjpJREFUOMu9kzFu2zAUhn+CAROgqrk+B2l0BWYxMjlXeYaAtFtbdA1sGgHqRQfI0CNkSG5AwYB0BQ8d5Bsomwah6CPVeGg6tEPzAxLwyI+P78cP4u9lNO9OoMKnLMOobG5020/yaj/MrRcCGh1gBbyiLTPJEYaIiom5KM9Jq7KgynMGtb6L4GL4MF2H4LQKCXTvDVw2I4MsgZT7QLExdiutH+D08VOP3INXRrWX1/mmpbkNgAPYRVANb4xpcegYvhiNbIXauQICEjBuYLfMakaakWQeXxiZ0VDtuJCKs3ztMV59QtsHJNcRxDzfdL21ty3PrfIcXTN+E+GFAv6T5nbT9jd50/WFxb5ksdAv49qS6ouymG66ji08UMT6moykYLAo+V0j23GN4m829ZySAD5K7QsBfQTvOG8eE+gTeGYRAmnNAubN3hf5Zv9tJWDHp/VTuaSm7SN4fyINQqaNO3RMVxvpSPXnOChnRNvFcGY0gnwiPswYwTKVPE0zVtX3mTEIOoFzaqLrGuJaV+Uqumb71fVk/VoOH3cdLNQP/FHi8hV0CQNoqBZsUPlLPMsdCJro9QAaQQ0woDy9BJm0eTxCFnO9srcYlhNVlfR2EyTrph1uUtbUtAJifwRgrKuYdXVHeb0YI3QpawohQHkloI3J5FuVwI5ORxC9k2Tuz9Ir1IjgeIPGMHYkAZe2RuYkmWFmt3gGbTPOmBUWVTmRmHtGrfpzG/yuQNOKa6gBB/WA9khitPgl6/GP+gl2Af6tCbvaygAAAABJRU5ErkJggg=="});var b=vec2();const c=vec2(8),d=new TextureInfo(a);b=new TileInfo(b,c,d,1,0);engineFontImage=new FontImage(b)}let mousePos=vec2(),mousePosScreen=vec2(),mouseDelta=vec2(),mouseDeltaScreen=vec2(),mouseWheel=0,mouseInWindow=!0,isUsingGamepad=!1,inputPreventDefault=!0,gamepadPrimary=0;const isTouchDevice=!headlessMode&&void 0!==window.ontouchstart;function setInputPreventDefault(a){inputPreventDefault=a}function inputClearKey(a,b=0,c=!0,d=!0,e=!0){inputData[b]&&(inputData[b][a]&=~((c?1:0)|(d?2:0)|(e?4:0)))}function inputClear(){inputData.length=0;inputData[0]=[];touchGamepadButtons.length=0;touchGamepadSticks.length=0;gamepadStickData.length=0;gamepadDpadData.length=0}function keyIsDown(a,b=0){ASSERT(isString(a),"key must be a number or string");ASSERT(0<b||"number"!==typeof a||3>a,"use code string for keyboard");return!!(inputData[b]?.[a]&1)}function keyWasPressed(a,b=0){ASSERT(isString(a),"key must be a number or string");ASSERT(0<b||"number"!==typeof a||3>a,"use code string for keyboard");return!!(inputData[b]?.[a]&2)}function keyWasReleased(a,b=0){ASSERT(isString(a),"key must be a number or string");ASSERT(0<b||"number"!==typeof a||3>a,"use code string for keyboard");return!!(inputData[b]?.[a]&4)}function keyDirection(a="ArrowUp",b="ArrowDown",c="ArrowLeft",d="ArrowRight"){ASSERT(isString(a),"up key must be a string");ASSERT(isString(b),"down key must be a string");ASSERT(isString(c),"left key must be a string");ASSERT(isString(d),"right key must be a string");return vec2((keyIsDown(d)?1:0)-(keyIsDown(c)?1:0),(keyIsDown(a)?1:0)-(keyIsDown(b)?1:0))}function mouseIsDown(a){ASSERT(isNumber(a),"mouse button must be a number");return keyIsDown(a)}function mouseWasPressed(a){ASSERT(isNumber(a),"mouse button must be a number");return keyWasPressed(a)}function mouseWasReleased(a){ASSERT(isNumber(a),"mouse button must be a number");return keyWasReleased(a)}function gamepadIsDown(a,b=gamepadPrimary){ASSERT(isNumber(a),"button must be a number");ASSERT(isNumber(b),"gamepad must be a number");return keyIsDown(a,b+1)}function gamepadWasPressed(a,b=gamepadPrimary){ASSERT(isNumber(a),"button must be a number");ASSERT(isNumber(b),"gamepad must be a number");return keyWasPressed(a,b+1)}function gamepadWasReleased(a,b=gamepadPrimary){ASSERT(isNumber(a),"button must be a number");ASSERT(isNumber(b),"gamepad must be a number");return keyWasReleased(a,b+1)}function gamepadStick(a,b=gamepadPrimary){ASSERT(isNumber(a),"stick must be a number");ASSERT(isNumber(b),"gamepad must be a number");return gamepadStickData[b]?.[a]??vec2()}function gamepadDpad(a=gamepadPrimary){ASSERT(isNumber(a),"gamepad must be a number");return gamepadDpadData[a]??vec2()}function gamepadConnected(a=gamepadPrimary){ASSERT(isNumber(a),"gamepad must be a number");return!!inputData[a+1]}function gamepadStickCount(a=gamepadPrimary){ASSERT(isNumber(a),"gamepad must be a number");return gamepadStickData[a]?.length??0}function vibrate(a=100){ASSERT(isNumber(a)||isArray(a),"pattern must be a number or array");vibrateEnable&&!headlessMode&&navigator?.vibrate?.(a)}function vibrateStop(){vibrate(0)}function pointerLockRequest(){!isTouchDevice&&mainCanvas.requestPointerLock?.()}function pointerLockExit(){document.exitPointerLock?.()}function pointerLockIsActive(){return document.pointerLockElement===mainCanvas}const inputData=[[]],gamepadStickData=[],gamepadDpadData=[],gamepadHadInput=[],touchGamepadTimer=new Timer,touchGamepadButtons=[],touchGamepadSticks=[];function inputInit(){function a(d){return inputWASDEmulateDirection?"KeyW"===d?"ArrowUp":"KeyS"===d?"ArrowDown":"KeyA"===d?"ArrowLeft":"KeyD"===d?"ArrowRight":d:d}function b(){function d(f){if(touchInputEnable){if(touchGamepadEnable)a:{touchGamepadSticks.length=0;touchGamepadSticks[0]=vec2();touchGamepadSticks[1]=vec2();touchGamepadButtons.length=0;isUsingGamepad=!0;if(f.touches.length&&(touchGamepadTimer.set(),touchGamepadCenterButton&&!e&&paused)){touchGamepadButtons[9]=1;break a}if(!paused){var g=vec2(touchGamepadSize,mainCanvasSize.y-touchGamepadSize),h=touchGamepadButtonCenter(),k=mainCanvasSize.scale(.5);for(const n of f.touches){var l=c(vec2(n.clientX,n.clientY));if(g.distance(l)<touchGamepadSize)l=l.subtract(g),touchGamepadSticks[0]=l.scale(2/touchGamepadSize).clampLength();else if(h.distance(l)<touchGamepadSize){if(1===touchGamepadButtonCount){var m=l.subtract(h);touchGamepadSticks[1]=m.scale(2/touchGamepadSize).clampLength()}m=h.subtract(l).direction();m=mod(m+2,4);1===touchGamepadButtonCount?m=0:2===touchGamepadButtonCount&&(l=h.subtract(l),m=-l.x<l.y?1:0);m=3===m?2:2===m?3:m;m<touchGamepadButtonCount&&(touchGamepadButtons[m]=1)}else touchGamepadCenterButton&&k.distance(l)<touchGamepadSize&&(touchGamepadButtons[9]=1)}}}soundEnable&&!headlessMode&&audioContext&&!audioIsRunning()&&audioContext.resume();(g=f.touches.length)?(h=vec2(f.touches[0].clientX,f.touches[0].clientY),k=mousePosScreen,mousePosScreen=c(h),e?(mouseDeltaScreen=mouseDeltaScreen.add(mousePosScreen.subtract(k)),isUsingGamepad=touchGamepadEnable):inputData[0][0]=3):e&&(inputData[0][0]=inputData[0][0]&2|4);e=g;inputPreventDefault&&document.hasFocus()&&f.cancelable&&f.preventDefault();return!0}}document.addEventListener("touchstart",f=>d(f),{passive:!1});document.addEventListener("touchmove",f=>d(f),{passive:!1});document.addEventListener("touchend",f=>d(f),{passive:!1});let e}function c(d){const e=mainCanvas.getBoundingClientRect(),f=percent(d.x,e.left,e.right);d=percent(d.y,e.top,e.bottom);return vec2(f*mainCanvas.width,d*mainCanvas.height)}headlessMode||(document.addEventListener("keydown",function(d){d.repeat||(isUsingGamepad=!1,inputData[0][d.code]=3,inputWASDEmulateDirection&&(inputData[0][a(d.code)]=3));"ArrowUp ArrowDown ArrowLeft Ar