breakout71/dist/index.html

3606 lines
188 KiB
HTML
Raw Normal View History

2025-03-15 21:29:38 +01:00
<!doctype html>
<html lang="en">
<head><script src="/index.c0fd3053.js"></script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Breakout 71</title>
<meta name="description" content="A breakout game with roguelite mechanics. Break bricks, catch coins, pick upgrades, repeat. Play for free on mobile and desktop.">
<link rel="manifest" href="/manifest.webmanifest">
<style>* {
box-sizing: border-box;
font-family: Courier New, Courier, Lucida Sans Typewriter, Lucida Typewriter, monospace;
}
body {
width: 100vw;
height: 100vh;
height: calc(var(--vh, 1vh) * 100);
color: #fff;
background-size: 120px 120px;
background-color: var(--background1);
--background1: #030c23;
--background2: #03112a;
margin: 0;
padding: 0;
overflow: hidden;
}
#game {
height: 100vh;
height: calc(var(--vh, 1vh) * 100);
width: 100vw;
position: absolute;
top: 0;
left: 0;
}
#score, #menu {
z-index: 1;
appearance: none;
font: inherit;
color: #fff;
background: none;
border: none;
min-width: 40px;
min-height: 40px;
padding: 10px;
line-height: 20px;
position: absolute;
top: 0;
}
#score:hover, #menu:hover, #score:focus, #menu:focus {
cursor: pointer;
background: #0000004d;
}
#score {
color: #ffffff4d;
transition: color .3s;
right: 0;
}
#score.active {
color: gold;
transition: color 10ms;
}
#menu {
left: 0;
}
.popup {
z-index: 10;
background: #000000e6;
display: flex;
position: fixed;
inset: 0;
overflow: auto;
}
.popup > div {
transform-origin: center;
flex-direction: column;
align-items: stretch;
width: 100%;
max-width: 450px;
margin: auto;
padding: 20px 10px;
display: flex;
}
.popup > div > * {
margin: 0;
padding: 0;
}
.popup > div > h2, .popup > div > p {
margin-bottom: 20px;
}
.popup > div > section {
flex-direction: column;
align-items: stretch;
margin-top: 20px;
display: flex;
}
.popup > div > section button {
font: inherit;
color: #fff;
cursor: pointer;
text-align: left;
background: #000c;
border: 1px solid #fff;
gap: 10px;
margin-top: -1px;
padding: 10px;
display: flex;
}
.popup > div > section button:not([disabled]):hover, .popup > div > section button:not([disabled]):focus {
z-index: 1;
border-color: #f1d33b;
position: relative;
}
.popup > div > section button[disabled] {
opacity: .5;
filter: saturate(0);
pointer-events: none;
}
.popup > div > section button > div {
flex-grow: 1;
}
.popup > div > section button > div > em {
opacity: .8;
display: block;
}
.popup > div > section button.grey-out-unless-hovered:not(:hover) {
opacity: .6;
}
.popup > div > section button.grey-out-unless-hovered:not(:hover) img {
filter: saturate(0);
}
.popup.actionsAsGrid > div {
max-width: 800px;
}
.popup.actionsAsGrid > div section {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
display: grid;
}
.popup button.close-modale {
color: #fff;
cursor: pointer;
background: none;
border: none;
width: 60px;
height: 60px;
position: absolute;
top: 0;
right: 0;
overflow: hidden;
}
.popup button.close-modale:before {
content: "+";
font-size: 80px;
display: inline-block;
position: absolute;
top: 34px;
left: 26px;
transform: translate(-50%, -50%)rotate(45deg);
}
.popup button.close-modale:hover {
background: #000;
font-weight: bold;
}
.popup .textAfterButtons {
color: #ffffff94;
}
.popup a[href] {
color: inherit;
}
.popup a[href]:hover, .popup a[href]:focus {
color: #fff;
}
.progress {
color: #fff;
text-align: center;
background: #1c1c2f;
border-radius: 5px;
padding: 5px 10px;
display: block;
position: relative;
overflow: hidden;
box-shadow: inset 3px 3px 5px #00000080;
}
.progress > .progress_bar_part {
transform-origin: 0 0;
z-index: 1;
background: #4049ca;
animation: 1s ease-out both grow;
display: block;
position: absolute;
inset: 0;
box-shadow: inset 3px 3px 5px #00000080;
}
.progress > span {
z-index: 2;
display: block;
position: relative;
}
@keyframes grow {
0% {
transform: scale(0, 1);
}
}
#level-recording-container {
text-align: center;
max-width: 400px;
margin: 40px;
}
#level-recording-container video {
max-width: 100%;
height: auto;
}
#level-recording-container a {
display: block;
}
#level-recording-container a video {
border-radius: 10px;
outline: 1px solid #fff;
margin: 20px auto;
display: block;
box-shadow: 2px 2px 5px #000;
}
@media (width >= 1200px) {
#level-recording-container {
max-width: calc(50vw - 305px);
position: absolute;
top: 40px;
left: 40px;
}
}
.histogram {
align-items: stretch;
gap: 10px;
margin: 10px 0 40px;
display: flex;
}
.histogram > span {
flex-direction: column;
flex-grow: 1;
justify-content: flex-end;
width: 10px;
display: flex;
position: relative;
}
.histogram > span.active > span {
background: #4049ca;
}
.histogram > span > span {
background: #1c1c2f;
border-radius: 5px;
width: 100%;
min-height: 1px;
display: block;
}
.histogram > span > span > span {
pointer-events: none;
white-space: nowrap;
transform-origin: 0 100%;
text-align: center;
font-size: 13px;
display: block;
position: absolute;
bottom: -20px;
left: 50%;
transform: translate(-50%);
}
.histogram > span:not(:hover):not(.active) > span > span {
opacity: 0;
}
h2.histogram-title {
color: #3b3f75;
font-size: 18px;
}
h2.histogram-title strong {
color: #4049ca;
}
</style>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🕹️</text></svg>">
</head>
<body>
<button id="menu"><span id="menuLabel">menu</span></button>
<button id="score"></button>
<canvas id="game"></canvas>
<script>// modules are defined as an array
// [ module function, map of requires ]
//
// map of requires is short require name -> numeric require
//
// anything defined in a previous bundle is accessed via the
// orig method which is the require for previous bundles
(function (modules, entry, mainEntry, parcelRequireName, globalName) {
/* eslint-disable no-undef */
var globalObject =
typeof globalThis !== 'undefined'
? globalThis
: typeof self !== 'undefined'
? self
: typeof window !== 'undefined'
? window
: typeof global !== 'undefined'
? global
: {};
/* eslint-enable no-undef */
// Save the require from previous bundle to this closure if any
var previousRequire =
typeof globalObject[parcelRequireName] === 'function' &&
globalObject[parcelRequireName];
var cache = previousRequire.cache || {};
// Do not use `require` to prevent Webpack from trying to bundle this call
var nodeRequire =
typeof module !== 'undefined' &&
typeof module.require === 'function' &&
module.require.bind(module);
function newRequire(name, jumped) {
if (!cache[name]) {
if (!modules[name]) {
// if we cannot find the module within our internal map or
// cache jump to the current global require ie. the last bundle
// that was added to the page.
var currentRequire =
typeof globalObject[parcelRequireName] === 'function' &&
globalObject[parcelRequireName];
if (!jumped && currentRequire) {
return currentRequire(name, true);
}
// If there are other bundles on this page the require from the
// previous one is saved to 'previousRequire'. Repeat this as
// many times as there are bundles until the module is found or
// we exhaust the require chain.
if (previousRequire) {
return previousRequire(name, true);
}
// Try the node require function if it exists.
if (nodeRequire && typeof name === 'string') {
return nodeRequire(name);
}
var err = new Error("Cannot find module '" + name + "'");
err.code = 'MODULE_NOT_FOUND';
throw err;
}
localRequire.resolve = resolve;
localRequire.cache = {};
var module = (cache[name] = new newRequire.Module(name));
modules[name][0].call(
module.exports,
localRequire,
module,
module.exports,
globalObject
);
}
return cache[name].exports;
function localRequire(x) {
var res = localRequire.resolve(x);
return res === false ? {} : newRequire(res);
}
function resolve(x) {
var id = modules[name][1][x];
return id != null ? id : x;
}
}
function Module(moduleName) {
this.id = moduleName;
this.bundle = newRequire;
this.exports = {};
}
newRequire.isParcelRequire = true;
newRequire.Module = Module;
newRequire.modules = modules;
newRequire.cache = cache;
newRequire.parent = previousRequire;
newRequire.register = function (id, exports) {
modules[id] = [
function (require, module) {
module.exports = exports;
},
{},
];
};
Object.defineProperty(newRequire, 'root', {
get: function () {
return globalObject[parcelRequireName];
},
});
globalObject[parcelRequireName] = newRequire;
for (var i = 0; i < entry.length; i++) {
newRequire(entry[i]);
}
if (mainEntry) {
// Expose entry point to Node, AMD or browser globals
// Based on https://github.com/ForbesLindesay/umd/blob/master/template.js
var mainExports = newRequire(mainEntry);
// CommonJS
if (typeof exports === 'object' && typeof module !== 'undefined') {
module.exports = mainExports;
// RequireJS
} else if (typeof define === 'function' && define.amd) {
define(function () {
return mainExports;
});
// <script>
} else if (globalName) {
this[globalName] = mainExports;
}
}
})({"fxnks":[function(require,module,exports,__globalThis) {
require("3ab5c1708a8ff3cf")(require("3bec9e730869a0f0").getBundleURL('28aWT') + "index.c0fd3053.js");
},{"3ab5c1708a8ff3cf":"61B45","3bec9e730869a0f0":"lgJ39"}],"61B45":[function(require,module,exports,__globalThis) {
"use strict";
var cacheLoader = require("ca2a84f7fa4a3bb0");
module.exports = cacheLoader(function(bundle) {
return new Promise(function(resolve, reject) {
// Don't insert the same script twice (e.g. if it was already in the HTML)
var existingScripts = document.getElementsByTagName('script');
if ([].concat(existingScripts).some(function(script) {
return script.src === bundle;
})) {
resolve();
return;
}
var preloadLink = document.createElement('link');
preloadLink.href = bundle;
preloadLink.rel = 'preload';
preloadLink.as = 'script';
document.head.appendChild(preloadLink);
var script = document.createElement('script');
script.async = true;
script.type = 'text/javascript';
script.src = bundle;
script.onerror = function(e) {
var error = new TypeError("Failed to fetch dynamically imported module: ".concat(bundle, ". Error: ").concat(e.message));
script.onerror = script.onload = null;
script.remove();
reject(error);
};
script.onload = function() {
script.onerror = script.onload = null;
resolve();
};
document.getElementsByTagName('head')[0].appendChild(script);
});
});
},{"ca2a84f7fa4a3bb0":"j49pS"}],"j49pS":[function(require,module,exports,__globalThis) {
"use strict";
var cachedBundles = {};
var cachedPreloads = {};
var cachedPrefetches = {};
function getCache(type) {
switch(type){
case 'preload':
return cachedPreloads;
case 'prefetch':
return cachedPrefetches;
default:
return cachedBundles;
}
}
module.exports = function(loader, type) {
return function(bundle) {
var cache = getCache(type);
if (cache[bundle]) return cache[bundle];
return cache[bundle] = loader.apply(null, arguments).catch(function(e) {
delete cache[bundle];
throw e;
});
};
};
},{}],"lgJ39":[function(require,module,exports,__globalThis) {
"use strict";
var bundleURL = {};
function getBundleURLCached(id) {
var value = bundleURL[id];
if (!value) {
value = getBundleURL();
bundleURL[id] = value;
}
return value;
}
function getBundleURL() {
try {
throw new Error();
} catch (err) {
var matches = ('' + err.stack).match(/(https?|file|ftp|(chrome|moz|safari-web)-extension):\/\/[^)\n]+/g);
if (matches) // The first two stack frames will be this function and getBundleURLCached.
// Use the 3rd one, which will be a runtime in the original bundle.
return getBaseURL(matches[2]);
}
return '/';
}
function getBaseURL(url) {
return ('' + url).replace(/^((?:https?|file|ftp|(chrome|moz|safari-web)-extension):\/\/.+)\/[^/]+$/, '$1') + '/';
}
// TODO: Replace uses with `new URL(url).origin` when ie11 is no longer supported.
function getOrigin(url) {
var matches = ('' + url).match(/(https?|file|ftp|(chrome|moz|safari-web)-extension):\/\/[^/]+/);
if (!matches) throw new Error('Origin not found');
return matches[0];
}
exports.getBundleURL = getBundleURLCached;
exports.getBaseURL = getBaseURL;
exports.getOrigin = getOrigin;
},{}],"bCo5X":[function(require,module,exports,__globalThis) {
var _gameTs = require("./game.ts");
},{"./game.ts":"edeGs"}],"edeGs":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "play", ()=>play);
parcelHelpers.export(exports, "pause", ()=>pause);
parcelHelpers.export(exports, "fitSize", ()=>fitSize);
parcelHelpers.export(exports, "recomputeTargetBaseSpeed", ()=>recomputeTargetBaseSpeed);
parcelHelpers.export(exports, "brickCenterX", ()=>brickCenterX);
parcelHelpers.export(exports, "brickCenterY", ()=>brickCenterY);
parcelHelpers.export(exports, "getRowColIndex", ()=>getRowColIndex);
parcelHelpers.export(exports, "spawnExplosion", ()=>spawnExplosion);
parcelHelpers.export(exports, "addToScore", ()=>addToScore);
parcelHelpers.export(exports, "pickedUpgradesHTMl", ()=>pickedUpgradesHTMl);
parcelHelpers.export(exports, "setLevel", ()=>setLevel);
parcelHelpers.export(exports, "currentLevelInfo", ()=>currentLevelInfo);
parcelHelpers.export(exports, "getPossibleUpgrades", ()=>getPossibleUpgrades);
parcelHelpers.export(exports, "getUpgraderUnlockPoints", ()=>getUpgraderUnlockPoints);
parcelHelpers.export(exports, "dontOfferTooSoon", ()=>dontOfferTooSoon);
parcelHelpers.export(exports, "pickRandomUpgrades", ()=>pickRandomUpgrades);
parcelHelpers.export(exports, "setMousePos", ()=>setMousePos);
parcelHelpers.export(exports, "brickIndex", ()=>brickIndex);
parcelHelpers.export(exports, "hasBrick", ()=>hasBrick);
parcelHelpers.export(exports, "hitsSomething", ()=>hitsSomething);
parcelHelpers.export(exports, "shouldPierceByColor", ()=>shouldPierceByColor);
parcelHelpers.export(exports, "coinBrickHitCheck", ()=>coinBrickHitCheck);
parcelHelpers.export(exports, "bordersHitCheck", ()=>bordersHitCheck);
parcelHelpers.export(exports, "tick", ()=>tick);
parcelHelpers.export(exports, "isTelekinesisActive", ()=>isTelekinesisActive);
parcelHelpers.export(exports, "ballTick", ()=>ballTick);
parcelHelpers.export(exports, "getTotalScore", ()=>getTotalScore);
parcelHelpers.export(exports, "addToTotalScore", ()=>addToTotalScore);
parcelHelpers.export(exports, "addToTotalPlayTime", ()=>addToTotalPlayTime);
parcelHelpers.export(exports, "gameOver", ()=>gameOver);
parcelHelpers.export(exports, "getHistograms", ()=>getHistograms);
parcelHelpers.export(exports, "explodeBrick", ()=>explodeBrick);
parcelHelpers.export(exports, "max_levels", ()=>max_levels);
parcelHelpers.export(exports, "render", ()=>render);
parcelHelpers.export(exports, "renderAllBricks", ()=>renderAllBricks);
parcelHelpers.export(exports, "drawPuck", ()=>drawPuck);
parcelHelpers.export(exports, "drawBall", ()=>drawBall);
parcelHelpers.export(exports, "drawCoin", ()=>drawCoin);
parcelHelpers.export(exports, "drawFuzzyBall", ()=>drawFuzzyBall);
parcelHelpers.export(exports, "drawBrick", ()=>drawBrick);
parcelHelpers.export(exports, "roundRect", ()=>roundRect);
parcelHelpers.export(exports, "drawIMG", ()=>drawIMG);
parcelHelpers.export(exports, "drawText", ()=>drawText);
parcelHelpers.export(exports, "asyncAlert", ()=>asyncAlert);
2025-03-16 10:24:46 +01:00
parcelHelpers.export(exports, "confirmRestart", ()=>confirmRestart);
2025-03-15 21:29:38 +01:00
parcelHelpers.export(exports, "distance2", ()=>distance2);
parcelHelpers.export(exports, "distanceBetween", ()=>distanceBetween);
parcelHelpers.export(exports, "rainbowColor", ()=>rainbowColor);
parcelHelpers.export(exports, "repulse", ()=>repulse);
parcelHelpers.export(exports, "attract", ()=>attract);
parcelHelpers.export(exports, "recordOneFrame", ()=>recordOneFrame);
parcelHelpers.export(exports, "drawMainCanvasOnSmallCanvas", ()=>drawMainCanvasOnSmallCanvas);
parcelHelpers.export(exports, "startRecordingGame", ()=>startRecordingGame);
parcelHelpers.export(exports, "pauseRecording", ()=>pauseRecording);
parcelHelpers.export(exports, "resumeRecording", ()=>resumeRecording);
parcelHelpers.export(exports, "stopRecording", ()=>stopRecording);
parcelHelpers.export(exports, "captureFileName", ()=>captureFileName);
parcelHelpers.export(exports, "findLast", ()=>findLast);
parcelHelpers.export(exports, "toggleFullScreen", ()=>toggleFullScreen);
parcelHelpers.export(exports, "setKeyPressed", ()=>setKeyPressed);
parcelHelpers.export(exports, "newGameState", ()=>newGameState);
parcelHelpers.export(exports, "gameState", ()=>gameState);
parcelHelpers.export(exports, "restart", ()=>restart);
var _loadGameData = require("./loadGameData");
var _options = require("./options");
var _sounds = require("./sounds");
var _resetBalls = require("./resetBalls");
var _gameUtils = require("./game_utils");
var _combo = require("./combo");
var _swLoader = require("./sw_loader");
var _i18N = require("./i18n/i18n");
var _settings = require("./settings");
const gameCanvas = document.getElementById("game");
const ctx = gameCanvas.getContext("2d", {
alpha: false
});
const bombSVG = document.createElement("img");
bombSVG.src = "data:image/svg+xml;base64," + btoa(`<svg width="144" height="144" viewBox="0 0 38.101 38.099" xmlns="http://www.w3.org/2000/svg">
<path d="m6.1528 26.516c-2.6992-3.4942-2.9332-8.281-.58305-11.981a10.454 10.454 0 017.3701-4.7582c1.962-.27726 4.1646.05953 5.8835.90027l.45013.22017.89782-.87417c.83748-.81464.91169-.87499 1.0992-.90271.40528-.058713.58876.03425 1.1971.6116l.55451.52679 1.0821-1.0821c1.1963-1.1963 1.383-1.3357 2.1039-1.5877.57898-.20223 1.5681-.19816 2.1691.00897 1.4613.50314 2.3673 1.7622 2.3567 3.2773-.0058.95654-.24464 1.5795-.90924 2.3746-.40936.48928-.55533.81057-.57898 1.2737-.02039.41018.1109.77714.42322 1.1792.30172.38816.3694.61323.2797.93044-.12803.45666-.56674.71598-1.0242.60507-.601-.14597-1.3031-1.3088-1.3969-2.3126-.09459-1.0161.19245-1.8682.92392-2.7432.42567-.50885.5643-.82851.5643-1.3031 0-.50151-.14026-.83177-.51211-1.2028-.50966-.50966-1.0968-.64829-1.781-.41996l-.37348.12477-2.1006 2.1006.52597.55696c.45421.48194.5325.58876.57898.78855.09622.41588.07502.45014-.88396 1.4548l-.87173.9125.26339.57979a10.193 10.193 0 01.9231 4.1001c.03996 2.046-.41996 3.8082-1.4442 5.537-.55044.928-1.0185 1.5013-1.8968 2.3241-.83503.78284-1.5526 1.2827-2.4904 1.7361-3.4266 1.657-7.4721 1.3422-10.549-.82035-.73473-.51782-1.7312-1.4621-2.2515-2.1357zm21.869-4.5584c-.0579-.19734-.05871-2.2662 0-2.4545.11906-.39142.57898-.63361 1.0038-.53005.23812.05708.54147.32455.6116.5382.06279.19163.06769 2.1805.0065 2.3811-.12558.40773-.61649.67602-1.0462.57164-.234-.05708-.51615-.30498-.57568-.50722m3.0417-2.6013c-.12313-.6222.37837-1.1049 1.0479-1.0079.18348.0261.25279.08399 1.0071.83911.75838.75838.81301.82362.84074 1.0112.10193.68499-.40365 1.1938-1.034 1.0405-.1949-.0473-.28786-.12558-1.0144-.85216-.7649-.76409-.80241-.81057-.84645-1.0316m.61323-3.0629a.85623.85623 0 01.59284-.99975c.28949-.09214 2.1814-.08318 2.3917.01141.38734.17369.6279.61078.53984.98181-.06035.25606-.35391.57327-.60181.64992-.25279.07747-2.2278.053-2.4097-.03017-.26013-.11906-.46318-.36125-.51374-.61323" fill="#fff" opacity="0.3"/>
</svg>`);
function play() {
if (gameState.running) return;
gameState.running = true;
startRecordingGame();
(0, _sounds.getAudioContext)()?.resume();
resumeRecording();
document.body.className = gameState.running ? " running " : " paused ";
}
function pause(playerAskedForPause) {
if (!gameState.running) return;
if (gameState.pauseTimeout) return;
gameState.pauseTimeout = setTimeout(()=>{
gameState.running = false;
gameState.needsRender = true;
setTimeout(()=>{
if (!gameState.running) (0, _sounds.getAudioContext)()?.suspend();
}, 1000);
pauseRecording();
gameState.pauseTimeout = null;
document.body.className = gameState.running ? " running " : " paused ";
scoreDisplay.className = "";
}, Math.min(Math.max(0, gameState.pauseUsesDuringRun - 5) * 50, 500));
if (playerAskedForPause) // Pausing many times in a run will make pause slower
gameState.pauseUsesDuringRun++;
if (document.exitPointerLock) document.exitPointerLock();
}
const background = document.createElement("img");
const backgroundCanvas = document.createElement("canvas");
background.addEventListener("load", ()=>{
gameState.needsRender = true;
});
const fitSize = ()=>{
const { width, height } = gameCanvas.getBoundingClientRect();
gameState.canvasWidth = width;
gameState.canvasHeight = height;
gameCanvas.width = width;
gameCanvas.height = height;
ctx.fillStyle = currentLevelInfo()?.color || "black";
ctx.globalAlpha = 1;
ctx.fillRect(0, 0, width, height);
backgroundCanvas.width = width;
backgroundCanvas.height = height;
gameState.gameZoneHeight = (0, _options.isOptionOn)("mobile-mode") ? height * 80 / 100 : height;
const baseWidth = Math.round(Math.min(gameState.canvasWidth, gameState.gameZoneHeight * 0.73));
gameState.brickWidth = Math.floor(baseWidth / gameState.gridSize / 2) * 2;
gameState.gameZoneWidth = gameState.brickWidth * gameState.gridSize;
gameState.offsetX = Math.floor((gameState.canvasWidth - gameState.gameZoneWidth) / 2);
gameState.offsetXRoundedDown = gameState.offsetX;
if (gameState.offsetX < gameState.ballSize) gameState.offsetXRoundedDown = 0;
gameState.gameZoneWidthRoundedUp = width - 2 * gameState.offsetXRoundedDown;
backgroundCanvas.title = "resized";
// Ensure puck stays within bounds
setMousePos(gameState.puckPosition);
gameState.coins = [];
gameState.flashes = [];
pause(true);
(0, _resetBalls.putBallsAtPuck)(gameState);
// For safari mobile https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
document.documentElement.style.setProperty("--vh", `${window.innerHeight * 0.01}px`);
};
window.addEventListener("resize", fitSize);
window.addEventListener("fullscreenchange", fitSize);
setInterval(()=>{
// Sometimes, the page changes size without triggering the event (when switching to fullscreen, closing debug panel...)
const { width, height } = gameCanvas.getBoundingClientRect();
if (width !== gameState.canvasWidth || height !== gameState.canvasHeight) fitSize();
}, 1000);
function recomputeTargetBaseSpeed() {
// We never want the ball to completely stop, it will move at least 3px per frame
gameState.baseSpeed = Math.max(3, gameState.gameZoneWidth / 12 / 10 + gameState.currentLevel / 3 + gameState.levelTime / 30000 - gameState.perks.slow_down * 2);
}
function brickCenterX(index) {
return gameState.offsetX + (index % gameState.gridSize + 0.5) * gameState.brickWidth;
}
function brickCenterY(index) {
return (Math.floor(index / gameState.gridSize) + 0.5) * gameState.brickWidth;
}
function getRowColIndex(row, col) {
if (row < 0 || col < 0 || row >= gameState.gridSize || col >= gameState.gridSize) return -1;
return row * gameState.gridSize + col;
}
function spawnExplosion(count, x, y, color, duration = 150, size = gameState.coinSize) {
if (!!(0, _options.isOptionOn)("basic")) return;
if (gameState.flashes.length > gameState.MAX_PARTICLES) // Avoid freezing when lots of explosion happen at once
count = 1;
for(let i = 0; i < count; i++)gameState.flashes.push({
type: "particle",
time: gameState.levelTime,
size,
x: x + (Math.random() - 0.5) * gameState.brickWidth / 2,
y: y + (Math.random() - 0.5) * gameState.brickWidth / 2,
vx: (Math.random() - 0.5) * 30,
vy: (Math.random() - 0.5) * 30,
color,
duration,
ethereal: false
});
}
function addToScore(coin) {
coin.destroyed = true;
gameState.score += coin.points;
gameState.lastScoreIncrease = gameState.levelTime;
addToTotalScore(coin.points);
if (gameState.score > gameState.highScore && !gameState.isCreativeModeRun) {
gameState.highScore = gameState.score;
localStorage.setItem("breakout-3-hs", gameState.score.toString());
}
if (!(0, _options.isOptionOn)("basic")) gameState.flashes.push({
type: "particle",
duration: 100 + Math.random() * 50,
time: gameState.levelTime,
size: gameState.coinSize / 2,
color: coin.color,
x: coin.previousX,
y: coin.previousY,
vx: (gameState.canvasWidth - coin.x) / 100,
vy: -coin.y / 100,
ethereal: true
});
if (Date.now() - gameState.lastPlayedCoinGrab > 16) {
gameState.lastPlayedCoinGrab = Date.now();
(0, _sounds.sounds).coinCatch(coin.x);
}
gameState.runStatistics.score += coin.points;
}
function pickedUpgradesHTMl() {
let list = "";
for (let u of (0, _loadGameData.upgrades))for(let i = 0; i < gameState.perks[u.id]; i++)list += (0, _loadGameData.icons)["icon:" + u.id] + " ";
return list;
}
async function openUpgradesPicker() {
const catchRate = (gameState.score - gameState.levelStartScore) / (gameState.levelSpawnedCoins || 1);
let repeats = 1;
let choices = 3;
let timeGain = "", catchGain = "", missesGain = "";
if (gameState.levelTime < 30000) {
repeats++;
choices++;
timeGain = (0, _i18N.t)('level_up.plus_one_upgrade');
} else if (gameState.levelTime < 60000) {
choices++;
timeGain = (0, _i18N.t)('level_up.plus_one_choice');
}
if (catchRate === 1) {
repeats++;
choices++;
catchGain = (0, _i18N.t)('level_up.plus_one_upgrade');
} else if (catchRate > 0.9) {
choices++;
catchGain = (0, _i18N.t)('level_up.plus_one_choice');
}
if (gameState.levelMisses === 0) {
repeats++;
choices++;
missesGain = (0, _i18N.t)('level_up.plus_one_upgrade');
} else if (gameState.levelMisses <= 3) {
choices++;
missesGain = (0, _i18N.t)('level_up.plus_one_choice');
}
while(repeats--){
const actions = pickRandomUpgrades(choices + gameState.perks.one_more_choice - gameState.perks.instant_upgrade);
if (!actions.length) break;
let textAfterButtons = `
<p>${(0, _i18N.t)('level_up.after_buttons', {
level: gameState.currentLevel + 1,
max: max_levels()
})} </p>
<p>${pickedUpgradesHTMl()}</p>
<div id="level-recording-container"></div>
`;
const compliment = timeGain && catchGain && missesGain && (0, _i18N.t)("level_up.compliment_perfect") || (timeGain || catchGain || missesGain) && (0, _i18N.t)("level_up.compliment_good") || (0, _i18N.t)('level_up.compliment_advice');
const upgradeId = await asyncAlert({
title: (0, _i18N.t)('level_up.pick_upgrade_title') + (repeats ? " (" + (repeats + 1) + ")" : ""),
actions,
text: `<p>${(0, _i18N.t)('level_up.before_buttons', {
score: gameState.score - gameState.levelStartScore,
catchGain,
levelSpawnedCoins: gameState.levelSpawnedCoins,
time: Math.round(gameState.levelTime / 1000),
timeGain,
levelMisses: gameState.levelMisses,
missesGain,
compliment
})}
</p>`,
allowClose: false,
textAfterButtons
});
gameState.perks[upgradeId]++;
if (upgradeId === "instant_upgrade") repeats += 2;
gameState.runStatistics.upgrades_picked++;
}
(0, _combo.resetCombo)(gameState, undefined, undefined);
(0, _resetBalls.resetBalls)(gameState);
}
function setLevel(l) {
stopRecording();
pause(false);
if (l > 0) openUpgradesPicker();
gameState.currentLevel = l;
gameState.levelTime = 0;
gameState.autoCleanUses = 0;
gameState.lastTickDown = gameState.levelTime;
gameState.levelStartScore = gameState.score;
gameState.levelSpawnedCoins = 0;
gameState.levelMisses = 0;
gameState.runStatistics.levelsPlayed++;
(0, _combo.resetCombo)(gameState, undefined, undefined);
recomputeTargetBaseSpeed();
(0, _resetBalls.resetBalls)(gameState);
const lvl = currentLevelInfo();
if (lvl.size !== gameState.gridSize) {
gameState.gridSize = lvl.size;
fitSize();
}
gameState.coins = [];
gameState.bricks = [
...lvl.bricks
];
gameState.flashes = [];
// This caused problems with accented characters like the ô of côte d'ivoire for odd reasons
// background.src = 'data:image/svg+xml;base64,' + btoa(lvl.svg)
background.src = "data:image/svg+xml;UTF8," + lvl.svg;
}
function currentLevelInfo() {
return gameState.runLevels[gameState.currentLevel % gameState.runLevels.length];
}
function getPossibleUpgrades(gameState) {
return (0, _loadGameData.upgrades).filter((u)=>gameState.totalScoreAtRunStart >= u.threshold).filter((u)=>!u?.requires || gameState.perks[u?.requires]);
}
function getUpgraderUnlockPoints() {
let list = [];
(0, _loadGameData.upgrades).forEach((u)=>{
if (u.threshold) list.push({
threshold: u.threshold,
title: u.name + ' ' + (0, _i18N.t)('level_up.unlocked_perk')
});
});
(0, _loadGameData.allLevels).forEach((l)=>{
list.push({
threshold: l.threshold,
title: l.name + ' ' + (0, _i18N.t)('level_up.unlocked_level')
});
});
return list.filter((o)=>o.threshold).sort((a, b)=>a.threshold - b.threshold);
}
function dontOfferTooSoon(gameState, id) {
gameState.lastOffered[id] = Math.round(Date.now() / 1000);
}
function pickRandomUpgrades(count) {
let list = getPossibleUpgrades(gameState).map((u)=>({
...u,
score: Math.random() + (gameState.lastOffered[u.id] || 0)
})).sort((a, b)=>a.score - b.score).filter((u)=>gameState.perks[u.id] < u.max).slice(0, count).sort((a, b)=>a.id > b.id ? 1 : -1);
list.forEach((u)=>{
dontOfferTooSoon(gameState, u.id);
});
return list.map((u)=>({
text: u.name + (gameState.perks[u.id] ? (0, _i18N.t)('level_up.upgrade_perk_to_level', {
level: gameState.perks[u.id] + 1
}) : ""),
icon: (0, _loadGameData.icons)["icon:" + u.id],
value: u.id,
help: u.help(gameState.perks[u.id] + 1)
}));
}
function setMousePos(x) {
gameState.needsRender = true;
gameState.puckPosition = x;
// We have borders visible, enforce them
if (gameState.puckPosition < gameState.offsetXRoundedDown + gameState.puckWidth / 2) gameState.puckPosition = gameState.offsetXRoundedDown + gameState.puckWidth / 2;
if (gameState.puckPosition > gameState.offsetXRoundedDown + gameState.gameZoneWidthRoundedUp - gameState.puckWidth / 2) gameState.puckPosition = gameState.offsetXRoundedDown + gameState.gameZoneWidthRoundedUp - gameState.puckWidth / 2;
if (!gameState.running && !gameState.levelTime) (0, _resetBalls.putBallsAtPuck)(gameState);
}
gameCanvas.addEventListener("mouseup", (e)=>{
if (e.button !== 0) return;
if (gameState.running) pause(true);
else {
play();
if ((0, _options.isOptionOn)("pointerLock")) gameCanvas.requestPointerLock();
}
});
gameCanvas.addEventListener("mousemove", (e)=>{
if (document.pointerLockElement === gameCanvas) setMousePos(gameState.puckPosition + e.movementX);
else setMousePos(e.x);
});
gameCanvas.addEventListener("touchstart", (e)=>{
e.preventDefault();
if (!e.touches?.length) return;
setMousePos(e.touches[0].pageX);
play();
});
gameCanvas.addEventListener("touchend", (e)=>{
e.preventDefault();
pause(true);
});
gameCanvas.addEventListener("touchcancel", (e)=>{
e.preventDefault();
pause(true);
gameState.needsRender = true;
});
gameCanvas.addEventListener("touchmove", (e)=>{
if (!e.touches?.length) return;
setMousePos(e.touches[0].pageX);
});
function brickIndex(x, y) {
return getRowColIndex(Math.floor(y / gameState.brickWidth), Math.floor((x - gameState.offsetX) / gameState.brickWidth));
}
function hasBrick(index) {
if (gameState.bricks[index]) return index;
}
function hitsSomething(x, y, radius) {
return hasBrick(brickIndex(x - radius, y - radius)) ?? hasBrick(brickIndex(x + radius, y - radius)) ?? hasBrick(brickIndex(x + radius, y + radius)) ?? hasBrick(brickIndex(x - radius, y + radius));
}
function shouldPierceByColor(vhit, hhit, chit) {
if (!gameState.perks.pierce_color) return false;
if (typeof vhit !== "undefined" && gameState.bricks[vhit] !== gameState.ballsColor) return false;
if (typeof hhit !== "undefined" && gameState.bricks[hhit] !== gameState.ballsColor) return false;
if (typeof chit !== "undefined" && gameState.bricks[chit] !== gameState.ballsColor) return false;
return true;
}
function coinBrickHitCheck(coin) {
// Make ball/coin bonce, and return bricks that were hit
const radius = coin.size / 2;
const { x, y, previousX, previousY } = coin;
const vhit = hitsSomething(previousX, y, radius);
const hhit = hitsSomething(x, previousY, radius);
const chit = typeof vhit == "undefined" && typeof hhit == "undefined" && hitsSomething(x, y, radius) || undefined;
if (typeof vhit !== "undefined" || typeof chit !== "undefined") {
coin.y = coin.previousY;
coin.vy *= -1;
// Roll on corners
const leftHit = gameState.bricks[brickIndex(x - radius, y + radius)];
const rightHit = gameState.bricks[brickIndex(x + radius, y + radius)];
if (leftHit && !rightHit) {
coin.vx += 1;
coin.sa -= 1;
}
if (!leftHit && rightHit) {
coin.vx -= 1;
coin.sa += 1;
}
}
if (typeof hhit !== "undefined" || typeof chit !== "undefined") {
coin.x = coin.previousX;
coin.vx *= -1;
}
return vhit ?? hhit ?? chit;
}
function bordersHitCheck(coin, radius, delta) {
if (coin.destroyed) return;
coin.previousX = coin.x;
coin.previousY = coin.y;
coin.x += coin.vx * delta;
coin.y += coin.vy * delta;
coin.sx ||= 0;
coin.sy ||= 0;
coin.sx += coin.previousX - coin.x;
coin.sy += coin.previousY - coin.y;
coin.sx *= 0.9;
coin.sy *= 0.9;
if (gameState.perks.wind) coin.vx += (gameState.puckPosition - (gameState.offsetX + gameState.gameZoneWidth / 2)) / gameState.gameZoneWidth * gameState.perks.wind * 0.5;
let vhit = 0, hhit = 0;
if (coin.x < gameState.offsetXRoundedDown + radius) {
coin.x = gameState.offsetXRoundedDown + radius + (gameState.offsetXRoundedDown + radius - coin.x);
coin.vx *= -1;
hhit = 1;
}
if (coin.y < radius) {
coin.y = radius + (radius - coin.y);
coin.vy *= -1;
vhit = 1;
}
if (coin.x > gameState.canvasWidth - gameState.offsetXRoundedDown - radius) {
coin.x = gameState.canvasWidth - gameState.offsetXRoundedDown - radius - (coin.x - (gameState.canvasWidth - gameState.offsetXRoundedDown - radius));
coin.vx *= -1;
hhit = 1;
}
return hhit + vhit * 2;
}
function tick() {
recomputeTargetBaseSpeed();
const currentTick = performance.now();
gameState.puckWidth = gameState.gameZoneWidth / 12 * (3 - gameState.perks.smaller_puck + gameState.perks.bigger_puck);
if (gameState.keyboardPuckSpeed) setMousePos(gameState.puckPosition + gameState.keyboardPuckSpeed);
if (gameState.running) {
gameState.levelTime += currentTick - gameState.lastTick;
gameState.runStatistics.runTime += currentTick - gameState.lastTick;
gameState.runStatistics.max_combo = Math.max(gameState.runStatistics.max_combo, gameState.combo);
// How many times to compute
let delta = Math.min(4, (currentTick - gameState.lastTick) / (1000 / 60));
delta *= gameState.running ? 1 : 0;
gameState.coins = gameState.coins.filter((coin)=>!coin.destroyed);
gameState.balls = gameState.balls.filter((ball)=>!ball.destroyed);
const remainingBricks = gameState.bricks.filter((b)=>b && b !== "black").length;
if (gameState.levelTime > gameState.lastTickDown + 1000 && gameState.perks.hot_start) {
gameState.lastTickDown = gameState.levelTime;
(0, _combo.decreaseCombo)(gameState, gameState.perks.hot_start, gameState.puckPosition, gameState.gameZoneHeight - 2 * gameState.puckHeight);
}
if (remainingBricks <= gameState.perks.skip_last && !gameState.autoCleanUses) {
gameState.bricks.forEach((type, index)=>{
if (type) explodeBrick(index, gameState.balls[0], true);
});
gameState.autoCleanUses++;
}
if (!remainingBricks && !gameState.coins.length) {
if (gameState.currentLevel + 1 < max_levels()) setLevel(gameState.currentLevel + 1);
else gameOver((0, _i18N.t)('gameOver.win.title'), (0, _i18N.t)('gameOver.win.summary', {
score: gameState.score
}));
} else if (gameState.running || gameState.levelTime) {
let playedCoinBounce = false;
const coinRadius = Math.round(gameState.coinSize / 2);
gameState.coins.forEach((coin)=>{
if (coin.destroyed) return;
if (gameState.perks.coin_magnet) {
const attractionX = delta * (gameState.puckPosition - coin.x) / (100 + Math.pow(coin.y - gameState.gameZoneHeight, 2) + Math.pow(coin.x - gameState.puckPosition, 2)) * gameState.perks.coin_magnet * 100;
coin.vx += attractionX;
coin.sa -= attractionX / 10;
}
const ratio = 1 - (gameState.perks.viscosity * 0.03 + 0.005) * delta;
coin.vy *= ratio;
coin.vx *= ratio;
if (coin.vx > 7 * gameState.baseSpeed) coin.vx = 7 * gameState.baseSpeed;
if (coin.vx < -7 * gameState.baseSpeed) coin.vx = -7 * gameState.baseSpeed;
if (coin.vy > 7 * gameState.baseSpeed) coin.vy = 7 * gameState.baseSpeed;
if (coin.vy < -7 * gameState.baseSpeed) coin.vy = -7 * gameState.baseSpeed;
coin.a += coin.sa;
// Gravity
coin.vy += delta * coin.weight * 0.8;
const speed = Math.abs(coin.sx) + Math.abs(coin.sx);
const hitBorder = bordersHitCheck(coin, coin.size / 2, delta);
if (coin.y > gameState.gameZoneHeight - coinRadius - gameState.puckHeight && coin.y < gameState.gameZoneHeight + gameState.puckHeight + coin.vy && Math.abs(coin.x - gameState.puckPosition) < coinRadius + gameState.puckWidth / 2 + // a bit of margin to be nice
gameState.puckHeight) addToScore(coin);
else if (coin.y > gameState.canvasHeight + coinRadius) {
coin.destroyed = true;
if (gameState.perks.compound_interest) (0, _combo.resetCombo)(gameState, coin.x, coin.y);
}
const hitBrick = coinBrickHitCheck(coin);
if (gameState.perks.metamorphosis && typeof hitBrick !== "undefined") {
if (gameState.bricks[hitBrick] && coin.color !== gameState.bricks[hitBrick] && gameState.bricks[hitBrick] !== "black" && !coin.coloredABrick) {
gameState.bricks[hitBrick] = coin.color;
coin.coloredABrick = true;
(0, _sounds.sounds).colorChange(coin.x, 0.3);
}
}
if (typeof hitBrick !== "undefined" || hitBorder) {
coin.vx *= 0.8;
coin.vy *= 0.8;
coin.sa *= 0.9;
if (speed > 20 && !playedCoinBounce) {
playedCoinBounce = true;
(0, _sounds.sounds).coinBounce(coin.x, 0.2);
}
if (Math.abs(coin.vy) < 3) coin.vy = 0;
}
});
gameState.balls.forEach((ball)=>ballTick(ball, delta));
if (gameState.perks.wind) {
const windD = (gameState.puckPosition - (gameState.offsetX + gameState.gameZoneWidth / 2)) / gameState.gameZoneWidth * 2 * gameState.perks.wind;
for(let i = 0; i < gameState.perks.wind; i++)if (Math.random() * Math.abs(windD) > 0.5) gameState.flashes.push({
type: "particle",
duration: 150,
ethereal: true,
time: gameState.levelTime,
size: gameState.coinSize / 2,
color: rainbowColor(),
x: gameState.offsetXRoundedDown + Math.random() * gameState.gameZoneWidthRoundedUp,
y: Math.random() * gameState.gameZoneHeight,
vx: windD * 8,
vy: 0
});
}
gameState.flashes.forEach((flash)=>{
if (flash.type === "particle") {
flash.x += flash.vx * delta;
flash.y += flash.vy * delta;
if (!flash.ethereal) {
flash.vy += 0.5;
if (hasBrick(brickIndex(flash.x, flash.y))) flash.destroyed = true;
}
}
});
}
if (gameState.combo > (0, _combo.baseCombo)(gameState)) {
// The red should still be visible on a white bg
const baseParticle = !(0, _options.isOptionOn)("basic") && (gameState.combo - (0, _combo.baseCombo)(gameState)) * Math.random() > 5 && gameState.running && {
type: "particle",
duration: 100 * (Math.random() + 1),
time: gameState.levelTime,
size: gameState.coinSize / 2,
color: "red",
ethereal: true
};
if (gameState.perks.top_is_lava) baseParticle && gameState.flashes.push({
...baseParticle,
x: gameState.offsetXRoundedDown + Math.random() * gameState.gameZoneWidthRoundedUp,
y: 0,
vx: (Math.random() - 0.5) * 10,
vy: 5
});
if (gameState.perks.left_is_lava && baseParticle) gameState.flashes.push({
...baseParticle,
x: gameState.offsetXRoundedDown,
y: Math.random() * gameState.gameZoneHeight,
vx: 5,
vy: (Math.random() - 0.5) * 10
});
if (gameState.perks.right_is_lava && baseParticle) gameState.flashes.push({
...baseParticle,
x: gameState.offsetXRoundedDown + gameState.gameZoneWidthRoundedUp,
y: Math.random() * gameState.gameZoneHeight,
vx: -5,
vy: (Math.random() - 0.5) * 10
});
if (gameState.perks.compound_interest) {
let x = gameState.puckPosition, attemps = 0;
do {
x = gameState.offsetXRoundedDown + gameState.gameZoneWidthRoundedUp * Math.random();
attemps++;
}while (Math.abs(x - gameState.puckPosition) < gameState.puckWidth / 2 && attemps < 10);
baseParticle && gameState.flashes.push({
...baseParticle,
x,
y: gameState.gameZoneHeight,
vx: (Math.random() - 0.5) * 10,
vy: -5
});
}
if (gameState.perks.streak_shots) {
const pos = 0.5 - Math.random();
baseParticle && gameState.flashes.push({
...baseParticle,
duration: 100,
x: gameState.puckPosition + gameState.puckWidth * pos,
y: gameState.gameZoneHeight - gameState.puckHeight,
vx: pos * 10,
vy: -5
});
}
}
}
render();
requestAnimationFrame(tick);
gameState.lastTick = currentTick;
}
function isTelekinesisActive(ball) {
return gameState.perks.telekinesis && !ball.hitSinceBounce && ball.vy < 0;
}
function ballTick(ball, delta) {
ball.previousVX = ball.vx;
ball.previousVY = ball.vy;
let speedLimitDampener = 1 + gameState.perks.telekinesis + gameState.perks.ball_repulse_ball + gameState.perks.puck_repulse_ball + gameState.perks.ball_attract_ball;
if (isTelekinesisActive(ball)) {
speedLimitDampener += 3;
ball.vx += (gameState.puckPosition - ball.x) / 1000 * delta * gameState.perks.telekinesis;
}
if (ball.vx * ball.vx + ball.vy * ball.vy < gameState.baseSpeed * gameState.baseSpeed * 2) {
ball.vx *= 1 + 0.02 / speedLimitDampener;
ball.vy *= 1 + 0.02 / speedLimitDampener;
} else {
ball.vx *= 1 - 0.02 / speedLimitDampener;
ball.vy *= 1 - 0.02 / speedLimitDampener;
}
// Ball could get stuck horizontally because of ball-ball interactions in repulse/attract
if (Math.abs(ball.vy) < 0.2 * gameState.baseSpeed) ball.vy += (ball.vy > 0 ? 1 : -1) * 0.02 / speedLimitDampener;
if (gameState.perks.ball_repulse_ball) for (let b2 of gameState.balls){
// avoid computing this twice, and repulsing itself
if (b2.x >= ball.x) continue;
repulse(ball, b2, gameState.perks.ball_repulse_ball, true);
}
if (gameState.perks.ball_attract_ball) for (let b2 of gameState.balls){
// avoid computing this twice, and repulsing itself
if (b2.x >= ball.x) continue;
attract(ball, b2, gameState.perks.ball_attract_ball);
}
if (gameState.perks.puck_repulse_ball && Math.abs(ball.x - gameState.puckPosition) < gameState.puckWidth / 2 + gameState.ballSize * (9 + gameState.perks.puck_repulse_ball) / 10) repulse(ball, {
x: gameState.puckPosition,
y: gameState.gameZoneHeight
}, gameState.perks.puck_repulse_ball + 1, false);
if (gameState.perks.respawn && ball.hitItem?.length > 1 && !(0, _options.isOptionOn)("basic")) for(let i = 0; i < ball.hitItem?.length - 1 && i < gameState.perks.respawn; i++){
const { index, color } = ball.hitItem[i];
if (gameState.bricks[index] || color === "black") continue;
const vertical = Math.random() > 0.5;
const dx = Math.random() > 0.5 ? 1 : -1;
const dy = Math.random() > 0.5 ? 1 : -1;
gameState.flashes.push({
type: "particle",
duration: 250,
ethereal: true,
time: gameState.levelTime,
size: gameState.coinSize / 2,
color,
x: brickCenterX(index) + dx * gameState.brickWidth / 2,
y: brickCenterY(index) + dy * gameState.brickWidth / 2,
vx: vertical ? 0 : -dx * gameState.baseSpeed,
vy: vertical ? -dy * gameState.baseSpeed : 0
});
}
const borderHitCode = bordersHitCheck(ball, gameState.ballSize / 2, delta);
if (borderHitCode) {
if (gameState.perks.left_is_lava && borderHitCode % 2 && ball.x < gameState.offsetX + gameState.gameZoneWidth / 2) (0, _combo.resetCombo)(gameState, ball.x, ball.y);
if (gameState.perks.right_is_lava && borderHitCode % 2 && ball.x > gameState.offsetX + gameState.gameZoneWidth / 2) (0, _combo.resetCombo)(gameState, ball.x, ball.y);
if (gameState.perks.top_is_lava && borderHitCode >= 2) (0, _combo.resetCombo)(gameState, ball.x, ball.y + gameState.ballSize);
(0, _sounds.sounds).wallBeep(ball.x);
ball.bouncesList?.push({
x: ball.previousX,
y: ball.previousY
});
}
// Puck collision
const ylimit = gameState.gameZoneHeight - gameState.puckHeight - gameState.ballSize / 2;
const ballIsUnderPuck = Math.abs(ball.x - gameState.puckPosition) < gameState.ballSize / 2 + gameState.puckWidth / 2;
if (ball.y > ylimit && ball.vy > 0 && (ballIsUnderPuck || gameState.perks.extra_life && ball.y > ylimit + gameState.puckHeight / 2)) {
if (ballIsUnderPuck) {
const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
const angle = Math.atan2(-gameState.puckWidth / 2, ball.x - gameState.puckPosition);
ball.vx = speed * Math.cos(angle);
ball.vy = speed * Math.sin(angle);
(0, _sounds.sounds).wallBeep(ball.x);
} else {
ball.vy *= -1;
gameState.perks.extra_life = Math.max(0, gameState.perks.extra_life - 1);
(0, _sounds.sounds).lifeLost(ball.x);
if (!(0, _options.isOptionOn)("basic")) for(let i = 0; i < 10; i++)gameState.flashes.push({
type: "particle",
ethereal: false,
color: "red",
destroyed: false,
duration: 150,
size: gameState.coinSize / 2,
time: gameState.levelTime,
x: ball.x,
y: ball.y,
vx: Math.random() * gameState.baseSpeed * 3,
vy: gameState.baseSpeed * 3
});
}
if (gameState.perks.streak_shots) (0, _combo.resetCombo)(gameState, ball.x, ball.y);
if (gameState.perks.respawn) ball.hitItem.slice(0, -1).slice(0, gameState.perks.respawn).forEach(({ index, color })=>{
if (!gameState.bricks[index] && color !== "black") gameState.bricks[index] = color;
});
ball.hitItem = [];
if (!ball.hitSinceBounce) {
gameState.runStatistics.misses++;
gameState.levelMisses++;
(0, _combo.resetCombo)(gameState, ball.x, ball.y);
gameState.flashes.push({
type: "text",
text: (0, _i18N.t)('play.missed_ball'),
duration: 500,
time: gameState.levelTime,
size: gameState.puckHeight * 1.5,
color: "red",
x: gameState.puckPosition,
y: gameState.gameZoneHeight - gameState.puckHeight * 2
});
}
gameState.runStatistics.puck_bounces++;
ball.hitSinceBounce = 0;
ball.sapperUses = 0;
ball.piercedSinceBounce = 0;
ball.bouncesList = [
{
x: ball.previousX,
y: ball.previousY
}
];
}
if (ball.y > gameState.gameZoneHeight + gameState.ballSize / 2 && gameState.running) {
ball.destroyed = true;
gameState.runStatistics.balls_lost++;
if (!gameState.balls.find((b)=>!b.destroyed)) gameOver((0, _i18N.t)('gameOver.lost.title'), (0, _i18N.t)('gameOver.lost.summary', {
score: gameState.score
}));
}
const radius = gameState.ballSize / 2;
// Make ball/coin bonce, and return bricks that were hit
const { x, y, previousX, previousY } = ball;
const vhit = hitsSomething(previousX, y, radius);
const hhit = hitsSomething(x, previousY, radius);
const chit = typeof vhit == "undefined" && typeof hhit == "undefined" && hitsSomething(x, y, radius) || undefined;
const hitBrick = vhit ?? hhit ?? chit;
let sturdyBounce = hitBrick && gameState.bricks[hitBrick] !== "black" && gameState.perks.sturdy_bricks && gameState.perks.sturdy_bricks > Math.random() * 5;
let pierce = false;
if (sturdyBounce || typeof hitBrick === "undefined") ;
else if (shouldPierceByColor(vhit, hhit, chit)) pierce = true;
else if (ball.piercedSinceBounce < gameState.perks.pierce * 3) {
pierce = true;
ball.piercedSinceBounce++;
}
if (typeof vhit !== "undefined" || typeof chit !== "undefined") {
if (!pierce) {
ball.y = ball.previousY;
ball.vy *= -1;
}
}
if (typeof hhit !== "undefined" || typeof chit !== "undefined") {
if (!pierce) {
ball.x = ball.previousX;
ball.vx *= -1;
}
}
if (sturdyBounce) {
(0, _sounds.sounds).wallBeep(x);
return;
}
if (typeof hitBrick !== "undefined") {
const initialBrickColor = gameState.bricks[hitBrick];
explodeBrick(hitBrick, ball, false);
if (ball.sapperUses < gameState.perks.sapper && initialBrickColor !== "black" && // don't replace a brick that bounced with sturdy_bricks
!gameState.bricks[hitBrick]) {
gameState.bricks[hitBrick] = "black";
ball.sapperUses++;
}
}
if (!(0, _options.isOptionOn)("basic")) {
ball.sparks += delta * (gameState.combo - 1) / 30;
if (ball.sparks > 1) {
gameState.flashes.push({
type: "particle",
duration: 100 * ball.sparks,
time: gameState.levelTime,
size: gameState.coinSize / 2,
color: gameState.ballsColor,
x: ball.x,
y: ball.y,
vx: (Math.random() - 0.5) * gameState.baseSpeed,
vy: (Math.random() - 0.5) * gameState.baseSpeed,
ethereal: false
});
ball.sparks = 0;
}
}
}
function getTotalScore() {
try {
return JSON.parse(localStorage.getItem("breakout_71_total_score") || "0");
} catch (e) {
return 0;
}
}
function addToTotalScore(points) {
if (gameState.isCreativeModeRun) return;
try {
localStorage.setItem("breakout_71_total_score", JSON.stringify(getTotalScore() + points));
} catch (e) {}
}
function addToTotalPlayTime(ms) {
try {
localStorage.setItem("breakout_71_total_play_time", JSON.stringify(JSON.parse(localStorage.getItem("breakout_71_total_play_time") || "0") + ms));
} catch (e) {}
}
function gameOver(title, intro) {
if (!gameState.running) return;
pause(true);
stopRecording();
addToTotalPlayTime(gameState.runStatistics.runTime);
gameState.runStatistics.max_level = gameState.currentLevel + 1;
let animationDelay = -300;
const getDelay = ()=>{
animationDelay += 800;
return "animation-delay:" + animationDelay + "ms;";
};
// unlocks
let unlocksInfo = "";
const endTs = getTotalScore();
const startTs = endTs - gameState.score;
const list = getUpgraderUnlockPoints();
list.filter((u)=>u.threshold > startTs && u.threshold < endTs).forEach((u)=>{
unlocksInfo += `
<p class="progress" >
<span>${u.title}</span>
<span class="progress_bar_part" style="${getDelay()}"></span>
</p>
`;
});
const previousUnlockAt = findLast(list, (u)=>u.threshold <= endTs)?.threshold || 0;
const nextUnlock = list.find((u)=>u.threshold > endTs);
if (nextUnlock) {
const total = nextUnlock?.threshold - previousUnlockAt;
const done = endTs - previousUnlockAt;
intro += (0, _i18N.t)('gameOver.next_unlock', {
points: nextUnlock.threshold - endTs
});
const scaleX = (done / total).toFixed(2);
unlocksInfo += `
<p class="progress" >
<span>${nextUnlock.title}</span>
<span style="transform: scale(${scaleX},1);${getDelay()}" class="progress_bar_part"></span>
</p>
`;
list.slice(list.indexOf(nextUnlock) + 1).slice(0, 3).forEach((u)=>{
unlocksInfo += `
<p class="progress" >
<span>${u.title}</span>
</p>
`;
});
}
let unlockedItems = list.filter((u)=>u.threshold > startTs && u.threshold < endTs);
if (unlockedItems.length) unlocksInfo += `<p>${(0, _i18N.t)('gameOver.unlocked_count', {
count: unlockedItems.length
})} ${unlockedItems.map((u)=>u.title).join(", ")}</p>`;
// Avoid the sad sound right as we restart a new games
gameState.combo = 1;
asyncAlert({
allowClose: true,
title,
text: `
${gameState.isCreativeModeRun ? `<p>${(0, _i18N.t)('gameOver.test_run')}</p> ` : ""}
<p>${intro}</p>
<p>${(0, _i18N.t)('gameOver.cumulative_total', {
startTs,
endTs
})}</p>
${unlocksInfo}
`,
actions: [
{
value: null,
text: (0, _i18N.t)('gameOver.restart'),
help: ""
}
],
textAfterButtons: `<div id="level-recording-container"></div>
${getHistograms()}
`
}).then(()=>restart({
levelToAvoid: currentLevelInfo().name
}));
}
function getHistograms() {
let runStats = "";
try {
// Stores only top 100 runs
let runsHistory = JSON.parse(localStorage.getItem("breakout_71_runs_history") || "[]");
runsHistory.sort((a, b)=>a.score - b.score).reverse();
runsHistory = runsHistory.slice(0, 100);
runsHistory.push({
...gameState.runStatistics,
perks: gameState.perks,
appVersion: (0, _loadGameData.appVersion)
});
// Generate some histogram
if (!gameState.isCreativeModeRun) localStorage.setItem("breakout_71_runs_history", JSON.stringify(runsHistory, null, 2));
const makeHistogram = (title, getter, unit)=>{
let values = runsHistory.map((h)=>getter(h) || 0);
let min = Math.min(...values);
let max = Math.max(...values);
// No point
if (min === max) return "";
if (max - min < 10) {
// This is mostly useful for levels
min = Math.max(0, max - 10);
max = Math.max(max, min + 10);
}
// One bin per unique value, max 10
const binsCount = Math.min(values.length, 10);
if (binsCount < 3) return "";
const bins = [];
const binsTotal = [];
for(let i = 0; i < binsCount; i++){
bins.push(0);
binsTotal.push(0);
}
const binSize = (max - min) / bins.length;
const binIndexOf = (v)=>Math.min(bins.length - 1, Math.floor((v - min) / binSize));
values.forEach((v)=>{
if (isNaN(v)) return;
const index = binIndexOf(v);
bins[index]++;
binsTotal[index] += v;
});
if (bins.filter((b)=>b).length < 3) return "";
const maxBin = Math.max(...bins);
const lastValue = values[values.length - 1];
const activeBin = binIndexOf(lastValue);
const bars = bins.map((v, vi)=>{
const style = `height: ${v / maxBin * 80}px`;
return `<span class="${vi === activeBin ? "active" : ""}"><span style="${style}" title="${v} run${v > 1 ? "s" : ""} between ${Math.floor(min + vi * binSize)} and ${Math.floor(min + (vi + 1) * binSize)}${unit}"
><span>${!v && " " || vi == activeBin && lastValue + unit || Math.round(binsTotal[vi] / v) + unit}</span></span></span>`;
}).join("");
return `<h2 class="histogram-title">${title} : <strong>${lastValue}${unit}</strong></h2>
<div class="histogram">${bars}</div>
`;
};
runStats += makeHistogram((0, _i18N.t)('gameOver.stats.total_score'), (r)=>r.score, "");
runStats += makeHistogram((0, _i18N.t)('gameOver.stats.catch_rate'), (r)=>Math.round(r.score / r.coins_spawned * 100), "%");
runStats += makeHistogram((0, _i18N.t)('gameOver.stats.bricks_broken'), (r)=>r.bricks_broken, "");
runStats += makeHistogram((0, _i18N.t)('gameOver.stats.bricks_per_minute'), (r)=>Math.round(r.bricks_broken / r.runTime * 60000), "");
runStats += makeHistogram((0, _i18N.t)('gameOver.stats.hit_rate'), (r)=>Math.round((1 - r.misses / r.puck_bounces) * 100), "%");
runStats += makeHistogram((0, _i18N.t)('gameOver.stats.duration_per_level'), (r)=>Math.round(r.runTime / 1000 / r.levelsPlayed), "s");
runStats += makeHistogram((0, _i18N.t)('gameOver.stats.level_reached'), (r)=>r.levelsPlayed, "");
runStats += makeHistogram((0, _i18N.t)('gameOver.stats.upgrades_applied'), (r)=>r.upgrades_picked, "");
runStats += makeHistogram((0, _i18N.t)('gameOver.stats.balls_lost'), (r)=>r.balls_lost, "");
runStats += makeHistogram((0, _i18N.t)('gameOver.stats.combo_avg'), (r)=>Math.round(r.coins_spawned / r.bricks_broken), "");
runStats += makeHistogram((0, _i18N.t)('gameOver.stats.combo_max'), (r)=>r.max_combo, "");
if (runStats) runStats = `<p>${(0, _i18N.t)('gameOver.stats.intro', {
count: runsHistory.length - 1
})}</p>` + runStats;
} catch (e) {
console.warn(e);
}
return runStats;
}
function explodeBrick(index, ball, isExplosion) {
const color = gameState.bricks[index];
if (!color) return;
if (color === "black") {
delete gameState.bricks[index];
const x = brickCenterX(index), y = brickCenterY(index);
(0, _sounds.sounds).explode(ball.x);
const col = index % gameState.gridSize;
const row = Math.floor(index / gameState.gridSize);
const size = 1 + gameState.perks.bigger_explosions;
// Break bricks around
for(let dx = -size; dx <= size; dx++)for(let dy = -size; dy <= size; dy++){
const i = getRowColIndex(row + dy, col + dx);
if (gameState.bricks[i] && i !== -1) {
// Study bricks resist explosions too
if (gameState.bricks[i] !== "black" && gameState.perks.sturdy_bricks > Math.random() * 5) continue;
explodeBrick(i, ball, true);
}
}
// Blow nearby coins
gameState.coins.forEach((c)=>{
const dx = c.x - x;
const dy = c.y - y;
const d2 = Math.max(gameState.brickWidth, Math.abs(dx) + Math.abs(dy));
c.vx += dx / d2 * 10 * size / c.weight;
c.vy += dy / d2 * 10 * size / c.weight;
});
gameState.lastExplosion = Date.now();
gameState.flashes.push({
type: "ball",
duration: 150,
time: gameState.levelTime,
size: gameState.brickWidth * 2,
color: "white",
x,
y
});
spawnExplosion(7 * (1 + gameState.perks.bigger_explosions), x, y, "white", 150, gameState.coinSize);
ball.hitSinceBounce++;
gameState.runStatistics.bricks_broken++;
} else if (color) {
// Even if it bounces we don't want to count that as a miss
ball.hitSinceBounce++;
// Flashing is take care of by the tick loop
const x = brickCenterX(index), y = brickCenterY(index);
gameState.bricks[index] = "";
// coins = coins.filter((c) => !c.destroyed);
let coinsToSpawn = gameState.combo;
if (gameState.perks.sturdy_bricks) // +10% per level
coinsToSpawn += Math.ceil((10 + gameState.perks.sturdy_bricks) / 10 * coinsToSpawn);
gameState.levelSpawnedCoins += coinsToSpawn;
gameState.runStatistics.coins_spawned += coinsToSpawn;
gameState.runStatistics.bricks_broken++;
const maxCoins = gameState.MAX_COINS * ((0, _options.isOptionOn)("basic") ? 0.5 : 1);
const spawnableCoins = gameState.coins.length > gameState.MAX_COINS ? 1 : Math.floor(maxCoins - gameState.coins.length) / 3;
const pointsPerCoin = Math.max(1, Math.ceil(coinsToSpawn / spawnableCoins));
while(coinsToSpawn > 0){
const points = Math.min(pointsPerCoin, coinsToSpawn);
if (points < 0 || isNaN(points)) {
console.error({
points
});
debugger;
}
coinsToSpawn -= points;
const cx = x + (Math.random() - 0.5) * (gameState.brickWidth - gameState.coinSize), cy = y + (Math.random() - 0.5) * (gameState.brickWidth - gameState.coinSize);
gameState.coins.push({
points,
size: gameState.coinSize,
color: gameState.perks.metamorphosis ? color : "gold",
x: cx,
y: cy,
previousX: cx,
previousY: cy,
// Use previous speed because the ball has already bounced
vx: ball.previousVX * (0.5 + Math.random()),
vy: ball.previousVY * (0.5 + Math.random()),
sx: 0,
sy: 0,
a: Math.random() * Math.PI * 2,
sa: Math.random() - 0.5,
weight: 0.8 + Math.random() * 0.2
});
}
gameState.combo += Math.max(0, gameState.perks.streak_shots + gameState.perks.compound_interest + gameState.perks.left_is_lava + gameState.perks.right_is_lava + gameState.perks.top_is_lava + gameState.perks.picky_eater - Math.round(Math.random() * gameState.perks.soft_reset));
if (!isExplosion) {
// color change
if ((gameState.perks.picky_eater || gameState.perks.pierce_color) && color !== gameState.ballsColor && color) {
if (gameState.perks.picky_eater) (0, _combo.resetCombo)(gameState, ball.x, ball.y);
(0, _sounds.sounds).colorChange(ball.x, 0.8);
gameState.lastExplosion = gameState.levelTime;
gameState.ballsColor = color;
if (!(0, _options.isOptionOn)("basic")) gameState.balls.forEach((ball)=>{
spawnExplosion(7, ball.previousX, ball.previousY, color, 150, 15);
});
} else (0, _sounds.sounds).comboIncreaseMaybe(gameState.combo, ball.x, 1);
}
gameState.flashes.push({
type: "ball",
duration: 40,
time: gameState.levelTime,
size: gameState.brickWidth,
color: color,
x,
y
});
spawnExplosion(5 + Math.min(gameState.combo, 30), x, y, color, 150, gameState.coinSize / 2);
}
if (!gameState.bricks[index] && color !== "black") ball.hitItem?.push({
index,
color
});
}
function max_levels() {
return 7 + gameState.perks.extra_levels;
}
function render() {
if (gameState.running) gameState.needsRender = true;
if (!gameState.needsRender) return;
gameState.needsRender = false;
const level = currentLevelInfo();
const { width, height } = gameCanvas;
if (!width || !height) return;
if (gameState.currentLevel || gameState.levelTime) menuLabel.innerText = (0, _i18N.t)('play.current_lvl', {
level: gameState.currentLevel + 1,
max: max_levels()
});
else menuLabel.innerText = (0, _i18N.t)('play.menu_label');
scoreDisplay.innerText = `$${gameState.score}`;
scoreDisplay.className = gameState.lastScoreIncrease > gameState.levelTime - 500 ? "active" : "";
// Clear
if (!(0, _options.isOptionOn)("basic") && !level.color && level.svg) {
// Without this the light trails everything
ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = 1;
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, width, height);
ctx.globalCompositeOperation = "screen";
ctx.globalAlpha = 0.6;
gameState.coins.forEach((coin)=>{
if (!coin.destroyed) drawFuzzyBall(ctx, coin.color, gameState.coinSize * 2, coin.x, coin.y);
});
gameState.balls.forEach((ball)=>{
drawFuzzyBall(ctx, gameState.ballsColor, gameState.ballSize * 2, ball.x, ball.y);
});
ctx.globalAlpha = 0.5;
gameState.bricks.forEach((color, index)=>{
if (!color) return;
const x = brickCenterX(index), y = brickCenterY(index);
drawFuzzyBall(ctx, color == "black" ? "#666" : color, gameState.brickWidth, x, y);
});
ctx.globalAlpha = 1;
gameState.flashes.forEach((flash)=>{
const { x, y, time, color, size, type, duration } = flash;
const elapsed = gameState.levelTime - time;
ctx.globalAlpha = Math.min(1, 2 - elapsed / duration * 2);
if (type === "ball") drawFuzzyBall(ctx, color, size, x, y);
if (type === "particle") drawFuzzyBall(ctx, color, size * 3, x, y);
});
// Decides how brights the bg black parts can get
ctx.globalAlpha = 0.2;
ctx.globalCompositeOperation = "multiply";
ctx.fillStyle = "black";
ctx.fillRect(0, 0, width, height);
// Decides how dark the background black parts are when lit (1=black)
ctx.globalAlpha = 0.8;
ctx.globalCompositeOperation = "multiply";
if (level.svg && background.width && background.complete) {
if (backgroundCanvas.title !== level.name) {
backgroundCanvas.title = level.name;
backgroundCanvas.width = gameState.canvasWidth;
backgroundCanvas.height = gameState.canvasHeight;
const bgctx = backgroundCanvas.getContext("2d");
bgctx.fillStyle = level.color || "#000";
bgctx.fillRect(0, 0, gameState.canvasWidth, gameState.canvasHeight);
const pattern = ctx.createPattern(background, "repeat");
if (pattern) {
bgctx.fillStyle = pattern;
bgctx.fillRect(0, 0, width, height);
}
}
ctx.drawImage(backgroundCanvas, 0, 0);
} else {
// Background not loaded yes
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, width, height);
}
} else {
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
ctx.fillStyle = level.color || "#000";
ctx.fillRect(0, 0, width, height);
gameState.flashes.forEach((flash)=>{
const { x, y, time, color, size, type, duration } = flash;
const elapsed = gameState.levelTime - time;
ctx.globalAlpha = Math.min(1, 2 - elapsed / duration * 2);
if (type === "particle") drawBall(ctx, color, size, x, y);
});
}
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
const lastExplosionDelay = Date.now() - gameState.lastExplosion + 5;
const shaked = lastExplosionDelay < 200 && !(0, _options.isOptionOn)('basic');
if (shaked) {
const amplitude = (gameState.perks.bigger_explosions + 1) * 50 / lastExplosionDelay;
ctx.translate(Math.sin(Date.now()) * amplitude, Math.sin(Date.now() + 36) * amplitude);
}
if (gameState.perks.bigger_explosions && !(0, _options.isOptionOn)('basic')) {
if (shaked) gameCanvas.style.filter = 'brightness(' + (1 + 100 / (1 + lastExplosionDelay)) + ')';
else gameCanvas.style.filter = '';
}
// Coins
ctx.globalAlpha = 1;
gameState.coins.forEach((coin)=>{
if (!coin.destroyed) {
ctx.globalCompositeOperation = coin.color === "gold" || level.color ? "source-over" : "screen";
drawCoin(ctx, coin.color, coin.size, coin.x, coin.y, level.color || "black", coin.a);
}
});
// Black shadow around balls
if (!(0, _options.isOptionOn)("basic")) {
ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = Math.min(0.8, gameState.coins.length / 20);
gameState.balls.forEach((ball)=>{
drawBall(ctx, level.color || "#000", gameState.ballSize * 6, ball.x, ball.y);
});
}
ctx.globalCompositeOperation = "source-over";
renderAllBricks();
ctx.globalCompositeOperation = "screen";
gameState.flashes = gameState.flashes.filter((f)=>gameState.levelTime - f.time < f.duration && !f.destroyed);
gameState.flashes.forEach((flash)=>{
const { x, y, time, color, size, type, duration } = flash;
const elapsed = gameState.levelTime - time;
ctx.globalAlpha = Math.max(0, Math.min(1, 2 - elapsed / duration * 2));
if (type === "text") {
ctx.globalCompositeOperation = "source-over";
drawText(ctx, flash.text, color, size, x, y - elapsed / 10);
} else if (type === "particle") {
ctx.globalCompositeOperation = "screen";
drawBall(ctx, color, size, x, y);
drawFuzzyBall(ctx, color, size, x, y);
}
});
if (gameState.perks.extra_life) {
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
ctx.fillStyle = gameState.puckColor;
for(let i = 0; i < gameState.perks.extra_life; i++)ctx.fillRect(gameState.offsetXRoundedDown, gameState.gameZoneHeight - gameState.puckHeight / 2 + 2 * i, gameState.gameZoneWidthRoundedUp, 1);
}
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
gameState.balls.forEach((ball)=>{
// The white border around is to distinguish colored balls from coins/bg
drawBall(ctx, gameState.ballsColor, gameState.ballSize, ball.x, ball.y, gameState.puckColor);
if (isTelekinesisActive(ball)) {
ctx.strokeStyle = gameState.puckColor;
ctx.beginPath();
ctx.bezierCurveTo(gameState.puckPosition, gameState.gameZoneHeight, gameState.puckPosition, ball.y, ball.x, ball.y);
ctx.stroke();
}
});
// The puck
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
if (gameState.perks.streak_shots && gameState.combo > (0, _combo.baseCombo)(gameState)) drawPuck(ctx, "red", gameState.puckWidth, gameState.puckHeight, -2);
drawPuck(ctx, gameState.puckColor, gameState.puckWidth, gameState.puckHeight);
if (gameState.combo > 1) {
ctx.globalCompositeOperation = "source-over";
const comboText = "x " + gameState.combo;
const comboTextWidth = comboText.length * gameState.puckHeight / 1.8;
const totalWidth = comboTextWidth + gameState.coinSize * 2;
const left = gameState.puckPosition - totalWidth / 2;
if (totalWidth < gameState.puckWidth) {
drawCoin(ctx, "gold", gameState.coinSize, left + gameState.coinSize / 2, gameState.gameZoneHeight - gameState.puckHeight / 2, gameState.puckColor, 0);
drawText(ctx, comboText, "#000", gameState.puckHeight, left + gameState.coinSize * 1.5, gameState.gameZoneHeight - gameState.puckHeight / 2, true);
} else drawText(ctx, comboText, "#FFF", gameState.puckHeight, gameState.puckPosition, gameState.gameZoneHeight - gameState.puckHeight / 2, false);
}
// Borders
const hasCombo = gameState.combo > (0, _combo.baseCombo)(gameState);
ctx.globalCompositeOperation = "source-over";
if (gameState.offsetXRoundedDown) {
// draw outside of gaming area to avoid capturing borders in recordings
ctx.fillStyle = hasCombo && gameState.perks.left_is_lava ? "red" : gameState.puckColor;
ctx.fillRect(gameState.offsetX - 1, 0, 1, height);
ctx.fillStyle = hasCombo && gameState.perks.right_is_lava ? "red" : gameState.puckColor;
ctx.fillRect(width - gameState.offsetX + 1, 0, 1, height);
} else {
ctx.fillStyle = "red";
if (hasCombo && gameState.perks.left_is_lava) ctx.fillRect(0, 0, 1, height);
if (hasCombo && gameState.perks.right_is_lava) ctx.fillRect(width - 1, 0, 1, height);
}
if (gameState.perks.top_is_lava && gameState.combo > (0, _combo.baseCombo)(gameState)) {
ctx.fillStyle = "red";
ctx.fillRect(gameState.offsetXRoundedDown, 0, gameState.gameZoneWidthRoundedUp, 1);
}
const redBottom = gameState.perks.compound_interest && gameState.combo > (0, _combo.baseCombo)(gameState);
ctx.fillStyle = redBottom ? "red" : gameState.puckColor;
if ((0, _options.isOptionOn)("mobile-mode")) {
ctx.fillRect(gameState.offsetXRoundedDown, gameState.gameZoneHeight, gameState.gameZoneWidthRoundedUp, 1);
if (!gameState.running) drawText(ctx, (0, _i18N.t)('play.mobile_press_to_play'), gameState.puckColor, gameState.puckHeight, gameState.canvasWidth / 2, gameState.gameZoneHeight + (gameState.canvasHeight - gameState.gameZoneHeight) / 2);
} else if (redBottom) ctx.fillRect(gameState.offsetXRoundedDown, gameState.gameZoneHeight - 1, gameState.gameZoneWidthRoundedUp, 1);
if (shaked) ctx.resetTransform();
recordOneFrame();
}
let cachedBricksRender = document.createElement("canvas");
let cachedBricksRenderKey = "";
function renderAllBricks() {
ctx.globalAlpha = 1;
const redBorderOnBricksWithWrongColor = gameState.combo > (0, _combo.baseCombo)(gameState) && gameState.perks.picky_eater && !(0, _options.isOptionOn)('basic');
const newKey = gameState.gameZoneWidth + "_" + gameState.bricks.join("_") + bombSVG.complete + "_" + redBorderOnBricksWithWrongColor + "_" + gameState.ballsColor + "_" + gameState.perks.pierce_color;
if (newKey !== cachedBricksRenderKey) {
cachedBricksRenderKey = newKey;
cachedBricksRender.width = gameState.gameZoneWidth;
cachedBricksRender.height = gameState.gameZoneWidth + 1;
const canctx = cachedBricksRender.getContext("2d");
canctx.clearRect(0, 0, gameState.gameZoneWidth, gameState.gameZoneWidth);
canctx.resetTransform();
canctx.translate(-gameState.offsetX, 0);
// Bricks
gameState.bricks.forEach((color, index)=>{
const x = brickCenterX(index), y = brickCenterY(index);
if (!color) return;
const borderColor = gameState.ballsColor !== color && color !== "black" && redBorderOnBricksWithWrongColor && "red" || color;
drawBrick(canctx, color, borderColor, x, y);
if (color === "black") {
canctx.globalCompositeOperation = "source-over";
drawIMG(canctx, bombSVG, gameState.brickWidth, x, y);
}
});
}
ctx.drawImage(cachedBricksRender, gameState.offsetX, 0);
}
let cachedGraphics = {};
function drawPuck(ctx, color, puckWidth, puckHeight, yOffset = 0) {
const key = "puck" + color + "_" + puckWidth + "_" + puckHeight;
if (!cachedGraphics[key]) {
const can = document.createElement("canvas");
can.width = puckWidth;
can.height = puckHeight * 2;
const canctx = can.getContext("2d");
canctx.fillStyle = color;
canctx.beginPath();
canctx.moveTo(0, puckHeight * 2);
canctx.lineTo(0, puckHeight * 1.25);
canctx.bezierCurveTo(0, puckHeight * 0.75, puckWidth, puckHeight * 0.75, puckWidth, puckHeight * 1.25);
canctx.lineTo(puckWidth, puckHeight * 2);
canctx.fill();
cachedGraphics[key] = can;
}
ctx.drawImage(cachedGraphics[key], Math.round(gameState.puckPosition - puckWidth / 2), gameState.gameZoneHeight - puckHeight * 2 + yOffset);
}
function drawBall(ctx, color, width, x, y, borderColor = "") {
const key = "ball" + color + "_" + width + "_" + borderColor;
const size = Math.round(width);
if (!cachedGraphics[key]) {
const can = document.createElement("canvas");
can.width = size;
can.height = size;
const canctx = can.getContext("2d");
canctx.beginPath();
canctx.arc(size / 2, size / 2, Math.round(size / 2) - 1, 0, 2 * Math.PI);
canctx.fillStyle = color;
canctx.fill();
if (borderColor) {
canctx.lineWidth = 2;
canctx.strokeStyle = borderColor;
canctx.stroke();
}
cachedGraphics[key] = can;
}
ctx.drawImage(cachedGraphics[key], Math.round(x - size / 2), Math.round(y - size / 2));
}
const angles = 32;
function drawCoin(ctx, color, size, x, y, borderColor, rawAngle) {
const angle = (Math.round(rawAngle / Math.PI * 2 * angles) % angles + angles) % angles;
const key = "coin with halo_" + color + "_" + size + "_" + borderColor + "_" + (color === "gold" ? angle : "whatever");
if (!cachedGraphics[key]) {
const can = document.createElement("canvas");
can.width = size;
can.height = size;
const canctx = can.getContext("2d");
// coin
canctx.beginPath();
canctx.arc(size / 2, size / 2, size / 2, 0, 2 * Math.PI);
canctx.fillStyle = color;
canctx.fill();
if (color === "gold") {
canctx.strokeStyle = borderColor;
canctx.stroke();
canctx.beginPath();
canctx.arc(size / 2, size / 2, size / 2 * 0.6, 0, 2 * Math.PI);
canctx.fillStyle = "rgba(255,255,255,0.5)";
canctx.fill();
canctx.translate(size / 2, size / 2);
canctx.rotate(angle / 16);
canctx.translate(-size / 2, -size / 2);
canctx.globalCompositeOperation = "multiply";
drawText(canctx, "$", color, size - 2, size / 2, size / 2 + 1);
drawText(canctx, "$", color, size - 2, size / 2, size / 2 + 1);
}
cachedGraphics[key] = can;
}
ctx.drawImage(cachedGraphics[key], Math.round(x - size / 2), Math.round(y - size / 2));
}
function drawFuzzyBall(ctx, color, width, x, y) {
const key = "fuzzy-circle" + color + "_" + width;
if (!color) debugger;
const size = Math.round(width * 3);
if (!cachedGraphics[key]) {
const can = document.createElement("canvas");
can.width = size;
can.height = size;
const canctx = can.getContext("2d");
const gradient = canctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2);
gradient.addColorStop(0, color);
gradient.addColorStop(1, "transparent");
canctx.fillStyle = gradient;
canctx.fillRect(0, 0, size, size);
cachedGraphics[key] = can;
}
ctx.drawImage(cachedGraphics[key], Math.round(x - size / 2), Math.round(y - size / 2));
}
function drawBrick(ctx, color, borderColor, x, y) {
const tlx = Math.ceil(x - gameState.brickWidth / 2);
const tly = Math.ceil(y - gameState.brickWidth / 2);
const brx = Math.ceil(x + gameState.brickWidth / 2) - 1;
const bry = Math.ceil(y + gameState.brickWidth / 2) - 1;
const width = brx - tlx, height = bry - tly;
const key = "brick" + color + "_" + borderColor + "_" + width + "_" + height;
if (!cachedGraphics[key]) {
const can = document.createElement("canvas");
can.width = width;
can.height = height;
const bord = 2;
const cornerRadius = 2;
const canctx = can.getContext("2d");
canctx.fillStyle = color;
canctx.strokeStyle = borderColor;
canctx.lineJoin = "round";
canctx.lineWidth = bord;
roundRect(canctx, bord / 2, bord / 2, width - bord, height - bord, cornerRadius);
canctx.fill();
canctx.stroke();
cachedGraphics[key] = can;
}
ctx.drawImage(cachedGraphics[key], tlx, tly, width, height);
// It's not easy to have a 1px gap between bricks without antialiasing
}
function roundRect(ctx, x, y, width, height, radius) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
}
function drawIMG(ctx, img, size, x, y) {
const key = "svg" + img + "_" + size + "_" + img.complete;
if (!cachedGraphics[key]) {
const can = document.createElement("canvas");
can.width = size;
can.height = size;
const canctx = can.getContext("2d");
const ratio = size / Math.max(img.width, img.height);
const w = img.width * ratio;
const h = img.height * ratio;
canctx.drawImage(img, (size - w) / 2, (size - h) / 2, w, h);
cachedGraphics[key] = can;
}
ctx.drawImage(cachedGraphics[key], Math.round(x - size / 2), Math.round(y - size / 2));
}
function drawText(ctx, text, color, fontSize, x, y, left = false) {
const key = "text" + text + "_" + color + "_" + fontSize + "_" + left;
if (!cachedGraphics[key]) {
const can = document.createElement("canvas");
can.width = fontSize * text.length;
can.height = fontSize;
const canctx = can.getContext("2d");
canctx.fillStyle = color;
canctx.textAlign = left ? "left" : "center";
canctx.textBaseline = "middle";
canctx.font = fontSize + "px monospace";
canctx.fillText(text, left ? 0 : can.width / 2, can.height / 2, can.width);
cachedGraphics[key] = can;
}
ctx.drawImage(cachedGraphics[key], left ? x : Math.round(x - cachedGraphics[key].width / 2), Math.round(y - cachedGraphics[key].height / 2));
}
window.addEventListener("visibilitychange", ()=>{
if (document.hidden) pause(true);
});
const scoreDisplay = document.getElementById("score");
const menuLabel = document.getElementById("menuLabel");
let alertsOpen = 0, closeModal = null;
function asyncAlert({ title, text, actions, allowClose = true, textAfterButtons = "", actionsAsGrid = false }) {
alertsOpen++;
return new Promise((resolve)=>{
const popupWrap = document.createElement("div");
document.body.appendChild(popupWrap);
popupWrap.className = "popup " + (actionsAsGrid ? "actionsAsGrid " : "");
function closeWithResult(value) {
resolve(value);
// Doing this async lets the menu scroll persist if it's shown a second time
setTimeout(()=>{
document.body.removeChild(popupWrap);
});
}
if (allowClose) {
const closeButton = document.createElement("button");
closeButton.title = (0, _i18N.t)('play.close_modale_window_tooltip');
closeButton.className = "close-modale";
closeButton.addEventListener("click", (e)=>{
e.preventDefault();
closeWithResult(undefined);
});
closeModal = ()=>{
closeWithResult(undefined);
};
popupWrap.appendChild(closeButton);
}
const popup = document.createElement("div");
if (title) {
const p = document.createElement("h2");
p.innerHTML = title;
popup.appendChild(p);
}
if (text) {
const p = document.createElement("div");
p.innerHTML = text;
popup.appendChild(p);
}
const buttons = document.createElement("section");
popup.appendChild(buttons);
actions?.filter((i)=>i).forEach(({ text, value, help, disabled, className = "", icon = "" })=>{
const button = document.createElement("button");
button.innerHTML = `
${icon}
<div>
<strong>${text}</strong>
<em>${help || ""}</em>
</div>`;
if (disabled) button.setAttribute("disabled", "disabled");
else button.addEventListener("click", (e)=>{
e.preventDefault();
closeWithResult(value);
});
button.className = className;
buttons.appendChild(button);
});
if (textAfterButtons) {
const p = document.createElement("div");
p.className = "textAfterButtons";
p.innerHTML = textAfterButtons;
popup.appendChild(p);
}
popupWrap.appendChild(popup);
popup.querySelector("button:not([disabled])")?.focus();
}).then((v)=>{
alertsOpen--;
closeModal = null;
return v;
}, ()=>{
closeModal = null;
alertsOpen--;
});
}
scoreDisplay.addEventListener("click", (e)=>{
e.preventDefault();
openScorePanel();
});
document.addEventListener("visibilitychange", ()=>{
if (document.hidden) pause(true);
});
async function openScorePanel() {
pause(true);
const cb = await asyncAlert({
title: (0, _i18N.t)('score_panel.title', {
score: gameState.score,
level: gameState.currentLevel + 1,
max: max_levels()
}),
text: `
${gameState.isCreativeModeRun ? "<p>${t('score_panel.test_run}</p>" : ""}
<p>${(0, _i18N.t)('score_panel.upgrades_picked')}</p>
<p>${pickedUpgradesHTMl()}</p>
`,
allowClose: true,
actions: [
{
text: (0, _i18N.t)('score_panel.resume'),
help: (0, _i18N.t)('score_panel.resume_help'),
value: ()=>{}
},
{
text: (0, _i18N.t)('score_panel.restart'),
help: (0, _i18N.t)('score_panel.restart_help'),
value: ()=>{
restart({
levelToAvoid: currentLevelInfo().name
});
}
}
]
});
if (cb) cb();
}
document.getElementById("menu")?.addEventListener("click", (e)=>{
e.preventDefault();
openSettingsPanel();
});
async function openSettingsPanel() {
pause(true);
const actions = [
{
text: (0, _i18N.t)('main_menu.resume'),
help: (0, _i18N.t)('main_menu.resume_help'),
value () {}
},
{
text: (0, _i18N.t)("main_menu.unlocks"),
help: (0, _i18N.t)('main_menu.unlocks_help'),
value () {
openUnlocksList();
}
}
];
for (const key of Object.keys((0, _options.options)))if ((0, _options.options)[key]) actions.push({
disabled: (0, _options.options)[key].disabled(),
icon: (0, _options.isOptionOn)(key) ? (0, _loadGameData.icons)["icon:checkmark_checked"] : (0, _loadGameData.icons)["icon:checkmark_unchecked"],
text: (0, _options.options)[key].name,
help: (0, _options.options)[key].help,
value: ()=>{
(0, _options.toggleOption)(key);
openSettingsPanel();
}
});
const creativeModeThreshold = Math.max(...(0, _loadGameData.upgrades).map((u)=>u.threshold));
if (document.fullscreenEnabled || document.webkitFullscreenEnabled) {
if (document.fullscreenElement !== null) actions.push({
text: (0, _i18N.t)('main_menu.fullscreen_exit'),
help: (0, _i18N.t)('main_menu.fullscreen_exit_help'),
icon: (0, _loadGameData.icons)["icon:exit_fullscreen"],
value () {
toggleFullScreen();
}
});
else actions.push({
text: (0, _i18N.t)('main_menu.fullscreen'),
help: (0, _i18N.t)('main_menu.fullscreen_help'),
icon: (0, _loadGameData.icons)["icon:fullscreen"],
value () {
toggleFullScreen();
}
});
}
actions.push({
text: (0, _i18N.t)('sandbox.title'),
help: getTotalScore() < creativeModeThreshold ? (0, _i18N.t)('sandbox.unlocks_at', {
score: creativeModeThreshold
}) : (0, _i18N.t)('sandbox.help'),
disabled: getTotalScore() < creativeModeThreshold,
async value () {
let creativeModePerks = (0, _settings.getSettingValue)('creativeModePerks', {}), choice;
while(choice = await asyncAlert({
title: (0, _i18N.t)('sandbox.title'),
text: (0, _i18N.t)('sandbox.instructions'),
actionsAsGrid: true,
actions: [
...(0, _loadGameData.upgrades).map((u)=>({
icon: u.icon,
text: u.name,
help: (creativeModePerks[u.id] || 0) + "/" + u.max,
value: u,
className: creativeModePerks[u.id] ? "" : "grey-out-unless-hovered"
})),
{
text: (0, _i18N.t)('sandbox.start'),
value: "start"
}
]
})){
if (choice === "start") {
(0, _settings.setSettingValue)('creativeModePerks', creativeModePerks);
restart({
perks: creativeModePerks
});
break;
} else if (choice) creativeModePerks[choice.id] = ((creativeModePerks[choice.id] || 0) + 1) % (choice.max + 1);
}
}
});
actions.push({
text: (0, _i18N.t)('main_menu.reset'),
help: (0, _i18N.t)('main_menu.reset_help'),
async value () {
if (await asyncAlert({
title: (0, _i18N.t)('main_menu.reset'),
text: (0, _i18N.t)('main_menu.reset_instruction'),
actions: [
{
text: (0, _i18N.t)('main_menu.reset_confirm'),
value: true
},
{
text: (0, _i18N.t)('main_menu.reset_cancel'),
value: false
}
],
allowClose: true
})) {
localStorage.clear();
window.location.reload();
}
}
});
actions.push({
text: (0, _i18N.t)('main_menu.language'),
help: (0, _i18N.t)('main_menu.language_help'),
async value () {
const pick = await asyncAlert({
title: (0, _i18N.t)('main_menu.language'),
text: (0, _i18N.t)('main_menu.language_help'),
actions: [
{
text: 'English',
value: 'en'
},
{
text: "Fran\xe7ais",
value: 'fr'
}
],
allowClose: true
});
2025-03-16 10:24:46 +01:00
if (pick && pick !== (0, _i18N.getCurrentLang)() && await confirmRestart()) {
(0, _settings.setSettingValue)('lang', pick);
window.location.reload();
}
}
});
2025-03-15 21:29:38 +01:00
const cb = await asyncAlert({
title: (0, _i18N.t)('main_menu.title'),
text: ``,
allowClose: true,
actions,
textAfterButtons: (0, _i18N.t)('main_menu.footer_html', {
appVersion: (0, _loadGameData.appVersion)
})
});
if (cb) cb();
}
async function openUnlocksList() {
const ts = getTotalScore();
const actions = [
...(0, _loadGameData.upgrades).sort((a, b)=>a.threshold - b.threshold).map(({ name, id, threshold, icon, fullHelp })=>({
text: name,
help: ts >= threshold ? fullHelp : (0, _i18N.t)('unlocks.unlocks_at', {
threshold
}),
disabled: ts < threshold,
value: {
perks: {
[id]: 1
}
},
icon
})),
...(0, _loadGameData.allLevels).sort((a, b)=>a.threshold - b.threshold).map((l)=>{
const available = ts >= l.threshold;
return {
text: l.name,
help: available ? (0, _i18N.t)('unlocks.level_description', {
size: l.size,
bricks: l.bricks.filter((i)=>i).length
}) : (0, _i18N.t)('unlocks.unlocks_at', {
threshold: l.threshold
}),
disabled: !available,
value: {
level: l.name
},
icon: (0, _loadGameData.icons)[l.name]
};
})
];
const percentUnlock = Math.round(actions.filter((a)=>!a.disabled).length / actions.length * 100);
const tryOn = await asyncAlert({
title: (0, _i18N.t)('unlocks.title', {
percentUnlock
}),
text: `<p>${(0, _i18N.t)('unlocks.intro', {
ts
})}
${percentUnlock < 100 ? (0, _i18N.t)('unlocks.greyed_out_help') : ""}</p>
`,
textAfterButtons: `<p>
Your high score is ${gameState.highScore}.
Click an item above to start a run with it.
</p>`,
actions,
allowClose: true
});
if (tryOn) {
2025-03-16 10:24:46 +01:00
if (await confirmRestart()) restart(tryOn);
2025-03-15 21:29:38 +01:00
}
}
2025-03-16 10:24:46 +01:00
async function confirmRestart() {
if (!gameState.currentLevel) return true;
return asyncAlert({
title: (0, _i18N.t)('confirmRestart.title'),
text: (0, _i18N.t)('confirmRestart.text'),
actions: [
{
value: true,
text: (0, _i18N.t)('confirmRestart.yes')
},
{
value: false,
text: (0, _i18N.t)('confirmRestart.no')
}
]
});
}
2025-03-15 21:29:38 +01:00
function distance2(a, b) {
return Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2);
}
function distanceBetween(a, b) {
return Math.sqrt(distance2(a, b));
}
function rainbowColor() {
return `hsl(${Math.round(gameState.levelTime / 4) * 2 % 360},100%,70%)`;
}
function repulse(a, b, power, impactsBToo) {
const distance = distanceBetween(a, b);
// Ensure we don't get soft locked
const max = gameState.gameZoneWidth / 2;
if (distance > max) return;
// Unit vector
const dx = (a.x - b.x) / distance;
const dy = (a.y - b.y) / distance;
const fact = -power * (max - distance) / (max * 1.2) / 3 * Math.min(500, gameState.levelTime) / 500;
if (impactsBToo && typeof b.vx !== "undefined" && typeof b.vy !== "undefined") {
b.vx += dx * fact;
b.vy += dy * fact;
}
a.vx -= dx * fact;
a.vy -= dy * fact;
const speed = 10;
const rand = 2;
gameState.flashes.push({
type: "particle",
duration: 100,
time: gameState.levelTime,
size: gameState.coinSize / 2,
color: rainbowColor(),
ethereal: true,
x: a.x,
y: a.y,
vx: -dx * speed + a.vx + (Math.random() - 0.5) * rand,
vy: -dy * speed + a.vy + (Math.random() - 0.5) * rand
});
if (impactsBToo && typeof b.vx !== "undefined" && typeof b.vy !== "undefined") gameState.flashes.push({
type: "particle",
duration: 100,
time: gameState.levelTime,
size: gameState.coinSize / 2,
color: rainbowColor(),
ethereal: true,
x: b.x,
y: b.y,
vx: dx * speed + b.vx + (Math.random() - 0.5) * rand,
vy: dy * speed + b.vy + (Math.random() - 0.5) * rand
});
}
function attract(a, b, power) {
const distance = distanceBetween(a, b);
// Ensure we don't get soft locked
const min = gameState.gameZoneWidth * 0.5;
if (distance < min) return;
// Unit vector
const dx = (a.x - b.x) / distance;
const dy = (a.y - b.y) / distance;
const fact = power * (distance - min) / min * Math.min(500, gameState.levelTime) / 500;
b.vx += dx * fact;
b.vy += dy * fact;
a.vx -= dx * fact;
a.vy -= dy * fact;
const speed = 10;
const rand = 2;
gameState.flashes.push({
type: "particle",
duration: 100,
time: gameState.levelTime,
size: gameState.coinSize / 2,
color: rainbowColor(),
ethereal: true,
x: a.x,
y: a.y,
vx: dx * speed + a.vx + (Math.random() - 0.5) * rand,
vy: dy * speed + a.vy + (Math.random() - 0.5) * rand
});
gameState.flashes.push({
type: "particle",
duration: 100,
time: gameState.levelTime,
size: gameState.coinSize / 2,
color: rainbowColor(),
ethereal: true,
x: b.x,
y: b.y,
vx: -dx * speed + b.vx + (Math.random() - 0.5) * rand,
vy: -dy * speed + b.vy + (Math.random() - 0.5) * rand
});
}
let mediaRecorder, captureStream, captureTrack, recordCanvas, recordCanvasCtx;
function recordOneFrame() {
if (!(0, _options.isOptionOn)("record")) return;
if (!gameState.running) return;
if (!captureStream) return;
drawMainCanvasOnSmallCanvas();
if (captureTrack?.requestFrame) captureTrack?.requestFrame();
else if (captureStream?.requestFrame) captureStream.requestFrame();
}
function drawMainCanvasOnSmallCanvas() {
if (!recordCanvasCtx) return;
recordCanvasCtx.drawImage(gameCanvas, gameState.offsetXRoundedDown, 0, gameState.gameZoneWidthRoundedUp, gameState.gameZoneHeight, 0, 0, recordCanvas.width, recordCanvas.height);
// Here we don't use drawText as we don't want to cache a picture for each distinct value of score
recordCanvasCtx.fillStyle = "#FFF";
recordCanvasCtx.textBaseline = "top";
recordCanvasCtx.font = "12px monospace";
recordCanvasCtx.textAlign = "right";
recordCanvasCtx.fillText(gameState.score.toString(), recordCanvas.width - 12, 12);
recordCanvasCtx.textAlign = "left";
recordCanvasCtx.fillText("Level " + (gameState.currentLevel + 1) + "/" + max_levels(), 12, 12);
}
function startRecordingGame() {
if (!(0, _options.isOptionOn)("record")) return;
if (mediaRecorder) return;
if (!recordCanvas) {
// Smaller canvas with fewer details
recordCanvas = document.createElement("canvas");
recordCanvasCtx = recordCanvas.getContext("2d", {
antialias: false,
alpha: false
});
captureStream = recordCanvas.captureStream(0);
captureTrack = captureStream.getVideoTracks()[0];
const track = (0, _sounds.getAudioRecordingTrack)();
if (track) captureStream.addTrack(track.stream.getAudioTracks()[0]);
}
recordCanvas.width = gameState.gameZoneWidthRoundedUp;
recordCanvas.height = gameState.gameZoneHeight;
// drawMainCanvasOnSmallCanvas()
const recordedChunks = [];
const instance = new MediaRecorder(captureStream, {
videoBitsPerSecond: 3500000
});
mediaRecorder = instance;
instance.start();
mediaRecorder.pause();
instance.ondataavailable = function(event) {
recordedChunks.push(event.data);
};
instance.onstop = async function() {
let targetDiv;
let blob = new Blob(recordedChunks, {
type: "video/webm"
});
if (blob.size < 200000) return; // under 0.2MB, probably bugged out or pointlessly short
while(!(targetDiv = document.getElementById("level-recording-container")))await new Promise((r)=>setTimeout(r, 200));
const video = document.createElement("video");
video.autoplay = true;
video.controls = false;
video.disablePictureInPicture = true;
video.disableRemotePlayback = true;
video.width = recordCanvas.width;
video.height = recordCanvas.height;
video.loop = true;
video.muted = true;
video.playsInline = true;
video.src = URL.createObjectURL(blob);
const a = document.createElement("a");
a.download = captureFileName("webm");
a.target = "_blank";
a.href = video.src;
a.textContent = (0, _i18N.t)('main_menu.record_download', {
size: (blob.size / 1000000).toFixed(2)
});
targetDiv.appendChild(video);
targetDiv.appendChild(a);
};
}
function pauseRecording() {
if (!(0, _options.isOptionOn)("record")) return;
if (mediaRecorder?.state === "recording") mediaRecorder?.pause();
}
function resumeRecording() {
if (!(0, _options.isOptionOn)("record")) return;
if (mediaRecorder?.state === "paused") mediaRecorder.resume();
}
function stopRecording() {
if (!(0, _options.isOptionOn)("record")) return;
if (!mediaRecorder) return;
mediaRecorder?.stop();
mediaRecorder = null;
}
function captureFileName(ext = "webm") {
return "breakout-71-capture-" + new Date().toISOString().replace(/[^0-9\-]+/gi, "-") + "." + ext;
}
function findLast(arr, predicate) {
let i = arr.length;
while(--i)if (predicate(arr[i], i, arr)) return arr[i];
}
function toggleFullScreen() {
try {
if (document.fullscreenElement !== null) {
if (document.exitFullscreen) document.exitFullscreen();
else if (document.webkitCancelFullScreen) document.webkitCancelFullScreen();
} else {
const docel = document.documentElement;
if (docel.requestFullscreen) docel.requestFullscreen();
else if (docel.webkitRequestFullscreen) docel.webkitRequestFullscreen();
}
} catch (e) {
console.warn(e);
}
}
const pressed = {
ArrowLeft: 0,
ArrowRight: 0,
Shift: 0
};
function setKeyPressed(key, on) {
pressed[key] = on;
gameState.keyboardPuckSpeed = (pressed.ArrowRight - pressed.ArrowLeft) * (1 + pressed.Shift * 2) * gameState.gameZoneWidth / 50;
}
document.addEventListener("keydown", (e)=>{
if (e.key.toLowerCase() === "f" && !e.ctrlKey && !e.metaKey) toggleFullScreen();
else if (e.key in pressed) setKeyPressed(e.key, 1);
if (e.key === " " && !alertsOpen) {
if (gameState.running) pause(true);
else play();
} else return;
e.preventDefault();
});
document.addEventListener("keyup", async (e)=>{
const focused = document.querySelector("button:focus");
if (e.key in pressed) setKeyPressed(e.key, 0);
else if (e.key === "ArrowDown" && focused?.nextElementSibling?.tagName === "BUTTON") focused?.nextElementSibling?.focus();
else if (e.key === "ArrowUp" && focused?.previousElementSibling?.tagName === "BUTTON") focused?.previousElementSibling?.focus();
else if (e.key === "Escape" && closeModal) closeModal();
else if (e.key === "Escape" && gameState.running) pause(true);
else if (e.key.toLowerCase() === "m" && !alertsOpen) openSettingsPanel().then();
else if (e.key.toLowerCase() === "s" && !alertsOpen) openScorePanel().then();
else if (e.key.toLowerCase() === "r" && !alertsOpen) {
2025-03-16 10:24:46 +01:00
if (await confirmRestart()) restart({
2025-03-15 21:29:38 +01:00
levelToAvoid: currentLevelInfo().name
});
} else return;
e.preventDefault();
});
function newGameState(params) {
const totalScoreAtRunStart = getTotalScore();
const firstLevel = params?.level ? (0, _loadGameData.allLevels).filter((l)=>l.name === params?.level) : [];
const restInRandomOrder = (0, _loadGameData.allLevels).filter((l)=>totalScoreAtRunStart >= l.threshold).filter((l)=>l.name !== params?.level).filter((l)=>l.name !== params?.levelToAvoid).sort(()=>Math.random() - 0.5);
const runLevels = firstLevel.concat(restInRandomOrder.slice(0, 10).sort((a, b)=>a.sortKey - b.sortKey));
const perks = {
...(0, _gameUtils.makeEmptyPerksMap)((0, _loadGameData.upgrades)),
...params?.perks || {}
};
const gameState = {
runLevels,
currentLevel: 0,
perks,
puckWidth: 200,
baseSpeed: 12,
combo: 1,
gridSize: 12,
running: false,
puckPosition: 400,
pauseTimeout: null,
canvasWidth: 0,
canvasHeight: 0,
offsetX: 0,
offsetXRoundedDown: 0,
gameZoneWidth: 0,
gameZoneWidthRoundedUp: 0,
gameZoneHeight: 0,
brickWidth: 0,
needsRender: true,
score: 0,
lastScoreIncrease: -1000,
lastExplosion: -1000,
highScore: parseFloat(localStorage.getItem("breakout-3-hs") || "0"),
balls: [],
ballsColor: "white",
bricks: [],
flashes: [],
coins: [],
levelStartScore: 0,
levelMisses: 0,
levelSpawnedCoins: 0,
lastPlayedCoinGrab: 0,
MAX_COINS: 400,
MAX_PARTICLES: 600,
puckColor: "#FFF",
ballSize: 20,
coinSize: 14,
puckHeight: 20,
totalScoreAtRunStart,
isCreativeModeRun: (0, _gameUtils.sumOfKeys)(perks) > 1,
pauseUsesDuringRun: 0,
keyboardPuckSpeed: 0,
lastTick: performance.now(),
lastTickDown: 0,
runStatistics: {
started: Date.now(),
levelsPlayed: 0,
runTime: 0,
coins_spawned: 0,
score: 0,
bricks_broken: 0,
misses: 0,
balls_lost: 0,
puck_bounces: 0,
upgrades_picked: 1,
max_combo: 1,
max_level: 0
},
lastOffered: {},
levelTime: 0,
autoCleanUses: 0
};
(0, _resetBalls.resetBalls)(gameState);
if (!(0, _gameUtils.sumOfKeys)(gameState.perks)) {
const giftable = getPossibleUpgrades(gameState).filter((u)=>u.giftable);
const randomGift = (0, _options.isOptionOn)("easy") && "slow_down" || giftable[Math.floor(Math.random() * giftable.length)].id;
perks[randomGift] = 1;
dontOfferTooSoon(gameState, randomGift);
}
for (let perk of (0, _loadGameData.upgrades))if (gameState.perks[perk.id]) dontOfferTooSoon(gameState, perk.id);
return gameState;
}
const gameState = newGameState({});
function restart(params) {
Object.assign(gameState, newGameState(params));
pauseRecording();
setLevel(0);
}
restart({});
fitSize();
tick();
// @ts-ignore
// window.stressTest= ()=>restart({level:'Shark',perks:{base_combo:100, pierce:10, multiball:8}})
window.stressTest = ()=>restart({
level: 'Shark',
perks: {
sapper: 2,
pierce: 10,
multiball: 3
}
});
},{"./loadGameData":"l1B4x","./options":"d5NoS","./sounds":"dQKPV","./resetBalls":"gVgfx","./game_utils":"cEeac","./combo":"9S1mS","./sw_loader":"kRstf","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3","./i18n/i18n":"eNPRm","./settings":"5blfu"}],"l1B4x":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "appVersion", ()=>appVersion);
parcelHelpers.export(exports, "icons", ()=>icons);
parcelHelpers.export(exports, "allLevels", ()=>allLevels);
parcelHelpers.export(exports, "upgrades", ()=>upgrades);
var _paletteJson = require("./palette.json");
var _paletteJsonDefault = parcelHelpers.interopDefault(_paletteJson);
var _levelsJson = require("./levels.json");
var _levelsJsonDefault = parcelHelpers.interopDefault(_levelsJson);
var _versionJson = require("./version.json");
var _versionJsonDefault = parcelHelpers.interopDefault(_versionJson);
var _rawUpgrades = require("./rawUpgrades");
var _getLevelBackground = require("./getLevelBackground");
const palette = (0, _paletteJsonDefault.default);
const rawLevelsList = (0, _levelsJsonDefault.default);
const appVersion = (0, _versionJsonDefault.default);
let levelIconHTMLCanvas = document.createElement("canvas");
const levelIconHTMLCanvasCtx = levelIconHTMLCanvas.getContext("2d", {
antialias: false,
alpha: true
});
function levelIconHTML(bricks, levelSize, color) {
const size = 40;
const c = levelIconHTMLCanvas;
const ctx = levelIconHTMLCanvasCtx;
c.width = size;
c.height = size;
if (color) {
ctx.fillStyle = color;
ctx.fillRect(0, 0, size, size);
} else ctx.clearRect(0, 0, size, size);
const pxSize = size / levelSize;
for(let x = 0; x < levelSize; x++)for(let y = 0; y < levelSize; y++){
const c = bricks[y * levelSize + x];
if (c) {
ctx.fillStyle = c;
ctx.fillRect(Math.floor(pxSize * x), Math.floor(pxSize * y), Math.ceil(pxSize), Math.ceil(pxSize));
}
}
return `<img alt="" width="${size}" height="${size}" src="${c.toDataURL()}"/>`;
}
const icons = {};
const allLevels = rawLevelsList.map((level)=>{
const bricks = level.bricks.split("").map((c)=>palette[c]).slice(0, level.size * level.size);
const icon = levelIconHTML(bricks, level.size, level.color);
icons[level.name] = icon;
return {
...level,
bricks,
icon,
svg: (0, _getLevelBackground.getLevelBackground)(level)
};
}).filter((l)=>!l.name.startsWith("icon:")).map((l, li)=>({
...l,
threshold: li < 8 ? 0 : Math.round(Math.min(Math.pow(10, 1 + (li + l.size) / 30) * 10, 5000) * li),
sortKey: (Math.random() + 3) / 3.5 * l.bricks.filter((i)=>i).length
}));
const upgrades = (0, _rawUpgrades.rawUpgrades).map((u)=>({
...u,
icon: icons["icon:" + u.id]
}));
},{"./palette.json":"jhnsJ","./levels.json":"kqnNl","./version.json":"h1X9A","./rawUpgrades":"cvg5m","./getLevelBackground":"7OIPf","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"h1X9A":[function(require,module,exports,__globalThis) {
module.exports = JSON.parse("\"29033878\"");
},{}],"cvg5m":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "rawUpgrades", ()=>rawUpgrades);
var _i18N = require("./i18n/i18n");
const rawUpgrades = [
{
requires: "",
threshold: 0,
giftable: false,
id: "extra_life",
max: 7,
name: (0, _i18N.t)('upgrades.extra_life.name'),
help: (lvl)=>lvl === 1 ? (0, _i18N.t)('upgrades.extra_life.help') : (0, _i18N.t)('upgrades.extra_life.help_plural', {
lvl
}),
fullHelp: (0, _i18N.t)('upgrades.extra_life.fullHelp')
},
{
requires: "",
threshold: 0,
id: "streak_shots",
giftable: true,
max: 1,
name: (0, _i18N.t)('upgrades.streak_shots.name'),
help: (lvl)=>(0, _i18N.t)('upgrades.streak_shots.help', {
lvl
}),
fullHelp: (0, _i18N.t)('upgrades.streak_shots.fullHelp')
},
{
requires: "",
threshold: 0,
id: "base_combo",
giftable: true,
max: 7,
name: (0, _i18N.t)('upgrades.base_combo.name'),
help: (lvl)=>(0, _i18N.t)('upgrades.base_combo.help', {
coins: 1 + lvl * 3
}),
fullHelp: (0, _i18N.t)('upgrades.base_combo.fullHelp')
},
{
requires: "",
threshold: 0,
giftable: false,
id: "slow_down",
max: 2,
name: (0, _i18N.t)('upgrades.slow_down.name'),
help: ()=>(0, _i18N.t)('upgrades.slow_down.help'),
fullHelp: (0, _i18N.t)('upgrades.slow_down.fullHelp')
},
{
requires: "",
threshold: 0,
giftable: false,
id: "bigger_puck",
max: 2,
name: (0, _i18N.t)('upgrades.bigger_puck.name'),
help: ()=>(0, _i18N.t)('upgrades.bigger_puck.help'),
fullHelp: (0, _i18N.t)('upgrades.bigger_puck.fullHelp')
},
{
requires: "",
threshold: 0,
giftable: false,
id: "viscosity",
max: 3,
name: (0, _i18N.t)('upgrades.viscosity.name'),
help: ()=>(0, _i18N.t)('upgrades.viscosity.help'),
fullHelp: (0, _i18N.t)('upgrades.viscosity.fullHelp')
},
{
requires: "",
threshold: 0,
id: "left_is_lava",
giftable: true,
max: 1,
name: (0, _i18N.t)('upgrades.left_is_lava.name'),
help: ()=>(0, _i18N.t)('upgrades.left_is_lava.help'),
fullHelp: (0, _i18N.t)('upgrades.left_is_lava.fullHelp')
},
{
requires: "",
threshold: 0,
id: "right_is_lava",
giftable: true,
max: 1,
name: (0, _i18N.t)('upgrades.right_is_lava.name'),
help: ()=>(0, _i18N.t)('upgrades.right_is_lava.help'),
fullHelp: (0, _i18N.t)('upgrades.right_is_lava.fullHelp')
},
{
requires: "",
threshold: 0,
id: "top_is_lava",
giftable: true,
max: 1,
name: (0, _i18N.t)('upgrades.top_is_lava.name'),
help: ()=>(0, _i18N.t)('upgrades.top_is_lava.help'),
fullHelp: (0, _i18N.t)('upgrades.top_is_lava.fullHelp')
},
{
requires: "",
threshold: 0,
giftable: false,
id: "skip_last",
max: 7,
name: (0, _i18N.t)('upgrades.skip_last.name'),
help: (lvl)=>lvl == 1 ? (0, _i18N.t)('upgrades.skip_last.help') : (0, _i18N.t)('upgrades.skip_last.help_plural', {
2025-03-15 21:29:38 +01:00
lvl
}),
fullHelp: (0, _i18N.t)('upgrades.skip_last.fullHelp')
},
{
requires: "",
threshold: 500,
id: "telekinesis",
giftable: true,
max: 2,
name: (0, _i18N.t)('upgrades.telekinesis.name'),
help: (lvl)=>lvl == 1 ? (0, _i18N.t)('upgrades.telekinesis.help') : (0, _i18N.t)('upgrades.telekinesis.help_plural'),
fullHelp: (0, _i18N.t)('upgrades.telekinesis.fullHelp')
},
{
requires: "",
threshold: 1000,
giftable: false,
id: "coin_magnet",
max: 3,
name: (0, _i18N.t)('upgrades.coin_magnet.name'),
help: (lvl)=>lvl == 1 ? (0, _i18N.t)('upgrades.coin_magnet.help') : (0, _i18N.t)('upgrades.coin_magnet.help_plural'),
fullHelp: (0, _i18N.t)('upgrades.coin_magnet.fullHelp')
},
{
requires: "",
threshold: 1500,
id: "multiball",
giftable: true,
max: 6,
name: (0, _i18N.t)('upgrades.multiball.name'),
help: (lvl)=>(0, _i18N.t)('upgrades.multiball.help', {
count: lvl + 1
}),
fullHelp: (0, _i18N.t)('upgrades.multiball.fullHelp')
},
{
requires: "",
threshold: 2000,
giftable: false,
id: "smaller_puck",
max: 2,
name: (0, _i18N.t)('upgrades.smaller_puck.name'),
help: (lvl)=>lvl == 1 ? (0, _i18N.t)('upgrades.smaller_puck.help') : (0, _i18N.t)('upgrades.smaller_puck.help_plural'),
fullHelp: (0, _i18N.t)('upgrades.smaller_puck.fullHelp')
},
{
requires: "",
threshold: 3000,
id: "pierce",
giftable: true,
max: 3,
name: (0, _i18N.t)('upgrades.pierce.name'),
help: (lvl)=>(0, _i18N.t)('upgrades.pierce.help', {
count: 3 * lvl
}),
fullHelp: (0, _i18N.t)('upgrades.pierce.fullHelp')
},
{
requires: "",
threshold: 4000,
id: "picky_eater",
giftable: true,
max: 1,
name: (0, _i18N.t)('upgrades.picky_eater.name'),
help: (lvl)=>(0, _i18N.t)('upgrades.picky_eater.help'),
fullHelp: (0, _i18N.t)('upgrades.picky_eater.fullHelp')
},
{
requires: "",
threshold: 5000,
giftable: false,
id: "metamorphosis",
max: 1,
name: (0, _i18N.t)('upgrades.metamorphosis.name'),
help: (lvl)=>(0, _i18N.t)('upgrades.metamorphosis.help'),
fullHelp: (0, _i18N.t)('upgrades.metamorphosis.fullHelp')
},
{
requires: "",
threshold: 6000,
id: "compound_interest",
giftable: true,
max: 1,
name: (0, _i18N.t)('upgrades.compound_interest.name'),
help: (lvl)=>(0, _i18N.t)('upgrades.compound_interest.help'),
fullHelp: (0, _i18N.t)('upgrades.compound_interest.fullHelp')
},
{
requires: "",
threshold: 7000,
id: "hot_start",
giftable: true,
max: 3,
name: (0, _i18N.t)('upgrades.hot_start.name'),
help: (lvl)=>(0, _i18N.t)('upgrades.hot_start.help', {
start: lvl * 15 + 1,
lvl
}),
fullHelp: (0, _i18N.t)('upgrades.hot_start.fullHelp')
},
{
requires: "",
threshold: 9000,
id: "sapper",
giftable: true,
max: 7,
name: (0, _i18N.t)('upgrades.sapper.name'),
help: (lvl)=>lvl == 1 ? (0, _i18N.t)('upgrades.sapper.help') : (0, _i18N.t)('upgrades.sapper.help_plural', {
lvl
}),
fullHelp: (0, _i18N.t)('upgrades.sapper.fullHelp')
},
{
requires: "",
threshold: 11000,
id: "bigger_explosions",
giftable: false,
max: 1,
name: (0, _i18N.t)('upgrades.bigger_explosions.name'),
help: (lvl)=>(0, _i18N.t)('upgrades.bigger_explosions.help'),
fullHelp: (0, _i18N.t)('upgrades.bigger_explosions.fullHelp')
},
{
requires: "",
threshold: 13000,
giftable: false,
id: "extra_levels",
max: 3,
name: (0, _i18N.t)('upgrades.extra_levels.name'),
help: (lvl)=>(0, _i18N.t)('upgrades.extra_levels.help', {
count: lvl + 7
}),
fullHelp: (0, _i18N.t)('upgrades.extra_levels.fullHelp')
},
{
requires: "",
threshold: 15000,
giftable: false,
id: "pierce_color",
max: 1,
name: (0, _i18N.t)('upgrades.pierce_color.name'),
help: (lvl)=>(0, _i18N.t)('upgrades.pierce_color.help'),
fullHelp: (0, _i18N.t)('upgrades.pierce_color.fullHelp')
},
{
requires: "",
threshold: 18000,
giftable: false,
id: "soft_reset",
max: 2,
name: (0, _i18N.t)('upgrades.soft_reset.name'),
help: (lvl)=>(0, _i18N.t)('upgrades.soft_reset.help'),
fullHelp: (0, _i18N.t)('upgrades.soft_reset.fullHelp')
},
{
requires: "multiball",
threshold: 21000,
giftable: false,
id: "ball_repulse_ball",
max: 3,
name: (0, _i18N.t)('upgrades.ball_repulse_ball.name'),
help: (lvl)=>lvl == 1 ? (0, _i18N.t)('upgrades.ball_repulse_ball.help') : (0, _i18N.t)('upgrades.ball_repulse_ball.help_plural'),
fullHelp: (0, _i18N.t)('upgrades.ball_repulse_ball.fullHelp')
},
{
requires: "multiball",
threshold: 25000,
giftable: false,
id: "ball_attract_ball",
max: 3,
name: (0, _i18N.t)('upgrades.ball_attract_ball.name'),
help: (lvl)=>lvl == 1 ? (0, _i18N.t)('upgrades.ball_attract_ball.help') : (0, _i18N.t)('upgrades.ball_attract_ball.help_plural'),
fullHelp: (0, _i18N.t)('upgrades.ball_attract_ball.fullHelp')
},
{
requires: "",
threshold: 30000,
giftable: false,
id: "puck_repulse_ball",
max: 2,
name: (0, _i18N.t)('upgrades.puck_repulse_ball.name'),
help: (lvl)=>lvl == 1 ? (0, _i18N.t)('upgrades.puck_repulse_ball.help') : (0, _i18N.t)('upgrades.puck_repulse_ball.help_plural'),
fullHelp: (0, _i18N.t)('upgrades.puck_repulse_ball.fullHelp')
},
{
requires: "",
threshold: 35000,
giftable: false,
id: "wind",
max: 3,
name: (0, _i18N.t)('upgrades.wind.name'),
help: (lvl)=>lvl == 1 ? (0, _i18N.t)('upgrades.wind.help') : (0, _i18N.t)('upgrades.wind.help_plural'),
fullHelp: (0, _i18N.t)('upgrades.wind.fullHelp')
},
{
requires: "",
threshold: 40000,
giftable: false,
id: "sturdy_bricks",
max: 4,
name: (0, _i18N.t)('upgrades.telekinesis.name'),
help: (lvl)=>lvl == 1 ? (0, _i18N.t)('upgrades.telekinesis.help') : (0, _i18N.t)('upgrades.telekinesis.help_plural'),
fullHelp: (0, _i18N.t)('upgrades.telekinesis.fullHelp')
},
{
requires: "",
threshold: 45000,
giftable: false,
id: "respawn",
max: 4,
name: (0, _i18N.t)('upgrades.respawn.name'),
help: (lvl)=>lvl == 1 ? (0, _i18N.t)('upgrades.respawn.help') : (0, _i18N.t)('upgrades.respawn.help_plural'),
fullHelp: (0, _i18N.t)('upgrades.respawn.fullHelp')
},
{
requires: "",
threshold: 50000,
giftable: false,
id: "one_more_choice",
max: 3,
name: (0, _i18N.t)('upgrades.one_more_choice.name'),
help: (lvl)=>(0, _i18N.t)('upgrades.one_more_choice.help'),
2025-03-15 21:29:38 +01:00
fullHelp: (0, _i18N.t)('upgrades.one_more_choice.fullHelp')
},
{
requires: "",
threshold: 55000,
giftable: false,
id: "instant_upgrade",
max: 2,
name: (0, _i18N.t)('upgrades.instant_upgrade.name'),
help: (lvl)=>(0, _i18N.t)('upgrades.instant_upgrade.help'),
2025-03-15 21:29:38 +01:00
fullHelp: (0, _i18N.t)('upgrades.instant_upgrade.fullHelp')
}
];
},{"@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3","./i18n/i18n":"eNPRm"}],"eNPRm":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "getCurrentLang", ()=>getCurrentLang);
2025-03-15 21:29:38 +01:00
parcelHelpers.export(exports, "t", ()=>t);
var _frJson = require("./fr.json");
var _frJsonDefault = parcelHelpers.interopDefault(_frJson);
var _enJson = require("./en.json");
var _enJsonDefault = parcelHelpers.interopDefault(_enJson);
var _settings = require("../settings");
const languages = {
fr: (0, _frJsonDefault.default),
en: (0, _enJsonDefault.default)
};
function getCurrentLang() {
return (0, _settings.getSettingValue)('lang', getFirstBrowserLanguage());
}
2025-03-15 21:29:38 +01:00
function t(key, params = {}) {
const lang = getCurrentLang();
2025-03-15 21:29:38 +01:00
let template = languages[lang]?.[key] || languages.en[key];
for(let key in params)template = template.split('{{' + key + '}}').join(`${params[key]}`);
return template;
}
function getFirstBrowserLanguage() {
const preferred_languages = [
...navigator.languages,
navigator.language,
'en'
].filter((i)=>i).map((i)=>i.slice(0, 2).toLowerCase());
const supported = Object.keys(languages);
return preferred_languages.find((k)=>supported.includes(k)) || 'en';
}
},{"./fr.json":"b97sx","./en.json":"uYc9N","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3","../settings":"5blfu"}],"b97sx":[function(require,module,exports,__globalThis) {
2025-03-16 11:29:43 +01:00
module.exports = JSON.parse('{"confirmRestart.no":"Annuler ,continuer ma partie en cours","confirmRestart.text":"Vous \xeates sur le point de commencer une nouvelle partie, est-ce vraiment ce que vous vouliez ?","confirmRestart.title":"D\xe9marrer une nouvelle partie ?","confirmRestart.yes":"Commencer une nouvelle partie","gameOver.cumulative_total":"Votre score total cumul\xe9 est pass\xe9 de {{startTs}} \xe0 {{endTs}}.","gameOver.lost.summary":"Vous avez fait tomber la balle apr\xe8s avoir attrap\xe9 {{score}} pi\xe8ces.","gameOver.lost.title":"Balle perdue","gameOver.next_unlock":"Marquez {{points}} points suppl\xe9mentaires pour d\xe9bloquer la prochaine am\xe9lioration ou le prochain niveau.","gameOver.restart":"Nouvelle partie","gameOver.stats.balls_lost":"Balles perdues","gameOver.stats.bricks_broken":"Briques cass\xe9es","gameOver.stats.bricks_per_minute":"Briques cass\xe9es par minute","gameOver.stats.catch_rate":"Taux de capture des pi\xe8ces","gameOver.stats.combo_avg":"Combo moyen","gameOver.stats.combo_max":"Combo maximum","gameOver.stats.duration_per_level":"Dur\xe9e par niveau","gameOver.stats.hit_rate":"Pr\xe9cision","gameOver.stats.intro":"Vous trouverez ci-dessous les statistiques de cette partie compar\xe9es \xe0 vos {{count}} meilleures parties.","gameOver.stats.level_reached":"Niveau atteint","gameOver.stats.total_score":"Score total","gameOver.stats.upgrades_applied":"Mises \xe0 jour appliqu\xe9es","gameOver.test_run":"Cette partie de test et son score ne sont pas enregistr\xe9s.","gameOver.unlocked_count":"Vous avez d\xe9bloqu\xe9 {{count}} objet(s) :","gameOver.win.summary":"Vous avez nettoy\xe9 tous les niveaux pour cette partie, en attrapant {{score}} pi\xe8ces au total.","gameOver.win.title":"Partie termin\xe9e","level_up.after_buttons":"Vous venez de terminer le niveau {{level}}/{{max}} et vous avez choisi ces am\xe9liorations jusqu\'\xe0 pr\xe9sent :","level_up.before_buttons":"Vous avez attrap\xe9 {{score}} pi\xe8ces {{catchGain}} sur {{levelSpawnedCoins}} en {{time}} secondes ${timeGain}.\\n\\nVous avez rat\xe9 les briques {{levelMisses}} fois {{missesGain}}.\\n\\n{{compliment}}","level_up.compliment_advice":"Essayez d\'attraper toutes les pi\xe8ces, de ne jamais rater les briques ou de terminer le niveau en moins de 30 secondes pour obtenir des choix suppl\xe9mentaires et des am\xe9liorations.","level_up.compliment_good":"Bravo !","level_up.compliment_perfect":"Impressionnant, continuez comme \xe7a !","level_up.pick_upgrade_title":"Choisir une am\xe9lioration","level_up.plus_one_choice":"(+1 choix)","level_up.plus_one_upgrade":"(+1 am\xe9lioration et +1 choix)","level_up.unlocked_level":" (Niveau)","level_up.unlocked_perk":" (Am\xe9lioration)","level_up.upgrade_perk_to_level":" niveau {{level}}","main_menu.basic":"Graphismes simplifi\xe9s","main_menu.basic_help":"Moins de particules et effets, meilleures performances.","main_menu.footer_html":" <p> <span>Programm\xe9 en France par <a href=\\"https://lecaro.me\\">Renan LE CARO</a>.</span> <a href=\\"https://breakout.lecaro.me/privacy.html\\" target=\\"_blank\\">Politique de confidentialit\xe9</a> <a href=\\"https://f-droid.org/en/packages/me.lecaro.breakout/\\" target=\\"_blank\\">F-Droid</a> <a href=\\"https://play.google.com/store/apps/details?id=me.lecaro.breakout\\" target=\\"_blank\\">Google Play</a> <a href=\\"https://renanlecaro.itch.io/breakout71\\" target=\\"_blank\\">itch.io</a> <a href=\\"https://gitlab.com/lecarore/breakout71\\" target=\\"_blank\\">Gitlab</a> <a href=\\"https://breakout.lecaro.me/\\" target=\\"_blank\\">Version web</a> <a href=\\"https://news.ycombinator.com/item?id=43183131\\" target=\\"_blank\\">HackerNews</a> <span>v.{{appVersion}}</span></p>","main_menu.fullscreen":"Plein \xe9cran","main_menu.fullscreen_exit":"Quitter le plein \xe9cran","main_menu.fullscreen_exit_help":"Peut ne pas fonctionner sur certaines machines","main_menu.fullscreen_help":"Peut ne pas fonctionner sur certaines machines","main_menu.kid":"Mode enfants","m
2025-03-15 21:29:38 +01:00
},{}],"uYc9N":[function(require,module,exports,__globalThis) {
2025-03-16 11:29:43 +01:00
module.exports = JSON.parse("{\"confirmRestart.no\":\"Cancel\",\"confirmRestart.text\":\"You're about to start a new run, is that really what you wanted ?\",\"confirmRestart.title\":\"Start a new run ?\",\"confirmRestart.yes\":\"Restart game\",\"gameOver.cumulative_total\":\"Your total cumulative score went from {{startTs}} to {{endTs}}.\",\"gameOver.lost.summary\":\"You dropped the ball after catching {{score}} coins.\",\"gameOver.lost.title\":\"Game Over\",\"gameOver.next_unlock\":\"Score {{points}} more points to reach the next unlock\",\"gameOver.restart\":\"Start a new run\",\"gameOver.stats.balls_lost\":\"Balls lost\",\"gameOver.stats.bricks_broken\":\"Bricks broken\",\"gameOver.stats.bricks_per_minute\":\"Bricks broken per minute\",\"gameOver.stats.catch_rate\":\"Catch rate\",\"gameOver.stats.combo_avg\":\"Average combo\",\"gameOver.stats.combo_max\":\"Max combo\",\"gameOver.stats.duration_per_level\":\"Duration per level\",\"gameOver.stats.hit_rate\":\"Hit rate\",\"gameOver.stats.intro\":\"Find below your run statistics compared to your {{count}} best runs.\",\"gameOver.stats.level_reached\":\"Level reached\",\"gameOver.stats.total_score\":\"Total score\",\"gameOver.stats.upgrades_applied\":\"Upgrades applied\",\"gameOver.test_run\":\"This test run and its score are not being recorded\",\"gameOver.unlocked_count\":\"You unlocked {{count}} item(s) :\",\"gameOver.win.summary\":\"You cleared all levels for this run, catching {{score}} coins in total.\",\"gameOver.win.title\":\"Run finished\",\"level_up.after_buttons\":\"You just finished level {{level}}/{{max}} and picked those upgrades so far :\",\"level_up.before_buttons\":\"You caught {{score}} coins {{catchGain}} out of {{levelSpawnedCoins}} in {{time}} seconds ${timeGain}.\\n\\nYou missed {{levelMisses}} times {{missesGain}}.\\n\\n{{compliment}}\",\"level_up.compliment_advice\":\"Try to catch all coins, never miss the bricks or clear the level under 30s to gain additional choices and upgrades.\",\"level_up.compliment_good\":\"Well done !\",\"level_up.compliment_perfect\":\"Impressive, keep it up !\",\"level_up.pick_upgrade_title\":\"Pick an upgrade\",\"level_up.plus_one_choice\":\"(+1 choice)\",\"level_up.plus_one_upgrade\":\"(+1 upgrade and choice)\",\"level_up.unlocked_level\":\" (Level)\",\"level_up.unlocked_perk\":\" (Perk)\",\"level_up.upgrade_perk_to_level\":\" lvl {{level}}\",\"main_menu.basic\":\"Basic graphics\",\"main_menu.basic_help\":\"Fewer particles and flashes, better performance.\",\"main_menu.footer_html\":\" <p> <span>Made in France by <a href=\\\"https://lecaro.me\\\">Renan LE CARO</a>.</span> \\n <a href=\\\"https://breakout.lecaro.me/privacy.html\\\" target=\\\"_blank\\\">Privacy Policy</a>\\n <a href=\\\"https://f-droid.org/en/packages/me.lecaro.breakout/\\\" target=\\\"_blank\\\">F-Droid</a>\\n <a href=\\\"https://play.google.com/store/apps/details?id=me.lecaro.breakout\\\" target=\\\"_blank\\\">Google Play</a>\\n <a href=\\\"https://renanlecaro.itch.io/breakout71\\\" target=\\\"_blank\\\">itch.io</a> \\n <a href=\\\"https://gitlab.com/lecarore/breakout71\\\" target=\\\"_blank\\\">Gitlab</a>\\n <a href=\\\"https://breakout.lecaro.me/\\\" target=\\\"_blank\\\">Web version</a>\\n <a href=\\\"https://news.ycombinator.com/item?id=43183131\\\" target=\\\"_blank\\\">HackerNews</a>\\n <span>v.{{appVersion}}</span></p>\",\"main_menu.fullscreen\":\"Fullscreen\",\"main_menu.fullscreen_exit\":\"Exit Fullscreen\",\"main_menu.fullscreen_exit_help\":\"Might not work on some machines\",\"main_menu.fullscreen_help\":\"Might not work on some machines\",\"main_menu.kid\":\"Kids mode\",\"main_menu.kid_help\":\"Start future runs with \\\"slower ball\\\".\",\"main_menu.language\":\"Language\",\"main_menu.language_help\":\"Choose the game's language\",\"main_menu.mobile\":\"Mobile mode\",\"main_menu.mobile_help\":\"Leaves space for your thumb under the puck.\",\"main_menu.pointer_lock\":\"Mouse pointer lock\",\"main_menu.pointer_lock_help\":\"Locks and hides the mouse cursor
2025-03-15 21:29:38 +01:00
},{}],"5blfu":[function(require,module,exports,__globalThis) {
// Settings
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "getSettingValue", ()=>getSettingValue);
parcelHelpers.export(exports, "setSettingValue", ()=>setSettingValue);
let cachedSettings = {};
function getSettingValue(key, defaultValue) {
if (typeof cachedSettings[key] == "undefined") try {
const ls = localStorage.getItem("breakout-settings-enable-" + key);
if (ls) cachedSettings[key] = JSON.parse(ls);
} catch (e) {
console.warn(e);
}
return cachedSettings[key] ?? defaultValue;
}
function setSettingValue(key, value) {
cachedSettings[key] = value;
try {
localStorage.setItem("breakout-settings-enable-" + key, JSON.stringify(value));
} catch (e) {
console.warn(e);
}
}
},{"@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"d5NoS":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "options", ()=>options);
parcelHelpers.export(exports, "isOptionOn", ()=>isOptionOn);
parcelHelpers.export(exports, "toggleOption", ()=>toggleOption);
var _game = require("./game");
var _i18N = require("./i18n/i18n");
var _settings = require("./settings");
const options = {
sound: {
default: true,
name: (0, _i18N.t)('main_menu.sounds'),
help: (0, _i18N.t)('main_menu.sounds_help'),
afterChange: ()=>{},
disabled: ()=>false
},
"mobile-mode": {
default: window.innerHeight > window.innerWidth,
name: (0, _i18N.t)('main_menu.mobile'),
help: (0, _i18N.t)('main_menu.mobile_help'),
afterChange () {
(0, _game.fitSize)();
},
disabled: ()=>false
},
basic: {
default: false,
name: (0, _i18N.t)('main_menu.basic'),
help: (0, _i18N.t)('main_menu.basic_help'),
afterChange: ()=>{},
disabled: ()=>false
},
pointerLock: {
default: false,
name: (0, _i18N.t)('main_menu.pointer_lock'),
help: (0, _i18N.t)('main_menu.pointer_lock_help'),
afterChange: ()=>{},
disabled: ()=>!document.body.requestPointerLock
},
easy: {
default: false,
name: (0, _i18N.t)('main_menu.kid'),
help: (0, _i18N.t)('main_menu.kid_help'),
afterChange: ()=>{},
disabled: ()=>false
},
// Could not get the sharing to work without loading androidx and all the modern android things so for now I'll just disable sharing in the android app
record: {
default: false,
name: (0, _i18N.t)('main_menu.record'),
help: (0, _i18N.t)('main_menu.record_help'),
afterChange: ()=>{},
disabled () {
return window.location.search.includes("isInWebView=true");
}
}
};
function isOptionOn(key) {
return (0, _settings.getSettingValue)(key, options[key]?.default);
}
function toggleOption(key) {
(0, _settings.setSettingValue)(key, !isOptionOn(key));
options[key].afterChange();
}
},{"./game":"edeGs","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3","./i18n/i18n":"eNPRm","./settings":"5blfu"}],"dQKPV":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "sounds", ()=>sounds);
parcelHelpers.export(exports, "getAudioContext", ()=>getAudioContext);
parcelHelpers.export(exports, "getAudioRecordingTrack", ()=>getAudioRecordingTrack);
var _game = require("./game");
var _options = require("./options");
const sounds = {
wallBeep: (pan)=>{
if (!(0, _options.isOptionOn)("sound")) return;
createSingleBounceSound(800, pixelsToPan(pan));
},
comboIncreaseMaybe: (combo, x, volume)=>{
if (!(0, _options.isOptionOn)("sound")) return;
let delta = 0;
if (!isNaN(lastComboPlayed)) {
if (lastComboPlayed < combo) delta = 1;
if (lastComboPlayed > combo) delta = -1;
}
playShepard(delta, pixelsToPan(x), volume);
lastComboPlayed = combo;
},
comboDecrease () {
if (!(0, _options.isOptionOn)("sound")) return;
playShepard(-1, 0.5, 0.5);
},
coinBounce: (pan, volume)=>{
if (!(0, _options.isOptionOn)("sound")) return;
createSingleBounceSound(1200, pixelsToPan(pan), volume, 0.1, "triangle");
},
explode: (pan)=>{
if (!(0, _options.isOptionOn)("sound")) return;
createExplosionSound(pixelsToPan(pan));
},
lifeLost (pan) {
if (!(0, _options.isOptionOn)("sound")) return;
createShatteredGlassSound(pixelsToPan(pan));
},
coinCatch (pan) {
if (!(0, _options.isOptionOn)("sound")) return;
createSingleBounceSound(900, pixelsToPan(pan), 0.8, 0.1, "triangle");
},
colorChange (pan, volume) {
createSingleBounceSound(400, pixelsToPan(pan), volume, 0.5, "sine");
createSingleBounceSound(800, pixelsToPan(pan), volume * 0.5, 0.2, "square");
}
};
// How to play the code on the leftconst context = new window.AudioContext();
let audioContext, audioRecordingTrack;
function getAudioContext() {
if (!audioContext) {
if (!(0, _options.isOptionOn)("sound")) return null;
audioContext = new (window.AudioContext || window.webkitAudioContext)();
audioRecordingTrack = audioContext.createMediaStreamDestination();
}
return audioContext;
}
function getAudioRecordingTrack() {
getAudioContext();
return audioRecordingTrack;
}
function createSingleBounceSound(baseFreq = 800, pan = 0.5, volume = 1, duration = 0.1, type = "sine") {
const context = getAudioContext();
if (!context) return;
const oscillator = createOscillator(context, baseFreq, type);
// Create a gain node to control the volume
const gainNode = context.createGain();
oscillator.connect(gainNode);
// Create a stereo panner node for left-right panning
const panner = context.createStereoPanner();
panner.pan.setValueAtTime(pan * 2 - 1, context.currentTime);
gainNode.connect(panner);
panner.connect(context.destination);
panner.connect(audioRecordingTrack);
// Set up the gain envelope to simulate the impact and quick decay
gainNode.gain.setValueAtTime(0.8 * volume, context.currentTime); // Initial impact
gainNode.gain.exponentialRampToValueAtTime(0.001, context.currentTime + duration); // Quick decay
// Start the oscillator
oscillator.start(context.currentTime);
// Stop the oscillator after the decay
oscillator.stop(context.currentTime + duration);
}
let noiseBuffer;
function getNoiseBuffer(context) {
if (!noiseBuffer) {
const bufferSize = context.sampleRate * 2; // 2 seconds
noiseBuffer = context.createBuffer(1, bufferSize, context.sampleRate);
const output = noiseBuffer.getChannelData(0);
// Fill the buffer with random noise
for(let i = 0; i < bufferSize; i++)output[i] = Math.random() * 2 - 1;
}
return noiseBuffer;
}
function createExplosionSound(pan = 0.5) {
const context = getAudioContext();
if (!context) return;
// Create an audio buffer
// Create a noise source
const noiseSource = context.createBufferSource();
noiseSource.buffer = getNoiseBuffer(context);
// Create a gain node to control the volume
const gainNode = context.createGain();
noiseSource.connect(gainNode);
// Create a filter to shape the explosion sound
const filter = context.createBiquadFilter();
filter.type = "lowpass";
filter.frequency.setValueAtTime(1000, context.currentTime); // Set the initial frequency
gainNode.connect(filter);
// Create a stereo panner node for left-right panning
const panner = context.createStereoPanner();
panner.pan.setValueAtTime(pan * 2 - 1, context.currentTime); // pan 0 to 1 maps to -1 to 1
// Connect filter to panner and then to the destination (speakers)
filter.connect(panner);
panner.connect(context.destination);
panner.connect(audioRecordingTrack);
// Ramp down the gain to simulate the explosion's fade-out
gainNode.gain.setValueAtTime(1, context.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, context.currentTime + 1);
// Lower the filter frequency over time to create the "explosive" effect
filter.frequency.exponentialRampToValueAtTime(60, context.currentTime + 1);
// Start the noise source
noiseSource.start(context.currentTime);
// Stop the noise source after the sound has played
noiseSource.stop(context.currentTime + 1);
}
function pixelsToPan(pan) {
return Math.max(0, Math.min(1, (pan - (0, _game.gameState).offsetXRoundedDown) / (0, _game.gameState).gameZoneWidthRoundedUp));
}
let lastComboPlayed = NaN, shepard = 6;
function playShepard(delta, pan, volume) {
const shepardMax = 11, factor = 1.05945594920268, baseNote = 392;
shepard += delta;
if (shepard > shepardMax) shepard = 0;
if (shepard < 0) shepard = shepardMax;
const play = (note)=>{
const freq = baseNote * Math.pow(factor, note);
const diff = Math.abs(note - shepardMax * 0.5);
const maxDistanceToIdeal = 1.5 * shepardMax;
const vol = Math.max(0, volume * (1 - diff / maxDistanceToIdeal));
createSingleBounceSound(freq, pan, vol);
return freq.toFixed(2) + " at " + Math.floor(vol * 100) + "% diff " + diff;
};
play(1 + shepardMax + shepard);
play(shepard);
play(-1 - shepardMax + shepard);
}
function createShatteredGlassSound(pan) {
const context = getAudioContext();
if (!context) return;
const oscillators = [
createOscillator(context, 3000, "square"),
createOscillator(context, 4500, "square"),
createOscillator(context, 6000, "square")
];
const gainNode = context.createGain();
const noiseSource = context.createBufferSource();
noiseSource.buffer = getNoiseBuffer(context);
oscillators.forEach((oscillator)=>oscillator.connect(gainNode));
noiseSource.connect(gainNode);
gainNode.gain.setValueAtTime(0.2, context.currentTime);
oscillators.forEach((oscillator)=>oscillator.start());
noiseSource.start();
oscillators.forEach((oscillator)=>oscillator.stop(context.currentTime + 0.2));
noiseSource.stop(context.currentTime + 0.2);
gainNode.gain.exponentialRampToValueAtTime(0.001, context.currentTime + 0.2);
// Create a stereo panner node for left-right panning
const panner = context.createStereoPanner();
panner.pan.setValueAtTime(pan * 2 - 1, context.currentTime);
gainNode.connect(panner);
panner.connect(context.destination);
panner.connect(audioRecordingTrack);
gainNode.connect(panner);
}
// Helper function to create an oscillator with a specific frequency
function createOscillator(context, frequency, type) {
const oscillator = context.createOscillator();
oscillator.type = type;
oscillator.frequency.setValueAtTime(frequency, context.currentTime);
return oscillator;
}
},{"./game":"edeGs","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3","./options":"d5NoS"}],"gVgfx":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "resetBalls", ()=>resetBalls);
parcelHelpers.export(exports, "putBallsAtPuck", ()=>putBallsAtPuck);
var _gameUtils = require("./game_utils");
function resetBalls(gameState) {
const count = 1 + (gameState.perks?.multiball || 0);
const perBall = gameState.puckWidth / (count + 1);
gameState.balls = [];
gameState.ballsColor = "#FFF";
if (gameState.perks.picky_eater || gameState.perks.pierce_color) gameState.ballsColor = (0, _gameUtils.getMajorityValue)(gameState.bricks.filter((i)=>i)) || "#FFF";
for(let i = 0; i < count; i++){
const x = gameState.puckPosition - gameState.puckWidth / 2 + perBall * (i + 1);
const vx = Math.random() > 0.5 ? gameState.baseSpeed : -gameState.baseSpeed;
gameState.balls.push({
x,
previousX: x,
y: gameState.gameZoneHeight - 1.5 * gameState.ballSize,
previousY: gameState.gameZoneHeight - 1.5 * gameState.ballSize,
vx,
previousVX: vx,
vy: -gameState.baseSpeed,
previousVY: -gameState.baseSpeed,
sx: 0,
sy: 0,
sparks: 0,
piercedSinceBounce: 0,
hitSinceBounce: 0,
hitItem: [],
bouncesList: [],
sapperUses: 0
});
}
}
function putBallsAtPuck(gameState) {
// This reset could be abused to cheat quite easily
const count = gameState.balls.length;
const perBall = gameState.puckWidth / (count + 1);
gameState.balls.forEach((ball, i)=>{
const x = gameState.puckPosition - gameState.puckWidth / 2 + perBall * (i + 1);
ball.x = x;
ball.previousX = x;
ball.y = gameState.gameZoneHeight - 1.5 * gameState.ballSize;
ball.previousY = ball.y;
ball.vx = Math.random() > 0.5 ? gameState.baseSpeed : -gameState.baseSpeed;
ball.previousVX = ball.vx;
ball.vy = -gameState.baseSpeed;
ball.previousVY = ball.vy;
ball.sx = 0;
ball.sy = 0;
ball.hitItem = [];
ball.hitSinceBounce = 0;
ball.piercedSinceBounce = 0;
});
}
},{"./game_utils":"cEeac","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"cEeac":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "getMajorityValue", ()=>getMajorityValue);
parcelHelpers.export(exports, "sample", ()=>sample);
parcelHelpers.export(exports, "sumOfKeys", ()=>sumOfKeys);
parcelHelpers.export(exports, "makeEmptyPerksMap", ()=>makeEmptyPerksMap);
function getMajorityValue(arr) {
const count = {};
arr.forEach((v)=>count[v] = (count[v] || 0) + 1);
// Object.values inline polyfill
const max = Math.max(...Object.keys(count).map((k)=>count[k]));
return sample(Object.keys(count).filter((k)=>count[k] == max));
}
function sample(arr) {
return arr[Math.floor(arr.length * Math.random())];
}
function sumOfKeys(obj) {
if (!obj) return 0;
return Object.values(obj)?.reduce((a, b)=>a + b, 0) || 0;
}
const makeEmptyPerksMap = (upgrades)=>{
const p = {};
upgrades.forEach((u)=>p[u.id] = 0);
return p;
};
},{"@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"9S1mS":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "baseCombo", ()=>baseCombo);
parcelHelpers.export(exports, "resetCombo", ()=>resetCombo);
parcelHelpers.export(exports, "decreaseCombo", ()=>decreaseCombo);
var _sounds = require("./sounds");
function baseCombo(gameState) {
return 1 + gameState.perks.base_combo * 3 + gameState.perks.smaller_puck * 5;
}
function resetCombo(gameState, x, y) {
const prev = gameState.combo;
gameState.combo = baseCombo(gameState);
if (!gameState.levelTime) gameState.combo += gameState.perks.hot_start * 15;
if (prev > gameState.combo && gameState.perks.soft_reset) gameState.combo += Math.floor((prev - gameState.combo) / (1 + gameState.perks.soft_reset));
const lost = Math.max(0, prev - gameState.combo);
if (lost) {
for(let i = 0; i < lost && i < 8; i++)setTimeout(()=>(0, _sounds.sounds).comboDecrease(), i * 100);
if (typeof x !== "undefined" && typeof y !== "undefined") gameState.flashes.push({
type: "text",
text: "-" + lost,
time: gameState.levelTime,
color: "red",
x: x,
y: y,
duration: 150,
size: gameState.puckHeight
});
}
return lost;
}
function decreaseCombo(gameState, by, x, y) {
const prev = gameState.combo;
gameState.combo = Math.max(baseCombo(gameState), gameState.combo - by);
const lost = Math.max(0, prev - gameState.combo);
if (lost) {
(0, _sounds.sounds).comboDecrease();
if (typeof x !== "undefined" && typeof y !== "undefined") gameState.flashes.push({
type: "text",
text: "-" + lost,
time: gameState.levelTime,
color: "red",
x: x,
y: y,
duration: 300,
size: gameState.puckHeight
});
}
}
},{"./sounds":"dQKPV","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"kRstf":[function(require,module,exports,__globalThis) {
if ("serviceWorker" in navigator && window.location.search.includes("isPWA=true")) // @ts-ignore
navigator.serviceWorker.register(require("994b1835e761d5ae"));
},{"994b1835e761d5ae":"6vb6r"}],"6vb6r":[function(require,module,exports,__globalThis) {
module.exports = require("e4f4efefa01a2f07").getBundleURL('28aWT') + "sw-b71.js";
},{"e4f4efefa01a2f07":"lgJ39"}]},["fxnks","bCo5X"], "bCo5X", "parcelRequire94c2")
</script>
</body>
</html>