mirror of
https://gitlab.com/lecarore/breakout71.git
synced 2025-04-25 22:46:15 -04:00
3583 lines
169 KiB
HTML
3583 lines
169 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head><script src="/index.c0fd3053.js"></script>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
|
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
<title>Breakout 71</title>
|
|
<meta name="description" content="A breakout game with roguelite mechanics. Break bricks, catch coins, pick upgrades, repeat. Play for free on mobile and desktop.">
|
|
<link rel="manifest" href="/manifest.webmanifest">
|
|
|
|
<style>* {
|
|
box-sizing: border-box;
|
|
font-family: Courier New, Courier, Lucida Sans Typewriter, Lucida Typewriter, monospace;
|
|
}
|
|
|
|
body {
|
|
width: 100vw;
|
|
height: 100vh;
|
|
height: calc(var(--vh, 1vh) * 100);
|
|
color: #fff;
|
|
background-size: 120px 120px;
|
|
background-color: var(--background1);
|
|
--background1: #030c23;
|
|
--background2: #03112a;
|
|
margin: 0;
|
|
padding: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
#game {
|
|
height: 100vh;
|
|
height: calc(var(--vh, 1vh) * 100);
|
|
width: 100vw;
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
}
|
|
|
|
#score, #menu {
|
|
z-index: 1;
|
|
appearance: none;
|
|
font: inherit;
|
|
color: #fff;
|
|
background: none;
|
|
border: none;
|
|
min-width: 40px;
|
|
min-height: 40px;
|
|
padding: 10px;
|
|
line-height: 20px;
|
|
position: absolute;
|
|
top: 0;
|
|
}
|
|
|
|
#score:hover, #menu:hover, #score:focus, #menu:focus {
|
|
cursor: pointer;
|
|
background: #0000004d;
|
|
}
|
|
|
|
#score {
|
|
color: #ffffff4d;
|
|
transition: color .3s;
|
|
right: 0;
|
|
}
|
|
|
|
#score.active {
|
|
color: gold;
|
|
transition: color 10ms;
|
|
}
|
|
|
|
#menu {
|
|
left: 0;
|
|
}
|
|
|
|
.popup {
|
|
z-index: 10;
|
|
background: #000000e6;
|
|
display: flex;
|
|
position: fixed;
|
|
inset: 0;
|
|
overflow: auto;
|
|
}
|
|
|
|
.popup > div {
|
|
transform-origin: center;
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
width: 100%;
|
|
max-width: 450px;
|
|
margin: auto;
|
|
padding: 20px 10px;
|
|
display: flex;
|
|
}
|
|
|
|
.popup > div > * {
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
.popup > div > h2, .popup > div > p {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.popup > div > section {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
margin-top: 20px;
|
|
display: flex;
|
|
}
|
|
|
|
.popup > div > section button {
|
|
font: inherit;
|
|
color: #fff;
|
|
cursor: pointer;
|
|
text-align: left;
|
|
background: #000c;
|
|
border: 1px solid #fff;
|
|
gap: 10px;
|
|
margin-top: -1px;
|
|
padding: 10px;
|
|
display: flex;
|
|
}
|
|
|
|
.popup > div > section button:not([disabled]):hover, .popup > div > section button:not([disabled]):focus {
|
|
z-index: 1;
|
|
border-color: #f1d33b;
|
|
position: relative;
|
|
}
|
|
|
|
.popup > div > section button[disabled] {
|
|
opacity: .5;
|
|
filter: saturate(0);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.popup > div > section button > div {
|
|
flex-grow: 1;
|
|
}
|
|
|
|
.popup > div > section button > div > em {
|
|
opacity: .8;
|
|
display: block;
|
|
}
|
|
|
|
.popup > div > section button.grey-out-unless-hovered:not(:hover) {
|
|
opacity: .6;
|
|
}
|
|
|
|
.popup > div > section button.grey-out-unless-hovered:not(:hover) img {
|
|
filter: saturate(0);
|
|
}
|
|
|
|
.popup.actionsAsGrid > div {
|
|
max-width: 800px;
|
|
}
|
|
|
|
.popup.actionsAsGrid > div section {
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
display: grid;
|
|
}
|
|
|
|
.popup button.close-modale {
|
|
color: #fff;
|
|
cursor: pointer;
|
|
background: none;
|
|
border: none;
|
|
width: 60px;
|
|
height: 60px;
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.popup button.close-modale:before {
|
|
content: "+";
|
|
font-size: 80px;
|
|
display: inline-block;
|
|
position: absolute;
|
|
top: 34px;
|
|
left: 26px;
|
|
transform: translate(-50%, -50%)rotate(45deg);
|
|
}
|
|
|
|
.popup button.close-modale:hover {
|
|
background: #000;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.popup .textAfterButtons {
|
|
color: #ffffff94;
|
|
}
|
|
|
|
.popup a[href] {
|
|
color: inherit;
|
|
}
|
|
|
|
.popup a[href]:hover, .popup a[href]:focus {
|
|
color: #fff;
|
|
}
|
|
|
|
.progress {
|
|
color: #fff;
|
|
text-align: center;
|
|
background: #1c1c2f;
|
|
border-radius: 5px;
|
|
padding: 5px 10px;
|
|
display: block;
|
|
position: relative;
|
|
overflow: hidden;
|
|
box-shadow: inset 3px 3px 5px #00000080;
|
|
}
|
|
|
|
.progress > .progress_bar_part {
|
|
transform-origin: 0 0;
|
|
z-index: 1;
|
|
background: #4049ca;
|
|
animation: 1s ease-out both grow;
|
|
display: block;
|
|
position: absolute;
|
|
inset: 0;
|
|
box-shadow: inset 3px 3px 5px #00000080;
|
|
}
|
|
|
|
.progress > span {
|
|
z-index: 2;
|
|
display: block;
|
|
position: relative;
|
|
}
|
|
|
|
@keyframes grow {
|
|
0% {
|
|
transform: scale(0, 1);
|
|
}
|
|
}
|
|
|
|
#level-recording-container {
|
|
text-align: center;
|
|
max-width: 400px;
|
|
margin: 40px;
|
|
}
|
|
|
|
#level-recording-container video {
|
|
max-width: 100%;
|
|
height: auto;
|
|
}
|
|
|
|
#level-recording-container a {
|
|
display: block;
|
|
}
|
|
|
|
#level-recording-container a video {
|
|
border-radius: 10px;
|
|
outline: 1px solid #fff;
|
|
margin: 20px auto;
|
|
display: block;
|
|
box-shadow: 2px 2px 5px #000;
|
|
}
|
|
|
|
@media (width >= 1200px) {
|
|
#level-recording-container {
|
|
max-width: calc(50vw - 305px);
|
|
position: absolute;
|
|
top: 40px;
|
|
left: 40px;
|
|
}
|
|
}
|
|
|
|
.histogram {
|
|
align-items: stretch;
|
|
gap: 10px;
|
|
margin: 10px 0 40px;
|
|
display: flex;
|
|
}
|
|
|
|
.histogram > span {
|
|
flex-direction: column;
|
|
flex-grow: 1;
|
|
justify-content: flex-end;
|
|
width: 10px;
|
|
display: flex;
|
|
position: relative;
|
|
}
|
|
|
|
.histogram > span.active > span {
|
|
background: #4049ca;
|
|
}
|
|
|
|
.histogram > span > span {
|
|
background: #1c1c2f;
|
|
border-radius: 5px;
|
|
width: 100%;
|
|
min-height: 1px;
|
|
display: block;
|
|
}
|
|
|
|
.histogram > span > span > span {
|
|
pointer-events: none;
|
|
white-space: nowrap;
|
|
transform-origin: 0 100%;
|
|
text-align: center;
|
|
font-size: 13px;
|
|
display: block;
|
|
position: absolute;
|
|
bottom: -20px;
|
|
left: 50%;
|
|
transform: translate(-50%);
|
|
}
|
|
|
|
.histogram > span:not(:hover):not(.active) > span > span {
|
|
opacity: 0;
|
|
}
|
|
|
|
h2.histogram-title {
|
|
color: #3b3f75;
|
|
font-size: 18px;
|
|
}
|
|
|
|
h2.histogram-title strong {
|
|
color: #4049ca;
|
|
}
|
|
</style>
|
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🕹️</text></svg>">
|
|
</head>
|
|
<body>
|
|
<button id="menu">☰ <span id="menuLabel">menu</span></button>
|
|
<button id="score"></button>
|
|
<canvas id="game"></canvas>
|
|
<script>// modules are defined as an array
|
|
// [ module function, map of requires ]
|
|
//
|
|
// map of requires is short require name -> numeric require
|
|
//
|
|
// anything defined in a previous bundle is accessed via the
|
|
// orig method which is the require for previous bundles
|
|
|
|
(function (modules, entry, mainEntry, parcelRequireName, globalName) {
|
|
/* eslint-disable no-undef */
|
|
var globalObject =
|
|
typeof globalThis !== 'undefined'
|
|
? globalThis
|
|
: typeof self !== 'undefined'
|
|
? self
|
|
: typeof window !== 'undefined'
|
|
? window
|
|
: typeof global !== 'undefined'
|
|
? global
|
|
: {};
|
|
/* eslint-enable no-undef */
|
|
|
|
// Save the require from previous bundle to this closure if any
|
|
var previousRequire =
|
|
typeof globalObject[parcelRequireName] === 'function' &&
|
|
globalObject[parcelRequireName];
|
|
|
|
var cache = previousRequire.cache || {};
|
|
// Do not use `require` to prevent Webpack from trying to bundle this call
|
|
var nodeRequire =
|
|
typeof module !== 'undefined' &&
|
|
typeof module.require === 'function' &&
|
|
module.require.bind(module);
|
|
|
|
function newRequire(name, jumped) {
|
|
if (!cache[name]) {
|
|
if (!modules[name]) {
|
|
// if we cannot find the module within our internal map or
|
|
// cache jump to the current global require ie. the last bundle
|
|
// that was added to the page.
|
|
var currentRequire =
|
|
typeof globalObject[parcelRequireName] === 'function' &&
|
|
globalObject[parcelRequireName];
|
|
if (!jumped && currentRequire) {
|
|
return currentRequire(name, true);
|
|
}
|
|
|
|
// If there are other bundles on this page the require from the
|
|
// previous one is saved to 'previousRequire'. Repeat this as
|
|
// many times as there are bundles until the module is found or
|
|
// we exhaust the require chain.
|
|
if (previousRequire) {
|
|
return previousRequire(name, true);
|
|
}
|
|
|
|
// Try the node require function if it exists.
|
|
if (nodeRequire && typeof name === 'string') {
|
|
return nodeRequire(name);
|
|
}
|
|
|
|
var err = new Error("Cannot find module '" + name + "'");
|
|
err.code = 'MODULE_NOT_FOUND';
|
|
throw err;
|
|
}
|
|
|
|
localRequire.resolve = resolve;
|
|
localRequire.cache = {};
|
|
|
|
var module = (cache[name] = new newRequire.Module(name));
|
|
|
|
modules[name][0].call(
|
|
module.exports,
|
|
localRequire,
|
|
module,
|
|
module.exports,
|
|
globalObject
|
|
);
|
|
}
|
|
|
|
return cache[name].exports;
|
|
|
|
function localRequire(x) {
|
|
var res = localRequire.resolve(x);
|
|
return res === false ? {} : newRequire(res);
|
|
}
|
|
|
|
function resolve(x) {
|
|
var id = modules[name][1][x];
|
|
return id != null ? id : x;
|
|
}
|
|
}
|
|
|
|
function Module(moduleName) {
|
|
this.id = moduleName;
|
|
this.bundle = newRequire;
|
|
this.exports = {};
|
|
}
|
|
|
|
newRequire.isParcelRequire = true;
|
|
newRequire.Module = Module;
|
|
newRequire.modules = modules;
|
|
newRequire.cache = cache;
|
|
newRequire.parent = previousRequire;
|
|
newRequire.register = function (id, exports) {
|
|
modules[id] = [
|
|
function (require, module) {
|
|
module.exports = exports;
|
|
},
|
|
{},
|
|
];
|
|
};
|
|
|
|
Object.defineProperty(newRequire, 'root', {
|
|
get: function () {
|
|
return globalObject[parcelRequireName];
|
|
},
|
|
});
|
|
|
|
globalObject[parcelRequireName] = newRequire;
|
|
|
|
for (var i = 0; i < entry.length; i++) {
|
|
newRequire(entry[i]);
|
|
}
|
|
|
|
if (mainEntry) {
|
|
// Expose entry point to Node, AMD or browser globals
|
|
// Based on https://github.com/ForbesLindesay/umd/blob/master/template.js
|
|
var mainExports = newRequire(mainEntry);
|
|
|
|
// CommonJS
|
|
if (typeof exports === 'object' && typeof module !== 'undefined') {
|
|
module.exports = mainExports;
|
|
|
|
// RequireJS
|
|
} else if (typeof define === 'function' && define.amd) {
|
|
define(function () {
|
|
return mainExports;
|
|
});
|
|
|
|
// <script>
|
|
} else if (globalName) {
|
|
this[globalName] = mainExports;
|
|
}
|
|
}
|
|
})({"fxnks":[function(require,module,exports,__globalThis) {
|
|
require("3ab5c1708a8ff3cf")(require("3bec9e730869a0f0").getBundleURL('28aWT') + "index.c0fd3053.js");
|
|
|
|
},{"3ab5c1708a8ff3cf":"61B45","3bec9e730869a0f0":"lgJ39"}],"61B45":[function(require,module,exports,__globalThis) {
|
|
"use strict";
|
|
var cacheLoader = require("ca2a84f7fa4a3bb0");
|
|
module.exports = cacheLoader(function(bundle) {
|
|
return new Promise(function(resolve, reject) {
|
|
// Don't insert the same script twice (e.g. if it was already in the HTML)
|
|
var existingScripts = document.getElementsByTagName('script');
|
|
if ([].concat(existingScripts).some(function(script) {
|
|
return script.src === bundle;
|
|
})) {
|
|
resolve();
|
|
return;
|
|
}
|
|
var preloadLink = document.createElement('link');
|
|
preloadLink.href = bundle;
|
|
preloadLink.rel = 'preload';
|
|
preloadLink.as = 'script';
|
|
document.head.appendChild(preloadLink);
|
|
var script = document.createElement('script');
|
|
script.async = true;
|
|
script.type = 'text/javascript';
|
|
script.src = bundle;
|
|
script.onerror = function(e) {
|
|
var error = new TypeError("Failed to fetch dynamically imported module: ".concat(bundle, ". Error: ").concat(e.message));
|
|
script.onerror = script.onload = null;
|
|
script.remove();
|
|
reject(error);
|
|
};
|
|
script.onload = function() {
|
|
script.onerror = script.onload = null;
|
|
resolve();
|
|
};
|
|
document.getElementsByTagName('head')[0].appendChild(script);
|
|
});
|
|
});
|
|
|
|
},{"ca2a84f7fa4a3bb0":"j49pS"}],"j49pS":[function(require,module,exports,__globalThis) {
|
|
"use strict";
|
|
var cachedBundles = {};
|
|
var cachedPreloads = {};
|
|
var cachedPrefetches = {};
|
|
function getCache(type) {
|
|
switch(type){
|
|
case 'preload':
|
|
return cachedPreloads;
|
|
case 'prefetch':
|
|
return cachedPrefetches;
|
|
default:
|
|
return cachedBundles;
|
|
}
|
|
}
|
|
module.exports = function(loader, type) {
|
|
return function(bundle) {
|
|
var cache = getCache(type);
|
|
if (cache[bundle]) return cache[bundle];
|
|
return cache[bundle] = loader.apply(null, arguments).catch(function(e) {
|
|
delete cache[bundle];
|
|
throw e;
|
|
});
|
|
};
|
|
};
|
|
|
|
},{}],"lgJ39":[function(require,module,exports,__globalThis) {
|
|
"use strict";
|
|
var bundleURL = {};
|
|
function getBundleURLCached(id) {
|
|
var value = bundleURL[id];
|
|
if (!value) {
|
|
value = getBundleURL();
|
|
bundleURL[id] = value;
|
|
}
|
|
return value;
|
|
}
|
|
function getBundleURL() {
|
|
try {
|
|
throw new Error();
|
|
} catch (err) {
|
|
var matches = ('' + err.stack).match(/(https?|file|ftp|(chrome|moz|safari-web)-extension):\/\/[^)\n]+/g);
|
|
if (matches) // The first two stack frames will be this function and getBundleURLCached.
|
|
// Use the 3rd one, which will be a runtime in the original bundle.
|
|
return getBaseURL(matches[2]);
|
|
}
|
|
return '/';
|
|
}
|
|
function getBaseURL(url) {
|
|
return ('' + url).replace(/^((?:https?|file|ftp|(chrome|moz|safari-web)-extension):\/\/.+)\/[^/]+$/, '$1') + '/';
|
|
}
|
|
// TODO: Replace uses with `new URL(url).origin` when ie11 is no longer supported.
|
|
function getOrigin(url) {
|
|
var matches = ('' + url).match(/(https?|file|ftp|(chrome|moz|safari-web)-extension):\/\/[^/]+/);
|
|
if (!matches) throw new Error('Origin not found');
|
|
return matches[0];
|
|
}
|
|
exports.getBundleURL = getBundleURLCached;
|
|
exports.getBaseURL = getBaseURL;
|
|
exports.getOrigin = getOrigin;
|
|
|
|
},{}],"bCo5X":[function(require,module,exports,__globalThis) {
|
|
var _gameTs = require("./game.ts");
|
|
|
|
},{"./game.ts":"edeGs"}],"edeGs":[function(require,module,exports,__globalThis) {
|
|
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
|
|
parcelHelpers.defineInteropFlag(exports);
|
|
parcelHelpers.export(exports, "play", ()=>play);
|
|
parcelHelpers.export(exports, "pause", ()=>pause);
|
|
parcelHelpers.export(exports, "fitSize", ()=>fitSize);
|
|
parcelHelpers.export(exports, "recomputeTargetBaseSpeed", ()=>recomputeTargetBaseSpeed);
|
|
parcelHelpers.export(exports, "brickCenterX", ()=>brickCenterX);
|
|
parcelHelpers.export(exports, "brickCenterY", ()=>brickCenterY);
|
|
parcelHelpers.export(exports, "getRowColIndex", ()=>getRowColIndex);
|
|
parcelHelpers.export(exports, "spawnExplosion", ()=>spawnExplosion);
|
|
parcelHelpers.export(exports, "addToScore", ()=>addToScore);
|
|
parcelHelpers.export(exports, "pickedUpgradesHTMl", ()=>pickedUpgradesHTMl);
|
|
parcelHelpers.export(exports, "setLevel", ()=>setLevel);
|
|
parcelHelpers.export(exports, "currentLevelInfo", ()=>currentLevelInfo);
|
|
parcelHelpers.export(exports, "getPossibleUpgrades", ()=>getPossibleUpgrades);
|
|
parcelHelpers.export(exports, "getUpgraderUnlockPoints", ()=>getUpgraderUnlockPoints);
|
|
parcelHelpers.export(exports, "dontOfferTooSoon", ()=>dontOfferTooSoon);
|
|
parcelHelpers.export(exports, "pickRandomUpgrades", ()=>pickRandomUpgrades);
|
|
parcelHelpers.export(exports, "setMousePos", ()=>setMousePos);
|
|
parcelHelpers.export(exports, "brickIndex", ()=>brickIndex);
|
|
parcelHelpers.export(exports, "hasBrick", ()=>hasBrick);
|
|
parcelHelpers.export(exports, "hitsSomething", ()=>hitsSomething);
|
|
parcelHelpers.export(exports, "shouldPierceByColor", ()=>shouldPierceByColor);
|
|
parcelHelpers.export(exports, "coinBrickHitCheck", ()=>coinBrickHitCheck);
|
|
parcelHelpers.export(exports, "bordersHitCheck", ()=>bordersHitCheck);
|
|
parcelHelpers.export(exports, "tick", ()=>tick);
|
|
parcelHelpers.export(exports, "isTelekinesisActive", ()=>isTelekinesisActive);
|
|
parcelHelpers.export(exports, "ballTick", ()=>ballTick);
|
|
parcelHelpers.export(exports, "getTotalScore", ()=>getTotalScore);
|
|
parcelHelpers.export(exports, "addToTotalScore", ()=>addToTotalScore);
|
|
parcelHelpers.export(exports, "addToTotalPlayTime", ()=>addToTotalPlayTime);
|
|
parcelHelpers.export(exports, "gameOver", ()=>gameOver);
|
|
parcelHelpers.export(exports, "getHistograms", ()=>getHistograms);
|
|
parcelHelpers.export(exports, "explodeBrick", ()=>explodeBrick);
|
|
parcelHelpers.export(exports, "max_levels", ()=>max_levels);
|
|
parcelHelpers.export(exports, "render", ()=>render);
|
|
parcelHelpers.export(exports, "renderAllBricks", ()=>renderAllBricks);
|
|
parcelHelpers.export(exports, "drawPuck", ()=>drawPuck);
|
|
parcelHelpers.export(exports, "drawBall", ()=>drawBall);
|
|
parcelHelpers.export(exports, "drawCoin", ()=>drawCoin);
|
|
parcelHelpers.export(exports, "drawFuzzyBall", ()=>drawFuzzyBall);
|
|
parcelHelpers.export(exports, "drawBrick", ()=>drawBrick);
|
|
parcelHelpers.export(exports, "roundRect", ()=>roundRect);
|
|
parcelHelpers.export(exports, "drawIMG", ()=>drawIMG);
|
|
parcelHelpers.export(exports, "drawText", ()=>drawText);
|
|
parcelHelpers.export(exports, "asyncAlert", ()=>asyncAlert);
|
|
parcelHelpers.export(exports, "distance2", ()=>distance2);
|
|
parcelHelpers.export(exports, "distanceBetween", ()=>distanceBetween);
|
|
parcelHelpers.export(exports, "rainbowColor", ()=>rainbowColor);
|
|
parcelHelpers.export(exports, "repulse", ()=>repulse);
|
|
parcelHelpers.export(exports, "attract", ()=>attract);
|
|
parcelHelpers.export(exports, "recordOneFrame", ()=>recordOneFrame);
|
|
parcelHelpers.export(exports, "drawMainCanvasOnSmallCanvas", ()=>drawMainCanvasOnSmallCanvas);
|
|
parcelHelpers.export(exports, "startRecordingGame", ()=>startRecordingGame);
|
|
parcelHelpers.export(exports, "pauseRecording", ()=>pauseRecording);
|
|
parcelHelpers.export(exports, "resumeRecording", ()=>resumeRecording);
|
|
parcelHelpers.export(exports, "stopRecording", ()=>stopRecording);
|
|
parcelHelpers.export(exports, "captureFileName", ()=>captureFileName);
|
|
parcelHelpers.export(exports, "findLast", ()=>findLast);
|
|
parcelHelpers.export(exports, "toggleFullScreen", ()=>toggleFullScreen);
|
|
parcelHelpers.export(exports, "setKeyPressed", ()=>setKeyPressed);
|
|
parcelHelpers.export(exports, "newGameState", ()=>newGameState);
|
|
parcelHelpers.export(exports, "gameState", ()=>gameState);
|
|
parcelHelpers.export(exports, "restart", ()=>restart);
|
|
var _loadGameData = require("./loadGameData");
|
|
var _options = require("./options");
|
|
var _sounds = require("./sounds");
|
|
var _resetBalls = require("./resetBalls");
|
|
var _gameUtils = require("./game_utils");
|
|
var _combo = require("./combo");
|
|
var _swLoader = require("./sw_loader");
|
|
var _i18N = require("./i18n/i18n");
|
|
var _settings = require("./settings");
|
|
const gameCanvas = document.getElementById("game");
|
|
const ctx = gameCanvas.getContext("2d", {
|
|
alpha: false
|
|
});
|
|
const bombSVG = document.createElement("img");
|
|
bombSVG.src = "data:image/svg+xml;base64," + btoa(`<svg width="144" height="144" viewBox="0 0 38.101 38.099" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="m6.1528 26.516c-2.6992-3.4942-2.9332-8.281-.58305-11.981a10.454 10.454 0 017.3701-4.7582c1.962-.27726 4.1646.05953 5.8835.90027l.45013.22017.89782-.87417c.83748-.81464.91169-.87499 1.0992-.90271.40528-.058713.58876.03425 1.1971.6116l.55451.52679 1.0821-1.0821c1.1963-1.1963 1.383-1.3357 2.1039-1.5877.57898-.20223 1.5681-.19816 2.1691.00897 1.4613.50314 2.3673 1.7622 2.3567 3.2773-.0058.95654-.24464 1.5795-.90924 2.3746-.40936.48928-.55533.81057-.57898 1.2737-.02039.41018.1109.77714.42322 1.1792.30172.38816.3694.61323.2797.93044-.12803.45666-.56674.71598-1.0242.60507-.601-.14597-1.3031-1.3088-1.3969-2.3126-.09459-1.0161.19245-1.8682.92392-2.7432.42567-.50885.5643-.82851.5643-1.3031 0-.50151-.14026-.83177-.51211-1.2028-.50966-.50966-1.0968-.64829-1.781-.41996l-.37348.12477-2.1006 2.1006.52597.55696c.45421.48194.5325.58876.57898.78855.09622.41588.07502.45014-.88396 1.4548l-.87173.9125.26339.57979a10.193 10.193 0 01.9231 4.1001c.03996 2.046-.41996 3.8082-1.4442 5.537-.55044.928-1.0185 1.5013-1.8968 2.3241-.83503.78284-1.5526 1.2827-2.4904 1.7361-3.4266 1.657-7.4721 1.3422-10.549-.82035-.73473-.51782-1.7312-1.4621-2.2515-2.1357zm21.869-4.5584c-.0579-.19734-.05871-2.2662 0-2.4545.11906-.39142.57898-.63361 1.0038-.53005.23812.05708.54147.32455.6116.5382.06279.19163.06769 2.1805.0065 2.3811-.12558.40773-.61649.67602-1.0462.57164-.234-.05708-.51615-.30498-.57568-.50722m3.0417-2.6013c-.12313-.6222.37837-1.1049 1.0479-1.0079.18348.0261.25279.08399 1.0071.83911.75838.75838.81301.82362.84074 1.0112.10193.68499-.40365 1.1938-1.034 1.0405-.1949-.0473-.28786-.12558-1.0144-.85216-.7649-.76409-.80241-.81057-.84645-1.0316m.61323-3.0629a.85623.85623 0 01.59284-.99975c.28949-.09214 2.1814-.08318 2.3917.01141.38734.17369.6279.61078.53984.98181-.06035.25606-.35391.57327-.60181.64992-.25279.07747-2.2278.053-2.4097-.03017-.26013-.11906-.46318-.36125-.51374-.61323" fill="#fff" opacity="0.3"/>
|
|
</svg>`);
|
|
function play() {
|
|
if (gameState.running) return;
|
|
gameState.running = true;
|
|
startRecordingGame();
|
|
(0, _sounds.getAudioContext)()?.resume();
|
|
resumeRecording();
|
|
document.body.className = gameState.running ? " running " : " paused ";
|
|
}
|
|
function pause(playerAskedForPause) {
|
|
if (!gameState.running) return;
|
|
if (gameState.pauseTimeout) return;
|
|
gameState.pauseTimeout = setTimeout(()=>{
|
|
gameState.running = false;
|
|
gameState.needsRender = true;
|
|
setTimeout(()=>{
|
|
if (!gameState.running) (0, _sounds.getAudioContext)()?.suspend();
|
|
}, 1000);
|
|
pauseRecording();
|
|
gameState.pauseTimeout = null;
|
|
document.body.className = gameState.running ? " running " : " paused ";
|
|
scoreDisplay.className = "";
|
|
}, Math.min(Math.max(0, gameState.pauseUsesDuringRun - 5) * 50, 500));
|
|
if (playerAskedForPause) // Pausing many times in a run will make pause slower
|
|
gameState.pauseUsesDuringRun++;
|
|
if (document.exitPointerLock) document.exitPointerLock();
|
|
}
|
|
const background = document.createElement("img");
|
|
const backgroundCanvas = document.createElement("canvas");
|
|
background.addEventListener("load", ()=>{
|
|
gameState.needsRender = true;
|
|
});
|
|
const fitSize = ()=>{
|
|
const { width, height } = gameCanvas.getBoundingClientRect();
|
|
gameState.canvasWidth = width;
|
|
gameState.canvasHeight = height;
|
|
gameCanvas.width = width;
|
|
gameCanvas.height = height;
|
|
ctx.fillStyle = currentLevelInfo()?.color || "black";
|
|
ctx.globalAlpha = 1;
|
|
ctx.fillRect(0, 0, width, height);
|
|
backgroundCanvas.width = width;
|
|
backgroundCanvas.height = height;
|
|
gameState.gameZoneHeight = (0, _options.isOptionOn)("mobile-mode") ? height * 80 / 100 : height;
|
|
const baseWidth = Math.round(Math.min(gameState.canvasWidth, gameState.gameZoneHeight * 0.73));
|
|
gameState.brickWidth = Math.floor(baseWidth / gameState.gridSize / 2) * 2;
|
|
gameState.gameZoneWidth = gameState.brickWidth * gameState.gridSize;
|
|
gameState.offsetX = Math.floor((gameState.canvasWidth - gameState.gameZoneWidth) / 2);
|
|
gameState.offsetXRoundedDown = gameState.offsetX;
|
|
if (gameState.offsetX < gameState.ballSize) gameState.offsetXRoundedDown = 0;
|
|
gameState.gameZoneWidthRoundedUp = width - 2 * gameState.offsetXRoundedDown;
|
|
backgroundCanvas.title = "resized";
|
|
// Ensure puck stays within bounds
|
|
setMousePos(gameState.puckPosition);
|
|
gameState.coins = [];
|
|
gameState.flashes = [];
|
|
pause(true);
|
|
(0, _resetBalls.putBallsAtPuck)(gameState);
|
|
// For safari mobile https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
|
document.documentElement.style.setProperty("--vh", `${window.innerHeight * 0.01}px`);
|
|
};
|
|
window.addEventListener("resize", fitSize);
|
|
window.addEventListener("fullscreenchange", fitSize);
|
|
setInterval(()=>{
|
|
// Sometimes, the page changes size without triggering the event (when switching to fullscreen, closing debug panel...)
|
|
const { width, height } = gameCanvas.getBoundingClientRect();
|
|
if (width !== gameState.canvasWidth || height !== gameState.canvasHeight) fitSize();
|
|
}, 1000);
|
|
function recomputeTargetBaseSpeed() {
|
|
// We never want the ball to completely stop, it will move at least 3px per frame
|
|
gameState.baseSpeed = Math.max(3, gameState.gameZoneWidth / 12 / 10 + gameState.currentLevel / 3 + gameState.levelTime / 30000 - gameState.perks.slow_down * 2);
|
|
}
|
|
function brickCenterX(index) {
|
|
return gameState.offsetX + (index % gameState.gridSize + 0.5) * gameState.brickWidth;
|
|
}
|
|
function brickCenterY(index) {
|
|
return (Math.floor(index / gameState.gridSize) + 0.5) * gameState.brickWidth;
|
|
}
|
|
function getRowColIndex(row, col) {
|
|
if (row < 0 || col < 0 || row >= gameState.gridSize || col >= gameState.gridSize) return -1;
|
|
return row * gameState.gridSize + col;
|
|
}
|
|
function spawnExplosion(count, x, y, color, duration = 150, size = gameState.coinSize) {
|
|
if (!!(0, _options.isOptionOn)("basic")) return;
|
|
if (gameState.flashes.length > gameState.MAX_PARTICLES) // Avoid freezing when lots of explosion happen at once
|
|
count = 1;
|
|
for(let i = 0; i < count; i++)gameState.flashes.push({
|
|
type: "particle",
|
|
time: gameState.levelTime,
|
|
size,
|
|
x: x + (Math.random() - 0.5) * gameState.brickWidth / 2,
|
|
y: y + (Math.random() - 0.5) * gameState.brickWidth / 2,
|
|
vx: (Math.random() - 0.5) * 30,
|
|
vy: (Math.random() - 0.5) * 30,
|
|
color,
|
|
duration,
|
|
ethereal: false
|
|
});
|
|
}
|
|
function addToScore(coin) {
|
|
coin.destroyed = true;
|
|
gameState.score += coin.points;
|
|
gameState.lastScoreIncrease = gameState.levelTime;
|
|
addToTotalScore(coin.points);
|
|
if (gameState.score > gameState.highScore && !gameState.isCreativeModeRun) {
|
|
gameState.highScore = gameState.score;
|
|
localStorage.setItem("breakout-3-hs", gameState.score.toString());
|
|
}
|
|
if (!(0, _options.isOptionOn)("basic")) gameState.flashes.push({
|
|
type: "particle",
|
|
duration: 100 + Math.random() * 50,
|
|
time: gameState.levelTime,
|
|
size: gameState.coinSize / 2,
|
|
color: coin.color,
|
|
x: coin.previousX,
|
|
y: coin.previousY,
|
|
vx: (gameState.canvasWidth - coin.x) / 100,
|
|
vy: -coin.y / 100,
|
|
ethereal: true
|
|
});
|
|
if (Date.now() - gameState.lastPlayedCoinGrab > 16) {
|
|
gameState.lastPlayedCoinGrab = Date.now();
|
|
(0, _sounds.sounds).coinCatch(coin.x);
|
|
}
|
|
gameState.runStatistics.score += coin.points;
|
|
}
|
|
function pickedUpgradesHTMl() {
|
|
let list = "";
|
|
for (let u of (0, _loadGameData.upgrades))for(let i = 0; i < gameState.perks[u.id]; i++)list += (0, _loadGameData.icons)["icon:" + u.id] + " ";
|
|
return list;
|
|
}
|
|
async function openUpgradesPicker() {
|
|
const catchRate = (gameState.score - gameState.levelStartScore) / (gameState.levelSpawnedCoins || 1);
|
|
let repeats = 1;
|
|
let choices = 3;
|
|
let timeGain = "", catchGain = "", missesGain = "";
|
|
if (gameState.levelTime < 30000) {
|
|
repeats++;
|
|
choices++;
|
|
timeGain = (0, _i18N.t)('level_up.plus_one_upgrade');
|
|
} else if (gameState.levelTime < 60000) {
|
|
choices++;
|
|
timeGain = (0, _i18N.t)('level_up.plus_one_choice');
|
|
}
|
|
if (catchRate === 1) {
|
|
repeats++;
|
|
choices++;
|
|
catchGain = (0, _i18N.t)('level_up.plus_one_upgrade');
|
|
} else if (catchRate > 0.9) {
|
|
choices++;
|
|
catchGain = (0, _i18N.t)('level_up.plus_one_choice');
|
|
}
|
|
if (gameState.levelMisses === 0) {
|
|
repeats++;
|
|
choices++;
|
|
missesGain = (0, _i18N.t)('level_up.plus_one_upgrade');
|
|
} else if (gameState.levelMisses <= 3) {
|
|
choices++;
|
|
missesGain = (0, _i18N.t)('level_up.plus_one_choice');
|
|
}
|
|
while(repeats--){
|
|
const actions = pickRandomUpgrades(choices + gameState.perks.one_more_choice - gameState.perks.instant_upgrade);
|
|
if (!actions.length) break;
|
|
let textAfterButtons = `
|
|
<p>${(0, _i18N.t)('level_up.after_buttons', {
|
|
level: gameState.currentLevel + 1,
|
|
max: max_levels()
|
|
})} </p>
|
|
<p>${pickedUpgradesHTMl()}</p>
|
|
<div id="level-recording-container"></div>
|
|
|
|
`;
|
|
const compliment = timeGain && catchGain && missesGain && (0, _i18N.t)("level_up.compliment_perfect") || (timeGain || catchGain || missesGain) && (0, _i18N.t)("level_up.compliment_good") || (0, _i18N.t)('level_up.compliment_advice');
|
|
const upgradeId = await asyncAlert({
|
|
title: (0, _i18N.t)('level_up.pick_upgrade_title') + (repeats ? " (" + (repeats + 1) + ")" : ""),
|
|
actions,
|
|
text: `<p>${(0, _i18N.t)('level_up.before_buttons', {
|
|
score: gameState.score - gameState.levelStartScore,
|
|
catchGain,
|
|
levelSpawnedCoins: gameState.levelSpawnedCoins,
|
|
time: Math.round(gameState.levelTime / 1000),
|
|
timeGain,
|
|
levelMisses: gameState.levelMisses,
|
|
missesGain,
|
|
compliment
|
|
})}
|
|
</p>`,
|
|
allowClose: false,
|
|
textAfterButtons
|
|
});
|
|
gameState.perks[upgradeId]++;
|
|
if (upgradeId === "instant_upgrade") repeats += 2;
|
|
gameState.runStatistics.upgrades_picked++;
|
|
}
|
|
(0, _combo.resetCombo)(gameState, undefined, undefined);
|
|
(0, _resetBalls.resetBalls)(gameState);
|
|
}
|
|
function setLevel(l) {
|
|
stopRecording();
|
|
pause(false);
|
|
if (l > 0) openUpgradesPicker();
|
|
gameState.currentLevel = l;
|
|
gameState.levelTime = 0;
|
|
gameState.autoCleanUses = 0;
|
|
gameState.lastTickDown = gameState.levelTime;
|
|
gameState.levelStartScore = gameState.score;
|
|
gameState.levelSpawnedCoins = 0;
|
|
gameState.levelMisses = 0;
|
|
gameState.runStatistics.levelsPlayed++;
|
|
(0, _combo.resetCombo)(gameState, undefined, undefined);
|
|
recomputeTargetBaseSpeed();
|
|
(0, _resetBalls.resetBalls)(gameState);
|
|
const lvl = currentLevelInfo();
|
|
if (lvl.size !== gameState.gridSize) {
|
|
gameState.gridSize = lvl.size;
|
|
fitSize();
|
|
}
|
|
gameState.coins = [];
|
|
gameState.bricks = [
|
|
...lvl.bricks
|
|
];
|
|
gameState.flashes = [];
|
|
// This caused problems with accented characters like the ô of côte d'ivoire for odd reasons
|
|
// background.src = 'data:image/svg+xml;base64,' + btoa(lvl.svg)
|
|
background.src = "data:image/svg+xml;UTF8," + lvl.svg;
|
|
}
|
|
function currentLevelInfo() {
|
|
return gameState.runLevels[gameState.currentLevel % gameState.runLevels.length];
|
|
}
|
|
function getPossibleUpgrades(gameState) {
|
|
return (0, _loadGameData.upgrades).filter((u)=>gameState.totalScoreAtRunStart >= u.threshold).filter((u)=>!u?.requires || gameState.perks[u?.requires]);
|
|
}
|
|
function getUpgraderUnlockPoints() {
|
|
let list = [];
|
|
(0, _loadGameData.upgrades).forEach((u)=>{
|
|
if (u.threshold) list.push({
|
|
threshold: u.threshold,
|
|
title: u.name + ' ' + (0, _i18N.t)('level_up.unlocked_perk')
|
|
});
|
|
});
|
|
(0, _loadGameData.allLevels).forEach((l)=>{
|
|
list.push({
|
|
threshold: l.threshold,
|
|
title: l.name + ' ' + (0, _i18N.t)('level_up.unlocked_level')
|
|
});
|
|
});
|
|
return list.filter((o)=>o.threshold).sort((a, b)=>a.threshold - b.threshold);
|
|
}
|
|
function dontOfferTooSoon(gameState, id) {
|
|
gameState.lastOffered[id] = Math.round(Date.now() / 1000);
|
|
}
|
|
function pickRandomUpgrades(count) {
|
|
let list = getPossibleUpgrades(gameState).map((u)=>({
|
|
...u,
|
|
score: Math.random() + (gameState.lastOffered[u.id] || 0)
|
|
})).sort((a, b)=>a.score - b.score).filter((u)=>gameState.perks[u.id] < u.max).slice(0, count).sort((a, b)=>a.id > b.id ? 1 : -1);
|
|
list.forEach((u)=>{
|
|
dontOfferTooSoon(gameState, u.id);
|
|
});
|
|
return list.map((u)=>({
|
|
text: u.name + (gameState.perks[u.id] ? (0, _i18N.t)('level_up.upgrade_perk_to_level', {
|
|
level: gameState.perks[u.id] + 1
|
|
}) : ""),
|
|
icon: (0, _loadGameData.icons)["icon:" + u.id],
|
|
value: u.id,
|
|
help: u.help(gameState.perks[u.id] + 1)
|
|
}));
|
|
}
|
|
function setMousePos(x) {
|
|
gameState.needsRender = true;
|
|
gameState.puckPosition = x;
|
|
// We have borders visible, enforce them
|
|
if (gameState.puckPosition < gameState.offsetXRoundedDown + gameState.puckWidth / 2) gameState.puckPosition = gameState.offsetXRoundedDown + gameState.puckWidth / 2;
|
|
if (gameState.puckPosition > gameState.offsetXRoundedDown + gameState.gameZoneWidthRoundedUp - gameState.puckWidth / 2) gameState.puckPosition = gameState.offsetXRoundedDown + gameState.gameZoneWidthRoundedUp - gameState.puckWidth / 2;
|
|
if (!gameState.running && !gameState.levelTime) (0, _resetBalls.putBallsAtPuck)(gameState);
|
|
}
|
|
gameCanvas.addEventListener("mouseup", (e)=>{
|
|
if (e.button !== 0) return;
|
|
if (gameState.running) pause(true);
|
|
else {
|
|
play();
|
|
if ((0, _options.isOptionOn)("pointerLock")) gameCanvas.requestPointerLock();
|
|
}
|
|
});
|
|
gameCanvas.addEventListener("mousemove", (e)=>{
|
|
if (document.pointerLockElement === gameCanvas) setMousePos(gameState.puckPosition + e.movementX);
|
|
else setMousePos(e.x);
|
|
});
|
|
gameCanvas.addEventListener("touchstart", (e)=>{
|
|
e.preventDefault();
|
|
if (!e.touches?.length) return;
|
|
setMousePos(e.touches[0].pageX);
|
|
play();
|
|
});
|
|
gameCanvas.addEventListener("touchend", (e)=>{
|
|
e.preventDefault();
|
|
pause(true);
|
|
});
|
|
gameCanvas.addEventListener("touchcancel", (e)=>{
|
|
e.preventDefault();
|
|
pause(true);
|
|
gameState.needsRender = true;
|
|
});
|
|
gameCanvas.addEventListener("touchmove", (e)=>{
|
|
if (!e.touches?.length) return;
|
|
setMousePos(e.touches[0].pageX);
|
|
});
|
|
function brickIndex(x, y) {
|
|
return getRowColIndex(Math.floor(y / gameState.brickWidth), Math.floor((x - gameState.offsetX) / gameState.brickWidth));
|
|
}
|
|
function hasBrick(index) {
|
|
if (gameState.bricks[index]) return index;
|
|
}
|
|
function hitsSomething(x, y, radius) {
|
|
return hasBrick(brickIndex(x - radius, y - radius)) ?? hasBrick(brickIndex(x + radius, y - radius)) ?? hasBrick(brickIndex(x + radius, y + radius)) ?? hasBrick(brickIndex(x - radius, y + radius));
|
|
}
|
|
function shouldPierceByColor(vhit, hhit, chit) {
|
|
if (!gameState.perks.pierce_color) return false;
|
|
if (typeof vhit !== "undefined" && gameState.bricks[vhit] !== gameState.ballsColor) return false;
|
|
if (typeof hhit !== "undefined" && gameState.bricks[hhit] !== gameState.ballsColor) return false;
|
|
if (typeof chit !== "undefined" && gameState.bricks[chit] !== gameState.ballsColor) return false;
|
|
return true;
|
|
}
|
|
function coinBrickHitCheck(coin) {
|
|
// Make ball/coin bonce, and return bricks that were hit
|
|
const radius = coin.size / 2;
|
|
const { x, y, previousX, previousY } = coin;
|
|
const vhit = hitsSomething(previousX, y, radius);
|
|
const hhit = hitsSomething(x, previousY, radius);
|
|
const chit = typeof vhit == "undefined" && typeof hhit == "undefined" && hitsSomething(x, y, radius) || undefined;
|
|
if (typeof vhit !== "undefined" || typeof chit !== "undefined") {
|
|
coin.y = coin.previousY;
|
|
coin.vy *= -1;
|
|
// Roll on corners
|
|
const leftHit = gameState.bricks[brickIndex(x - radius, y + radius)];
|
|
const rightHit = gameState.bricks[brickIndex(x + radius, y + radius)];
|
|
if (leftHit && !rightHit) {
|
|
coin.vx += 1;
|
|
coin.sa -= 1;
|
|
}
|
|
if (!leftHit && rightHit) {
|
|
coin.vx -= 1;
|
|
coin.sa += 1;
|
|
}
|
|
}
|
|
if (typeof hhit !== "undefined" || typeof chit !== "undefined") {
|
|
coin.x = coin.previousX;
|
|
coin.vx *= -1;
|
|
}
|
|
return vhit ?? hhit ?? chit;
|
|
}
|
|
function bordersHitCheck(coin, radius, delta) {
|
|
if (coin.destroyed) return;
|
|
coin.previousX = coin.x;
|
|
coin.previousY = coin.y;
|
|
coin.x += coin.vx * delta;
|
|
coin.y += coin.vy * delta;
|
|
coin.sx ||= 0;
|
|
coin.sy ||= 0;
|
|
coin.sx += coin.previousX - coin.x;
|
|
coin.sy += coin.previousY - coin.y;
|
|
coin.sx *= 0.9;
|
|
coin.sy *= 0.9;
|
|
if (gameState.perks.wind) coin.vx += (gameState.puckPosition - (gameState.offsetX + gameState.gameZoneWidth / 2)) / gameState.gameZoneWidth * gameState.perks.wind * 0.5;
|
|
let vhit = 0, hhit = 0;
|
|
if (coin.x < gameState.offsetXRoundedDown + radius) {
|
|
coin.x = gameState.offsetXRoundedDown + radius + (gameState.offsetXRoundedDown + radius - coin.x);
|
|
coin.vx *= -1;
|
|
hhit = 1;
|
|
}
|
|
if (coin.y < radius) {
|
|
coin.y = radius + (radius - coin.y);
|
|
coin.vy *= -1;
|
|
vhit = 1;
|
|
}
|
|
if (coin.x > gameState.canvasWidth - gameState.offsetXRoundedDown - radius) {
|
|
coin.x = gameState.canvasWidth - gameState.offsetXRoundedDown - radius - (coin.x - (gameState.canvasWidth - gameState.offsetXRoundedDown - radius));
|
|
coin.vx *= -1;
|
|
hhit = 1;
|
|
}
|
|
return hhit + vhit * 2;
|
|
}
|
|
function tick() {
|
|
recomputeTargetBaseSpeed();
|
|
const currentTick = performance.now();
|
|
gameState.puckWidth = gameState.gameZoneWidth / 12 * (3 - gameState.perks.smaller_puck + gameState.perks.bigger_puck);
|
|
if (gameState.keyboardPuckSpeed) setMousePos(gameState.puckPosition + gameState.keyboardPuckSpeed);
|
|
if (gameState.running) {
|
|
gameState.levelTime += currentTick - gameState.lastTick;
|
|
gameState.runStatistics.runTime += currentTick - gameState.lastTick;
|
|
gameState.runStatistics.max_combo = Math.max(gameState.runStatistics.max_combo, gameState.combo);
|
|
// How many times to compute
|
|
let delta = Math.min(4, (currentTick - gameState.lastTick) / (1000 / 60));
|
|
delta *= gameState.running ? 1 : 0;
|
|
gameState.coins = gameState.coins.filter((coin)=>!coin.destroyed);
|
|
gameState.balls = gameState.balls.filter((ball)=>!ball.destroyed);
|
|
const remainingBricks = gameState.bricks.filter((b)=>b && b !== "black").length;
|
|
if (gameState.levelTime > gameState.lastTickDown + 1000 && gameState.perks.hot_start) {
|
|
gameState.lastTickDown = gameState.levelTime;
|
|
(0, _combo.decreaseCombo)(gameState, gameState.perks.hot_start, gameState.puckPosition, gameState.gameZoneHeight - 2 * gameState.puckHeight);
|
|
}
|
|
if (remainingBricks <= gameState.perks.skip_last && !gameState.autoCleanUses) {
|
|
gameState.bricks.forEach((type, index)=>{
|
|
if (type) explodeBrick(index, gameState.balls[0], true);
|
|
});
|
|
gameState.autoCleanUses++;
|
|
}
|
|
if (!remainingBricks && !gameState.coins.length) {
|
|
if (gameState.currentLevel + 1 < max_levels()) setLevel(gameState.currentLevel + 1);
|
|
else gameOver((0, _i18N.t)('gameOver.win.title'), (0, _i18N.t)('gameOver.win.summary', {
|
|
score: gameState.score
|
|
}));
|
|
} else if (gameState.running || gameState.levelTime) {
|
|
let playedCoinBounce = false;
|
|
const coinRadius = Math.round(gameState.coinSize / 2);
|
|
gameState.coins.forEach((coin)=>{
|
|
if (coin.destroyed) return;
|
|
if (gameState.perks.coin_magnet) {
|
|
const attractionX = delta * (gameState.puckPosition - coin.x) / (100 + Math.pow(coin.y - gameState.gameZoneHeight, 2) + Math.pow(coin.x - gameState.puckPosition, 2)) * gameState.perks.coin_magnet * 100;
|
|
coin.vx += attractionX;
|
|
coin.sa -= attractionX / 10;
|
|
}
|
|
const ratio = 1 - (gameState.perks.viscosity * 0.03 + 0.005) * delta;
|
|
coin.vy *= ratio;
|
|
coin.vx *= ratio;
|
|
if (coin.vx > 7 * gameState.baseSpeed) coin.vx = 7 * gameState.baseSpeed;
|
|
if (coin.vx < -7 * gameState.baseSpeed) coin.vx = -7 * gameState.baseSpeed;
|
|
if (coin.vy > 7 * gameState.baseSpeed) coin.vy = 7 * gameState.baseSpeed;
|
|
if (coin.vy < -7 * gameState.baseSpeed) coin.vy = -7 * gameState.baseSpeed;
|
|
coin.a += coin.sa;
|
|
// Gravity
|
|
coin.vy += delta * coin.weight * 0.8;
|
|
const speed = Math.abs(coin.sx) + Math.abs(coin.sx);
|
|
const hitBorder = bordersHitCheck(coin, coin.size / 2, delta);
|
|
if (coin.y > gameState.gameZoneHeight - coinRadius - gameState.puckHeight && coin.y < gameState.gameZoneHeight + gameState.puckHeight + coin.vy && Math.abs(coin.x - gameState.puckPosition) < coinRadius + gameState.puckWidth / 2 + // a bit of margin to be nice
|
|
gameState.puckHeight) addToScore(coin);
|
|
else if (coin.y > gameState.canvasHeight + coinRadius) {
|
|
coin.destroyed = true;
|
|
if (gameState.perks.compound_interest) (0, _combo.resetCombo)(gameState, coin.x, coin.y);
|
|
}
|
|
const hitBrick = coinBrickHitCheck(coin);
|
|
if (gameState.perks.metamorphosis && typeof hitBrick !== "undefined") {
|
|
if (gameState.bricks[hitBrick] && coin.color !== gameState.bricks[hitBrick] && gameState.bricks[hitBrick] !== "black" && !coin.coloredABrick) {
|
|
gameState.bricks[hitBrick] = coin.color;
|
|
coin.coloredABrick = true;
|
|
(0, _sounds.sounds).colorChange(coin.x, 0.3);
|
|
}
|
|
}
|
|
if (typeof hitBrick !== "undefined" || hitBorder) {
|
|
coin.vx *= 0.8;
|
|
coin.vy *= 0.8;
|
|
coin.sa *= 0.9;
|
|
if (speed > 20 && !playedCoinBounce) {
|
|
playedCoinBounce = true;
|
|
(0, _sounds.sounds).coinBounce(coin.x, 0.2);
|
|
}
|
|
if (Math.abs(coin.vy) < 3) coin.vy = 0;
|
|
}
|
|
});
|
|
gameState.balls.forEach((ball)=>ballTick(ball, delta));
|
|
if (gameState.perks.wind) {
|
|
const windD = (gameState.puckPosition - (gameState.offsetX + gameState.gameZoneWidth / 2)) / gameState.gameZoneWidth * 2 * gameState.perks.wind;
|
|
for(let i = 0; i < gameState.perks.wind; i++)if (Math.random() * Math.abs(windD) > 0.5) gameState.flashes.push({
|
|
type: "particle",
|
|
duration: 150,
|
|
ethereal: true,
|
|
time: gameState.levelTime,
|
|
size: gameState.coinSize / 2,
|
|
color: rainbowColor(),
|
|
x: gameState.offsetXRoundedDown + Math.random() * gameState.gameZoneWidthRoundedUp,
|
|
y: Math.random() * gameState.gameZoneHeight,
|
|
vx: windD * 8,
|
|
vy: 0
|
|
});
|
|
}
|
|
gameState.flashes.forEach((flash)=>{
|
|
if (flash.type === "particle") {
|
|
flash.x += flash.vx * delta;
|
|
flash.y += flash.vy * delta;
|
|
if (!flash.ethereal) {
|
|
flash.vy += 0.5;
|
|
if (hasBrick(brickIndex(flash.x, flash.y))) flash.destroyed = true;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
if (gameState.combo > (0, _combo.baseCombo)(gameState)) {
|
|
// The red should still be visible on a white bg
|
|
const baseParticle = !(0, _options.isOptionOn)("basic") && (gameState.combo - (0, _combo.baseCombo)(gameState)) * Math.random() > 5 && gameState.running && {
|
|
type: "particle",
|
|
duration: 100 * (Math.random() + 1),
|
|
time: gameState.levelTime,
|
|
size: gameState.coinSize / 2,
|
|
color: "red",
|
|
ethereal: true
|
|
};
|
|
if (gameState.perks.top_is_lava) baseParticle && gameState.flashes.push({
|
|
...baseParticle,
|
|
x: gameState.offsetXRoundedDown + Math.random() * gameState.gameZoneWidthRoundedUp,
|
|
y: 0,
|
|
vx: (Math.random() - 0.5) * 10,
|
|
vy: 5
|
|
});
|
|
if (gameState.perks.left_is_lava && baseParticle) gameState.flashes.push({
|
|
...baseParticle,
|
|
x: gameState.offsetXRoundedDown,
|
|
y: Math.random() * gameState.gameZoneHeight,
|
|
vx: 5,
|
|
vy: (Math.random() - 0.5) * 10
|
|
});
|
|
if (gameState.perks.right_is_lava && baseParticle) gameState.flashes.push({
|
|
...baseParticle,
|
|
x: gameState.offsetXRoundedDown + gameState.gameZoneWidthRoundedUp,
|
|
y: Math.random() * gameState.gameZoneHeight,
|
|
vx: -5,
|
|
vy: (Math.random() - 0.5) * 10
|
|
});
|
|
if (gameState.perks.compound_interest) {
|
|
let x = gameState.puckPosition, attemps = 0;
|
|
do {
|
|
x = gameState.offsetXRoundedDown + gameState.gameZoneWidthRoundedUp * Math.random();
|
|
attemps++;
|
|
}while (Math.abs(x - gameState.puckPosition) < gameState.puckWidth / 2 && attemps < 10);
|
|
baseParticle && gameState.flashes.push({
|
|
...baseParticle,
|
|
x,
|
|
y: gameState.gameZoneHeight,
|
|
vx: (Math.random() - 0.5) * 10,
|
|
vy: -5
|
|
});
|
|
}
|
|
if (gameState.perks.streak_shots) {
|
|
const pos = 0.5 - Math.random();
|
|
baseParticle && gameState.flashes.push({
|
|
...baseParticle,
|
|
duration: 100,
|
|
x: gameState.puckPosition + gameState.puckWidth * pos,
|
|
y: gameState.gameZoneHeight - gameState.puckHeight,
|
|
vx: pos * 10,
|
|
vy: -5
|
|
});
|
|
}
|
|
}
|
|
}
|
|
render();
|
|
requestAnimationFrame(tick);
|
|
gameState.lastTick = currentTick;
|
|
}
|
|
function isTelekinesisActive(ball) {
|
|
return gameState.perks.telekinesis && !ball.hitSinceBounce && ball.vy < 0;
|
|
}
|
|
function ballTick(ball, delta) {
|
|
ball.previousVX = ball.vx;
|
|
ball.previousVY = ball.vy;
|
|
let speedLimitDampener = 1 + gameState.perks.telekinesis + gameState.perks.ball_repulse_ball + gameState.perks.puck_repulse_ball + gameState.perks.ball_attract_ball;
|
|
if (isTelekinesisActive(ball)) {
|
|
speedLimitDampener += 3;
|
|
ball.vx += (gameState.puckPosition - ball.x) / 1000 * delta * gameState.perks.telekinesis;
|
|
}
|
|
if (ball.vx * ball.vx + ball.vy * ball.vy < gameState.baseSpeed * gameState.baseSpeed * 2) {
|
|
ball.vx *= 1 + 0.02 / speedLimitDampener;
|
|
ball.vy *= 1 + 0.02 / speedLimitDampener;
|
|
} else {
|
|
ball.vx *= 1 - 0.02 / speedLimitDampener;
|
|
ball.vy *= 1 - 0.02 / speedLimitDampener;
|
|
}
|
|
// Ball could get stuck horizontally because of ball-ball interactions in repulse/attract
|
|
if (Math.abs(ball.vy) < 0.2 * gameState.baseSpeed) ball.vy += (ball.vy > 0 ? 1 : -1) * 0.02 / speedLimitDampener;
|
|
if (gameState.perks.ball_repulse_ball) for (let b2 of gameState.balls){
|
|
// avoid computing this twice, and repulsing itself
|
|
if (b2.x >= ball.x) continue;
|
|
repulse(ball, b2, gameState.perks.ball_repulse_ball, true);
|
|
}
|
|
if (gameState.perks.ball_attract_ball) for (let b2 of gameState.balls){
|
|
// avoid computing this twice, and repulsing itself
|
|
if (b2.x >= ball.x) continue;
|
|
attract(ball, b2, gameState.perks.ball_attract_ball);
|
|
}
|
|
if (gameState.perks.puck_repulse_ball && Math.abs(ball.x - gameState.puckPosition) < gameState.puckWidth / 2 + gameState.ballSize * (9 + gameState.perks.puck_repulse_ball) / 10) repulse(ball, {
|
|
x: gameState.puckPosition,
|
|
y: gameState.gameZoneHeight
|
|
}, gameState.perks.puck_repulse_ball + 1, false);
|
|
if (gameState.perks.respawn && ball.hitItem?.length > 1 && !(0, _options.isOptionOn)("basic")) for(let i = 0; i < ball.hitItem?.length - 1 && i < gameState.perks.respawn; i++){
|
|
const { index, color } = ball.hitItem[i];
|
|
if (gameState.bricks[index] || color === "black") continue;
|
|
const vertical = Math.random() > 0.5;
|
|
const dx = Math.random() > 0.5 ? 1 : -1;
|
|
const dy = Math.random() > 0.5 ? 1 : -1;
|
|
gameState.flashes.push({
|
|
type: "particle",
|
|
duration: 250,
|
|
ethereal: true,
|
|
time: gameState.levelTime,
|
|
size: gameState.coinSize / 2,
|
|
color,
|
|
x: brickCenterX(index) + dx * gameState.brickWidth / 2,
|
|
y: brickCenterY(index) + dy * gameState.brickWidth / 2,
|
|
vx: vertical ? 0 : -dx * gameState.baseSpeed,
|
|
vy: vertical ? -dy * gameState.baseSpeed : 0
|
|
});
|
|
}
|
|
const borderHitCode = bordersHitCheck(ball, gameState.ballSize / 2, delta);
|
|
if (borderHitCode) {
|
|
if (gameState.perks.left_is_lava && borderHitCode % 2 && ball.x < gameState.offsetX + gameState.gameZoneWidth / 2) (0, _combo.resetCombo)(gameState, ball.x, ball.y);
|
|
if (gameState.perks.right_is_lava && borderHitCode % 2 && ball.x > gameState.offsetX + gameState.gameZoneWidth / 2) (0, _combo.resetCombo)(gameState, ball.x, ball.y);
|
|
if (gameState.perks.top_is_lava && borderHitCode >= 2) (0, _combo.resetCombo)(gameState, ball.x, ball.y + gameState.ballSize);
|
|
(0, _sounds.sounds).wallBeep(ball.x);
|
|
ball.bouncesList?.push({
|
|
x: ball.previousX,
|
|
y: ball.previousY
|
|
});
|
|
}
|
|
// Puck collision
|
|
const ylimit = gameState.gameZoneHeight - gameState.puckHeight - gameState.ballSize / 2;
|
|
const ballIsUnderPuck = Math.abs(ball.x - gameState.puckPosition) < gameState.ballSize / 2 + gameState.puckWidth / 2;
|
|
if (ball.y > ylimit && ball.vy > 0 && (ballIsUnderPuck || gameState.perks.extra_life && ball.y > ylimit + gameState.puckHeight / 2)) {
|
|
if (ballIsUnderPuck) {
|
|
const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
|
|
const angle = Math.atan2(-gameState.puckWidth / 2, ball.x - gameState.puckPosition);
|
|
ball.vx = speed * Math.cos(angle);
|
|
ball.vy = speed * Math.sin(angle);
|
|
(0, _sounds.sounds).wallBeep(ball.x);
|
|
} else {
|
|
ball.vy *= -1;
|
|
gameState.perks.extra_life = Math.max(0, gameState.perks.extra_life - 1);
|
|
(0, _sounds.sounds).lifeLost(ball.x);
|
|
if (!(0, _options.isOptionOn)("basic")) for(let i = 0; i < 10; i++)gameState.flashes.push({
|
|
type: "particle",
|
|
ethereal: false,
|
|
color: "red",
|
|
destroyed: false,
|
|
duration: 150,
|
|
size: gameState.coinSize / 2,
|
|
time: gameState.levelTime,
|
|
x: ball.x,
|
|
y: ball.y,
|
|
vx: Math.random() * gameState.baseSpeed * 3,
|
|
vy: gameState.baseSpeed * 3
|
|
});
|
|
}
|
|
if (gameState.perks.streak_shots) (0, _combo.resetCombo)(gameState, ball.x, ball.y);
|
|
if (gameState.perks.respawn) ball.hitItem.slice(0, -1).slice(0, gameState.perks.respawn).forEach(({ index, color })=>{
|
|
if (!gameState.bricks[index] && color !== "black") gameState.bricks[index] = color;
|
|
});
|
|
ball.hitItem = [];
|
|
if (!ball.hitSinceBounce) {
|
|
gameState.runStatistics.misses++;
|
|
gameState.levelMisses++;
|
|
(0, _combo.resetCombo)(gameState, ball.x, ball.y);
|
|
gameState.flashes.push({
|
|
type: "text",
|
|
text: (0, _i18N.t)('play.missed_ball'),
|
|
duration: 500,
|
|
time: gameState.levelTime,
|
|
size: gameState.puckHeight * 1.5,
|
|
color: "red",
|
|
x: gameState.puckPosition,
|
|
y: gameState.gameZoneHeight - gameState.puckHeight * 2
|
|
});
|
|
}
|
|
gameState.runStatistics.puck_bounces++;
|
|
ball.hitSinceBounce = 0;
|
|
ball.sapperUses = 0;
|
|
ball.piercedSinceBounce = 0;
|
|
ball.bouncesList = [
|
|
{
|
|
x: ball.previousX,
|
|
y: ball.previousY
|
|
}
|
|
];
|
|
}
|
|
if (ball.y > gameState.gameZoneHeight + gameState.ballSize / 2 && gameState.running) {
|
|
ball.destroyed = true;
|
|
gameState.runStatistics.balls_lost++;
|
|
if (!gameState.balls.find((b)=>!b.destroyed)) gameOver((0, _i18N.t)('gameOver.lost.title'), (0, _i18N.t)('gameOver.lost.summary', {
|
|
score: gameState.score
|
|
}));
|
|
}
|
|
const radius = gameState.ballSize / 2;
|
|
// Make ball/coin bonce, and return bricks that were hit
|
|
const { x, y, previousX, previousY } = ball;
|
|
const vhit = hitsSomething(previousX, y, radius);
|
|
const hhit = hitsSomething(x, previousY, radius);
|
|
const chit = typeof vhit == "undefined" && typeof hhit == "undefined" && hitsSomething(x, y, radius) || undefined;
|
|
const hitBrick = vhit ?? hhit ?? chit;
|
|
let sturdyBounce = hitBrick && gameState.bricks[hitBrick] !== "black" && gameState.perks.sturdy_bricks && gameState.perks.sturdy_bricks > Math.random() * 5;
|
|
let pierce = false;
|
|
if (sturdyBounce || typeof hitBrick === "undefined") ;
|
|
else if (shouldPierceByColor(vhit, hhit, chit)) pierce = true;
|
|
else if (ball.piercedSinceBounce < gameState.perks.pierce * 3) {
|
|
pierce = true;
|
|
ball.piercedSinceBounce++;
|
|
}
|
|
if (typeof vhit !== "undefined" || typeof chit !== "undefined") {
|
|
if (!pierce) {
|
|
ball.y = ball.previousY;
|
|
ball.vy *= -1;
|
|
}
|
|
}
|
|
if (typeof hhit !== "undefined" || typeof chit !== "undefined") {
|
|
if (!pierce) {
|
|
ball.x = ball.previousX;
|
|
ball.vx *= -1;
|
|
}
|
|
}
|
|
if (sturdyBounce) {
|
|
(0, _sounds.sounds).wallBeep(x);
|
|
return;
|
|
}
|
|
if (typeof hitBrick !== "undefined") {
|
|
const initialBrickColor = gameState.bricks[hitBrick];
|
|
explodeBrick(hitBrick, ball, false);
|
|
if (ball.sapperUses < gameState.perks.sapper && initialBrickColor !== "black" && // don't replace a brick that bounced with sturdy_bricks
|
|
!gameState.bricks[hitBrick]) {
|
|
gameState.bricks[hitBrick] = "black";
|
|
ball.sapperUses++;
|
|
}
|
|
}
|
|
if (!(0, _options.isOptionOn)("basic")) {
|
|
ball.sparks += delta * (gameState.combo - 1) / 30;
|
|
if (ball.sparks > 1) {
|
|
gameState.flashes.push({
|
|
type: "particle",
|
|
duration: 100 * ball.sparks,
|
|
time: gameState.levelTime,
|
|
size: gameState.coinSize / 2,
|
|
color: gameState.ballsColor,
|
|
x: ball.x,
|
|
y: ball.y,
|
|
vx: (Math.random() - 0.5) * gameState.baseSpeed,
|
|
vy: (Math.random() - 0.5) * gameState.baseSpeed,
|
|
ethereal: false
|
|
});
|
|
ball.sparks = 0;
|
|
}
|
|
}
|
|
}
|
|
function getTotalScore() {
|
|
try {
|
|
return JSON.parse(localStorage.getItem("breakout_71_total_score") || "0");
|
|
} catch (e) {
|
|
return 0;
|
|
}
|
|
}
|
|
function addToTotalScore(points) {
|
|
if (gameState.isCreativeModeRun) return;
|
|
try {
|
|
localStorage.setItem("breakout_71_total_score", JSON.stringify(getTotalScore() + points));
|
|
} catch (e) {}
|
|
}
|
|
function addToTotalPlayTime(ms) {
|
|
try {
|
|
localStorage.setItem("breakout_71_total_play_time", JSON.stringify(JSON.parse(localStorage.getItem("breakout_71_total_play_time") || "0") + ms));
|
|
} catch (e) {}
|
|
}
|
|
function gameOver(title, intro) {
|
|
if (!gameState.running) return;
|
|
pause(true);
|
|
stopRecording();
|
|
addToTotalPlayTime(gameState.runStatistics.runTime);
|
|
gameState.runStatistics.max_level = gameState.currentLevel + 1;
|
|
let animationDelay = -300;
|
|
const getDelay = ()=>{
|
|
animationDelay += 800;
|
|
return "animation-delay:" + animationDelay + "ms;";
|
|
};
|
|
// unlocks
|
|
let unlocksInfo = "";
|
|
const endTs = getTotalScore();
|
|
const startTs = endTs - gameState.score;
|
|
const list = getUpgraderUnlockPoints();
|
|
list.filter((u)=>u.threshold > startTs && u.threshold < endTs).forEach((u)=>{
|
|
unlocksInfo += `
|
|
<p class="progress" >
|
|
<span>${u.title}</span>
|
|
<span class="progress_bar_part" style="${getDelay()}"></span>
|
|
</p>
|
|
`;
|
|
});
|
|
const previousUnlockAt = findLast(list, (u)=>u.threshold <= endTs)?.threshold || 0;
|
|
const nextUnlock = list.find((u)=>u.threshold > endTs);
|
|
if (nextUnlock) {
|
|
const total = nextUnlock?.threshold - previousUnlockAt;
|
|
const done = endTs - previousUnlockAt;
|
|
intro += (0, _i18N.t)('gameOver.next_unlock', {
|
|
points: nextUnlock.threshold - endTs
|
|
});
|
|
const scaleX = (done / total).toFixed(2);
|
|
unlocksInfo += `
|
|
<p class="progress" >
|
|
<span>${nextUnlock.title}</span>
|
|
<span style="transform: scale(${scaleX},1);${getDelay()}" class="progress_bar_part"></span>
|
|
</p>
|
|
|
|
`;
|
|
list.slice(list.indexOf(nextUnlock) + 1).slice(0, 3).forEach((u)=>{
|
|
unlocksInfo += `
|
|
<p class="progress" >
|
|
<span>${u.title}</span>
|
|
</p>
|
|
`;
|
|
});
|
|
}
|
|
let unlockedItems = list.filter((u)=>u.threshold > startTs && u.threshold < endTs);
|
|
if (unlockedItems.length) unlocksInfo += `<p>${(0, _i18N.t)('gameOver.unlocked_count', {
|
|
count: unlockedItems.length
|
|
})} ${unlockedItems.map((u)=>u.title).join(", ")}</p>`;
|
|
// Avoid the sad sound right as we restart a new games
|
|
gameState.combo = 1;
|
|
asyncAlert({
|
|
allowClose: true,
|
|
title,
|
|
text: `
|
|
${gameState.isCreativeModeRun ? `<p>${(0, _i18N.t)('gameOver.test_run')}</p> ` : ""}
|
|
<p>${intro}</p>
|
|
<p>${(0, _i18N.t)('gameOver.cumulative_total', {
|
|
startTs,
|
|
endTs
|
|
})}</p>
|
|
${unlocksInfo}
|
|
`,
|
|
actions: [
|
|
{
|
|
value: null,
|
|
text: (0, _i18N.t)('gameOver.restart'),
|
|
help: ""
|
|
}
|
|
],
|
|
textAfterButtons: `<div id="level-recording-container"></div>
|
|
${getHistograms()}
|
|
`
|
|
}).then(()=>restart({
|
|
levelToAvoid: currentLevelInfo().name
|
|
}));
|
|
}
|
|
function getHistograms() {
|
|
let runStats = "";
|
|
try {
|
|
// Stores only top 100 runs
|
|
let runsHistory = JSON.parse(localStorage.getItem("breakout_71_runs_history") || "[]");
|
|
runsHistory.sort((a, b)=>a.score - b.score).reverse();
|
|
runsHistory = runsHistory.slice(0, 100);
|
|
runsHistory.push({
|
|
...gameState.runStatistics,
|
|
perks: gameState.perks,
|
|
appVersion: (0, _loadGameData.appVersion)
|
|
});
|
|
// Generate some histogram
|
|
if (!gameState.isCreativeModeRun) localStorage.setItem("breakout_71_runs_history", JSON.stringify(runsHistory, null, 2));
|
|
const makeHistogram = (title, getter, unit)=>{
|
|
let values = runsHistory.map((h)=>getter(h) || 0);
|
|
let min = Math.min(...values);
|
|
let max = Math.max(...values);
|
|
// No point
|
|
if (min === max) return "";
|
|
if (max - min < 10) {
|
|
// This is mostly useful for levels
|
|
min = Math.max(0, max - 10);
|
|
max = Math.max(max, min + 10);
|
|
}
|
|
// One bin per unique value, max 10
|
|
const binsCount = Math.min(values.length, 10);
|
|
if (binsCount < 3) return "";
|
|
const bins = [];
|
|
const binsTotal = [];
|
|
for(let i = 0; i < binsCount; i++){
|
|
bins.push(0);
|
|
binsTotal.push(0);
|
|
}
|
|
const binSize = (max - min) / bins.length;
|
|
const binIndexOf = (v)=>Math.min(bins.length - 1, Math.floor((v - min) / binSize));
|
|
values.forEach((v)=>{
|
|
if (isNaN(v)) return;
|
|
const index = binIndexOf(v);
|
|
bins[index]++;
|
|
binsTotal[index] += v;
|
|
});
|
|
if (bins.filter((b)=>b).length < 3) return "";
|
|
const maxBin = Math.max(...bins);
|
|
const lastValue = values[values.length - 1];
|
|
const activeBin = binIndexOf(lastValue);
|
|
const bars = bins.map((v, vi)=>{
|
|
const style = `height: ${v / maxBin * 80}px`;
|
|
return `<span class="${vi === activeBin ? "active" : ""}"><span style="${style}" title="${v} run${v > 1 ? "s" : ""} between ${Math.floor(min + vi * binSize)} and ${Math.floor(min + (vi + 1) * binSize)}${unit}"
|
|
><span>${!v && " " || vi == activeBin && lastValue + unit || Math.round(binsTotal[vi] / v) + unit}</span></span></span>`;
|
|
}).join("");
|
|
return `<h2 class="histogram-title">${title} : <strong>${lastValue}${unit}</strong></h2>
|
|
<div class="histogram">${bars}</div>
|
|
`;
|
|
};
|
|
runStats += makeHistogram((0, _i18N.t)('gameOver.stats.total_score'), (r)=>r.score, "");
|
|
runStats += makeHistogram((0, _i18N.t)('gameOver.stats.catch_rate'), (r)=>Math.round(r.score / r.coins_spawned * 100), "%");
|
|
runStats += makeHistogram((0, _i18N.t)('gameOver.stats.bricks_broken'), (r)=>r.bricks_broken, "");
|
|
runStats += makeHistogram((0, _i18N.t)('gameOver.stats.bricks_per_minute'), (r)=>Math.round(r.bricks_broken / r.runTime * 60000), "");
|
|
runStats += makeHistogram((0, _i18N.t)('gameOver.stats.hit_rate'), (r)=>Math.round((1 - r.misses / r.puck_bounces) * 100), "%");
|
|
runStats += makeHistogram((0, _i18N.t)('gameOver.stats.duration_per_level'), (r)=>Math.round(r.runTime / 1000 / r.levelsPlayed), "s");
|
|
runStats += makeHistogram((0, _i18N.t)('gameOver.stats.level_reached'), (r)=>r.levelsPlayed, "");
|
|
runStats += makeHistogram((0, _i18N.t)('gameOver.stats.upgrades_applied'), (r)=>r.upgrades_picked, "");
|
|
runStats += makeHistogram((0, _i18N.t)('gameOver.stats.balls_lost'), (r)=>r.balls_lost, "");
|
|
runStats += makeHistogram((0, _i18N.t)('gameOver.stats.combo_avg'), (r)=>Math.round(r.coins_spawned / r.bricks_broken), "");
|
|
runStats += makeHistogram((0, _i18N.t)('gameOver.stats.combo_max'), (r)=>r.max_combo, "");
|
|
if (runStats) runStats = `<p>${(0, _i18N.t)('gameOver.stats.intro', {
|
|
count: runsHistory.length - 1
|
|
})}</p>` + runStats;
|
|
} catch (e) {
|
|
console.warn(e);
|
|
}
|
|
return runStats;
|
|
}
|
|
function explodeBrick(index, ball, isExplosion) {
|
|
const color = gameState.bricks[index];
|
|
if (!color) return;
|
|
if (color === "black") {
|
|
delete gameState.bricks[index];
|
|
const x = brickCenterX(index), y = brickCenterY(index);
|
|
(0, _sounds.sounds).explode(ball.x);
|
|
const col = index % gameState.gridSize;
|
|
const row = Math.floor(index / gameState.gridSize);
|
|
const size = 1 + gameState.perks.bigger_explosions;
|
|
// Break bricks around
|
|
for(let dx = -size; dx <= size; dx++)for(let dy = -size; dy <= size; dy++){
|
|
const i = getRowColIndex(row + dy, col + dx);
|
|
if (gameState.bricks[i] && i !== -1) {
|
|
// Study bricks resist explosions too
|
|
if (gameState.bricks[i] !== "black" && gameState.perks.sturdy_bricks > Math.random() * 5) continue;
|
|
explodeBrick(i, ball, true);
|
|
}
|
|
}
|
|
// Blow nearby coins
|
|
gameState.coins.forEach((c)=>{
|
|
const dx = c.x - x;
|
|
const dy = c.y - y;
|
|
const d2 = Math.max(gameState.brickWidth, Math.abs(dx) + Math.abs(dy));
|
|
c.vx += dx / d2 * 10 * size / c.weight;
|
|
c.vy += dy / d2 * 10 * size / c.weight;
|
|
});
|
|
gameState.lastExplosion = Date.now();
|
|
gameState.flashes.push({
|
|
type: "ball",
|
|
duration: 150,
|
|
time: gameState.levelTime,
|
|
size: gameState.brickWidth * 2,
|
|
color: "white",
|
|
x,
|
|
y
|
|
});
|
|
spawnExplosion(7 * (1 + gameState.perks.bigger_explosions), x, y, "white", 150, gameState.coinSize);
|
|
ball.hitSinceBounce++;
|
|
gameState.runStatistics.bricks_broken++;
|
|
} else if (color) {
|
|
// Even if it bounces we don't want to count that as a miss
|
|
ball.hitSinceBounce++;
|
|
// Flashing is take care of by the tick loop
|
|
const x = brickCenterX(index), y = brickCenterY(index);
|
|
gameState.bricks[index] = "";
|
|
// coins = coins.filter((c) => !c.destroyed);
|
|
let coinsToSpawn = gameState.combo;
|
|
if (gameState.perks.sturdy_bricks) // +10% per level
|
|
coinsToSpawn += Math.ceil((10 + gameState.perks.sturdy_bricks) / 10 * coinsToSpawn);
|
|
gameState.levelSpawnedCoins += coinsToSpawn;
|
|
gameState.runStatistics.coins_spawned += coinsToSpawn;
|
|
gameState.runStatistics.bricks_broken++;
|
|
const maxCoins = gameState.MAX_COINS * ((0, _options.isOptionOn)("basic") ? 0.5 : 1);
|
|
const spawnableCoins = gameState.coins.length > gameState.MAX_COINS ? 1 : Math.floor(maxCoins - gameState.coins.length) / 3;
|
|
const pointsPerCoin = Math.max(1, Math.ceil(coinsToSpawn / spawnableCoins));
|
|
while(coinsToSpawn > 0){
|
|
const points = Math.min(pointsPerCoin, coinsToSpawn);
|
|
if (points < 0 || isNaN(points)) {
|
|
console.error({
|
|
points
|
|
});
|
|
debugger;
|
|
}
|
|
coinsToSpawn -= points;
|
|
const cx = x + (Math.random() - 0.5) * (gameState.brickWidth - gameState.coinSize), cy = y + (Math.random() - 0.5) * (gameState.brickWidth - gameState.coinSize);
|
|
gameState.coins.push({
|
|
points,
|
|
size: gameState.coinSize,
|
|
color: gameState.perks.metamorphosis ? color : "gold",
|
|
x: cx,
|
|
y: cy,
|
|
previousX: cx,
|
|
previousY: cy,
|
|
// Use previous speed because the ball has already bounced
|
|
vx: ball.previousVX * (0.5 + Math.random()),
|
|
vy: ball.previousVY * (0.5 + Math.random()),
|
|
sx: 0,
|
|
sy: 0,
|
|
a: Math.random() * Math.PI * 2,
|
|
sa: Math.random() - 0.5,
|
|
weight: 0.8 + Math.random() * 0.2
|
|
});
|
|
}
|
|
gameState.combo += Math.max(0, gameState.perks.streak_shots + gameState.perks.compound_interest + gameState.perks.left_is_lava + gameState.perks.right_is_lava + gameState.perks.top_is_lava + gameState.perks.picky_eater - Math.round(Math.random() * gameState.perks.soft_reset));
|
|
if (!isExplosion) {
|
|
// color change
|
|
if ((gameState.perks.picky_eater || gameState.perks.pierce_color) && color !== gameState.ballsColor && color) {
|
|
if (gameState.perks.picky_eater) (0, _combo.resetCombo)(gameState, ball.x, ball.y);
|
|
(0, _sounds.sounds).colorChange(ball.x, 0.8);
|
|
gameState.lastExplosion = gameState.levelTime;
|
|
gameState.ballsColor = color;
|
|
if (!(0, _options.isOptionOn)("basic")) gameState.balls.forEach((ball)=>{
|
|
spawnExplosion(7, ball.previousX, ball.previousY, color, 150, 15);
|
|
});
|
|
} else (0, _sounds.sounds).comboIncreaseMaybe(gameState.combo, ball.x, 1);
|
|
}
|
|
gameState.flashes.push({
|
|
type: "ball",
|
|
duration: 40,
|
|
time: gameState.levelTime,
|
|
size: gameState.brickWidth,
|
|
color: color,
|
|
x,
|
|
y
|
|
});
|
|
spawnExplosion(5 + Math.min(gameState.combo, 30), x, y, color, 150, gameState.coinSize / 2);
|
|
}
|
|
if (!gameState.bricks[index] && color !== "black") ball.hitItem?.push({
|
|
index,
|
|
color
|
|
});
|
|
}
|
|
function max_levels() {
|
|
return 7 + gameState.perks.extra_levels;
|
|
}
|
|
function render() {
|
|
if (gameState.running) gameState.needsRender = true;
|
|
if (!gameState.needsRender) return;
|
|
gameState.needsRender = false;
|
|
const level = currentLevelInfo();
|
|
const { width, height } = gameCanvas;
|
|
if (!width || !height) return;
|
|
if (gameState.currentLevel || gameState.levelTime) menuLabel.innerText = (0, _i18N.t)('play.current_lvl', {
|
|
level: gameState.currentLevel + 1,
|
|
max: max_levels()
|
|
});
|
|
else menuLabel.innerText = (0, _i18N.t)('play.menu_label');
|
|
scoreDisplay.innerText = `$${gameState.score}`;
|
|
scoreDisplay.className = gameState.lastScoreIncrease > gameState.levelTime - 500 ? "active" : "";
|
|
// Clear
|
|
if (!(0, _options.isOptionOn)("basic") && !level.color && level.svg) {
|
|
// Without this the light trails everything
|
|
ctx.globalCompositeOperation = "source-over";
|
|
ctx.globalAlpha = 1;
|
|
ctx.fillStyle = "#000";
|
|
ctx.fillRect(0, 0, width, height);
|
|
ctx.globalCompositeOperation = "screen";
|
|
ctx.globalAlpha = 0.6;
|
|
gameState.coins.forEach((coin)=>{
|
|
if (!coin.destroyed) drawFuzzyBall(ctx, coin.color, gameState.coinSize * 2, coin.x, coin.y);
|
|
});
|
|
gameState.balls.forEach((ball)=>{
|
|
drawFuzzyBall(ctx, gameState.ballsColor, gameState.ballSize * 2, ball.x, ball.y);
|
|
});
|
|
ctx.globalAlpha = 0.5;
|
|
gameState.bricks.forEach((color, index)=>{
|
|
if (!color) return;
|
|
const x = brickCenterX(index), y = brickCenterY(index);
|
|
drawFuzzyBall(ctx, color == "black" ? "#666" : color, gameState.brickWidth, x, y);
|
|
});
|
|
ctx.globalAlpha = 1;
|
|
gameState.flashes.forEach((flash)=>{
|
|
const { x, y, time, color, size, type, duration } = flash;
|
|
const elapsed = gameState.levelTime - time;
|
|
ctx.globalAlpha = Math.min(1, 2 - elapsed / duration * 2);
|
|
if (type === "ball") drawFuzzyBall(ctx, color, size, x, y);
|
|
if (type === "particle") drawFuzzyBall(ctx, color, size * 3, x, y);
|
|
});
|
|
// Decides how brights the bg black parts can get
|
|
ctx.globalAlpha = 0.2;
|
|
ctx.globalCompositeOperation = "multiply";
|
|
ctx.fillStyle = "black";
|
|
ctx.fillRect(0, 0, width, height);
|
|
// Decides how dark the background black parts are when lit (1=black)
|
|
ctx.globalAlpha = 0.8;
|
|
ctx.globalCompositeOperation = "multiply";
|
|
if (level.svg && background.width && background.complete) {
|
|
if (backgroundCanvas.title !== level.name) {
|
|
backgroundCanvas.title = level.name;
|
|
backgroundCanvas.width = gameState.canvasWidth;
|
|
backgroundCanvas.height = gameState.canvasHeight;
|
|
const bgctx = backgroundCanvas.getContext("2d");
|
|
bgctx.fillStyle = level.color || "#000";
|
|
bgctx.fillRect(0, 0, gameState.canvasWidth, gameState.canvasHeight);
|
|
const pattern = ctx.createPattern(background, "repeat");
|
|
if (pattern) {
|
|
bgctx.fillStyle = pattern;
|
|
bgctx.fillRect(0, 0, width, height);
|
|
}
|
|
}
|
|
ctx.drawImage(backgroundCanvas, 0, 0);
|
|
} else {
|
|
// Background not loaded yes
|
|
ctx.fillStyle = "#000";
|
|
ctx.fillRect(0, 0, width, height);
|
|
}
|
|
} else {
|
|
ctx.globalAlpha = 1;
|
|
ctx.globalCompositeOperation = "source-over";
|
|
ctx.fillStyle = level.color || "#000";
|
|
ctx.fillRect(0, 0, width, height);
|
|
gameState.flashes.forEach((flash)=>{
|
|
const { x, y, time, color, size, type, duration } = flash;
|
|
const elapsed = gameState.levelTime - time;
|
|
ctx.globalAlpha = Math.min(1, 2 - elapsed / duration * 2);
|
|
if (type === "particle") drawBall(ctx, color, size, x, y);
|
|
});
|
|
}
|
|
ctx.globalAlpha = 1;
|
|
ctx.globalCompositeOperation = "source-over";
|
|
const lastExplosionDelay = Date.now() - gameState.lastExplosion + 5;
|
|
const shaked = lastExplosionDelay < 200 && !(0, _options.isOptionOn)('basic');
|
|
if (shaked) {
|
|
const amplitude = (gameState.perks.bigger_explosions + 1) * 50 / lastExplosionDelay;
|
|
ctx.translate(Math.sin(Date.now()) * amplitude, Math.sin(Date.now() + 36) * amplitude);
|
|
}
|
|
if (gameState.perks.bigger_explosions && !(0, _options.isOptionOn)('basic')) {
|
|
if (shaked) gameCanvas.style.filter = 'brightness(' + (1 + 100 / (1 + lastExplosionDelay)) + ')';
|
|
else gameCanvas.style.filter = '';
|
|
}
|
|
// Coins
|
|
ctx.globalAlpha = 1;
|
|
gameState.coins.forEach((coin)=>{
|
|
if (!coin.destroyed) {
|
|
ctx.globalCompositeOperation = coin.color === "gold" || level.color ? "source-over" : "screen";
|
|
drawCoin(ctx, coin.color, coin.size, coin.x, coin.y, level.color || "black", coin.a);
|
|
}
|
|
});
|
|
// Black shadow around balls
|
|
if (!(0, _options.isOptionOn)("basic")) {
|
|
ctx.globalCompositeOperation = "source-over";
|
|
ctx.globalAlpha = Math.min(0.8, gameState.coins.length / 20);
|
|
gameState.balls.forEach((ball)=>{
|
|
drawBall(ctx, level.color || "#000", gameState.ballSize * 6, ball.x, ball.y);
|
|
});
|
|
}
|
|
ctx.globalCompositeOperation = "source-over";
|
|
renderAllBricks();
|
|
ctx.globalCompositeOperation = "screen";
|
|
gameState.flashes = gameState.flashes.filter((f)=>gameState.levelTime - f.time < f.duration && !f.destroyed);
|
|
gameState.flashes.forEach((flash)=>{
|
|
const { x, y, time, color, size, type, duration } = flash;
|
|
const elapsed = gameState.levelTime - time;
|
|
ctx.globalAlpha = Math.max(0, Math.min(1, 2 - elapsed / duration * 2));
|
|
if (type === "text") {
|
|
ctx.globalCompositeOperation = "source-over";
|
|
drawText(ctx, flash.text, color, size, x, y - elapsed / 10);
|
|
} else if (type === "particle") {
|
|
ctx.globalCompositeOperation = "screen";
|
|
drawBall(ctx, color, size, x, y);
|
|
drawFuzzyBall(ctx, color, size, x, y);
|
|
}
|
|
});
|
|
if (gameState.perks.extra_life) {
|
|
ctx.globalAlpha = 1;
|
|
ctx.globalCompositeOperation = "source-over";
|
|
ctx.fillStyle = gameState.puckColor;
|
|
for(let i = 0; i < gameState.perks.extra_life; i++)ctx.fillRect(gameState.offsetXRoundedDown, gameState.gameZoneHeight - gameState.puckHeight / 2 + 2 * i, gameState.gameZoneWidthRoundedUp, 1);
|
|
}
|
|
ctx.globalAlpha = 1;
|
|
ctx.globalCompositeOperation = "source-over";
|
|
gameState.balls.forEach((ball)=>{
|
|
// The white border around is to distinguish colored balls from coins/bg
|
|
drawBall(ctx, gameState.ballsColor, gameState.ballSize, ball.x, ball.y, gameState.puckColor);
|
|
if (isTelekinesisActive(ball)) {
|
|
ctx.strokeStyle = gameState.puckColor;
|
|
ctx.beginPath();
|
|
ctx.bezierCurveTo(gameState.puckPosition, gameState.gameZoneHeight, gameState.puckPosition, ball.y, ball.x, ball.y);
|
|
ctx.stroke();
|
|
}
|
|
});
|
|
// The puck
|
|
ctx.globalAlpha = 1;
|
|
ctx.globalCompositeOperation = "source-over";
|
|
if (gameState.perks.streak_shots && gameState.combo > (0, _combo.baseCombo)(gameState)) drawPuck(ctx, "red", gameState.puckWidth, gameState.puckHeight, -2);
|
|
drawPuck(ctx, gameState.puckColor, gameState.puckWidth, gameState.puckHeight);
|
|
if (gameState.combo > 1) {
|
|
ctx.globalCompositeOperation = "source-over";
|
|
const comboText = "x " + gameState.combo;
|
|
const comboTextWidth = comboText.length * gameState.puckHeight / 1.8;
|
|
const totalWidth = comboTextWidth + gameState.coinSize * 2;
|
|
const left = gameState.puckPosition - totalWidth / 2;
|
|
if (totalWidth < gameState.puckWidth) {
|
|
drawCoin(ctx, "gold", gameState.coinSize, left + gameState.coinSize / 2, gameState.gameZoneHeight - gameState.puckHeight / 2, gameState.puckColor, 0);
|
|
drawText(ctx, comboText, "#000", gameState.puckHeight, left + gameState.coinSize * 1.5, gameState.gameZoneHeight - gameState.puckHeight / 2, true);
|
|
} else drawText(ctx, comboText, "#FFF", gameState.puckHeight, gameState.puckPosition, gameState.gameZoneHeight - gameState.puckHeight / 2, false);
|
|
}
|
|
// Borders
|
|
const hasCombo = gameState.combo > (0, _combo.baseCombo)(gameState);
|
|
ctx.globalCompositeOperation = "source-over";
|
|
if (gameState.offsetXRoundedDown) {
|
|
// draw outside of gaming area to avoid capturing borders in recordings
|
|
ctx.fillStyle = hasCombo && gameState.perks.left_is_lava ? "red" : gameState.puckColor;
|
|
ctx.fillRect(gameState.offsetX - 1, 0, 1, height);
|
|
ctx.fillStyle = hasCombo && gameState.perks.right_is_lava ? "red" : gameState.puckColor;
|
|
ctx.fillRect(width - gameState.offsetX + 1, 0, 1, height);
|
|
} else {
|
|
ctx.fillStyle = "red";
|
|
if (hasCombo && gameState.perks.left_is_lava) ctx.fillRect(0, 0, 1, height);
|
|
if (hasCombo && gameState.perks.right_is_lava) ctx.fillRect(width - 1, 0, 1, height);
|
|
}
|
|
if (gameState.perks.top_is_lava && gameState.combo > (0, _combo.baseCombo)(gameState)) {
|
|
ctx.fillStyle = "red";
|
|
ctx.fillRect(gameState.offsetXRoundedDown, 0, gameState.gameZoneWidthRoundedUp, 1);
|
|
}
|
|
const redBottom = gameState.perks.compound_interest && gameState.combo > (0, _combo.baseCombo)(gameState);
|
|
ctx.fillStyle = redBottom ? "red" : gameState.puckColor;
|
|
if ((0, _options.isOptionOn)("mobile-mode")) {
|
|
ctx.fillRect(gameState.offsetXRoundedDown, gameState.gameZoneHeight, gameState.gameZoneWidthRoundedUp, 1);
|
|
if (!gameState.running) drawText(ctx, (0, _i18N.t)('play.mobile_press_to_play'), gameState.puckColor, gameState.puckHeight, gameState.canvasWidth / 2, gameState.gameZoneHeight + (gameState.canvasHeight - gameState.gameZoneHeight) / 2);
|
|
} else if (redBottom) ctx.fillRect(gameState.offsetXRoundedDown, gameState.gameZoneHeight - 1, gameState.gameZoneWidthRoundedUp, 1);
|
|
if (shaked) ctx.resetTransform();
|
|
recordOneFrame();
|
|
}
|
|
let cachedBricksRender = document.createElement("canvas");
|
|
let cachedBricksRenderKey = "";
|
|
function renderAllBricks() {
|
|
ctx.globalAlpha = 1;
|
|
const redBorderOnBricksWithWrongColor = gameState.combo > (0, _combo.baseCombo)(gameState) && gameState.perks.picky_eater && !(0, _options.isOptionOn)('basic');
|
|
const newKey = gameState.gameZoneWidth + "_" + gameState.bricks.join("_") + bombSVG.complete + "_" + redBorderOnBricksWithWrongColor + "_" + gameState.ballsColor + "_" + gameState.perks.pierce_color;
|
|
if (newKey !== cachedBricksRenderKey) {
|
|
cachedBricksRenderKey = newKey;
|
|
cachedBricksRender.width = gameState.gameZoneWidth;
|
|
cachedBricksRender.height = gameState.gameZoneWidth + 1;
|
|
const canctx = cachedBricksRender.getContext("2d");
|
|
canctx.clearRect(0, 0, gameState.gameZoneWidth, gameState.gameZoneWidth);
|
|
canctx.resetTransform();
|
|
canctx.translate(-gameState.offsetX, 0);
|
|
// Bricks
|
|
gameState.bricks.forEach((color, index)=>{
|
|
const x = brickCenterX(index), y = brickCenterY(index);
|
|
if (!color) return;
|
|
const borderColor = gameState.ballsColor !== color && color !== "black" && redBorderOnBricksWithWrongColor && "red" || color;
|
|
drawBrick(canctx, color, borderColor, x, y);
|
|
if (color === "black") {
|
|
canctx.globalCompositeOperation = "source-over";
|
|
drawIMG(canctx, bombSVG, gameState.brickWidth, x, y);
|
|
}
|
|
});
|
|
}
|
|
ctx.drawImage(cachedBricksRender, gameState.offsetX, 0);
|
|
}
|
|
let cachedGraphics = {};
|
|
function drawPuck(ctx, color, puckWidth, puckHeight, yOffset = 0) {
|
|
const key = "puck" + color + "_" + puckWidth + "_" + puckHeight;
|
|
if (!cachedGraphics[key]) {
|
|
const can = document.createElement("canvas");
|
|
can.width = puckWidth;
|
|
can.height = puckHeight * 2;
|
|
const canctx = can.getContext("2d");
|
|
canctx.fillStyle = color;
|
|
canctx.beginPath();
|
|
canctx.moveTo(0, puckHeight * 2);
|
|
canctx.lineTo(0, puckHeight * 1.25);
|
|
canctx.bezierCurveTo(0, puckHeight * 0.75, puckWidth, puckHeight * 0.75, puckWidth, puckHeight * 1.25);
|
|
canctx.lineTo(puckWidth, puckHeight * 2);
|
|
canctx.fill();
|
|
cachedGraphics[key] = can;
|
|
}
|
|
ctx.drawImage(cachedGraphics[key], Math.round(gameState.puckPosition - puckWidth / 2), gameState.gameZoneHeight - puckHeight * 2 + yOffset);
|
|
}
|
|
function drawBall(ctx, color, width, x, y, borderColor = "") {
|
|
const key = "ball" + color + "_" + width + "_" + borderColor;
|
|
const size = Math.round(width);
|
|
if (!cachedGraphics[key]) {
|
|
const can = document.createElement("canvas");
|
|
can.width = size;
|
|
can.height = size;
|
|
const canctx = can.getContext("2d");
|
|
canctx.beginPath();
|
|
canctx.arc(size / 2, size / 2, Math.round(size / 2) - 1, 0, 2 * Math.PI);
|
|
canctx.fillStyle = color;
|
|
canctx.fill();
|
|
if (borderColor) {
|
|
canctx.lineWidth = 2;
|
|
canctx.strokeStyle = borderColor;
|
|
canctx.stroke();
|
|
}
|
|
cachedGraphics[key] = can;
|
|
}
|
|
ctx.drawImage(cachedGraphics[key], Math.round(x - size / 2), Math.round(y - size / 2));
|
|
}
|
|
const angles = 32;
|
|
function drawCoin(ctx, color, size, x, y, borderColor, rawAngle) {
|
|
const angle = (Math.round(rawAngle / Math.PI * 2 * angles) % angles + angles) % angles;
|
|
const key = "coin with halo_" + color + "_" + size + "_" + borderColor + "_" + (color === "gold" ? angle : "whatever");
|
|
if (!cachedGraphics[key]) {
|
|
const can = document.createElement("canvas");
|
|
can.width = size;
|
|
can.height = size;
|
|
const canctx = can.getContext("2d");
|
|
// coin
|
|
canctx.beginPath();
|
|
canctx.arc(size / 2, size / 2, size / 2, 0, 2 * Math.PI);
|
|
canctx.fillStyle = color;
|
|
canctx.fill();
|
|
if (color === "gold") {
|
|
canctx.strokeStyle = borderColor;
|
|
canctx.stroke();
|
|
canctx.beginPath();
|
|
canctx.arc(size / 2, size / 2, size / 2 * 0.6, 0, 2 * Math.PI);
|
|
canctx.fillStyle = "rgba(255,255,255,0.5)";
|
|
canctx.fill();
|
|
canctx.translate(size / 2, size / 2);
|
|
canctx.rotate(angle / 16);
|
|
canctx.translate(-size / 2, -size / 2);
|
|
canctx.globalCompositeOperation = "multiply";
|
|
drawText(canctx, "$", color, size - 2, size / 2, size / 2 + 1);
|
|
drawText(canctx, "$", color, size - 2, size / 2, size / 2 + 1);
|
|
}
|
|
cachedGraphics[key] = can;
|
|
}
|
|
ctx.drawImage(cachedGraphics[key], Math.round(x - size / 2), Math.round(y - size / 2));
|
|
}
|
|
function drawFuzzyBall(ctx, color, width, x, y) {
|
|
const key = "fuzzy-circle" + color + "_" + width;
|
|
if (!color) debugger;
|
|
const size = Math.round(width * 3);
|
|
if (!cachedGraphics[key]) {
|
|
const can = document.createElement("canvas");
|
|
can.width = size;
|
|
can.height = size;
|
|
const canctx = can.getContext("2d");
|
|
const gradient = canctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2);
|
|
gradient.addColorStop(0, color);
|
|
gradient.addColorStop(1, "transparent");
|
|
canctx.fillStyle = gradient;
|
|
canctx.fillRect(0, 0, size, size);
|
|
cachedGraphics[key] = can;
|
|
}
|
|
ctx.drawImage(cachedGraphics[key], Math.round(x - size / 2), Math.round(y - size / 2));
|
|
}
|
|
function drawBrick(ctx, color, borderColor, x, y) {
|
|
const tlx = Math.ceil(x - gameState.brickWidth / 2);
|
|
const tly = Math.ceil(y - gameState.brickWidth / 2);
|
|
const brx = Math.ceil(x + gameState.brickWidth / 2) - 1;
|
|
const bry = Math.ceil(y + gameState.brickWidth / 2) - 1;
|
|
const width = brx - tlx, height = bry - tly;
|
|
const key = "brick" + color + "_" + borderColor + "_" + width + "_" + height;
|
|
if (!cachedGraphics[key]) {
|
|
const can = document.createElement("canvas");
|
|
can.width = width;
|
|
can.height = height;
|
|
const bord = 2;
|
|
const cornerRadius = 2;
|
|
const canctx = can.getContext("2d");
|
|
canctx.fillStyle = color;
|
|
canctx.strokeStyle = borderColor;
|
|
canctx.lineJoin = "round";
|
|
canctx.lineWidth = bord;
|
|
roundRect(canctx, bord / 2, bord / 2, width - bord, height - bord, cornerRadius);
|
|
canctx.fill();
|
|
canctx.stroke();
|
|
cachedGraphics[key] = can;
|
|
}
|
|
ctx.drawImage(cachedGraphics[key], tlx, tly, width, height);
|
|
// It's not easy to have a 1px gap between bricks without antialiasing
|
|
}
|
|
function roundRect(ctx, x, y, width, height, radius) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(x + radius, y);
|
|
ctx.lineTo(x + width - radius, y);
|
|
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
ctx.lineTo(x + width, y + height - radius);
|
|
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
ctx.lineTo(x + radius, y + height);
|
|
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
ctx.lineTo(x, y + radius);
|
|
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
ctx.closePath();
|
|
}
|
|
function drawIMG(ctx, img, size, x, y) {
|
|
const key = "svg" + img + "_" + size + "_" + img.complete;
|
|
if (!cachedGraphics[key]) {
|
|
const can = document.createElement("canvas");
|
|
can.width = size;
|
|
can.height = size;
|
|
const canctx = can.getContext("2d");
|
|
const ratio = size / Math.max(img.width, img.height);
|
|
const w = img.width * ratio;
|
|
const h = img.height * ratio;
|
|
canctx.drawImage(img, (size - w) / 2, (size - h) / 2, w, h);
|
|
cachedGraphics[key] = can;
|
|
}
|
|
ctx.drawImage(cachedGraphics[key], Math.round(x - size / 2), Math.round(y - size / 2));
|
|
}
|
|
function drawText(ctx, text, color, fontSize, x, y, left = false) {
|
|
const key = "text" + text + "_" + color + "_" + fontSize + "_" + left;
|
|
if (!cachedGraphics[key]) {
|
|
const can = document.createElement("canvas");
|
|
can.width = fontSize * text.length;
|
|
can.height = fontSize;
|
|
const canctx = can.getContext("2d");
|
|
canctx.fillStyle = color;
|
|
canctx.textAlign = left ? "left" : "center";
|
|
canctx.textBaseline = "middle";
|
|
canctx.font = fontSize + "px monospace";
|
|
canctx.fillText(text, left ? 0 : can.width / 2, can.height / 2, can.width);
|
|
cachedGraphics[key] = can;
|
|
}
|
|
ctx.drawImage(cachedGraphics[key], left ? x : Math.round(x - cachedGraphics[key].width / 2), Math.round(y - cachedGraphics[key].height / 2));
|
|
}
|
|
window.addEventListener("visibilitychange", ()=>{
|
|
if (document.hidden) pause(true);
|
|
});
|
|
const scoreDisplay = document.getElementById("score");
|
|
const menuLabel = document.getElementById("menuLabel");
|
|
let alertsOpen = 0, closeModal = null;
|
|
function asyncAlert({ title, text, actions, allowClose = true, textAfterButtons = "", actionsAsGrid = false }) {
|
|
alertsOpen++;
|
|
return new Promise((resolve)=>{
|
|
const popupWrap = document.createElement("div");
|
|
document.body.appendChild(popupWrap);
|
|
popupWrap.className = "popup " + (actionsAsGrid ? "actionsAsGrid " : "");
|
|
function closeWithResult(value) {
|
|
resolve(value);
|
|
// Doing this async lets the menu scroll persist if it's shown a second time
|
|
setTimeout(()=>{
|
|
document.body.removeChild(popupWrap);
|
|
});
|
|
}
|
|
if (allowClose) {
|
|
const closeButton = document.createElement("button");
|
|
closeButton.title = (0, _i18N.t)('play.close_modale_window_tooltip');
|
|
closeButton.className = "close-modale";
|
|
closeButton.addEventListener("click", (e)=>{
|
|
e.preventDefault();
|
|
closeWithResult(undefined);
|
|
});
|
|
closeModal = ()=>{
|
|
closeWithResult(undefined);
|
|
};
|
|
popupWrap.appendChild(closeButton);
|
|
}
|
|
const popup = document.createElement("div");
|
|
if (title) {
|
|
const p = document.createElement("h2");
|
|
p.innerHTML = title;
|
|
popup.appendChild(p);
|
|
}
|
|
if (text) {
|
|
const p = document.createElement("div");
|
|
p.innerHTML = text;
|
|
popup.appendChild(p);
|
|
}
|
|
const buttons = document.createElement("section");
|
|
popup.appendChild(buttons);
|
|
actions?.filter((i)=>i).forEach(({ text, value, help, disabled, className = "", icon = "" })=>{
|
|
const button = document.createElement("button");
|
|
button.innerHTML = `
|
|
${icon}
|
|
<div>
|
|
<strong>${text}</strong>
|
|
<em>${help || ""}</em>
|
|
</div>`;
|
|
if (disabled) button.setAttribute("disabled", "disabled");
|
|
else button.addEventListener("click", (e)=>{
|
|
e.preventDefault();
|
|
closeWithResult(value);
|
|
});
|
|
button.className = className;
|
|
buttons.appendChild(button);
|
|
});
|
|
if (textAfterButtons) {
|
|
const p = document.createElement("div");
|
|
p.className = "textAfterButtons";
|
|
p.innerHTML = textAfterButtons;
|
|
popup.appendChild(p);
|
|
}
|
|
popupWrap.appendChild(popup);
|
|
popup.querySelector("button:not([disabled])")?.focus();
|
|
}).then((v)=>{
|
|
alertsOpen--;
|
|
closeModal = null;
|
|
return v;
|
|
}, ()=>{
|
|
closeModal = null;
|
|
alertsOpen--;
|
|
});
|
|
}
|
|
scoreDisplay.addEventListener("click", (e)=>{
|
|
e.preventDefault();
|
|
openScorePanel();
|
|
});
|
|
document.addEventListener("visibilitychange", ()=>{
|
|
if (document.hidden) pause(true);
|
|
});
|
|
async function openScorePanel() {
|
|
pause(true);
|
|
const cb = await asyncAlert({
|
|
title: (0, _i18N.t)('score_panel.title', {
|
|
score: gameState.score,
|
|
level: gameState.currentLevel + 1,
|
|
max: max_levels()
|
|
}),
|
|
text: `
|
|
${gameState.isCreativeModeRun ? "<p>${t('score_panel.test_run}</p>" : ""}
|
|
<p>${(0, _i18N.t)('score_panel.upgrades_picked')}</p>
|
|
<p>${pickedUpgradesHTMl()}</p>
|
|
`,
|
|
allowClose: true,
|
|
actions: [
|
|
{
|
|
text: (0, _i18N.t)('score_panel.resume'),
|
|
help: (0, _i18N.t)('score_panel.resume_help'),
|
|
value: ()=>{}
|
|
},
|
|
{
|
|
text: (0, _i18N.t)('score_panel.restart'),
|
|
help: (0, _i18N.t)('score_panel.restart_help'),
|
|
value: ()=>{
|
|
restart({
|
|
levelToAvoid: currentLevelInfo().name
|
|
});
|
|
}
|
|
}
|
|
]
|
|
});
|
|
if (cb) cb();
|
|
}
|
|
document.getElementById("menu")?.addEventListener("click", (e)=>{
|
|
e.preventDefault();
|
|
openSettingsPanel();
|
|
});
|
|
async function openSettingsPanel() {
|
|
pause(true);
|
|
const actions = [
|
|
{
|
|
text: (0, _i18N.t)('main_menu.resume'),
|
|
help: (0, _i18N.t)('main_menu.resume_help'),
|
|
value () {}
|
|
},
|
|
{
|
|
text: (0, _i18N.t)("main_menu.unlocks"),
|
|
help: (0, _i18N.t)('main_menu.unlocks_help'),
|
|
value () {
|
|
openUnlocksList();
|
|
}
|
|
}
|
|
];
|
|
for (const key of Object.keys((0, _options.options)))if ((0, _options.options)[key]) actions.push({
|
|
disabled: (0, _options.options)[key].disabled(),
|
|
icon: (0, _options.isOptionOn)(key) ? (0, _loadGameData.icons)["icon:checkmark_checked"] : (0, _loadGameData.icons)["icon:checkmark_unchecked"],
|
|
text: (0, _options.options)[key].name,
|
|
help: (0, _options.options)[key].help,
|
|
value: ()=>{
|
|
(0, _options.toggleOption)(key);
|
|
openSettingsPanel();
|
|
}
|
|
});
|
|
const creativeModeThreshold = Math.max(...(0, _loadGameData.upgrades).map((u)=>u.threshold));
|
|
if (document.fullscreenEnabled || document.webkitFullscreenEnabled) {
|
|
if (document.fullscreenElement !== null) actions.push({
|
|
text: (0, _i18N.t)('main_menu.fullscreen_exit'),
|
|
help: (0, _i18N.t)('main_menu.fullscreen_exit_help'),
|
|
icon: (0, _loadGameData.icons)["icon:exit_fullscreen"],
|
|
value () {
|
|
toggleFullScreen();
|
|
}
|
|
});
|
|
else actions.push({
|
|
text: (0, _i18N.t)('main_menu.fullscreen'),
|
|
help: (0, _i18N.t)('main_menu.fullscreen_help'),
|
|
icon: (0, _loadGameData.icons)["icon:fullscreen"],
|
|
value () {
|
|
toggleFullScreen();
|
|
}
|
|
});
|
|
}
|
|
actions.push({
|
|
text: (0, _i18N.t)('sandbox.title'),
|
|
help: getTotalScore() < creativeModeThreshold ? (0, _i18N.t)('sandbox.unlocks_at', {
|
|
score: creativeModeThreshold
|
|
}) : (0, _i18N.t)('sandbox.help'),
|
|
disabled: getTotalScore() < creativeModeThreshold,
|
|
async value () {
|
|
let creativeModePerks = (0, _settings.getSettingValue)('creativeModePerks', {}), choice;
|
|
while(choice = await asyncAlert({
|
|
title: (0, _i18N.t)('sandbox.title'),
|
|
text: (0, _i18N.t)('sandbox.instructions'),
|
|
actionsAsGrid: true,
|
|
actions: [
|
|
...(0, _loadGameData.upgrades).map((u)=>({
|
|
icon: u.icon,
|
|
text: u.name,
|
|
help: (creativeModePerks[u.id] || 0) + "/" + u.max,
|
|
value: u,
|
|
className: creativeModePerks[u.id] ? "" : "grey-out-unless-hovered"
|
|
})),
|
|
{
|
|
text: (0, _i18N.t)('sandbox.start'),
|
|
value: "start"
|
|
}
|
|
]
|
|
})){
|
|
if (choice === "start") {
|
|
(0, _settings.setSettingValue)('creativeModePerks', creativeModePerks);
|
|
restart({
|
|
perks: creativeModePerks
|
|
});
|
|
break;
|
|
} else if (choice) creativeModePerks[choice.id] = ((creativeModePerks[choice.id] || 0) + 1) % (choice.max + 1);
|
|
}
|
|
}
|
|
});
|
|
actions.push({
|
|
text: (0, _i18N.t)('main_menu.reset'),
|
|
help: (0, _i18N.t)('main_menu.reset_help'),
|
|
async value () {
|
|
if (await asyncAlert({
|
|
title: (0, _i18N.t)('main_menu.reset'),
|
|
text: (0, _i18N.t)('main_menu.reset_instruction'),
|
|
actions: [
|
|
{
|
|
text: (0, _i18N.t)('main_menu.reset_confirm'),
|
|
value: true
|
|
},
|
|
{
|
|
text: (0, _i18N.t)('main_menu.reset_cancel'),
|
|
value: false
|
|
}
|
|
],
|
|
allowClose: true
|
|
})) {
|
|
localStorage.clear();
|
|
window.location.reload();
|
|
}
|
|
}
|
|
});
|
|
const cb = await asyncAlert({
|
|
title: (0, _i18N.t)('main_menu.title'),
|
|
text: ``,
|
|
allowClose: true,
|
|
actions,
|
|
textAfterButtons: (0, _i18N.t)('main_menu.footer_html', {
|
|
appVersion: (0, _loadGameData.appVersion)
|
|
})
|
|
});
|
|
if (cb) cb();
|
|
}
|
|
async function openUnlocksList() {
|
|
const ts = getTotalScore();
|
|
const actions = [
|
|
...(0, _loadGameData.upgrades).sort((a, b)=>a.threshold - b.threshold).map(({ name, id, threshold, icon, fullHelp })=>({
|
|
text: name,
|
|
help: ts >= threshold ? fullHelp : (0, _i18N.t)('unlocks.unlocks_at', {
|
|
threshold
|
|
}),
|
|
disabled: ts < threshold,
|
|
value: {
|
|
perks: {
|
|
[id]: 1
|
|
}
|
|
},
|
|
icon
|
|
})),
|
|
...(0, _loadGameData.allLevels).sort((a, b)=>a.threshold - b.threshold).map((l)=>{
|
|
const available = ts >= l.threshold;
|
|
return {
|
|
text: l.name,
|
|
help: available ? (0, _i18N.t)('unlocks.level_description', {
|
|
size: l.size,
|
|
bricks: l.bricks.filter((i)=>i).length
|
|
}) : (0, _i18N.t)('unlocks.unlocks_at', {
|
|
threshold: l.threshold
|
|
}),
|
|
disabled: !available,
|
|
value: {
|
|
level: l.name
|
|
},
|
|
icon: (0, _loadGameData.icons)[l.name]
|
|
};
|
|
})
|
|
];
|
|
const percentUnlock = Math.round(actions.filter((a)=>!a.disabled).length / actions.length * 100);
|
|
const tryOn = await asyncAlert({
|
|
title: (0, _i18N.t)('unlocks.title', {
|
|
percentUnlock
|
|
}),
|
|
text: `<p>${(0, _i18N.t)('unlocks.intro', {
|
|
ts
|
|
})}
|
|
${percentUnlock < 100 ? (0, _i18N.t)('unlocks.greyed_out_help') : ""}</p>
|
|
`,
|
|
textAfterButtons: `<p>
|
|
Your high score is ${gameState.highScore}.
|
|
Click an item above to start a run with it.
|
|
</p>`,
|
|
actions,
|
|
allowClose: true
|
|
});
|
|
if (tryOn) {
|
|
if (!gameState.currentLevel || await asyncAlert({
|
|
title: (0, _i18N.t)('unlocks.restart_title'),
|
|
text: (0, _i18N.t)('unlocks.restart_text'),
|
|
actions: [
|
|
{
|
|
value: true,
|
|
text: (0, _i18N.t)('unlocks.restart_confirm')
|
|
},
|
|
{
|
|
value: false,
|
|
text: (0, _i18N.t)('unlocks.restart_cancel')
|
|
}
|
|
]
|
|
})) restart(tryOn);
|
|
}
|
|
}
|
|
function distance2(a, b) {
|
|
return Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2);
|
|
}
|
|
function distanceBetween(a, b) {
|
|
return Math.sqrt(distance2(a, b));
|
|
}
|
|
function rainbowColor() {
|
|
return `hsl(${Math.round(gameState.levelTime / 4) * 2 % 360},100%,70%)`;
|
|
}
|
|
function repulse(a, b, power, impactsBToo) {
|
|
const distance = distanceBetween(a, b);
|
|
// Ensure we don't get soft locked
|
|
const max = gameState.gameZoneWidth / 2;
|
|
if (distance > max) return;
|
|
// Unit vector
|
|
const dx = (a.x - b.x) / distance;
|
|
const dy = (a.y - b.y) / distance;
|
|
const fact = -power * (max - distance) / (max * 1.2) / 3 * Math.min(500, gameState.levelTime) / 500;
|
|
if (impactsBToo && typeof b.vx !== "undefined" && typeof b.vy !== "undefined") {
|
|
b.vx += dx * fact;
|
|
b.vy += dy * fact;
|
|
}
|
|
a.vx -= dx * fact;
|
|
a.vy -= dy * fact;
|
|
const speed = 10;
|
|
const rand = 2;
|
|
gameState.flashes.push({
|
|
type: "particle",
|
|
duration: 100,
|
|
time: gameState.levelTime,
|
|
size: gameState.coinSize / 2,
|
|
color: rainbowColor(),
|
|
ethereal: true,
|
|
x: a.x,
|
|
y: a.y,
|
|
vx: -dx * speed + a.vx + (Math.random() - 0.5) * rand,
|
|
vy: -dy * speed + a.vy + (Math.random() - 0.5) * rand
|
|
});
|
|
if (impactsBToo && typeof b.vx !== "undefined" && typeof b.vy !== "undefined") gameState.flashes.push({
|
|
type: "particle",
|
|
duration: 100,
|
|
time: gameState.levelTime,
|
|
size: gameState.coinSize / 2,
|
|
color: rainbowColor(),
|
|
ethereal: true,
|
|
x: b.x,
|
|
y: b.y,
|
|
vx: dx * speed + b.vx + (Math.random() - 0.5) * rand,
|
|
vy: dy * speed + b.vy + (Math.random() - 0.5) * rand
|
|
});
|
|
}
|
|
function attract(a, b, power) {
|
|
const distance = distanceBetween(a, b);
|
|
// Ensure we don't get soft locked
|
|
const min = gameState.gameZoneWidth * 0.5;
|
|
if (distance < min) return;
|
|
// Unit vector
|
|
const dx = (a.x - b.x) / distance;
|
|
const dy = (a.y - b.y) / distance;
|
|
const fact = power * (distance - min) / min * Math.min(500, gameState.levelTime) / 500;
|
|
b.vx += dx * fact;
|
|
b.vy += dy * fact;
|
|
a.vx -= dx * fact;
|
|
a.vy -= dy * fact;
|
|
const speed = 10;
|
|
const rand = 2;
|
|
gameState.flashes.push({
|
|
type: "particle",
|
|
duration: 100,
|
|
time: gameState.levelTime,
|
|
size: gameState.coinSize / 2,
|
|
color: rainbowColor(),
|
|
ethereal: true,
|
|
x: a.x,
|
|
y: a.y,
|
|
vx: dx * speed + a.vx + (Math.random() - 0.5) * rand,
|
|
vy: dy * speed + a.vy + (Math.random() - 0.5) * rand
|
|
});
|
|
gameState.flashes.push({
|
|
type: "particle",
|
|
duration: 100,
|
|
time: gameState.levelTime,
|
|
size: gameState.coinSize / 2,
|
|
color: rainbowColor(),
|
|
ethereal: true,
|
|
x: b.x,
|
|
y: b.y,
|
|
vx: -dx * speed + b.vx + (Math.random() - 0.5) * rand,
|
|
vy: -dy * speed + b.vy + (Math.random() - 0.5) * rand
|
|
});
|
|
}
|
|
let mediaRecorder, captureStream, captureTrack, recordCanvas, recordCanvasCtx;
|
|
function recordOneFrame() {
|
|
if (!(0, _options.isOptionOn)("record")) return;
|
|
if (!gameState.running) return;
|
|
if (!captureStream) return;
|
|
drawMainCanvasOnSmallCanvas();
|
|
if (captureTrack?.requestFrame) captureTrack?.requestFrame();
|
|
else if (captureStream?.requestFrame) captureStream.requestFrame();
|
|
}
|
|
function drawMainCanvasOnSmallCanvas() {
|
|
if (!recordCanvasCtx) return;
|
|
recordCanvasCtx.drawImage(gameCanvas, gameState.offsetXRoundedDown, 0, gameState.gameZoneWidthRoundedUp, gameState.gameZoneHeight, 0, 0, recordCanvas.width, recordCanvas.height);
|
|
// Here we don't use drawText as we don't want to cache a picture for each distinct value of score
|
|
recordCanvasCtx.fillStyle = "#FFF";
|
|
recordCanvasCtx.textBaseline = "top";
|
|
recordCanvasCtx.font = "12px monospace";
|
|
recordCanvasCtx.textAlign = "right";
|
|
recordCanvasCtx.fillText(gameState.score.toString(), recordCanvas.width - 12, 12);
|
|
recordCanvasCtx.textAlign = "left";
|
|
recordCanvasCtx.fillText("Level " + (gameState.currentLevel + 1) + "/" + max_levels(), 12, 12);
|
|
}
|
|
function startRecordingGame() {
|
|
if (!(0, _options.isOptionOn)("record")) return;
|
|
if (mediaRecorder) return;
|
|
if (!recordCanvas) {
|
|
// Smaller canvas with fewer details
|
|
recordCanvas = document.createElement("canvas");
|
|
recordCanvasCtx = recordCanvas.getContext("2d", {
|
|
antialias: false,
|
|
alpha: false
|
|
});
|
|
captureStream = recordCanvas.captureStream(0);
|
|
captureTrack = captureStream.getVideoTracks()[0];
|
|
const track = (0, _sounds.getAudioRecordingTrack)();
|
|
if (track) captureStream.addTrack(track.stream.getAudioTracks()[0]);
|
|
}
|
|
recordCanvas.width = gameState.gameZoneWidthRoundedUp;
|
|
recordCanvas.height = gameState.gameZoneHeight;
|
|
// drawMainCanvasOnSmallCanvas()
|
|
const recordedChunks = [];
|
|
const instance = new MediaRecorder(captureStream, {
|
|
videoBitsPerSecond: 3500000
|
|
});
|
|
mediaRecorder = instance;
|
|
instance.start();
|
|
mediaRecorder.pause();
|
|
instance.ondataavailable = function(event) {
|
|
recordedChunks.push(event.data);
|
|
};
|
|
instance.onstop = async function() {
|
|
let targetDiv;
|
|
let blob = new Blob(recordedChunks, {
|
|
type: "video/webm"
|
|
});
|
|
if (blob.size < 200000) return; // under 0.2MB, probably bugged out or pointlessly short
|
|
while(!(targetDiv = document.getElementById("level-recording-container")))await new Promise((r)=>setTimeout(r, 200));
|
|
const video = document.createElement("video");
|
|
video.autoplay = true;
|
|
video.controls = false;
|
|
video.disablePictureInPicture = true;
|
|
video.disableRemotePlayback = true;
|
|
video.width = recordCanvas.width;
|
|
video.height = recordCanvas.height;
|
|
video.loop = true;
|
|
video.muted = true;
|
|
video.playsInline = true;
|
|
video.src = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.download = captureFileName("webm");
|
|
a.target = "_blank";
|
|
a.href = video.src;
|
|
a.textContent = (0, _i18N.t)('main_menu.record_download', {
|
|
size: (blob.size / 1000000).toFixed(2)
|
|
});
|
|
targetDiv.appendChild(video);
|
|
targetDiv.appendChild(a);
|
|
};
|
|
}
|
|
function pauseRecording() {
|
|
if (!(0, _options.isOptionOn)("record")) return;
|
|
if (mediaRecorder?.state === "recording") mediaRecorder?.pause();
|
|
}
|
|
function resumeRecording() {
|
|
if (!(0, _options.isOptionOn)("record")) return;
|
|
if (mediaRecorder?.state === "paused") mediaRecorder.resume();
|
|
}
|
|
function stopRecording() {
|
|
if (!(0, _options.isOptionOn)("record")) return;
|
|
if (!mediaRecorder) return;
|
|
mediaRecorder?.stop();
|
|
mediaRecorder = null;
|
|
}
|
|
function captureFileName(ext = "webm") {
|
|
return "breakout-71-capture-" + new Date().toISOString().replace(/[^0-9\-]+/gi, "-") + "." + ext;
|
|
}
|
|
function findLast(arr, predicate) {
|
|
let i = arr.length;
|
|
while(--i)if (predicate(arr[i], i, arr)) return arr[i];
|
|
}
|
|
function toggleFullScreen() {
|
|
try {
|
|
if (document.fullscreenElement !== null) {
|
|
if (document.exitFullscreen) document.exitFullscreen();
|
|
else if (document.webkitCancelFullScreen) document.webkitCancelFullScreen();
|
|
} else {
|
|
const docel = document.documentElement;
|
|
if (docel.requestFullscreen) docel.requestFullscreen();
|
|
else if (docel.webkitRequestFullscreen) docel.webkitRequestFullscreen();
|
|
}
|
|
} catch (e) {
|
|
console.warn(e);
|
|
}
|
|
}
|
|
const pressed = {
|
|
ArrowLeft: 0,
|
|
ArrowRight: 0,
|
|
Shift: 0
|
|
};
|
|
function setKeyPressed(key, on) {
|
|
pressed[key] = on;
|
|
gameState.keyboardPuckSpeed = (pressed.ArrowRight - pressed.ArrowLeft) * (1 + pressed.Shift * 2) * gameState.gameZoneWidth / 50;
|
|
}
|
|
document.addEventListener("keydown", (e)=>{
|
|
if (e.key.toLowerCase() === "f" && !e.ctrlKey && !e.metaKey) toggleFullScreen();
|
|
else if (e.key in pressed) setKeyPressed(e.key, 1);
|
|
if (e.key === " " && !alertsOpen) {
|
|
if (gameState.running) pause(true);
|
|
else play();
|
|
} else return;
|
|
e.preventDefault();
|
|
});
|
|
document.addEventListener("keyup", async (e)=>{
|
|
const focused = document.querySelector("button:focus");
|
|
if (e.key in pressed) setKeyPressed(e.key, 0);
|
|
else if (e.key === "ArrowDown" && focused?.nextElementSibling?.tagName === "BUTTON") focused?.nextElementSibling?.focus();
|
|
else if (e.key === "ArrowUp" && focused?.previousElementSibling?.tagName === "BUTTON") focused?.previousElementSibling?.focus();
|
|
else if (e.key === "Escape" && closeModal) closeModal();
|
|
else if (e.key === "Escape" && gameState.running) pause(true);
|
|
else if (e.key.toLowerCase() === "m" && !alertsOpen) openSettingsPanel().then();
|
|
else if (e.key.toLowerCase() === "s" && !alertsOpen) openScorePanel().then();
|
|
else if (e.key.toLowerCase() === "r" && !alertsOpen) {
|
|
if (gameState.currentLevel < 3 || await asyncAlert({
|
|
title: (0, _i18N.t)('play.confirm_restart'),
|
|
actions: [
|
|
{
|
|
value: true,
|
|
text: (0, _i18N.t)('play.confirm_restart_yes')
|
|
},
|
|
{
|
|
value: false,
|
|
text: (0, _i18N.t)('play.confirm_restart_no')
|
|
}
|
|
]
|
|
})) restart({
|
|
levelToAvoid: currentLevelInfo().name
|
|
});
|
|
} else return;
|
|
e.preventDefault();
|
|
});
|
|
function newGameState(params) {
|
|
const totalScoreAtRunStart = getTotalScore();
|
|
const firstLevel = params?.level ? (0, _loadGameData.allLevels).filter((l)=>l.name === params?.level) : [];
|
|
const restInRandomOrder = (0, _loadGameData.allLevels).filter((l)=>totalScoreAtRunStart >= l.threshold).filter((l)=>l.name !== params?.level).filter((l)=>l.name !== params?.levelToAvoid).sort(()=>Math.random() - 0.5);
|
|
const runLevels = firstLevel.concat(restInRandomOrder.slice(0, 10).sort((a, b)=>a.sortKey - b.sortKey));
|
|
const perks = {
|
|
...(0, _gameUtils.makeEmptyPerksMap)((0, _loadGameData.upgrades)),
|
|
...params?.perks || {}
|
|
};
|
|
const gameState = {
|
|
runLevels,
|
|
currentLevel: 0,
|
|
perks,
|
|
puckWidth: 200,
|
|
baseSpeed: 12,
|
|
combo: 1,
|
|
gridSize: 12,
|
|
running: false,
|
|
puckPosition: 400,
|
|
pauseTimeout: null,
|
|
canvasWidth: 0,
|
|
canvasHeight: 0,
|
|
offsetX: 0,
|
|
offsetXRoundedDown: 0,
|
|
gameZoneWidth: 0,
|
|
gameZoneWidthRoundedUp: 0,
|
|
gameZoneHeight: 0,
|
|
brickWidth: 0,
|
|
needsRender: true,
|
|
score: 0,
|
|
lastScoreIncrease: -1000,
|
|
lastExplosion: -1000,
|
|
highScore: parseFloat(localStorage.getItem("breakout-3-hs") || "0"),
|
|
balls: [],
|
|
ballsColor: "white",
|
|
bricks: [],
|
|
flashes: [],
|
|
coins: [],
|
|
levelStartScore: 0,
|
|
levelMisses: 0,
|
|
levelSpawnedCoins: 0,
|
|
lastPlayedCoinGrab: 0,
|
|
MAX_COINS: 400,
|
|
MAX_PARTICLES: 600,
|
|
puckColor: "#FFF",
|
|
ballSize: 20,
|
|
coinSize: 14,
|
|
puckHeight: 20,
|
|
totalScoreAtRunStart,
|
|
isCreativeModeRun: (0, _gameUtils.sumOfKeys)(perks) > 1,
|
|
pauseUsesDuringRun: 0,
|
|
keyboardPuckSpeed: 0,
|
|
lastTick: performance.now(),
|
|
lastTickDown: 0,
|
|
runStatistics: {
|
|
started: Date.now(),
|
|
levelsPlayed: 0,
|
|
runTime: 0,
|
|
coins_spawned: 0,
|
|
score: 0,
|
|
bricks_broken: 0,
|
|
misses: 0,
|
|
balls_lost: 0,
|
|
puck_bounces: 0,
|
|
upgrades_picked: 1,
|
|
max_combo: 1,
|
|
max_level: 0
|
|
},
|
|
lastOffered: {},
|
|
levelTime: 0,
|
|
autoCleanUses: 0
|
|
};
|
|
(0, _resetBalls.resetBalls)(gameState);
|
|
if (!(0, _gameUtils.sumOfKeys)(gameState.perks)) {
|
|
const giftable = getPossibleUpgrades(gameState).filter((u)=>u.giftable);
|
|
const randomGift = (0, _options.isOptionOn)("easy") && "slow_down" || giftable[Math.floor(Math.random() * giftable.length)].id;
|
|
perks[randomGift] = 1;
|
|
dontOfferTooSoon(gameState, randomGift);
|
|
}
|
|
for (let perk of (0, _loadGameData.upgrades))if (gameState.perks[perk.id]) dontOfferTooSoon(gameState, perk.id);
|
|
return gameState;
|
|
}
|
|
const gameState = newGameState({});
|
|
function restart(params) {
|
|
Object.assign(gameState, newGameState(params));
|
|
pauseRecording();
|
|
setLevel(0);
|
|
}
|
|
restart({});
|
|
fitSize();
|
|
tick();
|
|
// @ts-ignore
|
|
// window.stressTest= ()=>restart({level:'Shark',perks:{base_combo:100, pierce:10, multiball:8}})
|
|
window.stressTest = ()=>restart({
|
|
level: 'Shark',
|
|
perks: {
|
|
sapper: 2,
|
|
pierce: 10,
|
|
multiball: 3
|
|
}
|
|
});
|
|
|
|
},{"./loadGameData":"l1B4x","./options":"d5NoS","./sounds":"dQKPV","./resetBalls":"gVgfx","./game_utils":"cEeac","./combo":"9S1mS","./sw_loader":"kRstf","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3","./i18n/i18n":"eNPRm","./settings":"5blfu"}],"l1B4x":[function(require,module,exports,__globalThis) {
|
|
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
|
|
parcelHelpers.defineInteropFlag(exports);
|
|
parcelHelpers.export(exports, "appVersion", ()=>appVersion);
|
|
parcelHelpers.export(exports, "icons", ()=>icons);
|
|
parcelHelpers.export(exports, "allLevels", ()=>allLevels);
|
|
parcelHelpers.export(exports, "upgrades", ()=>upgrades);
|
|
var _paletteJson = require("./palette.json");
|
|
var _paletteJsonDefault = parcelHelpers.interopDefault(_paletteJson);
|
|
var _levelsJson = require("./levels.json");
|
|
var _levelsJsonDefault = parcelHelpers.interopDefault(_levelsJson);
|
|
var _versionJson = require("./version.json");
|
|
var _versionJsonDefault = parcelHelpers.interopDefault(_versionJson);
|
|
var _rawUpgrades = require("./rawUpgrades");
|
|
var _getLevelBackground = require("./getLevelBackground");
|
|
const palette = (0, _paletteJsonDefault.default);
|
|
const rawLevelsList = (0, _levelsJsonDefault.default);
|
|
const appVersion = (0, _versionJsonDefault.default);
|
|
let levelIconHTMLCanvas = document.createElement("canvas");
|
|
const levelIconHTMLCanvasCtx = levelIconHTMLCanvas.getContext("2d", {
|
|
antialias: false,
|
|
alpha: true
|
|
});
|
|
function levelIconHTML(bricks, levelSize, color) {
|
|
const size = 40;
|
|
const c = levelIconHTMLCanvas;
|
|
const ctx = levelIconHTMLCanvasCtx;
|
|
c.width = size;
|
|
c.height = size;
|
|
if (color) {
|
|
ctx.fillStyle = color;
|
|
ctx.fillRect(0, 0, size, size);
|
|
} else ctx.clearRect(0, 0, size, size);
|
|
const pxSize = size / levelSize;
|
|
for(let x = 0; x < levelSize; x++)for(let y = 0; y < levelSize; y++){
|
|
const c = bricks[y * levelSize + x];
|
|
if (c) {
|
|
ctx.fillStyle = c;
|
|
ctx.fillRect(Math.floor(pxSize * x), Math.floor(pxSize * y), Math.ceil(pxSize), Math.ceil(pxSize));
|
|
}
|
|
}
|
|
return `<img alt="" width="${size}" height="${size}" src="${c.toDataURL()}"/>`;
|
|
}
|
|
const icons = {};
|
|
const allLevels = rawLevelsList.map((level)=>{
|
|
const bricks = level.bricks.split("").map((c)=>palette[c]).slice(0, level.size * level.size);
|
|
const icon = levelIconHTML(bricks, level.size, level.color);
|
|
icons[level.name] = icon;
|
|
return {
|
|
...level,
|
|
bricks,
|
|
icon,
|
|
svg: (0, _getLevelBackground.getLevelBackground)(level)
|
|
};
|
|
}).filter((l)=>!l.name.startsWith("icon:")).map((l, li)=>({
|
|
...l,
|
|
threshold: li < 8 ? 0 : Math.round(Math.min(Math.pow(10, 1 + (li + l.size) / 30) * 10, 5000) * li),
|
|
sortKey: (Math.random() + 3) / 3.5 * l.bricks.filter((i)=>i).length
|
|
}));
|
|
const upgrades = (0, _rawUpgrades.rawUpgrades).map((u)=>({
|
|
...u,
|
|
icon: icons["icon:" + u.id]
|
|
}));
|
|
|
|
},{"./palette.json":"jhnsJ","./levels.json":"kqnNl","./version.json":"h1X9A","./rawUpgrades":"cvg5m","./getLevelBackground":"7OIPf","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"h1X9A":[function(require,module,exports,__globalThis) {
|
|
module.exports = JSON.parse("\"29033878\"");
|
|
|
|
},{}],"cvg5m":[function(require,module,exports,__globalThis) {
|
|
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
|
|
parcelHelpers.defineInteropFlag(exports);
|
|
parcelHelpers.export(exports, "rawUpgrades", ()=>rawUpgrades);
|
|
var _i18N = require("./i18n/i18n");
|
|
const rawUpgrades = [
|
|
{
|
|
requires: "",
|
|
threshold: 0,
|
|
giftable: false,
|
|
id: "extra_life",
|
|
max: 7,
|
|
name: (0, _i18N.t)('upgrades.extra_life.name'),
|
|
help: (lvl)=>lvl === 1 ? (0, _i18N.t)('upgrades.extra_life.help') : (0, _i18N.t)('upgrades.extra_life.help_plural', {
|
|
lvl
|
|
}),
|
|
fullHelp: (0, _i18N.t)('upgrades.extra_life.fullHelp')
|
|
},
|
|
{
|
|
requires: "",
|
|
threshold: 0,
|
|
id: "streak_shots",
|
|
giftable: true,
|
|
max: 1,
|
|
name: (0, _i18N.t)('upgrades.streak_shots.name'),
|
|
help: (lvl)=>(0, _i18N.t)('upgrades.streak_shots.help', {
|
|
lvl
|
|
}),
|
|
fullHelp: (0, _i18N.t)('upgrades.streak_shots.fullHelp')
|
|
},
|
|
{
|
|
requires: "",
|
|
threshold: 0,
|
|
id: "base_combo",
|
|
giftable: true,
|
|
max: 7,
|
|
name: (0, _i18N.t)('upgrades.base_combo.name'),
|
|
help: (lvl)=>(0, _i18N.t)('upgrades.base_combo.help', {
|
|
coins: 1 + lvl * 3
|
|
}),
|
|
fullHelp: (0, _i18N.t)('upgrades.base_combo.fullHelp')
|
|
},
|
|
{
|
|
requires: "",
|
|
threshold: 0,
|
|
giftable: false,
|
|
id: "slow_down",
|
|
max: 2,
|
|
name: (0, _i18N.t)('upgrades.slow_down.name'),
|
|
help: ()=>(0, _i18N.t)('upgrades.slow_down.help'),
|
|
fullHelp: (0, _i18N.t)('upgrades.slow_down.fullHelp')
|
|
},
|
|
{
|
|
requires: "",
|
|
threshold: 0,
|
|
giftable: false,
|
|
id: "bigger_puck",
|
|
max: 2,
|
|
name: (0, _i18N.t)('upgrades.bigger_puck.name'),
|
|
help: ()=>(0, _i18N.t)('upgrades.bigger_puck.help'),
|
|
fullHelp: (0, _i18N.t)('upgrades.bigger_puck.fullHelp')
|
|
},
|
|
{
|
|
requires: "",
|
|
threshold: 0,
|
|
giftable: false,
|
|
id: "viscosity",
|
|
max: 3,
|
|
name: (0, _i18N.t)('upgrades.viscosity.name'),
|
|
help: ()=>(0, _i18N.t)('upgrades.viscosity.help'),
|
|
fullHelp: (0, _i18N.t)('upgrades.viscosity.fullHelp')
|
|
},
|
|
{
|
|
requires: "",
|
|
threshold: 0,
|
|
id: "left_is_lava",
|
|
giftable: true,
|
|
max: 1,
|
|
name: (0, _i18N.t)('upgrades.left_is_lava.name'),
|
|
help: ()=>(0, _i18N.t)('upgrades.left_is_lava.help'),
|
|
fullHelp: (0, _i18N.t)('upgrades.left_is_lava.fullHelp')
|
|
},
|
|
{
|
|
requires: "",
|
|
threshold: 0,
|
|
id: "right_is_lava",
|
|
giftable: true,
|
|
max: 1,
|
|
name: (0, _i18N.t)('upgrades.right_is_lava.name'),
|
|
help: ()=>(0, _i18N.t)('upgrades.right_is_lava.help'),
|
|
fullHelp: (0, _i18N.t)('upgrades.right_is_lava.fullHelp')
|
|
},
|
|
{
|
|
requires: "",
|
|
threshold: 0,
|
|
id: "top_is_lava",
|
|
giftable: true,
|
|
max: 1,
|
|
name: (0, _i18N.t)('upgrades.top_is_lava.name'),
|
|
help: ()=>(0, _i18N.t)('upgrades.top_is_lava.help'),
|
|
fullHelp: (0, _i18N.t)('upgrades.top_is_lava.fullHelp')
|
|
},
|
|
{
|
|
requires: "",
|
|
threshold: 0,
|
|
giftable: false,
|
|
id: "skip_last",
|
|
max: 7,
|
|
name: (0, _i18N.t)('upgrades.skip_last.name'),
|
|
help: (lvl)=>(0, _i18N.t)('upgrades.skip_last.help', {
|
|
lvl
|
|
}),
|
|
fullHelp: (0, _i18N.t)('upgrades.skip_last.fullHelp')
|
|
},
|
|
{
|
|
requires: "",
|
|
threshold: 500,
|
|
id: "telekinesis",
|
|
giftable: true,
|
|
max: 2,
|
|
name: (0, _i18N.t)('upgrades.telekinesis.name'),
|
|
help: (lvl)=>lvl == 1 ? (0, _i18N.t)('upgrades.telekinesis.help') : (0, _i18N.t)('upgrades.telekinesis.help_plural'),
|
|
fullHelp: (0, _i18N.t)('upgrades.telekinesis.fullHelp')
|
|
},
|
|
{
|
|
requires: "",
|
|
threshold: 1000,
|
|
giftable: false,
|
|
id: "coin_magnet",
|
|
max: 3,
|
|
name: (0, _i18N.t)('upgrades.coin_magnet.name'),
|
|
help: (lvl)=>lvl == 1 ? (0, _i18N.t)('upgrades.coin_magnet.help') : (0, _i18N.t)('upgrades.coin_magnet.help_plural'),
|
|
fullHelp: (0, _i18N.t)('upgrades.coin_magnet.fullHelp')
|
|
},
|
|
{
|
|
requires: "",
|
|
threshold: 1500,
|
|
id: "multiball",
|
|
giftable: true,
|
|
max: 6,
|
|
name: (0, _i18N.t)('upgrades.multiball.name'),
|
|
help: (lvl)=>(0, _i18N.t)('upgrades.multiball.help', {
|
|
count: lvl + 1
|
|
}),
|
|
fullHelp: (0, _i18N.t)('upgrades.multiball.fullHelp')
|
|
},
|
|
{
|
|
requires: "",
|
|
threshold: 2000,
|
|
giftable: false,
|
|
id: "smaller_puck",
|
|
max: 2,
|
|
name: (0, _i18N.t)('upgrades.smaller_puck.name'),
|
|
help: (lvl)=>lvl == 1 ? (0, _i18N.t)('upgrades.smaller_puck.help') : (0, _i18N.t)('upgrades.smaller_puck.help_plural'),
|
|
fullHelp: (0, _i18N.t)('upgrades.smaller_puck.fullHelp')
|
|
},
|
|
{
|
|
requires: "",
|
|
threshold: 3000,
|
|
id: "pierce",
|
|
giftable: true,
|
|
max: 3,
|
|
name: (0, _i18N.t)('upgrades.pierce.name'),
|
|
help: (lvl)=>(0, _i18N.t)('upgrades.pierce.help', {
|
|
count: 3 * lvl
|
|
}),
|
|
fullHelp: (0, _i18N.t)('upgrades.pierce.fullHelp')
|
|
},
|
|
{
|
|
requires: "",
|
|
threshold: 4000,
|
|
id: "picky_eater",
|
|
giftable: true,
|
|
max: 1,
|
|
name: (0, _i18N.t)('upgrades.picky_eater.name'),
|
|
help: (lvl)=>(0, _i18N.t)('upgrades.picky_eater.help'),
|
|
fullHelp: (0, _i18N.t)('upgrades.picky_eater.fullHelp')
|
|
},
|
|
{
|
|
requires: "",
|
|
threshold: 5000,
|
|
giftable: false,
|
|
id: "metamorphosis",
|
|
max: 1,
|
|
name: (0, _i18N.t)('upgrades.metamorphosis.name'),
|
|
help: (lvl)=>(0, _i18N.t)('upgrades.metamorphosis.help'),
|
|
fullHelp: (0, _i18N.t)('upgrades.metamorphosis.fullHelp')
|
|
},
|
|
{
|
|
requires: "",
|
|
threshold: 6000,
|
|
id: "compound_interest",
|
|
giftable: true,
|
|
max: 1,
|
|
name: (0, _i18N.t)('upgrades.compound_interest.name'),
|
|
help: (lvl)=>(0, _i18N.t)('upgrades.compound_interest.help'),
|
|
fullHelp: (0, _i18N.t)('upgrades.compound_interest.fullHelp')
|
|
},
|
|
{
|
|
requires: "",
|
|
threshold: 7000,
|
|
id: "hot_start",
|
|
giftable: true,
|
|
max: 3,
|
|
name: (0, _i18N.t)('upgrades.hot_start.name'),
|
|
help: (lvl)=>(0, _i18N.t)('upgrades.hot_start.help', {
|
|
start: lvl * 15 + 1,
|
|
lvl
|
|
}),
|
|
fullHelp: (0, _i18N.t)('upgrades.hot_start.fullHelp')
|
|
},
|
|
{
|
|
requires: "",
|
|
threshold: 9000,
|
|
id: "sapper",
|
|
giftable: true,
|
|
max: 7,
|
|
name: (0, _i18N.t)('upgrades.sapper.name'),
|
|
help: (lvl)=>lvl == 1 ? (0, _i18N.t)('upgrades.sapper.help') : (0, _i18N.t)('upgrades.sapper.help_plural', {
|
|
lvl
|
|
}),
|
|
fullHelp: (0, _i18N.t)('upgrades.sapper.fullHelp')
|
|
},
|
|
{
|
|
requires: "",
|
|
threshold: 11000,
|
|
id: "bigger_explosions",
|
|
giftable: false,
|
|
max: 1,
|
|
name: (0, _i18N.t)('upgrades.bigger_explosions.name'),
|
|
help: (lvl)=>(0, _i18N.t)('upgrades.bigger_explosions.help'),
|
|
fullHelp: (0, _i18N.t)('upgrades.bigger_explosions.fullHelp')
|
|
},
|
|
{
|
|
requires: "",
|
|
threshold: 13000,
|
|
giftable: false,
|
|
id: "extra_levels",
|
|
max: 3,
|
|
name: (0, _i18N.t)('upgrades.extra_levels.name'),
|
|
help: (lvl)=>(0, _i18N.t)('upgrades.extra_levels.help', {
|
|
count: lvl + 7
|
|
}),
|
|
fullHelp: (0, _i18N.t)('upgrades.extra_levels.fullHelp')
|
|
},
|
|
{
|
|
requires: "",
|
|
threshold: 15000,
|
|
giftable: false,
|
|
id: "pierce_color",
|
|
max: 1,
|
|
name: (0, _i18N.t)('upgrades.pierce_color.name'),
|
|
help: (lvl)=>(0, _i18N.t)('upgrades.pierce_color.help'),
|
|
fullHelp: (0, _i18N.t)('upgrades.pierce_color.fullHelp')
|
|
},
|
|
{
|
|
requires: "",
|
|
threshold: 18000,
|
|
giftable: false,
|
|
id: "soft_reset",
|
|
max: 2,
|
|
name: (0, _i18N.t)('upgrades.soft_reset.name'),
|
|
help: (lvl)=>(0, _i18N.t)('upgrades.soft_reset.help'),
|
|
fullHelp: (0, _i18N.t)('upgrades.soft_reset.fullHelp')
|
|
},
|
|
{
|
|
requires: "multiball",
|
|
threshold: 21000,
|
|
giftable: false,
|
|
id: "ball_repulse_ball",
|
|
max: 3,
|
|
name: (0, _i18N.t)('upgrades.ball_repulse_ball.name'),
|
|
help: (lvl)=>lvl == 1 ? (0, _i18N.t)('upgrades.ball_repulse_ball.help') : (0, _i18N.t)('upgrades.ball_repulse_ball.help_plural'),
|
|
fullHelp: (0, _i18N.t)('upgrades.ball_repulse_ball.fullHelp')
|
|
},
|
|
{
|
|
requires: "multiball",
|
|
threshold: 25000,
|
|
giftable: false,
|
|
id: "ball_attract_ball",
|
|
max: 3,
|
|
name: (0, _i18N.t)('upgrades.ball_attract_ball.name'),
|
|
help: (lvl)=>lvl == 1 ? (0, _i18N.t)('upgrades.ball_attract_ball.help') : (0, _i18N.t)('upgrades.ball_attract_ball.help_plural'),
|
|
fullHelp: (0, _i18N.t)('upgrades.ball_attract_ball.fullHelp')
|
|
},
|
|
{
|
|
requires: "",
|
|
threshold: 30000,
|
|
giftable: false,
|
|
id: "puck_repulse_ball",
|
|
max: 2,
|
|
name: (0, _i18N.t)('upgrades.puck_repulse_ball.name'),
|
|
help: (lvl)=>lvl == 1 ? (0, _i18N.t)('upgrades.puck_repulse_ball.help') : (0, _i18N.t)('upgrades.puck_repulse_ball.help_plural'),
|
|
fullHelp: (0, _i18N.t)('upgrades.puck_repulse_ball.fullHelp')
|
|
},
|
|
{
|
|
requires: "",
|
|
threshold: 35000,
|
|
giftable: false,
|
|
id: "wind",
|
|
max: 3,
|
|
name: (0, _i18N.t)('upgrades.wind.name'),
|
|
help: (lvl)=>lvl == 1 ? (0, _i18N.t)('upgrades.wind.help') : (0, _i18N.t)('upgrades.wind.help_plural'),
|
|
fullHelp: (0, _i18N.t)('upgrades.wind.fullHelp')
|
|
},
|
|
{
|
|
requires: "",
|
|
threshold: 40000,
|
|
giftable: false,
|
|
id: "sturdy_bricks",
|
|
max: 4,
|
|
name: (0, _i18N.t)('upgrades.telekinesis.name'),
|
|
help: (lvl)=>lvl == 1 ? (0, _i18N.t)('upgrades.telekinesis.help') : (0, _i18N.t)('upgrades.telekinesis.help_plural'),
|
|
fullHelp: (0, _i18N.t)('upgrades.telekinesis.fullHelp')
|
|
},
|
|
{
|
|
requires: "",
|
|
threshold: 45000,
|
|
giftable: false,
|
|
id: "respawn",
|
|
max: 4,
|
|
name: (0, _i18N.t)('upgrades.respawn.name'),
|
|
help: (lvl)=>lvl == 1 ? (0, _i18N.t)('upgrades.respawn.help') : (0, _i18N.t)('upgrades.respawn.help_plural'),
|
|
fullHelp: (0, _i18N.t)('upgrades.respawn.fullHelp')
|
|
},
|
|
{
|
|
requires: "",
|
|
threshold: 50000,
|
|
giftable: false,
|
|
id: "one_more_choice",
|
|
max: 3,
|
|
name: (0, _i18N.t)('upgrades.one_more_choice.name'),
|
|
help: (lvl)=>lvl == 1 ? (0, _i18N.t)('upgrades.one_more_choice.help') : (0, _i18N.t)('upgrades.one_more_choice.help_plural'),
|
|
fullHelp: (0, _i18N.t)('upgrades.one_more_choice.fullHelp')
|
|
},
|
|
{
|
|
requires: "",
|
|
threshold: 55000,
|
|
giftable: false,
|
|
id: "instant_upgrade",
|
|
max: 2,
|
|
name: (0, _i18N.t)('upgrades.instant_upgrade.name'),
|
|
help: (lvl)=>lvl == 1 ? (0, _i18N.t)('upgrades.instant_upgrade.help') : (0, _i18N.t)('upgrades.instant_upgrade.help_plural'),
|
|
fullHelp: (0, _i18N.t)('upgrades.instant_upgrade.fullHelp')
|
|
}
|
|
];
|
|
|
|
},{"@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3","./i18n/i18n":"eNPRm"}],"eNPRm":[function(require,module,exports,__globalThis) {
|
|
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
|
|
parcelHelpers.defineInteropFlag(exports);
|
|
parcelHelpers.export(exports, "t", ()=>t);
|
|
var _frJson = require("./fr.json");
|
|
var _frJsonDefault = parcelHelpers.interopDefault(_frJson);
|
|
var _enJson = require("./en.json");
|
|
var _enJsonDefault = parcelHelpers.interopDefault(_enJson);
|
|
var _settings = require("../settings");
|
|
const languages = {
|
|
fr: (0, _frJsonDefault.default),
|
|
en: (0, _enJsonDefault.default)
|
|
};
|
|
function t(key, params = {}) {
|
|
const lang = (0, _settings.getSettingValue)('lang', getFirstBrowserLanguage());
|
|
let template = languages[lang]?.[key] || languages.en[key];
|
|
for(let key in params)template = template.split('{{' + key + '}}').join(`${params[key]}`);
|
|
return template;
|
|
}
|
|
function getFirstBrowserLanguage() {
|
|
const preferred_languages = [
|
|
...navigator.languages,
|
|
navigator.language,
|
|
'en'
|
|
].filter((i)=>i).map((i)=>i.slice(0, 2).toLowerCase());
|
|
const supported = Object.keys(languages);
|
|
return preferred_languages.find((k)=>supported.includes(k)) || 'en';
|
|
}
|
|
|
|
},{"./fr.json":"b97sx","./en.json":"uYc9N","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3","../settings":"5blfu"}],"b97sx":[function(require,module,exports,__globalThis) {
|
|
module.exports = JSON.parse('{"appName":"Breakout 71","gameOver.cumulative_total":"","gameOver.lost.summary":"","gameOver.lost.title":"","gameOver.next_unlock":"","gameOver.restart":"","gameOver.stats.balls_lost":"","gameOver.stats.bricks_broken":"","gameOver.stats.bricks_per_minute":"","gameOver.stats.catch_rate":"","gameOver.stats.combo_avg":"","gameOver.stats.combo_max":"","gameOver.stats.duration_per_level":"","gameOver.stats.hit_rate":"","gameOver.stats.intro":"","gameOver.stats.level_reached":"","gameOver.stats.total_score":"","gameOver.stats.upgrades_applied":"","gameOver.test_run":"","gameOver.unlocked_count":"","gameOver.win.summary":"","gameOver.win.title":"","level_up.after_buttons":"","level_up.before_buttons":"","level_up.compliment_advice":"","level_up.compliment_good":"","level_up.compliment_perfect":"","level_up.pick_upgrade_title":"","level_up.plus_one_choice":"","level_up.plus_one_upgrade":"","level_up.unlocked_level":"","level_up.unlocked_perk":"","level_up.upgrade_perk_to_level":"","main_menu.basic":"","main_menu.basic_help":"","main_menu.footer_html":"","main_menu.fullscreen":"","main_menu.fullscreen_exit":"","main_menu.fullscreen_exit_help":"","main_menu.fullscreen_help":"","main_menu.kid":"","main_menu.kid_help":"","main_menu.mobile":"","main_menu.mobile_help":"","main_menu.pointer_lock":"","main_menu.pointer_lock_help":"","main_menu.record":"","main_menu.record_download":"","main_menu.record_help":"","main_menu.reset":"","main_menu.reset_cancel":"","main_menu.reset_confirm":"","main_menu.reset_help":"","main_menu.reset_instruction":"","main_menu.resume":"","main_menu.resume_help":"","main_menu.sounds":"","main_menu.sounds_help":"","main_menu.title":"","main_menu.unlocks":"","main_menu.unlocks_help":"","play.close_modale_window_tooltip":"","play.confirm_restart":"","play.confirm_restart_no":"","play.confirm_restart_yes":"","play.current_lvl":"","play.menu_label":"","play.missed_ball":"","play.mobile_press_to_play":"","sandbox.help":"","sandbox.instructions":"","sandbox.start":"","sandbox.title":"","sandbox.unlocks_at":"","score_panel.restart":"","score_panel.restart_help":"","score_panel.resume":"","score_panel.resume_help":"","score_panel.test_run":"","score_panel.title":"","score_panel.upgrades_picked":"","tagLine":"Cassez les briques, attrapez les pi\xe8ces d\'or et s\xe9lectionnez des am\xe9liorations.","unlocks.greyed_out_help":"","unlocks.intro":"","unlocks.level_description":"","unlocks.restart_cancel":"","unlocks.restart_confirm":"","unlocks.restart_text":"","unlocks.restart_title":"","unlocks.title":"","unlocks.unlocks_at":"","upgrades.ball_attract_ball.fullHelp":"","upgrades.ball_attract_ball.help":"","upgrades.ball_attract_ball.help_plural":"","upgrades.ball_attract_ball.name":"","upgrades.ball_repulse_ball.fullHelp":"","upgrades.ball_repulse_ball.help":"","upgrades.ball_repulse_ball.help_plural":"","upgrades.ball_repulse_ball.name":"","upgrades.base_combo.fullHelp":"","upgrades.base_combo.help":"","upgrades.base_combo.name":"","upgrades.bigger_explosions.fullHelp":"","upgrades.bigger_explosions.help":"","upgrades.bigger_explosions.name":"","upgrades.bigger_puck.fullHelp":"","upgrades.bigger_puck.help":"","upgrades.bigger_puck.name":"","upgrades.coin_magnet.fullHelp":"","upgrades.coin_magnet.help":"","upgrades.coin_magnet.help_plural":"","upgrades.coin_magnet.name":"","upgrades.compound_interest.fullHelp":"","upgrades.compound_interest.help":"","upgrades.compound_interest.name":"","upgrades.extra_levels.fullHelp":"","upgrades.extra_levels.help":"","upgrades.extra_levels.name":"","upgrades.extra_life.fullHelp":"","upgrades.extra_life.help":"","upgrades.extra_life.help_plural":"","upgrades.extra_life.name":"","upgrades.hot_start.fullHelp":"","upgrades.hot_start.help":"","upgrades.hot_start.name":"","upgrades.instant_upgrade.fullHelp":"","upgrades.instant_upgrade.help":"","upgrades.instant_upgrade.help_plural":"","upgrades.instant_upgrade.name":"","upgrades.left_is_lava.fullHelp":"","upgrades.left_is_lava.help":"","upgrades.left_is_lava.name":"","upgrades.metamorphosis.fullHelp":"","upgrades.metamorphosis.help":"","upgrades.metamorphosis.name":"","upgrades.multiball.fullHelp":"","upgrades.multiball.help":"","upgrades.multiball.name":"","upgrades.one_more_choice.fullHelp":"","upgrades.one_more_choice.help":"","upgrades.one_more_choice.help_plural":"","upgrades.one_more_choice.name":"","upgrades.picky_eater.fullHelp":"","upgrades.picky_eater.help":"","upgrades.picky_eater.name":"","upgrades.pierce.fullHelp":"","upgrades.pierce.help":"","upgrades.pierce.name":"","upgrades.pierce_color.fullHelp":"","upgrades.pierce_color.help":"","upgrades.pierce_color.name":"","upgrades.puck_repulse_ball.fullHelp":"","upgrades.puck_repulse_ball.help":"","upgrades.puck_repulse_ball.help_plural":"","upgrades.puck_repulse_ball.name":"","upgrades.respawn.fullHelp":"","upgrades.respawn.help":"","upgrades.respawn.help_plural":"","upgrades.respawn.name":"","upgrades.right_is_lava.fullHelp":"","upgrades.right_is_lava.help":"","upgrades.right_is_lava.name":"","upgrades.sapper.fullHelp":"","upgrades.sapper.help":"","upgrades.sapper.help_plural":"","upgrades.sapper.name":"","upgrades.skip_last.fullHelp":"","upgrades.skip_last.help":"","upgrades.skip_last.name":"","upgrades.slow_down.fullHelp":"","upgrades.slow_down.help":"","upgrades.slow_down.name":"","upgrades.smaller_puck.fullHelp":"","upgrades.smaller_puck.help":"","upgrades.smaller_puck.help_plural":"","upgrades.smaller_puck.name":"","upgrades.soft_reset.fullHelp":"","upgrades.soft_reset.help":"","upgrades.soft_reset.name":"","upgrades.streak_shots.fullHelp":"","upgrades.streak_shots.help":"","upgrades.streak_shots.name":"","upgrades.sturdy_bricks.fullHelp":"","upgrades.sturdy_bricks.help":"","upgrades.sturdy_bricks.help_plural":"","upgrades.sturdy_bricks.name":"","upgrades.telekinesis.fullHelp":"","upgrades.telekinesis.help":"","upgrades.telekinesis.help_plural":"","upgrades.telekinesis.name":"","upgrades.top_is_lava.fullHelp":"","upgrades.top_is_lava.help":"","upgrades.top_is_lava.name":"","upgrades.viscosity.fullHelp":"","upgrades.viscosity.help":"","upgrades.viscosity.name":"","upgrades.wind.fullHelp":"","upgrades.wind.help":"","upgrades.wind.help_plural":"","upgrades.wind.name":""}');
|
|
|
|
},{}],"uYc9N":[function(require,module,exports,__globalThis) {
|
|
module.exports = JSON.parse("{\"appName\":\"Breakout 71\",\"gameOver.cumulative_total\":\"Your total cumulative score went from {{startTs}} to {{endTs}}.\",\"gameOver.lost.summary\":\"You dropped the ball after catching {{score}} coins.\",\"gameOver.lost.title\":\"Game Over\",\"gameOver.next_unlock\":\"Score {{points}} more points to reach the next unlock\",\"gameOver.restart\":\"Start a new run\",\"gameOver.stats.balls_lost\":\"Balls lost\",\"gameOver.stats.bricks_broken\":\"Bricks broken\",\"gameOver.stats.bricks_per_minute\":\"Bricks broken per minute\",\"gameOver.stats.catch_rate\":\"Catch rate\",\"gameOver.stats.combo_avg\":\"Average combo\",\"gameOver.stats.combo_max\":\"Max combo\",\"gameOver.stats.duration_per_level\":\"Duration per level\",\"gameOver.stats.hit_rate\":\"Hit rate\",\"gameOver.stats.intro\":\"Find below your run statistics compared to your {{count}} best runs.\",\"gameOver.stats.level_reached\":\"Level reached\",\"gameOver.stats.total_score\":\"Total score\",\"gameOver.stats.upgrades_applied\":\"Upgrades applied\",\"gameOver.test_run\":\"This test run and its score are not being recorded\",\"gameOver.unlocked_count\":\"You unlocked {{count}} item(s) :\",\"gameOver.win.summary\":\"You cleared all levels for this run, catching {{score}} coins in total.\",\"gameOver.win.title\":\"Run finished\",\"level_up.after_buttons\":\"You just finished level {{level}}/{{max}} and picked those upgrades so far :\",\"level_up.before_buttons\":\"You caught {{score}} coins {{catchGain}} out of {{levelSpawnedCoins}} in {{time}} seconds ${timeGain}.\\n\\nYou missed {{levelMisses}} times {{missesGain}}.\\n\\n{{compliment}}\",\"level_up.compliment_advice\":\"Try to catch all coins, never miss the bricks or clear the level under 30s to gain additional choices and upgrades.\",\"level_up.compliment_good\":\"Well done !\",\"level_up.compliment_perfect\":\"Impressive, keep it up !\",\"level_up.pick_upgrade_title\":\"Pick an upgrade\",\"level_up.plus_one_choice\":\"(+1 choice)\",\"level_up.plus_one_upgrade\":\"(+1 upgrade and choice)\",\"level_up.unlocked_level\":\" (Level)\",\"level_up.unlocked_perk\":\" (Perk)\",\"level_up.upgrade_perk_to_level\":\" lvl {{level}}\",\"main_menu.basic\":\"Basic graphics\",\"main_menu.basic_help\":\"Fewer particles and flashes, better performance.\",\"main_menu.footer_html\":\" <p> <span>Made in France by <a href=\\\"https://lecaro.me\\\">Renan LE CARO</a>.</span> \\n <a href=\\\"https://breakout.lecaro.me/privacy.html\\\" target=\\\"_blank\\\">Privacy Policy</a>\\n <a href=\\\"https://f-droid.org/en/packages/me.lecaro.breakout/\\\" target=\\\"_blank\\\">F-Droid</a>\\n <a href=\\\"https://play.google.com/store/apps/details?id=me.lecaro.breakout\\\" target=\\\"_blank\\\">Google Play</a>\\n <a href=\\\"https://renanlecaro.itch.io/breakout71\\\" target=\\\"_blank\\\">itch.io</a> \\n <a href=\\\"https://gitlab.com/lecarore/breakout71\\\" target=\\\"_blank\\\">Gitlab</a>\\n <a href=\\\"https://breakout.lecaro.me/\\\" target=\\\"_blank\\\">Web version</a>\\n <a href=\\\"https://news.ycombinator.com/item?id=43183131\\\" target=\\\"_blank\\\">HackerNews</a>\\n <span>v.{{appVersion}}</span></p>\",\"main_menu.fullscreen\":\"Fullscreen\",\"main_menu.fullscreen_exit\":\"Exit Fullscreen\",\"main_menu.fullscreen_exit_help\":\"Might not work on some machines\",\"main_menu.fullscreen_help\":\"Might not work on some machines\",\"main_menu.kid\":\"Kids mode\",\"main_menu.kid_help\":\"Start future runs with \\\"slower ball\\\".\",\"main_menu.mobile\":\"Mobile mode\",\"main_menu.mobile_help\":\"Leaves space for your thumb under the puck.\",\"main_menu.pointer_lock\":\"Mouse pointer lock\",\"main_menu.pointer_lock_help\":\"Locks and hides the mouse cursor.\",\"main_menu.record\":\"Record gameplay videos\",\"main_menu.record_download\":\"Download video ({{size}} MB)\",\"main_menu.record_help\":\"Get a video of each level.\",\"main_menu.reset\":\"Reset Game\",\"main_menu.reset_cancel\":\"No\",\"main_menu.reset_confirm\":\"Yes\",\"main_menu.reset_help\":\"Erase high score and statistics\",\"main_menu.reset_instruction\":\"You will loose all progress you made in the game, are you sure ?\",\"main_menu.resume\":\"Resume\",\"main_menu.resume_help\":\"Return to your run\",\"main_menu.sounds\":\"Game sounds\",\"main_menu.sounds_help\":\"Can slow down some phones.\",\"main_menu.title\":\"Breakout 71\",\"main_menu.unlocks\":\"Starting perk\",\"main_menu.unlocks_help\":\"Try perks and levels you unlocked\",\"play.close_modale_window_tooltip\":\"close \",\"play.confirm_restart\":\"You pressed [R], restart game now ? \",\"play.confirm_restart_no\":\"No\",\"play.confirm_restart_yes\":\"Yes\",\"play.current_lvl\":\"L{{level}}/{{max}}\",\"play.menu_label\":\"menu\",\"play.missed_ball\":\"miss\",\"play.mobile_press_to_play\":\"Press and hold here to play\",\"sandbox.help\":\"Test any perk combination\",\"sandbox.instructions\":\"Select perks below and press \\\"start run\\\" to try them out in a test run. Scores and stats are not recorded.\",\"sandbox.start\":\"Start test run\",\"sandbox.title\":\"Sandbox mode\",\"sandbox.unlocks_at\":\"Unlocks at total score ${{score}}\",\"score_panel.restart\":\"Restart\",\"score_panel.restart_help\":\"Start a brand new run\",\"score_panel.resume\":\"Resume\",\"score_panel.resume_help\":\"Return to your run\",\"score_panel.test_run\":\"This is a test run, score is not recorded permanently\",\"score_panel.title\":\"{{score}} points at level {{level}}/{{max}} \",\"score_panel.upgrades_picked\":\"Upgrades picked so far : \",\"tagLine\":\"Break bricks, catch coins, upgrade, repeat.\",\"unlocks.greyed_out_help\":\"The greyed out ones can be unlocked by increasing your total score. The total score increases every time you score in game.\",\"unlocks.intro\":\"Your total score is {{ts}}. Below are all the upgrades and levels the games has to offer.\",\"unlocks.level_description\":\"A {{size}}x{{size}} level with {{bricks}} bricks\",\"unlocks.restart_cancel\":\"Cancel\",\"unlocks.restart_confirm\":\"Restart game to test item\",\"unlocks.restart_text\":\"You're about to start a new run with the selected unlocked item, is that really what you wanted ?\",\"unlocks.restart_title\":\"Restart run to try this item?\",\"unlocks.title\":\"You unlocked {{percentUnlock}}% of the game.\",\"unlocks.unlocks_at\":\"Unlocks at total score {{threshold}}.\",\"upgrades.ball_attract_ball.fullHelp\":\"Balls that are more than half a screen width away will start attracting each other. The attraction force is stronger when they are furthest away from each other.\\n Rainbow particles will fly to symbolize the attraction force. This perk is only offered if you have more than one ball already.\",\"upgrades.ball_attract_ball.help\":\"Balls attract balls\",\"upgrades.ball_attract_ball.help_plural\":\"Stronger attraction force\",\"upgrades.ball_attract_ball.name\":\"Gravity\",\"upgrades.ball_repulse_ball.fullHelp\":\"Balls that are less than half a screen width away will start repulsing each other. The repulsion force is stronger if they are close to each other.\\n Particles will jet out to symbolize this force being applied. This perk is only offered if you have more than one ball already.\",\"upgrades.ball_repulse_ball.help\":\"Balls repulse balls\",\"upgrades.ball_repulse_ball.help_plural\":\"Stronger repulsion force\",\"upgrades.ball_repulse_ball.name\":\"Personal space\",\"upgrades.base_combo.fullHelp\":\"Your combo (number of coins per bricks) normally starts at 1 at the beginning of the level, and resets to one when you bounce around without hitting anything. \\n With this perk, the combo starts 3 points higher, so you'll always get at least 4 coins per brick. Whenever your combo reset, it will go back to 4 and not 1. \\n Your ball will glitter a bit to indicate that its combo is higher than one.\",\"upgrades.base_combo.help\":\"Every brick drops at least {{}} coins.\",\"upgrades.base_combo.name\":\"+3 base combo\",\"upgrades.bigger_explosions.fullHelp\":\"The default explosion clears a 3x3 square, with this it becomes a 5x5 square, and the blow on the coins is also significantly stronger. The screen will flash after each explosion (except in basic mode)\",\"upgrades.bigger_explosions.help\":\"Bigger explosions\",\"upgrades.bigger_explosions.name\":\"Kaboom\",\"upgrades.bigger_puck.fullHelp\":\"A bigger puck makes it easier to never miss the ball and to catch more coins, and also to precisely angle the bounces (the ball's angle only depends on where it hits the puck). \\n However, a large puck is harder to use around the sides of the level, and will make it sometimes unavoidable to miss (not hit anything) which comes with downsides. \",\"upgrades.bigger_puck.help\":\"Easily catch more coins.\",\"upgrades.bigger_puck.name\":\"Bigger puck\",\"upgrades.coin_magnet.fullHelp\":\"Directs the coins to the puck. The effect is stronger if the coin is close to it already. Catching 90% or 100% of coins bring special bonuses in the game. \\n Another way to catch more coins is to hit bricks from the bottom. The ball's speed and direction impacts the spawned coin's velocity. \",\"upgrades.coin_magnet.help\":\"Puck attracts coins\",\"upgrades.coin_magnet.help_plural\":\"Stronger effect on the coins\",\"upgrades.coin_magnet.name\":\"Coins magnet\",\"upgrades.compound_interest.fullHelp\":\"Your combo will grow by one every time you break a brick, spawning more and more coin with every brick you break. Be sure however to catch every one of those coins\\n with your puck, as any lost coin will decrease your combo by one point. One your combo is above the minimum, the bottom of the play area will\\n have a red line to remind you that coins should not go there. This perk combines with other combo perks, the combo will rise faster but reset more easily.\",\"upgrades.compound_interest.help\":\"+1 combo per brick broken, resets on coin lost\",\"upgrades.compound_interest.name\":\"Compound interest\",\"upgrades.extra_levels.fullHelp\":\"The default run can last a max of 7 levels, after which the game is over and whatever score you reached is your run score. \\n Each level of this perk lets you go one level higher. The last levels are often the ones where you make the most score, so the difference can be dramatic.\",\"upgrades.extra_levels.help\":\"Play {{count}} levels instead of 7\",\"upgrades.extra_levels.name\":\"+1 level\",\"upgrades.extra_life.fullHelp\":\"Normally, you have one ball per run, and the run is over as soon as you drop it.\\n This perk adds a white bar at the bottom of the screen that will save a ball once, and break in the process. \\n You'll loose one level of that perk every time a ball bounces at the bottom of the screen.\",\"upgrades.extra_life.help\":\"The ball will bounce once on the bottom line before being lost.\",\"upgrades.extra_life.help_plural\":\"The ball will bounce on the bottom {{lvl}} times before being lost.\",\"upgrades.extra_life.name\":\"+1 life\",\"upgrades.hot_start.fullHelp\":\"At the start of every level, your combo will start at +15 points, but then every second it will be decreased by one. This means the first 15 seconds in a level will spawn\\n many more coins than the following ones, and you should make sure that you clear the level quickly. The effect stacks with other combo related perks, so you might be able to raise \\n the combo after the 15s timeout, but it will keep ticking down. Every time you take the perk again, the effect will be more dramatic.\",\"upgrades.hot_start.help\":\"Start at combo {{start}}, -{{lvl}} combo per second\",\"upgrades.hot_start.name\":\"Hot start\",\"upgrades.instant_upgrade.fullHelp\":\"Immediately pick two upgrades, so that you get one free one and one to repay the one used to get this perk. Every further menu to pick upgrades will have fewer options to choose from.\",\"upgrades.instant_upgrade.help\":\"-1 choice until run end.\",\"upgrades.instant_upgrade.help_plural\":\"Even fewer options\",\"upgrades.instant_upgrade.name\":\"+2 upgrades now\",\"upgrades.left_is_lava.fullHelp\":\"Whenever you break a brick, your combo will increase by one, so you'll get one more coin from all the next bricks you break.\\n However, your combo will reset as soon as your ball hits the left side . \\n As soon as your combo rises, the left side becomes red to remind you that you should avoid hitting them. \\n The effect stacks with other combo perks, combo rises faster with more upgrades but will also reset if any\\n of the reset conditions are met.\",\"upgrades.left_is_lava.help\":\"More coins if you don't touch the left side.\",\"upgrades.left_is_lava.name\":\"Avoid left side\",\"upgrades.metamorphosis.fullHelp\":\"With this perk, coins will be of the color of the brick they come from, and will color the first brick they touch in the same color. Coins spawn with the speed of the ball that broke them, which means you can aim a bit in the direction of the bricks you want to \\\"paint\\\".\",\"upgrades.metamorphosis.help\":\"Coins stain the bricks they touch\",\"upgrades.metamorphosis.name\":\"Metamorphosis\",\"upgrades.multiball.fullHelp\":\"As soon as you drop the ball in Breakout 71, you loose. With this perk, you get two balls, and so you can afford to lose one. \\n The lost balls come back on the next level. Having more than one balls makes some further perks available, and of course clears the level faster.\",\"upgrades.multiball.help\":\"Start every levels with {{count}} balls.\",\"upgrades.multiball.name\":\"+1 ball\",\"upgrades.one_more_choice.fullHelp\":\"Every upgrade menu will have one more option. Doesn't increase the number of upgrades you can pick.\",\"upgrades.one_more_choice.help\":\"Further level ups will offer one more option in the list\",\"upgrades.one_more_choice.help_plural\":\"Even more options\",\"upgrades.one_more_choice.name\":\"+1 choice until run end\",\"upgrades.picky_eater.fullHelp\":\"Whenever you break a brick the same color as your ball, your combo increases by one. \\n If it's a different color, the ball takes that new color, but the combo resets.\\n The bricks with the right color will get a white border. \\n Once you get a combo higher than your minimum, the bricks of the wrong color will get a red halo. \\n If you have more than one ball, they all change color whenever one of them hits a brick.\",\"upgrades.picky_eater.help\":\"More coins if you break bricks color by color.\",\"upgrades.picky_eater.name\":\"Picky eater\",\"upgrades.pierce.fullHelp\":\"The ball normally bounces as soon as it touches something. With this perk, it will continue its trajectory for up to 3 bricks broken. \\n After that, it will bounce on the 4th brick, and you'll need to touch the puck to reset the counter. This combines particularly well with Sapper. \",\"upgrades.pierce.help\":\"Ball pierces {{count}} bricks after a puck bounce\",\"upgrades.pierce.name\":\"Piercing\",\"upgrades.pierce_color.fullHelp\":\"Whenever a ball hits a brick of the same color, it will just go through unimpeded. \\n Once it reaches a brick of a different color, it will break it, take its color and bounce.\",\"upgrades.pierce_color.help\":\"Balls pierce bricks of their color\",\"upgrades.pierce_color.name\":\"Color pierce\",\"upgrades.puck_repulse_ball.fullHelp\":\"When a ball gets close to the puck, it will start slowing down, and even potentially bouncing without touching the puck.\",\"upgrades.puck_repulse_ball.help\":\"Puck repulses balls\",\"upgrades.puck_repulse_ball.help_plural\":\"Stronger repulsion force\",\"upgrades.puck_repulse_ball.name\":\"Soft landing\",\"upgrades.respawn.fullHelp\":\"After breaking two or more bricks, when the ball hits the puck, the first brick will be put back in place, provided that space is free and the brick wasn't a bomb.\\n Some particle effect will let you know where bricks will appear. Levelling this up lets you respawn up to 4 bricks at a time, but there should always be at least one destroyed.\",\"upgrades.respawn.help\":\"The first brick hit of two+ will respawn\",\"upgrades.respawn.help_plural\":\"More bricks can respawn\",\"upgrades.respawn.name\":\"Respawn\",\"upgrades.right_is_lava.fullHelp\":\"Whenever you break a brick, your combo will increase by one, so you'll get one more coin from all the next bricks you break.\\n However, your combo will reset as soon as your ball hits the right side . \\n As soon as your combo rises, the right side becomes red to remind you that you should avoid hitting them. \\n The effect stacks with other combo perks, combo rises faster with more upgrades but will also reset if any\\n of the reset conditions are met.\",\"upgrades.right_is_lava.help\":\"More coins if you don't touch the right side.\",\"upgrades.right_is_lava.name\":\"Avoid right side\",\"upgrades.sapper.fullHelp\":\"Instead of just disappearing, the first brick you break will be replaced by a bomb brick. Bouncing the ball on the puck re-arms the effect. \\\"Piercing\\\" will instantly\\n detonate the bomb that was just placed. Leveling-up this perk will allow you to place more bombs. Remember that bombs impact the velocity of nearby coins, so too many explosions\\n could make it hard to catch the fruits of your hard work.\",\"upgrades.sapper.help\":\"The first brick broken becomes a bomb.\",\"upgrades.sapper.help_plural\":\"The first {{lvl}} bricks broken become bombs.\",\"upgrades.sapper.name\":\"Sapper\",\"upgrades.skip_last.fullHelp\":\"You need to break all bricks to go to the next level. However, it can be hard to get the last ones. \\n Clearing a level early brings extra choices when upgrading. Never missing the bricks is also very beneficial. \\n So if you find it difficult to break the last bricks, getting this perk a few time can help.\",\"upgrades.skip_last.help\":\"The last {{lvl}} brick(s) left will self-destruct.\",\"upgrades.skip_last.name\":\"Easy Cleanup\",\"upgrades.slow_down.fullHelp\":\"The ball starts relatively slow, but every level of your run it will start a bit faster, and it will also accelerate if you spend a lot of time in one level. This perk makes it\\n more manageable. You can get it at the start every time by enabling kid mode in the menu.\",\"upgrades.slow_down.help\":\"Ball moves more slowly\",\"upgrades.slow_down.name\":\"Slower ball\",\"upgrades.smaller_puck.fullHelp\":\"This makes the puck smaller, which in theory makes some corner shots easier, but really just raises the difficulty. \\n That's why you also get a nice bonus of +5 coins per brick for all bricks you'll break after picking this. \",\"upgrades.smaller_puck.help\":\"Also gives +5 base combo\",\"upgrades.smaller_puck.help_plural\":\"Even smaller puck and higher base combo\",\"upgrades.smaller_puck.name\":\"Smaller puck\",\"upgrades.soft_reset.fullHelp\":\"The combo normally climbs every time you break a brick. This will sometimes cancel that climb, but also limit the impact of a combo reset.\",\"upgrades.soft_reset.help\":\"Combo grows slower but resets less\",\"upgrades.soft_reset.name\":\"Soft reset\",\"upgrades.streak_shots.fullHelp\":\"Every time you break a brick, your combo (number of coins per bricks) increases by one. However, as soon as the ball touches your puck, \\n the combo is reset to its default value, and you'll just get one coin per brick. So you should try to hit many bricks in one go for more score. \\n Once your combo rises above the base value, your puck will become red to remind you that it will destroy your combo to touch it with the ball.\\n This can stack with other combo related perks, the combo will rise faster but reset more easily as any of the conditions is enough to reset it. \",\"upgrades.streak_shots.help\":\"More coins if you break many bricks at once.\",\"upgrades.streak_shots.name\":\"Single puck hit streak\",\"upgrades.sturdy_bricks.fullHelp\":\"With level one of this perk, the ball has a 20% chance to bounce harmlessly on bricks, \\n but generates 10% more coins when it does break one. \\n This +10% is not shown in the combo number. At level 4 the ball has 80% chance of bouncing and brings 40% more coins.\",\"upgrades.sturdy_bricks.help\":\"Bricks sometimes resist hits but drop more coins.\",\"upgrades.sturdy_bricks.help_plural\":\"Bricks resist more and drop more coins\",\"upgrades.sturdy_bricks.name\":\"Sturdy bricks\",\"upgrades.telekinesis.fullHelp\":\"Right after the ball hits your puck, you'll be able to direct it left and right by moving your puck. \\n The effect stops when the ball hits a brick and resets the next time it touches the puck. It also does nothing when the ball is going downward after bouncing at the top.\",\"upgrades.telekinesis.help\":\"Control the ball's trajectory\",\"upgrades.telekinesis.help_plural\":\"Stronger effect on the ball\",\"upgrades.telekinesis.name\":\"Puck controls ball\",\"upgrades.top_is_lava.fullHelp\":\"Whenever you break a brick, your combo will increase by one. However, your combo will reset as soon as your ball hit the top of the screen. \\n When your combo is above the minimum, a red bar will appear at the top to remind you that you should avoid hitting it. \\n The effect stacks with other combo perks.\",\"upgrades.top_is_lava.help\":\"More coins if you don't touch the top.\",\"upgrades.top_is_lava.name\":\"Sky is the limit\",\"upgrades.viscosity.fullHelp\":\"Coins normally accelerate with gravity and explosions to pretty high speeds. This perk constantly makes them slow down, as if they were in some sort of viscous liquid. \\n This makes catching them easier, and combines nicely with perks that influence the coin's movement.\",\"upgrades.viscosity.help\":\"Slower coin fall\",\"upgrades.viscosity.name\":\"Viscosity\",\"upgrades.wind.fullHelp\":\"The wind depends on where your puck is, if it's in the center of the screen nothing happens, if it's on the left it will blow leftwise, if it's on the right of the screen\\n then it will blow rightwise. The wind affects both the balls and coins.\",\"upgrades.wind.help\":\"Puck position creates wind\",\"upgrades.wind.help_plural\":\"Stronger wind force\",\"upgrades.wind.name\":\"Wind\"}");
|
|
|
|
},{}],"5blfu":[function(require,module,exports,__globalThis) {
|
|
// Settings
|
|
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
|
|
parcelHelpers.defineInteropFlag(exports);
|
|
parcelHelpers.export(exports, "getSettingValue", ()=>getSettingValue);
|
|
parcelHelpers.export(exports, "setSettingValue", ()=>setSettingValue);
|
|
let cachedSettings = {};
|
|
function getSettingValue(key, defaultValue) {
|
|
if (typeof cachedSettings[key] == "undefined") try {
|
|
const ls = localStorage.getItem("breakout-settings-enable-" + key);
|
|
if (ls) cachedSettings[key] = JSON.parse(ls);
|
|
} catch (e) {
|
|
console.warn(e);
|
|
}
|
|
return cachedSettings[key] ?? defaultValue;
|
|
}
|
|
function setSettingValue(key, value) {
|
|
cachedSettings[key] = value;
|
|
try {
|
|
localStorage.setItem("breakout-settings-enable-" + key, JSON.stringify(value));
|
|
} catch (e) {
|
|
console.warn(e);
|
|
}
|
|
}
|
|
|
|
},{"@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"d5NoS":[function(require,module,exports,__globalThis) {
|
|
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
|
|
parcelHelpers.defineInteropFlag(exports);
|
|
parcelHelpers.export(exports, "options", ()=>options);
|
|
parcelHelpers.export(exports, "isOptionOn", ()=>isOptionOn);
|
|
parcelHelpers.export(exports, "toggleOption", ()=>toggleOption);
|
|
var _game = require("./game");
|
|
var _i18N = require("./i18n/i18n");
|
|
var _settings = require("./settings");
|
|
const options = {
|
|
sound: {
|
|
default: true,
|
|
name: (0, _i18N.t)('main_menu.sounds'),
|
|
help: (0, _i18N.t)('main_menu.sounds_help'),
|
|
afterChange: ()=>{},
|
|
disabled: ()=>false
|
|
},
|
|
"mobile-mode": {
|
|
default: window.innerHeight > window.innerWidth,
|
|
name: (0, _i18N.t)('main_menu.mobile'),
|
|
help: (0, _i18N.t)('main_menu.mobile_help'),
|
|
afterChange () {
|
|
(0, _game.fitSize)();
|
|
},
|
|
disabled: ()=>false
|
|
},
|
|
basic: {
|
|
default: false,
|
|
name: (0, _i18N.t)('main_menu.basic'),
|
|
help: (0, _i18N.t)('main_menu.basic_help'),
|
|
afterChange: ()=>{},
|
|
disabled: ()=>false
|
|
},
|
|
pointerLock: {
|
|
default: false,
|
|
name: (0, _i18N.t)('main_menu.pointer_lock'),
|
|
help: (0, _i18N.t)('main_menu.pointer_lock_help'),
|
|
afterChange: ()=>{},
|
|
disabled: ()=>!document.body.requestPointerLock
|
|
},
|
|
easy: {
|
|
default: false,
|
|
name: (0, _i18N.t)('main_menu.kid'),
|
|
help: (0, _i18N.t)('main_menu.kid_help'),
|
|
afterChange: ()=>{},
|
|
disabled: ()=>false
|
|
},
|
|
// Could not get the sharing to work without loading androidx and all the modern android things so for now I'll just disable sharing in the android app
|
|
record: {
|
|
default: false,
|
|
name: (0, _i18N.t)('main_menu.record'),
|
|
help: (0, _i18N.t)('main_menu.record_help'),
|
|
afterChange: ()=>{},
|
|
disabled () {
|
|
return window.location.search.includes("isInWebView=true");
|
|
}
|
|
}
|
|
};
|
|
function isOptionOn(key) {
|
|
return (0, _settings.getSettingValue)(key, options[key]?.default);
|
|
}
|
|
function toggleOption(key) {
|
|
(0, _settings.setSettingValue)(key, !isOptionOn(key));
|
|
options[key].afterChange();
|
|
}
|
|
|
|
},{"./game":"edeGs","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3","./i18n/i18n":"eNPRm","./settings":"5blfu"}],"dQKPV":[function(require,module,exports,__globalThis) {
|
|
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
|
|
parcelHelpers.defineInteropFlag(exports);
|
|
parcelHelpers.export(exports, "sounds", ()=>sounds);
|
|
parcelHelpers.export(exports, "getAudioContext", ()=>getAudioContext);
|
|
parcelHelpers.export(exports, "getAudioRecordingTrack", ()=>getAudioRecordingTrack);
|
|
var _game = require("./game");
|
|
var _options = require("./options");
|
|
const sounds = {
|
|
wallBeep: (pan)=>{
|
|
if (!(0, _options.isOptionOn)("sound")) return;
|
|
createSingleBounceSound(800, pixelsToPan(pan));
|
|
},
|
|
comboIncreaseMaybe: (combo, x, volume)=>{
|
|
if (!(0, _options.isOptionOn)("sound")) return;
|
|
let delta = 0;
|
|
if (!isNaN(lastComboPlayed)) {
|
|
if (lastComboPlayed < combo) delta = 1;
|
|
if (lastComboPlayed > combo) delta = -1;
|
|
}
|
|
playShepard(delta, pixelsToPan(x), volume);
|
|
lastComboPlayed = combo;
|
|
},
|
|
comboDecrease () {
|
|
if (!(0, _options.isOptionOn)("sound")) return;
|
|
playShepard(-1, 0.5, 0.5);
|
|
},
|
|
coinBounce: (pan, volume)=>{
|
|
if (!(0, _options.isOptionOn)("sound")) return;
|
|
createSingleBounceSound(1200, pixelsToPan(pan), volume, 0.1, "triangle");
|
|
},
|
|
explode: (pan)=>{
|
|
if (!(0, _options.isOptionOn)("sound")) return;
|
|
createExplosionSound(pixelsToPan(pan));
|
|
},
|
|
lifeLost (pan) {
|
|
if (!(0, _options.isOptionOn)("sound")) return;
|
|
createShatteredGlassSound(pixelsToPan(pan));
|
|
},
|
|
coinCatch (pan) {
|
|
if (!(0, _options.isOptionOn)("sound")) return;
|
|
createSingleBounceSound(900, pixelsToPan(pan), 0.8, 0.1, "triangle");
|
|
},
|
|
colorChange (pan, volume) {
|
|
createSingleBounceSound(400, pixelsToPan(pan), volume, 0.5, "sine");
|
|
createSingleBounceSound(800, pixelsToPan(pan), volume * 0.5, 0.2, "square");
|
|
}
|
|
};
|
|
// How to play the code on the leftconst context = new window.AudioContext();
|
|
let audioContext, audioRecordingTrack;
|
|
function getAudioContext() {
|
|
if (!audioContext) {
|
|
if (!(0, _options.isOptionOn)("sound")) return null;
|
|
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
audioRecordingTrack = audioContext.createMediaStreamDestination();
|
|
}
|
|
return audioContext;
|
|
}
|
|
function getAudioRecordingTrack() {
|
|
getAudioContext();
|
|
return audioRecordingTrack;
|
|
}
|
|
function createSingleBounceSound(baseFreq = 800, pan = 0.5, volume = 1, duration = 0.1, type = "sine") {
|
|
const context = getAudioContext();
|
|
if (!context) return;
|
|
const oscillator = createOscillator(context, baseFreq, type);
|
|
// Create a gain node to control the volume
|
|
const gainNode = context.createGain();
|
|
oscillator.connect(gainNode);
|
|
// Create a stereo panner node for left-right panning
|
|
const panner = context.createStereoPanner();
|
|
panner.pan.setValueAtTime(pan * 2 - 1, context.currentTime);
|
|
gainNode.connect(panner);
|
|
panner.connect(context.destination);
|
|
panner.connect(audioRecordingTrack);
|
|
// Set up the gain envelope to simulate the impact and quick decay
|
|
gainNode.gain.setValueAtTime(0.8 * volume, context.currentTime); // Initial impact
|
|
gainNode.gain.exponentialRampToValueAtTime(0.001, context.currentTime + duration); // Quick decay
|
|
// Start the oscillator
|
|
oscillator.start(context.currentTime);
|
|
// Stop the oscillator after the decay
|
|
oscillator.stop(context.currentTime + duration);
|
|
}
|
|
let noiseBuffer;
|
|
function getNoiseBuffer(context) {
|
|
if (!noiseBuffer) {
|
|
const bufferSize = context.sampleRate * 2; // 2 seconds
|
|
noiseBuffer = context.createBuffer(1, bufferSize, context.sampleRate);
|
|
const output = noiseBuffer.getChannelData(0);
|
|
// Fill the buffer with random noise
|
|
for(let i = 0; i < bufferSize; i++)output[i] = Math.random() * 2 - 1;
|
|
}
|
|
return noiseBuffer;
|
|
}
|
|
function createExplosionSound(pan = 0.5) {
|
|
const context = getAudioContext();
|
|
if (!context) return;
|
|
// Create an audio buffer
|
|
// Create a noise source
|
|
const noiseSource = context.createBufferSource();
|
|
noiseSource.buffer = getNoiseBuffer(context);
|
|
// Create a gain node to control the volume
|
|
const gainNode = context.createGain();
|
|
noiseSource.connect(gainNode);
|
|
// Create a filter to shape the explosion sound
|
|
const filter = context.createBiquadFilter();
|
|
filter.type = "lowpass";
|
|
filter.frequency.setValueAtTime(1000, context.currentTime); // Set the initial frequency
|
|
gainNode.connect(filter);
|
|
// Create a stereo panner node for left-right panning
|
|
const panner = context.createStereoPanner();
|
|
panner.pan.setValueAtTime(pan * 2 - 1, context.currentTime); // pan 0 to 1 maps to -1 to 1
|
|
// Connect filter to panner and then to the destination (speakers)
|
|
filter.connect(panner);
|
|
panner.connect(context.destination);
|
|
panner.connect(audioRecordingTrack);
|
|
// Ramp down the gain to simulate the explosion's fade-out
|
|
gainNode.gain.setValueAtTime(1, context.currentTime);
|
|
gainNode.gain.exponentialRampToValueAtTime(0.01, context.currentTime + 1);
|
|
// Lower the filter frequency over time to create the "explosive" effect
|
|
filter.frequency.exponentialRampToValueAtTime(60, context.currentTime + 1);
|
|
// Start the noise source
|
|
noiseSource.start(context.currentTime);
|
|
// Stop the noise source after the sound has played
|
|
noiseSource.stop(context.currentTime + 1);
|
|
}
|
|
function pixelsToPan(pan) {
|
|
return Math.max(0, Math.min(1, (pan - (0, _game.gameState).offsetXRoundedDown) / (0, _game.gameState).gameZoneWidthRoundedUp));
|
|
}
|
|
let lastComboPlayed = NaN, shepard = 6;
|
|
function playShepard(delta, pan, volume) {
|
|
const shepardMax = 11, factor = 1.05945594920268, baseNote = 392;
|
|
shepard += delta;
|
|
if (shepard > shepardMax) shepard = 0;
|
|
if (shepard < 0) shepard = shepardMax;
|
|
const play = (note)=>{
|
|
const freq = baseNote * Math.pow(factor, note);
|
|
const diff = Math.abs(note - shepardMax * 0.5);
|
|
const maxDistanceToIdeal = 1.5 * shepardMax;
|
|
const vol = Math.max(0, volume * (1 - diff / maxDistanceToIdeal));
|
|
createSingleBounceSound(freq, pan, vol);
|
|
return freq.toFixed(2) + " at " + Math.floor(vol * 100) + "% diff " + diff;
|
|
};
|
|
play(1 + shepardMax + shepard);
|
|
play(shepard);
|
|
play(-1 - shepardMax + shepard);
|
|
}
|
|
function createShatteredGlassSound(pan) {
|
|
const context = getAudioContext();
|
|
if (!context) return;
|
|
const oscillators = [
|
|
createOscillator(context, 3000, "square"),
|
|
createOscillator(context, 4500, "square"),
|
|
createOscillator(context, 6000, "square")
|
|
];
|
|
const gainNode = context.createGain();
|
|
const noiseSource = context.createBufferSource();
|
|
noiseSource.buffer = getNoiseBuffer(context);
|
|
oscillators.forEach((oscillator)=>oscillator.connect(gainNode));
|
|
noiseSource.connect(gainNode);
|
|
gainNode.gain.setValueAtTime(0.2, context.currentTime);
|
|
oscillators.forEach((oscillator)=>oscillator.start());
|
|
noiseSource.start();
|
|
oscillators.forEach((oscillator)=>oscillator.stop(context.currentTime + 0.2));
|
|
noiseSource.stop(context.currentTime + 0.2);
|
|
gainNode.gain.exponentialRampToValueAtTime(0.001, context.currentTime + 0.2);
|
|
// Create a stereo panner node for left-right panning
|
|
const panner = context.createStereoPanner();
|
|
panner.pan.setValueAtTime(pan * 2 - 1, context.currentTime);
|
|
gainNode.connect(panner);
|
|
panner.connect(context.destination);
|
|
panner.connect(audioRecordingTrack);
|
|
gainNode.connect(panner);
|
|
}
|
|
// Helper function to create an oscillator with a specific frequency
|
|
function createOscillator(context, frequency, type) {
|
|
const oscillator = context.createOscillator();
|
|
oscillator.type = type;
|
|
oscillator.frequency.setValueAtTime(frequency, context.currentTime);
|
|
return oscillator;
|
|
}
|
|
|
|
},{"./game":"edeGs","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3","./options":"d5NoS"}],"gVgfx":[function(require,module,exports,__globalThis) {
|
|
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
|
|
parcelHelpers.defineInteropFlag(exports);
|
|
parcelHelpers.export(exports, "resetBalls", ()=>resetBalls);
|
|
parcelHelpers.export(exports, "putBallsAtPuck", ()=>putBallsAtPuck);
|
|
var _gameUtils = require("./game_utils");
|
|
function resetBalls(gameState) {
|
|
const count = 1 + (gameState.perks?.multiball || 0);
|
|
const perBall = gameState.puckWidth / (count + 1);
|
|
gameState.balls = [];
|
|
gameState.ballsColor = "#FFF";
|
|
if (gameState.perks.picky_eater || gameState.perks.pierce_color) gameState.ballsColor = (0, _gameUtils.getMajorityValue)(gameState.bricks.filter((i)=>i)) || "#FFF";
|
|
for(let i = 0; i < count; i++){
|
|
const x = gameState.puckPosition - gameState.puckWidth / 2 + perBall * (i + 1);
|
|
const vx = Math.random() > 0.5 ? gameState.baseSpeed : -gameState.baseSpeed;
|
|
gameState.balls.push({
|
|
x,
|
|
previousX: x,
|
|
y: gameState.gameZoneHeight - 1.5 * gameState.ballSize,
|
|
previousY: gameState.gameZoneHeight - 1.5 * gameState.ballSize,
|
|
vx,
|
|
previousVX: vx,
|
|
vy: -gameState.baseSpeed,
|
|
previousVY: -gameState.baseSpeed,
|
|
sx: 0,
|
|
sy: 0,
|
|
sparks: 0,
|
|
piercedSinceBounce: 0,
|
|
hitSinceBounce: 0,
|
|
hitItem: [],
|
|
bouncesList: [],
|
|
sapperUses: 0
|
|
});
|
|
}
|
|
}
|
|
function putBallsAtPuck(gameState) {
|
|
// This reset could be abused to cheat quite easily
|
|
const count = gameState.balls.length;
|
|
const perBall = gameState.puckWidth / (count + 1);
|
|
gameState.balls.forEach((ball, i)=>{
|
|
const x = gameState.puckPosition - gameState.puckWidth / 2 + perBall * (i + 1);
|
|
ball.x = x;
|
|
ball.previousX = x;
|
|
ball.y = gameState.gameZoneHeight - 1.5 * gameState.ballSize;
|
|
ball.previousY = ball.y;
|
|
ball.vx = Math.random() > 0.5 ? gameState.baseSpeed : -gameState.baseSpeed;
|
|
ball.previousVX = ball.vx;
|
|
ball.vy = -gameState.baseSpeed;
|
|
ball.previousVY = ball.vy;
|
|
ball.sx = 0;
|
|
ball.sy = 0;
|
|
ball.hitItem = [];
|
|
ball.hitSinceBounce = 0;
|
|
ball.piercedSinceBounce = 0;
|
|
});
|
|
}
|
|
|
|
},{"./game_utils":"cEeac","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"cEeac":[function(require,module,exports,__globalThis) {
|
|
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
|
|
parcelHelpers.defineInteropFlag(exports);
|
|
parcelHelpers.export(exports, "getMajorityValue", ()=>getMajorityValue);
|
|
parcelHelpers.export(exports, "sample", ()=>sample);
|
|
parcelHelpers.export(exports, "sumOfKeys", ()=>sumOfKeys);
|
|
parcelHelpers.export(exports, "makeEmptyPerksMap", ()=>makeEmptyPerksMap);
|
|
function getMajorityValue(arr) {
|
|
const count = {};
|
|
arr.forEach((v)=>count[v] = (count[v] || 0) + 1);
|
|
// Object.values inline polyfill
|
|
const max = Math.max(...Object.keys(count).map((k)=>count[k]));
|
|
return sample(Object.keys(count).filter((k)=>count[k] == max));
|
|
}
|
|
function sample(arr) {
|
|
return arr[Math.floor(arr.length * Math.random())];
|
|
}
|
|
function sumOfKeys(obj) {
|
|
if (!obj) return 0;
|
|
return Object.values(obj)?.reduce((a, b)=>a + b, 0) || 0;
|
|
}
|
|
const makeEmptyPerksMap = (upgrades)=>{
|
|
const p = {};
|
|
upgrades.forEach((u)=>p[u.id] = 0);
|
|
return p;
|
|
};
|
|
|
|
},{"@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"9S1mS":[function(require,module,exports,__globalThis) {
|
|
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
|
|
parcelHelpers.defineInteropFlag(exports);
|
|
parcelHelpers.export(exports, "baseCombo", ()=>baseCombo);
|
|
parcelHelpers.export(exports, "resetCombo", ()=>resetCombo);
|
|
parcelHelpers.export(exports, "decreaseCombo", ()=>decreaseCombo);
|
|
var _sounds = require("./sounds");
|
|
function baseCombo(gameState) {
|
|
return 1 + gameState.perks.base_combo * 3 + gameState.perks.smaller_puck * 5;
|
|
}
|
|
function resetCombo(gameState, x, y) {
|
|
const prev = gameState.combo;
|
|
gameState.combo = baseCombo(gameState);
|
|
if (!gameState.levelTime) gameState.combo += gameState.perks.hot_start * 15;
|
|
if (prev > gameState.combo && gameState.perks.soft_reset) gameState.combo += Math.floor((prev - gameState.combo) / (1 + gameState.perks.soft_reset));
|
|
const lost = Math.max(0, prev - gameState.combo);
|
|
if (lost) {
|
|
for(let i = 0; i < lost && i < 8; i++)setTimeout(()=>(0, _sounds.sounds).comboDecrease(), i * 100);
|
|
if (typeof x !== "undefined" && typeof y !== "undefined") gameState.flashes.push({
|
|
type: "text",
|
|
text: "-" + lost,
|
|
time: gameState.levelTime,
|
|
color: "red",
|
|
x: x,
|
|
y: y,
|
|
duration: 150,
|
|
size: gameState.puckHeight
|
|
});
|
|
}
|
|
return lost;
|
|
}
|
|
function decreaseCombo(gameState, by, x, y) {
|
|
const prev = gameState.combo;
|
|
gameState.combo = Math.max(baseCombo(gameState), gameState.combo - by);
|
|
const lost = Math.max(0, prev - gameState.combo);
|
|
if (lost) {
|
|
(0, _sounds.sounds).comboDecrease();
|
|
if (typeof x !== "undefined" && typeof y !== "undefined") gameState.flashes.push({
|
|
type: "text",
|
|
text: "-" + lost,
|
|
time: gameState.levelTime,
|
|
color: "red",
|
|
x: x,
|
|
y: y,
|
|
duration: 300,
|
|
size: gameState.puckHeight
|
|
});
|
|
}
|
|
}
|
|
|
|
},{"./sounds":"dQKPV","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"kRstf":[function(require,module,exports,__globalThis) {
|
|
if ("serviceWorker" in navigator && window.location.search.includes("isPWA=true")) // @ts-ignore
|
|
navigator.serviceWorker.register(require("994b1835e761d5ae"));
|
|
|
|
},{"994b1835e761d5ae":"6vb6r"}],"6vb6r":[function(require,module,exports,__globalThis) {
|
|
module.exports = require("e4f4efefa01a2f07").getBundleURL('28aWT') + "sw-b71.js";
|
|
|
|
},{"e4f4efefa01a2f07":"lgJ39"}]},["fxnks","bCo5X"], "bCo5X", "parcelRequire94c2")
|
|
|
|
</script>
|
|
</body>
|
|
</html>
|