breakout71/dist/index.html

4691 lines
257 KiB
HTML
Raw Normal View History

<!doctype html>
<html lang="en">
<head><script src="/editor.1350aee5.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="/PWA/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;
2025-03-20 18:44:46 +01:00
position: fixed;
top: 0;
left: 0;
}
#score, #menu {
z-index: 1;
appearance: none;
font: inherit;
color: #fff;
text-shadow: 0 0 4px #000c;
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: #fff;
transition: color .3s;
right: 0;
}
#score.active {
color: gold;
transition: color 10ms;
}
#menu {
left: 0;
}
#FPSDisplay {
z-index: 1;
white-space: nowrap;
pointer-events: none;
user-select: none;
opacity: .8;
color: #fff;
transform-origin: 0 0;
padding: 0;
line-height: 20px;
position: fixed;
bottom: 0;
left: 0;
transform: rotate(-90deg);
}
2025-03-20 18:44:46 +01:00
body.has-alert-open {
height: auto;
overflow: visible;
}
body:not(.has-alert-open) #popup {
display: none;
}
#popup {
display: flex;
overflow: auto;
}
#popup:before {
z-index: 10;
2025-03-20 18:44:46 +01:00
content: "";
background: #000000e6;
2025-03-20 18:44:46 +01:00
display: block;
position: fixed;
inset: 0;
}
2025-03-20 18:44:46 +01:00
#popup > div {
z-index: 11;
transform-origin: center;
flex-direction: column;
align-items: stretch;
width: 100%;
max-width: 450px;
margin: auto;
padding: 20px 10px;
display: flex;
2025-03-20 18:44:46 +01:00
position: relative;
}
2025-03-20 18:44:46 +01:00
#popup > div > * {
2025-03-27 10:52:31 +01:00
margin: 0 0 20px;
padding: 0;
}
2025-03-20 18:44:46 +01:00
#popup > div > section {
flex-direction: column;
align-items: stretch;
display: flex;
}
2025-03-20 18:44:46 +01:00
#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;
}
2025-03-20 18:44:46 +01:00
#popup > div > section button:not([disabled]):hover, #popup > div > section button:not([disabled]):focus {
z-index: 1;
border-color: #f1d33b;
position: relative;
}
2025-03-20 18:44:46 +01:00
#popup > div > section button[disabled] {
opacity: .5;
filter: saturate(0);
pointer-events: none;
}
2025-03-20 18:44:46 +01:00
#popup > div > section button > div {
flex-grow: 1;
}
2025-03-20 18:44:46 +01:00
#popup > div > section button > div > em {
opacity: .8;
display: block;
}
2025-03-20 18:44:46 +01:00
#popup > div > section button.grey-out-unless-hovered:not(:hover) {
opacity: .6;
}
2025-03-20 18:44:46 +01:00
#popup > div > section button.grey-out-unless-hovered:not(:hover) img {
filter: saturate(0);
}
2025-03-20 18:44:46 +01:00
#popup.actionsAsGrid > div {
max-width: none;
}
2025-03-20 18:44:46 +01:00
#popup.actionsAsGrid > div section {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
display: grid;
}
2025-03-20 18:44:46 +01:00
#popup button#close-modale {
color: #fff;
cursor: pointer;
2025-03-20 22:50:50 +01:00
z-index: 12;
background: none;
border: none;
width: 60px;
height: 60px;
position: absolute;
top: 0;
right: 0;
overflow: hidden;
}
2025-03-20 18:44:46 +01:00
#popup button#close-modale:before {
content: "+";
font-size: 80px;
display: inline-block;
position: absolute;
top: 34px;
left: 26px;
transform: translate(-50%, -50%)rotate(45deg);
}
2025-03-20 18:44:46 +01:00
#popup button#close-modale:hover {
background: #000;
font-weight: bold;
}
2025-03-20 18:44:46 +01:00
#popup .textAfterButtons {
color: #ffffff94;
}
2025-03-20 18:44:46 +01:00
#popup a[href] {
color: inherit;
}
2025-03-20 18:44:46 +01:00
#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;
}
.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;
}
2025-03-27 10:52:31 +01:00
.red-icon {
background: red;
}
.red-icon img {
filter: saturate(0);
mix-blend-mode: luminosity;
}
</style>
<link rel="icon" href="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%22500%22%20width%3D%22500%22%3E%0A%3Crect%20x%3D%220%22%20y%3D%220%22%20width%3D%22300%22%20height%3D%22100%22%20fill%3D%22%236262EA%22%3E%3C%2Frect%3E%0A%3Crect%20x%3D%22200%22%20y%3D%22100%22%20width%3D%22100%22%20height%3D%22100%22%20fill%3D%22%236262EA%22%3E%3C%2Frect%3E%0A%3Crect%20x%3D%22100%22%20y%3D%22200%22%20width%3D%22100%22%20height%3D%22200%22%20fill%3D%22%236262EA%22%3E%3C%2Frect%3E%0A%3Crect%20x%3D%22200%22%20y%3D%22200%22%20width%3D%22100%22%20height%3D%22100%22%20fill%3D%22%235DA3EA%22%3E%3C%2Frect%3E%0A%3Crect%20x%3D%22300%22%20y%3D%22100%22%20width%3D%22100%22%20height%3D%22300%22%20fill%3D%22%235DA3EA%22%3E%3C%2Frect%3E%0A%3Crect%20x%3D%22200%22%20y%3D%22400%22%20width%3D%22300%22%20height%3D%22100%22%20fill%3D%22%235DA3EA%22%3E%3C%2Frect%3E%0A%3C%2Fsvg%3E">
</head>
<body>
<button id="menu"><span id="menuLabel">menu</span></button>
<button id="score"></button>
<div id="FPSDisplay"></div>
<canvas id="game"></canvas>
2025-03-20 18:44:46 +01:00
<div id="popup">
<button id="close-modale"></button>
</div>
<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;
}
}
})({"gVqJ6":[function(require,module,exports,__globalThis) {
require("5e6e73082bdd189e")(require("e4e486661babe6bc").getBundleURL('bgzJG') + "editor.1350aee5.js");
},{"5e6e73082bdd189e":"61B45","e4e486661babe6bc":"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;
},{}],"67XFf":[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);
2025-03-26 08:27:56 +01:00
parcelHelpers.export(exports, "openShortRunUpgradesPicker", ()=>openShortRunUpgradesPicker);
parcelHelpers.export(exports, "brickIndex", ()=>brickIndex);
parcelHelpers.export(exports, "hasBrick", ()=>hasBrick);
parcelHelpers.export(exports, "hitsSomething", ()=>hitsSomething);
parcelHelpers.export(exports, "tick", ()=>tick);
2025-03-26 08:01:12 +01:00
parcelHelpers.export(exports, "openMainMenu", ()=>openMainMenu);
parcelHelpers.export(exports, "confirmRestart", ()=>confirmRestart);
parcelHelpers.export(exports, "toggleFullScreen", ()=>toggleFullScreen);
parcelHelpers.export(exports, "setKeyPressed", ()=>setKeyPressed);
parcelHelpers.export(exports, "gameState", ()=>gameState);
parcelHelpers.export(exports, "restart", ()=>restart);
var _loadGameData = require("./loadGameData");
var _sounds = require("./sounds");
var _gameUtils = require("./game_utils");
var _swLoader = require("./PWA/sw_loader");
var _i18N = require("./i18n/i18n");
var _settings = require("./settings");
var _gameStateMutators = require("./gameStateMutators");
var _render = require("./render");
var _recording = require("./recording");
var _newGameState = require("./newGameState");
var _asyncAlert = require("./asyncAlert");
var _options = require("./options");
var _getLevelBackground = require("./getLevelBackground");
2025-03-26 14:04:54 +01:00
var _premium = require("./premium");
function play() {
if (gameState.running) return;
gameState.running = true;
gameState.ballStickToPuck = false;
(0, _recording.startRecordingGame)(gameState);
(0, _sounds.getAudioContext)()?.resume();
(0, _recording.resumeRecording)();
2025-03-20 18:44:46 +01:00
// document.body.classList[gameState.running ? 'add' : 'remove']('running')
}
function pause(playerAskedForPause) {
if (!gameState.running) return;
if (gameState.pauseTimeout) return;
2025-03-23 16:11:12 +01:00
const stop = ()=>{
gameState.running = false;
setTimeout(()=>{
if (!gameState.running) (0, _sounds.getAudioContext)()?.suspend();
}, 1000);
(0, _recording.pauseRecording)();
gameState.pauseTimeout = null;
2025-03-20 18:44:46 +01:00
// document.body.className = gameState.running ? " running " : " paused ";
(0, _render.scoreDisplay).className = "";
gameState.needsRender = true;
2025-03-23 16:11:12 +01:00
};
if (playerAskedForPause) {
// Pausing many times in a run will make pause slower
gameState.pauseUsesDuringRun++;
gameState.pauseTimeout = setTimeout(stop, Math.min(Math.max(0, gameState.pauseUsesDuringRun - 5) * 50, 500));
} else stop();
if (document.exitPointerLock) document.exitPointerLock();
}
const fitSize = ()=>{
const past_off = gameState.offsetXRoundedDown, past_width = gameState.gameZoneWidthRoundedUp, past_heigh = gameState.gameZoneHeight;
const { width, height } = (0, _render.gameCanvas).getBoundingClientRect();
gameState.canvasWidth = width;
gameState.canvasHeight = height;
(0, _render.gameCanvas).width = width;
(0, _render.gameCanvas).height = height;
(0, _render.ctx).fillStyle = (0, _gameUtils.currentLevelInfo)(gameState)?.color || "black";
(0, _render.ctx).globalAlpha = 1;
(0, _render.ctx).fillRect(0, 0, width, height);
(0, _render.backgroundCanvas).width = width;
(0, _render.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;
(0, _render.backgroundCanvas).title = "resized";
// Ensure puck stays within bounds
(0, _gameStateMutators.setMousePos)(gameState, gameState.puckPosition);
function mapXY(item) {
item.x = gameState.offsetXRoundedDown + (item.x - past_off) / past_width * gameState.gameZoneWidthRoundedUp;
item.y = item.y / past_heigh * gameState.gameZoneHeight;
}
function mapXYPastCoord(coin) {
coin.x = gameState.offsetXRoundedDown + (coin.x - past_off) / past_width * gameState.gameZoneWidthRoundedUp;
coin.y = coin.y / past_heigh * gameState.gameZoneHeight;
coin.previousX = coin.x;
coin.previousY = coin.y;
}
gameState.balls.forEach(mapXYPastCoord);
(0, _gameStateMutators.forEachLiveOne)(gameState.coins, mapXYPastCoord);
(0, _gameStateMutators.forEachLiveOne)(gameState.particles, mapXY);
(0, _gameStateMutators.forEachLiveOne)(gameState.texts, mapXY);
(0, _gameStateMutators.forEachLiveOne)(gameState.lights, mapXY);
pause(true);
// 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 } = (0, _render.gameCanvas).getBoundingClientRect();
if (width !== gameState.canvasWidth || height !== gameState.canvasHeight) fitSize();
}, 1000);
2025-03-26 08:27:56 +01:00
async function openShortRunUpgradesPicker(gameState) {
const catchRate = (gameState.score - gameState.levelStartScore) / (gameState.levelSpawnedCoins || 1);
let repeats = 1;
let timeGain = "", catchGain = "", wallHitsGain = "", missesGain = "";
if (gameState.levelWallBounces == 0) {
repeats++;
2025-03-26 08:27:56 +01:00
gameState.rerolls++;
wallHitsGain = (0, _i18N.t)("level_up.plus_one_upgrade");
} else if (gameState.levelWallBounces < 5) {
2025-03-26 08:27:56 +01:00
gameState.rerolls++;
wallHitsGain = (0, _i18N.t)("level_up.plus_one_choice");
}
if (gameState.levelTime < 30000) {
repeats++;
2025-03-26 08:27:56 +01:00
gameState.rerolls++;
timeGain = (0, _i18N.t)("level_up.plus_one_upgrade");
} else if (gameState.levelTime < 60000) {
2025-03-26 08:27:56 +01:00
gameState.rerolls++;
timeGain = (0, _i18N.t)("level_up.plus_one_choice");
}
if (catchRate === 1) {
repeats++;
2025-03-26 08:27:56 +01:00
gameState.rerolls++;
catchGain = (0, _i18N.t)("level_up.plus_one_upgrade");
} else if (catchRate > 0.9) {
2025-03-26 08:27:56 +01:00
gameState.rerolls++;
catchGain = (0, _i18N.t)("level_up.plus_one_choice");
}
if (gameState.levelMisses === 0) {
repeats++;
2025-03-26 08:27:56 +01:00
gameState.rerolls++;
missesGain = (0, _i18N.t)("level_up.plus_one_upgrade");
} else if (gameState.levelMisses <= 3) {
2025-03-26 08:27:56 +01:00
gameState.rerolls++;
missesGain = (0, _i18N.t)("level_up.plus_one_choice");
}
while(repeats--){
2025-03-26 08:27:56 +01:00
const actions = (0, _gameStateMutators.pickRandomUpgrades)(gameState, 3 + gameState.perks.one_more_choice - gameState.perks.instant_upgrade);
2025-03-27 10:52:31 +01:00
if (!actions.length) break;
2025-03-26 08:27:56 +01:00
if (gameState.rerolls) actions.push({
text: (0, _i18N.t)("level_up.reroll", {
count: gameState.rerolls
}),
help: (0, _i18N.t)("level_up.reroll_help"),
2025-03-26 08:35:49 +01:00
value: "reroll",
icon: (0, _loadGameData.icons)["icon:reroll"]
2025-03-26 08:27:56 +01:00
});
let textAfterButtons = `
<p>${(0, _i18N.t)("level_up.after_buttons", {
level: gameState.currentLevel + 1,
max: (0, _gameUtils.max_levels)(gameState)
})} </p>
2025-03-27 10:52:31 +01:00
${(0, _gameUtils.pickedUpgradesHTMl)(gameState)}
<div id="level-recording-container"></div>
`;
const compliment = timeGain && catchGain && missesGain && wallHitsGain && (0, _i18N.t)("level_up.compliment_perfect") || (timeGain || catchGain || missesGain || wallHitsGain) && (0, _i18N.t)("level_up.compliment_good") || (0, _i18N.t)("level_up.compliment_advice");
2025-03-26 14:04:54 +01:00
const upgradeId = await (0, _asyncAlert.requiredAsyncAlert)({
title: (0, _i18N.t)("level_up.pick_upgrade_title") + (repeats ? " (" + (repeats + 1) + ")" : ""),
2025-03-27 10:52:31 +01:00
content: [
`<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,
levelWallBounces: gameState.levelWallBounces,
wallHitsGain,
compliment
})}
2025-03-23 19:11:01 +01:00
</p>
<p>${(0, _gameUtils.levelsListHTMl)(gameState)}</p>
`,
2025-03-27 10:52:31 +01:00
...actions,
textAfterButtons
]
});
2025-03-26 08:35:49 +01:00
if (upgradeId === "reroll") {
2025-03-26 08:27:56 +01:00
repeats++;
gameState.rerolls--;
} else {
gameState.perks[upgradeId]++;
if (upgradeId === "instant_upgrade") repeats += 2;
gameState.runStatistics.upgrades_picked++;
}
}
}
(0, _render.gameCanvas).addEventListener("mouseup", (e)=>{
if (e.button !== 0) return;
if (gameState.running) pause(true);
else {
play();
if ((0, _options.isOptionOn)("pointerLock") && (0, _render.gameCanvas).requestPointerLock) (0, _render.gameCanvas).requestPointerLock().then();
}
});
(0, _render.gameCanvas).addEventListener("mousemove", (e)=>{
if (document.pointerLockElement === (0, _render.gameCanvas)) (0, _gameStateMutators.setMousePos)(gameState, gameState.puckPosition + e.movementX);
else (0, _gameStateMutators.setMousePos)(gameState, e.x);
});
(0, _render.gameCanvas).addEventListener("touchstart", (e)=>{
e.preventDefault();
if (!e.touches?.length) return;
(0, _gameStateMutators.setMousePos)(gameState, e.touches[0].pageX);
(0, _gameStateMutators.normalizeGameState)(gameState);
play();
});
(0, _render.gameCanvas).addEventListener("touchend", (e)=>{
e.preventDefault();
pause(true);
});
(0, _render.gameCanvas).addEventListener("touchcancel", (e)=>{
e.preventDefault();
pause(true);
});
(0, _render.gameCanvas).addEventListener("touchmove", (e)=>{
if (!e.touches?.length) return;
(0, _gameStateMutators.setMousePos)(gameState, e.touches[0].pageX);
});
function brickIndex(x, y) {
return (0, _gameUtils.getRowColIndex)(gameState, 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 tick() {
const currentTick = performance.now();
const timeDeltaMs = currentTick - gameState.lastTick;
gameState.lastTick = currentTick;
const frames = Math.min(4, timeDeltaMs / (1000 / 60));
if (gameState.keyboardPuckSpeed) (0, _gameStateMutators.setMousePos)(gameState, gameState.puckPosition + gameState.keyboardPuckSpeed);
(0, _gameStateMutators.normalizeGameState)(gameState);
if (gameState.running) {
gameState.levelTime += timeDeltaMs;
gameState.runStatistics.runTime += timeDeltaMs;
(0, _gameStateMutators.gameStateTick)(gameState, frames);
}
if (gameState.running || gameState.needsRender) {
gameState.needsRender = false;
(0, _render.render)(gameState);
}
if (gameState.running) (0, _recording.recordOneFrame)(gameState);
if ((0, _options.isOptionOn)("sound")) (0, _sounds.playPendingSounds)(gameState);
requestAnimationFrame(tick);
FPSCounter++;
}
let FPSCounter = 0;
2025-03-23 16:11:12 +01:00
let FPSDisplay = document.getElementById("FPSDisplay");
setInterval(()=>{
2025-03-23 16:11:12 +01:00
if ((0, _options.isOptionOn)("show_fps")) FPSDisplay.innerText = FPSCounter + " FPS " + (0, _gameStateMutators.liveCount)(gameState.coins) + " COINS " + ((0, _gameStateMutators.liveCount)(gameState.particles) + (0, _gameStateMutators.liveCount)(gameState.texts) + (0, _gameStateMutators.liveCount)(gameState.lights)) + " PARTICLES ";
else FPSDisplay.innerText = "";
FPSCounter = 0;
}, 1000);
window.addEventListener("visibilitychange", ()=>{
if (document.hidden) pause(true);
});
(0, _render.scoreDisplay).addEventListener("click", (e)=>{
e.preventDefault();
2025-03-20 18:44:46 +01:00
if (!(0, _asyncAlert.alertsOpen)) openScorePanel();
});
document.addEventListener("visibilitychange", ()=>{
if (document.hidden) pause(true);
});
async function openScorePanel() {
pause(true);
const cb = await (0, _asyncAlert.asyncAlert)({
title: gameState.loop ? (0, _i18N.t)("score_panel.title_looped", {
loop: gameState.loop,
2025-03-27 10:52:31 +01:00
score: gameState.score,
level: gameState.currentLevel + 1,
max: (0, _gameUtils.max_levels)(gameState)
}) : (0, _i18N.t)("score_panel.title", {
score: gameState.score,
level: gameState.currentLevel + 1,
max: (0, _gameUtils.max_levels)(gameState)
}),
2025-03-27 10:52:31 +01:00
content: [
gameState.isCreativeModeRun ? `<p>${(0, _i18N.t)("score_panel.test_run")}</p>` : "",
(0, _gameUtils.pickedUpgradesHTMl)(gameState),
(0, _gameUtils.levelsListHTMl)(gameState),
(0, _gameUtils.debuffsHTMl)(gameState)
2025-03-27 10:52:31 +01:00
],
allowClose: true
});
}
document.getElementById("menu").addEventListener("click", (e)=>{
e.preventDefault();
if (!(0, _asyncAlert.alertsOpen)) openMainMenu();
});
async function openMainMenu() {
pause(true);
const creativeModeThreshold = Math.max(...(0, _loadGameData.upgrades).map((u)=>u.threshold));
const actions = [
{
2025-03-26 08:01:12 +01:00
icon: (0, _loadGameData.icons)["icon:7_levels_run"],
text: (0, _i18N.t)("main_menu.normal"),
help: (0, _i18N.t)("main_menu.normal_help"),
value: ()=>{
restart({
levelToAvoid: (0, _gameUtils.currentLevelInfo)(gameState).name
});
}
},
{
2025-03-23 16:11:12 +01:00
icon: (0, _loadGameData.icons)["icon:unlocks"],
text: (0, _i18N.t)("main_menu.unlocks"),
help: (0, _i18N.t)("main_menu.unlocks_help"),
value () {
openUnlocksList();
}
},
{
2025-03-23 16:11:12 +01:00
icon: (0, _loadGameData.icons)["icon:sandbox"],
text: (0, _i18N.t)("sandbox.title"),
help: (0, _settings.getTotalScore)() < creativeModeThreshold ? (0, _i18N.t)("sandbox.unlocks_at", {
score: creativeModeThreshold
}) : (0, _i18N.t)("sandbox.help"),
disabled: (0, _settings.getTotalScore)() < creativeModeThreshold,
async value () {
let creativeModePerks = (0, _settings.getSettingValue)("creativeModePerks", {}), choice;
while(choice = await (0, _asyncAlert.asyncAlert)({
title: (0, _i18N.t)("sandbox.title"),
actionsAsGrid: true,
2025-03-27 10:52:31 +01:00
content: [
(0, _i18N.t)("sandbox.instructions"),
...(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",
2025-03-23 16:11:12 +01:00
icon: (0, _loadGameData.icons)["icon:continue"]
}
]
})){
if (choice === "start") {
restart({
perks: creativeModePerks
});
break;
} else if (choice) {
creativeModePerks[choice.id] = ((creativeModePerks[choice.id] || 0) + 1) % (choice.max + 1);
(0, _settings.setSettingValue)("creativeModePerks", creativeModePerks);
}
}
}
},
2025-03-26 14:04:54 +01:00
(0, _premium.premiumMenuEntry)(gameState),
{
2025-03-26 08:01:12 +01:00
text: (0, _i18N.t)("main_menu.settings_title"),
help: (0, _i18N.t)("main_menu.settings_help"),
icon: (0, _loadGameData.icons)["icon:settings"],
value () {
openSettingsMenu();
}
}
];
const cb = await (0, _asyncAlert.asyncAlert)({
title: (0, _i18N.t)("main_menu.title"),
2025-03-27 10:52:31 +01:00
content: [
...actions,
(0, _i18N.t)("main_menu.footer_html", {
appVersion: (0, _loadGameData.appVersion)
})
],
allowClose: true
});
if (cb) {
cb();
gameState.needsRender = true;
}
}
async function openSettingsMenu() {
pause(true);
2025-03-19 20:14:55 +01:00
const actions = [];
for (const key of Object.keys((0, _options.options)))if ((0, _options.options)[key]) actions.push({
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);
if (key === "mobile-mode") fitSize();
openSettingsMenu();
}
});
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();
openSettingsMenu();
}
});
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();
openSettingsMenu();
}
});
}
actions.push({
text: (0, _i18N.t)("main_menu.reset"),
help: (0, _i18N.t)("main_menu.reset_help"),
async value () {
if (await (0, _asyncAlert.asyncAlert)({
title: (0, _i18N.t)("main_menu.reset"),
2025-03-27 10:52:31 +01:00
content: [
(0, _i18N.t)("main_menu.reset_instruction"),
{
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.download_save_file"),
help: (0, _i18N.t)("main_menu.download_save_file_help"),
async value () {
const localStorageContent = {};
for(let i = 0; i < localStorage.length; i++){
const key = localStorage.key(i);
const value = localStorage.getItem(key);
// Store the key-value pair in the object
localStorageContent[key] = value;
}
const signedPayload = JSON.stringify(localStorageContent);
const dlLink = document.createElement("a");
dlLink.setAttribute("href", "data:application/json;base64," + btoa(JSON.stringify({
fileType: "B71-save-file",
appVersion: (0, _loadGameData.appVersion),
signedPayload,
key: (0, _getLevelBackground.hashCode)("Security by obscurity, but really the game is oss so eh" + signedPayload)
})));
dlLink.setAttribute("download", "b71-save-" + new Date().toISOString().slice(0, 19).replace(/[^0-9]+/gi, "-") + ".b71");
document.body.appendChild(dlLink);
dlLink.click();
setTimeout(()=>document.body.removeChild(dlLink), 1000);
}
});
actions.push({
text: (0, _i18N.t)("main_menu.load_save_file"),
help: (0, _i18N.t)("main_menu.load_save_file_help"),
async value () {
if (!document.getElementById("save_file_picker")) {
let input = document.createElement("input");
input.setAttribute("type", "file");
input.setAttribute("id", "save_file_picker");
input.setAttribute("accept", ".b71,.json");
input.style.position = "absolute";
input.style.left = "-1000px";
input.addEventListener("change", async (e)=>{
try {
const file = input && input.files?.item(0);
if (file) {
const content = await new Promise((resolve, reject)=>{
const reader = new FileReader();
reader.onload = function() {
resolve(reader.result?.toString() || "");
};
reader.onerror = function() {
reject(reader.error);
};
// Read the file as a text string
reader.readAsText(file);
});
const { fileType, appVersion: fileVersion, signedPayload, key } = JSON.parse(content);
if (fileType !== "B71-save-file") throw new Error("Not a B71 save file");
if (fileVersion > (0, _loadGameData.appVersion)) throw new Error("Please update your app first, this file is for version " + fileVersion + " or newer.");
if (key !== (0, _getLevelBackground.hashCode)("Security by obscurity, but really the game is oss so eh" + signedPayload)) throw new Error("Key does not match content.");
const localStorageContent = JSON.parse(signedPayload);
localStorage.clear();
for(let key in localStorageContent)localStorage.setItem(key, localStorageContent[key]);
await (0, _asyncAlert.asyncAlert)({
title: (0, _i18N.t)("main_menu.save_file_loaded"),
2025-03-27 10:52:31 +01:00
content: [
(0, _i18N.t)("main_menu.save_file_loaded_help"),
{
text: (0, _i18N.t)("main_menu.save_file_loaded_ok")
}
]
});
window.location.reload();
}
} catch (e) {
await (0, _asyncAlert.asyncAlert)({
title: (0, _i18N.t)("main_menu.save_file_error"),
2025-03-27 10:52:31 +01:00
content: [
e.message,
{
text: (0, _i18N.t)("main_menu.save_file_loaded_ok")
}
]
});
}
input.value = "";
});
document.body.appendChild(input);
}
document.getElementById("save_file_picker")?.click();
}
});
actions.push({
text: (0, _i18N.t)("main_menu.language"),
help: (0, _i18N.t)("main_menu.language_help"),
async value () {
const pick = await (0, _asyncAlert.asyncAlert)({
title: (0, _i18N.t)("main_menu.language"),
2025-03-27 10:52:31 +01:00
content: [
(0, _i18N.t)("main_menu.language_help"),
{
text: "English",
value: "en"
},
{
text: "Fran\xe7ais",
value: "fr"
}
],
allowClose: true
});
2025-03-26 08:01:12 +01:00
if (pick && pick !== (0, _i18N.getCurrentLang)() && await confirmRestart(gameState)) {
(0, _settings.setSettingValue)("lang", pick);
window.location.reload();
}
}
});
actions.push({
text: (0, _i18N.t)("main_menu.max_coins", {
max: (0, _settings.getCurrentMaxCoins)()
}),
help: (0, _i18N.t)("main_menu.max_coins_help"),
async value () {
(0, _settings.cycleMaxCoins)();
await openSettingsMenu();
}
});
actions.push({
text: (0, _i18N.t)("main_menu.max_particles", {
max: (0, _settings.getCurrentMaxParticles)()
}),
help: (0, _i18N.t)("main_menu.max_particles_help"),
async value () {
(0, _settings.cycleMaxParticles)();
await openSettingsMenu();
}
});
const cb = await (0, _asyncAlert.asyncAlert)({
title: (0, _i18N.t)("main_menu.settings_title"),
2025-03-27 10:52:31 +01:00
content: [
(0, _i18N.t)("main_menu.settings_help"),
...actions
],
allowClose: true
});
if (cb) {
cb();
gameState.needsRender = true;
}
}
async function openUnlocksList() {
const ts = (0, _settings.getTotalScore)();
const actions = [
2025-03-26 08:01:12 +01:00
...(0, _loadGameData.upgrades).sort((a, b)=>a.threshold - b.threshold).map(({ name, id, threshold, icon, help })=>({
text: name,
2025-03-26 08:01:12 +01:00
// help:
// ts >= threshold ? help(1) : 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,
2025-03-26 08:01:12 +01:00
// help: available
// ? t("unlocks.level_description", {
// size: l.size,
// bricks: l.bricks.filter((i) => i).length,
// })
// : 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 (0, _asyncAlert.asyncAlert)({
title: (0, _i18N.t)("unlocks.title", {
percentUnlock
}),
2025-03-27 10:52:31 +01:00
content: [
`<p>${(0, _i18N.t)("unlocks.intro", {
ts
})}
${percentUnlock < 100 ? (0, _i18N.t)("unlocks.greyed_out_help") : ""}</p> `,
...actions,
`<p>
Your high score is ${gameState.highScore}.
Click an item above to start a run with it.
2025-03-27 10:52:31 +01:00
</p>`
],
2025-03-26 08:01:12 +01:00
allowClose: true,
actionsAsGrid: true
});
if (tryOn) {
2025-03-26 08:01:12 +01:00
if (await confirmRestart(gameState)) restart(tryOn);
}
}
2025-03-26 08:01:12 +01:00
async function confirmRestart(gameState) {
if (!gameState.currentLevel) return true;
return (0, _asyncAlert.asyncAlert)({
title: (0, _i18N.t)("confirmRestart.title"),
2025-03-27 10:52:31 +01:00
content: [
(0, _i18N.t)("confirmRestart.text"),
{
value: true,
text: (0, _i18N.t)("confirmRestart.yes")
},
{
value: false,
text: (0, _i18N.t)("confirmRestart.no")
}
]
});
}
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 === " " && !(0, _asyncAlert.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" && (0, _asyncAlert.closeModal)) (0, _asyncAlert.closeModal)();
else if (e.key === "Escape" && gameState.running) pause(true);
else if (e.key.toLowerCase() === "m" && !(0, _asyncAlert.alertsOpen)) openMainMenu().then();
else if (e.key.toLowerCase() === "s" && !(0, _asyncAlert.alertsOpen)) openScorePanel().then();
else if (e.key.toLowerCase() === "r" && !(0, _asyncAlert.alertsOpen)) {
2025-03-26 08:01:12 +01:00
if (await confirmRestart(gameState)) restart({
levelToAvoid: (0, _gameUtils.currentLevelInfo)(gameState).name
});
} else return;
e.preventDefault();
});
const gameState = (0, _newGameState.newGameState)({});
function restart(params) {
fitSize();
Object.assign(gameState, (0, _newGameState.newGameState)(params));
(0, _recording.pauseRecording)();
(0, _gameStateMutators.setLevel)(gameState, 0);
}
2025-03-26 14:04:54 +01:00
restart(window.location.search.includes("stressTest") && {
2025-03-23 16:21:56 +01:00
level: "Bird",
2025-03-23 16:19:29 +01:00
perks: {
// sapper: 1,
// bigger_explosions: 20,
// // unbounded: 1,
// // pierce_color: 1,
// pierce: 1,
// multiball: 6,
// base_combo: 7,
2025-03-23 16:19:29 +01:00
telekinesis: 2,
yoyo: 2
2025-03-27 10:52:31 +01:00
},
debuffs: {}
2025-03-26 14:04:54 +01:00
} || {});
tick();
},{"./loadGameData":"l1B4x","./sounds":"dQKPV","./game_utils":"cEeac","./PWA/sw_loader":"2n0gK","./i18n/i18n":"eNPRm","./settings":"5blfu","./gameStateMutators":"9ZeQl","./render":"9AS2t","./recording":"godmD","./newGameState":"aQN6X","./asyncAlert":"rSqLY","./options":"d5NoS","./getLevelBackground":"7OIPf","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3","./premium":"4GEPs"}],"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("./data/palette.json");
var _paletteJsonDefault = parcelHelpers.interopDefault(_paletteJson);
var _levelsJson = require("./data/levels.json");
var _levelsJsonDefault = parcelHelpers.interopDefault(_levelsJson);
var _versionJson = require("./data/version.json");
var _versionJsonDefault = parcelHelpers.interopDefault(_versionJson);
2025-03-19 20:14:55 +01:00
var _upgrades = require("./upgrades");
var _getLevelBackground = require("./getLevelBackground");
var _levelIcon = require("./levelIcon");
const palette = (0, _paletteJsonDefault.default);
const rawLevelsList = (0, _levelsJsonDefault.default);
const appVersion = (0, _versionJsonDefault.default);
const icons = {};
const allLevels = rawLevelsList.map((level)=>{
const bricks = level.bricks.split("").map((c)=>palette[c]).slice(0, level.size * level.size);
2025-03-27 10:52:31 +01:00
const bricksCount = bricks.filter((i)=>i).length;
const icon = (0, _levelIcon.levelIconHTML)(bricks, level.size, level.color);
icons[level.name] = icon;
return {
...level,
bricks,
2025-03-27 10:52:31 +01:00
bricksCount,
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),
2025-03-27 10:52:31 +01:00
sortKey: (Math.random() + 3) / 3.5 * l.bricksCount
}));
2025-03-19 20:14:55 +01:00
const upgrades = (0, _upgrades.rawUpgrades).map((u)=>({
...u,
icon: icons["icon:" + u.id]
}));
},{"./data/palette.json":"ktRBU","./data/levels.json":"8JSUc","./data/version.json":"iyP6E","./upgrades":"1u3Dx","./getLevelBackground":"7OIPf","./levelIcon":"6rQoT","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"iyP6E":[function(require,module,exports,__globalThis) {
2025-03-27 10:52:31 +01:00
module.exports = JSON.parse("\"29050375\"");
2025-03-20 18:44:46 +01:00
},{}],"1u3Dx":[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",
2025-03-19 21:58:08 +01:00
max: 3,
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", {
lvl
}),
fullHelp: (0, _i18N.t)("upgrades.skip_last.fullHelp")
},
{
requires: "",
threshold: 500,
id: "telekinesis",
giftable: false,
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: false,
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: false,
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: false,
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,
2025-03-27 10:52:31 +01:00
adventure: 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",
2025-03-23 19:11:01 +01:00
max: 4,
name: (0, _i18N.t)("upgrades.pierce_color.name"),
2025-03-23 19:11:01 +01:00
help: (lvl)=>(0, _i18N.t)("upgrades.pierce_color.help", {
lvl
}),
fullHelp: (0, _i18N.t)("upgrades.pierce_color.fullHelp")
},
{
requires: "",
threshold: 18000,
giftable: false,
id: "soft_reset",
max: 9,
name: (0, _i18N.t)("upgrades.soft_reset.name"),
help: (lvl)=>(0, _i18N.t)("upgrades.soft_reset.help", {
percent: 10 * lvl
}),
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.sturdy_bricks.name"),
help: (lvl)=>lvl == 1 ? (0, _i18N.t)("upgrades.sturdy_bricks.help") : (0, _i18N.t)("upgrades.sturdy_bricks.help_plural"),
fullHelp: (0, _i18N.t)("upgrades.sturdy_bricks.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"),
fullHelp: (0, _i18N.t)("upgrades.one_more_choice.fullHelp")
},
{
requires: "",
threshold: 55000,
giftable: false,
id: "instant_upgrade",
max: 2,
2025-03-27 10:52:31 +01:00
adventure: false,
name: (0, _i18N.t)("upgrades.instant_upgrade.name"),
help: (lvl)=>(0, _i18N.t)("upgrades.instant_upgrade.help"),
fullHelp: (0, _i18N.t)("upgrades.instant_upgrade.fullHelp")
},
{
requires: "",
threshold: 60000,
giftable: false,
id: "concave_puck",
max: 1,
name: (0, _i18N.t)("upgrades.concave_puck.name"),
help: (lvl)=>(0, _i18N.t)("upgrades.concave_puck.help"),
fullHelp: (0, _i18N.t)("upgrades.concave_puck.fullHelp")
2025-03-19 20:14:55 +01:00
},
{
requires: "",
threshold: 65000,
giftable: false,
2025-03-19 21:58:08 +01:00
id: "helium",
2025-03-19 20:14:55 +01:00
max: 1,
2025-03-19 21:58:08 +01:00
name: (0, _i18N.t)("upgrades.helium.name"),
help: (lvl)=>(0, _i18N.t)("upgrades.helium.help"),
fullHelp: (0, _i18N.t)("upgrades.helium.fullHelp")
2025-03-19 20:14:55 +01:00
},
{
requires: "",
threshold: 70000,
giftable: true,
2025-03-19 20:14:55 +01:00
id: "asceticism",
max: 1,
name: (0, _i18N.t)("upgrades.asceticism.name"),
help: (lvl)=>(0, _i18N.t)("upgrades.asceticism.help"),
fullHelp: (0, _i18N.t)("upgrades.asceticism.fullHelp")
},
{
requires: "",
threshold: 75000,
giftable: false,
id: "unbounded",
max: 1,
name: (0, _i18N.t)("upgrades.unbounded.name"),
help: (lvl)=>(0, _i18N.t)("upgrades.unbounded.help"),
fullHelp: (0, _i18N.t)("upgrades.unbounded.fullHelp")
},
{
requires: "",
threshold: 80000,
giftable: false,
id: "shunt",
2025-03-20 08:13:17 +01:00
max: 3,
2025-03-19 20:14:55 +01:00
name: (0, _i18N.t)("upgrades.shunt.name"),
2025-03-20 08:13:17 +01:00
help: (lvl)=>(0, _i18N.t)("upgrades.shunt.help", {
2025-03-20 21:24:25 +01:00
percent: lvl * 20
2025-03-20 08:13:17 +01:00
}),
2025-03-19 20:14:55 +01:00
fullHelp: (0, _i18N.t)("upgrades.shunt.fullHelp")
},
{
requires: "",
threshold: 85000,
giftable: false,
id: "yoyo",
max: 2,
name: (0, _i18N.t)("upgrades.yoyo.name"),
help: (lvl)=>(0, _i18N.t)("upgrades.yoyo.help"),
fullHelp: (0, _i18N.t)("upgrades.yoyo.fullHelp")
},
{
requires: "",
threshold: 90000,
giftable: true,
2025-03-19 20:14:55 +01:00
id: "nbricks",
max: 3,
name: (0, _i18N.t)("upgrades.nbricks.name"),
help: (lvl)=>(0, _i18N.t)("upgrades.nbricks.help", {
lvl
}),
fullHelp: (0, _i18N.t)("upgrades.nbricks.fullHelp")
},
{
requires: "",
threshold: 95000,
giftable: false,
id: "etherealcoins",
max: 1,
name: (0, _i18N.t)("upgrades.etherealcoins.name"),
help: (lvl)=>(0, _i18N.t)("upgrades.etherealcoins.help"),
fullHelp: (0, _i18N.t)("upgrades.etherealcoins.fullHelp")
2025-03-19 21:58:08 +01:00
},
{
requires: "multiball",
threshold: 100000,
giftable: false,
id: "shocks",
max: 1,
name: (0, _i18N.t)("upgrades.shocks.name"),
help: (lvl)=>(0, _i18N.t)("upgrades.shocks.help"),
fullHelp: (0, _i18N.t)("upgrades.shocks.fullHelp")
},
{
requires: "",
threshold: 105000,
giftable: true,
2025-03-19 21:58:08 +01:00
id: "zen",
max: 1,
name: (0, _i18N.t)("upgrades.zen.name"),
help: (lvl)=>(0, _i18N.t)("upgrades.zen.help"),
fullHelp: (0, _i18N.t)("upgrades.zen.fullHelp")
},
{
requires: "extra_life",
threshold: 110000,
giftable: false,
id: "sacrifice",
max: 1,
name: (0, _i18N.t)("upgrades.sacrifice.name"),
help: (lvl)=>(0, _i18N.t)("upgrades.sacrifice.help"),
fullHelp: (0, _i18N.t)("upgrades.sacrifice.fullHelp")
},
{
requires: "",
threshold: 115000,
giftable: true,
2025-03-19 21:58:08 +01:00
id: "trampoline",
2025-03-20 21:24:25 +01:00
max: 1,
2025-03-19 21:58:08 +01:00
name: (0, _i18N.t)("upgrades.trampoline.name"),
help: (lvl)=>(0, _i18N.t)("upgrades.trampoline.help", {
lvl
}),
fullHelp: (0, _i18N.t)("upgrades.trampoline.fullHelp")
},
{
requires: "",
threshold: 120000,
giftable: false,
id: "ghost_coins",
max: 1,
name: (0, _i18N.t)("upgrades.ghost_coins.name"),
help: (lvl)=>(0, _i18N.t)("upgrades.ghost_coins.help", {
lvl
}),
fullHelp: (0, _i18N.t)("upgrades.ghost_coins.fullHelp")
},
{
requires: "",
threshold: 125000,
giftable: false,
id: "forgiving",
max: 1,
name: (0, _i18N.t)("upgrades.forgiving.name"),
help: (lvl)=>(0, _i18N.t)("upgrades.forgiving.help"),
fullHelp: (0, _i18N.t)("upgrades.forgiving.fullHelp")
},
{
requires: "",
threshold: 130000,
giftable: false,
id: "ball_attracts_coins",
max: 3,
name: (0, _i18N.t)("upgrades.ball_attracts_coins.name"),
help: (lvl)=>(0, _i18N.t)("upgrades.ball_attracts_coins.help"),
fullHelp: (0, _i18N.t)("upgrades.ball_attracts_coins.fullHelp")
2025-03-20 21:02:51 +01:00
},
{
requires: "",
threshold: 135000,
// a bit too hard when starting up
2025-03-20 21:02:51 +01:00
giftable: false,
id: "reach",
2025-03-20 21:07:54 +01:00
max: 1,
2025-03-20 21:02:51 +01:00
name: (0, _i18N.t)("upgrades.reach.name"),
help: (lvl)=>(0, _i18N.t)("upgrades.reach.help", {
lvl
}),
fullHelp: (0, _i18N.t)("upgrades.reach.fullHelp")
2025-03-22 16:04:25 +01:00
},
{
requires: "",
threshold: 140000,
giftable: true,
2025-03-22 16:04:25 +01:00
id: "passive_income",
max: 4,
2025-03-22 16:04:25 +01:00
name: (0, _i18N.t)("upgrades.passive_income.name"),
help: (lvl)=>(0, _i18N.t)("upgrades.passive_income.help", {
2025-03-25 08:22:58 +01:00
time: lvl * 0.25,
lvl
}),
2025-03-22 16:04:25 +01:00
fullHelp: (0, _i18N.t)("upgrades.passive_income.fullHelp")
2025-03-23 19:11:01 +01:00
},
{
requires: "",
threshold: 145000,
giftable: false,
id: "clairvoyant",
max: 1,
2025-03-26 14:04:54 +01:00
// TODO update for adventure mode
2025-03-23 19:11:01 +01:00
name: (0, _i18N.t)("upgrades.clairvoyant.name"),
help: (lvl)=>(0, _i18N.t)("upgrades.clairvoyant.help"),
fullHelp: (0, _i18N.t)("upgrades.clairvoyant.fullHelp")
},
{
requires: "",
threshold: 150000,
giftable: true,
id: "side_kick",
max: 3,
name: (0, _i18N.t)("upgrades.side_kick.name"),
help: (lvl)=>(0, _i18N.t)("upgrades.side_kick.help", {
lvl
}),
fullHelp: (0, _i18N.t)("upgrades.side_kick.fullHelp")
},
{
requires: "",
threshold: 155000,
giftable: false,
id: "implosions",
max: 1,
name: (0, _i18N.t)("upgrades.implosions.name"),
help: (lvl)=>(0, _i18N.t)("upgrades.implosions.help"),
fullHelp: (0, _i18N.t)("upgrades.implosions.fullHelp")
},
{
requires: "",
threshold: 160000,
giftable: false,
id: "corner_shot",
max: 1,
name: (0, _i18N.t)("upgrades.corner_shot.name"),
help: (lvl)=>(0, _i18N.t)("upgrades.corner_shot.help"),
fullHelp: (0, _i18N.t)("upgrades.corner_shot.fullHelp")
}
];
},{"./i18n/i18n":"eNPRm","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"eNPRm":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "getCurrentLang", ()=>getCurrentLang);
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());
}
function t(key, params = {}) {
const lang = getCurrentLang();
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","../settings":"5blfu","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"b97sx":[function(require,module,exports,__globalThis) {
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","debuffs.banned.description":"{{lvl}} am\xe9lioration(s) bannie(s) : {{banned}}","debuffs.banned.help":"","debuffs.interference.help":"","debuffs.more_bombs.help":"","debuffs.negative_coins.help":"","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":"Pi\xe8ces attrap\xe9es","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.upgrades_picked":"Am\xe9lioration actives en fin de partie","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}} et touch\xe9 les bords de la zone de jeu {{levelWallBounces}} fois {{wallHitsGain}}.\\n\\n{{compliment}}","level_up.compliment_advice":"Essayez d\'attraper toutes les pi\xe8ces, de ne jamais rater les briques, de ne pas toucher les murs 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 re-roll)","level_up.plus_one_upgrade":"(+1 am\xe9lioration et +1 re-roll)","level_up.reroll":"Re-roll ({{count}})","level_up.reroll_help":"Nouveaux choix","level_up.unlocked_level":" (Niveau)","level_up.unlocked_perk":" (Am\xe9lioration)","level_up.upgrade_perk_to_level":" niveau {{level}}","loop.instructions":"","loop.title":"","main_menu.basic":"Graphismes simplifi\xe9s","main_menu.basic_help":"Meilleures performances.","main_menu.colorful_coins":"","main_menu.colorful_coins_help":"","main_menu.download_save_file":"Sauvegarder mes progr\xe8s","main_menu.download_save_file_help":"Obtenir un fichier de sauvegarde","main_menu.footer_html":" <p> \\n<span>Programm\xe9 en France par <a href=\\"https://lecaro.me\\">Renan LE CARO</a>.</span>\\n<a href=\\"https://paypal.me/renanlecaro\\" target=\\"_blank\\">Donner</a>\\n<a href=\\"https://discord.gg/DZSPqyJkwP\\" target=\\"_blank\\">Discord</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://renanlec
},{}],"uYc9N":[function(require,module,exports,__globalThis) {
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","debuffs.banned.description":"{{lvl}} banned perk(s) : {{banned}}","debuffs.banned.help":"{{perk}} is banned for this run","debuffs.interference.help":"Telekinesis and yo-yo stop working for {{lvl}}s every 7s","debuffs.more_bombs.help":"{{lvl}} bricks replaced by bombs","debuffs.negative_coins.help":"{{lvl}}% of coins spawn void and break the combo if caught","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.upgrades_picked":"Upgrades active at the end of the run","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}} and hit the walls or ceiling {{levelWallBounces}} times{{wallHitsGain}}.\\n\\n{{compliment}}","level_up.compliment_advice":"Try to catch all coins, never miss the bricks, never hit the walls/ceiling 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 re-roll)","level_up.plus_one_upgrade":"(+1 upgrade and +1 re-roll)","level_up.reroll":"Re-roll ({{count}})","level_up.reroll_help":"Offer new choices","level_up.unlocked_level":" (Level)","level_up.unlocked_perk":" (Perk)","level_up.upgrade_perk_to_level":" lvl {{level}}","loop.instructions":"All your perks will be erased except one that you can pick below. Each option comes with an additional hazard will appear on all levels going forward. ","loop.title":"Starting loop {{loop}}","main_menu.basic":"Basic graphics","main_menu.basic_help":"Better performance.","main_menu.colorful_coins":"Colorful coins","main_menu.colorful_coins_help":"Coins always spawn of the color of the brick","main_menu.download_save_file":"Download score and stats","main_menu.download_save_file_help":"Get a save file","main_menu.footer_html":"<p> \\n<span>Made in France by <a href=\\"https://lecaro.me\\">Renan LE CARO</a>.</span> \\n<a href=\\"https://paypal.me/renanlecaro\\" target=\\"_blank\\">Donate</a>\\n<a href=\\"https://discord.gg/DZSPqyJkwP\\" target=\\"_blank\\">Discord</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
},{}],"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);
parcelHelpers.export(exports, "getTotalScore", ()=>getTotalScore);
parcelHelpers.export(exports, "addToTotalScore", ()=>addToTotalScore);
parcelHelpers.export(exports, "getCurrentMaxCoins", ()=>getCurrentMaxCoins);
parcelHelpers.export(exports, "getCurrentMaxParticles", ()=>getCurrentMaxParticles);
parcelHelpers.export(exports, "cycleMaxCoins", ()=>cycleMaxCoins);
parcelHelpers.export(exports, "cycleMaxParticles", ()=>cycleMaxParticles);
let cachedSettings = {};
function getSettingValue(key, defaultValue) {
if (typeof cachedSettings[key] == "undefined") try {
const ls = localStorage.getItem(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(key, JSON.stringify(value));
} catch (e) {
console.warn(e);
}
}
function getTotalScore() {
return getSettingValue("breakout_71_total_score", 0);
}
function addToTotalScore(gameState, points) {
if (gameState.isCreativeModeRun) return;
setSettingValue("breakout_71_total_score", getTotalScore() + points);
}
function getCurrentMaxCoins() {
2025-03-23 16:11:12 +01:00
return Math.pow(2, getSettingValue("max_coins", 1)) * 200;
}
function getCurrentMaxParticles() {
2025-03-23 16:11:12 +01:00
return Math.pow(2, getSettingValue("max_particles", 1)) * 200;
}
function cycleMaxCoins() {
2025-03-23 16:11:12 +01:00
setSettingValue("max_coins", (getSettingValue("max_coins", 1) + 1) % 6);
}
function cycleMaxParticles() {
2025-03-23 16:11:12 +01:00
setSettingValue("max_particles", (getSettingValue("max_particles", 1) + 1) % 6);
}
},{"@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"6rQoT":[function(require,module,exports,__globalThis) {
2025-03-20 18:44:46 +01:00
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "levelIconHTML", ()=>levelIconHTML);
let levelIconHTMLCanvas = document.createElement("canvas");
const levelIconHTMLCanvasCtx = levelIconHTMLCanvas.getContext("2d", {
antialias: false,
alpha: true
});
function levelIconHTML(bricks, levelSize, color) {
2025-03-26 08:01:12 +01:00
const size = 46;
2025-03-20 18:44:46 +01:00
const c = levelIconHTMLCanvas;
const ctx = levelIconHTMLCanvasCtx;
if (!ctx) return "";
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()}"/>`;
}
},{"@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"dQKPV":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "playPendingSounds", ()=>playPendingSounds);
parcelHelpers.export(exports, "sounds", ()=>sounds);
parcelHelpers.export(exports, "getAudioContext", ()=>getAudioContext);
parcelHelpers.export(exports, "getAudioRecordingTrack", ()=>getAudioRecordingTrack);
var _options = require("./options");
let lastPlay = Date.now();
function playPendingSounds(gameState) {
if (lastPlay > Date.now() - 60) return;
lastPlay = Date.now();
for(let key in gameState.aboutToPlaySound){
const soundName = key;
const ex = gameState.aboutToPlaySound[soundName];
if (ex.vol) {
sounds[soundName](Math.min(2, ex.vol), pixelsToPan(gameState, ex.x), gameState.combo);
ex.vol = 0;
}
}
}
const sounds = {
wallBeep: (vol, pan, combo)=>{
if (!(0, _options.isOptionOn)("sound")) return;
createSingleBounceSound(800, pan, vol);
},
comboIncreaseMaybe: (volume, pan, combo)=>{
if (!(0, _options.isOptionOn)("sound")) return;
let delta = 0;
if (!isNaN(lastComboPlayed)) {
if (lastComboPlayed < combo) delta = 1;
if (lastComboPlayed > combo) delta = -1;
}
playShepard(delta, pan, volume);
lastComboPlayed = combo;
},
comboDecrease (volume, pan, combo) {
if (!(0, _options.isOptionOn)("sound")) return;
playShepard(-1, pan, volume);
},
coinBounce: (volume, pan, combo)=>{
if (!(0, _options.isOptionOn)("sound")) return;
createSingleBounceSound(1200, pan, volume, 0.1, "triangle");
},
2025-03-27 10:52:31 +01:00
void: (volume, pan)=>{
if (!(0, _options.isOptionOn)("sound")) return;
createSingleBounceSound(1200, pan, volume, 0.5, "sawtooth");
createSingleBounceSound(600, pan, volume, 0.3, "sawtooth");
},
explode: (volume, pan, combo)=>{
if (!(0, _options.isOptionOn)("sound")) return;
createExplosionSound(pan);
},
lifeLost (volume, pan, combo) {
if (!(0, _options.isOptionOn)("sound")) return;
createShatteredGlassSound(pan);
},
coinCatch (volume, pan, combo) {
if (!(0, _options.isOptionOn)("sound")) return;
createSingleBounceSound(900, pan, volume, 0.1, "triangle");
},
colorChange (volume, pan, combo) {
createSingleBounceSound(400, pan, volume, 0.5, "sine");
createSingleBounceSound(800, 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(gameState, pan) {
return Math.max(0, Math.min(1, (pan - gameState.offsetXRoundedDown) / 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;
}
},{"./options":"d5NoS","@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 _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")
},
"mobile-mode": {
default: window.innerHeight > window.innerWidth,
name: (0, _i18N.t)("main_menu.mobile"),
help: (0, _i18N.t)("main_menu.mobile_help")
},
basic: {
default: false,
name: (0, _i18N.t)("main_menu.basic"),
help: (0, _i18N.t)("main_menu.basic_help")
},
colorful_coins: {
default: false,
name: (0, _i18N.t)("main_menu.colorful_coins"),
help: (0, _i18N.t)("main_menu.colorful_coins_help")
},
show_fps: {
default: false,
name: (0, _i18N.t)("main_menu.show_fps"),
help: (0, _i18N.t)("main_menu.show_fps_help")
},
show_stats: {
default: false,
name: (0, _i18N.t)("main_menu.show_stats"),
help: (0, _i18N.t)("main_menu.show_stats_help")
},
pointerLock: {
default: false,
name: (0, _i18N.t)("main_menu.pointer_lock"),
help: (0, _i18N.t)("main_menu.pointer_lock_help")
},
easy: {
default: false,
name: (0, _i18N.t)("main_menu.kid"),
help: (0, _i18N.t)("main_menu.kid_help")
},
// 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")
}
};
function isOptionOn(key) {
return (0, _settings.getSettingValue)("breakout-settings-enable-" + key, options[key]?.default);
}
function toggleOption(key) {
(0, _settings.setSettingValue)("breakout-settings-enable-" + key, !isOptionOn(key));
}
},{"./i18n/i18n":"eNPRm","./settings":"5blfu","@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);
2025-03-26 14:04:54 +01:00
parcelHelpers.export(exports, "sumOfValues", ()=>sumOfValues);
parcelHelpers.export(exports, "makeEmptyPerksMap", ()=>makeEmptyPerksMap);
parcelHelpers.export(exports, "brickCenterX", ()=>brickCenterX);
parcelHelpers.export(exports, "brickCenterY", ()=>brickCenterY);
parcelHelpers.export(exports, "getRowColIndex", ()=>getRowColIndex);
parcelHelpers.export(exports, "getPossibleUpgrades", ()=>getPossibleUpgrades);
parcelHelpers.export(exports, "max_levels", ()=>max_levels);
parcelHelpers.export(exports, "pickedUpgradesHTMl", ()=>pickedUpgradesHTMl);
parcelHelpers.export(exports, "debuffsHTMl", ()=>debuffsHTMl);
2025-03-23 19:11:01 +01:00
parcelHelpers.export(exports, "levelsListHTMl", ()=>levelsListHTMl);
parcelHelpers.export(exports, "currentLevelInfo", ()=>currentLevelInfo);
parcelHelpers.export(exports, "isTelekinesisActive", ()=>isTelekinesisActive);
2025-03-19 20:14:55 +01:00
parcelHelpers.export(exports, "isYoyoActive", ()=>isYoyoActive);
parcelHelpers.export(exports, "findLast", ()=>findLast);
parcelHelpers.export(exports, "distance2", ()=>distance2);
parcelHelpers.export(exports, "distanceBetween", ()=>distanceBetween);
2025-03-19 21:58:08 +01:00
parcelHelpers.export(exports, "clamp", ()=>clamp);
parcelHelpers.export(exports, "defaultSounds", ()=>defaultSounds);
2025-03-19 20:14:55 +01:00
parcelHelpers.export(exports, "shouldPierceByColor", ()=>shouldPierceByColor);
2025-03-20 21:02:51 +01:00
parcelHelpers.export(exports, "countBricksAbove", ()=>countBricksAbove);
parcelHelpers.export(exports, "countBricksBelow", ()=>countBricksBelow);
var _loadGameData = require("./loadGameData");
2025-03-23 19:11:01 +01:00
var _i18N = require("./i18n/i18n");
var _debuffs = require("./debuffs");
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())];
}
2025-03-26 14:04:54 +01:00
function sumOfValues(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;
};
function brickCenterX(gameState, index) {
return gameState.offsetX + (index % gameState.gridSize + 0.5) * gameState.brickWidth;
}
function brickCenterY(gameState, index) {
return (Math.floor(index / gameState.gridSize) + 0.5) * gameState.brickWidth;
}
function getRowColIndex(gameState, row, col) {
if (row < 0 || col < 0 || row >= gameState.gridSize || col >= gameState.gridSize) return -1;
return row * gameState.gridSize + col;
}
function getPossibleUpgrades(gameState) {
return (0, _loadGameData.upgrades).filter((u)=>gameState.totalScoreAtRunStart >= u.threshold).filter((u)=>!u?.requires || gameState.perks[u?.requires]);
}
function max_levels(gameState) {
// TODO
return 2;
}
function pickedUpgradesHTMl(gameState) {
let list = "";
for (let u of (0, _loadGameData.upgrades))for(let i = 0; i < gameState.perks[u.id]; i++)list += `<span title="${u.name} : ${u.help(gameState.perks[u.id])}">${(0, _loadGameData.icons)["icon:" + u.id]}</span>`;
2025-03-27 10:52:31 +01:00
if (!list) return "";
return ` <p>${(0, _i18N.t)("score_panel.upgrades_picked")}</p> <p>${list}</p>`;
}
function debuffsHTMl(gameState) {
const banned = (0, _loadGameData.upgrades).filter((u)=>gameState.bannedPerks[u.id]).map((u)=>u.name).join(', ');
let list = (0, _debuffs.debuffs).filter((d)=>gameState.debuffs[d.id]).map((d)=>d.name(gameState.debuffs[d.id], banned)).join(', ');
if (!list) return "";
return `<p>${(0, _i18N.t)("score_panel.bebuffs_list")} : ${list}</p>`;
}
2025-03-23 19:11:01 +01:00
function levelsListHTMl(gameState) {
2025-03-23 22:19:28 +01:00
if (!gameState.perks.clairvoyant) return "";
2025-03-23 19:11:01 +01:00
let list = "";
2025-03-25 08:22:58 +01:00
for(let i = 0; i < max_levels(gameState); i++)list += `<span style="opacity: ${i >= gameState.currentLevel ? 1 : 0.2}" title="${gameState.runLevels[i].name}">${(0, _loadGameData.icons)[gameState.runLevels[i].name]}</span>`;
2025-03-23 22:19:28 +01:00
return `<p>${(0, _i18N.t)("score_panel.upcoming_levels")}</p><p>${list}</p>`;
2025-03-23 19:11:01 +01:00
}
function currentLevelInfo(gameState) {
2025-03-27 10:52:31 +01:00
return gameState.level;
}
function isTelekinesisActive(gameState, ball) {
2025-03-19 20:14:55 +01:00
return gameState.perks.telekinesis && ball.vy < 0;
}
function isYoyoActive(gameState, ball) {
return gameState.perks.yoyo && ball.vy > 0;
}
function findLast(arr, predicate) {
let i = arr.length;
while(--i)if (predicate(arr[i], i, arr)) return arr[i];
}
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));
}
2025-03-19 21:58:08 +01:00
function clamp(value, min, max) {
return Math.max(min, Math.min(value, max));
}
function defaultSounds() {
return {
aboutToPlaySound: {
wallBeep: {
vol: 0,
x: 0
},
comboIncreaseMaybe: {
vol: 0,
x: 0
},
comboDecrease: {
vol: 0,
x: 0
},
coinBounce: {
vol: 0,
x: 0
},
explode: {
vol: 0,
x: 0
},
lifeLost: {
vol: 0,
x: 0
},
coinCatch: {
vol: 0,
x: 0
},
colorChange: {
vol: 0,
x: 0
2025-03-27 10:52:31 +01:00
},
void: {
vol: 0,
x: 0
}
}
};
}
2025-03-19 20:14:55 +01:00
function shouldPierceByColor(gameState, 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;
}
2025-03-20 21:02:51 +01:00
function countBricksAbove(gameState, index) {
const col = index % gameState.gridSize;
const row = Math.floor(index / gameState.gridSize);
let count = 0;
for(let y = 0; y < row; y++)if (gameState.bricks[col + y * gameState.gridSize]) count++;
return count;
}
function countBricksBelow(gameState, index) {
const col = index % gameState.gridSize;
const row = Math.floor(index / gameState.gridSize);
let count = 0;
for(let y = row + 1; y < gameState.gridSize; y++)if (gameState.bricks[col + y * gameState.gridSize]) count++;
return count;
}
},{"./loadGameData":"l1B4x","./i18n/i18n":"eNPRm","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3","./debuffs":"9Mo9a"}],"9Mo9a":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "debuffs", ()=>debuffs);
var _i18N = require("./i18n/i18n");
const debuffs = [
{
id: "negative_coins",
max: 20,
name: (lvl)=>(0, _i18N.t)("debuffs.negative_coins.help", {
lvl
}),
help: (lvl)=>(0, _i18N.t)("debuffs.negative_coins.help", {
lvl
})
},
{
id: "more_bombs",
max: 20,
name: (lvl)=>(0, _i18N.t)("debuffs.more_bombs.help", {
lvl
}),
help: (lvl)=>(0, _i18N.t)("debuffs.more_bombs.help", {
lvl
})
},
{
id: "banned",
max: 50,
name: (lvl, banned)=>(0, _i18N.t)("debuffs.banned.description", {
lvl,
banned
}),
help: (lvl, perk)=>(0, _i18N.t)("debuffs.banned.help", {
lvl,
perk
})
},
{
id: "interference",
max: 20,
name: (lvl)=>(0, _i18N.t)("debuffs.interference.help", {
lvl
}),
help: (lvl)=>(0, _i18N.t)("debuffs.interference.help", {
lvl
})
}
]; /*
Possible challenges :
- interference : telekinesis works backward for lvl/2 seconds every 5 seconds (show timer ?)
- exclusion : one of your current perks (except the kept one) is banned
- fireworks : some bricks are explosive, you're not told which ones
-
- graphical effects like trail, contrast, blur to make it harder to see what's going on
- ball creates a draft behind itself that blows coins in odd patterns
- bricks are invisible
- downward wind
- side wind
- add red anti-coins that apply downgrades
- destroy your combo
- hurt your score
- behave like heavier coins.
- deactivate a perk for this level
- reduce your number of coins
- destroy all coins on screen
- lowers your combo
- reduce your choice for your next perk
*/
},{"./i18n/i18n":"eNPRm","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"2n0gK":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
if ("serviceWorker" in navigator && window.location.href.endsWith("/index.html?isPWA=true")) {
// @ts-ignore
const url = new URL(require("b04459cc43e56e8c"));
navigator.serviceWorker.register(url);
}
},{"b04459cc43e56e8c":"jblPb","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"jblPb":[function(require,module,exports,__globalThis) {
module.exports = require("a15a43021d40e52d").getBundleURL('bgzJG') + "sw-b71.41cdff1b.js";
},{"a15a43021d40e52d":"lgJ39"}],"9ZeQl":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "setMousePos", ()=>setMousePos);
parcelHelpers.export(exports, "resetBalls", ()=>resetBalls);
parcelHelpers.export(exports, "putBallsAtPuck", ()=>putBallsAtPuck);
parcelHelpers.export(exports, "normalizeGameState", ()=>normalizeGameState);
parcelHelpers.export(exports, "baseCombo", ()=>baseCombo);
parcelHelpers.export(exports, "resetCombo", ()=>resetCombo);
parcelHelpers.export(exports, "decreaseCombo", ()=>decreaseCombo);
parcelHelpers.export(exports, "spawnExplosion", ()=>spawnExplosion);
parcelHelpers.export(exports, "spawnImplosion", ()=>spawnImplosion);
2025-03-19 21:58:08 +01:00
parcelHelpers.export(exports, "explosionAt", ()=>explosionAt);
parcelHelpers.export(exports, "explodeBrick", ()=>explodeBrick);
parcelHelpers.export(exports, "dontOfferTooSoon", ()=>dontOfferTooSoon);
parcelHelpers.export(exports, "pickRandomUpgrades", ()=>pickRandomUpgrades);
parcelHelpers.export(exports, "schedulGameSound", ()=>schedulGameSound);
parcelHelpers.export(exports, "addToScore", ()=>addToScore);
parcelHelpers.export(exports, "gotoNextLoop", ()=>gotoNextLoop);
parcelHelpers.export(exports, "setLevel", ()=>setLevel);
parcelHelpers.export(exports, "rainbowColor", ()=>rainbowColor);
parcelHelpers.export(exports, "repulse", ()=>repulse);
parcelHelpers.export(exports, "attract", ()=>attract);
2025-03-19 20:14:55 +01:00
parcelHelpers.export(exports, "coinBrickHitCheck", ()=>coinBrickHitCheck);
parcelHelpers.export(exports, "bordersHitCheck", ()=>bordersHitCheck);
parcelHelpers.export(exports, "gameStateTick", ()=>gameStateTick);
parcelHelpers.export(exports, "ballTick", ()=>ballTick);
parcelHelpers.export(exports, "append", ()=>append);
parcelHelpers.export(exports, "destroy", ()=>destroy);
parcelHelpers.export(exports, "liveCount", ()=>liveCount);
parcelHelpers.export(exports, "empty", ()=>empty);
parcelHelpers.export(exports, "forEachLiveOne", ()=>forEachLiveOne);
var _gameUtils = require("./game_utils");
var _i18N = require("./i18n/i18n");
var _loadGameData = require("./loadGameData");
var _settings = require("./settings");
var _render = require("./render");
var _gameOver = require("./gameOver");
var _game = require("./game");
var _recording = require("./recording");
var _options = require("./options");
var _premium = require("./premium");
var _newGameState = require("./newGameState");
var _debuffs = require("./debuffs");
var _asyncAlert = require("./asyncAlert");
function setMousePos(gameState, x) {
// Sets the puck position, and updates the ball position if they are supposed to follow it
gameState.puckPosition = x;
gameState.needsRender = true;
}
function getBallDefaultVx(gameState) {
return (gameState.perks.concave_puck ? 0 : 1) * (Math.random() > 0.5 ? gameState.baseSpeed : -gameState.baseSpeed);
}
function resetBalls(gameState) {
2025-03-24 10:38:01 +01:00
// Always compute speed first
normalizeGameState(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 = getBallDefaultVx(gameState);
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,
2025-03-24 10:38:01 +01:00
// sx: 0,
// sy: 0,
2025-03-23 19:11:01 +01:00
piercePoints: gameState.perks.pierce * 3,
hitSinceBounce: 0,
2025-03-22 16:04:25 +01:00
brokenSinceBounce: 0,
hitItem: [],
sapperUses: 0
});
}
gameState.ballStickToPuck = true;
}
function putBallsAtPuck(gameState) {
// This reset could be abused to cheat quite easily
const count = gameState.balls.length;
const perBall = gameState.puckWidth / (count + 1);
// const vx = getBallDefaultVx(gameState);
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 = vx;
// ball.previousVX = ball.vx;
// ball.vy = -gameState.baseSpeed;
// ball.previousVY = ball.vy;
2025-03-24 10:38:01 +01:00
// ball.sx = 0;
// ball.sy = 0;
ball.hitItem = [];
ball.hitSinceBounce = 0;
2025-03-22 16:04:25 +01:00
ball.brokenSinceBounce = 0;
2025-03-23 19:11:01 +01:00
ball.piercePoints = gameState.perks.pierce * 3;
});
}
function normalizeGameState(gameState) {
// This function resets most parameters on the state to correct values, and should be used even when the game is paused
gameState.baseSpeed = Math.max(3, gameState.gameZoneWidth / 12 / 10 + gameState.currentLevel / 3 + gameState.levelTime / 30000 - gameState.perks.slow_down * 2);
gameState.puckWidth = gameState.gameZoneWidth / 12 * (3 - gameState.perks.smaller_puck + gameState.perks.bigger_puck);
let minX = gameState.perks.corner_shot && gameState.levelTime ? gameState.offsetXRoundedDown - gameState.puckWidth / 2 : gameState.offsetXRoundedDown + gameState.puckWidth / 2;
let maxX = gameState.perks.corner_shot && gameState.levelTime ? gameState.offsetXRoundedDown + gameState.gameZoneWidthRoundedUp + gameState.puckWidth / 2 : gameState.offsetXRoundedDown + gameState.gameZoneWidthRoundedUp - gameState.puckWidth / 2;
gameState.puckPosition = (0, _gameUtils.clamp)(gameState.puckPosition, minX, maxX);
if (gameState.ballStickToPuck) putBallsAtPuck(gameState);
2025-03-25 08:22:58 +01:00
if (Math.abs(gameState.lastPuckPosition - gameState.puckPosition) > 1 && gameState.running) gameState.lastPuckMove = gameState.levelTime;
gameState.lastPuckPosition = gameState.puckPosition;
}
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 (prev > gameState.combo && gameState.perks.soft_reset) gameState.combo += Math.floor((prev - gameState.combo) * (gameState.perks.soft_reset * 10) / 100);
const lost = Math.max(0, prev - gameState.combo);
if (lost) {
for(let i = 0; i < lost && i < 8; i++)setTimeout(()=>schedulGameSound(gameState, "comboDecrease", x, 1), i * 100);
if (typeof x !== "undefined" && typeof y !== "undefined") makeText(gameState, x, y, "red", "-" + lost, 20, 150);
}
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) {
schedulGameSound(gameState, "comboDecrease", x, 1);
if (typeof x !== "undefined" && typeof y !== "undefined") makeText(gameState, x, y, "red", "-" + lost, 20, 300);
}
}
function spawnExplosion(gameState, count, x, y, color) {
if (!!(0, _options.isOptionOn)("basic")) return;
if (liveCount(gameState.particles) > (0, _settings.getCurrentMaxParticles)()) // Avoid freezing when lots of explosion happen at once
count = 1;
for(let i = 0; i < count; i++)makeParticle(gameState, x + (Math.random() - 0.5) * gameState.brickWidth / 2, y + (Math.random() - 0.5) * gameState.brickWidth / 2, (Math.random() - 0.5) * 30, (Math.random() - 0.5) * 30, color, false);
}
function spawnImplosion(gameState, count, x, y, color) {
if (!!(0, _options.isOptionOn)("basic")) return;
if (liveCount(gameState.particles) > (0, _settings.getCurrentMaxParticles)()) // Avoid freezing when lots of explosion happen at once
count = 1;
for(let i = 0; i < count; i++){
const dx = (Math.random() - 0.5) * gameState.brickWidth / 2;
const dy = (Math.random() - 0.5) * gameState.brickWidth / 2;
makeParticle(gameState, x - dx * 10, y - dy * 10, dx, dy, color, false);
}
}
2025-03-19 21:58:08 +01:00
function explosionAt(gameState, index, x, y, ball) {
const size = 1 + gameState.perks.bigger_explosions;
schedulGameSound(gameState, "explode", ball.x, 1);
if (index !== -1) {
const col = index % gameState.gridSize;
const row = Math.floor(index / gameState.gridSize);
// Break bricks around
for(let dx = -size; dx <= size; dx++)for(let dy = -size; dy <= size; dy++){
const i = (0, _gameUtils.getRowColIndex)(gameState, row + dy, col + dx);
if (gameState.bricks[i] && i !== -1) {
// Study bricks resist explosions too
gameState.brickHP[i]--;
2025-03-25 08:47:24 +01:00
if (gameState.brickHP[i] <= 0) explodeBrick(gameState, i, ball, true);
}
2025-03-19 21:58:08 +01:00
}
}
const factor = gameState.perks.implosions ? -1 : 1;
2025-03-19 21:58:08 +01:00
// Blow nearby coins
forEachLiveOne(gameState.coins, (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 * factor;
c.vy += dy / d2 * 10 * size / c.weight * factor;
2025-03-19 21:58:08 +01:00
});
gameState.lastExplosion = Date.now();
// makeLight(gameState, x, y, "white", gameState.brickWidth * 2, 150);
if (gameState.perks.implosions) spawnImplosion(gameState, 7 * (1 + gameState.perks.bigger_explosions), x, y, "white");
else spawnExplosion(gameState, 7 * (1 + gameState.perks.bigger_explosions), x, y, "white");
2025-03-19 21:58:08 +01:00
gameState.runStatistics.bricks_broken++;
if (gameState.perks.zen) resetCombo(gameState, x, y);
}
function explodeBrick(gameState, index, ball, isExplosion) {
const color = gameState.bricks[index];
if (!color) return;
2025-03-27 10:52:31 +01:00
if (color === "black" || color === "transparent") {
const x = (0, _gameUtils.brickCenterX)(gameState, index), y = (0, _gameUtils.brickCenterY)(gameState, index);
2025-03-27 10:52:31 +01:00
if (color === "transparent") {
schedulGameSound(gameState, "void", x, 1);
resetCombo(gameState, x, y);
}
2025-03-23 22:19:28 +01:00
setBrick(gameState, index, "");
2025-03-19 21:58:08 +01:00
explosionAt(gameState, index, x, y, ball);
} else if (color) {
// Even if it bounces we don't want to count that as a miss
// Flashing is take care of by the tick loop
const x = (0, _gameUtils.brickCenterX)(gameState, index), y = (0, _gameUtils.brickCenterY)(gameState, index);
setBrick(gameState, index, "");
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 = (0, _settings.getCurrentMaxCoins)() * ((0, _options.isOptionOn)("basic") ? 0.5 : 1);
const spawnableCoins = liveCount(gameState.coins) > (0, _settings.getCurrentMaxCoins)() ? 1 : Math.floor(maxCoins - liveCount(gameState.coins)) / 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);
makeCoin(gameState, cx, cy, ball.previousVX * (0.5 + Math.random()), ball.previousVY * (0.5 + Math.random()), gameState.perks.metamorphosis || (0, _options.isOptionOn)('colorful_coins') ? color : "gold", points);
}
2025-03-22 16:04:25 +01:00
gameState.combo += 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 + gameState.perks.asceticism + gameState.perks.zen + gameState.perks.passive_income + gameState.perks.nbricks + gameState.perks.unbounded;
if (gameState.perks.side_kick) {
if (Math.abs(ball.vx) > Math.abs(ball.vy)) gameState.combo += gameState.perks.side_kick;
else decreaseCombo(gameState, gameState.perks.side_kick, ball.x, ball.y);
}
2025-03-20 21:02:51 +01:00
if (gameState.perks.reach) {
if ((0, _gameUtils.countBricksAbove)(gameState, index) && !(0, _gameUtils.countBricksBelow)(gameState, index)) resetCombo(gameState, x, y);
else gameState.combo += gameState.perks.reach;
}
2025-03-25 08:22:58 +01:00
if (gameState.lastPuckMove && gameState.perks.passive_income && gameState.lastPuckMove > gameState.levelTime - 250 * gameState.perks.passive_income) resetCombo(gameState, x, y);
2025-03-27 10:52:31 +01:00
if (gameState.perks.nbricks && ball.brokenSinceBounce > gameState.perks.nbricks) // We need to reset at each hit, otherwise it's just an OP version of single puck hit streak
resetCombo(gameState, ball.x, ball.y);
if (!isExplosion) {
// color change
if ((gameState.perks.picky_eater || gameState.perks.pierce_color) && color !== gameState.ballsColor && color) {
if (gameState.perks.picky_eater) resetCombo(gameState, ball.x, ball.y);
schedulGameSound(gameState, "colorChange", ball.x, 0.8);
gameState.lastExplosion = gameState.levelTime;
gameState.ballsColor = color;
if (!(0, _options.isOptionOn)("basic")) gameState.balls.forEach((ball)=>{
spawnExplosion(gameState, 7, ball.previousX, ball.previousY, color);
});
} else schedulGameSound(gameState, "comboIncreaseMaybe", ball.x, 1);
}
// makeLight(gameState, x, y, color, gameState.brickWidth, 40);
spawnExplosion(gameState, 5 + Math.min(gameState.combo, 30), x, y, color);
}
if (!gameState.bricks[index] && color !== "black") ball.hitItem?.push({
index,
color
});
}
function dontOfferTooSoon(gameState, id) {
gameState.lastOffered[id] = Math.round(Date.now() / 1000);
}
function pickRandomUpgrades(gameState, count) {
let list = (0, _gameUtils.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).filter((u)=>!gameState.bannedPerks[u.id]).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 schedulGameSound(gameState, sound, x, vol) {
if (!vol) return;
x ??= gameState.offsetX + gameState.gameZoneWidth / 2;
const ex = gameState.aboutToPlaySound[sound];
ex.x = (x * vol + ex.x * ex.vol) / (vol + ex.vol);
ex.vol += vol;
}
function addToScore(gameState, coin) {
gameState.score += coin.points;
gameState.lastScoreIncrease = gameState.levelTime;
(0, _settings.addToTotalScore)(gameState, 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")) makeParticle(gameState, coin.previousX, coin.previousY, (gameState.canvasWidth - coin.x) / 100, -coin.y / 100, coin.color, true, gameState.coinSize / 2, 100 + Math.random() * 50);
2025-03-27 10:52:31 +01:00
if (coin.points > 0) schedulGameSound(gameState, "coinCatch", coin.x, 1);
else resetCombo(gameState, coin.x, coin.y);
gameState.runStatistics.score += coin.points;
2025-03-19 20:14:55 +01:00
if (gameState.perks.asceticism) resetCombo(gameState, coin.x, coin.y);
}
async function gotoNextLoop(gameState) {
(0, _game.pause)(false);
gameState.loop++;
gameState.runLevels = (0, _newGameState.getRunLevels)(gameState.totalScoreAtRunStart, {});
gameState.upgradesOfferedFor = -1;
// Add random debuf
// gameState.debuffs[randomDebuff]++
const userPerks = (0, _loadGameData.upgrades).filter((u)=>gameState.perks[u.id]);
const { keep, debuff, targetPerk } = await (0, _asyncAlert.requiredAsyncAlert)({
title: (0, _i18N.t)('loop.title', {
loop: gameState.loop
}),
content: [
(0, _i18N.t)('loop.instructions'),
...userPerks.map((u)=>{
const randomDebuff = (0, _gameUtils.sample)((0, _debuffs.debuffs).filter((d)=>gameState.debuffs[d.id] < d.max)) || (0, _gameUtils.sample)((0, _debuffs.debuffs));
const targetPerk = (0, _gameUtils.sample)(userPerks.filter((tp)=>tp.id !== u.id));
return {
text: u.name + (0, _i18N.t)('level_up.upgrade_perk_to_level', {
level: gameState.perks[u.id]
}),
help: randomDebuff.help(gameState.debuffs[randomDebuff.id] + 1, targetPerk.name),
icon: u.icon,
value: {
keep: u.id,
debuff: randomDebuff.id,
targetPerk: targetPerk.id
}
};
})
]
});
Object.assign(gameState.perks, (0, _gameUtils.makeEmptyPerksMap)((0, _loadGameData.upgrades)), {
[keep]: gameState.perks[keep]
});
gameState.debuffs[debuff]++;
if (debuff == 'banned') gameState.bannedPerks[targetPerk]++;
await setLevel(gameState, 0);
}
async function setLevel(gameState, l) {
2025-03-20 23:11:42 +01:00
// Here to alleviate double upgrades issues
2025-03-22 16:04:25 +01:00
if (gameState.upgradesOfferedFor >= l) {
debugger;
return console.warn("Extra upgrade request ignored ");
}
2025-03-20 23:11:42 +01:00
gameState.upgradesOfferedFor = l;
(0, _game.pause)(false);
2025-03-20 22:50:50 +01:00
(0, _recording.stopRecording)();
if (l > 0) await (0, _game.openShortRunUpgradesPicker)(gameState);
gameState.currentLevel = l;
2025-03-27 10:52:31 +01:00
gameState.level = gameState.runLevels[l];
gameState.levelTime = 0;
2025-03-22 16:47:02 +01:00
gameState.winAt = 0;
gameState.levelWallBounces = 0;
gameState.autoCleanUses = 0;
gameState.lastTickDown = gameState.levelTime;
gameState.levelStartScore = gameState.score;
gameState.levelSpawnedCoins = 0;
gameState.levelMisses = 0;
gameState.runStatistics.levelsPlayed++;
// Reset combo silently
2025-03-20 08:13:17 +01:00
const finalCombo = gameState.combo;
gameState.combo = baseCombo(gameState);
2025-03-20 21:24:25 +01:00
if (gameState.perks.shunt) gameState.combo += Math.round(Math.max(0, (finalCombo - gameState.combo) * 20 * gameState.perks.shunt / 100));
2025-03-19 20:14:55 +01:00
gameState.combo += gameState.perks.hot_start * 15;
const lvl = (0, _gameUtils.currentLevelInfo)(gameState);
if (lvl.size !== gameState.gridSize) {
gameState.gridSize = lvl.size;
(0, _game.fitSize)();
}
empty(gameState.coins);
empty(gameState.particles);
empty(gameState.lights);
empty(gameState.texts);
gameState.bricks = [];
for(let i = 0; i < lvl.size * lvl.size; i++)setBrick(gameState, i, lvl.bricks[i]);
if (gameState.debuffs.more_bombs) {
2025-03-27 10:52:31 +01:00
let attemps = 0;
let changed = 0;
while(attemps < 100 && changed < gameState.debuffs.more_bombs){
2025-03-27 10:52:31 +01:00
attemps++;
const index = Math.floor(Math.random() * gameState.bricks.length);
if (gameState.bricks[index] && gameState.bricks[index] !== "black") {
gameState.bricks[index] = "black";
2025-03-27 10:52:31 +01:00
gameState.brickHP[index] = 1;
changed++;
}
}
}
2025-03-22 16:04:25 +01:00
// Balls color will depend on most common brick color sometimes
resetBalls(gameState);
gameState.needsRender = true;
// 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)
(0, _render.background).src = "data:image/svg+xml;UTF8," + lvl.svg;
}
function setBrick(gameState, index, color) {
2025-03-23 22:19:28 +01:00
gameState.bricks[index] = color || "";
gameState.brickHP[index] = color === "black" && 1 || color && 1 + gameState.perks.sturdy_bricks + gameState.loop || 0;
}
function rainbowColor() {
return `hsl(${Math.round((0, _game.gameState).levelTime / 4) * 2 % 360},100%,70%)`;
}
function repulse(gameState, a, b, power, impactsBToo) {
const distance = (0, _gameUtils.distanceBetween)(a, b);
// Ensure we don't get soft locked
const max = gameState.gameZoneWidth / 4;
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;
makeParticle(gameState, a.x, a.y, -dx * speed + a.vx + (Math.random() - 0.5) * rand, -dy * speed + a.vy + (Math.random() - 0.5) * rand, rainbowColor(), true, gameState.coinSize / 2, 100);
if (impactsBToo && typeof b.vx !== "undefined" && typeof b.vy !== "undefined") makeParticle(gameState, b.x, b.y, dx * speed + b.vx + (Math.random() - 0.5) * rand, dy * speed + b.vy + (Math.random() - 0.5) * rand, rainbowColor(), true, gameState.coinSize / 2, 100);
}
function attract(gameState, a, b, power) {
const distance = (0, _gameUtils.distanceBetween)(a, b);
// Ensure we don't get soft locked
const min = gameState.gameZoneWidth * 3 / 4;
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;
makeParticle(gameState, a.x, a.y, dx * speed + a.vx + (Math.random() - 0.5) * rand, dy * speed + a.vy + (Math.random() - 0.5) * rand, rainbowColor(), true, gameState.coinSize / 2, 100);
makeParticle(gameState, b.x, b.y, -dx * speed + b.vx + (Math.random() - 0.5) * rand, -dy * speed + b.vy + (Math.random() - 0.5) * rand, rainbowColor(), true, gameState.coinSize / 2, 100);
}
2025-03-19 20:14:55 +01:00
function coinBrickHitCheck(gameState, coin) {
// Make ball/coin bonce, and return bricks that were hit
const radius = coin.size / 2;
const { x, y, previousX, previousY } = coin;
const vhit = (0, _game.hitsSomething)(previousX, y, radius);
const hhit = (0, _game.hitsSomething)(x, previousY, radius);
const chit = typeof vhit == "undefined" && typeof hhit == "undefined" && (0, _game.hitsSomething)(x, y, radius) || undefined;
2025-03-23 19:11:01 +01:00
if (!gameState.perks.ghost_coins) {
if (typeof vhit !== "undefined" || typeof chit !== "undefined") {
coin.y = coin.previousY;
coin.vy *= -1;
// Roll on corners
const leftHit = gameState.bricks[(0, _game.brickIndex)(x - radius, y + radius)];
const rightHit = gameState.bricks[(0, _game.brickIndex)(x + radius, y + radius)];
if (leftHit && !rightHit) {
coin.vx += 1;
coin.sa -= 1;
}
if (!leftHit && rightHit) {
coin.vx -= 1;
coin.sa += 1;
}
2025-03-19 20:14:55 +01:00
}
2025-03-23 19:11:01 +01:00
if (typeof hhit !== "undefined" || typeof chit !== "undefined") {
coin.x = coin.previousX;
coin.vx *= -1;
2025-03-19 20:14:55 +01:00
}
}
return vhit ?? hhit ?? chit;
}
function bordersHitCheck(gameState, 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;
2025-03-24 10:38:01 +01:00
// 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;
2025-03-19 20:14:55 +01:00
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 && !gameState.perks.unbounded) {
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 && !gameState.perks.unbounded) {
coin.x = gameState.canvasWidth - gameState.offsetXRoundedDown - radius - (coin.x - (gameState.canvasWidth - gameState.offsetXRoundedDown - radius));
coin.vx *= -1;
hhit = 1;
}
return hhit + vhit * 2;
}
function gameStateTick(gameState, // How many frames to compute at once, can go above 1 to compensate lag
frames = 1) {
gameState.runStatistics.max_combo = Math.max(gameState.runStatistics.max_combo, gameState.combo);
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;
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(gameState, index, gameState.balls[0], true);
});
gameState.autoCleanUses++;
}
2025-03-22 16:04:25 +01:00
const hasPendingBricks = gameState.perks.respawn && gameState.balls.find((b)=>b.hitItem.length > 1);
2025-03-22 16:47:02 +01:00
if (gameState.running && !remainingBricks && !hasPendingBricks) {
if (!gameState.winAt) gameState.winAt = gameState.levelTime + 5000;
} else gameState.winAt = 0;
if (gameState.running && // Delayed win when coins are still flying
2025-03-22 16:47:02 +01:00
gameState.winAt && gameState.levelTime > gameState.winAt || // instant win condition
gameState.levelTime && !remainingBricks && !liveCount(gameState.coins)) {
if (gameState.currentLevel + 1 < (0, _gameUtils.max_levels)(gameState)) setLevel(gameState, gameState.currentLevel + 1);
else if ((0, _premium.isPremium)()) gotoNextLoop(gameState);
else (0, _gameOver.gameOver)((0, _i18N.t)("gameOver.win.title"), (0, _i18N.t)("gameOver.win.summary", {
score: gameState.score
}));
} else if (gameState.running || gameState.levelTime) {
const coinRadius = Math.round(gameState.coinSize / 2);
forEachLiveOne(gameState.coins, (coin, coinIndex)=>{
if (gameState.perks.coin_magnet) {
2025-03-19 20:14:55 +01:00
const strength = 100 / (100 + Math.pow(coin.y - gameState.gameZoneHeight, 2) + Math.pow(coin.x - gameState.puckPosition, 2)) * gameState.perks.coin_magnet;
const attractionX = frames * (gameState.puckPosition - coin.x) * strength;
coin.vx += attractionX;
2025-03-19 20:14:55 +01:00
coin.vy += frames * (gameState.gameZoneHeight - coin.y) * strength / 2;
coin.sa -= attractionX / 10;
}
2025-03-19 21:58:08 +01:00
if (gameState.perks.ball_attracts_coins) gameState.balls.forEach((ball)=>{
const d2 = (0, _gameUtils.distance2)(ball, coin);
coin.vx += (ball.x - coin.x) / d2 * 30 * gameState.perks.ball_attracts_coins;
coin.vy += (ball.y - coin.y) / d2 * 30 * gameState.perks.ball_attracts_coins;
});
const ratio = 1 - (gameState.perks.viscosity * 0.03 + 0.005) * frames;
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
2025-03-19 20:14:55 +01:00
if (!gameState.perks.etherealcoins) {
2025-03-19 21:58:08 +01:00
const flip = gameState.perks.helium > 0 && Math.abs(coin.x - gameState.puckPosition) * 2 > gameState.puckWidth + coin.size;
2025-03-19 20:14:55 +01:00
coin.vy += frames * coin.weight * 0.8 * (flip ? -1 : 1);
2025-03-24 10:38:01 +01:00
if (flip && !(0, _options.isOptionOn)("basic") && Math.random() < 0.1) makeParticle(gameState, coin.x, coin.y, 0, gameState.baseSpeed, coin.color, true, 5, 250);
2025-03-19 20:14:55 +01:00
}
2025-03-24 10:38:01 +01:00
const speed = (Math.abs(coin.vx) + Math.abs(coin.vy)) * 10;
2025-03-19 20:14:55 +01:00
const hitBorder = bordersHitCheck(gameState, coin, coin.size / 2, frames);
2025-03-27 10:52:31 +01:00
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 , negative in case it's a negative coin
gameState.puckHeight * (coin.points ? 1 : -1)) {
addToScore(gameState, coin);
destroy(gameState.coins, coinIndex);
} else if (coin.y > gameState.canvasHeight + coinRadius) {
destroy(gameState.coins, coinIndex);
if (gameState.perks.compound_interest) resetCombo(gameState, coin.x, coin.y);
2025-03-20 22:50:50 +01:00
} else if (gameState.perks.unbounded && (coin.x < -gameState.gameZoneWidth / 2 || coin.x > gameState.canvasWidth + gameState.gameZoneWidth / 2)) // Out of bound on sides
2025-03-19 20:14:55 +01:00
destroy(gameState.coins, coinIndex);
const hitBrick = coinBrickHitCheck(gameState, coin);
if (gameState.perks.metamorphosis && typeof hitBrick !== "undefined") {
if (gameState.bricks[hitBrick] && coin.color !== gameState.bricks[hitBrick] && gameState.bricks[hitBrick] !== "black" && !coin.coloredABrick) {
// Not using setbrick because we don't want to reset HP
gameState.bricks[hitBrick] = coin.color;
coin.coloredABrick = true;
schedulGameSound(gameState, "colorChange", coin.x, 0.3);
}
}
2025-03-23 19:11:01 +01:00
if (!gameState.perks.ghost_coins && typeof hitBrick !== "undefined" || hitBorder) {
coin.vx *= 0.8;
coin.vy *= 0.8;
coin.sa *= 0.9;
if (speed > 20) schedulGameSound(gameState, "coinBounce", coin.x, 0.2);
if (Math.abs(coin.vy) < 3) coin.vy = 0;
}
});
gameState.balls.forEach((ball)=>ballTick(gameState, ball, frames));
2025-03-19 21:58:08 +01:00
if (gameState.perks.shocks) gameState.balls.forEach((a, ai)=>gameState.balls.forEach((b, bi)=>{
if (ai < bi && !a.destroyed && !b.destroyed && (0, _gameUtils.distance2)(a, b) < gameState.ballSize * gameState.ballSize) {
let tempVx = a.vx;
let tempVy = a.vy;
a.vx = b.vx;
a.vy = b.vy;
b.vx = tempVx;
b.vy = tempVy;
let x = (a.x + b.x) / 2;
let y = (a.y + b.y) / 2;
const limit = gameState.baseSpeed;
a.vx += (0, _gameUtils.clamp)(a.x - x, -limit, limit) + (Math.random() - 0.5) * limit / 3;
a.vy += (0, _gameUtils.clamp)(a.y - y, -limit, limit) + (Math.random() - 0.5) * limit / 3;
b.vx += (0, _gameUtils.clamp)(b.x - x, -limit, limit) + (Math.random() - 0.5) * limit / 3;
b.vy += (0, _gameUtils.clamp)(b.y - y, -limit, limit) + (Math.random() - 0.5) * limit / 3;
let index = (0, _game.brickIndex)(x, y);
explosionAt(gameState, index, x, y, a);
}
}));
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) makeParticle(gameState, gameState.offsetXRoundedDown + Math.random() * gameState.gameZoneWidthRoundedUp, Math.random() * gameState.gameZoneHeight, windD * 8, 0, rainbowColor(), true, gameState.coinSize / 2, 150);
}
forEachLiveOne(gameState.particles, (flash, index)=>{
flash.x += flash.vx * frames;
flash.y += flash.vy * frames;
if (!flash.ethereal) {
flash.vy += 0.5;
if ((0, _game.hasBrick)((0, _game.brickIndex)(flash.x, flash.y))) destroy(gameState.particles, index);
}
});
}
if (gameState.combo > baseCombo(gameState) && !(0, _options.isOptionOn)("basic") && (gameState.combo - baseCombo(gameState)) * Math.random() > 5) {
// The red should still be visible on a white bg
if (gameState.perks.top_is_lava) makeParticle(gameState, gameState.offsetXRoundedDown + Math.random() * gameState.gameZoneWidthRoundedUp, 0, (Math.random() - 0.5) * 10, 5, "red", true, gameState.coinSize / 2, 100 * (Math.random() + 1));
if (gameState.perks.left_is_lava) makeParticle(gameState, gameState.offsetXRoundedDown, Math.random() * gameState.gameZoneHeight, 5, (Math.random() - 0.5) * 10, "red", true, gameState.coinSize / 2, 100 * (Math.random() + 1));
if (gameState.perks.right_is_lava) makeParticle(gameState, gameState.offsetXRoundedDown + gameState.gameZoneWidthRoundedUp, Math.random() * gameState.gameZoneHeight, -5, (Math.random() - 0.5) * 10, "red", true, gameState.coinSize / 2, 100 * (Math.random() + 1));
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);
makeParticle(gameState, x, gameState.gameZoneHeight, (Math.random() - 0.5) * 10, -5, "red", true, gameState.coinSize / 2, 100 * (Math.random() + 1));
}
if (gameState.perks.streak_shots) {
const pos = 0.5 - Math.random();
makeParticle(gameState, gameState.puckPosition + gameState.puckWidth * pos, gameState.gameZoneHeight - gameState.puckHeight, pos * 10, -5, "red", true, gameState.coinSize / 2, 100 * (Math.random() + 1));
}
}
forEachLiveOne(gameState.particles, (p, pi)=>{
if (gameState.levelTime > p.time + p.duration) destroy(gameState.particles, pi);
});
forEachLiveOne(gameState.texts, (p, pi)=>{
if (gameState.levelTime > p.time + p.duration) destroy(gameState.texts, pi);
});
forEachLiveOne(gameState.lights, (p, pi)=>{
if (gameState.levelTime > p.time + p.duration) destroy(gameState.lights, pi);
});
}
function ballTick(gameState, 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 ((0, _gameUtils.isTelekinesisActive)(gameState, ball)) {
speedLimitDampener += 3;
ball.vx += (gameState.puckPosition - ball.x) / 1000 * delta * gameState.perks.telekinesis;
}
2025-03-19 20:14:55 +01:00
if ((0, _gameUtils.isYoyoActive)(gameState, ball)) {
speedLimitDampener += 3;
ball.vx += (gameState.puckPosition - ball.x) / 1000 * delta * gameState.perks.yoyo;
}
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(gameState, 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(gameState, 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(gameState, 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;
makeParticle(gameState, (0, _gameUtils.brickCenterX)(gameState, index) + dx * gameState.brickWidth / 2, (0, _gameUtils.brickCenterY)(gameState, index) + dy * gameState.brickWidth / 2, vertical ? 0 : -dx * gameState.baseSpeed, vertical ? -dy * gameState.baseSpeed : 0, color, true, gameState.coinSize / 2, 250);
}
2025-03-19 20:14:55 +01:00
const borderHitCode = bordersHitCheck(gameState, ball, gameState.ballSize / 2, delta);
if (borderHitCode) {
if (gameState.perks.left_is_lava && borderHitCode % 2 && ball.x < gameState.offsetX + gameState.gameZoneWidth / 2) resetCombo(gameState, ball.x, ball.y);
if (gameState.perks.right_is_lava && borderHitCode % 2 && ball.x > gameState.offsetX + gameState.gameZoneWidth / 2) resetCombo(gameState, ball.x, ball.y);
if (gameState.perks.top_is_lava && borderHitCode >= 2) resetCombo(gameState, ball.x, ball.y + gameState.ballSize);
2025-03-20 18:44:46 +01:00
if (gameState.perks.trampoline && borderHitCode >= 2) decreaseCombo(gameState, gameState.perks.trampoline, ball.x, ball.y + gameState.ballSize);
schedulGameSound(gameState, "wallBeep", ball.x, 1);
gameState.levelWallBounces++;
gameState.runStatistics.wall_bounces++;
}
// 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) * (gameState.perks.concave_puck ? -0.5 : 1));
ball.vx = speed * Math.cos(angle);
ball.vy = speed * Math.sin(angle);
schedulGameSound(gameState, "wallBeep", ball.x, 1);
} else {
ball.vy *= -1;
2025-03-19 21:58:08 +01:00
gameState.perks.extra_life -= 1;
if (gameState.perks.extra_life < 0) gameState.perks.extra_life = 0;
else if (gameState.perks.sacrifice) gameState.bricks.forEach((color, index)=>color && explodeBrick(gameState, index, ball, true));
schedulGameSound(gameState, "lifeLost", ball.x, 1);
if (!(0, _options.isOptionOn)("basic")) for(let i = 0; i < 10; i++)makeParticle(gameState, ball.x, ball.y, Math.random() * gameState.baseSpeed * 3, gameState.baseSpeed * 3, "red", false, gameState.coinSize / 2, 150);
}
if (gameState.perks.streak_shots) resetCombo(gameState, ball.x, ball.y);
2025-03-19 21:58:08 +01:00
if (gameState.perks.trampoline) gameState.combo += gameState.perks.trampoline;
if (gameState.perks.nbricks && ball.brokenSinceBounce < gameState.perks.nbricks) 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") // respawns with full hp
setBrick(gameState, index, color);
// gameState.bricks[index] = color;
});
ball.hitItem = [];
2025-03-23 19:11:01 +01:00
if (!ball.hitSinceBounce && gameState.bricks.find((i)=>i)) {
gameState.runStatistics.misses++;
2025-03-19 21:58:08 +01:00
if (gameState.perks.forgiving) {
2025-03-20 21:24:25 +01:00
const loss = Math.floor(gameState.levelMisses / 10 * (gameState.combo - baseCombo(gameState)));
decreaseCombo(gameState, loss, ball.x, ball.y - gameState.ballSize);
2025-03-19 21:58:08 +01:00
} else resetCombo(gameState, ball.x, ball.y);
2025-03-20 21:24:25 +01:00
gameState.levelMisses++;
makeText(gameState, gameState.puckPosition, gameState.gameZoneHeight - gameState.puckHeight * 2, "red", (0, _i18N.t)("play.missed_ball"), gameState.puckHeight, 500);
}
gameState.runStatistics.puck_bounces++;
ball.hitSinceBounce = 0;
2025-03-22 16:04:25 +01:00
ball.brokenSinceBounce = 0;
ball.sapperUses = 0;
2025-03-23 19:11:01 +01:00
ball.piercePoints = gameState.perks.pierce * 3;
}
2025-03-20 22:50:50 +01:00
const lostOnSides = gameState.perks.unbounded && ball.x < -gameState.gameZoneWidth / 2 || ball.x > gameState.canvasWidth + gameState.gameZoneWidth / 2;
2025-03-19 20:14:55 +01:00
if (gameState.running && (ball.y > gameState.gameZoneHeight + gameState.ballSize / 2 || lostOnSides)) {
ball.destroyed = true;
gameState.runStatistics.balls_lost++;
if (!gameState.balls.find((b)=>!b.destroyed)) (0, _gameOver.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 = (0, _game.hitsSomething)(previousX, y, radius);
const hhit = (0, _game.hitsSomething)(x, previousY, radius);
const chit = typeof vhit == "undefined" && typeof hhit == "undefined" && (0, _game.hitsSomething)(x, y, radius) || undefined;
const hitBrick = vhit ?? hhit ?? chit;
if (typeof hitBrick !== "undefined") {
2025-03-19 20:14:55 +01:00
ball.hitSinceBounce++;
2025-03-22 16:04:25 +01:00
let pierce = false;
2025-03-23 19:11:01 +01:00
let damage = 1 + ((0, _gameUtils.shouldPierceByColor)(gameState, vhit, hhit, chit) ? gameState.perks.pierce_color : 0);
gameState.brickHP[hitBrick] -= damage;
const used = Math.min(ball.piercePoints, Math.max(1, gameState.brickHP[hitBrick]));
gameState.brickHP[hitBrick] -= used;
ball.piercePoints -= used;
if (gameState.brickHP[hitBrick] < 0) {
gameState.brickHP[hitBrick] = 0;
2025-03-22 16:04:25 +01:00
pierce = true;
}
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;
}
}
2025-03-23 19:11:01 +01:00
if (!gameState.brickHP[hitBrick]) {
2025-03-22 16:04:25 +01:00
const initialBrickColor = gameState.bricks[hitBrick];
ball.brokenSinceBounce++;
explodeBrick(gameState, hitBrick, ball, false);
if (ball.sapperUses < gameState.perks.sapper && initialBrickColor !== "black" && // don't replace a brick that bounced with sturdy_bricks
!gameState.bricks[hitBrick]) {
setBrick(gameState, hitBrick, "black");
2025-03-22 16:04:25 +01:00
ball.sapperUses++;
}
} else {
schedulGameSound(gameState, 'wallBeep', x, 1);
makeLight(gameState, (0, _gameUtils.brickCenterX)(gameState, hitBrick), (0, _gameUtils.brickCenterY)(gameState, hitBrick), "white", gameState.brickWidth + 2, 50 * gameState.brickHP[hitBrick]);
}
}
if (!(0, _options.isOptionOn)("basic")) {
2025-03-23 19:11:01 +01:00
const remainingPierce = ball.piercePoints;
const remainingSapper = ball.sapperUses < gameState.perks.sapper;
const extraCombo = gameState.combo - 1;
if (extraCombo && Math.random() > 0.1 / (1 + extraCombo) || remainingSapper && Math.random() > 0.1 / (1 + remainingSapper) || extraCombo && Math.random() > 0.1 / (1 + extraCombo)) {
const color = remainingSapper ? Math.random() > 0.5 ? "orange" : "red" : gameState.ballsColor;
makeParticle(gameState, ball.x, ball.y, gameState.perks.pierce_color || remainingPierce ? -ball.vx + (Math.random() - 0.5) * gameState.baseSpeed / 3 : (Math.random() - 0.5) * gameState.baseSpeed, gameState.perks.pierce_color || remainingPierce ? -ball.vy + (Math.random() - 0.5) * gameState.baseSpeed / 3 : (Math.random() - 0.5) * gameState.baseSpeed, color, true, gameState.coinSize / 2, 100);
}
}
}
function makeCoin(gameState, x, y, vx, vy, color = "gold", points = 1) {
2025-03-27 10:52:31 +01:00
if (gameState.debuffs.negative_coins > Math.random() * 100) {
points = 0;
color = "transparent";
}
append(gameState.coins, (p)=>{
p.x = x;
p.y = y;
p.size = gameState.coinSize;
p.previousX = x;
p.previousY = y;
p.vx = vx;
p.vy = vy;
2025-03-24 10:38:01 +01:00
// p.sx = 0;
// p.sy = 0;
p.color = color;
p.a = Math.random() * Math.PI * 2;
p.sa = Math.random() - 0.5;
p.weight = 0.8 + Math.random() * 0.2 + Math.min(2, points * 0.01);
p.points = points;
});
}
function makeParticle(gameState, x, y, vx, vy, color, ethereal = false, size = 8, duration = 150) {
append(gameState.particles, (p)=>{
p.time = gameState.levelTime;
p.x = x;
p.y = y;
p.vx = vx;
p.vy = vy;
p.color = color;
p.size = size;
p.duration = duration;
p.ethereal = ethereal;
});
}
function makeText(gameState, x, y, color, text, size = 20, duration = 150) {
append(gameState.texts, (p)=>{
p.time = gameState.levelTime;
p.x = x;
p.y = y;
p.color = color;
p.size = size;
p.duration = duration;
p.text = text;
});
}
function makeLight(gameState, x, y, color, size = 8, duration = 150) {
append(gameState.lights, (p)=>{
p.time = gameState.levelTime;
p.x = x;
p.y = y;
p.color = color;
p.size = size;
p.duration = duration;
});
}
function append(where, makeItem) {
while(where.list[where.indexMin] && !where.list[where.indexMin].destroyed && where.indexMin < where.list.length)where.indexMin++;
if (where.indexMin < where.list.length) {
where.list[where.indexMin].destroyed = false;
makeItem(where.list[where.indexMin]);
where.indexMin++;
} else {
const p = {
destroyed: false
};
makeItem(p);
where.list.push(p);
}
where.total++;
}
function destroy(where, index) {
if (where.list[index].destroyed) return;
where.list[index].destroyed = true;
where.indexMin = Math.min(where.indexMin, index);
where.total--;
}
function liveCount(where) {
return where.total;
}
function empty(where) {
where.total = 0;
where.indexMin = 0;
where.list.forEach((i)=>i.destroyed = true);
}
function forEachLiveOne(where, cb) {
where.list.forEach((item, index)=>{
if (item && !item.destroyed) cb(item, index);
});
}
},{"./game_utils":"cEeac","./i18n/i18n":"eNPRm","./loadGameData":"l1B4x","./settings":"5blfu","./render":"9AS2t","./gameOver":"caCAf","./game":"edeGs","./recording":"godmD","./options":"d5NoS","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3","./premium":"4GEPs","./newGameState":"aQN6X","./debuffs":"9Mo9a","./asyncAlert":"rSqLY"}],"9AS2t":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "gameCanvas", ()=>gameCanvas);
parcelHelpers.export(exports, "ctx", ()=>ctx);
parcelHelpers.export(exports, "bombSVG", ()=>bombSVG);
parcelHelpers.export(exports, "background", ()=>background);
parcelHelpers.export(exports, "backgroundCanvas", ()=>backgroundCanvas);
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, "scoreDisplay", ()=>scoreDisplay);
2025-03-25 08:22:58 +01:00
parcelHelpers.export(exports, "getDashOffset", ()=>getDashOffset);
var _gameStateMutators = require("./gameStateMutators");
var _gameUtils = require("./game_utils");
var _i18N = require("./i18n/i18n");
var _game = require("./game");
var _options = require("./options");
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>`);
const background = document.createElement("img");
const backgroundCanvas = document.createElement("canvas");
function render(gameState) {
const level = (0, _gameUtils.currentLevelInfo)(gameState);
const hasCombo = gameState.combo > (0, _gameStateMutators.baseCombo)(gameState);
const { width, height } = gameCanvas;
if (!width || !height) return;
if (gameState.currentLevel || gameState.levelTime) menuLabel.innerText = gameState.loop ? (0, _i18N.t)("play.current_lvl_loop", {
level: gameState.currentLevel + 1,
max: (0, _gameUtils.max_levels)(gameState),
loop: gameState.loop
2025-03-27 10:52:31 +01:00
}) : (0, _i18N.t)("play.current_lvl", {
level: gameState.currentLevel + 1,
max: (0, _gameUtils.max_levels)(gameState)
});
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;
(0, _gameStateMutators.forEachLiveOne)(gameState.coins, (coin)=>{
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 = (0, _gameUtils.brickCenterX)(gameState, index), y = (0, _gameUtils.brickCenterY)(gameState, index);
drawFuzzyBall(ctx, color == "black" ? "#666" : color, gameState.brickWidth, x, y);
});
ctx.globalAlpha = 1;
(0, _gameStateMutators.forEachLiveOne)(gameState.particles, (flash)=>{
const { x, y, time, color, size, duration } = flash;
const elapsed = gameState.levelTime - time;
ctx.globalAlpha = Math.min(1, 2 - elapsed / duration * 2);
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);
(0, _gameStateMutators.forEachLiveOne)(gameState.particles, (flash)=>{
const { x, y, time, color, size, duration } = flash;
const elapsed = gameState.levelTime - time;
ctx.globalAlpha = Math.min(1, 2 - elapsed / duration * 2);
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);
}
2025-03-20 21:02:51 +01:00
if (gameState.perks.bigger_explosions && !(0, _options.isOptionOn)("basic") && shaked) gameCanvas.style.filter = "brightness(" + (1 + 100 / (1 + lastExplosionDelay)) + ")";
else gameCanvas.style.filter = "";
// Coins
ctx.globalAlpha = 1;
(0, _gameStateMutators.forEachLiveOne)(gameState.coins, (coin)=>{
ctx.globalCompositeOperation = 'source-over';
// ctx.globalCompositeOperation =
// coin.color === "gold" || level.color ? "source-over" : "screen";
2025-03-27 10:52:31 +01:00
drawCoin(ctx, coin.color, coin.size, coin.x, coin.y, hasCombo && gameState.perks.asceticism && "red" || !coin.points && "red" || level.color || "black", coin.a);
});
// Black shadow around balls
if (!(0, _options.isOptionOn)("basic")) {
ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = Math.min(0.8, (0, _gameStateMutators.liveCount)(gameState.coins) / 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";
(0, _gameStateMutators.forEachLiveOne)(gameState.lights, (flash)=>{
const { x, y, time, color, size, duration } = flash;
const elapsed = gameState.levelTime - time;
ctx.globalAlpha = Math.min(1, 2 - elapsed / duration * 2) * 0.5;
drawBrick(ctx, color, x, y, -1);
});
ctx.globalCompositeOperation = "screen";
(0, _gameStateMutators.forEachLiveOne)(gameState.texts, (flash)=>{
const { x, y, time, color, size, duration } = flash;
const elapsed = gameState.levelTime - time;
ctx.globalAlpha = Math.max(0, Math.min(1, 2 - elapsed / duration * 2));
ctx.globalCompositeOperation = "source-over";
drawText(ctx, flash.text, color, size, x, y - elapsed / 10);
});
(0, _gameStateMutators.forEachLiveOne)(gameState.particles, (particle)=>{
const { x, y, time, color, size, duration } = particle;
const elapsed = gameState.levelTime - time;
ctx.globalAlpha = Math.max(0, Math.min(1, 2 - elapsed / duration * 2));
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;
2025-03-19 20:14:55 +01:00
for(let i = 0; i < gameState.perks.extra_life; i++)ctx.fillRect(gameState.perks.unbounded ? 0 : gameState.offsetXRoundedDown, gameState.gameZoneHeight - gameState.puckHeight / 2 + 2 * i, gameState.perks.unbounded ? gameState.canvasWidth : gameState.gameZoneWidthRoundedUp, 1);
}
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
gameState.balls.forEach((ball)=>{
2025-03-25 08:22:58 +01:00
const drawingColor = gameState.ballsColor;
// The white border around is to distinguish colored balls from coins/bg
drawBall(ctx, drawingColor, gameState.ballSize, ball.x, ball.y, gameState.puckColor);
2025-03-19 20:14:55 +01:00
if ((0, _gameUtils.isTelekinesisActive)(gameState, ball) || (0, _gameUtils.isYoyoActive)(gameState, ball)) {
ctx.strokeStyle = gameState.puckColor;
ctx.beginPath();
2025-03-19 20:14:55 +01:00
ctx.moveTo(gameState.puckPosition, gameState.gameZoneHeight);
ctx.bezierCurveTo(gameState.puckPosition, gameState.gameZoneHeight, gameState.puckPosition, ball.y, ball.x, ball.y);
ctx.stroke();
}
if (gameState.perks.clairvoyant && gameState.ballStickToPuck) {
ctx.strokeStyle = gameState.ballsColor;
ctx.beginPath();
ctx.moveTo(ball.x, ball.y);
ctx.lineTo(ball.x + ball.vx * 10, ball.y + ball.vy * 10);
ctx.stroke();
}
});
// The puck
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
drawPuck(ctx, gameState.puckColor, gameState.puckWidth, gameState.puckHeight, 0, !!gameState.perks.concave_puck, gameState.perks.streak_shots && hasCombo ? getDashOffset(gameState) : -1);
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, comboTextWidth > gameState.puckWidth ? gameState.combo.toString() : comboText, "#000", comboTextWidth > gameState.puckWidth ? 12 : 20, gameState.puckPosition, gameState.gameZoneHeight - gameState.puckHeight / 2, false);
}
// Borders
ctx.globalCompositeOperation = "source-over";
2025-03-19 20:14:55 +01:00
ctx.globalAlpha = gameState.perks.unbounded ? 0.1 : 1;
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;
2025-03-25 08:47:39 +01:00
drawStraightLine(ctx, gameState, hasCombo && gameState.perks.left_is_lava && !gameState.perks.unbounded && "red" || "white", gameState.offsetX - 1, 0, gameState.offsetX - 1, height, gameState.perks.unbounded ? 0.1 : 1);
drawStraightLine(ctx, gameState, hasCombo && gameState.perks.right_is_lava && !gameState.perks.unbounded && "red" || "white", width - gameState.offsetX + 1, 0, width - gameState.offsetX + 1, height, gameState.perks.unbounded ? 0.1 : 1);
} else {
ctx.fillStyle = "red";
2025-03-25 08:47:39 +01:00
drawStraightLine(ctx, gameState, hasCombo && gameState.perks.left_is_lava && !gameState.perks.unbounded && "red" || "", 0, 0, 0, height, 1);
drawStraightLine(ctx, gameState, hasCombo && gameState.perks.right_is_lava && !gameState.perks.unbounded && "red" || "", width - 1, 0, width - 1, height, 1);
}
2025-03-25 08:47:39 +01:00
drawStraightLine(ctx, gameState, hasCombo && gameState.perks.top_is_lava && "red" || "", gameState.offsetXRoundedDown, 1, width - gameState.offsetXRoundedDown, 1, 1);
drawStraightLine(ctx, gameState, hasCombo && gameState.perks.compound_interest && "red" || (0, _options.isOptionOn)("mobile-mode") && "white" || "", gameState.offsetXRoundedDown, gameState.gameZoneHeight, width - gameState.offsetXRoundedDown, gameState.gameZoneHeight, 1);
2025-03-25 08:22:58 +01:00
if ((0, _options.isOptionOn)("mobile-mode") && !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);
if (shaked) ctx.resetTransform();
}
2025-03-25 08:22:58 +01:00
function drawStraightLine(ctx, gameState, mode, x1, y1, x2, y2, alpha = 1) {
ctx.globalAlpha = alpha;
if (!mode) return;
2025-03-25 08:47:39 +01:00
if (mode == "red") {
ctx.strokeStyle = "red";
2025-03-25 08:22:58 +01:00
ctx.lineDashOffset = getDashOffset(gameState);
ctx.lineWidth = 2;
ctx.setLineDash(redBorderDash);
} else {
2025-03-25 08:47:39 +01:00
ctx.strokeStyle = "white";
2025-03-25 08:22:58 +01:00
ctx.lineWidth = 1;
}
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
2025-03-25 08:47:39 +01:00
if (mode == "red") {
2025-03-25 08:22:58 +01:00
ctx.setLineDash([]);
ctx.lineWidth = 1;
}
ctx.globalAlpha = 1;
}
let cachedBricksRender = document.createElement("canvas");
let cachedBricksRenderKey = "";
function renderAllBricks() {
ctx.globalAlpha = 1;
2025-03-25 08:47:24 +01:00
const hasCombo = (0, _game.gameState).combo > (0, _gameStateMutators.baseCombo)((0, _game.gameState));
const redBorderOnBricksWithWrongColor = hasCombo && (0, _game.gameState).perks.picky_eater && !(0, _options.isOptionOn)("basic");
const redColorOnAllBricks = !!((0, _game.gameState).lastPuckMove && (0, _game.gameState).perks.passive_income && hasCombo && (0, _game.gameState).lastPuckMove > (0, _game.gameState).levelTime - 250 * (0, _game.gameState).perks.passive_income);
2025-03-25 08:22:58 +01:00
let offset = getDashOffset((0, _game.gameState));
if (!(redBorderOnBricksWithWrongColor || redColorOnAllBricks || (0, _game.gameState).perks.reach || (0, _game.gameState).perks.zen)) offset = 0;
2025-03-23 19:11:01 +01:00
const clairVoyance = (0, _game.gameState).perks.clairvoyant && (0, _game.gameState).brickHP.reduce((a, b)=>a + b, 0);
2025-03-25 08:47:39 +01:00
const newKey = (0, _game.gameState).gameZoneWidth + "_" + (0, _game.gameState).bricks.join("_") + bombSVG.complete + "_" + redBorderOnBricksWithWrongColor + "_" + redColorOnAllBricks + "_" + (0, _game.gameState).ballsColor + "_" + (0, _game.gameState).perks.pierce_color + "_" + clairVoyance + "_" + offset;
if (newKey !== cachedBricksRenderKey) {
cachedBricksRenderKey = newKey;
cachedBricksRender.width = (0, _game.gameState).gameZoneWidth;
cachedBricksRender.height = (0, _game.gameState).gameZoneWidth + 1;
const canctx = cachedBricksRender.getContext("2d");
canctx.clearRect(0, 0, (0, _game.gameState).gameZoneWidth, (0, _game.gameState).gameZoneWidth);
canctx.resetTransform();
canctx.translate(-(0, _game.gameState).offsetX, 0);
// Bricks
(0, _game.gameState).bricks.forEach((color, index)=>{
const x = (0, _gameUtils.brickCenterX)((0, _game.gameState), index), y = (0, _gameUtils.brickCenterY)((0, _game.gameState), index);
if (!color) return;
2025-03-20 21:06:13 +01:00
let redBecauseOfReach = (0, _game.gameState).perks.reach && (0, _gameUtils.countBricksAbove)((0, _game.gameState), index) && !(0, _gameUtils.countBricksBelow)((0, _game.gameState), index);
2025-03-27 10:52:31 +01:00
let redBorder = color === "transparent" || (0, _game.gameState).ballsColor !== color && color !== "black" && redBorderOnBricksWithWrongColor || hasCombo && (0, _game.gameState).perks.zen && color === "black" || redBecauseOfReach || redColorOnAllBricks;
canctx.globalCompositeOperation = "source-over";
2025-03-25 08:22:58 +01:00
drawBrick(canctx, color, x, y, redBorder ? offset : -1);
2025-03-23 19:11:01 +01:00
if ((0, _game.gameState).brickHP[index] > 1 && (0, _game.gameState).perks.clairvoyant) {
canctx.globalCompositeOperation = "destination-out";
drawText(canctx, (0, _game.gameState).brickHP[index].toString(), "white", (0, _game.gameState).puckHeight, x, y);
}
if (color === "black") {
canctx.globalCompositeOperation = "source-over";
drawIMG(canctx, bombSVG, (0, _game.gameState).brickWidth, x, y);
}
});
}
ctx.drawImage(cachedBricksRender, (0, _game.gameState).offsetX, 0);
}
let cachedGraphics = {};
2025-03-25 08:22:58 +01:00
function drawPuck(ctx, color, puckWidth, puckHeight, yOffset = 0, flipped, redBorderOffset) {
2025-03-25 08:47:39 +01:00
const key = "puck" + color + "_" + puckWidth + "_" + puckHeight + "_" + flipped + "_" + redBorderOffset;
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);
if (flipped) {
canctx.lineTo(0, puckHeight * 0.75);
canctx.bezierCurveTo(puckWidth / 2, puckHeight, puckWidth / 2, puckHeight * 1, puckWidth, puckHeight * 0.75);
canctx.lineTo(puckWidth, puckHeight * 2);
} else {
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();
2025-03-25 08:22:58 +01:00
if (redBorderOffset !== -1) {
2025-03-25 08:47:39 +01:00
canctx.strokeStyle = "red";
2025-03-25 08:22:58 +01:00
canctx.lineWidth = 4;
canctx.setLineDash(redBorderDash);
canctx.lineDashOffset = redBorderOffset;
canctx.stroke();
}
cachedGraphics[key] = can;
}
ctx.drawImage(cachedGraphics[key], Math.round((0, _game.gameState).puckPosition - puckWidth / 2), (0, _game.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();
2025-03-25 08:47:39 +01:00
if (color === "gold" || borderColor === "red") {
canctx.strokeStyle = borderColor;
2025-03-25 08:47:39 +01:00
if (borderColor == "red") {
canctx.lineWidth = 2;
canctx.setLineDash(redBorderDash);
}
canctx.stroke();
}
if (color === "gold") {
// Fill in
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));
}
2025-03-25 08:22:58 +01:00
function drawBrick(ctx, color, x, y, offset = 0) {
const tlx = Math.ceil(x - (0, _game.gameState).brickWidth / 2);
const tly = Math.ceil(y - (0, _game.gameState).brickWidth / 2);
const brx = Math.ceil(x + (0, _game.gameState).brickWidth / 2) - 1;
const bry = Math.ceil(y + (0, _game.gameState).brickWidth / 2) - 1;
const width = brx - tlx, height = bry - tly;
2025-03-25 08:47:39 +01:00
const key = "brick" + color + "_" + "_" + width + "_" + height + "_" + offset;
if (!cachedGraphics[key]) {
const can = document.createElement("canvas");
can.width = width;
can.height = height;
2025-03-25 08:22:58 +01:00
const bord = 4;
const cornerRadius = 2;
const canctx = can.getContext("2d");
canctx.fillStyle = color;
2025-03-25 08:22:58 +01:00
canctx.setLineDash(offset !== -1 ? redBorderDash : []);
canctx.lineDashOffset = offset;
2025-03-25 08:47:39 +01:00
canctx.strokeStyle = offset !== -1 ? "red" : color;
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));
}
const scoreDisplay = document.getElementById("score");
const menuLabel = document.getElementById("menuLabel");
2025-03-25 08:22:58 +01:00
const redBorderDash = [
5,
5
];
function getDashOffset(gameState) {
if ((0, _options.isOptionOn)("basic")) return 0;
return Math.floor(gameState.levelTime % 500 / 500 * 10) % 10;
}
},{"./gameStateMutators":"9ZeQl","./game_utils":"cEeac","./i18n/i18n":"eNPRm","./game":"edeGs","./options":"d5NoS","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"caCAf":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "getUpgraderUnlockPoints", ()=>getUpgraderUnlockPoints);
parcelHelpers.export(exports, "addToTotalPlayTime", ()=>addToTotalPlayTime);
parcelHelpers.export(exports, "gameOver", ()=>gameOver);
parcelHelpers.export(exports, "getHistograms", ()=>getHistograms);
var _loadGameData = require("./loadGameData");
var _i18N = require("./i18n/i18n");
var _game = require("./game");
var _gameUtils = require("./game_utils");
var _settings = require("./settings");
var _recording = require("./recording");
var _asyncAlert = require("./asyncAlert");
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 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 (!(0, _game.gameState).running) return;
2025-03-22 16:04:25 +01:00
if ((0, _game.gameState).isGameOver) return;
(0, _game.gameState).isGameOver = true;
(0, _game.pause)(true);
(0, _recording.stopRecording)();
addToTotalPlayTime((0, _game.gameState).runStatistics.runTime);
(0, _game.gameState).runStatistics.max_level = (0, _game.gameState).currentLevel + 1;
let animationDelay = -300;
const getDelay = ()=>{
animationDelay += 800;
return "animation-delay:" + animationDelay + "ms;";
};
// unlocks
let unlocksInfo = "";
const endTs = (0, _settings.getTotalScore)();
const startTs = endTs - (0, _game.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 = (0, _gameUtils.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
(0, _game.gameState).combo = 1;
(0, _asyncAlert.asyncAlert)({
allowClose: true,
title,
2025-03-27 10:52:31 +01:00
content: [
`
${(0, _game.gameState).isCreativeModeRun ? `<p>${(0, _i18N.t)("gameOver.test_run")}</p> ` : ""}
<p>${intro}</p>
<p>${(0, _i18N.t)("gameOver.cumulative_total", {
2025-03-27 10:52:31 +01:00
startTs,
endTs
})}</p>
${unlocksInfo}
`,
{
value: null,
text: (0, _i18N.t)("gameOver.restart"),
help: ""
2025-03-27 10:52:31 +01:00
},
`<div id="level-recording-container"></div>
${(0, _gameUtils.pickedUpgradesHTMl)((0, _game.gameState))}
${getHistograms()}
`
2025-03-27 10:52:31 +01:00
]
}).then(()=>(0, _game.restart)({
levelToAvoid: (0, _gameUtils.currentLevelInfo)((0, _game.gameState)).name
}));
}
function getHistograms() {
let runStats = "";
2025-03-27 10:52:31 +01:00
// TODO separate adventure and normal runs
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({
...(0, _game.gameState).runStatistics,
perks: (0, _game.gameState).perks,
appVersion: (0, _loadGameData.appVersion)
});
// Generate some histogram
if (!(0, _game.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;
}
},{"./loadGameData":"l1B4x","./i18n/i18n":"eNPRm","./game":"edeGs","./game_utils":"cEeac","./settings":"5blfu","./recording":"godmD","./asyncAlert":"rSqLY","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"godmD":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
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);
var _render = require("./render");
var _gameUtils = require("./game_utils");
var _sounds = require("./sounds");
var _i18N = require("./i18n/i18n");
var _options = require("./options");
let mediaRecorder, captureStream, captureTrack, recordCanvas, recordCanvasCtx;
function recordOneFrame(gameState) {
if (!(0, _options.isOptionOn)("record")) return;
// if (!gameState.running) return;
if (!captureStream) return;
drawMainCanvasOnSmallCanvas(gameState);
if (captureTrack?.requestFrame) captureTrack?.requestFrame();
else if (captureStream?.requestFrame) captureStream.requestFrame();
}
function drawMainCanvasOnSmallCanvas(gameState) {
if (!recordCanvasCtx) return;
recordCanvasCtx.drawImage((0, _render.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) + "/" + (0, _gameUtils.max_levels)(gameState), 12, 12);
}
function startRecordingGame(gameState) {
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);
targetDiv.appendChild(video);
const a = document.createElement("a");
a.download = captureFileName("webm");
a.target = "_blank";
if (window.location.href.endsWith("index.html?isInWebView=true")) a.href = await blobToBase64(blob);
else a.href = video.src;
a.textContent = (0, _i18N.t)("main_menu.record_download", {
size: (blob.size / 1000000).toFixed(2)
});
targetDiv.appendChild(a);
};
}
function blobToBase64(blob) {
return new Promise((resolve, reject)=>{
let reader = new FileReader();
reader.onload = function() {
resolve(reader.result);
};
reader.onerror = function(e) {
console.error(e);
reject(new Error("Failed to readAsDataURL of the video "));
};
reader.readAsDataURL(blob);
});
}
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;
}
},{"./render":"9AS2t","./game_utils":"cEeac","./sounds":"dQKPV","./i18n/i18n":"eNPRm","./options":"d5NoS","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"rSqLY":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "alertsOpen", ()=>alertsOpen);
parcelHelpers.export(exports, "closeModal", ()=>closeModal);
2025-03-26 14:04:54 +01:00
parcelHelpers.export(exports, "requiredAsyncAlert", ()=>requiredAsyncAlert);
parcelHelpers.export(exports, "asyncAlert", ()=>asyncAlert);
var _i18N = require("./i18n/i18n");
let alertsOpen = 0, closeModal = null;
2025-03-20 18:44:46 +01:00
const popupWrap = document.getElementById("popup");
const closeModaleButton = document.getElementById("close-modale");
closeModaleButton.addEventListener("click", (e)=>{
e.preventDefault();
if (closeModal) closeModal();
});
closeModaleButton.title = (0, _i18N.t)("play.close_modale_window_tooltip");
let lastClickedItemIndex = -1;
2025-03-26 14:04:54 +01:00
function requiredAsyncAlert(p) {
return asyncAlert({
...p,
allowClose: false
});
}
2025-03-27 10:52:31 +01:00
async function asyncAlert({ title, content = [], allowClose = true, actionsAsGrid = false }) {
2025-03-20 18:44:46 +01:00
updateAlertsOpen(1);
return new Promise((resolve)=>{
popupWrap.className = actionsAsGrid ? " actionsAsGrid" : "";
2025-03-20 18:44:46 +01:00
closeModaleButton.style.display = allowClose ? "" : "none";
const popup = document.createElement("div");
2025-03-20 21:02:51 +01:00
let closed = false;
function closeWithResult(value) {
2025-03-20 21:02:51 +01:00
if (closed) return;
closed = true;
2025-03-20 22:50:50 +01:00
Array.prototype.forEach.call(popup.querySelectorAll("button:not([disabled])"), (b)=>b.disabled = true);
2025-03-20 18:44:46 +01:00
document.body.style.minHeight = document.body.scrollHeight + "px";
2025-03-20 22:50:50 +01:00
setTimeout(()=>document.body.style.minHeight = "", 0);
2025-03-20 18:44:46 +01:00
popup.remove();
resolve(value);
}
2025-03-20 18:44:46 +01:00
if (allowClose) closeModal = ()=>{
closeWithResult(undefined);
};
else closeModal = null;
if (title) {
2025-03-27 10:52:31 +01:00
const h2 = document.createElement("h2");
h2.innerHTML = title;
popup.appendChild(h2);
}
2025-03-27 10:52:31 +01:00
content?.filter((i)=>i).forEach((entry, index)=>{
if (typeof entry == "string") {
const p = document.createElement("div");
p.innerHTML = entry;
popup.appendChild(p);
return;
}
let addto;
if (popup.lastChild?.nodeName == "SECTION") addto = popup.lastChild;
else {
addto = document.createElement("section");
addto.className = "actions";
popup.appendChild(addto);
}
const { text, value, help, disabled, className = "", icon = "" } = entry;
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();
2025-03-20 18:44:46 +01:00
e.stopPropagation();
closeWithResult(value);
2025-03-20 18:44:46 +01:00
// Focus "same" button if it's still there
lastClickedItemIndex = index;
});
2025-03-20 18:44:46 +01:00
button.className = className + (lastClickedItemIndex === index ? " needs-focus" : "");
2025-03-27 10:52:31 +01:00
addto.appendChild(button);
});
popupWrap.appendChild(popup);
2025-03-20 18:44:46 +01:00
popupWrap.querySelector(`section.actions > button.needs-focus`)?.focus();
lastClickedItemIndex = -1;
}).then((v)=>{
2025-03-20 18:44:46 +01:00
updateAlertsOpen(-1);
closeModal = null;
return v;
}, ()=>{
closeModal = null;
2025-03-20 18:44:46 +01:00
updateAlertsOpen(-1);
});
}
2025-03-20 18:44:46 +01:00
function updateAlertsOpen(delta) {
alertsOpen += delta;
2025-03-20 21:02:51 +01:00
if (alertsOpen > 1) alert("Two alerts where opened at once");
2025-03-20 18:44:46 +01:00
document.body.classList[alertsOpen ? "add" : "remove"]("has-alert-open");
}
},{"./i18n/i18n":"eNPRm","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"4GEPs":[function(require,module,exports,__globalThis) {
2025-03-26 08:27:56 +01:00
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "isPremium", ()=>isPremium);
parcelHelpers.export(exports, "premiumMenuEntry", ()=>premiumMenuEntry);
2025-03-26 14:04:54 +01:00
var _loadGameData = require("./loadGameData");
var _i18N = require("./i18n/i18n");
var _settings = require("./settings");
2025-03-26 14:04:54 +01:00
var _asyncAlert = require("./asyncAlert");
var _game = require("./game");
const publicKeyString = `-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyGQJgs6gxa0Fd86TuZ2q
rGQ5ArSn8ug4VIKezru1QhIEkXeOT1lYXOLEryWaVUwXfOa9sVlKAGJY5y0TarAY
NF2m67ME8yzNPIoZWbKXutJ3CSCXNTjAqAxHgz7H+qxbNGZXAXw+ta8+PuZDzcCI
LbXT1u3/i0ahhA2Erdpv9XQBazKZt5AKzU31XhEEFh1jXZyk9D4XbatYXtvEwaJx
eSWmjSxJ6SJb6oH2mwm8V4E0PxYVIa0yX3cPgGuR0pZPMleOTc6o0T24I2AUQb0d
FckdFrr5U8bFIf/nwncMYVVNgt1vh88EuzWLjpc52nLrdOkVQNpiCN2uMgBBXQB7
iseIfdkGF0A4DBn8qdieDvaSY8zeRW/nAce4FNBidU1SebNRnIU9f/XpA493lJW+
Y/zXQBbmX/uSmeZDP4fjhKZv0Qa0ZeGzZiTdBKKb0BlIg/VYFFsqPytUVVyesO4J
RCASTIjXW61E7PQKir5qIXwkQDlzJ+bpZ3PHyAvspRrBaDxIYvEEw14evpuqOgS+
v/IlgPe+CWSvZa9xxnQl/aWZrOrD7syu6KKCbgUyXEm+Alp0YT3e6nwjn0qiM/cj
dZpWPx3O+rZbRQb0gHcvN4+n2Y7fWAeC9mxVZtADqvVr/GTumMbLj7DdhWtt1Ogu
4EcvkQ5SKCL0JC93DyctjOMCAwEAAQ==
-----END PUBLIC KEY-----`;
function pemToArrayBuffer(pem) {
const b64 = pem.replace(/-----BEGIN PUBLIC KEY-----/, "").replace(/-----END PUBLIC KEY-----/, "").replace(/\s+/g, "");
const binaryDerString = atob(b64);
const binaryDer = new Uint8Array(binaryDerString.length);
for(let i = 0; i < binaryDerString.length; i++)binaryDer[i] = binaryDerString.charCodeAt(i);
return binaryDer.buffer;
}
async function getPriceId(key, pem) {
// Split the key into its components
const [priceId, timestamp, signature] = key.split(":");
const data = `${priceId}:${timestamp}`;
const publicKeyBuffer = pemToArrayBuffer(pem);
const publicKey = await crypto.subtle.importKey("spki", publicKeyBuffer, {
name: "RSA-PSS",
hash: "SHA-256"
}, true, [
"verify"
]);
// Verify the signature using ECDSA
const isValid = await crypto.subtle.verify({
name: "RSA-PSS",
saltLength: 32
}, publicKey, new Uint8Array(Array.from(atob(signature), (c)=>c.charCodeAt(0))), new TextEncoder().encode(data));
if (!isValid) throw new Error("Invalid key signature");
return priceId;
}
let premium = false;
const gamePriceId = "price_1R6YaEGRf74lr2EkSo2GPvuO";
checkKey((0, _settings.getSettingValue)("license", "")).then();
async function checkKey(key) {
if (!key) return "No key";
try {
if (gamePriceId !== await getPriceId(key, publicKeyString)) return "Wrong product";
premium = true;
return "";
} catch (e) {
return "Could not upgrade : " + e.message;
2025-03-27 10:52:31 +01:00
}
}
function isPremium() {
return premium;
}
function premiumMenuEntry(gameState) {
if (isPremium()) return {
icon: (0, _loadGameData.icons)["icon:premium_active"],
text: (0, _i18N.t)("premium.thanks"),
help: (0, _i18N.t)("premium.thanks_help"),
value: async ()=>{
navigator.clipboard.writeText((0, _settings.getSettingValue)('license', ''));
(0, _game.openMainMenu)();
}
};
let text = (0, _i18N.t)("premium.title");
let help = (0, _i18N.t)("premium.buy");
try {
const timePlayed = localStorage.getItem('breakout_71_total_play_time');
if (timePlayed && !isGooglePlayInstall) {
const hours = parseFloat(timePlayed) / 1000 / 60 / 60;
const pricePerHours = 4.99 / hours;
const args = {
hours: Math.floor(hours),
pricePerHours: pricePerHours.toFixed(2)
};
if (pricePerHours > 0 && pricePerHours < 0.5) {
text = (0, _i18N.t)("premium.per_hours", args);
help = (0, _i18N.t)("premium.per_hours_help", args);
}
console.log({
args
});
}
} catch (e) {
console.warn(e);
}
return {
icon: (0, _loadGameData.icons)["icon:premium"],
text,
help,
value: ()=>openPremiumMenu("")
};
}
const isGooglePlayInstall = new URLSearchParams(location.search).get("source") === "com.android.vending";
async function openPremiumMenu(text) {
const cb = await (0, _asyncAlert.asyncAlert)({
title: (0, _i18N.t)("premium.title"),
content: [
text || isGooglePlayInstall && (0, _i18N.t)("premium.help_google") || (0, _i18N.t)("premium.help"),
{
text: (0, _i18N.t)("premium.buy"),
disabled: isGooglePlayInstall,
help: isGooglePlayInstall ? (0, _i18N.t)("premium.buy_disabled_help") : (0, _i18N.t)("premium.buy_help"),
value () {
window.open("https://licenses.lecaro.me/buy/price_1R6YaEGRf74lr2EkSo2GPvuO", "_blank");
}
},
{
text: (0, _i18N.t)("premium.enter"),
help: (0, _i18N.t)("premium.enter_help"),
async value () {
const value = (prompt("Please paste your license key") || "").replace(/\s+/g, "");
const problem = await checkKey(value);
if (problem) openPremiumMenu(problem).then();
else {
(0, _settings.setSettingValue)("license", value);
(0, _game.openMainMenu)().then();
}
}
},
{
text: (0, _i18N.t)("premium.back"),
help: (0, _i18N.t)("premium.back_help"),
value () {
(0, _game.openMainMenu)().then();
}
}
]
});
if (cb) cb();
}
2025-03-27 10:52:31 +01:00
},{"./loadGameData":"l1B4x","./i18n/i18n":"eNPRm","./settings":"5blfu","./asyncAlert":"rSqLY","./game":"edeGs","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"aQN6X":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "getRunLevels", ()=>getRunLevels);
parcelHelpers.export(exports, "newGameState", ()=>newGameState);
2025-03-27 10:52:31 +01:00
parcelHelpers.export(exports, "emptyDebuffsMap", ()=>emptyDebuffsMap);
var _settings = require("./settings");
var _loadGameData = require("./loadGameData");
var _gameUtils = require("./game_utils");
var _gameStateMutators = require("./gameStateMutators");
var _options = require("./options");
2025-03-27 10:52:31 +01:00
var _debuffs = require("./debuffs");
function getRunLevels(totalScoreAtRunStart, params) {
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);
return firstLevel.concat(restInRandomOrder.slice(0, 10).sort((a, b)=>a.sortKey - b.sortKey));
}
function newGameState(params) {
const totalScoreAtRunStart = (0, _settings.getTotalScore)();
const runLevels = getRunLevels(totalScoreAtRunStart, params);
const perks = {
...(0, _gameUtils.makeEmptyPerksMap)((0, _loadGameData.upgrades)),
...params?.perks || {}
};
const gameState = {
runLevels,
2025-03-27 10:52:31 +01:00
level: runLevels[0],
currentLevel: 0,
2025-03-20 23:11:42 +01:00
upgradesOfferedFor: -1,
perks,
bannedPerks: (0, _gameUtils.makeEmptyPerksMap)((0, _loadGameData.upgrades)),
2025-03-27 10:52:31 +01:00
debuffs: {
...emptyDebuffsMap(),
...params?.debuffs || {}
},
puckWidth: 200,
baseSpeed: 12,
combo: 1,
gridSize: 12,
running: false,
2025-03-22 16:04:25 +01:00
isGameOver: false,
ballStickToPuck: true,
puckPosition: 400,
2025-03-25 08:22:58 +01:00
lastPuckPosition: 400,
2025-03-26 08:01:12 +01:00
lastPuckMove: 0,
pauseTimeout: null,
canvasWidth: 0,
canvasHeight: 0,
offsetX: 0,
offsetXRoundedDown: 0,
gameZoneWidth: 0,
gameZoneWidthRoundedUp: 0,
gameZoneHeight: 0,
brickWidth: 0,
score: 0,
lastScoreIncrease: -1000,
lastExplosion: -1000,
highScore: parseFloat(localStorage.getItem("breakout-3-hs") || "0"),
balls: [],
ballsColor: "white",
bricks: [],
brickHP: [],
lights: {
indexMin: 0,
total: 0,
list: []
},
particles: {
indexMin: 0,
total: 0,
list: []
},
texts: {
indexMin: 0,
total: 0,
list: []
},
coins: {
indexMin: 0,
total: 0,
list: []
},
levelStartScore: 0,
levelMisses: 0,
levelSpawnedCoins: 0,
puckColor: "#FFF",
ballSize: 20,
coinSize: 14,
puckHeight: 20,
totalScoreAtRunStart,
2025-03-26 14:04:54 +01:00
isCreativeModeRun: (0, _gameUtils.sumOfValues)(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,
wall_bounces: 0,
upgrades_picked: 1,
max_combo: 1,
max_level: 0
},
lastOffered: {},
levelTime: 0,
2025-03-22 16:47:02 +01:00
winAt: 0,
levelWallBounces: 0,
needsRender: true,
autoCleanUses: 0,
2025-03-26 08:01:12 +01:00
...(0, _gameUtils.defaultSounds)(),
rerolls: 0,
loop: 0
};
(0, _gameStateMutators.resetBalls)(gameState);
if (!(0, _gameUtils.sumOfValues)(gameState.perks)) {
const giftable = (0, _gameUtils.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;
(0, _gameStateMutators.dontOfferTooSoon)(gameState, randomGift);
}
for (let perk of (0, _loadGameData.upgrades))if (gameState.perks[perk.id]) (0, _gameStateMutators.dontOfferTooSoon)(gameState, perk.id);
return gameState;
}
2025-03-27 10:52:31 +01:00
function emptyDebuffsMap() {
const map = {};
(0, _debuffs.debuffs).forEach((d)=>map[d.id] = 0);
return map;
}
},{"./settings":"5blfu","./loadGameData":"l1B4x","./game_utils":"cEeac","./gameStateMutators":"9ZeQl","./options":"d5NoS","./debuffs":"9Mo9a","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}]},["gVqJ6","67XFf"], "67XFf", "parcelRequire94c2")
</script>
</body>
</html>