UNPKG

@maximeij/css-brickout

Version:

Classic Brickout Game Engine implemented in Typescript and rendered with CSS. No dependencies.

11 lines (9 loc) β€’ 41.2 kB
(function(global,factory){typeof exports=="object"&&typeof module<"u"?factory(exports):typeof define=="function"&&define.amd?define(["exports"],factory):(global=typeof globalThis<"u"?globalThis:global||self,factory(global["CSS Brickout"]={}))})(this,function(exports2){"use strict";var __defProp=Object.defineProperty;var __defNormalProp=(obj,key,value)=>key in obj?__defProp(obj,key,{enumerable:!0,configurable:!0,writable:!0,value}):obj[key]=value;var __publicField=(obj,key,value)=>__defNormalProp(obj,typeof key!="symbol"?key+"":key,value);function createEvent(name,obj,bubbles=!0){return new CustomEvent(name,{detail:obj,bubbles})}function formatObjectTitle(object){var _a;return`${object.toString()} ${(_a=object.bonuses)==null?void 0:_a.map(bonus=>`.${bonus.cssClass}`).join(` `)}`}function msToString(ms){const seconds=Math.floor(ms/1e3),minutes=Math.floor(seconds/60),secondsLeft=seconds%60;return`${padZero(minutes)}:${padZero(secondsLeft)}`}function padZero(num){return num<10?num.toString().padStart(2,"0"):num.toString()}const clamp=(n,max=1,min=0)=>Math.min(max,Math.max(min,n)),pythagoras=(a2,b)=>Math.sqrt(a2*a2+b*b);function rotatePoint(x,y,originX,originY,angle){const cosTheta=Math.cos(angle),sinTheta=Math.sin(angle),newX=cosTheta*(x-originX)-sinTheta*(y-originY)+originX,newY=sinTheta*(x-originX)+cosTheta*(y-originY)+originY;return{x:newX,y:newY}}function rotateVector(x,y,angle){const cos=Math.cos(angle),sin=Math.sin(angle);return{x:cos*x-sin*y,y:sin*x+cos*y}}function normalize(vector){const length=pythagoras(vector.x,vector.y);return length===0?{x:0,y:0}:{x:vector.x/length,y:vector.y/length}}function projectRectangleOntoAxis(rectangleCorners,axis){const dotProduct1=rectangleCorners.topL.x*axis.x+rectangleCorners.topL.y*axis.y,dotProduct2=rectangleCorners.topR.x*axis.x+rectangleCorners.topR.y*axis.y,dotProduct3=rectangleCorners.bottomL.x*axis.x+rectangleCorners.bottomL.y*axis.y,dotProduct4=rectangleCorners.bottomR.x*axis.x+rectangleCorners.bottomR.y*axis.y,min=Math.min(dotProduct1,dotProduct2,dotProduct3,dotProduct4),max=Math.max(dotProduct1,dotProduct2,dotProduct3,dotProduct4);return{min,max}}function overlapOnAxis(circle,axis,rectangleCorners){const circleProjection=circle.x*axis.x+circle.y*axis.y,rectProjection=projectRectangleOntoAxis(rectangleCorners,axis);return Math.max(0,Math.min(rectProjection.max,circleProjection+circle.radius)-Math.max(rectProjection.min,circleProjection-circle.radius))}const EPSILON=1e-8,MIN_PAST=-EPSILON;function rayAABB(localP0,localVelocity,halfExtents){const invD={x:1/0,y:1/0};Math.abs(localVelocity.x)>EPSILON&&(invD.x=1/localVelocity.x),Math.abs(localVelocity.y)>EPSILON&&(invD.y=1/localVelocity.y);const bounds=[{x:-halfExtents.x,y:-halfExtents.y},{x:halfExtents.x,y:halfExtents.y}],signX=invD.x<0?1:0,signY=invD.y<0?1:0;let tmin=(bounds[signX].x-localP0.x)*invD.x;const tmax=(bounds[1-signX].x-localP0.x)*invD.x,tymin=(bounds[signY].y-localP0.y)*invD.y,tymax=(bounds[1-signY].y-localP0.y)*invD.y;return tmin>tymax||tymin>tmax?null:tymin>tmin?(tmin=tymin,{tmin,normal:{x:0,y:invD.y<0?1:-1}}):{tmin,normal:{x:invD.x<0?1:-1,y:0}}}function getCRCollisionPosition(rect,circle){const P0={x:circle.x-rect.x,y:circle.y-rect.y},localP0=rotatePoint(P0.x,P0.y,0,0,-rect.angle),relativeVelocity={x:(circle.dx??0)-(rect.dx??0),y:(circle.dy??0)-(rect.dy??0)},localVelocity=rotateVector(relativeVelocity.x,relativeVelocity.y,-rect.angle),halfExtentsWithRadius={x:rect.width/2+circle.radius,y:rect.height/2+circle.radius},result=rayAABB(localP0,localVelocity,halfExtentsWithRadius);if(!result)return null;const{tmin,normal}=result;if(tmin<MIN_PAST||tmin>1)return null;const hitPoint={x:localP0.x+localVelocity.x*tmin,y:localP0.y+localVelocity.y*tmin},rotatedHitPoint=rotatePoint(hitPoint.x,hitPoint.y,0,0,rect.angle),position={x:rotatedHitPoint.x+rect.x,y:rotatedHitPoint.y+rect.y},collisionWorldNormal=rotateVector(normal.x,normal.y,rect.angle);return{position,normal:collisionWorldNormal,tmin}}function getCCCollisionPosition(lead,other){const A0={x:lead.x,y:lead.y},B0={x:other.x,y:other.y},vA={x:lead.dx??0,y:lead.dy??0},vB={x:other.dx??0,y:other.dy??0},vRel={x:vA.x-vB.x,y:vA.y-vB.y},r=lead.radius+other.radius,d={x:A0.x-B0.x,y:A0.y-B0.y},a=vRel.x*vRel.x+vRel.y*vRel.y,b=2*(d.x*vRel.x+d.y*vRel.y),c=d.x*d.x+d.y*d.y-r*r,discriminant=b*b-4*a*c;if(discriminant<0||a===0)return null;const sqrtDisc=Math.sqrt(discriminant),t1=(-b-sqrtDisc)/(2*a),t2=(-b+sqrtDisc)/(2*a),tmin=t1>=MIN_PAST&&t1<=1?t1:t2>=MIN_PAST&&t2<=1?t2:null;if(tmin===null)return null;const A_t={x:A0.x+vA.x*tmin,y:A0.y+vA.y*tmin},B_t={x:B0.x+vB.x*tmin,y:B0.y+vB.y*tmin},normal=normalize({x:A_t.x-B_t.x,y:A_t.y-B_t.y}),contactPoint={x:A_t.x-lead.radius*normal.x,y:A_t.y-lead.radius*normal.y};return{tmin,position:contactPoint,normal}}function normalizeAngle(angle){let res=angle;for(;res>Math.PI;)res-=2*Math.PI;for(;res<=-Math.PI;)res+=2*Math.PI;return res}class GameObject{constructor({game,parent=game,elementId,className,x,y,width=0,height=0,angle=0,startingBonuses=[],showTitle=!1,permanent=!1,shape="rectangle",...rest}){__publicField(this,"x",0);__publicField(this,"y",0);__publicField(this,"width");__publicField(this,"height");__publicField(this,"area");__publicField(this,"_angle");__publicField(this,"bonuses");__publicField(this,"element");__publicField(this,"game");__publicField(this,"parent");__publicField(this,"boundingBox",{topL:{x:0,y:0},topR:{x:0,y:0},bottomL:{x:0,y:0},bottomR:{x:0,y:0}});__publicField(this,"permanent",!1);__publicField(this,"shape","rectangle");__publicField(this,"radius",0);__publicField(this,"rx",0);if(this.width=width,this.height=height,shape==="circle"&&(this.shape="circle",this.radius=height/2),this.area=width*height,this._angle=angle,this.game=game,this.parent=parent,this.bonuses=startingBonuses,this.element=document.createElement("div"),this.permanent=permanent,Object.keys(rest).length&&Object.assign(this,rest),showTitle&&(this.element.title=formatObjectTitle(this)),elementId&&(this.element.id=elementId),className){const classNames=className.trim().split(" ").filter(Boolean);classNames.length&&this.element.classList.add(...classNames)}shape==="circle"&&this.element.classList.add("circle"),this.parent.element.appendChild(this.element),this.updatePosition(x,y),this.angle=angle,this.updateElement()}get angle(){return this._angle}set angle(angle){this._angle=angle,this.updateBoundingBox(),this.element.style.setProperty("--angle",`${angle}rad`)}updateCircleShape(){var _a,_b;if(this.rx=this.radius,!Number.isNaN((_a=this.parent)==null?void 0:_a.sizes.width)&&((_b=this.parent)==null?void 0:_b.sizes.width)>0){const pxRadius=Math.round(this.radius/100*this.parent.sizes.height);this.element.style.setProperty("--diameter",pxRadius*2+"px"),this.rx=this.radius*this.parent.sizes.height/this.parent.sizes.width}this.width=this.rx*2,this.height=this.radius*2,this.updateBoundingBox()}updateElementSize(){var _a;const{width,height}=((_a=this.parent)==null?void 0:_a.sizes)??{width:1,height:1};this.shape==="circle"&&this.updateCircleShape(),this.width&&(this.element.style.width=`${Math.round(width*(this.width/100))}px`),this.height&&(this.element.style.height=`${height*(this.height/100)}px`)}updateElement(){this.updateElementSize(),this.updateElementPosition()}updateTitle(){this.element.title&&(this.element.title=formatObjectTitle(this))}applyBonuses(){this.bonuses.forEach(bonus=>{this.element.classList.add(bonus.cssClass);const undo=bonus.effect(this);bonus.duration&&setTimeout(()=>{undo(this),this.element.classList.remove(bonus.cssClass),this.bonuses.splice(this.bonuses.indexOf(bonus),1)},bonus.duration)})}updatePosition(x,y){this.x=x??this.x,this.y=y??this.y,this.width&&this.height&&this.updateBoundingBox()}updateBoundingBox(){const halfWidth=this.width/2,halfHeight=this.height/2,args2=[this.x,this.y,this.angle];this.boundingBox={bottomR:rotatePoint(this.x+halfWidth,this.y+halfHeight,...args2),bottomL:rotatePoint(this.x-halfWidth,this.y+halfHeight,...args2),topL:rotatePoint(this.x-halfWidth,this.y-halfHeight,...args2),topR:rotatePoint(this.x+halfWidth,this.y-halfHeight,...args2)}}updateElementPosition(){const{width,height}=this.parent.sizes,absX=this.x/100*width,absY=this.y/100*height;this.setStyle("transform",`translateX(calc(${absX}px - 50%)) translateY(calc(${absY}px - 50%)) rotateZ(var(--angle, 0rad))`),this.element.style.setProperty("--xp",this.x.toFixed(2)),this.element.style.setProperty("--yp",this.y.toFixed(2))}setStyle(style,value){this.element.style[style]=value}setContent(content){this.element.innerHTML=content}emitParticles(count,classNames,recycleCondition="animationend",inheriteSize=!1){const particles=[];for(let i=0;i<count;i++){const particle=this.game.level.getParticleElement(recycleCondition);classNames!=null&&classNames.length&&particle.classList.add(...classNames),inheriteSize&&(particle.style.setProperty("width",this.element.clientWidth+"px"),particle.style.setProperty("height",this.element.clientHeight+"px")),particle.style.setProperty("opacity","1"),particle.style.setProperty("transform",this.element.style.transform),particles.push(particle)}return particles}destroy(){this.element.remove()}toString(){var _a,_b;return`${this.constructor.name}: ${this.element.id} (${((_a=this.x)==null?void 0:_a.toFixed(2))??"?"}, ${((_b=this.y)==null?void 0:_b.toFixed(2))??"?"})`}}class MovingGameObject extends GameObject{constructor({movement={speed:0,angle:0},syncAngles,...rest}){var __super=(...args)=>(super(...args),__publicField(this,"_speed",0),__publicField(this,"_movementAngle",0),__publicField(this,"turnSteps",[]),__publicField(this,"dx",0),__publicField(this,"dy",0),__publicField(this,"active",!0),__publicField(this,"syncAngles",!1),this);Array.isArray(movement)||(movement==null?void 0:movement.speed)>0?(__super({...rest,className:`moving-object ${rest.className??""}`}),this.syncAngles=syncAngles??!1,this.updateElement(),Array.isArray(movement)?this.movement=[...movement]:this.movement={...movement}):(__super(rest),this.syncAngles=syncAngles??!1)}get fx(){var _a,_b;return((_b=(_a=this.game)==null?void 0:_a.level)==null?void 0:_b.fx)??1}get fy(){var _a,_b;return((_b=(_a=this.game)==null?void 0:_a.level)==null?void 0:_b.fy)??1}get speed(){return this._speed}set speed(speed){this._speed=speed,this.setD()}get movementAngle(){return this._movementAngle}set movementAngle(angle){this._movementAngle=angle,this.syncAngles&&(this.angle=-angle),this.setD()}get movement(){return{angle:this.movementAngle,speed:this.speed}}set movement(movementConfig){if(Array.isArray(movementConfig)){this.turnSteps=movementConfig;const firstStep=movementConfig[0];firstStep&&(this.movement=firstStep.movement)}else this.movementAngle=(movementConfig==null?void 0:movementConfig.angle)??0,this.speed=(movementConfig==null?void 0:movementConfig.speed)??0,this.setD()}updatePosition(x,y,fraction=1){var _a,_b;if(super.updatePosition((x??this.x??0)+(this.dx??0)*fraction,(y??this.y??0)+(this.dy??0)*fraction),(_a=this.turnSteps)!=null&&_a.length){const currentStep=this.turnSteps[0];if((_b=currentStep==null?void 0:currentStep.condition)!=null&&_b.call(currentStep,this)){this.turnSteps.shift(),this.turnSteps.push(currentStep);const nextStep=this.turnSteps[0];nextStep&&(this.movement=nextStep.movement)}}}setD(){this.dx=this.fy*this.speed*Math.cos(this.movementAngle),this.dy=this.fx*-this.speed*Math.sin(this.movementAngle)}processFrame(frameFraction=1){this.active&&this.updatePosition(void 0,void 0,frameFraction)}toString(){return this.movement.speed?`${super.toString()} ${(this.movement.speed*10).toFixed(2)} knots`:super.toString()}}class Clickable extends GameObject{constructor({x=50,y=50,onClick,...rest}){super({...rest,x,y,className:[rest.className??"","clickable"].filter(Boolean).join(" ")});__publicField(this,"onClick");this.onClick=onClick,this.element.addEventListener("click",this.onClick)}destroy(){this.element.removeEventListener("click",this.onClick),super.destroy()}}class Controls extends GameObject{constructor({elementId="controls",x=0,y=0,handleFullscreen,handlePause,handleDebug,...rest}){super({elementId,x,y,...rest});__publicField(this,"fullscreen");__publicField(this,"pause");__publicField(this,"debug");__publicField(this,"sizes",{width:0,height:0});this.fullscreen=new Clickable({game:this.game,parent:this,elementId:"ctrl-fullscreen",x:0,y:0,onClick:handleFullscreen}),this.fullscreen.element.title="Toggle fullscreen [F]",this.fullscreen.setContent("πŸ–₯️"),this.pause=new Clickable({game:this.game,parent:this,elementId:"ctrl-pause",x:20,y:0,onClick:handlePause}),this.pause.element.title="Pause [SPACE] [P]",this.pause.setContent("⏸️"),handleDebug&&(this.debug=new Clickable({game:this.game,parent:this,elementId:"ctrl-debug",x:0,y:0,onClick:handleDebug}),this.debug.element.title="Toggle debug mode [D]",this.debug.setContent("🐞"))}updateElementPositions(){var _a;this.fullscreen.updateElementPosition(),(_a=this.debug)==null||_a.updateElementPosition(),this.pause.updateElementPosition()}updateSizes(){this.sizes.width=this.element.offsetWidth,this.sizes.height=this.element.offsetHeight,this.updateElement()}destroy(){var _a;this.fullscreen.destroy(),this.pause.destroy(),(_a=this.debug)==null||_a.destroy(),super.destroy()}}class Ball extends MovingGameObject{constructor({idx,radius,movement,damage=1,...objConfig}){var _a;super({...objConfig,className:[...((_a=objConfig.className)==null?void 0:_a.split(" "))??[],"ball"].join(" "),elementId:`ball-${idx}`,movement,showTitle:!0,shape:"circle"});__publicField(this,"destroyed",!1);__publicField(this,"damage",1);__publicField(this,"antiJuggling",!1);this.radius=radius,this.damage=damage,this.applyBonuses(),this.updateElementSize(),this.updateTitle()}setD(){super.setD(),this.element.style.setProperty("--dx",this.dx+"px"),this.element.style.setProperty("--dy",this.dy+"px")}handleLevelCollision(level,paddle,frameFraction){if(this.handleBoundaryCollision())return!1;if(this.handlePaddleCollision(paddle,frameFraction))return!0;let hitBrick=!1,i=0;const nearby=level.getNearbyBricks(this);for(;i<nearby.length&&!hitBrick;){const brick=nearby[i];if(i++,brick.destroyed)continue;(brick.hitboxParts??[brick]).some(part=>{this.isColliding(part)&&(hitBrick=!0,this.handleBrickCollision(part,frameFraction,brick))})}return!0}handleBoundaryCollision(){const hitTop=this.y-this.radius<=0;if(this.x-this.rx<=0||this.x+this.rx>=100)return hitTop?(this.movementAngle=this.movementAngle-Math.PI,this.y=this.radius):this.movementAngle=Math.PI-this.movementAngle,this.x-this.rx<=0?this.x=this.rx:this.x=100-this.rx,this.antiJuggling=!1,this.dispatchCollisionEvent(),!0;if(hitTop)return this.movementAngle=-this.movementAngle,this.y-this.radius<=0&&(this.y=this.radius),this.antiJuggling=!1,this.dispatchCollisionEvent(),!0;if(this.y+this.radius>=100)return this.speed=0,this.destroy(!1),!0}resolveCollision(object,frameFraction=1){const virtualCircle={x:this.x-this.dx*frameFraction,y:this.y-this.dy*frameFraction,radius:this.radius,dx:this.dx*frameFraction,dy:this.dy*frameFraction},castObj=object,baseVirtualObject={angle:object.angle,boundingBox:object.boundingBox,width:object.width,height:object.height,radius:object.shape==="circle"?object.radius:0},virtualObject=object instanceof MovingGameObject?{...baseVirtualObject,x:object.x-castObj.dx*frameFraction,y:object.y-castObj.dy*frameFraction,dx:castObj.dx*frameFraction,dy:castObj.dy*frameFraction}:{...baseVirtualObject,x:object.x,y:object.y,dx:0,dy:0},collision=object.shape==="rectangle"?getCRCollisionPosition(virtualObject,virtualCircle):getCCCollisionPosition(virtualCircle,virtualObject);if(collision){const{normal,tmin}=collision,nextX=virtualCircle.x+virtualCircle.dx*tmin,nextY=virtualCircle.y+virtualCircle.dy*tmin;!Number.isNaN(nextX)&&!Number.isNaN(nextY)&&(this.x=nextX,this.y=nextY);const dot=virtualCircle.dx*normal.x+virtualCircle.dy*normal.y,reflectedDx=virtualCircle.dx-2*dot*normal.x,reflectedDy=virtualCircle.dy-2*dot*normal.y;return this.movementAngle=-Math.atan2(reflectedDy,reflectedDx),this.antiJuggling=object.element.id,!0}}handleBrickCollision(brick,frameFraction=1,composite){const parentBrick=composite??brick;if(brick.breakthrough){parentBrick.takeHit(this),this.dispatchCollisionEvent(parentBrick),this.antiJuggling=brick.element.id;return}if(this.resolveCollision(brick,frameFraction))return this.dispatchCollisionEvent(brick),parentBrick.takeHit(this),!0}handlePaddleCollision(paddle,frameFraction=1){if(this.isColliding(paddle)&&this.resolveCollision(paddle,frameFraction)){const hitPosition=this.x-paddle.x,hitPositionNormalized=Math.min(1,Math.max(-1,hitPosition/(paddle.width/2))),angleMultiplier=paddle.curveFactor??0,hitPositionSkew=hitPositionNormalized*angleMultiplier*(Math.PI/2);return this.movementAngle=normalizeAngle(this.movementAngle-hitPositionSkew),this.dispatchCollisionEvent(paddle),!0}}isColliding(object){if(this.antiJuggling===object.element.id)return!1;const cos=Math.cos(object.angle),sin=Math.sin(object.angle),axes=[{x:cos,y:sin},{x:-sin,y:cos}];for(const axis of axes)if(!overlapOnAxis(this,axis,object.boundingBox))return!1;return!0}processFrame(frameFraction=1,level,paddle){this.active&&(this.updatePosition(void 0,void 0,frameFraction),level&&paddle&&this.handleLevelCollision(level,paddle,frameFraction))}dispatchCollisionEvent(object){const event=createEvent("ballcollision",{ball:this,object});this.parent.element.dispatchEvent(event)}destroy(forReal=!0){this.element.classList.add("ball--destroyed"),this.destroyed=!0;const event=createEvent("balldestroyed",this);this.parent.element.dispatchEvent(event),forReal&&super.destroy()}toString(){return`${super.toString()} ${this.damage?`Damage: ${this.damage}`:""}`}}class Brick extends MovingGameObject{constructor({hp=1,breakthrough=!1,ignoreMobile=!1,...config}){super({...config,className:[config.className??"","brick"].filter(Boolean).join(" "),showTitle:!0});__publicField(this,"breakthrough");__publicField(this,"ignoreMobile");__publicField(this,"destroyed",!1);__publicField(this,"hp");__publicField(this,"maxHp");__publicField(this,"containedBy");this.hp=hp,this.maxHp=hp,this.breakthrough=breakthrough,this.ignoreMobile=ignoreMobile,this.applyBonuses(),this.updateTitle()}takeHit(ball){this.hp-=ball.damage,this.hp<=0&&this.destroy(!this.permanent)}destroy(forReal=!0){this.element.classList.add("brick--destroyed"),this.destroyed=!0,this.active=!1;const event=createEvent("brickdestroyed",this);this.parent.element.dispatchEvent(event),forReal&&super.destroy()}restore(){this.element.classList.remove("brick--destroyed"),this.destroyed=!1,this.hp=this.maxHp}}class CompositeBrick extends Brick{constructor(config){var _a;super(config);__publicField(this,"hitboxParts");this.hitboxParts=(_a=config.hitboxParts)==null?void 0:_a.map((part,idx)=>new Brick({...part,game:config.game,parent:config.parent,elementId:`${config.elementId}-p${idx}`}))}get compositeBoundingBox(){const allPoints=(this.hitboxParts??[this]).reduce((acc,part)=>{const partBox=part.boundingBox;return{x:[...acc.x,partBox.topL.x,partBox.topR.x,partBox.bottomL.x,partBox.bottomR.x],y:[...acc.y,partBox.topL.y,partBox.topR.y,partBox.bottomL.y,partBox.bottomR.y]}},{x:[],y:[]});return{topL:{x:Math.min(...allPoints.x),y:Math.min(...allPoints.y)},topR:{x:Math.max(...allPoints.x),y:Math.min(...allPoints.y)},bottomL:{x:Math.min(...allPoints.x),y:Math.max(...allPoints.y)},bottomR:{x:Math.max(...allPoints.x),y:Math.max(...allPoints.y)}}}updateElement(){var _a;super.updateElement(),(_a=this.hitboxParts)==null||_a.forEach(part=>{var _a2;return(_a2=part.updateElement)==null?void 0:_a2.call(part)})}updateElementPosition(){var _a;super.updateElementPosition(),(_a=this.hitboxParts)==null||_a.forEach(part=>{var _a2;return(_a2=part.updateElementPosition)==null?void 0:_a2.call(part)})}updateElementSize(){var _a;super.updateElementSize(),(_a=this.hitboxParts)==null||_a.forEach(part=>{var _a2;return(_a2=part.updateElementSize)==null?void 0:_a2.call(part)})}updateTitle(){var _a;super.updateTitle(),(_a=this.hitboxParts)==null||_a.forEach(part=>{var _a2;return(_a2=part.updateTitle)==null?void 0:_a2.call(part)})}takeHit(ball){var _a;(_a=this.hitboxParts)==null||_a.forEach(part=>part.takeHit(ball)),super.takeHit(ball)}destroy(forReal=!0){var _a;(_a=this.hitboxParts)==null||_a.forEach(part=>part.destroy(forReal)),super.destroy(forReal)}restore(){var _a;(_a=this.hitboxParts)==null||_a.forEach(part=>part.restore()),super.restore()}toString(){return`${super.toString()} ${this.hp}/${this.maxHp} HP`}}class Debug extends GameObject{constructor({elementId="debug",x=50,y=5,...rest}){super({elementId,x,y,...rest})}updateElementPosition(){}setContent(content){let fixed=content;content.includes(` `)||(fixed+=` &nbsp;`),super.setContent(fixed)}}const DEFAULT_OPTIONS={fps:60,capFps:!1,allowDebug:!1,nextLifeDelayMs:500,mouseoutPauseDelayMs:1e3,mouseoverResumeDelayMs:1e3,showCursorInPlay:!1,demoMode:!1,updatesPerFrame:1,skipDefaultRules:!1,columnAspectRatio:1.618},PAUSABLE=["playing","debug"],RESUMABLE=["paused","away"];class Game{constructor(params){__publicField(this,"element");__publicField(this,"sizes",{width:0,height:0});__publicField(this,"state","starting");__publicField(this,"debounceTimer");__publicField(this,"ogParams");__publicField(this,"debug");__publicField(this,"lastFrameTime",performance.now());__publicField(this,"lastFpsUpdate",performance.now());__publicField(this,"options");__publicField(this,"_speed",1);__publicField(this,"fpsInterval");__publicField(this,"fpsCap");__publicField(this,"msSinceStart",0);__publicField(this,"balls",[]);__publicField(this,"level");__publicField(this,"paddle");__publicField(this,"hud");__publicField(this,"controls");__publicField(this,"lives",0);__publicField(this,"score",0);__publicField(this,"paused");__publicField(this,"resumeLink");__publicField(this,"setBalls",()=>{this.balls.filter(b=>b.active).forEach(ball=>ball.destroy()),this.balls=this.balls.filter(b=>!b.active),this.ogParams.ballConfigs.forEach((ballConfig,idx)=>{const ball=new Ball({...ballConfig,idx,game:this,parent:this.level});this.balls.push(ball)})});__publicField(this,"debounce",(func,timeout=500)=>()=>{clearTimeout(this.debounceTimer),this.debounceTimer=setTimeout(()=>{func.apply(this)},timeout)});__publicField(this,"start",()=>{this.element.classList.add("paused"),this.createdPausedElement("Start"),this.dispatchGameEvent("gamestarted")});__publicField(this,"setOverallSpeed",speed=>{this.speed=speed});__publicField(this,"update",()=>{const now=performance.now(),msSinceLastFrame=now-this.lastFrameTime;if(PAUSABLE.includes(this.state)){if(msSinceLastFrame>=this.fpsCap){const speed=this._speed||1,virtualMsSinceLastFrame=msSinceLastFrame*speed;if(this.msSinceStart+=virtualMsSinceLastFrame,this.updateHUDTime(),this.lastFrameTime=now,this.debug&&now>this.lastFpsUpdate+1e3){const fps=1+Math.round(1e3/msSinceLastFrame);this.debug.setContent(`${this.options.demoMode?"demo":this.state} ${fps.toFixed(0)}fps`),this.lastFpsUpdate=now}const frameFraction=virtualMsSinceLastFrame/(this.fpsInterval*this.options.updatesPerFrame*speed);for(let i=0;i<Math.ceil(this.options.updatesPerFrame*speed);i++){this.paddle.processFrame(frameFraction),this.level.mobileBricks.forEach(brick=>brick.processFrame(frameFraction));for(const ball of this.balls)ball.destroyed||ball.processFrame(frameFraction,this.level,this.paddle)}this.paddle.updateElementPosition(),this.level.mobileBricks.forEach(brick=>brick.updateElementPosition());for(const ball of this.balls)if(!ball.destroyed&&(ball.updateElementPosition(),this.debug&&ball.y>this.paddle.maxY-this.paddle.height&&ball.y<this.paddle.maxY)){const semiR=Math.round(ball.x-this.paddle.width/2+Math.random()*this.paddle.width/2);this.paddle.handleMove(semiR,this.paddle.maxY??this.paddle.y)}}else this.debug&&console.info("skipping frame",msSinceLastFrame,this.fpsCap);requestAnimationFrame(()=>this.update())}});__publicField(this,"handleBallLost",()=>{this.balls=this.balls.filter(ball=>!ball.destroyed),this.balls.filter(b=>b.active).length===0&&(this.lives--,this.lives>=0?(this.updateHUDLives(),setTimeout(()=>{this.setBalls()},this.options.nextLifeDelayMs||20)):(this.state="lost",this.createdPausedElement("Game Over","final"),this.dispatchGameEvent("gamelost")))});__publicField(this,"handleBrickDestroyed",()=>{this.level.isDone()&&this.win()});__publicField(this,"win",()=>{this.state="won",this.createdPausedElement("Victory!","final"),this.dispatchGameEvent("gamewon")});__publicField(this,"updateHUDLives",()=>{var _a;(_a=this.hud)==null||_a.updateLives(this.lives)});__publicField(this,"updateHUDScore",()=>{var _a;(_a=this.hud)==null||_a.updateScore(this.score)});__publicField(this,"updateHUDTime",()=>{var _a;(_a=this.hud)==null||_a.updateTime(this.msSinceStart)});__publicField(this,"toggleDebug",()=>{this.options.allowDebug&&(this.debug?(this.debug.destroy(),this.debug=null,this.state==="debug"&&(this.state="playing")):(this.debug=new Debug({game:this}),this.state==="playing"&&(this.state="debug"),this.debug.setContent(this.options.demoMode?"demo":this.state),this.debug.updateElement()))});__publicField(this,"toggleFullscreen",async()=>{document.fullscreenElement?await document.exitFullscreen():this.element.requestFullscreen&&await this.element.requestFullscreen(),this.handleResize()});__publicField(this,"togglePause",()=>{this.paused?this.resume():this.pause()});__publicField(this,"updateSizes",(callResize=!1)=>{const{width,height}=this.element.getBoundingClientRect(),isColumn=this.element.classList.contains("column");let hasChanged=!1;return!isColumn&&width/height<this.options.columnAspectRatio?(this.element.classList.add("column"),hasChanged=!0):isColumn&&width/height>=this.options.columnAspectRatio&&(this.element.classList.remove("column"),hasChanged=!0),this.sizes.width=width,this.sizes.height=height,callResize&&this.handleResize(),hasChanged});__publicField(this,"handleResize",()=>{this.debounce(()=>{const layoutHasChanged=this.updateSizes(),updateOthers=()=>{var _a,_b,_c,_d,_e;this.level.updateSizes(),(_a=this.paused)==null||_a.updateSizes(),(_b=this.resumeLink)==null||_b.updateElement(),(_c=this.debug)==null||_c.updateElement(),(_d=this.controls)==null||_d.updateSizes(),(_e=this.hud)==null||_e.updateSizes(),this.paddle.updateElement(),this.balls.forEach(ball=>ball.updateElement())};updateOthers(),layoutHasChanged&&setTimeout(()=>{updateOthers()},1e3)})()});__publicField(this,"handleVisibilityChange",()=>{document.hidden?this.debounce(()=>{this.pause("away")})():this.debounce(()=>this.resume("away"),1e3)()});__publicField(this,"handleKeyPress",e=>{switch(e.code){case"KeyP":case"Space":this.state==="starting"?this.resume("starting"):this.togglePause(),e.preventDefault(),e.stopPropagation();break;case"KeyD":this.toggleDebug();break;case"KeyF":this.toggleFullscreen();break}});__publicField(this,"handleMouseEnter",()=>{this.debounce(()=>this.resume("away"),this.options.mouseoverResumeDelayMs)()});__publicField(this,"handleMouseLeave",()=>{this.options.mouseoutPauseDelayMs&&this.debounce(()=>this.pause("away"),this.options.mouseoutPauseDelayMs)()});__publicField(this,"createdPausedElement",(content,classes="")=>{var _a,_b;(_a=this.resumeLink)==null||_a.destroy(),(_b=this.paused)==null||_b.destroy(),this.paused=new Pause({game:this,parent:this.level,className:classes}),this.resumeLink=new Clickable({game:this,parent:this.paused,className:"resume-link",onClick:()=>this.resume(this.state==="starting"?"starting":void 0)}),this.resumeLink.setContent(content),this.resumeLink.updateElementPosition(),this.element.classList.add("paused")});__publicField(this,"pause",to=>{var _a;PAUSABLE.includes(this.state)&&(this.createdPausedElement(to==="away"?"Away":"Resume"),this.state=to??"paused",(_a=this.debug)==null||_a.setContent(this.state),this.balls.forEach(ball=>ball.updateTitle()),this.paddle.updateTitle(),this.level.bricks.forEach(brick=>brick.updateTitle()),this.dispatchGameEvent("gamepaused"))});__publicField(this,"resume",from=>{var _a;(from?from===this.state:RESUMABLE.includes(this.state))&&((_a=this.paused)==null||_a.destroy(),this.paused=null,this.state=this.debug?"debug":"playing",this.element.classList.remove("paused"),this.lastFrameTime=performance.now(),this.dispatchGameEvent("gameresumed"),this.update())});__publicField(this,"dispatchGameEvent",name=>{const event=createEvent(name,this);this.element.dispatchEvent(event)});__publicField(this,"destroy",()=>{var _a,_b,_c,_d,_e;document.removeEventListener("visibilitychange",this.handleVisibilityChange),document.removeEventListener("keyup",this.handleKeyPress),this.element.removeEventListener("balldestroyed",this.handleBallLost),this.element.removeEventListener("brickdestroyed",this.handleBrickDestroyed),this.element.removeEventListener("mouseenter",this.handleMouseEnter),this.element.removeEventListener("mouseleave",this.handleMouseLeave),this.paddle.destroy(),this.level.destroy(),(_a=this.hud)==null||_a.destroy(),(_b=this.controls)==null||_b.destroy(),this.state="lost",this.lives=0,this.balls.forEach(ball=>ball.destroy()),(_c=this.debug)==null||_c.destroy(),(_d=this.resumeLink)==null||_d.destroy(),(_e=this.paused)==null||_e.destroy()});var _a,_b,_c;this.ogParams={...params},this.options={...DEFAULT_OPTIONS,...params.options},this.element=document.getElementById(params.parentId??"game"),this.element.classList.add("game"),(_a=params.options)!=null&&_a.showCursorInPlay||this.element.classList.add("hide-cursor"),this.level=new Level({...params.levelConfig,game:this,onLevelMounted:()=>{params.playerConfig&&(this.lives=params.playerConfig.lives,this.score=params.playerConfig.score??0),this.setBalls(),this.updateSizes(!0),this.start()}}),this.paddle=new Paddle({...params.paddleConfig,game:this,parent:this.level,elementId:"paddle",x:((_b=params.paddleConfig)==null?void 0:_b.x)??50,y:((_c=params.paddleConfig)==null?void 0:_c.y)??83}),this.debug=null,this.paused=null,this.resumeLink=null,this.controls=new Controls({game:this,handleFullscreen:()=>this.toggleFullscreen(),handlePause:()=>this.togglePause(),handleDebug:this.options.allowDebug?()=>this.toggleDebug():void 0}),this.controls.updateElementPosition(),this.hud=new HUD({game:this}),this.updateHUDLives(),this.updateHUDScore(),this.updateHUDTime(),document.addEventListener("visibilitychange",this.handleVisibilityChange),this.options.skipDefaultRules||(this.element.addEventListener("balldestroyed",this.handleBallLost),this.element.addEventListener("brickdestroyed",this.handleBrickDestroyed)),this.element.addEventListener("mouseenter",this.handleMouseEnter),this.element.addEventListener("mouseleave",this.handleMouseLeave),ResizeObserver&&new ResizeObserver(this.handleResize).observe(this.element),this.options.demoMode?this.element.classList.add("demo"):document.addEventListener("keyup",this.handleKeyPress),this.fpsInterval=Math.floor(1e3/(this.options.fps||60))||1,this.fpsCap=this.options.capFps?this.fpsInterval:1}get speed(){return this._speed??1}set speed(speed){this._speed=Math.max(1/1e3,speed),this.element.style.setProperty("--game-speed",`${this._speed}`)}}class HUD extends GameObject{constructor({elementId="hud",x=0,y=0,...rest}){super({elementId,x,y,...rest});__publicField(this,"lives");__publicField(this,"time");__publicField(this,"score");__publicField(this,"sizes",{width:0,height:0});this.lives=new GameObject({game:this.game,parent:this,elementId:"lives",x:0,y:0}),this.time=new GameObject({game:this.game,parent:this,elementId:"time",x:0,y:0}),this.score=new GameObject({game:this.game,parent:this,elementId:"score",x:0,y:0})}updateLives(lives){this.lives.element.textContent=`🀍${lives}`}updateScore(score){this.score.element.textContent="πŸ’Ž"+score.toString()}updateTime(ms){this.time.element.textContent="⏳"+msToString(ms)}updateElementPositions(){super.updateElementPosition(),this.lives.updateElementPosition(),this.score.updateElementPosition(),this.time.updateElementPosition()}updateSizes(){this.sizes.width=this.element.offsetWidth,this.sizes.height=this.element.offsetHeight,this.updateElement()}destroy(){this.lives.destroy(),this.time.destroy(),this.score.destroy(),super.destroy()}}class Level{constructor({divisionFactor,enableContainment,layout,game,onLevelMounted}){__publicField(this,"element");__publicField(this,"game");__publicField(this,"brickMap");__publicField(this,"bricks");__publicField(this,"mobileBricks");__publicField(this,"_divisionFactor");__publicField(this,"_hitZones");__publicField(this,"fx",1);__publicField(this,"fy",1);__publicField(this,"sizes",{width:0,height:0});__publicField(this,"particles",[]);__publicField(this,"totalParticles",0);__publicField(this,"onLevelMounted");__publicField(this,"brickCanCollide",brick=>{var _a;return!brick.containedBy||((_a=this.brickMap[brick.containedBy])==null?void 0:_a.destroyed)});this.bricks=[],this.mobileBricks=[],this._hitZones=[],this.game=game,this.onLevelMounted=onLevelMounted,this._divisionFactor=divisionFactor??10;const exisitingLevel=this.game.element.getElementsByClassName("level")[0],frag=document.createDocumentFragment();exisitingLevel?this.element=exisitingLevel:(this.element=document.createElement("div"),this.element.classList.add("level")),frag.appendChild(this.element),layout instanceof Array?layout.forEach(l=>this.layBricks(l,this.game)):this.layBricks(layout,this.game);for(let divRow=0;divRow<this._divisionFactor;divRow++){this._hitZones.push([]);for(let divCol=0;divCol<this._divisionFactor;divCol++)this._hitZones[divRow].push([])}this.brickMap={},this.bricks.forEach(brick=>{var _a;if(brick.updateBoundingBox(),this.brickMap[brick.element.id]=brick,brick.speed||(_a=brick.hitboxParts)!=null&&_a.some(p=>p.speed))this.mobileBricks.push(brick);else{const cbb=brick.compositeBoundingBox;if(enableContainment){let smallestContainer;this.bricks.forEach(outerBrick=>{if(outerBrick!==brick&&outerBrick.area>brick.area&&(!smallestContainer||outerBrick.area<smallestContainer.area)){const outerCbb=outerBrick.compositeBoundingBox;cbb.topL.x-.1>outerCbb.topL.x&&cbb.topL.y-.1>outerCbb.topL.y&&cbb.bottomR.x+.1<outerCbb.bottomR.x&&cbb.bottomR.y+.1<outerCbb.bottomR.y&&(smallestContainer=outerBrick)}}),smallestContainer!==void 0&&(brick.containedBy=smallestContainer.element.id)}for(let divRow=0;divRow<this._divisionFactor;divRow++)for(let divCol=0;divCol<this._divisionFactor;divCol++){const x=divCol*(100/this._divisionFactor),y=divRow*(100/this._divisionFactor);cbb.topL.x<x+100/this._divisionFactor&&cbb.topR.x>x&&cbb.topL.y<y+100/this._divisionFactor&&cbb.bottomL.y>y&&this._hitZones[divRow][divCol].push(brick)}}}),requestAnimationFrame(()=>{var _a;this.game.element.appendChild(frag),(_a=this.onLevelMounted)==null||_a.call(this)})}getNearbyBricks(ball){const res=new Set,minDivRow=Math.max(0,Math.floor((ball.y-ball.radius)/(100/this._divisionFactor))),maxDivRow=Math.min(this._divisionFactor-1,Math.floor((ball.y+ball.radius)/(100/this._divisionFactor))),minDivCol=Math.max(0,Math.floor((ball.x-ball.radius)/(100/this._divisionFactor))),maxDivCol=Math.min(this._divisionFactor-1,Math.floor((ball.x+ball.radius)/(100/this._divisionFactor)));for(let divRow=minDivRow;divRow<=maxDivRow;divRow++)for(let divCol=minDivCol;divCol<=maxDivCol;divCol++)this._hitZones[divRow][divCol].filter(this.brickCanCollide).forEach(brick=>res.add(brick));return this.mobileBricks.forEach(brick=>res.add(brick)),Array.from(res)}updateElements(){this.bricks.forEach(brick=>{brick.updateElement()})}updateSizes(){this.sizes.width=this.element.offsetWidth,this.sizes.height=this.element.offsetHeight,this.updateSpeedRatios(),this.updateElements()}updateSpeedRatios(){const hypo=pythagoras(this.sizes.width,this.sizes.height);this.fx=this.sizes.width/hypo,this.fy=this.sizes.height/hypo}layBricks(layout,game){if(layout instanceof Array&&layout.forEach(layout2=>this.layBricks(layout2,game)),layout.type==="even"){const{y,height,rows,cols,hp=1}=layout;for(let i=0;i<rows;i++){const width=100/cols;for(let j=0;j<cols;j++)this.bricks.push(new CompositeBrick({game,parent:this,width,height,x:width*(j+.5),y:y+height*i,hp,elementId:`brick-${this.bricks.length}`}))}}else layout.type==="custom"&&layout.bricks.forEach(brick=>{this.bricks.push(new CompositeBrick({...brick,game,parent:this,elementId:brick.elementId??`brick-${this.bricks.length}`}))})}recycleParticle(particle){return()=>{particle.className="particle",particle.style.cssText="",particle.style.opacity="0",particle.innerHTML="",this.particles.push(particle)}}getParticleElement(recycleCondition="animationend"){let nextParticle=this.particles.pop();nextParticle||(nextParticle=document.createElement("particle"),nextParticle.classList.add("particle"),this.totalParticles++,nextParticle.id=`${this.element.id}-particle-${this.totalParticles}`),this.element.appendChild(nextParticle);const recycler=this.recycleParticle(nextParticle);return typeof recycleCondition=="number"?setTimeout(recycler,recycleCondition):nextParticle.addEventListener(recycleCondition,recycler,{once:!0}),nextParticle}isDone(){return!this.bricks.some(b=>!b.permanent&&!b.destroyed)}destroy(){this.bricks.forEach(brick=>brick.destroy()),this.particles.forEach(particle=>particle.remove())}}class Paddle extends MovingGameObject{constructor({angle,angleLimit,curveFactor,gripFactor,minY,maxY,...config}){var _a;super({...config,className:[...((_a=config.className)==null?void 0:_a.split(" "))??[],"paddle"].join(" "),showTitle:!0});__publicField(this,"curveFactor",0);__publicField(this,"gripFactor",.05);__publicField(this,"minY");__publicField(this,"maxY");__publicField(this,"cursorX");__publicField(this,"cursorY");__publicField(this,"angleLimit",0);__publicField(this,"vtBound",!0);__publicField(this,"handleMouseMove",({clientX,clientY,currentTarget})=>{if(this.game.state==="playing"&&currentTarget instanceof HTMLElement){const rect=currentTarget.getBoundingClientRect(),mouseX=clientX-rect.left,mouseY=clientY-rect.top;this.handleClientMove(mouseX,mouseY)}});__publicField(this,"handleTouchMove",e=>{if(this.game.state!=="playing")return;const touch=e.touches[0];this.handleClientMove(touch.clientX,touch.clientY)});__publicField(this,"handleClientMove",(x,y)=>{const normX=x/this.parent.sizes.width*100,normY=y/this.parent.sizes.height*100;this.handleMove(normX,normY)});__publicField(this,"handleMove",(x,y)=>{const targetPaddleX=x;let targetPaddleY=this.y;if(this.vtBound||(targetPaddleY=clamp(y,this.maxY,this.minY)),!this.speed)this.updatePosition(targetPaddleX,targetPaddleY);else{const movement={speed:this.speed,angle:-Math.atan2(targetPaddleY-this.y,targetPaddleX-this.x)};let verifyX=mgo=>mgo.x>=targetPaddleX;this.x>targetPaddleX&&(verifyX=mgo=>mgo.x<=targetPaddleX);let verifyY=mgo=>mgo.y>=targetPaddleY;this.y>targetPaddleY&&(verifyY=mgo=>mgo.y<=targetPaddleY),this.movement=[{condition:mgo=>(verifyX(mgo)&&verifyY(mgo)&&(this.active=!1,this.x=targetPaddleX,this.y=targetPaddleY),!this.active),movement:{...movement}}],this.active=!0}if(this.angleLimit!==0){const dx=x-this.cursorX,dy=y-this.cursorY,setAngle=()=>{let dAngle=Math.atan2(dy,dx);dx<0&&(dAngle>0?dAngle-=Math.PI:dAngle+=Math.PI),dAngle*=-1,dAngle=(dAngle+Math.PI/2)%Math.PI-Math.PI/2;const angle=1*(dAngle/(Math.PI/2))*this.angleLimit,currentAngle=this.angle??0;this.angle=currentAngle-angle/10,this.cursorX=x,this.cursorY=y};Math.abs(dx)>2&&Math.abs(dy)>0&&setAngle()}});curveFactor!==void 0&&(this.curveFactor=curveFactor),gripFactor!==void 0&&(this.gripFactor=gripFactor),this.angleLimit=angleLimit??0,this.angle=angle??0,this.minY=minY??this.y,this.maxY=maxY??this.y,this.cursorX=this.x,this.cursorY=this.y,this.maxY!==this.minY&&(this.vtBound=!1),this.applyBonuses(),this.updateTitle(),this.parent.element.addEventListener("touchmove",this.handleTouchMove,{passive:!0}),this.parent.element.addEventListener("mousemove",this.handleMouseMove)}set angle(angle){const newAngle=clamp(angle,this.angleLimit,-this.angleLimit);super.angle=newAngle}get angle(){return super.angle}updateElementSize(){if(!this.curveFactor)return super.updateElementSize();const{width,height}=this.parent.sizes;if(this.width&&(this.element.style.width=`${Math.round(width*(this.width/100))}px`),this.height){this.element.style.height=`${1.1*height*(this.height/100)}px`;const radiusStr=`100% ${this.curveFactor*100}%`;this.element.style.borderTopLeftRadius=radiusStr,this.element.style.borderTopRightRadius=radiusStr}}destroy(){this.parent.element.removeEventListener("mousemove",this.handleMouseMove),this.parent.element.removeEventListener("touchmove",this.handleTouchMove),super.destroy()}}class Pause extends GameObject{constructor({elementId="pause",x=50,y=50,...rest}){super({elementId,x,y,...rest});__publicField(this,"sizes",{width:0,height:0})}updateSizes(){this.sizes.width=this.element.offsetWidth,this.sizes.height=this.element.offsetHeight,this.updateElement()}}const version="0.19.1";exports2.Ball=Ball,exports2.Brick=Brick,exports2.Clickable=Clickable,exports2.CompositeBrick=CompositeBrick,exports2.Controls=Controls,exports2.Debug=Debug,exports2.Game=Game,exports2.GameObject=GameObject,exports2.HUD=HUD,exports2.Level=Level,exports2.MovingGameObject=MovingGameObject,exports2.Paddle=Paddle,exports2.Pause=Pause,exports2.version=version,Object.defineProperty(exports2,Symbol.toStringTag,{value:"Module"})});