2025-03-19 18:13:41 +01:00
<!doctype html>
< html lang = "en" >
2025-04-06 11:27:26 +02:00
< head >
2025-03-19 18:13:41 +01:00
< 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" >
2025-04-08 15:17:14 +02:00
< meta name = "theme-color" content = "#000000" id = "themeColor" >
2025-03-19 18:13:41 +01:00
< 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;
2025-03-19 18:13:41 +01:00
top: 0;
left: 0;
}
2025-04-03 15:15:00 +02:00
canvas:not(#game) {
display: none;
}
2025-03-19 18:13:41 +01:00
#score, #menu {
z-index: 1;
appearance: none;
font: inherit;
color: #fff;
min-width: 40px;
2025-03-30 21:07:58 +02:00
max-width: calc(100vw - 80px);
2025-03-19 18:13:41 +01:00
min-height: 40px;
2025-03-31 13:33:27 +02:00
text-shadow: 0 0 4px var(--level-background);
background: none;
border: none;
2025-03-19 18:13:41 +01:00
padding: 10px;
line-height: 20px;
position: absolute;
top: 0;
2025-03-30 21:07:58 +02:00
overflow: hidden;
2025-03-19 18:13:41 +01:00
}
#score:hover, #menu:hover, #score:focus, #menu:focus {
cursor: pointer;
background: #0000004d;
}
#score {
color: #fff;
transition: color .3s;
right: 0;
}
2025-04-01 18:26:40 +02:00
#score.active .score {
2025-03-19 18:13:41 +01:00
color: gold;
transition: color 10ms;
}
2025-04-15 16:47:04 +02:00
#score.computer_controlled {
pointer-events: none;
2025-04-12 20:58:24 +02:00
}
2025-03-29 20:45:54 +01:00
#score span {
2025-03-31 20:08:17 +02:00
color: #fffc;
2025-03-29 20:45:54 +01:00
}
#score span.great {
color: #90ee90;
}
2025-03-29 21:05:53 +01:00
#score span.good, #score span.bad {
2025-03-29 20:45:54 +01:00
color: #fff;
}
2025-03-19 18:13:41 +01:00
#menu {
left: 0;
}
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 {
2025-03-19 18:13:41 +01:00
z-index: 10;
2025-03-20 18:44:46 +01:00
content: "";
2025-03-19 18:13:41 +01:00
background: #000000e6;
2025-03-20 18:44:46 +01:00
display: block;
2025-03-19 18:13:41 +01:00
position: fixed;
inset: 0;
}
2025-03-20 18:44:46 +01:00
#popup > div {
z-index: 11;
2025-03-19 18:13:41 +01:00
transform-origin: center;
flex-direction: column;
align-items: stretch;
width: 100%;
2025-03-31 20:08:17 +02:00
max-width: 500px;
2025-03-19 18:13:41 +01:00
margin: auto;
padding: 20px 10px;
display: flex;
2025-03-20 18:44:46 +01:00
position: relative;
2025-03-19 18:13:41 +01:00
}
2025-03-20 18:44:46 +01:00
#popup > div > * {
2025-03-27 10:52:31 +01:00
margin: 0 0 20px;
2025-03-19 18:13:41 +01:00
padding: 0;
}
2025-03-20 18:44:46 +01:00
#popup > div > section {
2025-03-19 18:13:41 +01:00
flex-direction: column;
align-items: stretch;
display: flex;
}
2025-03-20 18:44:46 +01:00
#popup > div > section button {
2025-03-19 18:13:41 +01:00
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 {
2025-03-19 18:13:41 +01:00
z-index: 1;
border-color: #f1d33b;
position: relative;
}
2025-03-20 18:44:46 +01:00
#popup > div > section button[disabled] {
2025-03-19 18:13:41 +01:00
opacity: .5;
filter: saturate(0);
2025-04-06 15:38:30 +02:00
cursor: not-allowed;
2025-03-19 18:13:41 +01:00
}
2025-03-20 18:44:46 +01:00
#popup > div > section button > div {
2025-03-19 18:13:41 +01:00
flex-grow: 1;
}
2025-03-20 18:44:46 +01:00
#popup > div > section button > div > em {
2025-03-19 18:13:41 +01:00
opacity: .8;
display: block;
}
2025-03-20 18:44:46 +01:00
#popup > div > section button.grey-out-unless-hovered:not(:hover) {
2025-03-19 18:13:41 +01:00
opacity: .6;
}
2025-03-20 18:44:46 +01:00
#popup > div > section button.grey-out-unless-hovered:not(:hover) img {
2025-03-19 18:13:41 +01:00
filter: saturate(0);
}
2025-04-01 13:35:33 +02:00
#popup > div > section button.grey-out-unless-hovered[disabled] {
opacity: .2;
}
2025-03-20 18:44:46 +01:00
#popup.actionsAsGrid > div {
2025-03-23 15:48:21 +01:00
max-width: none;
2025-03-19 18:13:41 +01:00
}
2025-04-01 13:35:33 +02:00
#popup.actionsAsGrid > div > div, #popup.actionsAsGrid > div > p, #popup.actionsAsGrid > div > h1, #popup.actionsAsGrid > div > h2 {
width: 100%;
max-width: 550px;
margin-left: auto;
margin-right: auto;
}
2025-03-20 18:44:46 +01:00
#popup.actionsAsGrid > div section {
2025-03-19 18:13:41 +01:00
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
display: grid;
}
2025-03-20 18:44:46 +01:00
#popup button#close-modale {
2025-03-19 18:13:41 +01:00
color: #fff;
cursor: pointer;
2025-03-20 22:50:50 +01:00
z-index: 12;
2025-03-19 18:13:41 +01:00
background: none;
border: none;
width: 60px;
height: 60px;
2025-03-31 20:13:47 +02:00
position: fixed;
2025-03-19 18:13:41 +01:00
top: 0;
right: 0;
overflow: hidden;
}
2025-03-20 18:44:46 +01:00
#popup button#close-modale:before {
2025-03-19 18:13:41 +01:00
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 {
2025-03-19 18:13:41 +01:00
background: #000;
font-weight: bold;
}
2025-03-20 18:44:46 +01:00
#popup .textAfterButtons {
2025-03-19 18:13:41 +01:00
color: #ffffff94;
}
2025-03-20 18:44:46 +01:00
#popup a[href] {
2025-03-19 18:13:41 +01:00
color: inherit;
}
2025-03-20 18:44:46 +01:00
#popup a[href]:hover, #popup a[href]:focus {
2025-03-19 18:13:41 +01:00
color: #fff;
}
2025-04-04 12:07:24 +02:00
@media (width >= 1400px) {
#popup.settings:before {
opacity: 0;
}
#popup.settings > div {
max-width: 400px;
margin-right: 0;
}
}
2025-03-19 18:13:41 +01:00
.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;
}
2025-03-30 21:07:58 +02:00
.upgrade {
gap: 2px;
margin: 0 0 10px;
display: flex;
}
.upgrade img {
width: 32px;
height: 32px;
}
.upgrade p {
color: #fff9;
flex-grow: 1;
margin: 0 20px;
}
.upgrade.used p strong {
color: #fff;
}
.upgrade > span {
flex-grow: 0;
flex-shrink: 0;
align-self: center;
width: 5px;
height: 32px;
display: inline-block;
}
.upgrade > span.used {
background: #fff;
}
.upgrade > span.free {
2025-04-01 18:26:40 +02:00
opacity: .25;
2025-03-30 21:07:58 +02:00
background: #fff;
}
.upgrade > span.banned {
background: red;
}
.upgrade.used {
opacity: 1;
}
.upgrade.free, .upgrade.banned {
opacity: .8;
}
2025-04-01 13:35:33 +02:00
#tooltip {
color: #fff;
z-index: 11;
pointer-events: none;
user-select: none;
opacity: 1;
background: #000;
border: 1px solid #fff;
border-radius: 2px;
max-width: 300px;
padding: 10px;
display: block;
position: fixed;
top: 0;
left: 0;
}
2025-04-07 08:24:17 +02:00
#popup.history > div {
max-width: none;
}
#popup.history > div table th:hover {
cursor: pointer;
background: #000;
}
#popup.history > div table td, #popup.history > div table th {
text-align: right;
padding: 0 5px;
line-height: 20px;
}
#popup.history > div table th:first-child, #popup.history > div table td:first-child {
text-align: left;
}
#popup.history > div table img {
pointer-events: none;
width: 20px;
height: auto;
}
#popup.history > div table tr:nth-child(2n) {
background: #00000094;
}
2025-04-07 14:08:48 +02:00
.progress-inline {
background: gray;
border-radius: 2px;
height: 7px;
display: block;
position: absolute;
bottom: 2px;
left: 62px;
right: 2px;
}
.progress-inline span {
transform-origin: 0 0;
background: #fff;
position: absolute;
inset: 1px;
}
2025-04-08 10:36:30 +02:00
.toast {
opacity: .8;
pointer-events: none;
background: #000;
border: 1px solid #fff;
border-radius: 2px;
align-items: center;
gap: 10px;
padding-right: 10px;
2025-04-15 16:47:04 +02:00
transition: opacity .2s, transform .2s;
2025-04-08 10:36:30 +02:00
display: flex;
position: fixed;
top: 40px;
left: 0;
}
2025-04-15 16:47:04 +02:00
.toast.hidden {
opacity: 0;
transform: translate(-20px, -20px)scale(.5);
}
2025-04-08 10:36:30 +02:00
2025-04-15 16:47:04 +02:00
.toast.visible {
opacity: .8;
transform: none;
2025-04-08 10:36:30 +02:00
}
2025-04-14 13:39:30 +02:00
.gridEdit > div > span, .palette > span {
cursor: pointer;
border: 1px solid;
justify-content: center;
align-items: center;
width: 40px;
height: 40px;
display: inline-flex;
}
.gridEdit > div > span:hover, .palette > span:hover {
z-index: 1;
border-color: gold;
position: relative;
box-shadow: inset 2px 2px 4px #0003;
}
.gridEdit > div {
display: flex;
}
.gridEdit > div > span {
width: calc(min(500px, 100vw, 100vh - 200px) / var(--grid-size));
height: calc(min(500px, 100vw, 100vh - 200px) / var(--grid-size));
}
.palette {
flex-wrap: wrap;
display: flex;
}
.palette > span[data-selected="true"] {
border: 2px solid #fff;
}
2025-04-18 21:17:32 +02:00
#stats {
color: #fff;
z-index: 3;
pointer-events: none;
opacity: 1;
width: 100vw;
max-width: 400px;
position: fixed;
top: 40px;
left: 0;
}
#stats > div {
background: #26262680;
position: relative;
}
#stats > div > div {
transform-origin: 0 0;
background: #6262ea;
position: absolute;
inset: 0;
}
#stats > div > strong {
padding: 0 5px;
position: relative;
}
.highlight {
position: relative;
}
.highlight:before {
content: "";
mix-blend-mode: screen;
opacity: .3;
background: linear-gradient(-45deg, #6262ea, #0000);
position: absolute;
inset: 0;
}
2025-03-19 18:13:41 +01:00
< / 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 >
2025-04-18 21:17:32 +02:00
< div id = "stats" style = "display: none" > < / div >
2025-04-01 13:35:33 +02:00
2025-03-19 18:13:41 +01:00
< canvas id = "game" > < / canvas >
2025-03-20 18:44:46 +01:00
< div id = "popup" >
< button id = "close-modale" > < / button >
< / div >
2025-04-01 13:39:09 +02:00
< div id = "tooltip" style = "display: none" > < / div >
2025-03-19 18:13:41 +01:00
< script > / / m o d u l e s a r e d e f i n e d a s a n a r r a y
// [ 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;
}
}
2025-04-18 21:17:32 +02:00
})({"x07Me":[function(require,module,exports,__globalThis) {
2025-03-19 18:13:41 +01:00
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-29 20:45:54 +01:00
parcelHelpers.export(exports, "openUpgradesPicker", ()=>openUpgradesPicker);
2025-03-19 18:13:41 +01:00
parcelHelpers.export(exports, "brickIndex", ()=>brickIndex);
parcelHelpers.export(exports, "hasBrick", ()=>hasBrick);
parcelHelpers.export(exports, "hitsSomething", ()=>hitsSomething);
parcelHelpers.export(exports, "tick", ()=>tick);
2025-03-29 21:05:53 +01:00
parcelHelpers.export(exports, "lastMeasuredFPS", ()=>lastMeasuredFPS);
2025-04-15 21:25:27 +02:00
parcelHelpers.export(exports, "startWork", ()=>startWork);
2025-04-01 13:35:33 +02:00
parcelHelpers.export(exports, "creativeModeThreshold", ()=>creativeModeThreshold);
2025-03-26 08:01:12 +01:00
parcelHelpers.export(exports, "openMainMenu", ()=>openMainMenu);
2025-03-19 18:13:41 +01:00
parcelHelpers.export(exports, "confirmRestart", ()=>confirmRestart);
parcelHelpers.export(exports, "setKeyPressed", ()=>setKeyPressed);
parcelHelpers.export(exports, "gameState", ()=>gameState);
parcelHelpers.export(exports, "restart", ()=>restart);
2025-04-12 20:01:43 +02:00
parcelHelpers.export(exports, "startComputerControlledGame", ()=>startComputerControlledGame);
2025-03-19 18:13:41 +01:00
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");
2025-03-30 21:07:58 +02:00
var _pureFunctions = require("./pure_functions");
2025-03-31 20:08:17 +02:00
var _help = require("./help");
2025-04-01 13:35:33 +02:00
var _creative = require("./creative");
var _tooltip = require("./tooltip");
2025-04-02 10:41:35 +02:00
var _startingPerks = require("./startingPerks");
2025-04-02 17:03:53 +02:00
var _migrations = require("./migrations");
2025-04-06 10:13:10 +02:00
var _gameOver = require("./gameOver");
2025-04-06 18:21:53 +02:00
var _generateSaveFileContent = require("./generateSaveFileContent");
2025-04-07 08:24:17 +02:00
var _runHistoryViewer = require("./runHistoryViewer");
2025-04-07 14:08:48 +02:00
var _openScorePanel = require("./openScorePanel");
2025-04-08 10:36:30 +02:00
var _monitorLevelsUnlocks = require("./monitorLevelsUnlocks");
2025-04-14 13:39:30 +02:00
var _levelEditor = require("./levelEditor");
2025-04-02 10:41:35 +02:00
async function play() {
if (await applyFullScreenChoice()) return;
2025-03-19 18:13:41 +01:00
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')
2025-03-19 18:13:41 +01:00
}
function pause(playerAskedForPause) {
if (!gameState.running) return;
if (gameState.pauseTimeout) return;
2025-04-20 10:58:26 +02:00
if (gameState.startParams.computer_controlled) {
if (gameState.startParams?.computer_controlled) play();
return;
}
2025-03-23 16:11:12 +01:00
const stop = ()=>{
2025-03-19 18:13:41 +01:00
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 ";
2025-03-19 18:13:41 +01:00
(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();
2025-03-19 18:13:41 +01:00
if (document.exitPointerLock) document.exitPointerLock();
}
2025-04-11 20:34:11 +02:00
const fitSize = (gameState)=>{
if (!gameState) throw new Error("Missign game state");
2025-03-19 18:13:41 +01:00
const past_off = gameState.offsetXRoundedDown, past_width = gameState.gameZoneWidthRoundedUp, past_heigh = gameState.gameZoneHeight;
2025-04-11 20:34:11 +02:00
const width = window.innerWidth, height = window.innerHeight;
2025-03-19 18:13:41 +01:00
gameState.canvasWidth = width;
gameState.canvasHeight = height;
(0, _render.gameCanvas).width = width;
(0, _render.gameCanvas).height = height;
(0, _render.backgroundCanvas).width = width;
(0, _render.backgroundCanvas).height = height;
2025-04-18 21:17:32 +02:00
const haloScale = (0, _render.getHaloScale)();
(0, _render.haloCanvas).width = width / haloScale;
(0, _render.haloCanvas).height = height / haloScale;
2025-04-11 20:34:51 +02:00
gameState.gameZoneHeight = (0, _options.isOptionOn)("mobile-mode") ? Math.floor(height * 0.8) : height;
2025-04-11 20:34:11 +02:00
const baseWidth = Math.round(Math.min(gameState.canvasWidth, gameState.gameZoneHeight * 0.73 * (gameState.gridSize + gameState.perks.unbounded * 2) / gameState.gridSize));
gameState.brickWidth = Math.floor(baseWidth / (gameState.gridSize + gameState.perks.unbounded * 2) / 2) * 2;
2025-03-19 18:13:41 +01:00
gameState.gameZoneWidth = gameState.brickWidth * gameState.gridSize;
gameState.offsetX = Math.floor((gameState.canvasWidth - gameState.gameZoneWidth) / 2);
2025-04-11 20:34:11 +02:00
// Space between left side and border
gameState.offsetXRoundedDown = gameState.offsetX - gameState.perks.unbounded * gameState.brickWidth;
if (gameState.offsetX < gameState.ballSize + gameState . perks . unbounded * 2 * gameState . brickWidth ) gameState . offsetXRoundedDown = 0;
2025-03-19 18:13:41 +01:00
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`);
};
2025-04-11 20:34:11 +02:00
window.addEventListener("resize", ()=>fitSize(gameState));
window.addEventListener("fullscreenchange", ()=>fitSize(gameState));
2025-03-19 18:13:41 +01:00
setInterval(()=>{
// Sometimes, the page changes size without triggering the event (when switching to fullscreen, closing debug panel...)
2025-04-12 08:50:28 +02:00
const width = window.innerWidth, height = window.innerHeight;
2025-04-11 20:34:11 +02:00
if (width !== gameState.canvasWidth || height !== gameState.canvasHeight) fitSize(gameState);
2025-03-19 18:13:41 +01:00
}, 1000);
2025-03-29 20:45:54 +01:00
async function openUpgradesPicker(gameState) {
2025-03-19 18:13:41 +01:00
const catchRate = (gameState.score - gameState.levelStartScore) / (gameState.levelSpawnedCoins || 1);
let repeats = 1;
let timeGain = "", catchGain = "", wallHitsGain = "", missesGain = "";
2025-04-08 21:54:19 +02:00
if (gameState.levelWallBounces < (0, _pureFunctions.wallBouncedBest)) {
2025-03-19 18:13:41 +01:00
repeats++;
2025-03-26 08:27:56 +01:00
gameState.rerolls++;
2025-03-30 21:07:58 +02:00
wallHitsGain = (0, _i18N.t)("level_up.plus_one_upgrade_and_reroll");
2025-04-08 21:54:19 +02:00
} else if (gameState.levelWallBounces < (0, _pureFunctions.wallBouncedGood)) {
2025-03-30 21:07:58 +02:00
repeats++;
2025-03-19 18:13:41 +01:00
wallHitsGain = (0, _i18N.t)("level_up.plus_one_upgrade");
}
2025-04-08 21:54:19 +02:00
if (gameState.levelTime < (0, _pureFunctions.levelTimeBest) * 1000) {
2025-03-19 18:13:41 +01:00
repeats++;
2025-03-26 08:27:56 +01:00
gameState.rerolls++;
2025-03-30 21:07:58 +02:00
timeGain = (0, _i18N.t)("level_up.plus_one_upgrade_and_reroll");
2025-04-08 21:54:19 +02:00
} else if (gameState.levelTime < (0, _pureFunctions.levelTimeGood) * 1000) {
2025-03-30 21:07:58 +02:00
repeats++;
timeGain = (0, _i18N.t)("level_up.plus_one_upgrade");
2025-03-19 18:13:41 +01:00
}
2025-04-08 21:54:19 +02:00
if (catchRate > (0, _pureFunctions.catchRateBest) / 100) {
2025-03-19 18:13:41 +01:00
repeats++;
2025-03-26 08:27:56 +01:00
gameState.rerolls++;
2025-03-30 21:07:58 +02:00
catchGain = (0, _i18N.t)("level_up.plus_one_upgrade_and_reroll");
2025-04-08 21:54:19 +02:00
} else if (catchRate > (0, _pureFunctions.catchRateGood) / 100) {
2025-03-30 21:07:58 +02:00
repeats++;
catchGain = (0, _i18N.t)("level_up.plus_one_upgrade");
2025-03-19 18:13:41 +01:00
}
2025-04-08 21:54:19 +02:00
if (gameState.levelMisses < (0, _pureFunctions.missesBest)) {
2025-03-19 18:13:41 +01:00
repeats++;
2025-03-26 08:27:56 +01:00
gameState.rerolls++;
2025-03-30 21:07:58 +02:00
missesGain = (0, _i18N.t)("level_up.plus_one_upgrade_and_reroll");
2025-04-08 21:54:19 +02:00
} else if (gameState.levelMisses < (0, _pureFunctions.missesGood)) {
2025-03-30 21:07:58 +02:00
repeats++;
2025-03-19 18:13:41 +01:00
missesGain = (0, _i18N.t)("level_up.plus_one_upgrade");
}
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
});
2025-03-19 18:13:41 +01:00
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)({
2025-03-19 18:13:41 +01:00
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 >
2025-03-30 21:07:58 +02:00
< p > ${(0, _i18N.t)("level_up.after_buttons", {
level: gameState.currentLevel + 1,
max: (0, _gameUtils.max_levels)(gameState)
})} < / p >
2025-04-04 09:45:35 +02:00
< p > ${(0, _gameUtils.levelsListHTMl)(gameState, gameState.currentLevel + 1)}< / p >
2025-03-23 19:11:01 +01:00
`,
2025-03-27 10:52:31 +01:00
...actions,
2025-03-30 21:07:58 +02:00
(0, _gameUtils.pickedUpgradesHTMl)(gameState),
2025-04-08 14:03:38 +02:00
(0, _openScorePanel.getNearestUnlockHTML)(gameState),
2025-03-30 21:07:58 +02:00
`< div id = "level-recording-container" > < / div > `
2025-03-27 10:52:31 +01:00
]
2025-03-19 18:13:41 +01:00
});
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++;
}
2025-03-19 18:13:41 +01:00
}
}
(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() {
2025-04-18 21:17:32 +02:00
startWork("physics");
2025-03-19 18:13:41 +01:00
const currentTick = performance.now();
const timeDeltaMs = currentTick - gameState.lastTick;
gameState.lastTick = currentTick;
2025-04-10 15:27:38 +02:00
let frames = Math.min(4, timeDeltaMs / (1000 / 60));
2025-03-19 18:13:41 +01:00
if (gameState.keyboardPuckSpeed) (0, _gameStateMutators.setMousePos)(gameState, gameState.puckPosition + gameState.keyboardPuckSpeed);
2025-04-10 15:27:38 +02:00
if (gameState.perks.superhot) frames *= (0, _pureFunctions.clamp)(Math.abs(gameState.puckPosition - gameState.lastPuckPosition) / 5, 0.2 / gameState.perks.superhot, 1);
2025-03-19 18:13:41 +01:00
(0, _gameStateMutators.normalizeGameState)(gameState);
if (gameState.running) {
2025-04-11 20:34:11 +02:00
gameState.levelTime += timeDeltaMs * frames;
gameState.runStatistics.runTime += timeDeltaMs * frames;
2025-04-19 16:50:26 +02:00
const maxBallSpeed = Math.sqrt(Math.max(0, ...gameState.balls.map(({ vx, vy })=>vx * vx + vy * vy))) * frames;
const steps = Math.ceil(maxBallSpeed / 8);
2025-04-18 17:15:47 +02:00
for(let i = 0; i < steps ; i + + ) ( 0 , _gameStateMutators . gameStateTick ) ( gameState , frames / steps ) ;
2025-03-19 18:13:41 +01:00
}
if (gameState.running || gameState.needsRender) {
gameState.needsRender = false;
(0, _render.render)(gameState);
}
2025-04-18 21:17:32 +02:00
startWork("record video");
2025-03-19 18:13:41 +01:00
if (gameState.running) (0, _recording.recordOneFrame)(gameState);
2025-04-18 21:17:32 +02:00
startWork("sound");
2025-03-19 18:13:41 +01:00
if ((0, _options.isOptionOn)("sound")) (0, _sounds.playPendingSounds)(gameState);
2025-04-15 21:28:00 +02:00
startWork("idle");
2025-03-19 18:13:41 +01:00
requestAnimationFrame(tick);
2025-03-23 15:48:21 +01:00
FPSCounter++;
2025-03-19 18:13:41 +01:00
}
2025-04-18 21:17:32 +02:00
const stats = document.getElementById("stats");
2025-04-15 17:31:57 +02:00
let total = {};
let lastTick = performance.now();
2025-04-18 21:17:32 +02:00
let doing = "idle";
2025-04-20 10:58:26 +02:00
let FPSCounter = 0;
let lastMeasuredFPS = 60;
2025-04-15 17:31:57 +02:00
function startWork(what) {
2025-04-18 21:17:32 +02:00
if (!gameState.startParams.stress) return;
2025-04-15 17:31:57 +02:00
const newNow = performance.now();
if (doing) total[doing] = (total[doing] || 0) + (newNow - lastTick);
lastTick = newNow;
doing = what;
}
2025-04-18 21:17:32 +02:00
setInterval(()=>{
2025-04-20 10:58:26 +02:00
lastMeasuredFPS = FPSCounter;
FPSCounter = 0;
2025-04-18 21:17:32 +02:00
if (!gameState.startParams.stress) {
stats.style.display = "none";
return;
}
stats.style.display = "block";
2025-04-15 17:31:57 +02:00
const totalTime = (0, _gameUtils.sumOfValues)(total);
2025-04-18 21:17:32 +02:00
stats.innerHTML = `
< div >
2025-04-20 10:58:26 +02:00
${lastMeasuredFPS} FPS -
${(0, _gameStateMutators.liveCount)(gameState.coins)} / ${(0, _settings.getCurrentMaxCoins)()} Coins -
${(0, _gameStateMutators.liveCount)(gameState.particles) + (0, _gameStateMutators.liveCount)(gameState.lights) + (0, _gameStateMutators.liveCount)(gameState.texts)} / ${(0, _settings.getCurrentMaxParticles)() * 3} particles
< / div >
2025-04-18 21:17:32 +02:00
2025-04-20 10:58:26 +02:00
2025-04-18 21:17:32 +02:00
` + Object.entries(total)// .sort((a, b) => b[1] - a[1])
.map((t)=>` < div >
< div style = "transform: scale(${(0, _pureFunctions.clamp)(t[1] / totalTime, 0, 1)},1)" > < / div >
2025-04-20 10:58:26 +02:00
< strong > ${t[0]} : ${Math.floor(t[1])} ms< / strong >
2025-04-18 21:17:32 +02:00
< / div >
`).join("\n");
2025-04-15 17:31:57 +02:00
total = {};
2025-04-18 21:17:32 +02:00
}, 1000);
2025-04-08 10:36:30 +02:00
setInterval(()=>{
(0, _monitorLevelsUnlocks.monitorLevelsUnlocks)(gameState);
}, 500);
2025-03-19 18:13:41 +01:00
window.addEventListener("visibilitychange", ()=>{
if (document.hidden) pause(true);
});
(0, _render.scoreDisplay).addEventListener("click", (e)=>{
e.preventDefault();
2025-04-07 14:08:48 +02:00
if (!(0, _asyncAlert.alertsOpen)) (0, _openScorePanel.openScorePanel)(gameState);
2025-03-19 18:13:41 +01:00
});
document.addEventListener("visibilitychange", ()=>{
if (document.hidden) pause(true);
});
document.getElementById("menu").addEventListener("click", (e)=>{
e.preventDefault();
2025-03-23 15:48:21 +01:00
if (!(0, _asyncAlert.alertsOpen)) openMainMenu();
2025-03-19 18:13:41 +01:00
});
2025-04-01 13:35:33 +02:00
const creativeModeThreshold = Math.max(...(0, _loadGameData.upgrades).map((u)=>u.threshold));
2025-03-23 15:48:21 +01:00
async function openMainMenu() {
pause(true);
const actions = [
{
2025-04-06 10:47:44 +02:00
icon: (0, _loadGameData.icons)["icon:new_run"],
2025-03-26 08:01:12 +01:00
text: (0, _i18N.t)("main_menu.normal"),
2025-04-06 10:13:10 +02:00
help: (0, _gameUtils.highScoreText)() || (0, _i18N.t)("main_menu.normal_help"),
2025-03-26 08:01:12 +01:00
value: ()=>{
restart({
2025-04-06 10:13:10 +02:00
levelToAvoid: (0, _gameUtils.currentLevelInfo)(gameState).name
2025-03-26 08:01:12 +01:00
});
2025-03-23 15:48:21 +01:00
}
},
2025-04-01 13:35:33 +02:00
(0, _creative.creativeMode)(gameState),
2025-04-07 08:24:17 +02:00
(0, _runHistoryViewer.runHistoryViewerMenuEntry)(),
2025-04-14 13:39:30 +02:00
(0, _levelEditor.levelEditorMenuEntry)(),
2025-03-23 15:48:21 +01:00
{
2025-04-01 13:35:33 +02: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 15:48:21 +01:00
}
},
2025-03-30 21:07:58 +02:00
...donationNag(gameState),
2025-03-23 15:48:21 +01:00
{
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();
2025-03-23 15:48:21 +01:00
}
2025-03-31 20:08:17 +02:00
},
(0, _help.helpMenuEntry)()
2025-03-23 15:48:21 +01:00
];
const cb = await (0, _asyncAlert.asyncAlert)({
title: (0, _i18N.t)("main_menu.title"),
2025-03-27 10:52:31 +01:00
content: [
...actions,
2025-04-08 21:54:19 +02:00
`< p >
< span > Made in France by < a href = "https://lecaro.me" > Renan LE CARO< / a > .< / span >
< a href = "https://paypal.me/renanlecaro" target = "_blank" > Donate< / a >
< a href = "https://discord.gg/bbcQw4x5zA" target = "_blank" > Discord< / a >
< a href = "https://f-droid.org/en/packages/me.lecaro.breakout/" target = "_blank" > F-Droid< / a >
< a href = "https://play.google.com/store/apps/details?id=me.lecaro.breakout" target = "_blank" > Google Play< / a >
< a href = "https://renanlecaro.itch.io/breakout71" target = "_blank" > itch.io< / a >
< a href = "https://gitlab.com/lecarore/breakout71" target = "_blank" > Gitlab< / a >
< a href = "https://breakout.lecaro.me/" target = "_blank" > Web version< / a >
< a href = "https://news.ycombinator.com/item?id=43183131" target = "_blank" > HackerNews< / a >
< a href = "https://breakout.lecaro.me/privacy.html" target = "_blank" > Privacy Policy< / a >
< a href = "https://archive.lecaro.me/public-files/b71/" target = "_blank" > Archives< / a >
< span > v.${(0, _loadGameData.appVersion)}< / span >
< / p > `
2025-03-27 10:52:31 +01:00
],
allowClose: true
2025-03-23 15:48:21 +01:00
});
if (cb) {
cb();
gameState.needsRender = true;
}
}
2025-03-30 21:07:58 +02:00
function donationNag(gameState) {
if (!(0, _options.isOptionOn)("donation_reminder")) return [];
const hours = (0, _pureFunctions.hoursSpentPlaying)();
return [
{
text: (0, _i18N.t)("main_menu.donate", {
hours
}),
help: (0, _i18N.t)("main_menu.donate_help", {
suggestion: Math.min(20, Math.max(1, 0.2 * hours)).toFixed(0)
}),
icon: (0, _loadGameData.icons)["icon:premium"],
value () {
window.open("https://paypal.me/renanlecaro", "_blank");
}
}
];
}
2025-03-23 15:48:21 +01:00
async function openSettingsMenu() {
2025-03-19 18:13:41 +01:00
pause(true);
2025-04-02 10:41:35 +02:00
const actions = [
(0, _startingPerks.startingPerkMenuButton)()
];
actions.push({
2025-04-09 11:28:32 +02:00
icon: (0, _loadGameData.icons)[(0, _i18N.languages).find((l)=>l.value === (0, _i18N.getCurrentLang)())?.levelName],
2025-04-11 09:36:31 +02:00
text: (0, _i18N.t)("settings.language"),
help: (0, _i18N.t)("settings.language_help"),
2025-04-02 10:41:35 +02:00
async value () {
const pick = await (0, _asyncAlert.asyncAlert)({
2025-04-11 09:36:31 +02:00
title: (0, _i18N.t)("settings.language"),
2025-04-02 10:41:35 +02:00
content: [
2025-04-11 09:36:31 +02:00
(0, _i18N.t)("settings.language_help"),
2025-04-09 11:28:32 +02:00
...(0, _i18N.languages).map((l)=>({
...l,
icon: (0, _loadGameData.icons)[l.levelName]
}))
2025-04-02 10:41:35 +02:00
],
allowClose: true
});
if (pick & & pick !== (0, _i18N.getCurrentLang)() & & await confirmRestart(gameState)) {
(0, _settings.setSettingValue)("lang", pick);
2025-04-16 09:26:10 +02:00
(0, _settings.commitSettingsChangesToLocalStorage)();
2025-04-02 10:41:35 +02:00
window.location.reload();
}
}
});
2025-04-05 11:09:07 +02:00
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,
2025-04-18 21:17:32 +02:00
disabled: (0, _options.isOptionOn)("basic") & & [
"extra_bright",
"contrast",
"smooth_lighting",
2025-04-20 09:33:12 +02:00
"precise_lighting",
"probabilistic_lighting"
2025-04-20 10:58:26 +02:00
].includes(key) || (0, _gameUtils.isInWebView) & & key == "record" || false,
2025-04-05 11:09:07 +02:00
value: ()=>{
(0, _options.toggleOption)(key);
2025-04-11 20:34:11 +02:00
fitSize(gameState);
2025-04-05 11:09:07 +02:00
applyFullScreenChoice();
openSettingsMenu();
}
});
2025-03-19 18:13:41 +01:00
actions.push({
2025-04-02 10:42:01 +02:00
icon: (0, _loadGameData.icons)["icon:download"],
2025-04-11 09:36:31 +02:00
text: (0, _i18N.t)("settings.download_save_file"),
help: (0, _i18N.t)("settings.download_save_file_help"),
2025-03-19 18:13:41 +01:00
async value () {
const dlLink = document.createElement("a");
2025-04-19 17:26:45 +02:00
const obj = {
2025-03-19 18:13:41 +01:00
fileType: "B71-save-file",
appVersion: (0, _loadGameData.appVersion),
2025-04-19 17:26:45 +02:00
payload: (0, _generateSaveFileContent.generateSaveFileContent)()
};
const json = JSON.stringify(obj, null, 2);
dlLink.setAttribute("href", "data:application/json;charset=utf-8," + encodeURIComponent(json));
dlLink.setAttribute("download", "b71-save-" + new Date().toISOString().slice(0, 19).replace(/[^0-9]+/gi, "-") + ".json");
2025-03-19 18:13:41 +01:00
document.body.appendChild(dlLink);
dlLink.click();
setTimeout(()=>document.body.removeChild(dlLink), 1000);
}
});
actions.push({
2025-04-02 10:42:01 +02:00
icon: (0, _loadGameData.icons)["icon:upload"],
2025-04-11 09:36:31 +02:00
text: (0, _i18N.t)("settings.load_save_file"),
help: (0, _i18N.t)("settings.load_save_file_help"),
2025-03-19 18:13:41 +01:00
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);
});
2025-04-19 17:26:45 +02:00
const { fileType, signedPayload, payload } = JSON.parse(content);
2025-03-19 18:13:41 +01:00
if (fileType !== "B71-save-file") throw new Error("Not a B71 save file");
2025-04-19 17:26:45 +02:00
if (payload) {
localStorage.clear();
for(let key in payload)localStorage.setItem(key, JSON.stringify(payload[key]));
} else if (signedPayload) {
// Old file format
const localStorageContent = JSON.parse(signedPayload);
localStorage.clear();
for(let key in localStorageContent)localStorage.setItem(key, localStorageContent[key]);
}
2025-03-19 18:13:41 +01:00
await (0, _asyncAlert.asyncAlert)({
2025-04-11 09:36:31 +02:00
title: (0, _i18N.t)("settings.save_file_loaded"),
2025-03-27 10:52:31 +01:00
content: [
2025-04-11 09:36:31 +02:00
(0, _i18N.t)("settings.save_file_loaded_help"),
2025-03-19 18:13:41 +01:00
{
2025-04-11 09:36:31 +02:00
text: (0, _i18N.t)("settings.save_file_loaded_ok")
2025-03-19 18:13:41 +01:00
}
]
});
window.location.reload();
}
} catch (e) {
await (0, _asyncAlert.asyncAlert)({
2025-04-11 09:36:31 +02:00
title: (0, _i18N.t)("settings.save_file_error"),
2025-03-27 10:52:31 +01:00
content: [
e.message,
2025-03-19 18:13:41 +01:00
{
2025-04-11 09:36:31 +02:00
text: (0, _i18N.t)("settings.save_file_loaded_ok")
2025-03-19 18:13:41 +01:00
}
]
});
}
input.value = "";
});
document.body.appendChild(input);
}
document.getElementById("save_file_picker")?.click();
}
});
actions.push({
2025-04-02 10:42:01 +02:00
icon: (0, _loadGameData.icons)["icon:coins"],
2025-04-11 09:36:31 +02:00
text: (0, _i18N.t)("settings.max_coins", {
2025-03-23 15:48:21 +01:00
max: (0, _settings.getCurrentMaxCoins)()
}),
2025-04-11 09:36:31 +02:00
help: (0, _i18N.t)("settings.max_coins_help"),
2025-03-23 15:48:21 +01:00
async value () {
(0, _settings.cycleMaxCoins)();
await openSettingsMenu();
}
});
2025-04-02 10:41:35 +02:00
actions.push({
2025-04-02 10:42:01 +02:00
icon: (0, _loadGameData.icons)["icon:reset"],
2025-04-11 09:36:31 +02:00
text: (0, _i18N.t)("settings.reset"),
help: (0, _i18N.t)("settings.reset_help"),
2025-04-02 10:41:35 +02:00
async value () {
if (await (0, _asyncAlert.asyncAlert)({
2025-04-11 09:36:31 +02:00
title: (0, _i18N.t)("settings.reset"),
2025-04-02 10:41:35 +02:00
content: [
2025-04-11 09:36:31 +02:00
(0, _i18N.t)("settings.reset_instruction"),
2025-04-02 10:41:35 +02:00
{
2025-04-11 09:36:31 +02:00
text: (0, _i18N.t)("settings.reset_confirm"),
2025-04-02 10:41:35 +02:00
value: true
},
{
2025-04-11 09:36:31 +02:00
text: (0, _i18N.t)("settings.reset_cancel"),
2025-04-02 10:41:35 +02:00
value: false
}
],
allowClose: true
})) {
localStorage.clear();
window.location.reload();
}
}
});
2025-04-18 21:17:32 +02:00
actions.push({
text: (0, _i18N.t)("settings.autoplay"),
help: (0, _i18N.t)("settings.autoplay_help"),
async value () {
startComputerControlledGame(false);
}
});
actions.push({
text: (0, _i18N.t)("settings.stress_test"),
help: (0, _i18N.t)("settings.stress_test_help"),
async value () {
startComputerControlledGame(true);
}
});
2025-03-19 18:13:41 +01:00
const cb = await (0, _asyncAlert.asyncAlert)({
2025-03-23 15:48:21 +01:00
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
],
2025-04-04 12:07:24 +02:00
allowClose: true,
2025-04-04 12:07:51 +02:00
className: "settings"
2025-03-19 18:13:41 +01:00
});
if (cb) {
cb();
gameState.needsRender = true;
}
}
2025-04-02 10:41:35 +02:00
async function applyFullScreenChoice() {
2025-03-29 17:40:07 +01:00
try {
if (!(document.fullscreenEnabled || document.webkitFullscreenEnabled)) return false;
if (document.fullscreenElement !== null & & !(0, _options.isOptionOn)("fullscreen")) {
if (document.exitFullscreen) {
2025-04-02 10:41:35 +02:00
await document.exitFullscreen();
2025-03-29 17:40:07 +01:00
return true;
} else if (document.webkitCancelFullScreen) {
2025-04-02 10:41:35 +02:00
await document.webkitCancelFullScreen();
2025-03-29 17:40:07 +01:00
return true;
}
} else if ((0, _options.isOptionOn)("fullscreen") & & !document.fullscreenElement) {
const docel = document.documentElement;
if (docel.requestFullscreen) {
2025-04-02 10:41:35 +02:00
await docel.requestFullscreen();
2025-03-29 17:40:07 +01:00
return true;
} else if (docel.webkitRequestFullscreen) {
2025-04-02 10:41:35 +02:00
await docel.webkitRequestFullscreen();
2025-03-29 17:40:07 +01:00
return true;
}
}
} catch (e) {
console.warn(e);
}
return false;
}
2025-03-19 18:13:41 +01:00
async function openUnlocksList() {
const ts = (0, _settings.getTotalScore)();
2025-04-06 15:38:30 +02:00
const hintField = (0, _options.isOptionOn)("mobile-mode") ? "help" : "tooltip";
2025-03-29 17:40:07 +01:00
const upgradeActions = (0, _loadGameData.upgrades).sort((a, b)=>a.threshold - b.threshold).map(({ name, id, threshold, icon, help })=>({
text: name,
disabled: ts < threshold ,
value: {
perks: {
[id]: 1
2025-04-10 14:49:28 +02:00
},
2025-04-14 13:39:30 +02:00
level: (0, _loadGameData.allLevelsAndIcons).find((l)=>l.name === "icon:" + id)
2025-03-29 17:40:07 +01:00
},
2025-04-01 13:35:33 +02:00
icon,
2025-04-06 15:38:30 +02:00
[hintField]: ts < threshold ? ( 0 , _i18N . t ) ( " unlocks . minTotalScore " , {
score: threshold
}) : help(1)
2025-03-29 17:40:07 +01:00
}));
2025-04-08 14:29:00 +02:00
const unlockedBefore = new Set((0, _settings.getSettingValue)("breakout_71_unlocked_levels", []));
2025-04-06 15:38:30 +02:00
const levelActions = (0, _loadGameData.allLevels).map((l, li)=>{
2025-04-08 14:29:00 +02:00
const lockedBecause = unlockedBefore.has(l.name) ? null : (0, _gameUtils.reasonLevelIsLocked)(li, (0, _gameOver.getHistory)(), true);
2025-04-07 14:08:48 +02:00
const percentUnlocked = lockedBecause?.reached ? `< span class = "progress-inline" > < span style = "transform: scale(${Math.floor(lockedBecause.reached / lockedBecause.minScore * 100) / 100},1)" > < / span > < / span > ` : "";
2025-03-29 17:40:07 +01:00
return {
2025-04-07 14:08:48 +02:00
text: l.name + percentUnlocked,
disabled: !!lockedBecause,
2025-03-29 17:40:07 +01:00
value: {
2025-04-14 13:39:30 +02:00
level: l
2025-03-29 17:40:07 +01:00
},
2025-04-01 13:35:33 +02:00
icon: (0, _loadGameData.icons)[l.name],
2025-04-07 14:08:48 +02:00
[hintField]: lockedBecause?.text || (0, _gameUtils.describeLevel)(l)
2025-03-29 17:40:07 +01:00
};
});
2025-03-19 18:13:41 +01:00
const tryOn = await (0, _asyncAlert.asyncAlert)({
2025-04-06 11:57:52 +02:00
title: (0, _i18N.t)("unlocks.title_upgrades", {
unlocked: upgradeActions.filter((a)=>!a.disabled).length,
out_of: upgradeActions.length
2025-03-19 18:13:41 +01:00
}),
2025-03-27 10:52:31 +01:00
content: [
`< p > ${(0, _i18N.t)("unlocks.intro", {
2025-04-01 13:35:33 +02:00
ts
2025-03-27 10:52:31 +01:00
})}
2025-04-06 11:57:52 +02:00
${upgradeActions.find((u)=>u.disabled) ? (0, _i18N.t)("unlocks.greyed_out_help") : ""}< / p > `,
2025-03-29 17:40:07 +01:00
...upgradeActions,
2025-04-06 11:57:52 +02:00
(0, _i18N.t)("unlocks.level", {
unlocked: levelActions.filter((a)=>!a.disabled).length,
out_of: levelActions.length
}),
2025-03-29 17:40:07 +01:00
...levelActions
2025-03-27 10:52:31 +01:00
],
2025-03-26 08:01:12 +01:00
allowClose: true,
2025-04-06 15:38:30 +02:00
className: (0, _options.isOptionOn)("mobile-mode") ? "" : "actionsAsGrid"
2025-03-19 18:13:41 +01:00
});
if (tryOn) {
2025-03-30 21:07:58 +02:00
if (await confirmRestart(gameState)) restart({
2025-04-06 10:13:10 +02:00
...tryOn
2025-03-30 21:07:58 +02:00
});
2025-03-19 18:13:41 +01:00
}
}
2025-03-26 08:01:12 +01:00
async function confirmRestart(gameState) {
2025-03-19 18:13:41 +01:00
if (!gameState.currentLevel) return true;
2025-03-30 21:07:58 +02:00
if (0, _asyncAlert.alertsOpen) return true;
2025-04-06 10:13:10 +02:00
pause(true);
2025-03-19 18:13:41 +01:00
return (0, _asyncAlert.asyncAlert)({
title: (0, _i18N.t)("confirmRestart.title"),
2025-03-27 10:52:31 +01:00
content: [
(0, _i18N.t)("confirmRestart.text"),
2025-03-19 18:13:41 +01:00
{
value: true,
text: (0, _i18N.t)("confirmRestart.yes")
},
{
value: false,
text: (0, _i18N.t)("confirmRestart.no")
}
]
});
}
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;
}
2025-03-30 21:07:58 +02:00
document.addEventListener("keydown", async (e)=>{
2025-03-29 17:40:07 +01:00
if (e.key.toLowerCase() === "f" & & !e.ctrlKey & & !e.metaKey) {
2025-03-29 21:28:05 +01:00
(0, _options.toggleOption)("fullscreen");
2025-03-29 17:40:07 +01:00
applyFullScreenChoice();
} else if (e.key in pressed) setKeyPressed(e.key, 1);
2025-03-19 18:13:41 +01:00
if (e.key === " " & & !(0, _asyncAlert.alertsOpen)) {
if (gameState.running) pause(true);
else play();
} else return;
e.preventDefault();
});
2025-03-30 21:07:58 +02:00
let pageLoad = new Date();
2025-03-19 18:13:41 +01:00
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);
2025-03-23 15:48:21 +01:00
else if (e.key.toLowerCase() === "m" & & !(0, _asyncAlert.alertsOpen)) openMainMenu().then();
2025-04-10 21:40:45 +02:00
else if (e.key.toLowerCase() === "s" & & !(0, _asyncAlert.alertsOpen)) (0, _openScorePanel.openScorePanel)(gameState).then();
2025-04-12 20:58:24 +02:00
else if (e.key.toLowerCase() === "r" & & !(0, _asyncAlert.alertsOpen) & & pageLoad < Date.now ( ) - 500 ) {
2025-04-18 21:17:32 +02:00
if (gameState.startParams.computer_controlled) return startComputerControlledGame(gameState.startParams.stress);
2025-04-12 20:58:24 +02:00
// When doing ctrl + R in dev to refresh, i don't want to instantly restart a run
2025-03-26 08:01:12 +01:00
if (await confirmRestart(gameState)) restart({
2025-04-06 10:13:10 +02:00
levelToAvoid: (0, _gameUtils.currentLevelInfo)(gameState).name
2025-03-19 18:13:41 +01:00
});
} else return;
e.preventDefault();
});
2025-04-06 10:13:10 +02:00
const gameState = (0, _newGameState.newGameState)({});
2025-03-19 18:13:41 +01:00
function restart(params) {
Object.assign(gameState, (0, _newGameState.newGameState)(params));
2025-04-09 09:24:15 +02:00
// Recompute brick size according to level
2025-04-11 20:34:11 +02:00
fitSize(gameState);
2025-03-19 18:13:41 +01:00
(0, _recording.pauseRecording)();
(0, _gameStateMutators.setLevel)(gameState, 0);
2025-04-12 20:01:43 +02:00
if (params?.computer_controlled) play();
2025-03-19 18:13:41 +01:00
}
2025-04-18 21:17:32 +02:00
if (window.location.search.match(/autoplay|stress/)) startComputerControlledGame(window.location.search.includes("stress"));
else restart({});
function startComputerControlledGame(stress = false) {
2025-04-12 20:01:43 +02:00
const perks = {
2025-04-15 16:47:04 +02:00
base_combo: 20,
2025-04-12 20:01:43 +02:00
pierce: 3
};
2025-04-18 21:17:32 +02:00
if (stress) Object.assign(perks, {
2025-04-15 21:25:27 +02:00
base_combo: 5000,
pierce: 20,
rainbow: 3,
sapper: 2,
etherealcoins: 1,
bricks_attract_ball: 1,
respawn: 3
});
else {
for(let i = 0; i < 10 ; i + + ) {
const u = (0, _gameUtils.sample)((0, _loadGameData.upgrades));
perks[u.id] ||= Math.floor(Math.random() * u.max) + 1;
}
perks.superhot = 0;
2025-04-11 20:34:11 +02:00
}
2025-04-12 20:01:43 +02:00
restart({
2025-04-15 16:47:04 +02:00
level: (0, _gameUtils.sample)((0, _loadGameData.allLevels).filter((l)=>l.color === "#000000")),
2025-04-12 20:01:43 +02:00
computer_controlled: true,
2025-04-18 21:17:32 +02:00
perks,
stress
2025-04-12 20:01:43 +02:00
});
}
2025-03-19 18:13:41 +01:00
tick();
2025-04-01 13:35:33 +02:00
(0, _tooltip.setupTooltips)();
2025-04-01 13:39:09 +02:00
document.getElementById("menu")?.setAttribute("data-tooltip", (0, _i18N.t)("play.menu_tooltip"));
2025-03-19 18:13:41 +01:00
2025-04-19 17:26:45 +02:00
},{"./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","./pure_functions":"6pQh7","./help":"bqkdF","./creative":"63kYJ","./tooltip":"3RWxb","./startingPerks":"lv30m","./migrations":"a9qdY","./gameOver":"caCAf","./generateSaveFileContent":"iEcoB","./runHistoryViewer":"b80Ki","./openScorePanel":"aHTmD","./monitorLevelsUnlocks":"jjD0P","./levelEditor":"cirX1","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"l1B4x":[function(require,module,exports,__globalThis) {
2025-03-19 18:13:41 +01:00
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "appVersion", ()=>appVersion);
parcelHelpers.export(exports, "icons", ()=>icons);
2025-04-14 13:39:30 +02:00
parcelHelpers.export(exports, "transformRawLevel", ()=>transformRawLevel);
2025-04-07 14:22:59 +02:00
parcelHelpers.export(exports, "allLevelsAndIcons", ()=>allLevelsAndIcons);
2025-03-19 18:13:41 +01:00
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");
2025-03-19 18:13:41 +01:00
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 = {};
2025-04-14 13:39:30 +02:00
function transformRawLevel(level) {
2025-03-19 18:13:41 +01:00
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;
2025-03-19 18:13:41 +01:00
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,
2025-03-19 18:13:41 +01:00
icon,
2025-04-03 16:10:51 +02:00
color: level.color || "#000000",
2025-04-14 13:39:30 +02:00
svg: (0, _getLevelBackground.getLevelBackground)(level),
sortKey: (Math.random() + 3) / 3.5 * bricksCount
2025-03-19 18:13:41 +01:00
};
2025-04-14 13:39:30 +02:00
}
const allLevelsAndIcons = rawLevelsList.map(transformRawLevel);
2025-04-07 14:22:59 +02:00
const allLevels = allLevelsAndIcons.filter((l)=>!l.name.startsWith("icon:"));
2025-03-19 20:14:55 +01:00
const upgrades = (0, _upgrades.rawUpgrades).map((u)=>({
2025-03-19 18:13:41 +01:00
...u,
2025-03-28 10:21:14 +01:00
icon: icons["icon:" + u.id]
2025-03-19 18:13:41 +01:00
}));
2025-04-06 11:27:26 +02:00
},{"./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"}],"ktRBU":[function(require,module,exports,__globalThis) {
2025-04-09 09:24:15 +02:00
module.exports = JSON.parse("{\"_\":\"\",\"B\":\"black\",\"W\":\"#FFFFFF\",\"g\":\"#231f20\",\"y\":\"#FFD300\",\"b\":\"#6262EA\",\"t\":\"#5DA3EA\",\"s\":\"#E67070\",\"r\":\"#e32119\",\"R\":\"#ab0c0c\",\"c\":\"#59EEA3\",\"G\":\"#A1F051\",\"v\":\"#A664E8\",\"p\":\"#E869E8\",\"a\":\"#5BECEC\",\"C\":\"#53EE53\",\"S\":\"#F44848\",\"P\":\"#E66BA8\",\"O\":\"#F29E4A\",\"k\":\"#618227\",\"e\":\"#e1c8b4\",\"l\":\"#9b9fa4\"}");
2025-04-06 11:27:26 +02:00
},{}],"8JSUc":[function(require,module,exports,__globalThis) {
2025-04-16 15:30:20 +02:00
module.exports = JSON.parse('[{"name":"71 mini","size":5,"bricks":"bbb____bt__btt__b_t___ttt","color":""},{"name":"Butterfly","bricks":"_________bb_t_t_bbbbb_t_bbbbbbbtbbbb_bbbtbbb____btb____bbbtbbb__bb_t_bb__________","size":9,"color":""},{"name":"Castle","size":7,"bricks":"s_s_s_ssssssssssBBBssssBBBssttbbbttttbbbtttbtbtbt","color":""},{"name":"Eyes","size":9,"bricks":"ttttttt__tWWWWWWW_tWrrWttW_tWWWWWWW_ttttttt_____t______ttttt____ttttt_____t_t","color":"","credit":"My favorite character in https://nuclearthrone.com/"},{"name":"Creeper","size":10,"bricks":"___________ccGGccGG__cGccGcGc__GBBccBBc__cBBGcBBc__GccBBGGc__ccBBBBcG__GGBBBBcG__cGBccBGc___________","credit":"https://en.wikipedia.org/wiki/Creeper_(Minecraft)","color":""},{"name":"Stairs","size":8,"bricks":"tt______tt______bbtt____bbtt____vvbbtt__vvbbtt__ppvvbbttppvvbbtt","color":""},{"name":"Dots","size":9,"bricks":"b_t_a_c_c__________b_t_a_c__________P_b_t_a_c__________P_b_t_a__________P_P_b_t_a","color":""},{"name":"Lines","size":9,"bricks":"aaaaaaaa___________tttttttt_________aaaaaaaa___________tttttttt_________aaaaaaaa","color":""},{"name":"Heart","size":15,"bricks":"__________________RRR___RRR_____RSSSR_RSSSR___RSWWSSRSSSSSR__RSWSSSSSSSSSR__RSSSSSSSSSSSR__RSWSSSSSSSSSR___RSSSSSSSSSR_____RSSSSSSSR_______RSSSSSR_________RSSSR___________RSR_____________R____________________________________","color":"","credit":"https://www.youtube.com/watch?v=gdWiTfzXb1g"},{"name":"Swiss","size":7,"bricks":"________RRRRR__RRWRR__RWWWR__RRWRR__RRRRR________","color":""},{"name":"Germany","size":4,"bricks":"____ggggrrrryyyy","color":"#5da3ea"},{"name":"France","size":6,"bricks":"______ttWWrrttWWrrttWWrrttWWrrttWWrr","color":""},{"name":"Smiley","size":8,"bricks":"_________yy__yy__yy__yy__________________yyyyyy___yyyy__________","color":""},{"name":"Labyrinthe","size":11,"bricks":"_______tttS_Stttt_S________t___S__Stt_ttttt____t_____S__ttt_S_S____t___t_tttt_t_S_t____tSt_t_t_Sttt___t_t_____Sttt_tttttS"},{"name":"Temple","size":11,"bricks":"_______________WWW______WWWWWWW___WWWWWWWWW___b_b_b_b____b_b_b_b____v_v_v_v____P_P_P_P____P_P_P_P____WWWWWWW___WWWWWWWWW_","color":""},{"name":"Pacman","size":12,"bricks":"____yyyy______yyyyyyyy___yyyyByyyyy__yyyyyyyyy__yyyyyyyy____yyyyyy______yyyyyy___S_Syyyyyyyy_____yyyyyyyyy___yyyyyyyyyy___yyyyyyyy______yyyy","color":"","credit":"https://en.wikipedia.org/wiki/Pacman"},{"name":"Ship","size":11,"bricks":"____sWW________sWWW_______sWWW_______s___OOOOOOOOOOOOOO_OBOBOBOBOO__OOOOOOOO_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb___________"},{"name":"We come in peace","size":13,"bricks":"________________a_____a_______a___a_______aaaaaaa_____aaBaaaBaa___aaaaaaaaaaa__aaaaaaaaaaa__a_aaaaaaa_a__a_a_____a_a_____aa_aa_____________________________","color":"","credit":"https://en.wikipedia.org/wiki/Space_invaders"},{"name":"Space mushroom","size":10,"bricks":"______________WW_______WWWW_____WWWWWW___WWBWWBWW__WWWWWWWW____W__W_____W_WW_W___W_W__W_W","color":"","credit":"https://en.wikipedia.org/wiki/Space_invaders"},{"name":"Wololo","size":9,"bricks":"____WW_OOW___WW__OWW__W___OWWWbbbW_WWW_WbW_WOW__WWb__OW__bbb__O___W_W__O___W_W__O","color":"","credit":"https://aoe.heavengames.com/theacademy/unitsboatsandbuildings/priest/"},{"name":"Small heart","size":15,"bricks":"________________________________RRRR___RRRR___RrWWrR_RWWrrR__RWWrrrRWWrrrR__RrrrrrrrrrrrR__RrrrrrrrrrrrR___RrrrrrrrrrR_____RrrrrrrrR_______RrrrrrR_________RrrrR___________RrR_____________R______________________","color":""},{"name":"Eye","size":9,"bricks":"____________ggg_____gWWWg___gWbbbWg_gWWbBbWWg_gWbbbWg___gWWWg_____ggg____________","color":"#5da3ea"},{"name":"Enderman","size":10,"bricks":"___________gggggggg__gggggggg__gggggggg__gggggggg__vvvggvvv__gggggggg__gggggggg__gggggggg_____________________","color":"#154b07","credit":"https://minecraft.wiki/w/Enderman"},{"name":"Mushroom","size":16,"bricks":"_____________________rrrrWW________WWrrrrWWWW_____WWrrrrrrWWWW____WrrWWWWrrWWW___rrrWWWWWWrrrrr__rrrWWWWWWrrWWr__WrrWWWWWWrWWWW__WWrrWWWWrrWWWW__WWrrrrrrrrrWWr__WrrWWWWWWWWrrr_____WWBWWBWW_______WWWBWWBWW
2025-04-06 11:27:26 +02:00
},{}],"iyP6E":[function(require,module,exports,__globalThis) {
2025-04-20 10:58:26 +02:00
module.exports = JSON.parse("\"29085612\"");
2025-03-20 18:44:46 +01:00
},{}],"1u3Dx":[function(require,module,exports,__globalThis) {
2025-03-19 18:13:41 +01:00
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
2025-04-11 09:36:31 +02:00
parcelHelpers.export(exports, "noCreative", ()=>noCreative);
parcelHelpers.export(exports, "notStartingPerk", ()=>notStartingPerk);
2025-03-19 18:13:41 +01:00
parcelHelpers.export(exports, "rawUpgrades", ()=>rawUpgrades);
var _i18N = require("./i18n/i18n");
2025-03-29 21:22:19 +01:00
var _pureFunctions = require("./pure_functions");
2025-04-11 09:36:31 +02:00
const noCreative = [
"extra_levels",
"shunt",
"one_more_choice",
"instant_upgrade"
];
const notStartingPerk = [
"instant_upgrade"
];
2025-03-19 18:13:41 +01:00
const rawUpgrades = [
{
requires: "",
threshold: 0,
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-19 18:13:41 +01:00
id: "extra_life",
2025-04-08 10:36:30 +02:00
max: 7,
2025-03-19 18:13:41 +01:00
name: (0, _i18N.t)("upgrades.extra_life.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>lvl === 1 ? (0, _i18N.t)("upgrades.extra_life.tooltip") : (0, _i18N.t)("upgrades.extra_life.help_plural", {
2025-03-19 18:13:41 +01:00
lvl
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.extra_life.verbose_description")
2025-03-19 18:13:41 +01:00
},
{
requires: "",
threshold: 0,
id: "base_combo",
2025-04-11 09:36:31 +02:00
gift: true,
2025-03-19 18:13:41 +01:00
max: 7,
name: (0, _i18N.t)("upgrades.base_combo.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.base_combo.tooltip", {
2025-03-19 18:13:41 +01:00
coins: 1 + lvl * 3
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.base_combo.verbose_description")
2025-03-19 18:13:41 +01:00
},
{
requires: "",
threshold: 0,
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-19 18:13:41 +01:00
id: "slow_down",
max: 2,
name: (0, _i18N.t)("upgrades.slow_down.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.slow_down.tooltip", {
2025-03-29 11:24:45 +01:00
lvl
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.slow_down.verbose_description")
2025-03-19 18:13:41 +01:00
},
{
requires: "",
threshold: 0,
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-19 18:13:41 +01:00
id: "bigger_puck",
max: 2,
name: (0, _i18N.t)("upgrades.bigger_puck.name"),
2025-04-09 11:28:32 +02:00
help: ()=>(0, _i18N.t)("upgrades.bigger_puck.tooltip"),
fullHelp: (0, _i18N.t)("upgrades.bigger_puck.verbose_description")
2025-03-19 18:13:41 +01:00
},
{
requires: "",
threshold: 0,
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-19 18:13:41 +01:00
id: "viscosity",
max: 3,
name: (0, _i18N.t)("upgrades.viscosity.name"),
2025-04-09 11:28:32 +02:00
help: ()=>(0, _i18N.t)("upgrades.viscosity.tooltip"),
fullHelp: (0, _i18N.t)("upgrades.viscosity.verbose_description")
2025-03-19 18:13:41 +01:00
},
{
requires: "",
2025-04-08 14:03:38 +02:00
threshold: 50,
2025-04-11 09:36:31 +02:00
gift: false,
2025-04-08 14:03:38 +02:00
id: "skip_last",
max: 7,
name: (0, _i18N.t)("upgrades.skip_last.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>lvl == 1 ? (0, _i18N.t)("upgrades.skip_last.tooltip") : (0, _i18N.t)("upgrades.skip_last.help_plural", {
2025-04-08 14:03:38 +02:00
lvl
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.skip_last.verbose_description")
2025-04-08 14:03:38 +02:00
},
{
requires: "",
threshold: 100,
id: "streak_shots",
2025-04-11 09:36:31 +02:00
gift: true,
2025-04-08 14:03:38 +02:00
max: 1,
name: (0, _i18N.t)("upgrades.streak_shots.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.streak_shots.tooltip", {
2025-04-08 14:03:38 +02:00
lvl
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.streak_shots.verbose_description")
2025-04-08 14:03:38 +02:00
},
{
requires: "",
threshold: 200,
2025-03-19 18:13:41 +01:00
id: "left_is_lava",
2025-04-11 09:36:31 +02:00
gift: true,
2025-03-19 18:13:41 +01:00
max: 1,
name: (0, _i18N.t)("upgrades.left_is_lava.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.left_is_lava.tooltip", {
2025-03-29 11:24:45 +01:00
lvl
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.left_is_lava.verbose_description")
2025-03-19 18:13:41 +01:00
},
{
requires: "",
2025-04-08 14:03:38 +02:00
threshold: 300,
2025-03-19 18:13:41 +01:00
id: "right_is_lava",
2025-04-11 09:36:31 +02:00
gift: true,
2025-03-19 18:13:41 +01:00
max: 1,
name: (0, _i18N.t)("upgrades.right_is_lava.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.right_is_lava.tooltip", {
2025-03-29 11:24:45 +01:00
lvl
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.right_is_lava.verbose_description")
2025-03-19 18:13:41 +01:00
},
{
requires: "",
2025-04-08 14:03:38 +02:00
threshold: 400,
2025-03-19 18:13:41 +01:00
id: "top_is_lava",
2025-04-11 09:36:31 +02:00
gift: true,
2025-03-19 18:13:41 +01:00
max: 1,
name: (0, _i18N.t)("upgrades.top_is_lava.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.top_is_lava.tooltip", {
2025-03-29 11:24:45 +01:00
lvl
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.top_is_lava.verbose_description")
2025-03-19 18:13:41 +01:00
},
{
requires: "",
threshold: 500,
id: "telekinesis",
2025-04-11 09:36:31 +02:00
gift: true,
2025-03-30 21:07:58 +02:00
max: 1,
2025-03-19 18:13:41 +01:00
name: (0, _i18N.t)("upgrades.telekinesis.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>lvl == 1 ? (0, _i18N.t)("upgrades.telekinesis.tooltip") : (0, _i18N.t)("upgrades.telekinesis.help_plural"),
fullHelp: (0, _i18N.t)("upgrades.telekinesis.verbose_description")
2025-03-19 18:13:41 +01:00
},
{
requires: "",
2025-04-08 14:03:38 +02:00
threshold: 700,
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-19 18:13:41 +01:00
id: "coin_magnet",
max: 3,
name: (0, _i18N.t)("upgrades.coin_magnet.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>lvl == 1 ? (0, _i18N.t)("upgrades.coin_magnet.tooltip") : (0, _i18N.t)("upgrades.coin_magnet.help_plural"),
fullHelp: (0, _i18N.t)("upgrades.coin_magnet.verbose_description")
2025-03-19 18:13:41 +01:00
},
{
requires: "",
2025-04-08 14:03:38 +02:00
threshold: 800,
2025-03-19 18:13:41 +01:00
id: "multiball",
2025-04-11 09:36:31 +02:00
gift: true,
2025-03-19 18:13:41 +01:00
max: 6,
name: (0, _i18N.t)("upgrades.multiball.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.multiball.tooltip", {
2025-03-19 18:13:41 +01:00
count: lvl + 1
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.multiball.verbose_description")
2025-03-19 18:13:41 +01:00
},
{
requires: "",
2025-04-08 14:03:38 +02:00
threshold: 1000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-19 18:13:41 +01:00
id: "smaller_puck",
max: 2,
name: (0, _i18N.t)("upgrades.smaller_puck.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>lvl == 1 ? (0, _i18N.t)("upgrades.smaller_puck.tooltip") : (0, _i18N.t)("upgrades.smaller_puck.help_plural"),
fullHelp: (0, _i18N.t)("upgrades.smaller_puck.verbose_description")
2025-03-19 18:13:41 +01:00
},
{
requires: "",
2025-04-08 14:03:38 +02:00
threshold: 1500,
2025-03-19 18:13:41 +01:00
id: "pierce",
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-19 18:13:41 +01:00
max: 3,
name: (0, _i18N.t)("upgrades.pierce.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.pierce.tooltip", {
2025-03-19 18:13:41 +01:00
count: 3 * lvl
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.pierce.verbose_description")
2025-03-19 18:13:41 +01:00
},
{
requires: "",
2025-04-08 14:03:38 +02:00
threshold: 2000,
2025-03-19 18:13:41 +01:00
id: "picky_eater",
2025-04-11 09:36:31 +02:00
gift: true,
2025-03-19 18:13:41 +01:00
max: 1,
name: (0, _i18N.t)("upgrades.picky_eater.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.picky_eater.tooltip", {
2025-03-29 11:24:45 +01:00
lvl
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.picky_eater.verbose_description")
2025-03-19 18:13:41 +01:00
},
{
requires: "",
2025-04-08 14:03:38 +02:00
threshold: 2500,
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-19 18:13:41 +01:00
id: "metamorphosis",
max: 1,
name: (0, _i18N.t)("upgrades.metamorphosis.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.metamorphosis.tooltip", {
2025-03-29 11:24:45 +01:00
lvl
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.metamorphosis.verbose_description")
2025-03-19 18:13:41 +01:00
},
{
requires: "",
2025-04-08 14:03:38 +02:00
threshold: 3000,
2025-03-19 18:13:41 +01:00
id: "compound_interest",
2025-04-11 09:36:31 +02:00
gift: true,
2025-03-19 18:13:41 +01:00
max: 1,
name: (0, _i18N.t)("upgrades.compound_interest.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.compound_interest.tooltip", {
2025-03-29 11:24:45 +01:00
lvl
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.compound_interest.verbose_description")
2025-03-19 18:13:41 +01:00
},
{
requires: "",
2025-04-08 14:03:38 +02:00
threshold: 4000,
2025-03-19 18:13:41 +01:00
id: "hot_start",
2025-04-11 09:36:31 +02:00
gift: true,
2025-03-19 18:13:41 +01:00
max: 3,
name: (0, _i18N.t)("upgrades.hot_start.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.hot_start.tooltip", {
2025-03-30 21:07:58 +02:00
start: lvl * 30 + 1,
loss: lvl
2025-03-19 18:13:41 +01:00
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.hot_start.verbose_description")
2025-03-19 18:13:41 +01:00
},
{
requires: "",
2025-04-08 14:03:38 +02:00
threshold: 6000,
2025-03-19 18:13:41 +01:00
id: "sapper",
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-19 18:13:41 +01:00
max: 7,
name: (0, _i18N.t)("upgrades.sapper.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>lvl == 1 ? (0, _i18N.t)("upgrades.sapper.tooltip") : (0, _i18N.t)("upgrades.sapper.help_plural", {
2025-03-19 18:13:41 +01:00
lvl
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.sapper.verbose_description")
2025-03-19 18:13:41 +01:00
},
{
requires: "",
2025-04-08 14:03:38 +02:00
threshold: 9000,
2025-03-19 18:13:41 +01:00
id: "bigger_explosions",
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-19 18:13:41 +01:00
max: 1,
name: (0, _i18N.t)("upgrades.bigger_explosions.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.bigger_explosions.tooltip"),
fullHelp: (0, _i18N.t)("upgrades.bigger_explosions.verbose_description")
2025-03-19 18:13:41 +01:00
},
{
requires: "",
threshold: 13000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-27 10:52:31 +01:00
adventure: false,
2025-03-19 18:13:41 +01:00
id: "extra_levels",
max: 3,
name: (0, _i18N.t)("upgrades.extra_levels.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.extra_levels.tooltip", {
2025-03-19 18:13:41 +01:00
count: lvl + 7
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.extra_levels.verbose_description")
2025-03-19 18:13:41 +01:00
},
{
requires: "",
threshold: 15000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-19 18:13:41 +01:00
id: "pierce_color",
2025-03-23 19:11:01 +01:00
max: 4,
2025-03-19 18:13:41 +01:00
name: (0, _i18N.t)("upgrades.pierce_color.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.pierce_color.tooltip", {
2025-03-23 19:11:01 +01:00
lvl
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.pierce_color.verbose_description")
2025-03-19 18:13:41 +01:00
},
{
requires: "",
threshold: 18000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-19 18:13:41 +01:00
id: "soft_reset",
2025-03-29 11:24:45 +01:00
max: 3,
2025-03-19 18:13:41 +01:00
name: (0, _i18N.t)("upgrades.soft_reset.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.soft_reset.tooltip", {
2025-03-29 21:22:19 +01:00
percent: Math.round((0, _pureFunctions.comboKeepingRate)(lvl) * 100)
2025-03-19 18:13:41 +01:00
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.soft_reset.verbose_description")
2025-03-19 18:13:41 +01:00
},
{
requires: "multiball",
threshold: 21000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-19 18:13:41 +01:00
id: "ball_repulse_ball",
max: 3,
name: (0, _i18N.t)("upgrades.ball_repulse_ball.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>lvl == 1 ? (0, _i18N.t)("upgrades.ball_repulse_ball.tooltip") : (0, _i18N.t)("upgrades.ball_repulse_ball.help_plural"),
fullHelp: (0, _i18N.t)("upgrades.ball_repulse_ball.verbose_description")
2025-03-19 18:13:41 +01:00
},
{
requires: "multiball",
threshold: 25000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-19 18:13:41 +01:00
id: "ball_attract_ball",
max: 3,
name: (0, _i18N.t)("upgrades.ball_attract_ball.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>lvl == 1 ? (0, _i18N.t)("upgrades.ball_attract_ball.tooltip") : (0, _i18N.t)("upgrades.ball_attract_ball.help_plural"),
fullHelp: (0, _i18N.t)("upgrades.ball_attract_ball.verbose_description")
2025-03-19 18:13:41 +01:00
},
{
requires: "",
threshold: 30000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-19 18:13:41 +01:00
id: "puck_repulse_ball",
max: 2,
name: (0, _i18N.t)("upgrades.puck_repulse_ball.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>lvl == 1 ? (0, _i18N.t)("upgrades.puck_repulse_ball.tooltip") : (0, _i18N.t)("upgrades.puck_repulse_ball.help_plural"),
fullHelp: (0, _i18N.t)("upgrades.puck_repulse_ball.verbose_description")
2025-03-19 18:13:41 +01:00
},
{
requires: "",
threshold: 35000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-19 18:13:41 +01:00
id: "wind",
max: 3,
name: (0, _i18N.t)("upgrades.wind.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>lvl == 1 ? (0, _i18N.t)("upgrades.wind.tooltip") : (0, _i18N.t)("upgrades.wind.help_plural"),
fullHelp: (0, _i18N.t)("upgrades.wind.verbose_description")
2025-03-19 18:13:41 +01:00
},
{
requires: "",
threshold: 40000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-19 18:13:41 +01:00
id: "sturdy_bricks",
max: 4,
name: (0, _i18N.t)("upgrades.sturdy_bricks.name"),
2025-03-29 11:24:45 +01:00
help: (lvl)=>// lvl == 1
2025-04-09 11:28:32 +02:00
(0, _i18N.t)("upgrades.sturdy_bricks.tooltip", {
2025-03-29 11:24:45 +01:00
lvl,
2025-03-30 21:07:58 +02:00
percent: lvl * 50
2025-03-29 11:24:45 +01:00
}),
// ?
// : t("upgrades.sturdy_bricks.help_plural"),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.sturdy_bricks.verbose_description")
2025-03-19 18:13:41 +01:00
},
{
requires: "",
threshold: 45000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-19 18:13:41 +01:00
id: "respawn",
max: 4,
name: (0, _i18N.t)("upgrades.respawn.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.respawn.tooltip", {
2025-03-29 21:22:19 +01:00
percent: Math.floor(100 * (0, _pureFunctions.comboKeepingRate)(lvl)),
2025-03-29 15:00:44 +01:00
delay: (3 / lvl).toFixed(2)
2025-03-29 11:24:45 +01:00
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.respawn.verbose_description")
2025-03-19 18:13:41 +01:00
},
{
requires: "",
threshold: 50000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-19 18:13:41 +01:00
id: "one_more_choice",
max: 3,
name: (0, _i18N.t)("upgrades.one_more_choice.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.one_more_choice.tooltip", {
2025-03-29 15:00:44 +01:00
lvl
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.one_more_choice.verbose_description")
2025-03-19 18:13:41 +01:00
},
{
requires: "",
threshold: 55000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-19 18:13:41 +01:00
id: "instant_upgrade",
max: 2,
2025-03-27 10:52:31 +01:00
adventure: false,
2025-03-19 18:13:41 +01:00
name: (0, _i18N.t)("upgrades.instant_upgrade.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.instant_upgrade.tooltip", {
2025-03-29 15:00:44 +01:00
lvl
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.instant_upgrade.verbose_description")
2025-03-19 18:13:41 +01:00
},
{
requires: "",
threshold: 60000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-19 18:13:41 +01:00
id: "concave_puck",
max: 1,
name: (0, _i18N.t)("upgrades.concave_puck.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.concave_puck.tooltip"),
fullHelp: (0, _i18N.t)("upgrades.concave_puck.verbose_description")
2025-03-19 20:14:55 +01:00
},
{
requires: "",
threshold: 65000,
2025-04-11 09:36:31 +02:00
gift: 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"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.helium.tooltip"),
fullHelp: (0, _i18N.t)("upgrades.helium.verbose_description")
2025-03-19 20:14:55 +01:00
},
{
requires: "",
threshold: 70000,
2025-04-11 09:36:31 +02:00
gift: true,
2025-03-19 20:14:55 +01:00
id: "asceticism",
max: 1,
name: (0, _i18N.t)("upgrades.asceticism.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.asceticism.tooltip", {
2025-03-29 11:24:45 +01:00
combo: lvl * 3
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.asceticism.verbose_description")
2025-03-19 20:14:55 +01:00
},
{
requires: "",
threshold: 75000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-19 20:14:55 +01:00
id: "unbounded",
2025-04-11 20:34:11 +02:00
max: 3,
2025-03-19 20:14:55 +01:00
name: (0, _i18N.t)("upgrades.unbounded.name"),
2025-04-11 20:34:11 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.unbounded.tooltip", {
2025-03-29 11:24:45 +01:00
lvl
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.unbounded.verbose_description")
2025-03-19 20:14:55 +01:00
},
{
requires: "",
threshold: 80000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-19 20:14:55 +01:00
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-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.shunt.tooltip", {
2025-03-29 21:22:19 +01:00
percent: Math.round((0, _pureFunctions.comboKeepingRate)(lvl) * 100)
2025-03-20 08:13:17 +01:00
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.shunt.verbose_description")
2025-03-19 20:14:55 +01:00
},
{
requires: "",
threshold: 85000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-19 20:14:55 +01:00
id: "yoyo",
2025-03-30 21:07:58 +02:00
max: 1,
2025-03-19 20:14:55 +01:00
name: (0, _i18N.t)("upgrades.yoyo.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.yoyo.tooltip"),
fullHelp: (0, _i18N.t)("upgrades.yoyo.verbose_description")
2025-03-19 20:14:55 +01:00
},
{
requires: "",
threshold: 90000,
2025-04-11 09:36:31 +02:00
gift: true,
2025-03-19 20:14:55 +01:00
id: "nbricks",
max: 3,
name: (0, _i18N.t)("upgrades.nbricks.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.nbricks.tooltip", {
2025-03-19 20:14:55 +01:00
lvl
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.nbricks.verbose_description")
2025-03-19 20:14:55 +01:00
},
{
requires: "",
threshold: 95000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-19 20:14:55 +01:00
id: "etherealcoins",
max: 1,
name: (0, _i18N.t)("upgrades.etherealcoins.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.etherealcoins.tooltip"),
fullHelp: (0, _i18N.t)("upgrades.etherealcoins.verbose_description")
2025-03-19 21:58:08 +01:00
},
{
requires: "multiball",
threshold: 100000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-19 21:58:08 +01:00
id: "shocks",
max: 1,
name: (0, _i18N.t)("upgrades.shocks.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.shocks.tooltip"),
fullHelp: (0, _i18N.t)("upgrades.shocks.verbose_description")
2025-03-19 21:58:08 +01:00
},
{
requires: "",
threshold: 105000,
2025-04-11 09:36:31 +02:00
gift: true,
2025-03-19 21:58:08 +01:00
id: "zen",
max: 1,
name: (0, _i18N.t)("upgrades.zen.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.zen.tooltip", {
2025-03-29 15:00:44 +01:00
lvl
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.zen.verbose_description")
2025-03-19 21:58:08 +01:00
},
{
requires: "extra_life",
threshold: 110000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-19 21:58:08 +01:00
id: "sacrifice",
max: 1,
name: (0, _i18N.t)("upgrades.sacrifice.name"),
2025-03-29 15:00:44 +01:00
help: (lvl)=>lvl == 1 ? (0, _i18N.t)("upgrades.sacrifice.help_l1") : (0, _i18N.t)("upgrades.sacrifice.help_over", {
lvl
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.sacrifice.verbose_description")
2025-03-19 21:58:08 +01:00
},
{
requires: "",
threshold: 115000,
2025-04-11 09:36:31 +02:00
gift: 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"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.trampoline.tooltip", {
2025-03-19 21:58:08 +01:00
lvl
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.trampoline.verbose_description")
2025-03-19 21:58:08 +01:00
},
{
requires: "",
threshold: 120000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-19 21:58:08 +01:00
id: "ghost_coins",
2025-04-08 08:57:41 +02:00
max: 3,
2025-03-19 21:58:08 +01:00
name: (0, _i18N.t)("upgrades.ghost_coins.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.ghost_coins.tooltip", {
2025-03-19 21:58:08 +01:00
lvl
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.ghost_coins.verbose_description")
2025-03-19 21:58:08 +01:00
},
{
requires: "",
threshold: 125000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-19 21:58:08 +01:00
id: "forgiving",
max: 1,
name: (0, _i18N.t)("upgrades.forgiving.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.forgiving.tooltip"),
fullHelp: (0, _i18N.t)("upgrades.forgiving.verbose_description")
2025-03-19 21:58:08 +01:00
},
{
requires: "",
threshold: 130000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-19 21:58:08 +01:00
id: "ball_attracts_coins",
max: 3,
name: (0, _i18N.t)("upgrades.ball_attracts_coins.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.ball_attracts_coins.tooltip"),
fullHelp: (0, _i18N.t)("upgrades.ball_attracts_coins.verbose_description")
2025-03-20 21:02:51 +01:00
},
{
requires: "",
threshold: 135000,
2025-03-28 10:21:14 +01:00
// a bit too hard when starting up
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-20 21:02:51 +01:00
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"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.reach.tooltip", {
2025-03-20 21:02:51 +01:00
lvl
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.reach.verbose_description")
2025-03-22 16:04:25 +01:00
},
{
requires: "",
threshold: 140000,
2025-04-11 09:36:31 +02:00
gift: true,
2025-03-22 16:04:25 +01:00
id: "passive_income",
2025-03-24 10:19:15 +01:00
max: 4,
2025-03-22 16:04:25 +01:00
name: (0, _i18N.t)("upgrades.passive_income.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.passive_income.tooltip", {
2025-03-25 08:22:58 +01:00
time: lvl * 0.25,
2025-03-24 10:19:15 +01:00
lvl
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.passive_income.verbose_description")
2025-03-23 19:11:01 +01:00
},
{
requires: "",
threshold: 145000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-23 19:11:01 +01:00
id: "clairvoyant",
2025-04-04 09:45:35 +02:00
max: 1,
2025-03-23 19:11:01 +01:00
name: (0, _i18N.t)("upgrades.clairvoyant.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.clairvoyant.tooltip"),
fullHelp: (0, _i18N.t)("upgrades.clairvoyant.verbose_description")
2025-03-23 22:19:11 +01:00
},
{
requires: "",
threshold: 150000,
2025-04-11 09:36:31 +02:00
gift: true,
2025-03-23 22:19:11 +01:00
id: "side_kick",
max: 3,
name: (0, _i18N.t)("upgrades.side_kick.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.side_kick.tooltip", {
2025-04-06 18:21:53 +02:00
lvl,
loss: lvl * 2
2025-03-23 22:19:11 +01:00
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.side_kick.verbose_description")
2025-03-23 22:19:11 +01:00
},
2025-04-08 08:57:41 +02:00
{
requires: "",
threshold: 150000,
2025-04-11 09:36:31 +02:00
gift: true,
2025-04-08 08:57:41 +02:00
id: "side_flip",
max: 3,
name: (0, _i18N.t)("upgrades.side_flip.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.side_flip.tooltip", {
2025-04-08 08:57:41 +02:00
lvl,
loss: lvl * 2
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.side_flip.verbose_description")
2025-04-08 08:57:41 +02:00
},
2025-03-23 22:19:11 +01:00
{
requires: "",
threshold: 155000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-23 22:19:11 +01:00
id: "implosions",
max: 1,
name: (0, _i18N.t)("upgrades.implosions.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.implosions.tooltip"),
fullHelp: (0, _i18N.t)("upgrades.implosions.verbose_description")
2025-03-23 22:19:11 +01:00
},
{
requires: "",
threshold: 160000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-23 22:19:11 +01:00
id: "corner_shot",
max: 1,
name: (0, _i18N.t)("upgrades.corner_shot.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.corner_shot.tooltip"),
fullHelp: (0, _i18N.t)("upgrades.corner_shot.verbose_description")
2025-03-30 21:07:58 +02:00
},
{
requires: "",
threshold: 165000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-03-30 21:07:58 +02:00
id: "addiction",
2025-03-31 13:33:27 +02:00
max: 7,
2025-03-30 21:07:58 +02:00
name: (0, _i18N.t)("upgrades.addiction.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.addiction.tooltip", {
2025-03-30 21:07:58 +02:00
lvl,
delay: (5 / lvl).toFixed(2)
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.addiction.verbose_description")
2025-04-03 21:59:01 +02:00
},
{
requires: "",
threshold: 170000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-04-03 21:59:01 +02:00
id: "fountain_toss",
max: 7,
name: (0, _i18N.t)("upgrades.fountain_toss.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.fountain_toss.tooltip", {
2025-04-03 21:59:01 +02:00
lvl,
max: lvl * 30
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.fountain_toss.verbose_description")
2025-04-06 15:38:30 +02:00
},
{
requires: "",
threshold: 175000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-04-06 15:38:30 +02:00
id: "limitless",
max: 1,
name: (0, _i18N.t)("upgrades.limitless.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.limitless.tooltip", {
2025-04-06 15:38:30 +02:00
lvl
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.limitless.verbose_description")
2025-04-08 08:57:41 +02:00
},
{
requires: "",
threshold: 180000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-04-08 08:57:41 +02:00
id: "minefield",
max: 3,
name: (0, _i18N.t)("upgrades.minefield.name"),
2025-04-09 11:28:32 +02:00
help: (lvl)=>(0, _i18N.t)("upgrades.minefield.tooltip", {
2025-04-08 08:57:41 +02:00
lvl
}),
2025-04-09 11:28:32 +02:00
fullHelp: (0, _i18N.t)("upgrades.minefield.verbose_description")
2025-04-10 14:49:28 +02:00
},
{
requires: "",
threshold: 185000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-04-10 14:49:28 +02:00
id: "trickledown",
max: 1,
name: (0, _i18N.t)("upgrades.trickledown.name"),
help: (lvl)=>(0, _i18N.t)("upgrades.trickledown.tooltip", {
lvl
}),
fullHelp: (0, _i18N.t)("upgrades.trickledown.verbose_description")
},
{
requires: "",
threshold: 190000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-04-10 14:49:28 +02:00
id: "transparency",
max: 3,
name: (0, _i18N.t)("upgrades.transparency.name"),
help: (lvl)=>(0, _i18N.t)("upgrades.transparency.tooltip", {
2025-04-18 21:17:32 +02:00
lvl,
percent: lvl * 50
2025-04-10 14:49:28 +02:00
}),
fullHelp: (0, _i18N.t)("upgrades.transparency.verbose_description")
2025-04-10 15:27:38 +02:00
},
{
requires: "",
threshold: 195000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-04-10 15:27:38 +02:00
id: "superhot",
max: 3,
name: (0, _i18N.t)("upgrades.superhot.name"),
help: (lvl)=>(0, _i18N.t)("upgrades.superhot.tooltip", {
lvl
}),
fullHelp: (0, _i18N.t)("upgrades.superhot.verbose_description")
2025-04-10 21:40:45 +02:00
},
{
requires: "",
threshold: 200000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-04-10 21:40:45 +02:00
id: "bricks_attract_coins",
max: 3,
name: (0, _i18N.t)("upgrades.bricks_attract_coins.name"),
help: (lvl)=>(0, _i18N.t)("upgrades.bricks_attract_coins.tooltip", {
lvl
}),
fullHelp: (0, _i18N.t)("upgrades.bricks_attract_coins.verbose_description")
},
{
requires: "",
threshold: 205000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-04-10 21:40:45 +02:00
id: "rainbow",
max: 7,
name: (0, _i18N.t)("upgrades.rainbow.name"),
help: (lvl)=>(0, _i18N.t)("upgrades.rainbow.tooltip", {
lvl
}),
fullHelp: (0, _i18N.t)("upgrades.rainbow.verbose_description")
},
{
requires: "metamorphosis",
threshold: 210000,
2025-04-11 09:36:31 +02:00
gift: false,
2025-04-10 21:40:45 +02:00
id: "hypnosis",
max: 1,
name: (0, _i18N.t)("upgrades.hypnosis.name"),
help: (lvl)=>(0, _i18N.t)("upgrades.hypnosis.tooltip", {
lvl
}),
fullHelp: (0, _i18N.t)("upgrades.hypnosis.verbose_description")
2025-04-11 20:34:11 +02:00
},
{
requires: "",
threshold: 215000,
gift: false,
id: "bricks_attract_ball",
2025-04-15 21:25:27 +02:00
max: 1,
2025-04-11 20:34:11 +02:00
name: (0, _i18N.t)("upgrades.bricks_attract_ball.name"),
help: (lvl)=>(0, _i18N.t)("upgrades.bricks_attract_ball.tooltip", {
count: lvl * 3
}),
fullHelp: (0, _i18N.t)("upgrades.bricks_attract_ball.verbose_description")
2025-03-19 18:13:41 +01:00
}
];
2025-03-31 20:08:17 +02:00
},{"./i18n/i18n":"eNPRm","./pure_functions":"6pQh7","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"eNPRm":[function(require,module,exports,__globalThis) {
2025-03-19 18:13:41 +01:00
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
2025-04-09 11:28:32 +02:00
parcelHelpers.export(exports, "languages", ()=>languages);
2025-03-19 18:13:41 +01:00
parcelHelpers.export(exports, "getCurrentLang", ()=>getCurrentLang);
parcelHelpers.export(exports, "t", ()=>t);
var _enJson = require("./en.json");
var _enJsonDefault = parcelHelpers.interopDefault(_enJson);
2025-04-09 11:28:32 +02:00
var _frJson = require("./fr.json");
var _frJsonDefault = parcelHelpers.interopDefault(_frJson);
2025-04-12 09:24:07 +02:00
var _arJson = require("./ar.json");
var _arJsonDefault = parcelHelpers.interopDefault(_arJson);
2025-04-12 15:05:23 +02:00
var _ruJson = require("./ru.json");
var _ruJsonDefault = parcelHelpers.interopDefault(_ruJson);
var _esJson = require("./es.json");
var _esJsonDefault = parcelHelpers.interopDefault(_esJson);
2025-04-13 09:58:09 +02:00
var _trJson = require("./tr.json");
var _trJsonDefault = parcelHelpers.interopDefault(_trJson);
var _deJson = require("./de.json");
var _deJsonDefault = parcelHelpers.interopDefault(_deJson);
2025-03-19 18:13:41 +01:00
var _settings = require("../settings");
2025-04-09 11:28:32 +02:00
const languages = [
{
text: "English",
value: "en",
strings: (0, _enJsonDefault.default),
levelName: "UK"
},
{
text: "Fran\xe7ais",
value: "fr",
strings: (0, _frJsonDefault.default),
levelName: "France"
2025-04-12 09:24:07 +02:00
},
{
text: "\u0639\u0631\u0628\u064A",
value: "ar",
strings: (0, _arJsonDefault.default),
levelName: "Lebanon"
2025-04-12 15:05:23 +02:00
},
{
text: "Espa\xf1ol",
value: "es",
strings: (0, _esJsonDefault.default),
levelName: "Chile"
},
{
text: "\u0420\u0443\u0441\u0441\u043A\u0438\u0439",
value: "ru",
strings: (0, _ruJsonDefault.default),
levelName: "Russia"
2025-04-13 09:58:09 +02:00
},
{
text: "Deutsch",
value: "de",
strings: (0, _deJsonDefault.default),
levelName: "Germany"
},
{
text: "T\xfcrk\xe7e",
value: "tr",
strings: (0, _trJsonDefault.default),
levelName: "T\xfcrkiye"
2025-04-09 11:28:32 +02:00
}
];
const languagesMap = {};
languages.forEach((l)=>languagesMap[l.value] = l.strings);
2025-04-10 14:49:28 +02:00
let defaultLang = [
...navigator.languages,
navigator.language
].filter((i)=>i).map((i)=>i.slice(0, 2).toLowerCase()).find((k)=>k in languagesMap) || "en";
2025-03-19 18:13:41 +01:00
function getCurrentLang() {
2025-04-10 14:49:28 +02:00
return (0, _settings.getSettingValue)("lang", defaultLang);
2025-03-19 18:13:41 +01:00
}
function t(key, params = {}) {
const lang = getCurrentLang();
2025-04-09 11:28:32 +02:00
let template = languagesMap[lang]?.[key] || languagesMap.en[key];
2025-03-19 18:13:41 +01:00
for(let key in params)template = template.split("{{" + key + "}}").join(`${params[key]}`);
return template;
}
2025-04-15 16:47:04 +02:00
},{"./en.json":"uYc9N","./fr.json":"b97sx","./ar.json":"aDOut","./ru.json":"eedRO","./es.json":"hATkf","./tr.json":"l4sLF","./de.json":"1l6Zs","../settings":"5blfu","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"uYc9N":[function(require,module,exports,__globalThis) {
2025-04-20 09:33:12 +02:00
module.exports = JSON.parse("{\"confirmRestart.no\":\"Cancel\",\"confirmRestart.text\":\"You're about to start a new game. Are you sure you want to continue?\",\"confirmRestart.title\":\"Start a new game?\",\"confirmRestart.yes\":\"Restart game\",\"editor.editing.bigger\":\"Increase level size\",\"editor.editing.color\":\"Pick a color in the color list (max 5 per level)\",\"editor.editing.copy\":\"Copy level code\",\"editor.editing.copy_help\":\"Paste it in the #levels channel in our discord\",\"editor.editing.credit\":\"Credits and source\",\"editor.editing.credit_prompt\":\"Enter the source url or explanation of your level.\",\"editor.editing.delete\":\"Delete level\",\"editor.editing.down\":\"Move down all the bricks\",\"editor.editing.help\":\"Then click a tile to color it.\",\"editor.editing.left\":\"Move all bricks to the left\",\"editor.editing.play\":\"Play this level\",\"editor.editing.rename\":\"Level name\",\"editor.editing.rename_prompt\":\"Please enter a new name for the level\",\"editor.editing.right\":\"Move all bricks to the right\",\"editor.editing.smaller\":\"Decrease level size\",\"editor.editing.title\":\"Editing level : {{name}}\",\"editor.editing.up\":\"Move up all the bricks\",\"editor.help\":\"Create custom levels and share them for inclusion in the game.\",\"editor.import\":\"Import a level\",\"editor.import_instruction\":\"Paste a level code to import it in your level list\",\"editor.locked\":\"Reach a total score of {{min}} to unlock\",\"editor.new_level\":\"New level\",\"editor.title\":\"Level Editor\",\"gameOver.creative\":\"This run will not be recorded. \",\"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.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\":\"\",\"gameOver.stats.level_reached\":\"Level reached\",\"gameOver.stats.total_score\":\"Total score\",\"gameOver.stats.upgrades_applied\":\"Upgrades applied\",\"gameOver.stats_intro\":\"Find below your game statistics compared to your {{count}} best games.\",\"gameOver.unlocked_perk\":\"Upgrade unlocked\",\"gameOver.unlocked_perk_plural\":\"You just unlocked {{count}} perks\",\"gameOver.win.summary\":\"This game is over. You stashed {{score}} coins. \",\"gameOver.win.title\":\"You completed this game\",\"help.content\":\"## Goal\\n\\nCatch as many coins as possible during 7 levels. \\nCoins appear when you break bricks.\\nCatch them with your paddle to increase your score.\\nYour score is displayed in the top right corner of the screen.\\nDon't drop the ball or it's game over.\\n\\nAfter destroying all bricks, you'll get to pick an upgrade.\\n\\n## Upgrades \\n\\nThe upgrades you pick will apply until the end of the run. \\nSome can be picked multiple times for stronger effect.\\nSome help with aiming, or make the game easier in some other ways. \\nSome are only useful when combined.\\n\\nYou always get one upgrade at the beginning of each game. \\nIts icon will serve as the bricks of the first level. \\nYou can select starting upgrades in the settings.\\n\\nMany upgrades impact your combo. \\n\\n## Combo\\n\\nYour \\\"combo\\\" is the number of coins spawned when a brick breaks. \\nIt is displayed on your paddle, for example x4 means each brick will spawn 4 coins. \\nMost upgrades that increase the combo also add a condition to reset it. \\nThe combo will also reset if the ball returns to the paddle without hitting any brick.\\nA \\\"miss\\\" message will be shown when that happens. \\n\\nTry to aim towards a brick every time. \\n\\n## Aiming\\n\\nOnly the ball position on the paddle decides how it will
2025-04-15 16:47:04 +02:00
},{}],"b97sx":[function(require,module,exports,__globalThis) {
2025-04-20 09:33:12 +02:00
module.exports = JSON.parse('{"confirmRestart.no":"Annuler","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\u202F?","confirmRestart.yes":"Commencer une nouvelle partie","editor.editing.bigger":"Augmenter la taille du niveau","editor.editing.color":"Choisissez une couleur dans la liste des couleurs (max 5 par niveau)","editor.editing.copy":"Copier le code du niveau","editor.editing.copy_help":"Collez-le dans le canal #levels de notre discord","editor.editing.credit":"Cr\xe9dits et source","editor.editing.credit_prompt":"Entrez l\'url source ou l\'explication de votre niveau.","editor.editing.delete":"Supprimer le niveau","editor.editing.down":"D\xe9placez toutes les briques vers le bas","editor.editing.help":"Cliquez ensuite sur une tuile pour la colorier.","editor.editing.left":"D\xe9placer toutes les briques vers la gauche","editor.editing.play":"Jouez \xe0 ce niveau","editor.editing.rename":"Nom du niveau","editor.editing.rename_prompt":"Veuillez saisir un nouveau nom pour le niveau","editor.editing.right":"D\xe9placer toutes les briques vers la droite","editor.editing.smaller":"Diminuer la taille du niveau","editor.editing.title":"Niveau d\'\xe9dition\xa0: {{name}}","editor.editing.up":"D\xe9placez toutes les briques","editor.help":"Cr\xe9ez des niveaux personnalis\xe9s et partagez-les pour les inclure dans le jeu.","editor.import":"Importer un niveau","editor.import_instruction":"Collez un code de niveau pour l\'importer dans votre liste de niveaux","editor.locked":"Atteignez un score total de {{min}} pour d\xe9bloquer","editor.new_level":"Nouveau niveau","editor.title":"\xc9diteur de niveau","gameOver.creative":"Cette partie de test ne sera pas enregistr\xe9e.","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.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":"","gameOver.stats.level_reached":"Niveau atteint","gameOver.stats.total_score":"Score total","gameOver.stats.upgrades_applied":"Am\xe9liorations appliqu\xe9es","gameOver.stats_intro":"Vous trouverez ci-dessous les statistiques de cette partie compar\xe9es \xe0 vos {{count}} meilleures parties.","gameOver.unlocked_perk":"Am\xe9lioration d\xe9bloqu\xe9e","gameOver.unlocked_perk_plural":"Vous avez d\xe9bloqu\xe9 {{count}} am\xe9liorations","gameOver.win.summary":"Cette partie est termin\xe9e. Vous avez accumul\xe9 {{score}} pi\xe8ces. ","gameOver.win.title":"Vous avez termin\xe9 cette partie","help.content":"## Objectif\\n\\nAttrapez un maximum de pi\xe8ces au cours des 7 niveaux.\\nLes pi\xe8ces apparaissent lorsque vous cassez des briques.\\nAttrapez-les avec votre palet pour augmenter votre score.\\nVotre score est affich\xe9 en haut \xe0 droite de l\'\xe9cran.\\nNe laissez pas tomber la balle, sinon la partie est termin\xe9e.\\n\\nApr\xe8s avoir d\xe9truit toutes les briques, vous pourrez choisir une am\xe9lioration.\\n\\n## Am\xe9liorations\\n\\nLes am\xe9liorations que vous choisissez seront valables jusqu\'\xe0 la fin de la partie.\\nCertaines peuvent \xeatre s\xe9lectionn\xe9es plusieurs fois pour un effet plus puissant.\\nD\'autres aident \xe0 viser ou simplifient le jeu.\\nCertaines ne sont utiles que lorsqu\'elles sont combin\xe9es.\\n\\nVous obtenez toujours une am\xe9lioration au d\xe9but de chaque partie.\\nSon ic\xf4ne forme les briques du premier niveau.\\nVous pouvez s\xe9lectionner les am\xe9liorations de d\xe9part dans les param\xe8tres.\\n\\nDe nombreuses am\x
2025-04-15 16:47:04 +02:00
},{}],"aDOut":[function(require,module,exports,__globalThis) {
2025-04-20 09:33:12 +02:00
module.exports = JSON.parse('{"confirmRestart.no":"\u064A\u0644\u063A\u064A","confirmRestart.text":"\u0623\u0646\u062A \u0639\u0644\u0649 \u0648\u0634\u0643 \u0628\u062F\u0621 \u0644\u0639\u0628\u0629 \u062C\u062F\u064A\u062F\u0629. \u0647\u0644 \u0623\u0646\u062A \u0645\u062A\u0623\u0643\u062F \u0645\u0646 \u0631\u063A\u0628\u062A\u0643 \u0641\u064A \u0627\u0644\u0645\u062A\u0627\u0628\u0639\u0629\u061F","confirmRestart.title":"\u0628\u062F\u0621 \u0644\u0639\u0628\u0629 \u062C\u062F\u064A\u062F\u0629\u061F","confirmRestart.yes":"\u0625\u0639\u0627\u062F\u0629 \u062A\u0634\u063A\u064A\u0644 \u0627\u0644\u0644\u0639\u0628\u0629","editor.editing.bigger":"\u0632\u064A\u0627\u062F\u0629 \u062D\u062C\u0645 \u0627\u0644\u0645\u0633\u062A\u0648\u0649","editor.editing.color":"\u0627\u062E\u062A\u0631 \u0644\u0648\u0646\u064B\u0627 \u0645\u0646 \u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0623\u0644\u0648\u0627\u0646 (\u0628\u062D\u062F \u0623\u0642\u0635\u0649 5 \u0644\u0643\u0644 \u0645\u0633\u062A\u0648\u0649)","editor.editing.copy":"\u0646\u0633\u062E \u0631\u0645\u0632 \u0627\u0644\u0645\u0633\u062A\u0648\u0649","editor.editing.copy_help":"\u0623\u0644\u0635\u0642\u0647 \u0641\u064A \u0642\u0646\u0627\u0629 #levels \u0641\u064A Discord \u0627\u0644\u062E\u0627\u0635 \u0628\u0646\u0627","editor.editing.credit":"\u0627\u0644\u0627\u0639\u062A\u0645\u0627\u062F\u0627\u062A \u0648\u0627\u0644\u0645\u0635\u062F\u0631","editor.editing.credit_prompt":"\u0623\u062F\u062E\u0644 \u0639\u0646\u0648\u0627\u0646 URL \u0627\u0644\u0645\u0635\u062F\u0631 \u0623\u0648 \u0634\u0631\u062D\u064B\u0627 \u0644\u0645\u0633\u062A\u0648\u0627\u0643.","editor.editing.delete":"\u062D\u0630\u0641 \u0627\u0644\u0645\u0633\u062A\u0648\u0649","editor.editing.down":"\u0627\u0646\u0632\u0644 \u0643\u0644 \u0627\u0644\u0637\u0648\u0628 \u0625\u0644\u0649 \u0627\u0644\u0623\u0633\u0641\u0644","editor.editing.help":"\u062B\u0645 \u0627\u0646\u0642\u0631 \u0639\u0644\u0649 \u0627\u0644\u0628\u0644\u0627\u0637 \u0644\u062A\u0644\u0648\u064A\u0646\u0647.","editor.editing.left":"\u0646\u0642\u0644 \u062C\u0645\u064A\u0639 \u0627\u0644\u0637\u0648\u0628 \u0625\u0644\u0649 \u0627\u0644\u064A\u0633\u0627\u0631","editor.editing.play":"\u0627\u0644\u0639\u0628 \u0647\u0630\u0627 \u0627\u0644\u0645\u0633\u062A\u0648\u0649","editor.editing.rename":"\u0627\u0633\u0645 \u0627\u0644\u0645\u0633\u062A\u0648\u0649","editor.editing.rename_prompt":"\u0627\u0644\u0631\u062C\u0627\u0621 \u0625\u062F\u062E\u0627\u0644 \u0627\u0633\u0645 \u062C\u062F\u064A\u062F \u0644\u0644\u0645\u0633\u062A\u0648\u0649","editor.editing.right":"\u062D\u0631\u0643 \u0643\u0644 \u0627\u0644\u0637\u0648\u0628 \u0625\u0644\u0649 \u0627\u0644\u064A\u0645\u064A\u0646","editor.editing.smaller":"\u062A\u0642\u0644\u064A\u0644 \u062D\u062C\u0645 \u0627\u0644\u0645\u0633\u062A\u0648\u0649","editor.editing.title":"\u0645\u0633\u062A\u0648\u0649 \u0627\u0644\u062A\u062D\u0631\u064A\u0631: {{name}}","editor.editing.up":"\u062D\u0631\u0643 \u0643\u0644 \u0627\u0644\u0637\u0648\u0628 \u0644\u0623\u0639\u0644\u0649","editor.help":"\u0625\u0646\u0634\u0627\u0621 \u0645\u0633\u062A\u0648\u064A\u0627\u062A \u0645\u062E\u0635\u0635\u0629 \u0648\u0645\u0634\u0627\u0631\u0643\u062A\u0647\u0627 \u0644\u062A\u0636\u0645\u064A\u0646\u0647\u0627 \u0641\u064A \u0627\u0644\u0644\u0639\u0628\u0629.","editor.import":"\u0627\u0633\u062A\u064A\u0631\u0627\u062F \u0627\u0644\u0645\u0633\u062A\u0648\u0649","editor.import_instruction":"\u0627\u0644\u0635\u0642 \u0631\u0645\u0632 \u0627\u0644\u0645\u0633\u062A\u0648\u0649 \u0644\u0627\u0633\u062A\u064A\u0631\u0627\u062F\u0647 \u0641\u064A \u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0645\u0633\u062A\u0648\u064A\u0627\u062A \u0627\u0644\u062E\u0627\u0635\u0629 \u0628\u0643","editor.locked":"\u0627\u062D\u0635\u0644 \u0639\u0644\u0649 \u0645\u062C\u0645\u0648\u0639 \u0646\u0642\u0627\u0637 \u0642\u062F\u0631\u0647 {{min}} \u0644\u0641\u062A\u062D \u0627\u0644\u0642\u0641\u0644","editor.new_level":"\u0645\u0633\u062A\u0648\u0649 \u062C\u062F\u064A\u062F","editor.title":"\u0645\u062D
2025-04-15 16:47:04 +02:00
},{}],"eedRO":[function(require,module,exports,__globalThis) {
2025-04-20 09:33:12 +02:00
module.exports = JSON.parse('{"confirmRestart.no":"\u041E\u0442\u043C\u0435\u043D\u0430","confirmRestart.text":"\u0412\u044B \u0441\u043E\u0431\u0438\u0440\u0430\u0435\u0442\u0435\u0441\u044C \u043D\u0430\u0447\u0430\u0442\u044C \u043D\u043E\u0432\u0443\u044E \u0438\u0433\u0440\u0443. \u0412\u044B \u0443\u0432\u0435\u0440\u0435\u043D\u044B, \u0447\u0442\u043E \u0445\u043E\u0442\u0438\u0442\u0435 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C?","confirmRestart.title":"\u041D\u0430\u0447\u0430\u0442\u044C \u043D\u043E\u0432\u0443\u044E \u0438\u0433\u0440\u0443?","confirmRestart.yes":"\u041F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u0435 \u0438\u0433\u0440\u0443","editor.editing.bigger":"\u0423\u0432\u0435\u043B\u0438\u0447\u0438\u0442\u044C \u0440\u0430\u0437\u043C\u0435\u0440 \u0443\u0440\u043E\u0432\u043D\u044F","editor.editing.color":"\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0446\u0432\u0435\u0442 \u0438\u0437 \u0441\u043F\u0438\u0441\u043A\u0430 \u0446\u0432\u0435\u0442\u043E\u0432 (\u043C\u0430\u043A\u0441\u0438\u043C\u0443\u043C 5 \u043D\u0430 \u0443\u0440\u043E\u0432\u0435\u043D\u044C)","editor.editing.copy":"\u0421\u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043A\u043E\u0434 \u0443\u0440\u043E\u0432\u043D\u044F","editor.editing.copy_help":"\u0412\u0441\u0442\u0430\u0432\u044C\u0442\u0435 \u0435\u0433\u043E \u0432 \u043A\u0430\u043D\u0430\u043B #levels \u0432 \u043D\u0430\u0448\u0435\u043C Discord","editor.editing.credit":"\u041A\u0440\u0435\u0434\u0438\u0442\u044B \u0438 \u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A","editor.editing.credit_prompt":"\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u0441\u0445\u043E\u0434\u043D\u044B\u0439 URL-\u0430\u0434\u0440\u0435\u0441 \u0438\u043B\u0438 \u043F\u043E\u044F\u0441\u043D\u0435\u043D\u0438\u0435 \u0432\u0430\u0448\u0435\u0433\u043E \u0443\u0440\u043E\u0432\u043D\u044F.","editor.editing.delete":"\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0443\u0440\u043E\u0432\u0435\u043D\u044C","editor.editing.down":"\u0421\u0434\u0432\u0438\u043D\u044C\u0442\u0435 \u0432\u0441\u0435 \u043A\u0438\u0440\u043F\u0438\u0447\u0438 \u0432\u043D\u0438\u0437.","editor.editing.help":"\u0417\u0430\u0442\u0435\u043C \u0449\u0435\u043B\u043A\u043D\u0438\u0442\u0435 \u043F\u043E \u043F\u043B\u0438\u0442\u043A\u0435, \u0447\u0442\u043E\u0431\u044B \u0440\u0430\u0441\u043A\u0440\u0430\u0441\u0438\u0442\u044C \u0435\u0435.","editor.editing.left":"\u041F\u0435\u0440\u0435\u043C\u0435\u0441\u0442\u0438\u0442\u0435 \u0432\u0441\u0435 \u043A\u0438\u0440\u043F\u0438\u0447\u0438 \u0432\u043B\u0435\u0432\u043E.","editor.editing.play":"\u041F\u0440\u043E\u0439\u0442\u0438 \u044D\u0442\u043E\u0442 \u0443\u0440\u043E\u0432\u0435\u043D\u044C","editor.editing.rename":"\u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 \u0443\u0440\u043E\u0432\u043D\u044F","editor.editing.rename_prompt":"\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043D\u043E\u0432\u043E\u0435 \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 \u0443\u0440\u043E\u0432\u043D\u044F.","editor.editing.right":"\u041F\u0435\u0440\u0435\u043C\u0435\u0441\u0442\u0438\u0442\u0435 \u0432\u0441\u0435 \u043A\u0438\u0440\u043F\u0438\u0447\u0438 \u0432\u043F\u0440\u0430\u0432\u043E.","editor.editing.smaller":"\u0423\u043C\u0435\u043D\u044C\u0448\u0438\u0442\u044C \u0440\u0430\u0437\u043C\u0435\u0440 \u0443\u0440\u043E\u0432\u043D\u044F","editor.editing.title":"\u0423\u0440\u043E\u0432\u0435\u043D\u044C \u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u044F: {{name}}","editor.editing.up":"\u041F\u043E\u0434\u043D\u0438\u043C\u0438\u0442\u0435 \u0432\u0441\u0435 \u043A\u0438\u0440\u043F\u0438\u0447\u0438.","editor.help":"\u0421\u043E\u0437\u0434\u0430\u0432\u0430\u0439\u0442\u0435 \u0441\u043E\u0431\u0441\u0442\u0432\u0435\u043D\u043D\u044B\u0435 \u0443\u0440\u043E\u0432\u043D\u0438 \u0438 \u0434\u0435\u043B\u0438\u0442\u0435\u0441\u044C \u0438\u043C\u0438 \u0434\u043B\u044F \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u044F \u0432 \u0438\u0433\u0440\u0443."
2025-03-19 18:13:41 +01:00
2025-04-15 16:47:04 +02:00
},{}],"hATkf":[function(require,module,exports,__globalThis) {
2025-04-20 09:33:12 +02:00
module.exports = JSON.parse('{"confirmRestart.no":"Cancelar","confirmRestart.text":"Est\xe1s a punto de empezar un nuevo partido: \xbfes esto realmente lo que quer\xedas?","confirmRestart.title":"\xbfEmpezar una nueva partida?","confirmRestart.yes":"Empezar una nueva partida","editor.editing.bigger":"Aumentar el tama\xf1o del nivel","editor.editing.color":"Elige un color de la lista de colores (m\xe1ximo 5 por nivel)","editor.editing.copy":"Copiar c\xf3digo de nivel","editor.editing.copy_help":"P\xe9galo en el canal #levels en nuestro discord","editor.editing.credit":"Cr\xe9ditos y fuente","editor.editing.credit_prompt":"Introduce la URL de origen o la explicaci\xf3n de tu nivel.","editor.editing.delete":"Eliminar nivel","editor.editing.down":"Baja todos los ladrillos","editor.editing.help":"Luego haz clic en un mosaico para colorearlo.","editor.editing.left":"Mueve todos los ladrillos hacia la izquierda","editor.editing.play":"Juega este nivel","editor.editing.rename":"Nombre del nivel","editor.editing.rename_prompt":"Por favor, introduzca un nuevo nombre para el nivel","editor.editing.right":"Mueve todos los ladrillos hacia la derecha","editor.editing.smaller":"Disminuir el tama\xf1o del nivel","editor.editing.title":"Nivel de edici\xf3n: {{name}}","editor.editing.up":"Mueve todos los ladrillos hacia arriba","editor.help":"Crea niveles personalizados y comp\xe1rtelos para incluirlos en el juego.","editor.import":"Importar un nivel","editor.import_instruction":"Pegue un c\xf3digo de nivel para importarlo en su lista de niveles","editor.locked":"Alcanza una puntuaci\xf3n total de {{min}} para desbloquear","editor.new_level":"Nuevo nivel","editor.title":"Editor de niveles","gameOver.creative":"Esta parte de la prueba no se grabar\xe1.","gameOver.cumulative_total":"Su puntuaci\xf3n total acumulada ha pasado de {{startTs}} a {{endTs}}.","gameOver.lost.summary":"Se te ha ca\xeddo la bola despu\xe9s de coger {{score}} monedas.","gameOver.lost.title":"Pelota perdida","gameOver.stats.balls_lost":"Balas perdidas","gameOver.stats.bricks_broken":"Ladrillos rotos","gameOver.stats.bricks_per_minute":"Ladrillos rotos por minuto","gameOver.stats.catch_rate":"Monedas atrapadas","gameOver.stats.combo_avg":"Combo medio","gameOver.stats.combo_max":"Combinaci\xf3n m\xe1xima","gameOver.stats.duration_per_level":"Duraci\xf3n por nivel","gameOver.stats.hit_rate":"Precisi\xf3n","gameOver.stats.intro":"A continuaci\xf3n se muestran las estad\xedsticas de este juego en comparaci\xf3n con sus {{count}} mejores juegos.","gameOver.stats.level_reached":"Nivel alcanzado","gameOver.stats.total_score":"Puntuaci\xf3n total","gameOver.stats.upgrades_applied":"Mejoras aplicadas","gameOver.stats_intro":"Encuentra a continuaci\xf3n tus estad\xedsticas de juego comparadas con tus {{count}} mejores juegos.","gameOver.unlocked_perk":"Actualizaci\xf3n desbloqueada","gameOver.unlocked_perk_plural":"Has desbloqueado {{count}} mejoras","gameOver.win.summary":"Este juego ha terminado. Has acumulado {{score}} monedas.","gameOver.win.title":"Ha completado esta parte","help.content":"## Objetivo\\n\\nAtrapa tantas monedas como puedas durante 7 niveles.\\nLas monedas aparecen al romper ladrillos.\\nAtr\xe1palas con tu pala para aumentar tu puntuaci\xf3n.\\nTu puntuaci\xf3n se muestra en la esquina superior derecha de la pantalla.\\nNo dejes caer la bola o se acabar\xe1 la partida.\\n\\nDespu\xe9s de destruir todos los ladrillos, podr\xe1s elegir una mejora.\\n\\n## Mejoras\\n\\nLas mejoras que elijas se aplicar\xe1n hasta el final de la partida.\\n\\nAlgunas se pueden elegir varias veces para un efecto m\xe1s potente.\\nAlgunas ayudan a apuntar o facilitan el juego de otras maneras.\\n\\nAlgunas solo son \xfatiles al combinarlas.\\n\\nSiempre obtienes una mejora al principio de cada partida.\\n\\nSu icono servir\xe1 como los ladrillos del primer nivel.\\n\\nPuedes seleccionar las mejoras iniciales en la configuraci\xf3n.\\n\\nMuchas mejoras afectan a tu combo.\\n\\n## Combo\\n\\nTu \\"combo\\" es la cantidad de monedas que se generan al romper un ladrillo. Se muestra
2025-04-15 16:47:04 +02:00
},{}],"l4sLF":[function(require,module,exports,__globalThis) {
2025-04-20 09:33:12 +02:00
module.exports = JSON.parse('{"confirmRestart.no":"\u0130ptal etmek","confirmRestart.text":"Yeni bir oyuna ba\u015Flamak \xfczeresiniz. Devam etmek istedi\u011Finizden emin misiniz?","confirmRestart.title":"Yeni bir oyuna m\u0131 ba\u015Flasam?","confirmRestart.yes":"Oyunu yeniden ba\u015Flat","editor.editing.bigger":"Seviye boyutunu art\u0131r","editor.editing.color":"Renk listesinden bir renk se\xe7in (seviye ba\u015F\u0131na en fazla 5)","editor.editing.copy":"Kopyalama seviyesi kodu","editor.editing.copy_help":"Bunu Discord\'umuzdaki #levels kanal\u0131na yap\u0131\u015Ft\u0131r\u0131n","editor.editing.credit":"Krediler ve kaynak","editor.editing.credit_prompt":"Seviyenizin kaynak URL\'sini veya a\xe7\u0131klamas\u0131n\u0131 girin.","editor.editing.delete":"Seviyeyi Sil","editor.editing.down":"T\xfcm tu\u011Flalar\u0131 a\u015Fa\u011F\u0131 do\u011Fru hareket ettirin","editor.editing.help":"Daha sonra renklendirmek istedi\u011Finiz kutucu\u011Fa t\u0131klay\u0131n.","editor.editing.left":"T\xfcm tu\u011Flalar\u0131 sola ta\u015F\u0131","editor.editing.play":"Bu seviyeyi oyna","editor.editing.rename":"Seviye Ad\u0131","editor.editing.rename_prompt":"L\xfctfen seviye i\xe7in yeni bir ad girin","editor.editing.right":"T\xfcm tu\u011Flalar\u0131 sa\u011Fa ta\u015F\u0131","editor.editing.smaller":"Seviye boyutunu azalt","editor.editing.title":"D\xfczenleme d\xfczeyi : {{name}}","editor.editing.up":"T\xfcm tu\u011Flalar\u0131 yukar\u0131 ta\u015F\u0131","editor.help":"\xd6zel seviyeler yarat\u0131n ve bunlar\u0131 oyuna dahil etmek i\xe7in payla\u015F\u0131n.","editor.import":"Bir seviyeyi i\xe7e aktar","editor.import_instruction":"Seviye listenize aktarmak i\xe7in bir seviye kodunu yap\u0131\u015Ft\u0131r\u0131n","editor.locked":"Kilidi a\xe7mak i\xe7in toplam {{min}} puan\u0131na ula\u015F\u0131n","editor.new_level":"Yeni seviye","editor.title":"Seviye Edit\xf6r\xfc","gameOver.creative":"Bu ko\u015Fu kaydedilmeyecek.","gameOver.cumulative_total":"Toplam k\xfcm\xfclatif puan\u0131n\u0131z {{startTs}} \'dan {{endTs}}\'e \xe7\u0131kt\u0131.","gameOver.lost.summary":" {{score}} jeton yakalad\u0131ktan sonra topu d\xfc\u015F\xfcrd\xfcn.","gameOver.lost.title":"Oyun bitti","gameOver.stats.balls_lost":"Kaybedilen toplar","gameOver.stats.bricks_broken":"Tu\u011Flalar k\u0131r\u0131ld\u0131","gameOver.stats.bricks_per_minute":"Dakikada k\u0131r\u0131lan tu\u011Fla say\u0131s\u0131","gameOver.stats.catch_rate":"Yakalama oran\u0131","gameOver.stats.combo_avg":"Ortalama kombo","gameOver.stats.combo_max":"Maksimum kombo","gameOver.stats.duration_per_level":"Seviye ba\u015F\u0131na s\xfcre","gameOver.stats.hit_rate":"\u0130sabet oran\u0131","gameOver.stats.intro":"","gameOver.stats.level_reached":"Seviyeye ula\u015F\u0131ld\u0131","gameOver.stats.total_score":"Toplam Puan","gameOver.stats.upgrades_applied":"Uygulanan y\xfckseltmeler","gameOver.stats_intro":"A\u015Fa\u011F\u0131da {{count}} en iyi oyunlar\u0131n\u0131zla kar\u015F\u0131la\u015Ft\u0131r\u0131ld\u0131\u011F\u0131nda oyun istatistiklerinizi bulabilirsiniz.","gameOver.unlocked_perk":"Y\xfckseltme kilidi a\xe7\u0131ld\u0131","gameOver.unlocked_perk_plural":"Az \xf6nce {{count}} avantaj\u0131n kilidini a\xe7t\u0131n\u0131z","gameOver.win.summary":"Bu oyun bitti. {{score}} jeton saklad\u0131n.","gameOver.win.title":"Bu oyunu tamamlad\u0131n","help.content":"## Hedef\\n\\n7 seviye boyunca m\xfcmk\xfcn oldu\u011Funca \xe7ok jeton topla. \\nTu\u011Flalar\u0131 k\u0131rd\u0131\u011F\u0131nda jetonlar belirir.\\nPuan\u0131n\u0131 art\u0131rmak i\xe7in k\xfcre\u011Finle topla.\\nPuan\u0131n ekran\u0131n sa\u011F \xfcst k\xf6\u015Fesinde g\xf6sterilir.\\nTopu d\xfc\u015F\xfcrme yoksa oyun biter.\\n\\nT\xfcm tu\u011Flalar\u0131 yok ettikten sonra bir y\xfckseltme se\xe7ebilirsin.\\n\\n## Y\xfckseltmeler \\n\\nSe\xe7ti\u011Fin y\xfckseltmeler ko\u015Funun sonuna kadar ge\xe7erli olur. \\nBaz\u0131lar\u0131 daha g\xfc\xe7l\xfc etki i\xe7in birden fazla kez se\xe7ilebilir.\\nBaz\u0131lar\u0131 ni\u015Fan almaya yard\u0131mc\u0131 olur veya oyunu ba\u015Fka \u015Fekillerde kolayla\u015
2025-04-15 16:47:04 +02:00
},{}],"1l6Zs":[function(require,module,exports,__globalThis) {
2025-04-20 09:33:12 +02:00
module.exports = JSON.parse('{"confirmRestart.no":"Abbrechen","confirmRestart.text":"Sie sind dabei, ein neues Spiel zu beginnen. Sind Sie sicher, dass Sie weitermachen wollen?","confirmRestart.title":"Ein neues Spiel beginnen?","confirmRestart.yes":"Spiel neu starten","editor.editing.bigger":"Levelgr\xf6\xdfe erh\xf6hen","editor.editing.color":"W\xe4hlen Sie eine Farbe aus der Farbliste (max. 5 pro Level)","editor.editing.copy":"Levelcode kopieren","editor.editing.copy_help":"F\xfcgen Sie es in den Kanal #levels in unserem Discord ein","editor.editing.credit":"Credits und Quelle","editor.editing.credit_prompt":"Geben Sie die Quell-URL oder Erkl\xe4rung Ihres Levels ein.","editor.editing.delete":"Ebene l\xf6schen","editor.editing.down":"Bewegen Sie alle Steine nach unten","editor.editing.help":"Klicken Sie dann auf eine Kachel, um sie einzuf\xe4rben.","editor.editing.left":"Bewege alle Steine nach links","editor.editing.play":"Spiele dieses Level","editor.editing.rename":"Ebenenname","editor.editing.rename_prompt":"Bitte geben Sie einen neuen Namen f\xfcr das Level ein","editor.editing.right":"Bewege alle Steine nach rechts","editor.editing.smaller":"Verringern der Levelgr\xf6\xdfe","editor.editing.title":"Bearbeitungsebene: {{name}}","editor.editing.up":"Bewegen Sie alle Steine nach oben","editor.help":"Erstellen Sie benutzerdefinierte Level und geben Sie sie frei, um sie in das Spiel aufzunehmen.","editor.import":"Importieren einer Ebene","editor.import_instruction":"F\xfcgen Sie einen Levelcode ein, um ihn in Ihre Levelliste zu importieren","editor.locked":"Erreichen Sie eine Gesamtpunktzahl von {{min}} , um freizuschalten","editor.new_level":"Neues Level","editor.title":"Level-Editor","gameOver.creative":"Dieser Lauf wird nicht aufgezeichnet.","gameOver.cumulative_total":"Ihre kumulative Gesamtpunktzahl ist von {{startTs}} auf {{endTs}}gestiegen.","gameOver.lost.summary":"Du hast den Ball fallen lassen, nachdem du {{score}} M\xfcnzen gefangen hast.","gameOver.lost.title":"Spiel vorbei","gameOver.stats.balls_lost":"Verlorene B\xe4lle","gameOver.stats.bricks_broken":"Ziegelsteine gebrochen","gameOver.stats.bricks_per_minute":"Ziegelsteinbruch pro Minute","gameOver.stats.catch_rate":"Fangquote","gameOver.stats.combo_avg":"Durchschnittliche Combo","gameOver.stats.combo_max":"Max-Kombo","gameOver.stats.duration_per_level":"Dauer pro Stufe","gameOver.stats.hit_rate":"Trefferquote","gameOver.stats.intro":"","gameOver.stats.level_reached":"Erreichte Stufe","gameOver.stats.total_score":"Gesamtpunktzahl","gameOver.stats.upgrades_applied":"Angewandte Upgrades","gameOver.stats_intro":"Hier finden Sie Ihre Spielstatistik im Vergleich zu Ihren {{count}} besten Spielen.","gameOver.unlocked_perk":"Upgrade freigeschaltet","gameOver.unlocked_perk_plural":"Du hast soeben {{count}} Verg\xfcnstigungen freigeschaltet","gameOver.win.summary":"Das Spiel ist vorbei. Du hast {{score}} M\xfcnzen versteckt.","gameOver.win.title":"Du hast dieses Spiel abgeschlossen","help.content":"## Ziel\\n\\nSammle in 7 Levels so viele M\xfcnzen wie m\xf6glich ein.\\nDie M\xfcnzen erscheinen, wenn du Ziegel zerbrichst.\\nFangen Sie sie mit Ihrem Paddel auf, um Ihre Punktzahl zu erh\xf6hen.\\nIhr Punktestand wird in der oberen rechten Ecke des Bildschirms angezeigt.\\nLassen Sie den Ball nicht fallen, sonst ist das Spiel vorbei.\\n\\nWenn du alle Ziegel zerst\xf6rt hast, kannst du dir ein Upgrade aussuchen.\\n\\n## Upgrades\\n\\nDie Upgrades, die du w\xe4hlst, gelten bis zum Ende des Laufs.\\nEinige k\xf6nnen mehrmals ausgew\xe4hlt werden, um die Wirkung zu verst\xe4rken.\\nEinige helfen beim Zielen oder machen das Spiel auf andere Weise einfacher.\\nEinige sind nur in Kombination n\xfctzlich.\\n\\nZu Beginn eines jeden Spiels erh\xe4ltst du immer ein Upgrade.\\nIhr Symbol dient als Baustein des ersten Levels.\\nDu kannst die Start-Upgrades in den Einstellungen ausw\xe4hlen.\\n\\nViele Upgrades wirken sich auf deine Kombo aus.\\n\\n## Combo\\n\\nDeine \\"Combo\\" ist die Anzahl der M\xfcnzen, die beim Zerbrechen eines Steins entstehen.\\nSie wird auf deinem P
2025-03-19 18:13:41 +01:00
},{}],"5blfu":[function(require,module,exports,__globalThis) {
// Settings
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "getSettingValue", ()=>getSettingValue);
parcelHelpers.export(exports, "setSettingValue", ()=>setSettingValue);
2025-04-16 09:26:10 +02:00
parcelHelpers.export(exports, "commitSettingsChangesToLocalStorage", ()=>commitSettingsChangesToLocalStorage);
2025-03-19 18:13:41 +01:00
parcelHelpers.export(exports, "getTotalScore", ()=>getTotalScore);
2025-03-23 15:48:21 +01:00
parcelHelpers.export(exports, "getCurrentMaxCoins", ()=>getCurrentMaxCoins);
parcelHelpers.export(exports, "getCurrentMaxParticles", ()=>getCurrentMaxParticles);
parcelHelpers.export(exports, "cycleMaxCoins", ()=>cycleMaxCoins);
2025-03-19 18:13:41 +01:00
let cachedSettings = {};
2025-04-15 17:31:57 +02:00
try {
for(let key in localStorage)try {
2025-04-15 21:28:00 +02:00
cachedSettings[key] = JSON.parse(localStorage.getItem(key) || "null");
2025-03-19 18:13:41 +01:00
} catch (e) {
console.warn(e);
}
2025-04-15 17:31:57 +02:00
} catch (e) {
console.warn(e);
}
function getSettingValue(key, defaultValue) {
2025-03-19 18:13:41 +01:00
return cachedSettings[key] ?? defaultValue;
}
2025-04-15 21:25:27 +02:00
// We avoid using localstorage synchronously for perf reasons
let needsSaving = new Set();
2025-03-19 18:13:41 +01:00
function setSettingValue(key, value) {
2025-04-16 09:38:43 +02:00
needsSaving.add(key);
cachedSettings[key] = value;
2025-04-15 21:25:27 +02:00
}
2025-04-16 09:26:10 +02:00
function commitSettingsChangesToLocalStorage() {
2025-03-19 18:13:41 +01:00
try {
2025-04-15 21:25:27 +02:00
for (let key of needsSaving)localStorage.setItem(key, JSON.stringify(cachedSettings[key]));
needsSaving.clear();
2025-03-19 18:13:41 +01:00
} catch (e) {
console.warn(e);
}
2025-04-16 09:26:10 +02:00
}
2025-04-16 09:38:43 +02:00
setInterval(()=>commitSettingsChangesToLocalStorage(), 500);
2025-03-19 18:13:41 +01:00
function getTotalScore() {
return getSettingValue("breakout_71_total_score", 0);
}
2025-03-23 15:48:21 +01:00
function getCurrentMaxCoins() {
2025-04-15 17:31:57 +02:00
return Math.pow(2, getSettingValue("max_coins", 2)) * 200;
2025-03-23 15:48:21 +01:00
}
function getCurrentMaxParticles() {
2025-04-15 16:47:04 +02:00
return getCurrentMaxCoins();
2025-03-23 15:48:21 +01:00
}
function cycleMaxCoins() {
2025-04-15 21:25:27 +02:00
setSettingValue("max_coins", (getSettingValue("max_coins", 2) + 1) % 7);
2025-03-23 15:48:21 +01:00
}
2025-03-19 18:13:41 +01:00
2025-04-06 11:27:26 +02:00
},{"@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"gkKU3":[function(require,module,exports,__globalThis) {
exports.interopDefault = function(a) {
return a & & a.__esModule ? a : {
default: a
};
};
exports.defineInteropFlag = function(a) {
Object.defineProperty(a, '__esModule', {
value: true
});
};
exports.exportAll = function(source, dest) {
Object.keys(source).forEach(function(key) {
if (key === 'default' || key === '__esModule' || Object.prototype.hasOwnProperty.call(dest, key)) return;
Object.defineProperty(dest, key, {
enumerable: true,
get: function() {
return source[key];
}
});
});
return dest;
};
exports.export = function(dest, destName, get) {
Object.defineProperty(dest, destName, {
enumerable: true,
get: get
});
};
},{}],"6pQh7":[function(require,module,exports,__globalThis) {
2025-03-29 11:24:45 +01:00
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "clamp", ()=>clamp);
parcelHelpers.export(exports, "comboKeepingRate", ()=>comboKeepingRate);
2025-03-30 21:07:58 +02:00
parcelHelpers.export(exports, "hoursSpentPlaying", ()=>hoursSpentPlaying);
2025-03-31 20:08:17 +02:00
parcelHelpers.export(exports, "miniMarkDown", ()=>miniMarkDown);
2025-04-08 14:03:38 +02:00
parcelHelpers.export(exports, "firstWhere", ()=>firstWhere);
2025-04-08 21:54:19 +02:00
parcelHelpers.export(exports, "wallBouncedBest", ()=>wallBouncedBest);
parcelHelpers.export(exports, "wallBouncedGood", ()=>wallBouncedGood);
parcelHelpers.export(exports, "levelTimeBest", ()=>levelTimeBest);
parcelHelpers.export(exports, "levelTimeGood", ()=>levelTimeGood);
parcelHelpers.export(exports, "catchRateBest", ()=>catchRateBest);
parcelHelpers.export(exports, "catchRateGood", ()=>catchRateGood);
parcelHelpers.export(exports, "missesBest", ()=>missesBest);
parcelHelpers.export(exports, "missesGood", ()=>missesGood);
2025-04-15 16:47:04 +02:00
var _settings = require("./settings");
2025-03-29 11:24:45 +01:00
function clamp(value, min, max) {
return Math.max(min, Math.min(value, max));
}
function comboKeepingRate(level) {
2025-03-29 17:40:07 +01:00
return clamp(1 - 1 / (1 + level) * 1.5, 0, 1);
2025-03-29 11:24:45 +01:00
}
2025-03-30 21:07:58 +02:00
function hoursSpentPlaying() {
try {
2025-04-15 21:28:00 +02:00
const timePlayed = (0, _settings.getSettingValue)("breakout_71_total_play_time", 0);
2025-04-15 16:47:04 +02:00
return Math.floor(timePlayed / 1000 / 60 / 60);
2025-03-30 21:07:58 +02:00
} catch (e) {
return 0;
}
}
2025-03-31 20:08:17 +02:00
function miniMarkDown(md) {
let html = [];
let lastNode = null;
2025-03-31 20:13:47 +02:00
md.split("\n").forEach((line)=>{
2025-03-31 20:08:17 +02:00
const titlePrefix = line.match(/^#+ /)?.[0];
if (titlePrefix) {
if (lastNode) html.push(lastNode);
lastNode = {
2025-03-31 20:13:47 +02:00
tagName: "h" + (titlePrefix.length - 1),
2025-03-31 20:08:17 +02:00
text: line.slice(titlePrefix.length)
};
2025-03-31 20:13:47 +02:00
} else if (line.startsWith("- ")) {
if (lastNode?.tagName !== "ul") {
2025-03-31 20:08:17 +02:00
if (lastNode) html.push(lastNode);
lastNode = {
2025-03-31 20:13:47 +02:00
tagName: "ul",
text: ""
2025-03-31 20:08:17 +02:00
};
}
2025-03-31 20:13:47 +02:00
lastNode.text += "< li > " + line.slice(2) + "< / li > ";
2025-03-31 20:08:17 +02:00
} else if (!line.trim()) {
if (lastNode) html.push(lastNode);
lastNode = null;
} else {
2025-03-31 20:13:47 +02:00
if (lastNode?.tagName !== "p") {
2025-03-31 20:08:17 +02:00
if (lastNode) html.push(lastNode);
lastNode = {
2025-03-31 20:13:47 +02:00
tagName: "p",
text: ""
2025-03-31 20:08:17 +02:00
};
}
2025-03-31 20:13:47 +02:00
lastNode.text += line + " ";
2025-03-31 20:08:17 +02:00
}
});
if (lastNode) html.push(lastNode);
2025-03-31 20:13:47 +02:00
return html.map((h)=>"< " + h.tagName + ">" + h.text.replace(/\bhttps?:\/\/[^\s< >]+/gi, (a)=>`< a href = "${a}" > ${a}< / a > `) + "< /" + h.tagName + ">").join("\n");
2025-03-31 20:08:17 +02:00
}
2025-04-08 14:03:38 +02:00
function firstWhere(arr, mapper) {
for(let i = 0; i < arr.length ; i + + ) {
const result = mapper(arr[i], i);
if (typeof result !== "undefined") return result;
}
}
2025-04-08 21:54:19 +02:00
const wallBouncedBest = 3, wallBouncedGood = 10, levelTimeBest = 30, levelTimeGood = 60, catchRateBest = 95, catchRateGood = 90, missesBest = 3, missesGood = 6;
2025-03-29 11:24:45 +01:00
2025-04-18 17:15:47 +02:00
},{"./settings":"5blfu","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"7OIPf":[function(require,module,exports,__globalThis) {
2025-04-06 11:27:26 +02:00
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "getLevelBackground", ()=>getLevelBackground);
parcelHelpers.export(exports, "hashCode", ()=>hashCode);
var _backgroundsJson = require("./data/backgrounds.json");
var _backgroundsJsonDefault = parcelHelpers.interopDefault(_backgroundsJson);
const backgrounds = (0, _backgroundsJsonDefault.default);
function getLevelBackground(level) {
return backgrounds[hashCode(level.name) % backgrounds.length];
}
function hashCode(string) {
let hash = 0;
for(let i = 0; i < string.length ; i + + ) {
let code = string.charCodeAt(i);
hash = (hash < < 5 ) - hash + code ;
hash = hash & hash; // Convert to 32bit integer
}
return Math.abs(hash);
}
},{"./data/backgrounds.json":"31wW4","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"31wW4":[function(require,module,exports,__globalThis) {
module.exports = JSON.parse("[\"< svg xmlns = 'http://www.w3.org/2000/svg' width = '20' height = '20' > < path d = 'M3.25 10h13.5M10 3.25v13.5' stroke-width = '1' stroke = 'white' fill = 'none' / > < / svg > \",\"< svg xmlns = 'http://www.w3.org/2000/svg' width = '40' height = '40' > < path d = 'M11 6a5 5 0 01-5 5 5 5 0 01-5-5 5 5 0 015-5 5 5 0 015 5' stroke = 'none' fill = 'white' / > < / svg > \",\"< svg xmlns = 'http://www.w3.org/2000/svg' width = '20' height = '20' > < path d = 'M10-10L20 0v10L10 0zM20 0L10-10V0l10 10zm0 10L10 0v10l10 10zm0 10L10 10v10l10 10zM0 20l10-10v10L0 30zm0-10L10 0v10L0 20zM0 0l10-10V0L0 10z' stroke-width = '1' stroke = 'white' fill = 'none' / > < / svg > \",\"< svg xmlns = 'http://www.w3.org/2000/svg' width = '40' height = '40' > < path d = 'M15.986 4.186 4.1 16.072v.58L16.566 4.186Zm7.62 0 12.38 12.38v-.58l-11.8-11.8Zm12.38 19.248L23.52 35.9h.58l11.886-11.886ZM4.1 23.52v.58l11.8 11.8h.58z' stroke-width = '1' stroke = 'white' fill = 'none' / > < / svg > \",\"< svg xmlns = 'http://www.w3.org/2000/svg' width = '25' height = '25' > < path d = 'M9.19 0v3.93A9.187 9.187 0 003.93 9.19H0m0 6.618h3.93a9.188 9.188 0 005.26 5.26V25m6.619 0v-3.93a9.188 9.188 0 005.261-5.261H25m0-6.618h-3.93A9.188 9.188 0 0015.81 3.93V0' stroke-width = '1' stroke = 'white' fill = 'none' / > < / svg > \",\"< svg xmlns = 'http://www.w3.org/2000/svg' width = '29' height = '33.487' > < path d = 'M29 20.928v14.813M14.5 12.56v16.745M29-2.559v6.744l-14.5 8.374L0 4.189v-6.745m29 6.742l14.5 8.37m0 16.745L29 20.928l-14.5 8.376L0 20.931l-14.5 8.376m0-16.744L0 4.189m0 31.487V20.931' stroke-width = '1' stroke = 'white' fill = 'none' / > < / svg > \",\"< svg xmlns = 'http://www.w3.org/2000/svg' width = '29' height = '50.115' > < path d = 'M14.498 16.858L0 8.488.002-8.257l14.5-8.374L29-8.26l-.002 16.745zm0 50.06L0 58.548l.002-16.745 14.5-8.373L29 41.8l-.002 16.744zM28.996 41.8l-14.498-8.37.002-16.744L29 8.312l14.498 8.37-.002 16.745zm-29 0l-14.498-8.37.002-16.744L0 8.312l14.498 8.37-.002 16.745z' stroke-width = '1' stroke = 'white' fill = 'none' / > < / svg > \",\"< svg xmlns = 'http://www.w3.org/2000/svg' width = '62' height = '68' > < rect x = '0' y = '0' width = '62' height = '68' fill = 'black' / > < path d = 'M41.845 51.072h3.465v-7.035h-7.076v13.999H52.18V37.21H31.117m0 27.79V37.21M20.389 51.07h-3.466v-7.034H24v13.999H10.055V37.21h21.062m10.728-20.283h3.465v7.035h-7.076V9.964H52.18V30.79H31.117m0-27.789v27.79M20.389 16.927h-3.466v7.035H24V9.964H10.055V30.79h21.062M3 3h56v62H3.126z' stroke-width = '1' stroke = 'white' fill = 'none' / > < / svg > \",\"< svg xmlns = 'http://www.w3.org/2000/svg' width = '26.55' height = '25' > < rect x = '0' y = '0' width = '26.55' height = '25' fill = 'black' / > < path d = 'M0 10.86v3.22c2.7.08 4.9 2.31 4.9 5.03V25h3.2v-5.9c0-4.48-3.63-8.16-8.1-8.24ZM18.17 25h3.21v-5.9a5.05 5.05 0 0 1 5.03-5.02h.14v-3.21h-.14a8.27 8.27 0 0 0-8.24 8.24zm3.21-25h-3.21v1.64a5.05 5.05 0 0 1-5.03 5.02A5.05 5.05 0 0 1 8.1 1.64V0H4.89v1.64c0 4.53 3.7 8.24 8.25 8.24 4.53 0 8.24-3.7 8.24-8.24z' stroke = 'none' fill = 'white' / > < / svg > \",\"< svg xmlns = 'http://www.w3.org/2000/svg' width = '40' height = '79.392' > < path d = 'm.135 40.054-14.277-25.722M0 40.054l14.278-25.722M0 40.054v-40m0 40-20-20 20-20 20 20Zm-.135-.716L14.142 65.06M0 39.338-14.278 65.06M0 39.338v40m0-40 20 20-20 20-20-20Zm40.136.716L25.858 14.332M40 40.054l14.278-25.722M40 40.054v-40m-20 20 20-20 20 20-20 20Zm19.865 19.284L54.142 65.06M40 39.338 25.722 65.06M40 39.338v40m20-20-20 20-20-20 20-20Z' stroke-width = '1' stroke = 'white' fill = 'none' / > < / svg > \",\"< svg xmlns = 'http://www.w3.org/2000/svg' width = '50' height = '29.442' > < path d = 'M35.569-17.373 22.959 4.468l-12.61-21.841Zm0 29.442-12.61 21.84-12.61-21.84Zm25-14.721-12.61 21.841-12.61-21.841zm0 29.441-12.61 21.842-12.61-21.842Zm-33.478 0L39.7 4.95l12.61 21.84zM10.569-2.652l-12.61 21.841-12.61-21.841Zm0 29.441-12.61 21.842-12.61-21.842Zm-33.478 0L-10.3 4.95l12.61 21.84zm25-14.72L14.7-9.773l12.61 21.842zm0 29.441L14.7 19.67l12.61 21.841z' stroke-width = '1' stroke = 'white' fill = 'none' / > < / svg > \",\"< svg xmlns = 'http://www.w3.org/2000/svg' width = '40' height = '59.428' > < path d = 'M0 70 . 975V47 . 881m20-1 . 692L8 . 535 52 . 808v13 . 239L20 72 . 667l11 . 465-6 . 62V52 . 808zm0-32 . 95l11 . 465-6 . 62V-6 . 619L20-13 . 24 8 . 535-6 . 619V6 . 619L20 13 . 24m8 . 535 4 . 927v13 . 238L40 38 . 024l11 . 465-6 . 62V18 . 166L40 11 . 546z
},{}],"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;
2025-04-09 11:40:16 +02:00
ctx.clearRect(0, 0, size, size);
2025-03-20 18:44:46 +01:00
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()}" / > `;
}
2025-03-19 18:13:41 +01:00
},{"@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) {
2025-04-18 21:17:32 +02:00
sounds[soundName](// In stress test, dim the sounds but play them
Math.min(1, ex.vol), pixelsToPan(gameState, ex.x), gameState.combo);
2025-03-19 18:13:41 +01:00
ex.vol = 0;
}
}
}
const sounds = {
2025-04-15 16:47:04 +02:00
wallBeep: (volume, pan)=>{
if (!(0, _options.isOptionOn)("sound")) return;
createSingleBounceSound(800, pan, volume);
},
plouf: (volume, pan)=>{
2025-03-19 18:13:41 +01:00
if (!(0, _options.isOptionOn)("sound")) return;
2025-04-15 21:25:27 +02:00
createSingleBounceSound(500, pan, volume * 0.5);
2025-04-15 16:47:04 +02:00
// createWaterDropSound(800, pan, volume*0.2, 0.2,'triangle')
2025-03-19 18:13:41 +01:00
},
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-30 21:07:58 +02:00
// void: (volume: number, pan: number) => {
// if (!isOptionOn("sound")) return;
// createSingleBounceSound(1200, pan, volume, 0.5, "sawtooth");
// createSingleBounceSound(600, pan, volume, 0.3, "sawtooth");
// },
// freeze: (volume: number, pan: number) => {
// if (!isOptionOn("sound")) return;
// createSingleBounceSound(220, pan, volume, 0.5, "square");
// createSingleBounceSound(440, pan, volume, 0.5, "square");
// },
2025-03-19 18:13:41 +01:00
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");
2025-03-30 21:07:58 +02:00
var _pureFunctions = require("./pure_functions");
2025-03-19 18:13:41 +01:00
const options = {
sound: {
default: true,
2025-04-11 09:36:31 +02:00
name: (0, _i18N.t)("settings.sounds"),
help: (0, _i18N.t)("settings.sounds_help")
2025-03-19 18:13:41 +01:00
},
"mobile-mode": {
default: window.innerHeight > window.innerWidth,
2025-04-11 09:36:31 +02:00
name: (0, _i18N.t)("settings.mobile"),
help: (0, _i18N.t)("settings.mobile_help")
2025-03-19 18:13:41 +01:00
},
basic: {
default: false,
2025-04-11 09:36:31 +02:00
name: (0, _i18N.t)("settings.basic"),
help: (0, _i18N.t)("settings.basic_help")
2025-03-19 18:13:41 +01:00
},
2025-03-28 10:21:14 +01:00
colorful_coins: {
default: false,
2025-04-11 09:36:31 +02:00
name: (0, _i18N.t)("settings.colorful_coins"),
help: (0, _i18N.t)("settings.colorful_coins_help")
2025-03-28 10:21:14 +01:00
},
2025-04-03 16:10:51 +02:00
extra_bright: {
default: true,
2025-04-11 09:36:31 +02:00
name: (0, _i18N.t)("settings.extra_bright"),
help: (0, _i18N.t)("settings.extra_bright_help")
2025-04-03 16:10:51 +02:00
},
2025-04-18 21:17:32 +02:00
smooth_lighting: {
default: true,
name: (0, _i18N.t)("settings.smooth_lighting"),
help: (0, _i18N.t)("settings.smooth_lighting_help")
},
precise_lighting: {
default: true,
name: (0, _i18N.t)("settings.precise_lighting"),
help: (0, _i18N.t)("settings.precise_lighting_help")
},
2025-04-20 09:33:12 +02:00
probabilistic_lighting: {
default: false,
name: (0, _i18N.t)("settings.probabilistic_lighting"),
help: (0, _i18N.t)("settings.probabilistic_lighting_help")
},
2025-04-04 12:07:24 +02:00
contrast: {
default: false,
2025-04-11 09:36:31 +02:00
name: (0, _i18N.t)("settings.contrast"),
help: (0, _i18N.t)("settings.contrast_help")
2025-04-04 12:07:24 +02:00
},
2025-03-23 15:48:21 +01:00
show_fps: {
default: false,
2025-04-11 09:36:31 +02:00
name: (0, _i18N.t)("settings.show_fps"),
help: (0, _i18N.t)("settings.show_fps_help")
2025-03-23 15:48:21 +01:00
},
2025-03-28 10:21:14 +01:00
show_stats: {
default: false,
2025-04-11 09:36:31 +02:00
name: (0, _i18N.t)("settings.show_stats"),
help: (0, _i18N.t)("settings.show_stats_help")
2025-03-28 10:21:14 +01:00
},
2025-03-19 18:13:41 +01:00
pointerLock: {
default: false,
2025-04-11 09:36:31 +02:00
name: (0, _i18N.t)("settings.pointer_lock"),
help: (0, _i18N.t)("settings.pointer_lock_help")
2025-03-19 18:13:41 +01:00
},
easy: {
default: false,
2025-04-11 09:36:31 +02:00
name: (0, _i18N.t)("settings.kid"),
help: (0, _i18N.t)("settings.kid_help")
2025-03-19 18:13:41 +01:00
},
// 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,
2025-04-11 09:36:31 +02:00
name: (0, _i18N.t)("settings.record"),
help: (0, _i18N.t)("settings.record_help")
2025-03-29 17:40:07 +01:00
},
fullscreen: {
default: false,
2025-04-11 09:36:31 +02:00
name: (0, _i18N.t)("settings.fullscreen"),
help: (0, _i18N.t)("settings.fullscreen_help")
2025-03-30 21:07:58 +02:00
},
donation_reminder: {
default: (0, _pureFunctions.hoursSpentPlaying)() > 5,
2025-04-11 09:36:31 +02:00
name: (0, _i18N.t)("settings.donation_reminder"),
help: (0, _i18N.t)("settings.donation_reminder_help")
2025-04-01 21:43:36 +02:00
},
red_miss: {
2025-04-04 12:07:24 +02:00
default: true,
2025-04-11 09:36:31 +02:00
name: (0, _i18N.t)("settings.red_miss"),
help: (0, _i18N.t)("settings.red_miss_help")
2025-04-02 19:50:05 +02:00
},
comboIncreaseTexts: {
2025-04-04 12:07:24 +02:00
default: true,
2025-04-11 09:36:31 +02:00
name: (0, _i18N.t)("settings.comboIncreaseTexts"),
help: (0, _i18N.t)("settings.comboIncreaseTexts_help")
2025-03-19 18:13:41 +01:00
}
};
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));
}
2025-03-31 20:08:17 +02:00
},{"./i18n/i18n":"eNPRm","./settings":"5blfu","./pure_functions":"6pQh7","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"cEeac":[function(require,module,exports,__globalThis) {
2025-03-29 21:22:19 +01:00
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
2025-04-01 13:35:33 +02:00
parcelHelpers.export(exports, "describeLevel", ()=>describeLevel);
2025-03-29 21:22:19 +01:00
parcelHelpers.export(exports, "getMajorityValue", ()=>getMajorityValue);
parcelHelpers.export(exports, "sample", ()=>sample);
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);
2025-04-10 21:40:45 +02:00
parcelHelpers.export(exports, "getClosestBall", ()=>getClosestBall);
2025-03-29 21:22:19 +01:00
parcelHelpers.export(exports, "getPossibleUpgrades", ()=>getPossibleUpgrades);
parcelHelpers.export(exports, "max_levels", ()=>max_levels);
parcelHelpers.export(exports, "pickedUpgradesHTMl", ()=>pickedUpgradesHTMl);
parcelHelpers.export(exports, "levelsListHTMl", ()=>levelsListHTMl);
parcelHelpers.export(exports, "currentLevelInfo", ()=>currentLevelInfo);
2025-04-01 21:37:07 +02:00
parcelHelpers.export(exports, "isPickyEatingPossible", ()=>isPickyEatingPossible);
parcelHelpers.export(exports, "reachRedRowIndex", ()=>reachRedRowIndex);
2025-04-04 09:45:35 +02:00
parcelHelpers.export(exports, "telekinesisEffectRate", ()=>telekinesisEffectRate);
parcelHelpers.export(exports, "yoyoEffectRate", ()=>yoyoEffectRate);
2025-03-29 21:22:19 +01:00
parcelHelpers.export(exports, "findLast", ()=>findLast);
parcelHelpers.export(exports, "distance2", ()=>distance2);
parcelHelpers.export(exports, "distanceBetween", ()=>distanceBetween);
parcelHelpers.export(exports, "defaultSounds", ()=>defaultSounds);
parcelHelpers.export(exports, "shouldPierceByColor", ()=>shouldPierceByColor);
2025-04-02 19:36:03 +02:00
parcelHelpers.export(exports, "isMovingWhilePassiveIncome", ()=>isMovingWhilePassiveIncome);
2025-04-06 10:13:10 +02:00
parcelHelpers.export(exports, "getHighScore", ()=>getHighScore);
parcelHelpers.export(exports, "highScoreText", ()=>highScoreText);
2025-04-07 14:08:48 +02:00
parcelHelpers.export(exports, "getLevelUnlockCondition", ()=>getLevelUnlockCondition);
parcelHelpers.export(exports, "getBestScoreMatching", ()=>getBestScoreMatching);
2025-04-06 15:38:30 +02:00
parcelHelpers.export(exports, "reasonLevelIsLocked", ()=>reasonLevelIsLocked);
2025-04-10 14:49:28 +02:00
parcelHelpers.export(exports, "ballTransparency", ()=>ballTransparency);
2025-04-16 15:30:20 +02:00
parcelHelpers.export(exports, "getCoinRenderColor", ()=>getCoinRenderColor);
2025-04-18 21:17:32 +02:00
parcelHelpers.export(exports, "getCornerOffset", ()=>getCornerOffset);
2025-04-20 10:58:26 +02:00
parcelHelpers.export(exports, "isInWebView", ()=>isInWebView);
2025-03-29 21:22:19 +01:00
var _loadGameData = require("./loadGameData");
var _i18N = require("./i18n/i18n");
2025-04-04 09:45:35 +02:00
var _pureFunctions = require("./pure_functions");
2025-04-06 11:57:52 +02:00
var _upgrades = require("./upgrades");
2025-04-06 15:38:30 +02:00
var _getLevelBackground = require("./getLevelBackground");
2025-04-08 14:03:38 +02:00
var _settings = require("./settings");
2025-04-16 15:30:20 +02:00
var _options = require("./options");
2025-04-01 13:35:33 +02:00
function describeLevel(level) {
let bricks = 0, colors = new Set(), bombs = 0;
level.bricks.forEach((color)=>{
if (!color) return;
2025-04-01 13:39:09 +02:00
if (color === "black") {
2025-04-01 13:35:33 +02:00
bombs++;
return;
} else {
colors.add(color);
bricks++;
}
});
2025-04-01 13:39:09 +02:00
return (0, _i18N.t)("unlocks.level_description", {
2025-04-01 13:35:33 +02:00
size: level.size,
bricks,
colors: colors.size,
bombs
});
}
2025-03-29 21:22:19 +01:00
function getMajorityValue(arr) {
const count = {};
arr.forEach((v)=>count[v] = (count[v] || 0) + 1);
// Object.values inline polyfill
const max = Math.max(...Object.keys(count).map((k)=>count[k]));
return sample(Object.keys(count).filter((k)=>count[k] == max));
}
function sample(arr) {
return arr[Math.floor(arr.length * Math.random())];
}
function 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;
}
2025-04-10 21:40:45 +02:00
function getClosestBall(gameState, x, y) {
let closestBall = null;
let dist = 0;
gameState.balls.forEach((ball)=>{
const d2 = (ball.x - x) * (ball.x - x) + (ball.y - y) * (ball.y - y);
if (d2 < dist | | ! closestBall ) {
closestBall = ball;
dist = d2;
}
});
return closestBall;
}
2025-03-29 21:22:19 +01:00
function getPossibleUpgrades(gameState) {
2025-04-08 14:03:38 +02:00
return (0, _loadGameData.upgrades).filter((u)=>(0, _settings.getTotalScore)() >= u.threshold).filter((u)=>!u?.requires || gameState.perks[u?.requires]);
2025-03-29 21:22:19 +01:00
}
function max_levels(gameState) {
2025-04-06 10:13:10 +02:00
if (gameState.creative) return 1;
return 7 + gameState.perks.extra_levels;
2025-03-29 21:22:19 +01:00
}
function pickedUpgradesHTMl(gameState) {
2025-04-06 15:38:30 +02:00
const upgradesList = getPossibleUpgrades(gameState).filter((u)=>gameState.perks[u.id]).map((u)=>{
const newMax = Math.max(0, u.max + gameState.perks.limitless);
2025-04-01 18:26:40 +02:00
let bars = [];
2025-03-30 21:07:58 +02:00
for(let i = 0; i < Math.max ( u . max , newMax , gameState . perks [ u . id ] ) ; i + + ) {
2025-04-01 18:26:40 +02:00
if (i < gameState.perks [ u . id ] ) bars . push ( ' < span class = "used" > < / span > ');
else if (i < newMax ) bars . push ( ' < span class = "free" > < / span > ');
else bars.push('< span class = "banned" > < / span > ');
2025-03-30 21:07:58 +02:00
}
2025-04-01 13:35:33 +02:00
const state = gameState.perks[u.id] & & 1 || !newMax & & 2 || 3;
2025-03-30 21:07:58 +02:00
return {
state,
html: `
< div class = "upgrade $ { [
2025-04-01 13:35:33 +02:00
"??",
2025-03-30 21:07:58 +02:00
"used",
2025-04-01 13:35:33 +02:00
"banned",
"free"
2025-03-30 21:07:58 +02:00
][state]}">
${u.icon}
< p >
< strong > ${u.name}< / strong >
${u.help(Math.max(1, gameState.perks[u.id]))}
< / p >
2025-04-01 18:33:58 +02:00
${bars.reverse().join("")}
2025-03-30 21:07:58 +02:00
< / div >
`
};
}).sort((a, b)=>a.state - b.state).map((a)=>a.html);
return ` < p > ${(0, _i18N.t)("score_panel.upgrades_picked")}< / p > ` + upgradesList.join("");
2025-03-29 21:22:19 +01:00
}
2025-04-04 09:45:35 +02:00
function levelsListHTMl(gameState, level) {
2025-03-29 21:22:19 +01:00
if (!gameState.perks.clairvoyant) return "";
2025-04-06 10:13:10 +02:00
if (gameState.creative) return "";
2025-03-29 21:22:19 +01:00
let list = "";
2025-04-04 09:45:35 +02:00
for(let i = 0; i < max_levels ( gameState ) ; i + + ) list + = ` < span style = "opacity: ${i >= level ? 1 : 0.2}" title = "${gameState.runLevels[i].name}" > ${(0, _loadGameData.icons)[gameState.runLevels[i].name]}< / span > `;
2025-03-29 21:22:19 +01:00
return `< p > ${(0, _i18N.t)("score_panel.upcoming_levels")}< / p > < p > ${list}< / p > `;
}
function currentLevelInfo(gameState) {
return gameState.level;
}
2025-04-01 21:37:07 +02:00
function isPickyEatingPossible(gameState) {
return gameState.bricks.indexOf(gameState.ballsColor) !== -1;
}
function reachRedRowIndex(gameState) {
if (!gameState.perks.reach) return -1;
const { size } = gameState.level;
let minY = -1, maxY = -1, maxYCount = -1;
for(let y = 0; y < size ; y + + ) for ( let x = 0; x < size ; x + + ) if ( gameState . bricks [ x + y * size ] ) {
if (minY == -1) minY = y;
if (maxY < y ) {
maxY = y;
maxYCount = 0;
}
if (maxY == y) maxYCount++;
}
if (maxY < 1 ) return -1 ;
if (maxY == minY) return -1;
if (maxYCount === size) return -1;
return maxY;
}
2025-04-04 09:45:35 +02:00
function telekinesisEffectRate(gameState, ball) {
return gameState.perks.telekinesis & & ball.vy < 0 & & ( 0 , _pureFunctions . clamp ) ( ball . y / gameState . gameZoneHeight * 1 . 1 + 0 . 1 , 0 , 1 ) | | 0 ;
2025-03-29 21:22:19 +01:00
}
2025-04-04 09:45:35 +02:00
function yoyoEffectRate(gameState, ball) {
return gameState.perks.yoyo & & ball.vy > 0 & & (0, _pureFunctions.clamp)(1 - ball.y / gameState.gameZoneHeight * 1.1 + 0.1, 0, 1) || 0;
2025-03-29 21:22:19 +01:00
}
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));
}
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
},
2025-04-15 16:47:04 +02:00
plouf: {
vol: 0,
x: 0
},
2025-03-29 21:22:19 +01:00
colorChange: {
vol: 0,
x: 0
}
}
};
}
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-04-02 19:36:03 +02:00
function isMovingWhilePassiveIncome(gameState) {
return !!(gameState.lastPuckMove & & gameState.perks.passive_income & & gameState.lastPuckMove > gameState.levelTime - 250 * gameState.perks.passive_income);
}
2025-04-06 10:13:10 +02:00
function getHighScore() {
2025-04-01 13:35:33 +02:00
try {
2025-04-06 10:13:10 +02:00
return parseInt(localStorage.getItem("breakout-3-hs-short") || "0");
2025-04-01 13:35:33 +02:00
} catch (e) {}
2025-04-06 10:13:10 +02:00
return 0;
}
function highScoreText() {
if (getHighScore()) return (0, _i18N.t)("main_menu.high_score", {
score: getHighScore()
});
2025-04-01 13:39:09 +02:00
return "";
2025-04-01 13:35:33 +02:00
}
2025-04-08 10:36:30 +02:00
let excluded;
function isExcluded(id) {
if (!excluded) {
excluded = new Set([
2025-04-07 14:08:48 +02:00
"extra_levels",
"extra_life",
"one_more_choice",
"instant_upgrade",
"shunt",
"slow_down"
]);
// Avoid excluding a perk that's needed for the required one
(0, _upgrades.rawUpgrades).forEach((u)=>{
if (u.requires) excluded.add(u.requires);
2025-04-06 15:38:30 +02:00
});
2025-04-08 10:36:30 +02:00
}
return excluded.has(id);
}
function getLevelUnlockCondition(levelIndex) {
let required = [], forbidden = [], minScore = Math.max(-1000 + 100 * levelIndex, 0);
if (levelIndex > 20) {
const possibletargets = (0, _upgrades.rawUpgrades).slice(0, Math.floor(levelIndex / 2)).map((u)=>u).filter((u)=>!isExcluded(u.id)).sort((a, b)=>(0, _getLevelBackground.hashCode)(levelIndex + a.id) - (0, _getLevelBackground.hashCode)(levelIndex + b.id));
2025-04-08 14:29:00 +02:00
const length = Math.min(3, Math.ceil(levelIndex / 30));
2025-04-07 14:08:48 +02:00
required = possibletargets.slice(0, length);
forbidden = possibletargets.slice(length, length + length);
2025-04-06 15:38:30 +02:00
}
2025-04-07 14:08:48 +02:00
return {
required,
forbidden,
minScore
};
}
function getBestScoreMatching(history, required = [], forbidden = []) {
return Math.max(0, ...history.filter((r)=>!required.find((u)=>!r?.perks?.[u.id]) & & !forbidden.find((u)=>r?.perks?.[u.id])).map((r)=>r.score));
}
function reasonLevelIsLocked(levelIndex, history, mentionBestScore) {
const { required, forbidden, minScore } = getLevelUnlockCondition(levelIndex);
const reached = getBestScoreMatching(history, required, forbidden);
let reachedText = reached & & mentionBestScore ? (0, _i18N.t)("unlocks.reached", {
reached
}) : "";
if (reached >= minScore) return null;
else if (!required.length & & !forbidden.length) return {
reached,
2025-04-06 15:38:30 +02:00
minScore,
2025-04-07 14:08:48 +02:00
text: (0, _i18N.t)("unlocks.minScore", {
minScore
}) + reachedText
};
else return {
reached,
minScore,
text: (0, _i18N.t)("unlocks.minScoreWithPerks", {
minScore,
required: required.map((u)=>u.name).join(", "),
forbidden: forbidden.map((u)=>u.name).join(", ")
}) + reachedText
};
2025-04-06 11:57:52 +02:00
}
2025-04-10 14:49:28 +02:00
function ballTransparency(ball, gameState) {
2025-04-10 21:40:45 +02:00
if (!gameState.perks.transparency) return 0;
return (0, _pureFunctions.clamp)(gameState.perks.transparency * (1 - ball.y / gameState.gameZoneHeight * 1.2), 0, 1);
2025-04-10 14:49:28 +02:00
}
2025-04-16 15:30:20 +02:00
function getCoinRenderColor(gameState, coin) {
if (gameState.perks.metamorphosis || (0, _options.isOptionOn)("colorful_coins") || gameState.perks.hypnosis || gameState.perks.rainbow) return coin.color;
return "#ffd300";
}
2025-04-18 21:17:32 +02:00
function getCornerOffset(gameState) {
return (gameState.levelTime ? gameState.perks.corner_shot * gameState.brickWidth : 0) - gameState.perks.unbounded * gameState.brickWidth;
}
2025-04-20 10:58:26 +02:00
const isInWebView = !!window.location.href.includes("isInWebView=true");
2025-03-29 21:22:19 +01:00
2025-04-18 17:15:47 +02:00
},{"./loadGameData":"l1B4x","./i18n/i18n":"eNPRm","./pure_functions":"6pQh7","./upgrades":"1u3Dx","./getLevelBackground":"7OIPf","./settings":"5blfu","./options":"d5NoS","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"2n0gK":[function(require,module,exports,__globalThis) {
2025-03-19 18:13:41 +01:00
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);
}
2025-04-18 21:17:32 +02:00
},{"b04459cc43e56e8c":"e1UyG","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"e1UyG":[function(require,module,exports,__globalThis) {
module.exports = require("c74501e5727c0abc").getBundleURL('8P7OJ') + "sw-b71.41cdff1b.js";
2025-03-19 18:13:41 +01:00
2025-04-18 21:17:32 +02:00
},{"c74501e5727c0abc":"lgJ39"}],"lgJ39":[function(require,module,exports,__globalThis) {
2025-04-06 11:27:26 +02:00
"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;
},{}],"9ZeQl":[function(require,module,exports,__globalThis) {
2025-03-19 18:13:41 +01:00
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);
2025-04-02 19:50:05 +02:00
parcelHelpers.export(exports, "increaseCombo", ()=>increaseCombo);
2025-03-19 18:13:41 +01:00
parcelHelpers.export(exports, "decreaseCombo", ()=>decreaseCombo);
parcelHelpers.export(exports, "spawnExplosion", ()=>spawnExplosion);
2025-03-23 22:19:11 +01:00
parcelHelpers.export(exports, "spawnImplosion", ()=>spawnImplosion);
2025-03-19 21:58:08 +01:00
parcelHelpers.export(exports, "explosionAt", ()=>explosionAt);
2025-03-19 18:13:41 +01:00
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, "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);
2025-03-19 18:13:41 +01:00
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");
2025-03-29 21:22:19 +01:00
var _pureFunctions = require("./pure_functions");
2025-04-08 14:03:38 +02:00
var _addToTotalScore = require("./addToTotalScore");
2025-04-12 20:01:43 +02:00
var _getLevelBackground = require("./getLevelBackground");
2025-03-19 18:13:41 +01:00
function setMousePos(gameState, x) {
2025-04-18 17:15:47 +02:00
if (gameState.startParams.computer_controlled) return;
2025-03-29 11:24:45 +01:00
gameState.puckPosition = x;
2025-03-19 18:13:41 +01:00
// Sets the puck position, and updates the ball position if they are supposed to follow it
gameState.needsRender = true;
}
function getBallDefaultVx(gameState) {
return (gameState.perks.concave_puck ? 0 : 1) * (Math.random() > 0.5 ? gameState.baseSpeed : -gameState.baseSpeed);
}
2025-04-12 20:01:43 +02:00
function computerControl(gameState) {
let targetX = gameState.puckPosition;
const ball = (0, _gameUtils.getClosestBall)(gameState, gameState.puckPosition, gameState.gameZoneHeight);
if (!ball) return;
const puckOffset = ((0, _getLevelBackground.hashCode)(gameState.runStatistics.puck_bounces + "goeirjgoriejg") % 100 - 50) / 100 * gameState.puckWidth;
if (ball.y > gameState.gameZoneHeight / 2 & & ball.vy > 0) targetX = ball.x + puckOffset;
else {
let coinsTotalX = 0, coinsCount = 0;
forEachLiveOne(gameState.coins, (c)=>{
if (c.vy > 0 & & c.y > gameState.gameZoneHeight / 2) {
coinsTotalX += c.x;
coinsCount++;
}
});
if (coinsCount) targetX = coinsTotalX / coinsCount;
2025-04-12 20:58:24 +02:00
else targetX = gameState.canvasWidth / 2;
2025-04-12 20:01:43 +02:00
}
gameState.puckPosition += (0, _pureFunctions.clamp)((targetX - gameState.puckPosition) / 10, -10, 10);
2025-04-18 21:17:32 +02:00
if (gameState.levelTime > 30000) (0, _game.startComputerControlledGame)(gameState.startParams.stress);
2025-04-12 20:01:43 +02:00
}
2025-03-19 18:13:41 +01:00
function resetBalls(gameState) {
2025-03-24 10:38:01 +01:00
// Always compute speed first
normalizeGameState(gameState);
2025-03-19 18:13:41 +01:00
const count = 1 + (gameState.perks?.multiball || 0);
const perBall = gameState.puckWidth / (count + 1);
gameState.balls = [];
2025-04-06 10:13:10 +02:00
gameState.ballsColor = "#FFFFFF";
if (gameState.perks.picky_eater || gameState.perks.pierce_color) gameState.ballsColor = (0, _gameUtils.getMajorityValue)(gameState.bricks.filter((i)=>i)) || "#FFFFFF";
2025-03-19 18:13:41 +01:00
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-23 19:11:01 +01:00
piercePoints: gameState.perks.pierce * 3,
2025-03-19 18:13:41 +01:00
hitSinceBounce: 0,
2025-03-22 16:04:25 +01:00
brokenSinceBounce: 0,
2025-03-19 18:13:41 +01:00
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);
2025-03-24 10:19:15 +01:00
// const vx = getBallDefaultVx(gameState);
2025-03-19 18:13:41 +01:00
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.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;
2025-03-19 18:13:41 +01:00
});
}
function normalizeGameState(gameState) {
// This function resets most parameters on the state to correct values, and should be used even when the game is paused
2025-04-06 10:13:10 +02:00
gameState.baseSpeed = Math.max(3, gameState.gameZoneWidth / 12 / 10 + gameState.currentLevel / 3 + gameState.levelTime / 30000 - gameState.perks.slow_down * 2);
2025-03-29 11:24:45 +01:00
gameState.puckWidth = Math.max(gameState.ballSize, gameState.gameZoneWidth / 12 * Math.min(12, 3 - gameState.perks.smaller_puck + gameState.perks.bigger_puck));
2025-04-18 21:17:32 +02:00
const corner = (0, _gameUtils.getCornerOffset)(gameState);
2025-04-11 20:34:11 +02:00
let minX = gameState.offsetXRoundedDown + gameState.puckWidth / 2 - corner;
let maxX = gameState.offsetXRoundedDown + gameState.gameZoneWidthRoundedUp - gameState.puckWidth / 2 + corner;
2025-03-29 21:22:19 +01:00
gameState.puckPosition = (0, _pureFunctions.clamp)(gameState.puckPosition, minX, maxX);
2025-03-19 18:13:41 +01:00
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;
2025-03-19 18:13:41 +01:00
}
function baseCombo(gameState) {
2025-04-08 08:57:41 +02:00
const mineFieldBonus = gameState.perks.minefield & & gameState.bricks.filter((b)=>b === "black").length * gameState.perks.minefield;
return 1 + gameState.perks.base_combo * 3 + gameState.perks.smaller_puck * 5 + mineFieldBonus;
2025-03-19 18:13:41 +01:00
}
function resetCombo(gameState, x, y) {
const prev = gameState.combo;
gameState.combo = baseCombo(gameState);
2025-03-29 21:22:19 +01:00
if (prev > gameState.combo & & gameState.perks.soft_reset) gameState.combo += Math.floor((prev - gameState.combo) * (0, _pureFunctions.comboKeepingRate)(gameState.perks.soft_reset));
2025-03-19 18:13:41 +01:00
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);
2025-04-06 10:13:10 +02:00
if (typeof x !== "undefined" & & typeof y !== "undefined") makeText(gameState, x, y, "#FF0000", "-" + lost, 20, 500 + (0, _pureFunctions.clamp)(lost, 0, 500));
2025-03-19 18:13:41 +01:00
}
return lost;
}
2025-04-02 19:50:05 +02:00
function increaseCombo(gameState, by, x, y) {
2025-04-02 20:03:57 +02:00
if (by < = 0) return;
gameState.combo += by;
2025-04-06 10:13:10 +02:00
if ((0, _options.isOptionOn)("comboIncreaseTexts") & & typeof x !== "undefined" & & typeof y !== "undefined") makeText(gameState, x, y, "#ffd300", "+" + by, 25, 400 + by);
2025-04-02 19:50:05 +02:00
}
2025-03-19 18:13:41 +01:00
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);
2025-04-06 10:13:10 +02:00
if (typeof x !== "undefined" & & typeof y !== "undefined") makeText(gameState, x, y, "#FF0000", "-" + lost, 20, 400 + lost);
2025-03-19 18:13:41 +01:00
}
}
function spawnExplosion(gameState, count, x, y, color) {
if (!!(0, _options.isOptionOn)("basic")) return;
2025-03-23 15:48:21 +01:00
if (liveCount(gameState.particles) > (0, _settings.getCurrentMaxParticles)()) // Avoid freezing when lots of explosion happen at once
2025-03-19 18:13:41 +01:00
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 ) ;
}
2025-03-23 22:19:11 +01:00
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-29 20:45:54 +01:00
function explosionAt(gameState, index, x, y, ball, extraSize = 0) {
const size = 1 + gameState.perks.bigger_explosions + Math.max(0, gameState.perks.implosions - 1) + extraSize;
2025-03-22 16:58:35 +01:00
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
2025-03-23 17:52:25 +01:00
gameState.brickHP[i]--;
2025-03-25 08:47:24 +01:00
if (gameState.brickHP[i] < = 0) explodeBrick(gameState, i, ball, true);
2025-03-22 16:58:35 +01:00
}
2025-03-19 21:58:08 +01:00
}
}
2025-03-23 22:19:11 +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));
2025-03-23 22:19:11 +01:00
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();
2025-04-06 10:13:10 +02:00
if (gameState.perks.implosions) spawnImplosion(gameState, 7 * size, x, y, "#FFFFFF");
else spawnExplosion(gameState, 7 * size, x, y, "#FFFFFF");
2025-03-19 21:58:08 +01:00
gameState.runStatistics.bricks_broken++;
if (gameState.perks.zen) resetCombo(gameState, x, y);
}
2025-03-19 18:13:41 +01:00
function explodeBrick(gameState, index, ball, isExplosion) {
const color = gameState.bricks[index];
if (!color) return;
2025-04-01 21:37:07 +02:00
const wasPickyEaterPossible = gameState.perks.picky_eater & & (0, _gameUtils.isPickyEatingPossible)(gameState);
const redRowReach = (0, _gameUtils.reachRedRowIndex)(gameState);
2025-03-30 21:07:58 +02:00
gameState.lastBrickBroken = gameState.levelTime;
2025-03-28 11:58:58 +01:00
if (color === "black") {
2025-03-19 18:13:41 +01:00
const x = (0, _gameUtils.brickCenterX)(gameState, index), y = (0, _gameUtils.brickCenterY)(gameState, index);
2025-03-28 11:58:58 +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-29 20:45:54 +01:00
explosionAt(gameState, index, x, y, ball, 0);
2025-04-08 08:57:41 +02:00
if (gameState.perks.minefield) decreaseCombo(gameState, gameState.perks.minefield, x, y);
2025-03-19 18:13:41 +01:00
} 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);
2025-03-23 17:52:25 +01:00
setBrick(gameState, index, "");
2025-03-19 18:13:41 +01:00
let coinsToSpawn = gameState.combo;
2025-04-08 08:57:41 +02:00
if (gameState.lastCombo > coinsToSpawn) // In case a reset happens in the same frame as a spawn, i want the combo to stay high (for minefield and zen in particular)
coinsToSpawn = gameState.lastCombo;
2025-03-19 18:13:41 +01:00
if (gameState.perks.sturdy_bricks) // +10% per level
2025-03-30 21:07:58 +02:00
coinsToSpawn += Math.ceil((2 + gameState.perks.sturdy_bricks) / 2 * coinsToSpawn);
2025-04-16 09:26:10 +02:00
if (gameState.perks.transparency) coinsToSpawn = Math.ceil(coinsToSpawn * (1 + (0, _gameUtils.ballTransparency)(ball, gameState) * gameState.perks.transparency / 2));
2025-03-19 18:13:41 +01:00
gameState.levelSpawnedCoins += coinsToSpawn;
gameState.runStatistics.coins_spawned += coinsToSpawn;
gameState.runStatistics.bricks_broken++;
2025-04-15 21:25:27 +02:00
const maxCoins = (0, _settings.getCurrentMaxCoins)();
const spawnableCoins = liveCount(gameState.coins) > (0, _settings.getCurrentMaxCoins)() ? 1 : Math.floor((maxCoins - liveCount(gameState.coins)) / 2);
2025-03-19 18:13:41 +01:00
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);
2025-04-04 12:07:24 +02:00
makeCoin(gameState, cx, cy, ball.previousVX * (0.5 + Math.random()), ball.previousVY * (0.5 + Math.random()), color, points);
2025-03-19 18:13:41 +01:00
}
2025-04-11 20:34:11 +02:00
increaseCombo(gameState, 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 * 3 + gameState.perks.zen + gameState.perks.passive_income + gameState.perks.addiction, ball.x, ball.y);
2025-04-08 08:57:41 +02:00
if (Math.abs(ball.y - y) < Math.abs ( ball . x - x ) ) {
if (gameState.perks.side_kick) {
if (ball.previousVX > 0) increaseCombo(gameState, gameState.perks.side_kick, ball.x, ball.y);
else decreaseCombo(gameState, gameState.perks.side_kick * 2, ball.x, ball.y);
}
if (gameState.perks.side_flip) {
if (ball.previousVX < 0 ) increaseCombo ( gameState , gameState . perks . side_flip , ball . x , ball . y ) ;
else decreaseCombo(gameState, gameState.perks.side_flip * 2, ball.x, ball.y);
}
2025-03-23 22:19:11 +01:00
}
2025-04-01 21:37:07 +02:00
if (redRowReach !== -1) {
if (Math.floor(index / gameState.level.size) === redRowReach) resetCombo(gameState, x, y);
else {
for(let x = 0; x < gameState.level.size ; x + + ) if ( gameState . bricks [ redRowReach * gameState . level . size + x ] ) gameState . combo + + ;
}
2025-03-20 21:02:51 +01:00
}
2025-04-02 19:36:03 +02:00
if ((0, _gameUtils.isMovingWhilePassiveIncome)(gameState)) resetCombo(gameState, x, y);
2025-03-19 18:13:41 +01:00
if (!isExplosion) {
// color change
if ((gameState.perks.picky_eater || gameState.perks.pierce_color) & & color !== gameState.ballsColor & & color) {
2025-04-01 21:37:07 +02:00
if (wasPickyEaterPossible) resetCombo(gameState, ball.x, ball.y);
2025-03-19 18:13:41 +01:00
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);
}
2025-03-28 10:21:14 +01:00
// makeLight(gameState, x, y, color, gameState.brickWidth, 40);
2025-03-19 18:13:41 +01:00
spawnExplosion(gameState, 5 + Math.min(gameState.combo, 30), x, y, color);
}
2025-03-29 15:00:44 +01:00
if (gameState.perks.respawn & & color !== "black" & & !gameState.bricks[index]) {
2025-03-29 21:22:19 +01:00
if (Math.random() < (0, _pureFunctions.comboKeepingRate)(gameState.perks.respawn)) append(gameState.respawns, (b)=>{
2025-03-29 15:00:44 +01:00
b.color = color;
b.index = index;
b.time = gameState.levelTime + 3000 / gameState.perks.respawn;
});
}
2025-03-19 18:13:41 +01:00
}
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,
2025-03-28 10:21:14 +01:00
score: Math.random() + (gameState.lastOffered[u.id] || 0)
2025-04-06 15:38:30 +02:00
})).sort((a, b)=>a.score - b.score).filter((u)=>gameState.perks[u.id] < u.max + gameState . perks . limitless ) . slice ( 0 , count ) . sort ( ( a , b ) = > a.id > b.id ? 1 : -1);
2025-03-19 18:13:41 +01:00
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;
2025-04-08 08:57:41 +02:00
if (!(0, _options.isOptionOn)("sound")) return;
2025-03-19 18:13:41 +01:00
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;
2025-04-08 14:03:38 +02:00
(0, _addToTotalScore.addToTotalScore)(gameState, coin.points);
2025-04-06 10:13:10 +02:00
if (gameState.score > gameState.highScore & & !gameState.creative) {
2025-03-19 18:13:41 +01:00
gameState.highScore = gameState.score;
2025-04-06 10:13:10 +02:00
localStorage.setItem("breakout-3-hs-short", gameState.score.toString());
2025-03-19 18:13:41 +01:00
}
2025-04-16 15:30:20 +02:00
if (!(0, _options.isOptionOn)("basic")) makeParticle(gameState, coin.previousX, coin.previousY, (gameState.canvasWidth - coin.x) / 100, -coin.y / 100, (0, _gameUtils.getCoinRenderColor)(gameState, coin), true, gameState.coinSize / 2, 100 + Math.random() * 50);
2025-03-28 11:58:58 +01:00
schedulGameSound(gameState, "coinCatch", coin.x, 1);
2025-03-19 18:13:41 +01:00
gameState.runStatistics.score += coin.points;
2025-04-03 21:59:01 +02:00
if (gameState.perks.asceticism) decreaseCombo(gameState, gameState.perks.asceticism * 3 * coin.points, coin.x, coin.y);
2025-03-19 18:13:41 +01:00
}
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;
2025-03-19 18:13:41 +01:00
(0, _game.pause)(false);
2025-03-20 22:50:50 +01:00
(0, _recording.stopRecording)();
2025-04-06 10:13:10 +02:00
if (l > 0) await (0, _game.openUpgradesPicker)(gameState);
2025-03-19 18:13:41 +01:00
gameState.currentLevel = l;
2025-04-12 15:39:32 +02:00
gameState.level = gameState.runLevels[l % gameState.runLevels.length];
2025-03-19 18:13:41 +01:00
gameState.levelTime = 0;
2025-03-22 16:47:02 +01:00
gameState.winAt = 0;
2025-03-19 18:13:41 +01:00
gameState.levelWallBounces = 0;
2025-04-02 19:36:03 +02:00
gameState.lastPuckMove = 0;
2025-03-19 18:13:41 +01:00
gameState.autoCleanUses = 0;
gameState.lastTickDown = gameState.levelTime;
gameState.levelStartScore = gameState.score;
gameState.levelSpawnedCoins = 0;
2025-03-29 20:45:54 +01:00
gameState.levelLostCoins = 0;
2025-03-19 18:13:41 +01:00
gameState.levelMisses = 0;
2025-03-30 21:07:58 +02:00
gameState.lastBrickBroken = 0;
2025-03-19 18:13:41 +01:00
gameState.runStatistics.levelsPlayed++;
// Reset combo silently
2025-03-20 08:13:17 +01:00
const finalCombo = gameState.combo;
gameState.combo = baseCombo(gameState);
2025-03-29 21:22:19 +01:00
if (gameState.perks.shunt) gameState.combo += Math.round(Math.max(0, (finalCombo - gameState.combo) * (0, _pureFunctions.comboKeepingRate)(gameState.perks.shunt)));
2025-03-30 21:07:58 +02:00
gameState.combo += gameState.perks.hot_start * 30;
2025-03-19 18:13:41 +01:00
const lvl = (0, _gameUtils.currentLevelInfo)(gameState);
if (lvl.size !== gameState.gridSize) {
gameState.gridSize = lvl.size;
2025-04-11 20:34:11 +02:00
(0, _game.fitSize)(gameState);
2025-03-19 18:13:41 +01:00
}
2025-03-29 20:45:54 +01:00
gameState.levelLostCoins += empty(gameState.coins);
2025-03-19 18:13:41 +01:00
empty(gameState.particles);
empty(gameState.lights);
empty(gameState.texts);
2025-03-29 15:00:44 +01:00
empty(gameState.respawns);
2025-03-23 17:52:25 +01:00
gameState.bricks = [];
for(let i = 0; i < lvl.size * lvl . size ; i + + ) setBrick ( gameState , i , lvl . bricks [ i ] ) ;
2025-03-22 16:04:25 +01:00
// Balls color will depend on most common brick color sometimes
resetBalls(gameState);
2025-03-19 18:13:41 +01:00
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;
2025-04-08 15:17:14 +02:00
document.body.style.setProperty("--level-background", lvl.color || "#000000");
document.getElementById("themeColor")?.setAttribute("content", lvl.color || "#000000");
2025-03-19 18:13:41 +01:00
}
2025-03-23 17:52:25 +01:00
function setBrick(gameState, index, color) {
2025-03-23 22:19:28 +01:00
gameState.bricks[index] = color || "";
2025-03-29 11:24:45 +01:00
gameState.brickHP[index] = color === "black" & & 1 || color & & 1 + gameState.perks.sturdy_bricks || 0;
2025-04-08 08:57:41 +02:00
if (gameState.perks.minefield & & color === "black") increaseCombo(gameState, gameState.perks.minefield, (0, _gameUtils.brickCenterX)(gameState, index), (0, _gameUtils.brickCenterY)(gameState, index));
2025-03-23 17:52:25 +01:00
}
2025-04-06 10:13:10 +02:00
const rainbow = [
2025-04-06 15:38:30 +02:00
"#ff2e2e",
"#ffe02e",
"#70ff33",
"#33ffa7",
"#38acff",
"#7038ff",
"#ff3de5"
2025-04-06 10:13:10 +02:00
];
2025-03-19 18:13:41 +01:00
function rainbowColor() {
2025-04-06 10:13:10 +02:00
return rainbow[Math.floor((0, _game.gameState).levelTime / 50) % rainbow.length];
2025-03-19 18:13:41 +01:00
}
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-29 20:45:54 +01:00
if (gameState.perks.ghost_coins) // slow down
{
if (typeof (vhit ?? hhit ?? chit) !== "undefined") {
coin.vy *= 1 - 0.2 / gameState.perks.ghost_coins;
coin.vx *= 1 - 0.2 / gameState.perks.ghost_coins;
}
} else {
2025-03-23 19:11:01 +01:00
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;
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;
2025-04-11 20:34:11 +02:00
if (coin.x < gameState.offsetXRoundedDown + radius ) {
2025-03-19 20:14:55 +01:00
coin.x = gameState.offsetXRoundedDown + radius + (gameState.offsetXRoundedDown + radius - coin.x);
coin.vx *= -1;
hhit = 1;
}
2025-04-11 20:34:11 +02:00
if (coin.y < radius ) {
2025-03-19 20:14:55 +01:00
coin.y = radius + (radius - coin.y);
coin.vy *= -1;
vhit = 1;
}
2025-04-11 20:34:11 +02:00
if (coin.x > gameState.canvasWidth - gameState.offsetXRoundedDown - radius) {
2025-03-19 20:14:55 +01:00
coin.x = gameState.canvasWidth - gameState.offsetXRoundedDown - radius - (coin.x - (gameState.canvasWidth - gameState.offsetXRoundedDown - radius));
coin.vx *= -1;
hhit = 1;
}
return hhit + vhit * 2;
}
2025-03-19 18:13:41 +01:00
function gameStateTick(gameState, // How many frames to compute at once, can go above 1 to compensate lag
frames = 1) {
2025-04-12 20:01:43 +02:00
// Ai movement of puck
2025-04-18 17:15:47 +02:00
if (gameState.startParams.computer_controlled) computerControl(gameState);
2025-03-19 18:13:41 +01:00
gameState.runStatistics.max_combo = Math.max(gameState.runStatistics.max_combo, gameState.combo);
2025-04-08 08:57:41 +02:00
gameState.lastCombo = gameState.combo;
2025-03-30 21:07:58 +02:00
if (gameState.perks.addiction & & gameState.lastBrickBroken & & gameState.lastBrickBroken < gameState.levelTime - 5000 / gameState . perks . addiction ) resetCombo ( gameState , gameState . puckPosition , gameState . gameZoneHeight - gameState . puckHeight * 2 ) ;
2025-03-19 18:13:41 +01:00
gameState.balls = gameState.balls.filter((ball)=>!ball.destroyed);
const remainingBricks = gameState.bricks.filter((b)=>b & & b !== "black").length;
2025-03-31 13:33:27 +02:00
if (!remainingBricks & & gameState.lastBrickBroken) // Avoid a combo reset just because we're waiting for coins
gameState.lastBrickBroken = 0;
2025-03-19 18:13:41 +01:00
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-29 15:00:44 +01:00
const hasPendingBricks = liveCount(gameState.respawns);
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;
2025-03-28 10:21:14 +01:00
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
2025-03-28 10:21:14 +01:00
gameState.levelTime & & !remainingBricks & & !liveCount(gameState.coins)) {
2025-04-18 21:17:32 +02:00
if (gameState.startParams.computer_controlled) (0, _game.startComputerControlledGame)(gameState.startParams.stress);
2025-04-12 20:01:43 +02:00
else if (gameState.currentLevel + 1 < (0, _gameUtils.max_levels)(gameState)) setLevel(gameState, gameState.currentLevel + 1);
2025-04-06 10:13:10 +02:00
else (0, _gameOver.gameOver)((0, _i18N.t)("gameOver.win.title"), (0, _i18N.t)("gameOver.win.summary", {
2025-04-01 18:33:58 +02:00
score: gameState.score
}));
2025-03-19 18:13:41 +01:00
} 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;
2025-03-19 18:13:41 +01:00
coin.vx += attractionX;
2025-03-19 20:14:55 +01:00
coin.vy += frames * (gameState.gameZoneHeight - coin.y) * strength / 2;
2025-03-19 18:13:41 +01:00
coin.sa -= attractionX / 10;
}
2025-04-10 21:40:45 +02:00
if (gameState.perks.ball_attracts_coins & & gameState.balls.length) {
2025-04-09 09:24:15 +02:00
// Find closest ball
2025-04-10 21:40:45 +02:00
let closestBall = (0, _gameUtils.getClosestBall)(gameState, coin.x, coin.y);
if (closestBall) {
let dist = (0, _gameUtils.distance2)(closestBall, coin);
const minDist = gameState.brickWidth * gameState.brickWidth;
if (dist > minDist & & dist < minDist * 16 * gameState . perks . ball_attracts_coins ) {
// Slow down coins in effect radius
const ratio = 1 - 0.02 * (0.5 + gameState.perks.ball_attracts_coins);
coin.vx *= ratio;
coin.vy *= ratio;
coin.vy *= ratio;
// Carry them
const dx = (closestBall.x - coin.x) / dist * 50 * gameState.perks.ball_attracts_coins;
const dy = (closestBall.y - coin.y) / dist * 50 * gameState.perks.ball_attracts_coins;
coin.vx += dx;
coin.vy += dy;
if (!(0, _options.isOptionOn)("basic") & & Math.random() * gameState.perks.ball_attracts_coins * frames > 0.9) makeParticle(gameState, coin.x + dx * 5, coin.y + dy * 5, dx * 2, dy * 2, rainbowColor(), true, gameState.coinSize / 2, 100);
}
}
}
2025-04-11 20:34:11 +02:00
if (gameState.perks.bricks_attract_coins) goToNearestBrick(gameState, coin, gameState.perks.bricks_attract_coins * frames, 2, false);
2025-04-15 16:47:04 +02:00
const ratio = 1 - (gameState.perks.viscosity * 0.03 + 0.002 + (coin.y > gameState.gameZoneHeight ? 0.2 : 0)) * frames / (1 + gameState.perks.etherealcoins);
2025-04-10 14:49:28 +02:00
if (!gameState.perks.etherealcoins) {
coin.vy *= ratio;
coin.vx *= ratio;
}
2025-03-19 18:13:41 +01:00
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-04-10 21:40:45 +02:00
const flip = gameState.perks.helium > 0 & & Math.abs(coin.x - gameState.puckPosition) * 2 > gameState.puckWidth + coin.size;
let dvy = frames * coin.weight * 0.8 * (flip ? -gameState.perks.helium : 1);
if (gameState.perks.etherealcoins) {
if (gameState.perks.helium) dvy *= 0.2 / gameState.perks.etherealcoins;
else dvy *= 0;
2025-03-19 20:14:55 +01:00
}
2025-04-10 21:40:45 +02:00
coin.vy += dvy;
if (gameState.perks.helium & & !(0, _options.isOptionOn)("basic") & & Math.random() < 0.1 * frames ) makeParticle ( gameState , coin . x , coin . y , 0 , dvy * 10 , gameState . perks . metamorphosis | | ( 0 , _options . isOptionOn ) ( " colorful_coins " ) ? coin . color : " # ffd300 " , true , 5 , 250 ) ;
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-04-15 21:25:27 +02:00
if (coin.previousY < gameState.gameZoneHeight & & coin . y > gameState.gameZoneHeight & & coin.vy > 0 & & speed > 20) {
schedulGameSound(gameState, "plouf", coin.x, (0, _pureFunctions.clamp)(speed, 20, 100) / 100 * 0.2);
2025-04-16 15:30:20 +02:00
if (!(0, _options.isOptionOn)("basic")) makeParticle(gameState, coin.x, gameState.gameZoneHeight, -coin.vx / 5, -coin.vy / 5, (0, _gameUtils.getCoinRenderColor)(gameState, coin), false);
2025-04-15 21:25:27 +02:00
}
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)) {
2025-03-29 09:25:17 +01:00
addToScore(gameState, coin);
2025-03-19 18:13:41 +01:00
destroy(gameState.coins, coinIndex);
2025-04-03 21:59:01 +02:00
} else if (coin.y > gameState.canvasHeight + coinRadius * 10) {
2025-03-29 20:45:54 +01:00
gameState.levelLostCoins += coin.points;
2025-03-19 18:13:41 +01:00
destroy(gameState.coins, coinIndex);
2025-04-02 19:52:47 +02:00
if (gameState.perks.compound_interest) resetCombo(gameState, coin.x, gameState.gameZoneHeight - 20);
2025-04-03 21:59:01 +02:00
if (gameState.combo < gameState.perks.fountain_toss * 30 & & Math . random ( ) < 1 / gameState . combo * gameState . perks . fountain_toss ) increaseCombo ( gameState , 1 , coin . x , gameState . gameZoneHeight - 20 ) ;
2025-03-29 20:45:54 +01:00
}
2025-03-19 20:14:55 +01:00
const hitBrick = coinBrickHitCheck(gameState, coin);
2025-03-28 10:21:14 +01:00
if (gameState.perks.metamorphosis & & typeof hitBrick !== "undefined") {
2025-03-29 11:24:45 +01:00
if (gameState.bricks[hitBrick] & & coin.color !== gameState.bricks[hitBrick] & & gameState.bricks[hitBrick] !== "black" & & coin.metamorphosisPoints) {
2025-03-23 17:52:25 +01:00
// Not using setbrick because we don't want to reset HP
2025-03-19 18:13:41 +01:00
gameState.bricks[hitBrick] = coin.color;
2025-03-29 11:24:45 +01:00
coin.metamorphosisPoints--;
2025-03-19 18:13:41 +01:00
schedulGameSound(gameState, "colorChange", coin.x, 0.3);
2025-04-10 21:40:45 +02:00
if (gameState.perks.hypnosis) {
const closestBall = (0, _gameUtils.getClosestBall)(gameState, coin.x, coin.y);
if (closestBall) {
coin.x = closestBall.x;
coin.y = closestBall.y;
coin.vx = (Math.random() - 0.5) * gameState.baseSpeed;
coin.vy = (Math.random() - 0.5) * gameState.baseSpeed;
coin.metamorphosisPoints = gameState.perks.metamorphosis;
}
}
2025-03-19 18:13:41 +01:00
}
}
2025-03-23 19:11:01 +01:00
if (!gameState.perks.ghost_coins & & typeof hitBrick !== "undefined" || hitBorder) {
2025-04-10 21:40:45 +02:00
const ratio = 1 - 0.2 / (1 + gameState.perks.etherealcoins);
coin.vx *= ratio;
coin.vy *= ratio;
if (Math.abs(coin.vy) < 1 ) coin . vy = 0;
2025-03-19 18:13:41 +01:00
coin.sa *= 0.9;
2025-03-28 19:40:59 +01:00
if (speed > 20 & & !coin.collidedLastFrame) schedulGameSound(gameState, "coinBounce", coin.x, 0.2);
coin.collidedLastFrame = true;
} else coin.collidedLastFrame = false;
2025-03-19 18:13:41 +01:00
});
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;
2025-03-29 21:22:19 +01:00
a.vx += (0, _pureFunctions.clamp)(a.x - x, -limit, limit) + (Math.random() - 0.5) * limit / 3;
a.vy += (0, _pureFunctions.clamp)(a.y - y, -limit, limit) + (Math.random() - 0.5) * limit / 3;
b.vx += (0, _pureFunctions.clamp)(b.x - x, -limit, limit) + (Math.random() - 0.5) * limit / 3;
b.vy += (0, _pureFunctions.clamp)(b.y - y, -limit, limit) + (Math.random() - 0.5) * limit / 3;
2025-03-19 21:58:08 +01:00
let index = (0, _game.brickIndex)(x, y);
2025-03-29 20:45:54 +01:00
explosionAt(gameState, index, x, y, a, Math.max(0, gameState.perks.shocks - 1));
2025-03-19 21:58:08 +01:00
}
}));
2025-03-19 18:13:41 +01:00
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) {
2025-04-11 20:34:11 +02:00
flash.vy += 0.5 * frames;
2025-03-19 18:13:41 +01:00
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
2025-04-06 10:13:10 +02:00
if (gameState.perks.top_is_lava) makeParticle(gameState, gameState.offsetXRoundedDown + Math.random() * gameState.gameZoneWidthRoundedUp, 0, (Math.random() - 0.5) * 10, 5, "#FF0000", 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, "#FF0000", 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, "#FF0000", true, gameState.coinSize / 2, 100 * (Math.random() + 1));
2025-03-19 18:13:41 +01:00
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 ) ;
2025-04-06 10:13:10 +02:00
makeParticle(gameState, x, gameState.gameZoneHeight, (Math.random() - 0.5) * 10, -5, "#FF0000", true, gameState.coinSize / 2, 100 * (Math.random() + 1));
2025-03-19 18:13:41 +01:00
}
if (gameState.perks.streak_shots) {
const pos = 0.5 - Math.random();
2025-04-06 10:13:10 +02:00
makeParticle(gameState, gameState.puckPosition + gameState.puckWidth * pos, gameState.gameZoneHeight - gameState.puckHeight, pos * 10, -5, "#FF0000", true, gameState.coinSize / 2, 100 * (Math.random() + 1));
2025-03-19 18:13:41 +01:00
}
}
2025-03-29 15:00:44 +01:00
// Respawn what's needed, show particles
forEachLiveOne(gameState.respawns, (r, ri)=>{
if (gameState.bricks[r.index]) destroy(gameState.respawns, ri);
else if (gameState.levelTime > r.time) {
setBrick(gameState, r.index, r.color);
destroy(gameState.respawns, ri);
2025-04-03 21:22:13 +02:00
} else {
2025-03-29 15:00:44 +01:00
const { index, color } = r;
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 18:13:41 +01:00
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);
});
}
2025-04-11 20:34:11 +02:00
function ballTick(gameState, ball, frames) {
2025-03-19 18:13:41 +01:00
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;
2025-04-04 09:45:35 +02:00
if ((0, _gameUtils.telekinesisEffectRate)(gameState, ball) > 0) {
2025-03-19 18:13:41 +01:00
speedLimitDampener += 3;
2025-04-11 20:34:11 +02:00
ball.vx += (gameState.puckPosition - ball.x) / 1000 * frames * gameState.perks.telekinesis * (0, _gameUtils.telekinesisEffectRate)(gameState, ball);
2025-03-19 18:13:41 +01:00
}
2025-04-04 09:45:35 +02:00
if ((0, _gameUtils.yoyoEffectRate)(gameState, ball) > 0) {
2025-03-19 20:14:55 +01:00
speedLimitDampener += 3;
2025-04-11 20:34:11 +02:00
ball.vx += (gameState.puckPosition - ball.x) / 1000 * frames * gameState.perks.yoyo * (0, _gameUtils.yoyoEffectRate)(gameState, ball);
2025-03-19 20:14:55 +01:00
}
2025-04-11 20:34:11 +02:00
if (ball.hitSinceBounce < gameState.perks.bricks_attract_ball * 3 ) goToNearestBrick ( gameState , ball , gameState . perks . bricks_attract_ball * frames * 0 . 2 , 2 + gameState . perks . bricks_attract_ball , Math . random ( ) < 0 . 5 * frames ) ;
2025-03-19 18:13:41 +01:00
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);
2025-04-11 20:34:11 +02:00
const borderHitCode = bordersHitCheck(gameState, ball, gameState.ballSize / 2, frames);
2025-03-19 18:13:41 +01:00
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);
2025-04-04 09:45:35 +02:00
if (gameState.perks.top_is_lava & & borderHitCode >= 2) resetCombo(gameState, ball.x, ball.y + gameState.ballSize * 3);
2025-04-01 13:35:33 +02:00
if (gameState.perks.trampoline) decreaseCombo(gameState, gameState.perks.trampoline, ball.x, ball.y + gameState.ballSize);
2025-03-19 18:13:41 +01:00
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 ;
2025-04-08 10:36:30 +02:00
if (ball.y > ylimit & & ball.vy > 0 & & (ballIsUnderPuck || gameState.balls.length < 2 & & gameState . perks . extra_life & & ball . y > ylimit + gameState.puckHeight / 2)) {
2025-03-19 18:13:41 +01:00
if (ballIsUnderPuck) {
const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
2025-03-29 20:45:54 +01:00
const angle = Math.atan2(-gameState.puckWidth / 2, (ball.x - gameState.puckPosition) * (gameState.perks.concave_puck ? -1 / (1 + gameState.perks.concave_puck) : 1));
2025-03-19 18:13:41 +01:00
ball.vx = speed * Math.cos(angle);
ball.vy = speed * Math.sin(angle);
schedulGameSound(gameState, "wallBeep", ball.x, 1);
} else {
ball.vy *= -1;
2025-03-28 11:58:58 +01:00
justLostALife(gameState, ball, ball.x, ball.y);
2025-03-19 18:13:41 +01:00
}
if (gameState.perks.streak_shots) resetCombo(gameState, ball.x, ball.y);
2025-04-02 19:50:05 +02:00
if (gameState.perks.trampoline) increaseCombo(gameState, gameState.perks.trampoline, ball.x, ball.y);
2025-04-02 19:36:03 +02:00
if (gameState.perks.nbricks & & ball.hitSinceBounce < gameState.perks.nbricks ) resetCombo ( gameState , ball . x , ball . y ) ;
2025-03-23 19:11:01 +01:00
if (!ball.hitSinceBounce & & gameState.bricks.find((i)=>i)) {
2025-03-19 18:13:41 +01:00
gameState.runStatistics.misses++;
2025-03-19 21:58:08 +01:00
if (gameState.perks.forgiving) {
2025-03-29 15:00:44 +01:00
const loss = Math.floor(gameState.levelMisses / 10 / gameState.perks.forgiving * (gameState.combo - baseCombo(gameState)));
2025-03-20 21:24:25 +01:00
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++;
2025-04-06 10:13:10 +02:00
makeText(gameState, gameState.puckPosition, gameState.gameZoneHeight - gameState.puckHeight * 2, "#FF0000", (0, _i18N.t)("play.missed_ball"), gameState.puckHeight, 500);
2025-03-19 18:13:41 +01:00
}
gameState.runStatistics.puck_bounces++;
ball.hitSinceBounce = 0;
2025-03-22 16:04:25 +01:00
ball.brokenSinceBounce = 0;
2025-03-19 18:13:41 +01:00
ball.sapperUses = 0;
2025-03-23 19:11:01 +01:00
ball.piercePoints = gameState.perks.pierce * 3;
2025-03-19 18:13:41 +01:00
}
2025-04-11 20:34:11 +02:00
if (gameState.running & & ball.y > gameState.gameZoneHeight + gameState.ballSize / 2) {
2025-03-19 18:13:41 +01:00
ball.destroyed = true;
gameState.runStatistics.balls_lost++;
2025-04-12 20:01:43 +02:00
if (!gameState.balls.find((b)=>!b.destroyed)) {
2025-04-18 21:17:32 +02:00
if (gameState.startParams.computer_controlled) (0, _game.startComputerControlledGame)(gameState.startParams.stress);
2025-04-12 20:01:43 +02:00
else (0, _gameOver.gameOver)((0, _i18N.t)("gameOver.lost.title"), (0, _i18N.t)("gameOver.lost.summary", {
score: gameState.score
}));
}
2025-03-19 18:13:41 +01:00
}
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-30 21:07:58 +02:00
const initialBrickColor = gameState.bricks[hitBrick];
2025-03-19 20:14:55 +01:00
ball.hitSinceBounce++;
2025-04-02 19:50:05 +02:00
if (gameState.perks.nbricks) {
if (ball.hitSinceBounce > gameState.perks.nbricks) resetCombo(gameState, ball.x, ball.y);
else increaseCombo(gameState, gameState.perks.nbricks, ball.x, ball.y);
}
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;
2025-03-30 21:07:58 +02:00
const used = Math.min(ball.piercePoints, Math.max(1, gameState.brickHP[hitBrick] + 1));
2025-03-23 19:11:01 +01:00
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
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]) {
2025-03-23 17:52:25 +01:00
setBrick(gameState, hitBrick, "black");
2025-03-22 16:04:25 +01:00
ball.sapperUses++;
}
2025-03-28 10:21:14 +01:00
} else {
2025-03-28 19:40:59 +01:00
schedulGameSound(gameState, "wallBeep", x, 1);
2025-04-06 10:13:10 +02:00
makeLight(gameState, (0, _gameUtils.brickCenterX)(gameState, hitBrick), (0, _gameUtils.brickCenterY)(gameState, hitBrick), "#FFFFFF", gameState.brickWidth + 2, 50 * gameState.brickHP[hitBrick]);
2025-03-19 18:13:41 +01:00
}
}
2025-04-10 21:40:45 +02:00
if (!(0, _options.isOptionOn)("basic") & & (0, _gameUtils.ballTransparency)(ball, gameState) < Math.random ( ) ) {
2025-03-23 19:11:01 +01:00
const remainingPierce = ball.piercePoints;
2025-03-19 18:13:41 +01:00
const remainingSapper = ball.sapperUses < gameState.perks.sapper ;
2025-04-01 21:43:36 +02:00
const willMiss = (0, _options.isOptionOn)("red_miss") & & ball.vy > 0 & & !ball.hitSinceBounce;
2025-03-19 18:13:41 +01:00
const extraCombo = gameState.combo - 1;
2025-04-01 21:43:36 +02:00
if (willMiss || extraCombo & & Math.random() > 0.1 / (1 + extraCombo) || remainingSapper & & Math.random() > 0.1 / (1 + remainingSapper) || extraCombo & & Math.random() > 0.1 / (1 + extraCombo)) {
2025-04-06 10:13:10 +02:00
const color = remainingSapper & & (Math.random() > 0.5 ? "#ffb92a" : "#FF0000") || willMiss & & "#FF0000" || gameState.ballsColor;
2025-03-19 18:13:41 +01:00
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);
}
}
}
2025-03-28 11:58:58 +01:00
function justLostALife(gameState, ball, x, y) {
gameState.perks.extra_life -= 1;
if (gameState.perks.extra_life < 0 ) gameState . perks . extra_life = 0;
2025-03-29 15:00:44 +01:00
else if (gameState.perks.sacrifice) {
gameState.combo *= gameState.perks.sacrifice;
gameState.bricks.forEach((color, index)=>color & & explodeBrick(gameState, index, ball, true));
}
2025-03-28 11:58:58 +01:00
schedulGameSound(gameState, "lifeLost", ball.x, 1);
2025-04-06 10:13:10 +02:00
if (!(0, _options.isOptionOn)("basic")) for(let i = 0; i < 10 ; i + + ) makeParticle ( gameState , x , y , Math . random ( ) * gameState . baseSpeed * 3 , gameState . baseSpeed * 3 , " # FF0000 " , false , gameState . coinSize / 2 , 150 ) ;
2025-03-28 11:58:58 +01:00
}
2025-04-06 10:13:10 +02:00
function makeCoin(gameState, x, y, vx, vy, color = "#ffd300", points = 1) {
2025-03-29 09:25:17 +01:00
let weight = 0.8 + Math.random() * 0.2 + Math.min(2, points * 0.01);
2025-03-29 20:45:54 +01:00
weight *= 5 / (5 + gameState.perks.etherealcoins);
2025-04-10 14:49:28 +02:00
if (gameState.perks.trickledown) y = -20;
2025-04-10 21:40:45 +02:00
if (gameState.perks.rainbow & & Math.random() > 1 / (1 + gameState.perks.rainbow)) color = rainbowColor();
2025-03-19 18:13:41 +01:00
append(gameState.coins, (p)=>{
p.x = x;
p.y = y;
2025-03-28 19:40:59 +01:00
p.collidedLastFrame = true;
2025-03-19 18:13:41 +01:00
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;
2025-03-19 18:13:41 +01:00
p.color = color;
p.a = Math.random() * Math.PI * 2;
p.sa = Math.random() - 0.5;
p.points = points;
2025-03-29 09:25:17 +01:00
p.weight = weight;
2025-03-29 11:24:45 +01:00
p.metamorphosisPoints = gameState.perks.metamorphosis;
2025-03-19 18:13:41 +01:00
});
}
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;
});
}
2025-03-29 17:40:07 +01:00
function makeText(gameState, x, y, color, text, size = 20, duration = 500) {
2025-03-19 18:13:41 +01:00
append(gameState.texts, (p)=>{
p.time = gameState.levelTime;
p.x = x;
p.y = y;
p.color = color;
p.size = size;
2025-03-29 21:22:19 +01:00
p.duration = (0, _pureFunctions.clamp)(duration, 400, 2000);
2025-03-19 18:13:41 +01:00
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) {
2025-03-29 20:45:54 +01:00
let destroyed = 0;
2025-03-19 18:13:41 +01:00
where.total = 0;
where.indexMin = 0;
2025-03-29 20:45:54 +01:00
where.list.forEach((i)=>{
if (!i.destroyed) {
i.destroyed = true;
destroyed++;
}
});
return destroyed;
2025-03-19 18:13:41 +01:00
}
function forEachLiveOne(where, cb) {
where.list.forEach((item, index)=>{
if (item & & !item.destroyed) cb(item, index);
});
}
2025-04-11 20:34:11 +02:00
function goToNearestBrick(gameState, coin, strength, size = 2, particle = false) {
const row = Math.floor(coin.y / gameState.brickWidth);
const col = Math.floor((coin.x - gameState.offsetX) / gameState.brickWidth);
let vx = 0, vy = 0;
for(let dcol = -size; dcol < size ; dcol + + ) for ( let drow = -size; drow < size ; drow + + ) {
const index = (0, _gameUtils.getRowColIndex)(gameState, row + drow, col + dcol);
if (gameState.bricks[index]) {
const dx = (0, _gameUtils.brickCenterX)(gameState, index) + (0, _pureFunctions.clamp)(-dcol, -1, 1) * gameState.brickWidth / 2 - coin.x;
const dy = (0, _gameUtils.brickCenterY)(gameState, index) + (0, _pureFunctions.clamp)(-drow, -1, 1) * gameState.brickWidth / 2 - coin.y;
const d2 = dx * dx + dy * dy;
vx += dx / d2 * 20;
vy += dy / d2 * 20;
}
}
coin.vx += vx * strength;
coin.vy += vy * strength;
const s2 = coin.vx * coin.vx + coin.vy * coin.vy;
if (s2 > gameState.baseSpeed * gameState.baseSpeed * 2) {
coin.vx *= 0.95;
coin.vy *= 0.95;
}
if ((vx || vy) & & particle) makeParticle(gameState, coin.x, coin.y, -vx * 2, -vy * 2, rainbowColor(), true);
}
2025-03-19 18:13:41 +01:00
2025-04-15 16:47:04 +02:00
},{"./game_utils":"cEeac","./i18n/i18n":"eNPRm","./loadGameData":"l1B4x","./settings":"5blfu","./render":"9AS2t","./gameOver":"caCAf","./game":"edeGs","./recording":"godmD","./options":"d5NoS","./pure_functions":"6pQh7","./addToTotalScore":"ka4dG","./getLevelBackground":"7OIPf","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"9AS2t":[function(require,module,exports,__globalThis) {
2025-03-19 18:13:41 +01:00
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);
2025-04-03 15:15:00 +02:00
parcelHelpers.export(exports, "haloCanvas", ()=>haloCanvas);
2025-04-18 21:17:32 +02:00
parcelHelpers.export(exports, "getHaloScale", ()=>getHaloScale);
2025-03-19 18:13:41 +01:00
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);
2025-03-19 18:13:41 +01:00
var _gameStateMutators = require("./gameStateMutators");
var _gameUtils = require("./game_utils");
var _i18N = require("./i18n/i18n");
var _game = require("./game");
var _options = require("./options");
2025-04-08 21:54:19 +02:00
var _pureFunctions = require("./pure_functions");
2025-03-19 18:13:41 +01:00
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 > `);
2025-03-29 20:45:54 +01:00
bombSVG.onload = ()=>(0, _game.gameState).needsRender = true;
2025-03-19 18:13:41 +01:00
const background = document.createElement("img");
2025-03-30 21:07:58 +02:00
background.onload = ()=>(0, _game.gameState).needsRender = true;
2025-03-19 18:13:41 +01:00
const backgroundCanvas = document.createElement("canvas");
2025-04-03 15:15:00 +02:00
const haloCanvas = document.createElement("canvas");
const haloCanvasCtx = haloCanvas.getContext("2d", {
alpha: false
});
2025-04-18 21:17:32 +02:00
function getHaloScale() {
return 16 * ((0, _options.isOptionOn)("precise_lighting") ? 1 : 2);
}
2025-04-20 09:33:12 +02:00
let framesCounter = 0;
2025-03-19 18:13:41 +01:00
function render(gameState) {
2025-04-20 09:33:12 +02:00
framesCounter++;
2025-04-15 21:28:00 +02:00
(0, _game.startWork)("render:init");
2025-03-19 18:13:41 +01:00
const level = (0, _gameUtils.currentLevelInfo)(gameState);
2025-03-25 08:33:09 +01:00
const hasCombo = gameState.combo > (0, _gameStateMutators.baseCombo)(gameState);
2025-03-19 18:13:41 +01:00
const { width, height } = gameCanvas;
if (!width || !height) return;
2025-04-06 10:13:10 +02:00
if (gameState.currentLevel || gameState.levelTime) menuLabel.innerText = (0, _i18N.t)("play.current_lvl", {
2025-03-19 18:13:41 +01:00
level: gameState.currentLevel + 1,
max: (0, _gameUtils.max_levels)(gameState)
});
else menuLabel.innerText = (0, _i18N.t)("play.menu_label");
2025-03-29 20:45:54 +01:00
const catchRate = gameState.levelSpawnedCoins ? (gameState.levelSpawnedCoins - gameState.levelLostCoins) / gameState.levelSpawnedCoins : 1;
2025-04-15 21:28:00 +02:00
(0, _game.startWork)("render:scoreDisplay");
2025-04-18 17:15:47 +02:00
scoreDisplay.innerHTML = ((0, _options.isOptionOn)("show_fps") || gameState.startParams.computer_controlled ? `
2025-04-15 21:25:27 +02:00
< span class = "${Math.abs((0, _game.lastMeasuredFPS) - 60) < 2 && " " | | Math . abs ( ( 0 , _game . lastMeasuredFPS ) - 60 ) < 10 & & " good " | | " bad " } " >
2025-03-29 21:05:53 +01:00
${0, _game.lastMeasuredFPS} FPS
< / span > < span > / < / span >
2025-03-29 21:28:05 +01:00
` : "") + ((0, _options.isOptionOn)("show_stats") ? `
2025-04-08 21:54:19 +02:00
< span class = "${catchRate > (0, _pureFunctions.catchRateBest) / 100 && " great " | | catchRate > (0, _pureFunctions.catchRateGood) / 100 & & "good" || ""}" data-tooltip="${(0, _i18N.t)("play.stats.coins_catch_rate")}">
2025-03-29 20:45:54 +01:00
${Math.floor(catchRate * 100)}%
< / span > < span > / < / span >
2025-04-08 21:54:19 +02:00
< span class = "${gameState.levelTime < (0, _pureFunctions.levelTimeBest) * 1000 && " great " | | gameState . levelTime < ( 0 , _pureFunctions . levelTimeGood ) * 1000 & & " good " | | " " } " data-tooltip = "${(0, _i18N.t)(" play . stats . levelTime " ) } " >
2025-03-29 20:45:54 +01:00
${Math.ceil(gameState.levelTime / 1000)}s
< / span > < span > / < / span >
2025-04-08 21:54:19 +02:00
< span class = "${gameState.levelWallBounces < (0, _pureFunctions.wallBouncedBest) && " great " | | gameState . levelWallBounces < ( 0 , _pureFunctions . wallBouncedGood ) & & " good " | | " " } " data-tooltip = "${(0, _i18N.t)(" play . stats . levelWallBounces " ) } " >
2025-03-30 21:07:58 +02:00
${gameState.levelWallBounces} B
< / span > < span > / < / span >
2025-04-08 21:54:19 +02:00
< span class = "${gameState.levelMisses < (0, _pureFunctions.missesBest) && " great " | | gameState . levelMisses < ( 0 , _pureFunctions . missesGood ) & & " good " | | " " } " data-tooltip = "${(0, _i18N.t)(" play . stats . levelMisses " ) } " >
2025-03-29 20:45:54 +01:00
${gameState.levelMisses} M
< / span > < span > / < / span >
2025-04-01 18:26:40 +02:00
` : "") + `< span class = "score" data-tooltip = "${(0, _i18N.t)(" play . score_tooltip " ) } " > $${gameState.score}< / span > `;
2025-04-18 17:15:47 +02:00
scoreDisplay.className = gameState.startParams.computer_controlled & & "computer_controlled" || gameState.lastScoreIncrease > gameState.levelTime - 500 & & "active" || "";
2025-03-19 18:13:41 +01:00
// Clear
2025-04-03 16:10:51 +02:00
if (!(0, _options.isOptionOn)("basic") & & level.svg & & level.color === "#000000") {
2025-04-20 09:33:55 +02:00
const skipN = (0, _options.isOptionOn)("probabilistic_lighting") & & (0, _gameStateMutators.liveCount)(gameState.coins) > 150 ? 3 : 0;
2025-04-20 09:33:12 +02:00
const shouldSkip = (index)=>skipN ? (framesCounter + index) % (skipN + 1) !== 0 : false;
2025-04-18 21:17:32 +02:00
const haloScale = getHaloScale();
2025-04-15 21:28:00 +02:00
(0, _game.startWork)("render:halo:clear");
2025-04-03 15:15:00 +02:00
haloCanvasCtx.globalCompositeOperation = "source-over";
2025-04-20 09:33:12 +02:00
haloCanvasCtx.globalAlpha = skipN ? 0.1 : 0.99;
2025-04-03 16:10:51 +02:00
haloCanvasCtx.fillStyle = level.color;
2025-04-03 15:15:00 +02:00
haloCanvasCtx.fillRect(0, 0, width / haloScale, height / haloScale);
2025-04-06 10:13:10 +02:00
const brightness = (0, _options.isOptionOn)("extra_bright") ? 3 : 1;
haloCanvasCtx.globalCompositeOperation = "lighten";
2025-04-08 08:57:41 +02:00
haloCanvasCtx.globalAlpha = 0.1 + 5 / ((0, _gameStateMutators.liveCount)(gameState.coins) + 10);
2025-04-15 21:28:00 +02:00
(0, _game.startWork)("render:halo:coins");
2025-04-20 09:33:12 +02:00
(0, _gameStateMutators.forEachLiveOne)(gameState.coins, (coin, index)=>{
if (shouldSkip(index)) return;
2025-04-16 15:30:20 +02:00
const color = (0, _gameUtils.getCoinRenderColor)(gameState, coin);
2025-04-06 10:13:10 +02:00
drawFuzzyBall(haloCanvasCtx, color, gameState.coinSize * 2 * brightness / haloScale, coin.x / haloScale, coin.y / haloScale);
2025-03-19 18:13:41 +01:00
});
2025-04-15 21:28:00 +02:00
(0, _game.startWork)("render:halo:balls");
2025-04-20 09:33:12 +02:00
gameState.balls.forEach((ball, index)=>{
if (shouldSkip(index)) return;
2025-04-10 21:40:45 +02:00
haloCanvasCtx.globalAlpha = 0.3 * (1 - (0, _gameUtils.ballTransparency)(ball, gameState));
2025-04-07 15:25:58 +02:00
drawFuzzyBall(haloCanvasCtx, gameState.ballsColor, gameState.ballSize * 2 * brightness / haloScale, ball.x / haloScale, ball.y / haloScale);
2025-03-19 18:13:41 +01:00
});
2025-04-15 21:28:00 +02:00
(0, _game.startWork)("render:halo:bricks");
2025-04-06 10:13:10 +02:00
haloCanvasCtx.globalAlpha = 0.05;
2025-03-19 18:13:41 +01:00
gameState.bricks.forEach((color, index)=>{
if (!color) return;
2025-04-20 09:33:12 +02:00
if (shouldSkip(index)) return;
2025-03-19 18:13:41 +01:00
const x = (0, _gameUtils.brickCenterX)(gameState, index), y = (0, _gameUtils.brickCenterY)(gameState, index);
2025-04-07 16:52:42 +02:00
drawFuzzyBall(haloCanvasCtx, color == "black" ? "#666666" : color, // Perf could really go down there because of the size of the halo
Math.min(200, gameState.brickWidth * 1.5 * brightness) / haloScale, x / haloScale, y / haloScale);
2025-03-19 18:13:41 +01:00
});
2025-04-15 21:28:00 +02:00
(0, _game.startWork)("render:halo:particles");
2025-04-06 10:13:10 +02:00
haloCanvasCtx.globalCompositeOperation = "screen";
2025-04-20 09:33:12 +02:00
(0, _gameStateMutators.forEachLiveOne)(gameState.particles, (flash, index)=>{
if (shouldSkip(index)) return;
2025-03-19 18:13:41 +01:00
const { x, y, time, color, size, duration } = flash;
const elapsed = gameState.levelTime - time;
2025-04-07 16:52:42 +02:00
haloCanvasCtx.globalAlpha = 0.1 * Math.min(1, 2 - elapsed / duration * 2);
2025-04-06 10:13:10 +02:00
drawFuzzyBall(haloCanvasCtx, color, size * 3 * brightness / haloScale, x / haloScale, y / haloScale);
2025-03-19 18:13:41 +01:00
});
2025-04-18 21:17:32 +02:00
(0, _game.startWork)("render:halo:scale_up");
2025-04-03 15:15:00 +02:00
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
2025-04-07 16:52:42 +02:00
ctx.imageSmoothingQuality = "high";
2025-04-18 21:17:32 +02:00
ctx.imageSmoothingEnabled = (0, _options.isOptionOn)("smooth_lighting") || false;
2025-04-03 15:15:00 +02:00
ctx.drawImage(haloCanvas, 0, 0, width, height);
2025-04-07 16:52:42 +02:00
ctx.imageSmoothingEnabled = false;
2025-04-15 21:28:00 +02:00
(0, _game.startWork)("render:halo:pattern");
2025-04-03 21:22:13 +02:00
ctx.globalAlpha = 1;
2025-03-19 18:13:41 +01:00
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");
2025-04-03 16:10:51 +02:00
bgctx.globalCompositeOperation = "source-over";
2025-03-19 18:13:41 +01:00
bgctx.fillStyle = level.color || "#000";
bgctx.fillRect(0, 0, gameState.canvasWidth, gameState.canvasHeight);
2025-03-29 20:45:54 +01:00
if (gameState.perks.clairvoyant >= 3) {
2025-03-29 21:28:05 +01:00
const pageSource = document.body.innerHTML.replace(/\s+/gi, "");
2025-03-29 20:45:54 +01:00
const lineWidth = Math.ceil(gameState.canvasWidth / 15);
const lines = Math.ceil(gameState.canvasHeight / 20);
const chars = lineWidth * lines;
let start = Math.ceil(Math.random() * (pageSource.length - chars));
for(let i = 0; i < lines ; i + + ) {
2025-04-06 10:13:10 +02:00
bgctx.fillStyle = "#FFFFFF";
2025-03-29 21:28:05 +01:00
bgctx.font = "20px Courier";
2025-03-29 20:45:54 +01:00
bgctx.fillText(pageSource.slice(start + i * lineWidth, start + (i + 1) * lineWidth), 0, i * 20, gameState.canvasWidth);
}
} else {
const pattern = ctx.createPattern(background, "repeat");
if (pattern) {
2025-04-03 16:10:51 +02:00
bgctx.globalCompositeOperation = "screen";
2025-03-29 20:45:54 +01:00
bgctx.fillStyle = pattern;
bgctx.fillRect(0, 0, width, height);
}
2025-03-19 18:13:41 +01:00
}
}
2025-04-03 16:10:51 +02:00
ctx.globalCompositeOperation = "darken";
2025-03-19 18:13:41 +01:00
ctx.drawImage(backgroundCanvas, 0, 0);
} else {
// Background not loaded yes
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, width, height);
}
} else {
2025-04-15 21:28:00 +02:00
(0, _game.startWork)("render:halo-basic");
2025-03-19 18:13:41 +01:00
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);
});
}
2025-04-15 21:28:00 +02:00
(0, _game.startWork)("render:explosionshake");
2025-03-19 18:13:41 +01:00
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-04-15 21:28:00 +02:00
(0, _game.startWork)("render:coins");
2025-03-19 18:13:41 +01:00
// Coins
ctx.globalAlpha = 1;
(0, _gameStateMutators.forEachLiveOne)(gameState.coins, (coin)=>{
2025-04-16 15:30:20 +02:00
const color = (0, _gameUtils.getCoinRenderColor)(gameState, coin);
2025-04-10 21:40:45 +02:00
const hollow = gameState.perks.metamorphosis & & !coin.metamorphosisPoints;
2025-04-06 10:13:10 +02:00
ctx.globalCompositeOperation = "source-over";
2025-04-10 21:40:45 +02:00
drawCoin(ctx, hollow ? "transparent" : color, coin.size, coin.x, coin.y, // Red border around coins with asceticism
2025-04-09 09:24:15 +02:00
hasCombo & & gameState.perks.asceticism & & "#FF0000" || // Gold coins
// (color === "#ffd300" & & "#ffd300") ||
2025-04-10 21:40:45 +02:00
hollow & & color || gameState.level.color, coin.a);
2025-03-19 18:13:41 +01:00
});
2025-04-15 21:28:00 +02:00
(0, _game.startWork)("render:ball shade");
2025-03-19 18:13:41 +01:00
// Black shadow around balls
if (!(0, _options.isOptionOn)("basic")) {
ctx.globalCompositeOperation = "source-over";
gameState.balls.forEach((ball)=>{
2025-04-10 14:49:28 +02:00
ctx.globalAlpha = Math.min(0.8, (0, _gameStateMutators.liveCount)(gameState.coins) / 20) * (1 - (0, _gameUtils.ballTransparency)(ball, gameState));
2025-03-19 18:13:41 +01:00
drawBall(ctx, level.color || "#000", gameState.ballSize * 6, ball.x, ball.y);
});
}
2025-04-15 21:28:00 +02:00
(0, _game.startWork)("render:bricks");
2025-03-19 18:13:41 +01:00
ctx.globalCompositeOperation = "source-over";
renderAllBricks();
2025-04-15 21:28:00 +02:00
(0, _game.startWork)("render:lights");
2025-03-19 18:13:41 +01:00
ctx.globalCompositeOperation = "screen";
2025-03-28 10:21:14 +01:00
(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;
2025-04-06 10:13:10 +02:00
drawBrick(gameState, ctx, color, x, y, -1, gameState.perks.clairvoyant >= 2);
2025-03-28 10:21:14 +01:00
});
2025-04-15 21:28:00 +02:00
(0, _game.startWork)("render:texts");
2025-03-28 10:21:14 +01:00
ctx.globalCompositeOperation = "screen";
2025-03-19 18:13:41 +01:00
(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);
});
2025-04-15 21:28:00 +02:00
(0, _game.startWork)("render:particles");
2025-03-19 18:13:41 +01:00
(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);
});
2025-04-15 21:28:00 +02:00
(0, _game.startWork)("render:extra_life");
2025-03-19 18:13:41 +01:00
if (gameState.perks.extra_life) {
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
ctx.fillStyle = gameState.puckColor;
2025-04-11 20:34:11 +02:00
for(let i = 0; i < gameState.perks.extra_life ; i + + ) ctx . fillRect ( gameState . offsetXRoundedDown , gameState . gameZoneHeight - gameState . puckHeight / 2 + 2 * i , gameState . gameZoneWidthRoundedUp , 1 ) ;
2025-03-19 18:13:41 +01:00
}
2025-04-15 21:28:00 +02:00
(0, _game.startWork)("render:balls");
2025-03-19 18:13:41 +01:00
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
gameState.balls.forEach((ball)=>{
2025-03-25 08:22:58 +01:00
const drawingColor = gameState.ballsColor;
2025-04-10 14:49:28 +02:00
const ballAlpha = 1 - (0, _gameUtils.ballTransparency)(ball, gameState);
ctx.globalAlpha = ballAlpha;
2025-03-19 18:13:41 +01:00
// The white border around is to distinguish colored balls from coins/bg
2025-03-24 10:19:15 +01:00
drawBall(ctx, drawingColor, gameState.ballSize, ball.x, ball.y, gameState.puckColor);
2025-04-04 09:45:35 +02:00
if ((0, _gameUtils.telekinesisEffectRate)(gameState, ball) || (0, _gameUtils.yoyoEffectRate)(gameState, ball)) {
2025-03-19 18:13:41 +01:00
ctx.beginPath();
2025-03-19 20:14:55 +01:00
ctx.moveTo(gameState.puckPosition, gameState.gameZoneHeight);
2025-04-10 14:49:28 +02:00
ctx.globalAlpha = Math.max((0, _gameUtils.telekinesisEffectRate)(gameState, ball), (0, _gameUtils.yoyoEffectRate)(gameState, ball)) * ballAlpha;
2025-03-29 11:24:45 +01:00
ctx.strokeStyle = gameState.puckColor;
2025-03-19 18:13:41 +01:00
ctx.bezierCurveTo(gameState.puckPosition, gameState.gameZoneHeight, gameState.puckPosition, ball.y, ball.x, ball.y);
ctx.stroke();
2025-03-28 11:58:58 +01:00
ctx.lineWidth = 2;
ctx.setLineDash(emptyArray);
2025-03-19 18:13:41 +01:00
}
2025-04-04 09:45:35 +02:00
ctx.globalAlpha = 1;
2025-03-24 10:19:15 +01:00
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();
}
2025-03-19 18:13:41 +01:00
});
2025-04-15 21:28:00 +02:00
(0, _game.startWork)("render:puck");
2025-03-19 18:13:41 +01:00
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
2025-03-29 20:45:54 +01:00
drawPuck(ctx, gameState.puckColor, gameState.puckWidth, gameState.puckHeight, 0, gameState.perks.concave_puck, gameState.perks.streak_shots & & hasCombo ? getDashOffset(gameState) : -1);
2025-04-15 21:28:00 +02:00
(0, _game.startWork)("render:combotext");
2025-03-19 18:13:41 +01:00
if (gameState.combo > 1) {
ctx.globalCompositeOperation = "source-over";
2025-04-16 09:26:10 +02:00
ctx.globalAlpha = 1;
2025-03-19 18:13:41 +01:00
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;
2025-04-16 09:26:10 +02:00
ctx.globalAlpha = gameState.combo > (0, _gameStateMutators.baseCombo)(gameState) ? 1 : 0.3;
2025-03-19 18:13:41 +01:00
if (totalWidth < gameState.puckWidth ) {
drawText(ctx, comboText, "#000", gameState.puckHeight, left + gameState.coinSize * 1.5, gameState.gameZoneHeight - gameState.puckHeight / 2, true);
2025-04-16 09:26:10 +02:00
ctx.globalAlpha = 1;
drawCoin(ctx, "#ffd300", gameState.coinSize, left + gameState.coinSize / 2, gameState.gameZoneHeight - gameState.puckHeight / 2, "#ffd300", 0);
2025-03-23 22:19:11 +01:00
} else drawText(ctx, comboTextWidth > gameState.puckWidth ? gameState.combo.toString() : comboText, "#000", comboTextWidth > gameState.puckWidth ? 12 : 20, gameState.puckPosition, gameState.gameZoneHeight - gameState.puckHeight / 2, false);
2025-03-19 18:13:41 +01:00
}
2025-04-16 09:26:10 +02:00
(0, _game.startWork)("render:borders");
2025-03-19 18:13:41 +01:00
// Borders
ctx.globalCompositeOperation = "source-over";
2025-04-11 20:34:11 +02:00
ctx.globalAlpha = 1;
let redLeftSide = hasCombo & & (gameState.perks.left_is_lava || gameState.perks.trampoline);
let redRightSide = hasCombo & & (gameState.perks.right_is_lava || gameState.perks.trampoline);
let redTop = hasCombo & & (gameState.perks.top_is_lava || gameState.perks.trampoline);
2025-03-19 18:13:41 +01:00
if (gameState.offsetXRoundedDown) {
// draw outside of gaming area to avoid capturing borders in recordings
2025-04-11 20:34:11 +02:00
drawStraightLine(ctx, gameState, redLeftSide & & "#FF0000" || "#FFFFFF", gameState.offsetXRoundedDown - 1, 0, gameState.offsetXRoundedDown - 1, height, 1);
drawStraightLine(ctx, gameState, redRightSide & & "#FF0000" || "#FFFFFF", width - gameState.offsetXRoundedDown + 1, 0, width - gameState.offsetXRoundedDown + 1, height, 1);
2025-03-19 18:13:41 +01:00
} else {
2025-04-06 10:13:10 +02:00
drawStraightLine(ctx, gameState, redLeftSide & & "#FF0000" || "", 0, 0, 0, height, 1);
drawStraightLine(ctx, gameState, redRightSide & & "#FF0000" || "", width - 1, 0, width - 1, height, 1);
2025-03-19 18:13:41 +01:00
}
2025-04-11 20:34:11 +02:00
if (redTop) drawStraightLine(ctx, gameState, "#FF0000", gameState.offsetXRoundedDown, 1, width - gameState.offsetXRoundedDown, 1, 1);
2025-04-16 09:26:10 +02:00
(0, _game.startWork)("render:bottom_line");
2025-03-29 11:24:45 +01:00
ctx.globalAlpha = 1;
2025-04-18 21:17:32 +02:00
const corner = (0, _gameUtils.getCornerOffset)(gameState);
2025-04-18 22:06:16 +02:00
drawStraightLine(ctx, gameState, hasCombo & & gameState.perks.compound_interest & & "#FF0000" || (0, _options.isOptionOn)("mobile-mode") & & "#666666" || corner & & "#666666" || "", gameState.offsetXRoundedDown - corner, gameState.gameZoneHeight - 1, width - gameState.offsetXRoundedDown + corner, gameState.gameZoneHeight - 1, 1);
2025-04-15 21:28:00 +02:00
(0, _game.startWork)("render:contrast");
2025-04-04 12:07:24 +02:00
if (!(0, _options.isOptionOn)("basic") & & (0, _options.isOptionOn)("contrast") & & level.svg & & level.color === "#000000") {
2025-04-18 21:17:32 +02:00
ctx.imageSmoothingEnabled = (0, _options.isOptionOn)("smooth_lighting") || false;
2025-04-20 09:33:55 +02:00
if ((0, _options.isOptionOn)("probabilistic_lighting")) {
2025-04-20 09:33:12 +02:00
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "soft-light";
} else {
haloCanvasCtx.fillStyle = "#FFFFFF";
haloCanvasCtx.globalAlpha = 0.25;
haloCanvasCtx.globalCompositeOperation = "screen";
haloCanvasCtx.fillRect(0, 0, haloCanvas.width, haloCanvas.height);
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "overlay";
}
2025-04-04 12:07:24 +02:00
ctx.drawImage(haloCanvas, 0, 0, width, height);
2025-04-07 16:52:42 +02:00
ctx.imageSmoothingEnabled = false;
2025-04-04 12:07:24 +02:00
}
2025-04-16 09:26:10 +02:00
(0, _game.startWork)("render:text_under_puck");
2025-04-04 12:07:51 +02:00
ctx.globalCompositeOperation = "source-over";
2025-04-04 12:07:24 +02:00
ctx.globalAlpha = 1;
2025-04-18 17:15:47 +02:00
if ((0, _options.isOptionOn)("mobile-mode") & & gameState.startParams.computer_controlled) drawText(ctx, "breakout.lecaro.me?autoplay", gameState.puckColor, gameState.puckHeight, gameState.canvasWidth / 2, gameState.gameZoneHeight + (gameState.canvasHeight - gameState.gameZoneHeight) / 2);
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);
2025-04-15 21:28:00 +02:00
(0, _game.startWork)("render:askForWakeLock");
2025-04-15 16:47:04 +02:00
askForWakeLock(gameState);
2025-04-15 21:28:00 +02:00
(0, _game.startWork)("render:resetTransform");
2025-03-19 18:13:41 +01:00
if (shaked) ctx.resetTransform();
}
2025-03-25 08:22:58 +01:00
function drawStraightLine(ctx, gameState, mode, x1, y1, x2, y2, alpha = 1) {
2025-04-18 22:06:16 +02:00
x1 = Math.round(x1);
y1 = Math.round(y1);
x2 = Math.round(x2);
y2 = Math.round(y2);
2025-03-25 08:22:58 +01:00
ctx.globalAlpha = alpha;
if (!mode) return;
2025-04-18 22:06:16 +02:00
ctx.strokeStyle = mode;
2025-04-06 10:13:10 +02:00
if (mode == "#FF0000") {
2025-03-25 08:22:58 +01:00
ctx.lineDashOffset = getDashOffset(gameState);
ctx.lineWidth = 2;
ctx.setLineDash(redBorderDash);
2025-04-18 22:06:16 +02:00
} else ctx.lineWidth = 1;
2025-03-25 08:22:58 +01:00
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
2025-04-06 10:13:10 +02:00
if (mode == "#FF0000") {
2025-03-28 11:58:58 +01:00
ctx.setLineDash(emptyArray);
2025-03-25 08:22:58 +01:00
ctx.lineWidth = 1;
}
ctx.globalAlpha = 1;
}
2025-03-19 18:13:41 +01:00
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));
2025-04-01 21:37:07 +02:00
const redBorderOnBricksWithWrongColor = hasCombo & & (0, _game.gameState).perks.picky_eater & & (0, _gameUtils.isPickyEatingPossible)((0, _game.gameState));
2025-04-02 19:36:03 +02:00
const redColorOnAllBricks = hasCombo & & (0, _gameUtils.isMovingWhilePassiveIncome)((0, _game.gameState));
2025-04-01 21:37:07 +02:00
const redRowReach = (0, _gameUtils.reachRedRowIndex)((0, _game.gameState));
2025-04-06 10:13:10 +02:00
const { clairvoyant } = (0, _game.gameState).perks;
2025-03-25 08:22:58 +01:00
let offset = getDashOffset((0, _game.gameState));
2025-04-01 21:37:07 +02:00
if (!(redBorderOnBricksWithWrongColor || redColorOnAllBricks || redRowReach !== -1 || (0, _game.gameState).perks.zen)) offset = 0;
2025-04-06 10:13:10 +02:00
const clairVoyance = clairvoyant & & (0, _game.gameState).brickHP.reduce((a, b)=>a + b, 0);
2025-04-01 21:37:07 +02:00
const newKey = (0, _game.gameState).gameZoneWidth + "_" + (0, _game.gameState).bricks.join("_") + bombSVG.complete + "_" + redRowReach + "_" + redBorderOnBricksWithWrongColor + "_" + redColorOnAllBricks + "_" + (0, _game.gameState).ballsColor + "_" + (0, _game.gameState).perks.pierce_color + "_" + clairVoyance + "_" + offset;
2025-03-19 18:13:41 +01:00
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-04-01 21:37:07 +02:00
let redBecauseOfReach = redRowReach === Math.floor(index / (0, _game.gameState).level.size);
2025-03-29 11:24:45 +01:00
let redBorder = (0, _game.gameState).ballsColor !== color & & color !== "black" & & redBorderOnBricksWithWrongColor || hasCombo & & (0, _game.gameState).perks.zen & & color === "black" || redBecauseOfReach || redColorOnAllBricks;
2025-03-23 17:52:25 +01:00
canctx.globalCompositeOperation = "source-over";
2025-04-06 10:13:10 +02:00
drawBrick((0, _game.gameState), canctx, color, x, y, redBorder ? offset : -1, clairvoyant >= 2);
if ((0, _game.gameState).brickHP[index] > 1 & & clairvoyant) {
canctx.globalCompositeOperation = "source-over";
drawText(canctx, (0, _game.gameState).brickHP[index].toString(), clairvoyant >= 2 ? color : (0, _game.gameState).level.color, (0, _game.gameState).puckHeight, x, y);
2025-03-23 17:52:25 +01:00
}
2025-03-19 18:13:41 +01:00
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-29 20:45:54 +01:00
function drawPuck(ctx, color, puckWidth, puckHeight, yOffset = 0, concave_puck, redBorderOffset) {
const key = "puck" + color + "_" + puckWidth + "_" + puckHeight + "_" + concave_puck + "_" + redBorderOffset;
2025-03-19 18:13:41 +01:00
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);
2025-03-29 20:45:54 +01:00
if (concave_puck) {
2025-03-19 18:13:41 +01:00
canctx.lineTo(0, puckHeight * 0.75);
2025-03-29 20:45:54 +01:00
canctx.bezierCurveTo(puckWidth / 2, puckHeight * (2 + concave_puck) / 3, puckWidth / 2, puckHeight * (2 + concave_puck) / 3, puckWidth, puckHeight * 0.75);
2025-03-19 18:13:41 +01:00
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-04-06 10:13:10 +02:00
canctx.strokeStyle = "#FF0000";
2025-03-25 08:22:58 +01:00
canctx.lineWidth = 4;
canctx.setLineDash(redBorderDash);
canctx.lineDashOffset = redBorderOffset;
canctx.stroke();
}
2025-03-19 18:13:41 +01:00
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;
2025-04-06 10:13:10 +02:00
const key = "coin with halo_" + color + "_" + size + "_" + borderColor + "_" + (color === "#ffd300" ? angle : "whatever");
2025-03-19 18:13:41 +01:00
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-29 20:45:54 +01:00
canctx.strokeStyle = borderColor;
2025-04-06 10:13:10 +02:00
if (borderColor == "#FF0000") {
2025-03-29 20:45:54 +01:00
canctx.lineWidth = 2;
canctx.setLineDash(redBorderDash);
2025-03-25 08:33:09 +01:00
}
2025-04-10 21:40:45 +02:00
if (color === "transparent") canctx.lineWidth = 2;
2025-03-29 20:45:54 +01:00
canctx.stroke();
2025-04-06 10:13:10 +02:00
if (color === "#ffd300") {
2025-03-25 08:33:09 +01:00
// Fill in
2025-03-19 18:13:41 +01:00
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) {
2025-04-20 09:33:12 +02:00
width = Math.max(width, 2);
2025-03-19 18:13:41 +01:00
const key = "fuzzy-circle" + color + "_" + width;
2025-04-09 11:28:32 +02:00
if (!color?.startsWith("#")) debugger;
2025-03-19 18:13:41 +01:00
const size = Math.round(width * 3);
2025-04-09 09:24:15 +02:00
if (!size || isNaN(size)) {
debugger;
return;
}
2025-03-19 18:13:41 +01:00
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);
2025-04-06 10:13:10 +02:00
gradient.addColorStop(0.3, color + "88");
gradient.addColorStop(0.6, color + "22");
2025-03-19 18:13:41 +01:00
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-04-06 10:13:10 +02:00
function drawBrick(gameState, ctx, color, x, y, offset = 0, borderOnly) {
const tlx = Math.ceil(x - gameState.brickWidth / 2);
const tly = Math.ceil(y - gameState.brickWidth / 2);
const brx = Math.ceil(x + gameState.brickWidth / 2) - 1;
const bry = Math.ceil(y + gameState.brickWidth / 2) - 1;
2025-03-19 18:13:41 +01:00
const width = brx - tlx, height = bry - tly;
2025-04-09 09:24:15 +02:00
const key = "brick" + color + "_" + "_" + width + "_" + height + "_" + offset + "_" + borderOnly + "_";
2025-03-19 18:13:41 +01:00
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;
2025-03-19 18:13:41 +01:00
const cornerRadius = 2;
const canctx = can.getContext("2d");
canctx.fillStyle = color;
2025-03-28 11:58:58 +01:00
canctx.setLineDash(offset !== -1 ? redBorderDash : emptyArray);
2025-03-25 08:22:58 +01:00
canctx.lineDashOffset = offset;
2025-04-09 09:24:15 +02:00
canctx.strokeStyle = offset !== -1 & & "#FF000033" || color;
2025-03-19 18:13:41 +01:00
canctx.lineJoin = "round";
2025-04-09 09:24:15 +02:00
canctx.lineWidth = bord;
2025-03-19 18:13:41 +01:00
roundRect(canctx, bord / 2, bord / 2, width - bord, height - bord, cornerRadius);
2025-03-29 20:45:54 +01:00
if (!borderOnly) canctx.fill();
2025-03-19 18:13:41 +01:00
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-28 11:58:58 +01:00
const emptyArray = [];
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;
}
2025-04-15 16:47:04 +02:00
let wakeLock = null, wakeLockPending = false;
function askForWakeLock(gameState) {
2025-04-18 17:15:47 +02:00
if (gameState.startParams.computer_controlled & & !wakeLock & & !wakeLockPending) {
2025-04-15 16:47:04 +02:00
wakeLockPending = true;
try {
navigator.wakeLock.request("screen").then((lock)=>{
wakeLock = lock;
wakeLockPending = false;
lock.addEventListener("release", ()=>{
// the wake lock has been released
wakeLock = null;
});
});
} catch (e) {
console.warn("askForWakeLock error", e);
}
}
}
2025-03-19 18:13:41 +01:00
2025-04-18 21:17:32 +02:00
},{"./gameStateMutators":"9ZeQl","./game_utils":"cEeac","./i18n/i18n":"eNPRm","./game":"edeGs","./options":"d5NoS","./pure_functions":"6pQh7","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"caCAf":[function(require,module,exports,__globalThis) {
2025-03-19 18:13:41 +01:00
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "addToTotalPlayTime", ()=>addToTotalPlayTime);
parcelHelpers.export(exports, "gameOver", ()=>gameOver);
2025-04-06 10:13:10 +02:00
parcelHelpers.export(exports, "getCreativeModeWarning", ()=>getCreativeModeWarning);
2025-04-06 15:38:30 +02:00
parcelHelpers.export(exports, "getHistory", ()=>getHistory);
2025-03-19 18:13:41 +01:00
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");
2025-04-06 15:38:30 +02:00
var _upgrades = require("./upgrades");
2025-04-14 13:39:30 +02:00
var _levelEditor = require("./levelEditor");
2025-04-18 17:15:47 +02:00
var _creative = require("./creative");
2025-03-19 18:13:41 +01:00
function addToTotalPlayTime(ms) {
2025-04-15 21:28:00 +02:00
(0, _settings.setSettingValue)("breakout_71_total_play_time", (0, _settings.getSettingValue)("breakout_71_total_play_time", 0) + ms);
2025-03-19 18:13:41 +01:00
}
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;
2025-03-19 18:13:41 +01:00
(0, _game.pause)(true);
(0, _recording.stopRecording)();
addToTotalPlayTime((0, _game.gameState).runStatistics.runTime);
2025-04-18 17:15:47 +02:00
if (typeof (0, _game.gameState).startParams.isEditorTrialRun === "number") {
(0, _levelEditor.editRawLevelList)((0, _game.gameState).startParams.isEditorTrialRun);
(0, _game.restart)({});
return;
}
2025-04-19 16:50:26 +02:00
if ((0, _game.gameState).startParams.isCreativeRun) {
2025-04-18 17:15:47 +02:00
(0, _creative.openCreativeModePerksPicker)();
2025-04-14 13:39:30 +02:00
(0, _game.restart)({});
return;
}
2025-03-19 18:13:41 +01:00
// unlocks
const endTs = (0, _settings.getTotalScore)();
const startTs = endTs - (0, _game.gameState).score;
2025-04-06 15:38:30 +02:00
const unlockedPerks = (0, _upgrades.rawUpgrades).filter((o)=>o.threshold > startTs & & o.threshold < endTs ) ;
let unlocksInfo = unlockedPerks.length ? `
2025-03-19 18:13:41 +01:00
2025-04-06 15:38:30 +02:00
< h2 > ${unlockedPerks.length === 1 ? (0, _i18N.t)("gameOver.unlocked_perk") : (0, _i18N.t)("gameOver.unlocked_perk_plural", {
count: unlockedPerks.length
})}< / h2 >
${unlockedPerks.map((u)=>`
< div class = "upgrade used" >
${(0, _loadGameData.icons)["icon:" + u.id]}
< p >
< strong > ${u.name}< / strong >
${u.help(1)}
< / p >
< / div >
`).join("\n")}
` : "";
2025-03-19 18:13:41 +01:00
// 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: [
2025-04-07 14:50:35 +02:00
getCreativeModeWarning((0, _game.gameState)) || `
2025-03-19 18:13:41 +01:00
< p > ${intro}< / p >
< p > ${(0, _i18N.t)("gameOver.cumulative_total", {
2025-03-27 10:52:31 +01:00
startTs,
endTs
2025-04-06 10:13:10 +02:00
})}< / p >
2025-03-19 18:13:41 +01:00
`,
{
2025-04-06 11:36:32 +02:00
icon: (0, _loadGameData.icons)["icon:new_run"],
2025-03-19 18:13:41 +01:00
value: null,
2025-04-08 21:54:19 +02:00
text: (0, _i18N.t)("confirmRestart.yes"),
2025-03-19 18:13:41 +01:00
help: ""
2025-03-27 10:52:31 +01:00
},
2025-04-06 15:38:30 +02:00
`< div id = "level-recording-container" > < / div > `,
unlocksInfo,
getHistograms((0, _game.gameState))
2025-03-27 10:52:31 +01:00
]
2025-03-19 18:13:41 +01:00
}).then(()=>(0, _game.restart)({
2025-04-06 10:13:10 +02:00
levelToAvoid: (0, _gameUtils.currentLevelInfo)((0, _game.gameState)).name
2025-03-19 18:13:41 +01:00
}));
}
2025-04-06 10:13:10 +02:00
function getCreativeModeWarning(gameState) {
2025-04-06 15:38:30 +02:00
if (gameState.creative) return "< p > " + (0, _i18N.t)("gameOver.creative") + "< / p > ";
return "";
}
let runsHistory = [];
try {
2025-04-08 17:14:11 +02:00
runsHistory = JSON.parse(localStorage.getItem("breakout_71_runs_history") || "[]").sort((a, b)=>b.score - a.score).slice(0, 100);
2025-04-06 15:38:30 +02:00
} catch (e) {}
function getHistory() {
return runsHistory;
2025-04-06 10:13:10 +02:00
}
2025-04-01 13:35:33 +02:00
function getHistograms(gameState) {
2025-04-06 15:38:30 +02:00
if (gameState.creative) return "";
let unlockedLevels = "";
2025-03-19 18:13:41 +01:00
let runStats = "";
try {
2025-04-06 15:38:30 +02:00
const locked = (0, _loadGameData.allLevels).map((l, li)=>({
li,
l,
2025-04-07 14:08:48 +02:00
r: (0, _gameUtils.reasonLevelIsLocked)(li, runsHistory, false)?.text
2025-04-06 15:38:30 +02:00
})).filter((l)=>l.r);
2025-04-06 18:21:53 +02:00
gameState.runStatistics.runTime = Math.round(gameState.runStatistics.runTime);
const perks = {
...gameState.perks
};
for(let id in perks)if (!perks[id]) delete perks[id];
2025-03-19 18:13:41 +01:00
runsHistory.push({
2025-04-01 13:35:33 +02:00
...gameState.runStatistics,
2025-04-06 18:21:53 +02:00
perks,
2025-03-19 18:13:41 +01:00
appVersion: (0, _loadGameData.appVersion)
});
2025-04-07 14:08:48 +02:00
const unlocked = locked.filter(({ li })=>!(0, _gameUtils.reasonLevelIsLocked)(li, runsHistory, true));
2025-04-06 15:38:30 +02:00
if (unlocked.length) unlockedLevels = `
< h2 > ${unlocked.length === 1 ? (0, _i18N.t)("unlocks.just_unlocked") : (0, _i18N.t)("unlocks.just_unlocked_plural", {
count: unlocked.length
})}< / h2 >
${unlocked.map(({ l, r })=>`
< div class = "upgrade used" >
${(0, _loadGameData.icons)[l.name]}
< p >
< strong > ${l.name}< / strong >
${(0, _gameUtils.describeLevel)(l)}
< / p >
< / div >
`).join("\n")}
`;
2025-03-19 18:13:41 +01:00
// Generate some histogram
2025-04-01 13:49:10 +02:00
localStorage.setItem("breakout_71_runs_history", JSON.stringify(runsHistory, null, 2));
2025-03-19 18:13:41 +01:00
const makeHistogram = (title, getter, unit)=>{
2025-04-06 10:13:10 +02:00
let values = runsHistory.map((h)=>getter(h) || 0);
2025-03-19 18:13:41 +01:00
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, "");
2025-04-11 09:36:31 +02:00
if (runStats) runStats = `< p > ${(0, _i18N.t)("gameOver.stats_intro", {
2025-03-19 18:13:41 +01:00
count: runsHistory.length - 1
})}< / p > ` + runStats;
} catch (e) {
console.warn(e);
}
2025-04-07 16:52:42 +02:00
return unlockedLevels + runStats;
2025-03-19 18:13:41 +01:00
}
2025-04-18 21:17:32 +02:00
},{"./loadGameData":"l1B4x","./i18n/i18n":"eNPRm","./game":"edeGs","./game_utils":"cEeac","./settings":"5blfu","./recording":"godmD","./asyncAlert":"rSqLY","./upgrades":"1u3Dx","./levelEditor":"cirX1","./creative":"63kYJ","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"godmD":[function(require,module,exports,__globalThis) {
2025-03-19 18:13:41 +01:00
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) {
2025-04-20 10:58:26 +02:00
if (!(0, _options.isOptionOn)("record") || (0, _gameUtils.isInWebView)) return;
2025-03-19 18:13:41 +01:00
// 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
2025-04-06 10:13:10 +02:00
recordCanvasCtx.fillStyle = "#FFFFFF";
2025-03-19 18:13:41 +01:00
recordCanvasCtx.textBaseline = "top";
recordCanvasCtx.font = "12px monospace";
recordCanvasCtx.textAlign = "right";
recordCanvasCtx.fillText(gameState.score.toString(), recordCanvas.width - 12, 12);
recordCanvasCtx.textAlign = "left";
2025-03-28 10:21:14 +01:00
recordCanvasCtx.fillText("Level " + (gameState.currentLevel + 1) + "/" + (0, _gameUtils.max_levels)(gameState), 12, 12);
2025-03-19 18:13:41 +01:00
}
function startRecordingGame(gameState) {
2025-04-20 10:58:26 +02:00
if (!(0, _options.isOptionOn)("record") || (0, _gameUtils.isInWebView)) return;
2025-03-19 18:13:41 +01:00
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";
2025-04-20 10:58:26 +02:00
a.href = video.src;
2025-04-11 09:36:31 +02:00
a.textContent = (0, _i18N.t)("settings.record_download", {
2025-03-19 18:13:41 +01:00
size: (blob.size / 1000000).toFixed(2)
});
targetDiv.appendChild(a);
};
}
function pauseRecording() {
if (!(0, _options.isOptionOn)("record")) return;
if (mediaRecorder?.state === "recording") mediaRecorder?.pause();
}
function resumeRecording() {
if (!(0, _options.isOptionOn)("record")) return;
if (mediaRecorder?.state === "paused") mediaRecorder.resume();
}
function stopRecording() {
if (!(0, _options.isOptionOn)("record")) return;
if (!mediaRecorder) return;
mediaRecorder?.stop();
mediaRecorder = null;
}
function captureFileName(ext = "webm") {
return "breakout-71-capture-" + new Date().toISOString().replace(/[^0-9\-]+/gi, "-") + "." + ext;
}
},{"./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);
2025-03-19 18:13:41 +01:00
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-04-04 12:07:51 +02:00
async function asyncAlert({ title, content = [], allowClose = true, className = "" }) {
2025-03-20 18:44:46 +01:00
updateAlertsOpen(1);
2025-03-19 18:13:41 +01:00
return new Promise((resolve)=>{
2025-04-04 12:07:24 +02:00
popupWrap.className = className;
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;
2025-03-19 18:13:41 +01:00
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();
2025-03-19 18:13:41 +01:00
resolve(value);
}
2025-03-20 18:44:46 +01:00
if (allowClose) closeModal = ()=>{
closeWithResult(undefined);
};
else closeModal = null;
2025-03-19 18:13:41 +01:00
if (title) {
2025-04-08 21:54:19 +02:00
const h1 = document.createElement("h1");
h1.innerHTML = title;
popup.appendChild(h1);
2025-03-19 18:13:41 +01:00
}
2025-03-27 10:52:31 +01:00
content?.filter((i)=>i).forEach((entry, index)=>{
2025-03-29 09:25:17 +01:00
if (!entry) return;
2025-03-27 10:52:31 +01:00
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);
}
2025-04-01 13:35:33 +02:00
const { text, value, help, disabled, className = "", icon = "", tooltip } = entry;
2025-03-19 18:13:41 +01:00
const button = document.createElement("button");
button.innerHTML = `
${icon}
< div >
< strong > ${text}< / strong >
< em > ${help || ""}< / em >
< / div > `;
2025-04-01 13:39:09 +02:00
if (tooltip) button.setAttribute("data-tooltip", tooltip);
2025-03-19 18:13:41 +01:00
if (disabled) button.setAttribute("disabled", "disabled");
else button.addEventListener("click", (e)=>{
e.preventDefault();
2025-03-20 18:44:46 +01:00
e.stopPropagation();
2025-03-19 18:13:41 +01:00
closeWithResult(value);
2025-03-20 18:44:46 +01:00
// Focus "same" button if it's still there
lastClickedItemIndex = index;
2025-03-19 18:13:41 +01:00
});
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);
2025-03-19 18:13:41 +01:00
});
2025-04-07 14:08:48 +02:00
popup.addEventListener("click", (e)=>{
const target = e.target;
if (target.getAttribute("data-resolve-to")) closeWithResult(target.getAttribute("data-resolve-to"));
2025-04-07 08:24:17 +02:00
}, true);
2025-03-19 18:13:41 +01:00
popupWrap.appendChild(popup);
2025-03-20 18:44:46 +01:00
popupWrap.querySelector(`section.actions > button.needs-focus`)?.focus();
lastClickedItemIndex = -1;
2025-03-19 18:13:41 +01:00
}).then((v)=>{
2025-03-20 18:44:46 +01:00
updateAlertsOpen(-1);
2025-03-19 18:13:41 +01:00
closeModal = null;
return v;
}, ()=>{
closeModal = null;
2025-03-20 18:44:46 +01:00
updateAlertsOpen(-1);
2025-03-19 18:13:41 +01:00
});
}
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");
}
2025-03-19 18:13:41 +01:00
2025-04-14 13:39:30 +02:00
},{"./i18n/i18n":"eNPRm","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"cirX1":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "levelEditorMenuEntry", ()=>levelEditorMenuEntry);
parcelHelpers.export(exports, "editRawLevelList", ()=>editRawLevelList);
var _loadGameData = require("./loadGameData");
var _i18N = require("./i18n/i18n");
var _settings = require("./settings");
var _asyncAlert = require("./asyncAlert");
var _levelIcon = require("./levelIcon");
var _paletteJson = require("./data/palette.json");
var _paletteJsonDefault = parcelHelpers.interopDefault(_paletteJson);
var _game = require("./game");
var _gameUtils = require("./game_utils");
const palette = (0, _paletteJsonDefault.default);
2025-04-16 09:26:10 +02:00
const MAX_LEVEL_SIZE = 21;
const MIN_LEVEL_SIZE = 2;
2025-04-14 13:39:30 +02:00
function levelEditorMenuEntry() {
const min = 10000;
const disabled = (0, _settings.getTotalScore)() < min ;
return {
icon: (0, _loadGameData.icons)["icon:editor"],
text: (0, _i18N.t)("editor.title"),
disabled,
help: disabled ? (0, _i18N.t)("editor.locked", {
min
}) : (0, _i18N.t)("editor.help"),
async value () {
openLevelEditorLevelsList().then();
}
};
}
async function openLevelEditorLevelsList() {
const rawList = (0, _settings.getSettingValue)("custom_levels", []);
const customLevels = rawList.map((0, _loadGameData.transformRawLevel));
let choice = await (0, _asyncAlert.asyncAlert)({
title: (0, _i18N.t)("editor.title"),
content: [
...customLevels.map((l, li)=>({
text: l.name,
icon: (0, _levelIcon.levelIconHTML)(l.bricks, l.size, l.color),
value () {
editRawLevelList(li);
},
help: l.credit || (0, _gameUtils.describeLevel)(l)
})),
{
text: (0, _i18N.t)("editor.new_level"),
icon: (0, _loadGameData.icons)["icon:editor"],
value () {
rawList.push({
color: "",
size: 6,
bricks: "____________________________________",
name: "custom level" + (rawList.length + 1),
credit: ""
});
(0, _settings.setSettingValue)("custom_levels", rawList);
editRawLevelList(rawList.length - 1);
}
},
{
text: (0, _i18N.t)("editor.import"),
help: (0, _i18N.t)("editor.import_instruction"),
value () {
const code = prompt((0, _i18N.t)("editor.import_instruction"))?.trim();
if (code) {
let [name, credit] = code.match(/\[([^\]]+)]/gi);
let bricks = code.split(name)[1].split(credit)[0].replace(/\s/gi, "");
name = name.slice(1, -1);
credit = credit.slice(1, -1);
name ||= "Imported on " + new Date().toISOString().slice(0, 10);
credit ||= "";
const size = Math.sqrt(bricks.length);
2025-04-16 09:26:10 +02:00
if (Math.floor(size) === size & & size >= MIN_LEVEL_SIZE & & size < = MAX_LEVEL_SIZE) {
2025-04-14 13:39:30 +02:00
rawList.push({
color: automaticBackgroundColor(bricks.split("")),
size,
bricks,
name,
credit
});
(0, _settings.setSettingValue)("custom_levels", rawList);
}
}
openLevelEditorLevelsList();
}
}
]
});
if (typeof choice == "function") choice();
}
async function editRawLevelList(nth, color = "W") {
let rawList = (0, _settings.getSettingValue)("custom_levels", []);
const level = rawList[nth];
const bricks = level.bricks.split("");
let grid = "";
for(let y = 0; y < level.size ; y + + ) {
grid += '< div style = "background: ' + (level.color || " black " ) + ' ; " > ';
for(let x = 0; x < level.size ; x + + ) {
const c = bricks[y * level.size + x];
grid += `< span data-resolve-to = "paint_brick:${x}:${y}" style = "background: ${palette[c]}" > ${c == "B" ? "\uD83D\uDCA3" : ""}< / span > `;
}
grid += "< / div > ";
}
const levelColors = new Set(bricks);
levelColors.delete("_");
levelColors.delete("B");
let colorList = '< div class = "palette" > ' + Object.entries(palette).filter(([key, value])=>key !== "_").filter(([key, value])=>levelColors.size < 5 | | levelColors . has ( key ) | | key = == " B " ) . map ( ( [ key , value ] ) = > `< span data-resolve-to = "set_color:${key}" data-selected = "${key == color}" style = "background: ${value}" > ${key == "B" ? "\uD83D\uDCA3" : ""}< / span > `).join("") + "< / div > ";
const clicked = await (0, _asyncAlert.asyncAlert)({
title: (0, _i18N.t)("editor.editing.title", {
name: level.name
}),
content: [
(0, _i18N.t)("editor.editing.color"),
colorList,
(0, _i18N.t)("editor.editing.help"),
`< div class = "gridEdit" style = "--grid-size:${level.size};" > ${grid}< / div > `,
{
icon: (0, _loadGameData.icons)["icon:new_run"],
text: (0, _i18N.t)("editor.editing.play"),
value: "play"
},
{
text: (0, _i18N.t)("editor.editing.rename"),
value: "rename",
help: level.name
},
{
text: (0, _i18N.t)("editor.editing.credit"),
value: "credit",
help: level.credit
},
{
text: (0, _i18N.t)("editor.editing.delete"),
value: "delete"
},
{
text: (0, _i18N.t)("editor.editing.copy"),
value: "copy",
help: (0, _i18N.t)("editor.editing.copy_help"),
disabled: !level.name || !level.credit || bricks.filter((b)=>b !== "_").length < 6
},
{
text: (0, _i18N.t)("editor.editing.bigger"),
value: "size:+1",
2025-04-16 09:26:10 +02:00
disabled: level.size >= MAX_LEVEL_SIZE
2025-04-14 13:39:30 +02:00
},
{
text: (0, _i18N.t)("editor.editing.smaller"),
value: "size:-1",
2025-04-16 09:26:10 +02:00
disabled: level.size < = MIN_LEVEL_SIZE
2025-04-14 13:39:30 +02:00
},
{
text: (0, _i18N.t)("editor.editing.left"),
value: "move:-1:0"
},
{
text: (0, _i18N.t)("editor.editing.right"),
value: "move:1:0"
},
{
text: (0, _i18N.t)("editor.editing.up"),
value: "move:0:-1"
},
{
text: (0, _i18N.t)("editor.editing.down"),
value: "move:0:1"
}
]
});
if (!clicked) return;
if (typeof clicked === "string") {
const [action, a, b] = clicked.split(":");
if (action == "paint_brick") {
const x = parseInt(a), y = parseInt(b);
bricks[y * level.size + x] = bricks[y * level.size + x] === color ? "_" : color;
level.bricks = bricks.join("");
}
if (action == "set_color") color = a;
if (action == "size") {
const newSize = level.size + parseInt(a);
const newBricks = [];
for(let y = 0; y < newSize ; y + + ) for ( let x = 0; x < newSize ; x + + ) newBricks . push ( x < level . size & & y < level . size & & bricks [ y * level . size + x ] | | " _ " ) ;
level.size = newSize;
level.bricks = newBricks.join("");
}
if (action == "move") {
const dx = parseInt(a), dy = parseInt(b);
const newBricks = [];
for(let y = 0; y < level.size ; y + + ) for ( let x = 0; x < level . size ; x + + ) {
const tx = x - dx;
const ty = y - dy;
if (tx < 0 | | tx > = level.size || ty < 0 | | ty > = level.size) newBricks.push("_");
else newBricks.push(bricks[ty * level.size + tx]);
}
level.bricks = newBricks.join("");
}
if (action === "play") {
(0, _game.restart)({
level: (0, _loadGameData.transformRawLevel)(level),
isEditorTrialRun: nth,
perks: {
base_combo: 7
}
});
return;
}
if (action === "copy") {
let text = "```\n[" + level.name?.replace(/\[|\]/gi, " ") + "]";
bricks.forEach((b, bi)=>{
if (!(bi % level.size)) text += "\n";
text += b;
});
text += "\n[" + (level.credit?.replace(/\[|\]/gi, " ") || "Missing credits!") + "]\n```";
navigator.clipboard.writeText(text);
// return
}
if (action === "rename") {
const name = prompt((0, _i18N.t)("editor.editing.rename_prompt"), level.name);
if (name) level.name = name;
}
if (action === "credit") {
const credit = prompt((0, _i18N.t)("editor.editing.credit_prompt"), level.credit || "");
if (credit !== "null") level.credit = credit || "";
}
if (action === "delete") {
rawList = rawList.filter((l, li)=>li !== nth);
(0, _settings.setSettingValue)("custom_levels", rawList);
openLevelEditorLevelsList();
return;
}
}
level.color = automaticBackgroundColor(bricks);
(0, _settings.setSettingValue)("custom_levels", rawList);
editRawLevelList(nth, color);
}
function automaticBackgroundColor(bricks) {
return bricks.filter((b)=>b === "g").length > bricks.filter((b)=>b !== "_").length * 0.05 ? "#115988" : "";
}
2025-04-18 17:15:47 +02:00
},{"./loadGameData":"l1B4x","./i18n/i18n":"eNPRm","./settings":"5blfu","./asyncAlert":"rSqLY","./levelIcon":"6rQoT","./data/palette.json":"ktRBU","./game":"edeGs","./game_utils":"cEeac","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"63kYJ":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "creativeMode", ()=>creativeMode);
parcelHelpers.export(exports, "openCreativeModePerksPicker", ()=>openCreativeModePerksPicker);
var _loadGameData = require("./loadGameData");
var _i18N = require("./i18n/i18n");
var _settings = require("./settings");
var _game = require("./game");
var _asyncAlert = require("./asyncAlert");
var _gameUtils = require("./game_utils");
var _gameOver = require("./gameOver");
var _upgrades = require("./upgrades");
var _levelIcon = require("./levelIcon");
function creativeMode(gameState) {
return {
icon: (0, _loadGameData.icons)["icon:creative"],
text: (0, _i18N.t)("lab.menu_entry"),
help: (0, _settings.getTotalScore)() < (0, _game.creativeModeThreshold) & & (0, _i18N.t)("lab.unlocks_at", {
score: (0, _game.creativeModeThreshold)
}) || (0, _i18N.t)("lab.help"),
disabled: (0, _settings.getTotalScore)() < (0, _game.creativeModeThreshold),
async value () {
openCreativeModePerksPicker();
}
};
}
async function openCreativeModePerksPicker() {
let creativeModePerks = (0, _settings.getSettingValue)("creativeModePerks", {}), choice;
const customLevels = (0, _settings.getSettingValue)("custom_levels", []).map((0, _loadGameData.transformRawLevel));
while(choice = await (0, _asyncAlert.asyncAlert)({
title: (0, _i18N.t)("lab.menu_entry"),
className: "actionsAsGrid",
content: [
(0, _i18N.t)("lab.instructions"),
{
value: "reset",
text: (0, _i18N.t)("lab.reset"),
disabled: !(0, _gameUtils.sumOfValues)(creativeModePerks)
},
...(0, _loadGameData.upgrades).filter((u)=>!(0, _upgrades.noCreative).includes(u.id)).map((u)=>({
icon: u.icon,
text: u.name,
help: (creativeModePerks[u.id] || 0) + "/" + (u.max + (creativeModePerks.limitless || 0)),
value: u,
2025-04-20 09:33:12 +02:00
className: creativeModePerks[u.id] ? "sandbox highlight" : "sandbox ",
2025-04-18 17:15:47 +02:00
tooltip: u.help(creativeModePerks[u.id] || 1)
})),
(0, _i18N.t)("lab.select_level"),
...(0, _loadGameData.allLevels).map((l, li)=>{
const problem = (0, _gameUtils.reasonLevelIsLocked)(li, (0, _gameOver.getHistory)(), true)?.text || "";
return {
icon: (0, _loadGameData.icons)[l.name],
text: l.name,
value: l,
disabled: !!problem,
2025-04-18 21:17:32 +02:00
tooltip: problem || (0, _gameUtils.describeLevel)(l),
className: (0, _settings.getSettingValue)("creativeModeLevel", "") === l.name ? "highlight" : ""
2025-04-18 17:15:47 +02:00
};
}),
...customLevels.map((l)=>({
icon: (0, _levelIcon.levelIconHTML)(l.bricks, l.size, l.color),
text: l.name,
value: l,
disabled: !l.bricks.filter((b)=>b !== "_").length,
tooltip: (0, _gameUtils.describeLevel)(l)
}))
]
})){
if (choice === "reset") (0, _loadGameData.upgrades).forEach((u)=>{
creativeModePerks[u.id] = 0;
});
else if ("bricks" in choice) {
(0, _settings.setSettingValue)("creativeModePerks", creativeModePerks);
2025-04-18 21:17:32 +02:00
(0, _settings.setSettingValue)("creativeModeLevel", choice.name);
2025-04-18 17:15:47 +02:00
if (await (0, _game.confirmRestart)((0, _game.gameState))) (0, _game.restart)({
perks: creativeModePerks,
level: choice,
isCreativeRun: true
});
return;
} else if (choice) creativeModePerks[choice.id] = ((creativeModePerks[choice.id] || 0) + 1) % (choice.max + 1 + (creativeModePerks.limitless || 0));
else return;
}
}
},{"./loadGameData":"l1B4x","./i18n/i18n":"eNPRm","./settings":"5blfu","./game":"edeGs","./asyncAlert":"rSqLY","./game_utils":"cEeac","./gameOver":"caCAf","./upgrades":"1u3Dx","./levelIcon":"6rQoT","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"ka4dG":[function(require,module,exports,__globalThis) {
2025-04-08 14:03:38 +02:00
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "addToTotalScore", ()=>addToTotalScore);
var _loadGameData = require("./loadGameData");
var _gameStateMutators = require("./gameStateMutators");
var _toast = require("./toast");
var _i18N = require("./i18n/i18n");
var _settings = require("./settings");
function addToTotalScore(gameState, points) {
if (gameState.creative) return;
const pastScore = (0, _settings.getTotalScore)();
const newScore = pastScore + points;
(0, _settings.setSettingValue)("breakout_71_total_score", newScore);
// Check unlocked upgrades
(0, _loadGameData.upgrades).forEach((u)=>{
if (u.threshold > pastScore & & u.threshold < = newScore) {
(0, _gameStateMutators.schedulGameSound)(gameState, "colorChange", 0, 1);
(0, _toast.toast)((0, _loadGameData.icons)["icon:" + u.id] + "< strong > " + (0, _i18N.t)("gameOver.unlocked_perk") + "< / strong > ");
}
});
}
},{"./loadGameData":"l1B4x","./gameStateMutators":"9ZeQl","./toast":"nAuvo","./i18n/i18n":"eNPRm","./settings":"5blfu","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"nAuvo":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "toast", ()=>toast);
2025-04-15 16:47:04 +02:00
let div = document.createElement("div");
2025-04-15 21:28:00 +02:00
div.classList = "hidden toast";
2025-04-15 16:47:04 +02:00
document.body.appendChild(div);
let timeout;
2025-04-08 14:03:38 +02:00
function toast(html) {
2025-04-15 16:47:04 +02:00
div.classList = "toast visible";
2025-04-08 14:03:38 +02:00
div.innerHTML = html;
2025-04-15 16:47:04 +02:00
if (timeout) clearTimeout(timeout);
timeout = setTimeout(()=>{
timeout = undefined;
2025-04-15 21:28:00 +02:00
div.classList = "hidden toast";
2025-04-15 16:47:04 +02:00
}, 1500);
2025-04-08 14:03:38 +02:00
}
},{"@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"aQN6X":[function(require,module,exports,__globalThis) {
2025-03-19 18:13:41 +01:00
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
2025-03-28 10:21:14 +01:00
parcelHelpers.export(exports, "getRunLevels", ()=>getRunLevels);
2025-03-19 18:13:41 +01:00
parcelHelpers.export(exports, "newGameState", ()=>newGameState);
var _loadGameData = require("./loadGameData");
var _gameUtils = require("./game_utils");
var _gameStateMutators = require("./gameStateMutators");
var _options = require("./options");
2025-04-06 15:38:30 +02:00
var _gameOver = require("./gameOver");
var _settings = require("./settings");
2025-04-07 14:08:48 +02:00
var _startingPerks = require("./startingPerks");
2025-04-07 14:22:59 +02:00
function getRunLevels(params, randomGift) {
2025-04-08 14:29:00 +02:00
const unlockedBefore = new Set((0, _settings.getSettingValue)("breakout_71_unlocked_levels", []));
2025-04-06 15:38:30 +02:00
const history = (0, _gameOver.getHistory)();
2025-04-08 14:29:00 +02:00
const unlocked = (0, _loadGameData.allLevels).filter((l, li)=>unlockedBefore.has(l.name) || !(0, _gameUtils.reasonLevelIsLocked)(li, history, false));
2025-04-14 13:39:30 +02:00
const firstLevel = params?.level ? [
params.level
] : (0, _loadGameData.allLevelsAndIcons).filter((l)=>l.name == "icon:" + randomGift);
const restInRandomOrder = unlocked.filter((l)=>l.name !== params?.level?.name).filter((l)=>l.name !== params?.levelToAvoid).sort(()=>Math.random() - 0.5);
2025-04-12 15:39:32 +02:00
return firstLevel.concat(restInRandomOrder.slice(0, 10).sort((a, b)=>a.sortKey - b.sortKey)).concat(restInRandomOrder.slice(10));
2025-03-28 10:21:14 +01:00
}
function newGameState(params) {
2025-04-07 14:22:59 +02:00
const highScore = (0, _gameUtils.getHighScore)();
2025-03-19 18:13:41 +01:00
const perks = {
...(0, _gameUtils.makeEmptyPerksMap)((0, _loadGameData.upgrades)),
...params?.perks || {}
};
2025-04-07 14:22:59 +02:00
let randomGift = undefined;
if (!(0, _gameUtils.sumOfValues)(perks)) {
2025-04-10 13:17:38 +02:00
let giftable = (0, _loadGameData.upgrades).filter((u)=>(0, _startingPerks.isStartingPerk)(u));
if (!giftable.length) giftable = (0, _loadGameData.upgrades).filter((u)=>!(0, _startingPerks.isBlackListedForStart)(u));
2025-04-07 14:22:59 +02:00
randomGift = (0, _options.isOptionOn)("easy") & & "slow_down" || giftable[Math.floor(Math.random() * giftable.length)].id;
perks[randomGift] = 1;
}
const runLevels = getRunLevels(params, randomGift);
2025-03-19 18:13:41 +01:00
const gameState = {
2025-04-18 17:15:47 +02:00
startParams: params,
2025-03-19 18:13:41 +01:00
runLevels,
2025-03-27 10:52:31 +01:00
level: runLevels[0],
2025-03-19 18:13:41 +01:00
currentLevel: 0,
2025-03-20 23:11:42 +01:00
upgradesOfferedFor: -1,
2025-03-19 18:13:41 +01:00
perks,
puckWidth: 200,
baseSpeed: 12,
combo: 1,
2025-04-08 08:57:41 +02:00
lastCombo: 1,
2025-03-19 18:13:41 +01:00
gridSize: 12,
running: false,
2025-03-22 16:04:25 +01:00
isGameOver: false,
2025-03-19 18:13:41 +01:00
ballStickToPuck: true,
puckPosition: 400,
2025-03-25 08:22:58 +01:00
lastPuckPosition: 400,
2025-03-26 08:01:12 +01:00
lastPuckMove: 0,
2025-03-19 18:13:41 +01:00
pauseTimeout: null,
canvasWidth: 0,
canvasHeight: 0,
offsetX: 0,
offsetXRoundedDown: 0,
gameZoneWidth: 0,
gameZoneWidthRoundedUp: 0,
gameZoneHeight: 0,
brickWidth: 0,
score: 0,
lastScoreIncrease: -1000,
lastExplosion: -1000,
2025-03-30 21:07:58 +02:00
lastBrickBroken: 0,
2025-04-07 14:22:59 +02:00
highScore,
2025-03-19 18:13:41 +01:00
balls: [],
2025-04-06 10:13:10 +02:00
ballsColor: "#FFFFFF",
2025-03-19 18:13:41 +01:00
bricks: [],
2025-03-23 17:52:25 +01:00
brickHP: [],
2025-03-19 18:13:41 +01:00
lights: {
indexMin: 0,
total: 0,
list: []
},
particles: {
indexMin: 0,
total: 0,
list: []
},
texts: {
indexMin: 0,
total: 0,
list: []
},
coins: {
indexMin: 0,
2025-03-29 15:00:44 +01:00
total: 0,
list: []
},
respawns: {
indexMin: 0,
2025-03-19 18:13:41 +01:00
total: 0,
list: []
},
levelStartScore: 0,
levelMisses: 0,
levelSpawnedCoins: 0,
2025-04-06 10:13:10 +02:00
puckColor: "#FFFFFF",
2025-03-19 18:13:41 +01:00
ballSize: 20,
coinSize: 14,
puckHeight: 20,
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,
2025-04-07 08:24:17 +02:00
max_combo: 1
2025-03-19 18:13:41 +01:00
},
lastOffered: {},
levelTime: 0,
2025-03-22 16:47:02 +01:00
winAt: 0,
2025-03-19 18:13:41 +01:00
levelWallBounces: 0,
needsRender: true,
autoCleanUses: 0,
2025-03-26 08:01:12 +01:00
...(0, _gameUtils.defaultSounds)(),
2025-03-28 10:21:14 +01:00
rerolls: 0,
2025-04-18 17:15:47 +02:00
creative: params?.computer_controlled || (0, _gameUtils.sumOfValues)(params.perks) > 1 || params.level & & !params.level.name.startsWith("icon:")
2025-03-19 18:13:41 +01:00
};
(0, _gameStateMutators.resetBalls)(gameState);
2025-04-07 14:22:59 +02:00
for (let perk of (0, _loadGameData.upgrades))if (perks[perk.id]) (0, _gameStateMutators.dontOfferTooSoon)(gameState, perk.id);
2025-03-19 18:13:41 +01:00
return gameState;
}
2025-04-08 15:17:14 +02:00
},{"./loadGameData":"l1B4x","./game_utils":"cEeac","./gameStateMutators":"9ZeQl","./options":"d5NoS","./gameOver":"caCAf","./settings":"5blfu","./startingPerks":"lv30m","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"lv30m":[function(require,module,exports,__globalThis) {
2025-04-07 14:08:48 +02:00
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "startingPerkMenuButton", ()=>startingPerkMenuButton);
2025-04-10 13:17:38 +02:00
parcelHelpers.export(exports, "isBlackListedForStart", ()=>isBlackListedForStart);
2025-04-07 14:08:48 +02:00
parcelHelpers.export(exports, "isStartingPerk", ()=>isStartingPerk);
parcelHelpers.export(exports, "openStartingPerksEditor", ()=>openStartingPerksEditor);
var _asyncAlert = require("./asyncAlert");
var _i18N = require("./i18n/i18n");
var _loadGameData = require("./loadGameData");
var _settings = require("./settings");
2025-04-07 14:50:35 +02:00
var _options = require("./options");
2025-04-11 09:36:31 +02:00
var _upgrades = require("./upgrades");
2025-04-07 14:08:48 +02:00
function startingPerkMenuButton() {
return {
2025-04-07 15:25:58 +02:00
disabled: (0, _options.isOptionOn)("easy"),
2025-04-07 14:08:48 +02:00
icon: (0, _loadGameData.icons)["icon:starting_perks"],
2025-04-11 09:36:31 +02:00
text: (0, _i18N.t)("starting_perks.title"),
help: (0, _i18N.t)("starting_perks.help"),
2025-04-07 14:08:48 +02:00
async value () {
await openStartingPerksEditor();
}
};
}
2025-04-10 13:17:38 +02:00
function isBlackListedForStart(u) {
2025-04-11 09:36:31 +02:00
return !!((0, _upgrades.notStartingPerk).includes(u.id) || u.requires || u.threshold > (0, _settings.getTotalScore)());
2025-04-10 13:17:38 +02:00
}
2025-04-07 14:08:48 +02:00
function isStartingPerk(u) {
2025-04-11 09:36:31 +02:00
return !isBlackListedForStart(u) & & (0, _settings.getSettingValue)("start_with_" + u.id, u.gift);
2025-04-07 14:08:48 +02:00
}
async function openStartingPerksEditor() {
2025-04-10 13:17:38 +02:00
const avaliable = (0, _loadGameData.upgrades).filter((u)=>!isBlackListedForStart(u));
2025-04-07 14:08:48 +02:00
const buttons = avaliable.map((u)=>{
const checked = isStartingPerk(u);
return {
icon: u.icon,
text: u.name,
tooltip: u.help(1),
2025-04-10 13:17:38 +02:00
value: [
u
],
2025-04-07 14:08:48 +02:00
checked
};
});
2025-04-10 13:17:38 +02:00
const checkedList = buttons.filter((b)=>b.checked);
const perks = await (0, _asyncAlert.asyncAlert)({
2025-04-11 09:36:31 +02:00
title: (0, _i18N.t)("starting_perks.title"),
2025-04-07 14:08:48 +02:00
className: "actionsAsGrid",
content: [
2025-04-11 09:36:31 +02:00
checkedList.length ? (0, _i18N.t)("starting_perks.checked") : (0, _i18N.t)("starting_perks.random"),
2025-04-10 13:17:38 +02:00
...checkedList,
2025-04-11 09:36:31 +02:00
(0, _i18N.t)("starting_perks.unchecked"),
2025-04-07 14:08:48 +02:00
...buttons.filter((b)=>!b.checked)
]
});
2025-04-10 13:17:38 +02:00
if (perks) {
perks?.forEach((perk)=>{
(0, _settings.setSettingValue)("start_with_" + perk.id, !isStartingPerk(perk));
});
2025-04-07 14:08:48 +02:00
openStartingPerksEditor();
}
}
2025-04-15 16:47:04 +02:00
},{"./asyncAlert":"rSqLY","./i18n/i18n":"eNPRm","./loadGameData":"l1B4x","./settings":"5blfu","./options":"d5NoS","./upgrades":"1u3Dx","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"bqkdF":[function(require,module,exports,__globalThis) {
2025-04-06 10:13:10 +02:00
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "helpMenuEntry", ()=>helpMenuEntry);
var _loadGameData = require("./loadGameData");
var _i18N = require("./i18n/i18n");
var _asyncAlert = require("./asyncAlert");
var _pureFunctions = require("./pure_functions");
function helpMenuEntry() {
return {
icon: (0, _loadGameData.icons)["icon:help"],
2025-04-11 09:36:31 +02:00
text: (0, _i18N.t)("help.title"),
help: (0, _i18N.t)("help.help"),
2025-04-06 10:13:10 +02:00
async value () {
await (0, _asyncAlert.asyncAlert)({
2025-04-11 09:36:31 +02:00
title: (0, _i18N.t)("help.title"),
2025-04-06 10:13:10 +02:00
allowClose: true,
content: [
2025-04-11 09:36:31 +02:00
(0, _pureFunctions.miniMarkDown)((0, _i18N.t)("help.content", {
2025-04-08 21:54:19 +02:00
catchRateBest: (0, _pureFunctions.catchRateBest),
catchRateGood: (0, _pureFunctions.catchRateGood),
levelTimeBest: (0, _pureFunctions.levelTimeBest),
levelTimeGood: (0, _pureFunctions.levelTimeGood),
missesBest: (0, _pureFunctions.missesBest),
missesGood: (0, _pureFunctions.missesGood),
wallBouncedBest: (0, _pureFunctions.wallBouncedBest),
wallBouncedGood: (0, _pureFunctions.wallBouncedGood)
})),
2025-04-11 09:36:31 +02:00
(0, _pureFunctions.miniMarkDown)((0, _i18N.t)("help.upgrades")),
2025-04-06 10:13:10 +02:00
...(0, _loadGameData.upgrades).map((u)=>`
< div class = "upgrade used" >
${u.icon}
< p >
< strong > ${u.name}< / strong > < br / >
${u.help(1)}
< / p >
< / div >
${(0, _pureFunctions.miniMarkDown)(u.fullHelp)}
`),
2025-04-11 09:36:31 +02:00
"< h2 > " + (0, _i18N.t)("help.levels") + "< / h2 > ",
2025-04-08 15:02:38 +02:00
...(0, _loadGameData.allLevels).filter((l)=>l.credit?.trim()).map((l)=>`
2025-04-06 10:13:10 +02:00
< div class = "upgrade used" >
${(0, _loadGameData.icons)[l.name]}
2025-04-08 15:02:38 +02:00
< div >
2025-04-06 10:13:10 +02:00
< p >
2025-04-08 15:02:38 +02:00
< strong > ${l.name}< / strong >
2025-04-06 10:13:10 +02:00
< / p >
2025-04-08 15:02:38 +02:00
${(0, _pureFunctions.miniMarkDown)(l.credit || "")}
< / div >
2025-04-06 10:13:10 +02:00
< / div > `)
]
});
}
};
}
2025-04-18 17:15:47 +02:00
},{"./loadGameData":"l1B4x","./i18n/i18n":"eNPRm","./asyncAlert":"rSqLY","./pure_functions":"6pQh7","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"3RWxb":[function(require,module,exports,__globalThis) {
2025-04-01 13:35:33 +02:00
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "setupTooltips", ()=>setupTooltips);
var _options = require("./options");
function setupTooltips() {
2025-04-01 13:39:09 +02:00
const tooltip = document.getElementById("tooltip");
if ((0, _options.isOptionOn)("mobile-mode")) {
tooltip.style.display = "none";
2025-04-01 13:35:33 +02:00
return;
}
function updateTooltipPosition(e) {
2025-04-07 15:25:58 +02:00
tooltip.style.transform = `translate(${e.clientX}px,${e.clientY}px) translate(${e.clientX > window.innerWidth / 2 ? "-100%" : "0"},${e.clientY > window.innerHeight * 2 / 3 ? "-100%" : "20px"})`;
2025-04-01 13:35:33 +02:00
}
2025-04-02 10:41:35 +02:00
function closeToolTip() {
tooltip.style.display = "none";
hovering = null;
}
let hovering = null;
2025-04-01 13:39:09 +02:00
document.body.addEventListener("mouseenter", (e)=>{
2025-04-01 13:35:33 +02:00
let parent = e.target;
2025-04-01 13:39:09 +02:00
while(parent & & !parent.hasAttribute("data-tooltip"))parent = parent.parentElement;
2025-04-07 08:24:17 +02:00
if (parent?.getAttribute("data-tooltip")?.trim()) {
2025-04-02 10:41:35 +02:00
hovering = parent;
2025-04-02 10:42:01 +02:00
tooltip.innerHTML = hovering.getAttribute("data-tooltip") || "";
2025-04-01 13:39:09 +02:00
tooltip.style.display = "";
2025-04-01 13:35:33 +02:00
updateTooltipPosition(e);
2025-04-02 10:41:35 +02:00
} else closeToolTip();
2025-04-01 13:35:33 +02:00
}, true);
2025-04-02 10:41:35 +02:00
setInterval(()=>{
if (hovering) {
if (!document.body.contains(hovering)) closeToolTip();
}
}, 200);
2025-04-01 13:39:09 +02:00
document.body.addEventListener("mousemove", (e)=>{
2025-04-01 13:35:33 +02:00
if (!tooltip.style.display) updateTooltipPosition(e);
}, true);
2025-04-01 13:39:09 +02:00
document.body.addEventListener("mouseleave", (e)=>{
2025-04-02 10:41:35 +02:00
closeToolTip();
2025-04-07 15:25:58 +02:00
}, true);
2025-04-02 10:41:35 +02:00
}
2025-04-07 14:08:48 +02:00
},{"./options":"d5NoS","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"a9qdY":[function(require,module,exports,__globalThis) {
2025-04-02 17:03:53 +02:00
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
2025-04-06 18:21:53 +02:00
var _versionJson = require("./data/version.json");
var _versionJsonDefault = parcelHelpers.interopDefault(_versionJson);
var _generateSaveFileContent = require("./generateSaveFileContent");
2025-04-08 10:36:30 +02:00
var _gameUtils = require("./game_utils");
var _loadGameData = require("./loadGameData");
2025-04-06 18:21:53 +02:00
// The page will be reloaded if any migrations were run
let migrationsRun = 0;
2025-04-02 17:03:53 +02:00
function migrate(name, cb) {
if (!localStorage.getItem(name)) try {
cb();
console.debug("Ran migration : " + name);
localStorage.setItem(name, "" + Date.now());
2025-04-06 18:21:53 +02:00
migrationsRun++;
2025-04-02 17:03:53 +02:00
} catch (e) {
console.warn("Migration " + name + " failed : ", e);
}
}
2025-04-08 10:36:30 +02:00
function afterMigration() {
// Avoid a boot loop by setting the hash before reloading
// We can't set the query string as it is used for other things
if (migrationsRun & & !window.location.hash) {
window.location.hash = "#reloadAfterMigration";
window.location.reload();
}
if (!migrationsRun) window.location.hash = "";
}
2025-04-06 18:21:53 +02:00
migrate("save_data_before_upgrade_to_" + (0, _versionJsonDefault.default), ()=>{
localStorage.setItem("recovery_data", JSON.stringify((0, _generateSaveFileContent.generateSaveFileContent)()));
});
2025-04-02 17:03:53 +02:00
migrate("migrate_high_scores", ()=>{
const old = localStorage.getItem("breakout-3-hs");
if (old) {
localStorage.setItem("breakout-3-hs-short", old);
localStorage.removeItem("breakout-3-hs");
}
});
migrate("recover_high_scores", ()=>{
let runsHistory = JSON.parse(localStorage.getItem("breakout_71_runs_history") || "[]");
runsHistory.forEach((r)=>{
const currentHS = parseInt(localStorage.getItem("breakout-3-hs-" + (r.mode || "short")) || "0");
if (r.score > currentHS) localStorage.setItem("breakout-3-hs-" + (r.mode || "short"), "" + r.score);
});
});
2025-04-06 10:13:10 +02:00
migrate("remove_long_and_creative_mode_data", ()=>{
let runsHistory = JSON.parse(localStorage.getItem("breakout_71_runs_history") || "[]");
let cleaned = runsHistory.filter((r)=>{
2025-04-07 14:08:48 +02:00
if (!r.perks) return false;
2025-04-06 15:38:30 +02:00
if ("mode" in r) {
if (r.mode !== "short") return false;
2025-04-06 10:13:10 +02:00
}
return true;
});
if (cleaned.length !== runsHistory.length) localStorage.setItem("breakout_71_runs_history", JSON.stringify(cleaned));
});
2025-04-19 17:26:45 +02:00
migrate("compact_runs_data_again", ()=>{
2025-04-06 18:21:53 +02:00
let runsHistory = JSON.parse(localStorage.getItem("breakout_71_runs_history") || "[]");
2025-04-19 17:26:45 +02:00
runsHistory = runsHistory.filter((r)=>{
if (!r.perks) return false;
if ("mode" in r) {
if (r.mode !== "short") return false;
delete r.mode;
}
return true;
});
2025-04-06 18:21:53 +02:00
runsHistory.forEach((r)=>{
r.runTime = Math.round(r.runTime);
2025-04-19 17:26:45 +02:00
if (r.perks) {
for(let key in r.perks)if (!r.perks[key]) delete r.perks[key];
}
2025-04-07 14:08:48 +02:00
if ("best_level_score" in r) delete r.best_level_score;
if ("worst_level_score" in r) delete r.worst_level_score;
2025-04-06 18:21:53 +02:00
});
localStorage.setItem("breakout_71_runs_history", JSON.stringify(runsHistory));
});
2025-04-08 10:36:30 +02:00
migrate("set_breakout_71_unlocked_levels" + (0, _versionJsonDefault.default), ()=>{
// We want to lock any level unlocked by an app upgrade too
let runsHistory = JSON.parse(localStorage.getItem("breakout_71_runs_history") || "[]");
let breakout_71_unlocked_levels = JSON.parse(localStorage.getItem("breakout_71_unlocked_levels") || "[]");
(0, _loadGameData.allLevels).filter((l, li)=>!(0, _gameUtils.reasonLevelIsLocked)(li, runsHistory, false)).forEach((l)=>{
if (!breakout_71_unlocked_levels.includes(l.name)) breakout_71_unlocked_levels.push(l.name);
});
localStorage.setItem("breakout_71_unlocked_levels", JSON.stringify(breakout_71_unlocked_levels));
});
afterMigration();
2025-04-06 18:21:53 +02:00
2025-04-08 15:17:14 +02:00
},{"./data/version.json":"iyP6E","./generateSaveFileContent":"iEcoB","./game_utils":"cEeac","./loadGameData":"l1B4x","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"iEcoB":[function(require,module,exports,__globalThis) {
2025-04-06 18:21:53 +02:00
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "generateSaveFileContent", ()=>generateSaveFileContent);
function generateSaveFileContent() {
const localStorageContent = {};
for(let i = 0; i < localStorage.length ; i + + ) {
const key = localStorage.key(i);
// Avoid including recovery info in the recovery info
if ([
2025-04-07 14:08:48 +02:00
"recovery_data"
2025-04-06 18:21:53 +02:00
].includes(key)) continue;
2025-04-19 17:26:45 +02:00
try {
const value = localStorage.getItem(key);
localStorageContent[key] = JSON.parse(value);
} catch (e) {}
2025-04-06 18:21:53 +02:00
}
2025-04-19 17:26:45 +02:00
return localStorageContent;
2025-04-06 18:21:53 +02:00
}
2025-04-02 17:03:53 +02:00
2025-04-07 08:24:17 +02:00
},{"@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"b80Ki":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "runHistoryViewerMenuEntry", ()=>runHistoryViewerMenuEntry);
var _gameOver = require("./gameOver");
var _loadGameData = require("./loadGameData");
var _i18N = require("./i18n/i18n");
var _asyncAlert = require("./asyncAlert");
var _upgrades = require("./upgrades");
function runHistoryViewerMenuEntry() {
const history = (0, _gameOver.getHistory)();
return {
2025-04-07 14:08:48 +02:00
icon: (0, _loadGameData.icons)["icon:history"],
text: (0, _i18N.t)("history.title"),
2025-04-07 08:24:17 +02:00
disabled: history.length < 10 ,
2025-04-07 14:08:48 +02:00
help: history.length < 10 ? ( 0 , _i18N . t ) ( " history . locked " ) : ( 0 , _i18N . t ) ( " history . help " , {
2025-04-07 08:24:17 +02:00
count: history.length
}),
async value () {
let sort = 0;
let sortDir = -1;
let columns = [
{
2025-04-07 14:08:48 +02:00
label: (0, _i18N.t)("history.columns.started"),
2025-04-07 08:24:17 +02:00
field: (r)=>r.started,
render (v) {
return new Date(v).toISOString().slice(0, 10);
}
},
{
2025-04-07 14:08:48 +02:00
label: (0, _i18N.t)("history.columns.score"),
2025-04-07 08:24:17 +02:00
field: (r)=>r.score
},
...(0, _upgrades.rawUpgrades).map((u)=>({
2025-04-07 14:08:48 +02:00
label: (0, _loadGameData.icons)["icon:" + u.id],
2025-04-07 08:24:17 +02:00
tooltip: u.name,
2025-04-07 14:08:48 +02:00
field: (r)=>r.perks?.[u.id] || 0,
2025-04-07 08:24:17 +02:00
render (v) {
2025-04-07 14:08:48 +02:00
if (!v) return "-";
2025-04-07 08:24:17 +02:00
return v;
}
}))
];
while(true){
2025-04-07 14:08:48 +02:00
const header = columns.map((c, ci)=>`< th data-tooltip = "${c.tooltip || " " } " data-resolve-to = "sort:${ci}" > ${c.label}< / th > `).join("");
2025-04-07 08:24:17 +02:00
const toString = (v)=>v.toString();
2025-04-07 14:08:48 +02:00
const tbody = history.sort((a, b)=>sortDir * (columns[sort].field(a) - columns[sort].field(b))).map((h)=>"< tr > " + columns.map((c)=>{
2025-04-07 08:24:17 +02:00
const value = c.field(h) ?? 0;
const render = c.render || toString;
2025-04-07 14:08:48 +02:00
return "< td > " + render(value) + "< / td > ";
}).join("") + "< / tr > ").join("");
2025-04-07 08:24:17 +02:00
const result = await (0, _asyncAlert.asyncAlert)({
2025-04-07 14:08:48 +02:00
title: (0, _i18N.t)("history.title"),
className: "history",
2025-04-07 08:24:17 +02:00
content: [
`
< table >
< thead > < tr > ${header}< / tr > < / thead >
< tbody > ${tbody}< / tbody >
< / table >
`
]
});
if (!result) return;
2025-04-07 14:08:48 +02:00
if (result.startsWith("sort:")) {
const newSort = parseInt(result.split(":")[1]);
2025-04-07 08:24:17 +02:00
if (newSort == sort) sortDir *= -1;
else {
sortDir = -1;
sort = newSort;
}
}
}
}
};
}
2025-04-08 15:17:14 +02:00
},{"./gameOver":"caCAf","./loadGameData":"l1B4x","./i18n/i18n":"eNPRm","./asyncAlert":"rSqLY","./upgrades":"1u3Dx","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"aHTmD":[function(require,module,exports,__globalThis) {
2025-04-07 14:08:48 +02:00
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "openScorePanel", ()=>openScorePanel);
parcelHelpers.export(exports, "getNearestUnlockHTML", ()=>getNearestUnlockHTML);
var _asyncAlert = require("./asyncAlert");
var _i18N = require("./i18n/i18n");
var _gameUtils = require("./game_utils");
var _gameOver = require("./gameOver");
var _game = require("./game");
var _loadGameData = require("./loadGameData");
2025-04-08 14:03:38 +02:00
var _pureFunctions = require("./pure_functions");
var _settings = require("./settings");
2025-04-07 14:08:48 +02:00
async function openScorePanel(gameState) {
(0, _game.pause)(true);
await (0, _asyncAlert.asyncAlert)({
title: (0, _i18N.t)("score_panel.title", {
score: gameState.score,
level: gameState.currentLevel + 1,
max: (0, _gameUtils.max_levels)(gameState)
}),
content: [
(0, _gameOver.getCreativeModeWarning)(gameState),
(0, _gameUtils.pickedUpgradesHTMl)(gameState),
(0, _gameUtils.levelsListHTMl)(gameState, gameState.currentLevel),
getNearestUnlockHTML(gameState),
gameState.rerolls ? (0, _i18N.t)("score_panel.rerolls_count", {
rerolls: gameState.rerolls
}) : ""
],
allowClose: true
});
}
function getNearestUnlockHTML(gameState) {
2025-04-07 15:25:58 +02:00
if (gameState.creative) return "";
2025-04-08 14:03:38 +02:00
const unlocked = new Set((0, _settings.getSettingValue)("breakout_71_unlocked_levels", []));
const firstUnlockable = (0, _pureFunctions.firstWhere)((0, _loadGameData.allLevels), (l, li)=>{
if (unlocked.has(l.name)) return;
const reason = (0, _gameUtils.reasonLevelIsLocked)(li, (0, _gameOver.getHistory)(), false);
if (!reason) return;
2025-04-07 14:08:48 +02:00
const { minScore, forbidden, required } = (0, _gameUtils.getLevelUnlockCondition)(li);
2025-04-08 14:03:38 +02:00
const missing = required.filter((u)=>!gameState?.perks?.[u.id]);
// we can't have a forbidden perk
if (forbidden.find((u)=>gameState?.perks?.[u.id])) return;
// All required upgrades need to be unlocked
if (missing.find((u)=>u.threshold > (0, _settings.getTotalScore)())) return;
2025-04-07 14:08:48 +02:00
return {
l,
li,
minScore,
forbidden,
required,
2025-04-08 14:03:38 +02:00
missing,
reason
2025-04-07 14:08:48 +02:00
};
2025-04-08 14:03:38 +02:00
});
2025-04-07 14:08:48 +02:00
if (!firstUnlockable) return "";
2025-04-11 08:15:58 +02:00
let missingPoints = Math.max(0, firstUnlockable.minScore - gameState.score);
2025-04-07 14:08:48 +02:00
let missingUpgrades = firstUnlockable.missing.map((u)=>u.name).join(", ");
const title = missingUpgrades & & (0, _i18N.t)("score_panel.get_upgrades_to_unlock", {
missingUpgrades,
points: missingPoints,
level: firstUnlockable.l.name
2025-04-08 14:03:38 +02:00
}) || (0, _i18N.t)("score_panel.score_to_unlock", {
2025-04-07 14:08:48 +02:00
points: missingPoints,
level: firstUnlockable.l.name
});
return `
2025-04-11 08:15:58 +02:00
< p > ${(0, _i18N.t)("score_panel.close_to_unlock")}< / p >
2025-04-07 14:08:48 +02:00
< div class = "upgrade used" >
${(0, _loadGameData.icons)[firstUnlockable.l.name]}
< p >
< strong > ${title}< / strong >
${firstUnlockable.reason?.text}
< / p >
< / div >
`;
}
2025-04-08 15:17:14 +02:00
},{"./asyncAlert":"rSqLY","./i18n/i18n":"eNPRm","./game_utils":"cEeac","./gameOver":"caCAf","./game":"edeGs","./loadGameData":"l1B4x","./pure_functions":"6pQh7","./settings":"5blfu","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"jjD0P":[function(require,module,exports,__globalThis) {
2025-04-08 10:36:30 +02:00
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "monitorLevelsUnlocks", ()=>monitorLevelsUnlocks);
var _settings = require("./settings");
var _loadGameData = require("./loadGameData");
var _gameUtils = require("./game_utils");
var _i18N = require("./i18n/i18n");
var _toast = require("./toast");
var _gameStateMutators = require("./gameStateMutators");
let list;
2025-04-08 14:03:38 +02:00
let unlocked = new Set((0, _settings.getSettingValue)("breakout_71_unlocked_levels", []));
2025-04-08 10:36:30 +02:00
function monitorLevelsUnlocks(gameState) {
if (gameState.creative) return;
if (!list) list = (0, _loadGameData.allLevels).map((l, li)=>({
name: l.name,
li,
l,
...(0, _gameUtils.getLevelUnlockCondition)(li)
}));
list.forEach(({ name, minScore, forbidden, required, l })=>{
// Already unlocked
if (unlocked.has(name)) return;
// Score not reached yet
if (gameState.score < minScore ) return ;
2025-04-16 15:30:20 +02:00
if (!minScore) return;
if (gameState.score < minScore ) return ;
2025-04-08 10:36:30 +02:00
// We are missing a required perk
if (required.find((id)=>!gameState.perks[id])) return;
// We have a forbidden perk
if (forbidden.find((id)=>gameState.perks[id])) return;
// Level just got unlocked
unlocked.add(name);
2025-04-08 14:03:38 +02:00
(0, _settings.setSettingValue)("breakout_71_unlocked_levels", (0, _settings.getSettingValue)("breakout_71_unlocked_levels", []).concat([
2025-04-08 10:36:30 +02:00
name
]));
2025-04-08 14:03:38 +02:00
(0, _toast.toast)((0, _loadGameData.icons)[name] + "< strong > " + (0, _i18N.t)("unlocks.just_unlocked") + "< / strong > ");
(0, _gameStateMutators.schedulGameSound)(gameState, "colorChange", 0, 1);
2025-04-08 10:36:30 +02:00
});
}
2025-04-18 21:17:32 +02:00
},{"./settings":"5blfu","./loadGameData":"l1B4x","./game_utils":"cEeac","./i18n/i18n":"eNPRm","./toast":"nAuvo","./gameStateMutators":"9ZeQl","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}]},["x07Me"], "x07Me", "parcelRequire94c2")
2025-03-19 18:13:41 +01:00
< / script >
< / body >
< / html >