@maximeij/css-brickout
Version:
Classic Brickout Game Engine implemented in Typescript and rendered with CSS. No dependencies.
1,072 lines (1,071 loc) β’ 51.8 kB
JavaScript
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
// rest is used to allow for any other properties to be added to the object
}) {
__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");
// for circle shapes
__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, args = [this.x, this.y, this.angle];
this.boundingBox = {
bottomR: rotatePoint(this.x + halfWidth, this.y + halfHeight, ...args),
bottomL: rotatePoint(this.x - halfWidth, this.y + halfHeight, ...args),
topL: rotatePoint(this.x - halfWidth, this.y - halfHeight, ...args),
topR: rotatePoint(this.x + halfWidth, this.y - halfHeight, ...args)
};
}
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);
// Prevents the ball from hitting the same object twice in a row
__publicField(this, "antiJuggling", !1);
this.radius = radius, this.damage = damage, this.applyBonuses(), this.updateElementSize(), this.updateTitle();
}
// Used for cosmetics, can move to MovingGameObject
setD() {
super.setD(), this.element.style.setProperty("--dx", this.dx + "px"), this.element.style.setProperty("--dy", this.dy + "px");
}
/**
* Detects collisions between this ball and the boundaries, level bricks, and paddle (in that order)
* @param level The level with the bricks and strips
* @param paddle The player's paddle
* @returns true if the position has been updated already
*/
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}` })
);
}
// Always axis-aligned, contains all parts of the hitbox
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 += `
`), 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
// golden ratio
}, PAUSABLE = ["playing", "debug"], RESUMABLE = ["paused", "away"];
class Game {
constructor(params) {
// Internals
__publicField(this, "element");
__publicField(this, "sizes", { width: 0, height: 0 });
__publicField(this, "state", "starting");
__publicField(this, "debounceTimer");
__publicField(this, "ogParams");
// Debug
__publicField(this, "debug");
__publicField(this, "lastFrameTime", performance.now());
__publicField(this, "lastFpsUpdate", performance.now());
// Gameplay
__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);
// Pause
__publicField(this, "paused");
__publicField(this, "resumeLink");
// Create Ball objects based on ballConfig
__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");
});
/**
* @deprecated Set speed instead
*/
__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 - 0.1 > outerCbb.topL.x && cbb.topL.y - 0.1 > outerCbb.topL.y && cbb.bottomR.x + 0.1 < outerCbb.bottomR.x && cbb.bottomR.y + 0.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 = /* @__PURE__ */ 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 + 0.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);
};
}
// pops a particle from the particle pool, creates one if none exist
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 });
// How much ball angle is modified when it hits the paddle further from the center. 1 = pi/2 at the edge
__publicField(this, "curveFactor", 0);
// unused for now
__publicField(this, "gripFactor", 0.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.mi