diff --git a/Readme.md b/Readme.md
index 995d29b..d391c31 100644
--- a/Readme.md
+++ b/Readme.md
@@ -41,16 +41,7 @@ perks. No video though, this happened on the app while I was playing
casually (it's quite easy to reproduce, though). I wouldn't do that
because it's mind-numbingly dull, but still I'm not sure this kind of
play is intended or if it should even be allowed.
-- Combo balancing – one thing that makes the best strategy overly
-dominant is the way combo resets abruptly with everything other than
-Compound Interest, which pairs exceptionally well to Coin Magnet (which
-is in itself instrumental to scoring high). If the other combos would
-scale down accordingly (i.e. 1 coin less when you hit the
-walls/ceiling/puck – or at least drop some percentage, but not all of
-the combo at once). This would instantly make many more strategies and
-combinations viable, because right now it doesn't make much sense to
-pair Compound Interest to any other combo perks (except in very specific
-circumstances).
+
# Game engine features
@@ -96,7 +87,7 @@ circumstances).
- when missing, redo particle trail, but give speed to particle that matches ball direction
# Perks ideas
-
+- Combo balancing : make Compound Interest less OP by defaulting to soft reset for others, or by making it loose more for each missed coin
- second puck (symmeric to the first one)
- keep combo between level, loose half your run score when missing any bricks
- offer next level choice after upgrade pick
diff --git a/deploy.sh b/deploy.sh
index bbc0bc7..1750c76 100755
--- a/deploy.sh
+++ b/deploy.sh
@@ -27,6 +27,8 @@ echo "\"$versionCode\"" > src/version.json
# remove all exif metadata from pictures, because i think fdroid doesn't like that. odd
find -name '*.jp*g' -o -name '*.png' | xargs exiftool -all=
+npx prettier --write src/
+
npm run build
rm -rf ./app/src/main/assets/*
diff --git a/dist/index.html b/dist/index.html
index 57a4a7d..1691ef8 100644
--- a/dist/index.html
+++ b/dist/index.html
@@ -313,13 +313,13 @@ h2.histogram-title strong {
color: #4049ca;
}
-
+
-
diff --git a/src/game.ts b/src/game.ts
index a1a81f0..5c0a1d5 100644
--- a/src/game.ts
+++ b/src/game.ts
@@ -1,6 +1,5 @@
import {allLevels, appVersion, icons, upgrades} from "./loadGameData";
-import {PerkId} from "./types";
-
+import {Ball, Coin, colorString, Flash, FlashTypes, Level, PerkId} from "./types";
const MAX_COINS = 400;
const MAX_PARTICLES = 600;
@@ -11,18 +10,24 @@ let ballSize = 20;
const coinSize = Math.round(ballSize * 0.8);
const puckHeight = ballSize;
-
allLevels.forEach((l, li) => {
- l.threshold = li < 8 ? 0 : Math.round(Math.min(Math.pow(10, 1 + (li + l.size) / 30) * 10, 5000) * (li))
- l.sortKey = (Math.random() + 3) / 3.5 * l.bricks.filter(i => i).length
-})
+ l.threshold =
+ li < 8
+ ? 0
+ : Math.round(
+ Math.min(Math.pow(10, 1 + (li + l.size) / 30) * 10, 5000) * li,
+ );
+ l.sortKey = ((Math.random() + 3) / 3.5) * l.bricks.filter((i) => i).length;
+});
-let runLevels = []
+let runLevels: Level[] = [];
let currentLevel = 0;
-const bombSVG = document.createElement('img')
-bombSVG.src = 'data:image/svg+xml;base64,' + btoa(`
+const bombSVG = document.createElement("img");
+bombSVG.src =
+ "data:image/svg+xml;base64," +
+ btoa(`
`);
@@ -45,11 +50,10 @@ function resetCombo(x: number | undefined, y: number | undefined) {
combo += perks.hot_start * 15;
}
if (prev > combo && perks.soft_reset) {
- combo += Math.floor((prev - combo) / (1 + perks.soft_reset))
+ combo += Math.floor((prev - combo) / (1 + perks.soft_reset));
}
const lost = Math.max(0, prev - combo);
if (lost) {
-
for (let i = 0; i < lost && i < 8; i++) {
setTimeout(() => sounds.comboDecrease(), i * 100);
}
@@ -58,7 +62,7 @@ function resetCombo(x: number | undefined, y: number | undefined) {
type: "text",
text: "-" + lost,
time: levelTime,
- color: "r",
+ color: "red",
x: x,
y: y,
duration: 150,
@@ -66,7 +70,7 @@ function resetCombo(x: number | undefined, y: number | undefined) {
});
}
}
- return lost
+ return lost;
}
function decreaseCombo(by: number, x: number, y: number) {
@@ -81,7 +85,7 @@ function decreaseCombo(by: number, x: number, y: number) {
type: "text",
text: "-" + lost,
time: levelTime,
- color: "r",
+ color: "red",
x: x,
y: y,
duration: 300,
@@ -93,113 +97,125 @@ function decreaseCombo(by: number, x: number, y: number) {
let gridSize = 12;
-let running = false, puck = 400, pauseTimeout: number | null = null;
+let running = false,
+ puck = 400,
+ pauseTimeout: number | null = null;
function play() {
- if (running) return
- running = true
+ if (running) return;
+ running = true;
if (audioContext) {
- audioContext.resume()
+ audioContext.resume();
}
- resumeRecording()
+ resumeRecording();
}
-function pause(playerAskedForPause) {
- if (!running) return
- if (pauseTimeout) return
+function pause(playerAskedForPause: boolean) {
+ if (!running) return;
+ if (pauseTimeout) return;
- pauseTimeout = setTimeout(() => {
- running = false
- needsRender = true
- if (audioContext) {
- setTimeout(() => {
- if (!running) audioContext.suspend()
- }, 1000)
- }
- pauseRecording()
- pauseTimeout = null
- }, Math.min(Math.max(0, pauseUsesDuringRun - 5) * 50, 500))
+ pauseTimeout = setTimeout(
+ () => {
+ running = false;
+ needsRender = true;
+ if (audioContext) {
+ setTimeout(() => {
+ if (!running) audioContext.suspend();
+ }, 1000);
+ }
+ pauseRecording();
+ pauseTimeout = null;
+ },
+ Math.min(Math.max(0, pauseUsesDuringRun - 5) * 50, 500),
+ );
if (playerAskedForPause) {
// Pausing many times in a run will make pause slower
- pauseUsesDuringRun++
+ pauseUsesDuringRun++;
}
if (document.exitPointerLock) {
- document.exitPointerLock()
+ document.exitPointerLock();
}
-
-
}
-let offsetX: number, offsetXRoundedDown: number, gameZoneWidth: number, gameZoneWidthRoundedUp: number,
- gameZoneHeight: number, brickWidth: number, needsRender = true;
+let offsetX: number,
+ offsetXRoundedDown: number,
+ gameZoneWidth: number,
+ gameZoneWidthRoundedUp: number,
+ gameZoneHeight: number,
+ brickWidth: number,
+ needsRender = true;
const background = document.createElement("img");
const backgroundCanvas = document.createElement("canvas");
background.addEventListener("load", () => {
- needsRender = true
-})
-
+ needsRender = true;
+});
const fitSize = () => {
const {width, height} = canvas.getBoundingClientRect();
canvas.width = width;
canvas.height = height;
- ctx.fillStyle = currentLevelInfo()?.color || 'black'
- ctx.globalAlpha = 1
- ctx.fillRect(0, 0, width, height)
+ ctx.fillStyle = currentLevelInfo()?.color || "black";
+ ctx.globalAlpha = 1;
+ ctx.fillRect(0, 0, width, height);
backgroundCanvas.width = width;
backgroundCanvas.height = height;
-
gameZoneHeight = isSettingOn("mobile-mode") ? (height * 80) / 100 : height;
const baseWidth = Math.round(Math.min(canvas.width, gameZoneHeight * 0.73));
brickWidth = Math.floor(baseWidth / gridSize / 2) * 2;
gameZoneWidth = brickWidth * gridSize;
offsetX = Math.floor((canvas.width - gameZoneWidth) / 2);
- offsetXRoundedDown = offsetX
- if (offsetX < ballSize) offsetXRoundedDown = 0
- gameZoneWidthRoundedUp = width - 2 * offsetXRoundedDown
- backgroundCanvas.title = 'resized'
+ offsetXRoundedDown = offsetX;
+ if (offsetX < ballSize) offsetXRoundedDown = 0;
+ gameZoneWidthRoundedUp = width - 2 * offsetXRoundedDown;
+ backgroundCanvas.title = "resized";
// Ensure puck stays within bounds
setMousePos(puck);
coins = [];
flashes = [];
- pause(true)
+ pause(true);
putBallsAtPuck();
// For safari mobile https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
- document.documentElement.style.setProperty('--vh', `${window.innerHeight * 0.01}px`);
+ document.documentElement.style.setProperty(
+ "--vh",
+ `${window.innerHeight * 0.01}px`,
+ );
};
window.addEventListener("resize", fitSize);
window.addEventListener("fullscreenchange", fitSize);
function recomputeTargetBaseSpeed() {
// We never want the ball to completely stop, it will move at least 3px per frame
- baseSpeed = Math.max(3, gameZoneWidth / 12 / 10 + currentLevel / 3 + levelTime / (30 * 1000) - perks.slow_down * 2);
+ baseSpeed = Math.max(
+ 3,
+ gameZoneWidth / 12 / 10 +
+ currentLevel / 3 +
+ levelTime / (30 * 1000) -
+ perks.slow_down * 2,
+ );
}
-
-function brickCenterX(index) {
+function brickCenterX(index: number) {
return offsetX + ((index % gridSize) + 0.5) * brickWidth;
}
-function brickCenterY(index) {
+function brickCenterY(index: number) {
return (Math.floor(index / gridSize) + 0.5) * brickWidth;
}
-
-function getRowColIndex(row, col) {
+function getRowColIndex(row: number, col: number) {
if (row < 0 || col < 0 || row >= gridSize || col >= gridSize) return -1;
return row * gridSize + col;
}
-
-function spawnExplosion(count, x, y, color, duration = 150, size = coinSize) {
+function spawnExplosion(count: number, x: number, y: number, color: string, duration = 150, size = coinSize) {
if (!!isSettingOn("basic")) return;
if (flashes.length > MAX_PARTICLES) {
// Avoid freezing when lots of explosion happen at once
- count = 1
+ count = 1;
}
for (let i = 0; i < count; i++) {
flashes.push({
@@ -211,28 +227,27 @@ function spawnExplosion(count, x, y, color, duration = 150, size = coinSize) {
vx: (Math.random() - 0.5) * 30,
vy: (Math.random() - 0.5) * 30,
color,
- duration: 150
+ duration,
});
}
}
-
let score = 0;
-let lastexplosion = 0;
+let lastExplosion = 0;
let highScore = parseFloat(localStorage.getItem("breakout-3-hs") || "0");
-let lastPlayedCoinGrab = 0
+let lastPlayedCoinGrab = 0;
-function addToScore(coin) {
- coin.destroyed = true
+function addToScore(coin: Coin) {
+ coin.destroyed = true;
score += coin.points;
- addToTotalScore(coin.points)
+ addToTotalScore(coin.points);
if (score > highScore) {
highScore = score;
localStorage.setItem("breakout-3-hs", score.toString());
}
- if (!isSettingOn('basic')) {
+ if (!isSettingOn("basic")) {
flashes.push({
type: "particle",
duration: 100 + Math.random() * 50,
@@ -244,26 +259,24 @@ function addToScore(coin) {
vx: (canvas.width - coin.x) / 100,
vy: -coin.y / 100,
ethereal: true,
- })
+ });
}
if (Date.now() - lastPlayedCoinGrab > 16) {
- lastPlayedCoinGrab = Date.now()
- sounds.coinCatch(coin.x)
+ lastPlayedCoinGrab = Date.now();
+ sounds.coinCatch(coin.x);
}
- runStatistics.score += coin.points
-
-
+ runStatistics.score += coin.points;
}
-let balls = [];
-let ballsColor = 'white'
+let balls: Ball[] = [];
+let ballsColor:colorString = "white" ;
function resetBalls() {
const count = 1 + (perks?.multiball || 0);
const perBall = puckWidth / (count + 1);
balls = [];
- ballsColor = "#FFF"
+ ballsColor = "#FFF";
for (let i = 0; i < count; i++) {
const x = puck - puckWidth / 2 + perBall * (i + 1);
balls.push({
@@ -290,104 +303,107 @@ function putBallsAtPuck() {
const perBall = puckWidth / (count + 1);
balls.forEach((ball, i) => {
const x = puck - puckWidth / 2 + perBall * (i + 1);
- ball.x = x
- ball.previousx = x
- ball.y = gameZoneHeight - 1.5 * ballSize
- ball.previousy = ball.y
- ball.vx = Math.random() > 0.5 ? baseSpeed : -baseSpeed
- ball.vy = -baseSpeed
- ball.sx = 0
- ball.sy = 0
- ball.hitItem = []
- ball.hitSinceBounce = 0
- ball.piercedSinceBounce = 0
+ ball.x = x;
+ ball.previousx = x;
+ ball.y = gameZoneHeight - 1.5 * ballSize;
+ ball.previousy = ball.y;
+ ball.vx = Math.random() > 0.5 ? baseSpeed : -baseSpeed;
+ ball.vy = -baseSpeed;
+ ball.sx = 0;
+ ball.sy = 0;
+ ball.hitItem = [];
+ ball.hitSinceBounce = 0;
+ ball.piercedSinceBounce = 0;
});
}
resetBalls();
// Default, recomputed at each level load
-let bricks = [];
-let flashes = [];
-let coins = [];
+let bricks : colorString[] = [];
+let flashes :Flash[] = [];
+let coins: Coin[] = [];
let levelStartScore = 0;
let levelMisses = 0;
let levelSpawnedCoins = 0;
-
function pickedUpgradesHTMl() {
- let list = ''
+ let list = "";
for (let u of upgrades) {
- for (let i = 0; i < perks[u.id]; i++) list += icons['icon:' + u.id] + ' '
+ for (let i = 0; i < perks[u.id]; i++) list += icons["icon:" + u.id] + " ";
}
- return list
+ return list;
}
async function openUpgradesPicker() {
-
const catchRate = (score - levelStartScore) / (levelSpawnedCoins || 1);
let repeats = 1;
let choices = 3;
- let timeGain = '', catchGain = '', missesGain = ''
+ let timeGain = "",
+ catchGain = "",
+ missesGain = "";
if (levelTime < 30 * 1000) {
repeats++;
choices++;
- timeGain = " (+1 upgrade and choice)"
+ timeGain = " (+1 upgrade and choice)";
} else if (levelTime < 60 * 1000) {
choices++;
- timeGain = " (+1 choice)"
+ timeGain = " (+1 choice)";
}
if (catchRate === 1) {
repeats++;
choices++;
- catchGain = " (+1 upgrade and choice)"
+ catchGain = " (+1 upgrade and choice)";
} else if (catchRate > 0.9) {
choices++;
- catchGain = " (+1 choice)"
+ catchGain = " (+1 choice)";
}
if (levelMisses === 0) {
repeats++;
choices++;
- missesGain = " (+1 upgrade and choice)"
+ missesGain = " (+1 upgrade and choice)";
} else if (levelMisses <= 3) {
choices++;
- missesGain = " (+1 choice)"
+ missesGain = " (+1 choice)";
}
-
while (repeats--) {
- const actions = pickRandomUpgrades(choices + perks.one_more_choice - perks.instant_upgrade);
- if (!actions.length) break
+ const actions = pickRandomUpgrades(
+ choices + perks.one_more_choice - perks.instant_upgrade,
+ );
+ if (!actions.length) break;
let textAfterButtons = `
You just finished level ${currentLevel + 1}/${max_levels()} and picked those upgrades so far :
${pickedUpgradesHTMl()}
`;
-
- const upgradeId = await asyncAlert({
- title: "Pick an upgrade " + (repeats ? "(" + (repeats + 1) + ")" : ""), actions, text: `
+ const upgradeId = (await asyncAlert({
+ title: "Pick an upgrade " + (repeats ? "(" + (repeats + 1) + ")" : ""),
+ actions,
+ text: `
You caught ${score - levelStartScore} coins ${catchGain} out of ${levelSpawnedCoins} in ${Math.round(levelTime / 1000)} seconds${timeGain}.
You missed ${levelMisses} times ${missesGain}.
- ${((timeGain && catchGain && missesGain) && 'Impressive, keep it up !') || ((timeGain || catchGain || missesGain) && 'Well done !') || 'Try to catch all coins, never miss the bricks or clear the level under 30s to gain additional choices and upgrades.'}
-
`, allowClose: false, textAfterButtons
- });
+ ${(timeGain && catchGain && missesGain && "Impressive, keep it up !") || ((timeGain || catchGain || missesGain) && "Well done !") || "Try to catch all coins, never miss the bricks or clear the level under 30s to gain additional choices and upgrades."}
+
`,
+ allowClose: false,
+ textAfterButtons,
+ })) as PerkId;
+
perks[upgradeId]++;
- if (upgradeId === 'instant_upgrade') {
- repeats += 2
+ if (upgradeId === "instant_upgrade") {
+ repeats += 2;
}
- runStatistics.upgrades_picked++
+ runStatistics.upgrades_picked++;
}
resetCombo(undefined, undefined);
resetBalls();
}
function setLevel(l) {
-
-
- pause(false)
+ pause(false);
if (l > 0) {
openUpgradesPicker().then();
}
@@ -398,7 +414,7 @@ function setLevel(l) {
levelStartScore = score;
levelSpawnedCoins = 0;
levelMisses = 0;
- runStatistics.levelsPlayed++
+ runStatistics.levelsPlayed++;
resetCombo(undefined, undefined);
recomputeTargetBaseSpeed();
@@ -415,9 +431,9 @@ function setLevel(l) {
// 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
- stopRecording()
- startRecordingGame()
+ background.src = "data:image/svg+xml;UTF8," + lvl.svg;
+ stopRecording();
+ startRecordingGame();
}
function currentLevelInfo() {
@@ -425,129 +441,121 @@ function currentLevelInfo() {
}
function reset_perks() {
-
for (let u of upgrades) {
perks[u.id] = 0;
}
- if (nextRunOverrides.perks) {
- const first = Object.keys(nextRunOverrides.perks)[0]
- Object.assign(perks, nextRunOverrides.perks)
- nextRunOverrides.perks = null
- return first
- }
-
- const giftable = getPossibleUpgrades().filter(u => u.giftable)
- const randomGift = isSettingOn('easy') ? 'slow_down' : giftable[Math.floor(Math.random() * giftable.length)].id;
+ const giftable = getPossibleUpgrades().filter((u) => u.giftable);
+ const randomGift =
+ nextRunOverrides?.perk || isSettingOn("easy")
+ ? "slow_down"
+ : giftable[Math.floor(Math.random() * giftable.length)].id;
perks[randomGift] = 1;
- return randomGift
+ delete nextRunOverrides.perk;
+ return randomGift;
}
-
-let totalScoreAtRunStart = getTotalScore()
+let totalScoreAtRunStart = getTotalScore();
function getPossibleUpgrades() {
return upgrades
- .filter(u => totalScoreAtRunStart >= u.threshold)
- .filter(u => !u?.requires || perks[u?.requires])
+ .filter((u) => totalScoreAtRunStart >= u.threshold)
+ .filter((u) => !u?.requires || perks[u?.requires]);
}
-
function shuffleLevels(nameToAvoid = null) {
const target = nextRunOverrides?.level;
- const firstLevel = nextRunOverrides?.level ?
- allLevels.filter(l => l.name === target) : []
+ const firstLevel = nextRunOverrides?.level
+ ? allLevels.filter((l) => l.name === target)
+ : [];
const restInRandomOrder = allLevels
- .filter((l, li) => totalScoreAtRunStart >= l.threshold)
- .filter(l => l.name !== nextRunOverrides?.level)
- .filter(l => l.name !== nameToAvoid || allLevels.length === 1)
- .sort(() => Math.random() - 0.5)
+ .filter((l) => totalScoreAtRunStart >= l.threshold)
+ .filter((l) => l.name !== nextRunOverrides?.level)
+ .filter((l) => l.name !== nameToAvoid || allLevels.length === 1)
+ .sort(() => Math.random() - 0.5);
runLevels = firstLevel.concat(
- restInRandomOrder.slice(0, 7 + 3)
- .sort((a, b) => a.sortKey - b.sortKey)
- )
-
+ restInRandomOrder.slice(0, 7 + 3).sort((a, b) => a.sortKey - b.sortKey),
+ );
}
function getUpgraderUnlockPoints() {
- let list = []
+ let list = [];
- upgrades
- .forEach(u => {
- if (u.threshold) {
- list.push({
- threshold: u.threshold, title: u.name + ' (Perk)'
- })
- }
- })
+ upgrades.forEach((u) => {
+ if (u.threshold) {
+ list.push({
+ threshold: u.threshold,
+ title: u.name + " (Perk)",
+ });
+ }
+ });
- allLevels.forEach((l, li) => {
+ allLevels.forEach((l) => {
list.push({
- threshold: l.threshold, title: l.name + ' (Level)',
- })
- })
+ threshold: l.threshold,
+ title: l.name + " (Level)",
+ });
+ });
- return list.filter(o => o.threshold).sort((a, b) => a.threshold - b.threshold)
+ return list
+ .filter((o) => o.threshold)
+ .sort((a, b) => a.threshold - b.threshold);
}
-
-let lastOffered = {}
+let lastOffered = {};
function dontOfferTooSoon(id) {
- lastOffered[id] = Math.round(Date.now() / 1000)
+ lastOffered[id] = Math.round(Date.now() / 1000);
}
-function pickRandomUpgrades(count) {
-
+function pickRandomUpgrades(count: number) {
let list = getPossibleUpgrades()
- .map(u => ({...u, score: Math.random() + (lastOffered[u.id] || 0)}))
+ .map((u) => ({...u, score: Math.random() + (lastOffered[u.id] || 0)}))
.sort((a, b) => a.score - b.score)
- .filter(u => perks[u.id] < u.max)
+ .filter((u) => perks[u.id] < u.max)
.slice(0, count)
- .sort((a, b) => a.id > b.id ? 1 : -1)
-
- list.forEach(u => {
- dontOfferTooSoon(u.id)
- })
-
- return list.map(u => ({
- text: u.name + (perks[u.id] ? ' lvl ' + (perks[u.id] + 1) : ''),
- icon: icons['icon:' + u.id],
- value: u.id,
- help: u.help(perks[u.id] + 1), // max: u.max,
- // checked: perks[u.id]
- }))
+ .sort((a, b) => (a.id > b.id ? 1 : -1));
+ list.forEach((u) => {
+ dontOfferTooSoon(u.id);
+ });
+ return list.map((u) => ({
+ text: u.name + (perks[u.id] ? " lvl " + (perks[u.id] + 1) : ""),
+ icon: icons["icon:" + u.id],
+ value: u.id as PerkId,
+ help: u.help(perks[u.id] + 1),
+ }));
}
-let nextRunOverrides = {level: null, perks: null}
-let pauseUsesDuringRun = 0
+type RunOverrides = { level?: PerkId; perk?: string };
+
+let nextRunOverrides = {} as RunOverrides;
+let pauseUsesDuringRun = 0;
function restart() {
// When restarting, we want to avoid restarting with the same level we're on, so we exclude from the next
// run's level list
- totalScoreAtRunStart = getTotalScore()
+ totalScoreAtRunStart = getTotalScore();
shuffleLevels(levelTime || score ? currentLevelInfo().name : null);
- resetRunStatistics()
+ resetRunStatistics();
score = 0;
- pauseUsesDuringRun = 0
+ pauseUsesDuringRun = 0;
const randomGift = reset_perks();
- dontOfferTooSoon(randomGift)
+ dontOfferTooSoon(randomGift);
setLevel(0);
- pauseRecording()
+ pauseRecording();
}
-let keyboardPuckSpeed = 0
+let keyboardPuckSpeed = 0;
function setMousePos(x) {
-
needsRender = true;
puck = x;
@@ -566,11 +574,11 @@ function setMousePos(x) {
canvas.addEventListener("mouseup", (e) => {
if (e.button !== 0) return;
if (running) {
- pause(true)
+ pause(true);
} else {
- play()
- if (isSettingOn('pointerLock')) {
- canvas.requestPointerLock()
+ play();
+ if (isSettingOn("pointerLock")) {
+ canvas.requestPointerLock();
}
}
});
@@ -587,16 +595,16 @@ canvas.addEventListener("touchstart", (e) => {
e.preventDefault();
if (!e.touches?.length) return;
setMousePos(e.touches[0].pageX);
- play()
+ play();
});
canvas.addEventListener("touchend", (e) => {
e.preventDefault();
- pause(true)
+ pause(true);
});
canvas.addEventListener("touchcancel", (e) => {
e.preventDefault();
- pause(true)
- needsRender = true
+ pause(true);
+ needsRender = true;
});
canvas.addEventListener("touchmove", (e) => {
if (!e.touches?.length) return;
@@ -606,7 +614,10 @@ canvas.addEventListener("touchmove", (e) => {
let lastTick = performance.now();
function brickIndex(x, y) {
- return getRowColIndex(Math.floor(y / brickWidth), Math.floor((x - offsetX) / brickWidth))
+ return getRowColIndex(
+ Math.floor(y / brickWidth),
+ Math.floor((x - offsetX) / brickWidth),
+ );
}
function hasBrick(index) {
@@ -614,21 +625,26 @@ function hasBrick(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)));
+ 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 (!perks.pierce_color) return false
- if (typeof vhit !== 'undefined' && bricks[vhit] !== ballsColor) {
- return false
+ if (!perks.pierce_color) return false;
+ if (typeof vhit !== "undefined" && bricks[vhit] !== ballsColor) {
+ return false;
}
- if (typeof hhit !== 'undefined' && bricks[hhit] !== ballsColor) {
- return false
+ if (typeof hhit !== "undefined" && bricks[hhit] !== ballsColor) {
+ return false;
}
- if (typeof chit !== 'undefined' && bricks[chit] !== ballsColor) {
- return false
+ if (typeof chit !== "undefined" && bricks[chit] !== ballsColor) {
+ return false;
}
- return true
+ return true;
}
function brickHitCheck(ballOrCoin, radius, isBall) {
@@ -637,18 +653,25 @@ function brickHitCheck(ballOrCoin, radius, isBall) {
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 chit =
+ (typeof vhit == "undefined" &&
+ typeof hhit == "undefined" &&
+ hitsSomething(x, y, radius)) ||
+ undefined;
let pierce = isBall && ballOrCoin.piercedSinceBounce < perks.pierce * 3;
- if (pierce && (typeof vhit !== "undefined" || typeof hhit !== "undefined" || typeof chit !== "undefined")) {
- ballOrCoin.piercedSinceBounce++
+ if (
+ pierce &&
+ (typeof vhit !== "undefined" ||
+ typeof hhit !== "undefined" ||
+ typeof chit !== "undefined")
+ ) {
+ ballOrCoin.piercedSinceBounce++;
}
if (isBall && shouldPierceByColor(vhit, hhit, chit)) {
- pierce = true
+ pierce = true;
}
-
if (typeof vhit !== "undefined" || typeof chit !== "undefined") {
if (!pierce) {
ballOrCoin.y = ballOrCoin.previousy;
@@ -692,11 +715,14 @@ function bordersHitCheck(coin, radius, delta) {
coin.sy *= 0.9;
if (perks.wind) {
- coin.vx += (puck - (offsetX + gameZoneWidth / 2)) / gameZoneWidth * perks.wind * 0.5;
+ coin.vx +=
+ ((puck - (offsetX + gameZoneWidth / 2)) / gameZoneWidth) *
+ perks.wind *
+ 0.5;
}
- let vhit = 0, hhit = 0;
-
+ let vhit = 0,
+ hhit = 0;
if (coin.x < offsetXRoundedDown + radius) {
coin.x = offsetXRoundedDown + radius;
@@ -719,29 +745,25 @@ function bordersHitCheck(coin, radius, delta) {
let lastTickDown = 0;
-
function tick() {
-
recomputeTargetBaseSpeed();
const currentTick = performance.now();
- puckWidth = (gameZoneWidth / 12) * (3 - perks.smaller_puck + perks.bigger_puck);
+ puckWidth =
+ (gameZoneWidth / 12) * (3 - perks.smaller_puck + perks.bigger_puck);
if (keyboardPuckSpeed) {
- setMousePos(puck + keyboardPuckSpeed)
-
+ setMousePos(puck + keyboardPuckSpeed);
}
if (running) {
-
levelTime += currentTick - lastTick;
- runStatistics.runTime += currentTick - lastTick
- runStatistics.max_combo = Math.max(runStatistics.max_combo, combo)
+ runStatistics.runTime += currentTick - lastTick;
+ runStatistics.max_combo = Math.max(runStatistics.max_combo, combo);
// How many times to compute
let delta = Math.min(4, (currentTick - lastTick) / (1000 / 60));
- delta *= running ? 1 : 0
-
+ delta *= running ? 1 : 0;
coins = coins.filter((coin) => !coin.destroyed);
balls = balls.filter((ball) => !ball.destroyed);
@@ -761,32 +783,39 @@ function tick() {
});
}
if (!remainingBricks && !coins.length) {
-
if (currentLevel + 1 < max_levels()) {
setLevel(currentLevel + 1);
} else {
- gameOver("Run finished with " + score + " points", "You cleared all levels for this run.");
+ gameOver(
+ "Run finished with " + score + " points",
+ "You cleared all levels for this run.",
+ );
}
} else if (running || levelTime) {
let playedCoinBounce = false;
const coinRadius = Math.round(coinSize / 2);
-
coins.forEach((coin) => {
if (coin.destroyed) return;
if (perks.coin_magnet) {
- coin.vx += ((delta * (puck - coin.x)) / (100 + Math.pow(coin.y - gameZoneHeight, 2) + Math.pow(coin.x - puck, 2))) * perks.coin_magnet * 100;
+ coin.vx +=
+ ((delta * (puck - coin.x)) /
+ (100 +
+ Math.pow(coin.y - gameZoneHeight, 2) +
+ Math.pow(coin.x - puck, 2))) *
+ perks.coin_magnet *
+ 100;
}
const ratio = 1 - (perks.viscosity * 0.03 + 0.005) * delta;
coin.vy *= ratio;
coin.vx *= ratio;
- if (coin.vx > 7 * baseSpeed) coin.vx = 7 * baseSpeed
- if (coin.vx < -7 * baseSpeed) coin.vx = -7 * baseSpeed
- if (coin.vy > 7 * baseSpeed) coin.vy = 7 * baseSpeed
- if (coin.vy < -7 * baseSpeed) coin.vy = -7 * baseSpeed
- coin.a += coin.sa
+ if (coin.vx > 7 * baseSpeed) coin.vx = 7 * baseSpeed;
+ if (coin.vx < -7 * baseSpeed) coin.vx = -7 * baseSpeed;
+ if (coin.vy > 7 * baseSpeed) coin.vy = 7 * baseSpeed;
+ if (coin.vy < -7 * baseSpeed) coin.vy = -7 * baseSpeed;
+ coin.a += coin.sa;
// Gravity
coin.vy += delta * coin.weight * 0.8;
@@ -794,29 +823,43 @@ function tick() {
const speed = Math.abs(coin.sx) + Math.abs(coin.sx);
const hitBorder = bordersHitCheck(coin, coinRadius, delta);
- if (coin.y > gameZoneHeight - coinRadius - puckHeight && coin.y < gameZoneHeight + puckHeight + coin.vy && Math.abs(coin.x - puck) < coinRadius + puckWidth / 2 + // a bit of margin to be nice
- puckHeight) {
+ if (
+ coin.y > gameZoneHeight - coinRadius - puckHeight &&
+ coin.y < gameZoneHeight + puckHeight + coin.vy &&
+ Math.abs(coin.x - puck) <
+ coinRadius +
+ puckWidth / 2 + // a bit of margin to be nice
+ puckHeight
+ ) {
addToScore(coin);
-
} else if (coin.y > canvas.height + coinRadius) {
coin.destroyed = true;
if (perks.compound_interest) {
- decreaseCombo(coin.points * perks.compound_interest, coin.x, canvas.height - coinRadius);
+ decreaseCombo(
+ coin.points * perks.compound_interest,
+ coin.x,
+ canvas.height - coinRadius,
+ );
}
}
const hitBrick = brickHitCheck(coin, coinRadius, false);
if (perks.metamorphosis && typeof hitBrick !== "undefined") {
- if (bricks[hitBrick] && coin.color !== bricks[hitBrick] && bricks[hitBrick] !== "black" && !coin.coloredABrick) {
+ if (
+ bricks[hitBrick] &&
+ coin.color !== bricks[hitBrick] &&
+ bricks[hitBrick] !== "black" &&
+ !coin.coloredABrick
+ ) {
bricks[hitBrick] = coin.color;
- coin.coloredABrick = true
+ coin.coloredABrick = true;
}
}
if (typeof hitBrick !== "undefined" || hitBorder) {
coin.vx *= 0.8;
coin.vy *= 0.8;
- coin.sa *= 0.9
+ coin.sa *= 0.9;
if (speed > 20 && !playedCoinBounce) {
playedCoinBounce = true;
sounds.coinBounce(coin.x, 0.2);
@@ -831,8 +874,10 @@ function tick() {
balls.forEach((ball) => ballTick(ball, delta));
if (perks.wind) {
-
- const windD = (puck - (offsetX + gameZoneWidth / 2)) / gameZoneWidth * 2 * perks.wind
+ const windD =
+ ((puck - (offsetX + gameZoneWidth / 2)) / gameZoneWidth) *
+ 2 *
+ perks.wind;
for (var i = 0; i < perks.wind; i++) {
if (Math.random() * Math.abs(windD) > 0.5) {
flashes.push({
@@ -851,7 +896,6 @@ function tick() {
}
}
-
flashes.forEach((flash) => {
if (flash.type === "particle") {
flash.x += flash.vx * delta;
@@ -866,56 +910,66 @@ function tick() {
});
}
-
if (combo > baseCombo()) {
// The red should still be visible on a white bg
- const baseParticle = !isSettingOn('basic') && (combo - baseCombo()) * Math.random() > 5 && running && {
- type: "particle",
- duration: 100 * (Math.random() + 1),
- time: levelTime,
- size: coinSize / 2,
- color: 'red',
- ethereal: true,
- }
+ const baseParticle = !isSettingOn("basic") &&
+ (combo - baseCombo()) * Math.random() > 5 &&
+ running && {
+ type: "particle" as FlashTypes,
+ duration: 100 * (Math.random() + 1),
+ time: levelTime,
+ size: coinSize / 2,
+ color: "red",
+ ethereal: true,
+ };
if (perks.top_is_lava) {
- baseParticle && flashes.push({
+ baseParticle &&
+ flashes.push({
...baseParticle,
x: offsetXRoundedDown + Math.random() * gameZoneWidthRoundedUp,
y: 0,
vx: (Math.random() - 0.5) * 10,
vy: 5,
- })
+ });
}
if (perks.sides_are_lava) {
- const fromLeft = Math.random() > 0.5
- baseParticle && flashes.push({
+ const fromLeft = Math.random() > 0.5;
+ baseParticle &&
+ flashes.push({
...baseParticle,
x: offsetXRoundedDown + (fromLeft ? 0 : gameZoneWidthRoundedUp),
y: Math.random() * gameZoneHeight,
vx: fromLeft ? 5 : -5,
vy: (Math.random() - 0.5) * 10,
- })
+ });
}
if (perks.compound_interest) {
- let x = puck
+ let x = puck, attemps=0;
do {
- x = offsetXRoundedDown + gameZoneWidthRoundedUp * Math.random()
- } while (Math.abs(x - puck) < puckWidth / 2)
- baseParticle && flashes.push({
- ...baseParticle, x, y: gameZoneHeight, vx: (Math.random() - 0.5) * 10, vy: -5,
- })
+ x = offsetXRoundedDown + gameZoneWidthRoundedUp * Math.random();
+ attemps++
+ } while (Math.abs(x - puck) < puckWidth / 2 && attemps<10);
+ baseParticle &&
+ flashes.push({
+ ...baseParticle,
+ x,
+ y: gameZoneHeight,
+ vx: (Math.random() - 0.5) * 10,
+ vy: -5,
+ });
}
if (perks.streak_shots) {
- const pos = (0.5 - Math.random())
- baseParticle && flashes.push({
+ const pos = 0.5 - Math.random();
+ baseParticle &&
+ flashes.push({
...baseParticle,
duration: 100,
x: puck + puckWidth * pos,
y: gameZoneHeight - puckHeight,
- vx: (pos) * 10,
+ vx: pos * 10,
vy: -5,
- })
+ });
}
}
}
@@ -934,54 +988,66 @@ function ballTick(ball, delta) {
ball.previousvx = ball.vx;
ball.previousvy = ball.vy;
-
- let speedLimitDampener = 1 + perks.telekinesis + perks.ball_repulse_ball + perks.puck_repulse_ball + perks.ball_attract_ball
+ let speedLimitDampener =
+ 1 +
+ perks.telekinesis +
+ perks.ball_repulse_ball +
+ perks.puck_repulse_ball +
+ perks.ball_attract_ball;
if (isTelekinesisActive(ball)) {
- speedLimitDampener += 3
+ speedLimitDampener += 3;
ball.vx += ((puck - ball.x) / 1000) * delta * perks.telekinesis;
}
-
if (ball.vx * ball.vx + ball.vy * ball.vy < baseSpeed * baseSpeed * 2) {
- ball.vx *= (1 + .02 / speedLimitDampener);
- ball.vy *= (1 + .02 / speedLimitDampener);
+ ball.vx *= 1 + 0.02 / speedLimitDampener;
+ ball.vy *= 1 + 0.02 / speedLimitDampener;
} else {
- ball.vx *= (1 - .02 / speedLimitDampener);
- ball.vy *= (1 - .02 / speedLimitDampener);
+ 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 * baseSpeed) {
- ball.vy += (ball.vy > 0 ? 1 : -1) * .02 / speedLimitDampener
+ ball.vy += ((ball.vy > 0 ? 1 : -1) * 0.02) / speedLimitDampener;
}
-
if (perks.ball_repulse_ball) {
for (let b2 of balls) {
// avoid computing this twice, and repulsing itself
- if (b2.x >= ball.x) continue
- repulse(ball, b2, perks.ball_repulse_ball, true)
+ if (b2.x >= ball.x) continue;
+ repulse(ball, b2, perks.ball_repulse_ball, true);
}
}
if (perks.ball_attract_ball) {
for (let b2 of balls) {
// avoid computing this twice, and repulsing itself
- if (b2.x >= ball.x) continue
- attract(ball, b2, perks.ball_attract_ball)
+ if (b2.x >= ball.x) continue;
+ attract(ball, b2, perks.ball_attract_ball);
}
}
- if (perks.puck_repulse_ball && Math.abs(ball.x - puck) < puckWidth / 2 + ballSize * (9 + perks.puck_repulse_ball) / 10) {
- repulse(ball, {
- x: puck, y: gameZoneHeight
- }, perks.puck_repulse_ball, false)
+ if (
+ perks.puck_repulse_ball &&
+ Math.abs(ball.x - puck) <
+ puckWidth / 2 + (ballSize * (9 + perks.puck_repulse_ball)) / 10
+ ) {
+ repulse(
+ ball,
+ {
+ x: puck,
+ y: gameZoneHeight,
+ },
+ perks.puck_repulse_ball,
+ false,
+ );
}
- if (perks.respawn && ball.hitItem?.length > 1 && !isSettingOn('basic')) {
+ if (perks.respawn && ball.hitItem?.length > 1 && !isSettingOn("basic")) {
for (let i = 0; i < ball.hitItem?.length - 1 && i < perks.respawn; i++) {
- const {index, color} = ball.hitItem[i]
- if (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
+ const {index, color} = ball.hitItem[i];
+ if (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;
flashes.push({
type: "particle",
@@ -990,8 +1056,8 @@ function ballTick(ball, delta) {
time: levelTime,
size: coinSize / 2,
color,
- x: brickCenterX(index) + dx * brickWidth / 2,
- y: brickCenterY(index) + dy * brickWidth / 2,
+ x: brickCenterX(index) + (dx * brickWidth) / 2,
+ y: brickCenterY(index) + (dy * brickWidth) / 2,
vx: vertical ? 0 : -dx * baseSpeed,
vy: vertical ? -dy * baseSpeed : 0,
});
@@ -1007,12 +1073,16 @@ function ballTick(ball, delta) {
resetCombo(ball.x, ball.y + ballSize);
}
sounds.wallBeep(ball.x);
- ball.bouncesList?.push({x: ball.previousx, y: ball.previousy})
+ ball.bouncesList?.push({x: ball.previousx, y: ball.previousy});
}
// Puck collision
const ylimit = gameZoneHeight - puckHeight - ballSize / 2;
- if (ball.y > ylimit && Math.abs(ball.x - puck) < ballSize / 2 + puckWidth / 2 && ball.vy > 0) {
+ if (
+ ball.y > ylimit &&
+ Math.abs(ball.x - puck) < ballSize / 2 + puckWidth / 2 &&
+ ball.vy > 0
+ ) {
const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
const angle = Math.atan2(-puckWidth / 2, ball.x - puck);
ball.vx = speed * Math.cos(angle);
@@ -1024,48 +1094,50 @@ function ballTick(ball, delta) {
}
if (perks.respawn) {
- ball.hitItem.slice(0, -1).slice(0, perks.respawn)
+ ball.hitItem
+ .slice(0, -1)
+ .slice(0, perks.respawn)
.forEach(({index, color}) => {
- if (!bricks[index] && color !== 'black') bricks[index] = color
- })
+ if (!bricks[index] && color !== "black") bricks[index] = color;
+ });
}
- ball.hitItem = []
+ ball.hitItem = [];
if (!ball.hitSinceBounce) {
- runStatistics.misses++
+ runStatistics.misses++;
levelMisses++;
- resetCombo(ball.x, ball.y)
+ resetCombo(ball.x, ball.y);
flashes.push({
type: "text",
- text: 'miss',
+ text: "miss",
duration: 500,
time: levelTime,
size: puckHeight * 1.5,
- color: 'red',
+ color: "red",
x: puck,
y: gameZoneHeight - puckHeight * 2,
-
});
-
-
}
- runStatistics.puck_bounces++
+ runStatistics.puck_bounces++;
ball.hitSinceBounce = 0;
ball.sapperUses = 0;
ball.piercedSinceBounce = 0;
- ball.bouncesList = [{
- x: ball.previousx, y: ball.previousy
- }]
+ ball.bouncesList = [
+ {
+ x: ball.previousx,
+ y: ball.previousy,
+ },
+ ];
}
if (ball.y > gameZoneHeight + ballSize / 2 && running) {
ball.destroyed = true;
- runStatistics.balls_lost++
+ runStatistics.balls_lost++;
if (!balls.find((b) => !b.destroyed)) {
if (perks.extra_life) {
perks.extra_life--;
resetBalls();
sounds.revive();
- pause(false)
+ pause(false);
coins = [];
flashes.push({
type: "ball",
@@ -1077,23 +1149,27 @@ function ballTick(ball, delta) {
y: ball.y,
});
} else {
- gameOver("Game Over", "You dropped the ball after catching " + score + " coins. ");
+ gameOver(
+ "Game Over",
+ "You dropped the ball after catching " + score + " coins. ",
+ );
}
}
}
const hitBrick = brickHitCheck(ball, ballSize / 2, true);
if (typeof hitBrick !== "undefined") {
- const initialBrickColor = bricks[hitBrick]
+ const initialBrickColor = bricks[hitBrick];
explodeBrick(hitBrick, ball, false);
- if (ball.sapperUses < perks.sapper && initialBrickColor !== "black" && // don't replace a brick that bounced with sturdy_bricks
- !bricks[hitBrick]) {
+ if (
+ ball.sapperUses < perks.sapper &&
+ initialBrickColor !== "black" && // don't replace a brick that bounced with sturdy_bricks
+ !bricks[hitBrick]
+ ) {
bricks[hitBrick] = "black";
- ball.sapperUses++
-
+ ball.sapperUses++;
}
-
}
if (!isSettingOn("basic")) {
@@ -1113,8 +1189,6 @@ function ballTick(ball, delta) {
ball.sparks = 0;
}
}
-
-
}
const defaultRunStats = () => ({
@@ -1129,213 +1203,254 @@ const defaultRunStats = () => ({
puck_bounces: 0,
upgrades_picked: 1,
max_combo: 1,
- max_level: 0
-})
+ max_level: 0,
+});
let runStatistics = defaultRunStats();
function resetRunStatistics() {
- runStatistics = defaultRunStats()
+ runStatistics = defaultRunStats();
}
-
function getTotalScore() {
try {
- return JSON.parse(localStorage.getItem('breakout_71_total_score') || '0')
+ return JSON.parse(localStorage.getItem("breakout_71_total_score") || "0");
} catch (e) {
- return 0
+ return 0;
}
}
function addToTotalScore(points) {
try {
- localStorage.setItem('breakout_71_total_score', JSON.stringify(getTotalScore() + points))
+ 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))
+ 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 (!running) return;
- pause(true)
- stopRecording()
- addToTotalPlayTime(runStatistics.runTime)
- runStatistics.max_level = currentLevel + 1
+ pause(true);
+ stopRecording();
+ addToTotalPlayTime(runStatistics.runTime);
+ runStatistics.max_level = currentLevel + 1;
- let animationDelay = -300
+ let animationDelay = -300;
const getDelay = () => {
- animationDelay += 800
- return 'animation-delay:' + animationDelay + 'ms;'
- }
+ animationDelay += 800;
+ return "animation-delay:" + animationDelay + "ms;";
+ };
// unlocks
- let unlocksInfo = ''
- const endTs = getTotalScore()
- const startTs = endTs - score
- const list = getUpgraderUnlockPoints()
- list.filter(u => u.threshold > startTs && u.threshold < endTs).forEach(u => {
- unlocksInfo += `
+ let unlocksInfo = "";
+ const endTs = getTotalScore();
+ const startTs = endTs - score;
+ const list = getUpgraderUnlockPoints();
+ list
+ .filter((u) => u.threshold > startTs && u.threshold < endTs)
+ .forEach((u) => {
+ unlocksInfo += `
${u.title}
-`
- })
- const previousUnlockAt = findLast(list, u => u.threshold <= endTs)?.threshold || 0
- const nextUnlock = list.find(u => u.threshold > endTs)
+`;
+ });
+ 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 += `Score ${nextUnlock.threshold - endTs} more points to reach the next unlock.`
+ const total = nextUnlock?.threshold - previousUnlockAt;
+ const done = endTs - previousUnlockAt;
+ intro += `Score ${nextUnlock.threshold - endTs} more points to reach the next unlock.`;
- const scaleX = (done / total).toFixed(2)
+ const scaleX = (done / total).toFixed(2);
unlocksInfo += `
${nextUnlock.title}
-`
- list.slice(list.indexOf(nextUnlock) + 1).slice(0, 3).forEach(u => {
- unlocksInfo += `
+`;
+ list
+ .slice(list.indexOf(nextUnlock) + 1)
+ .slice(0, 3)
+ .forEach((u) => {
+ unlocksInfo += `
${u.title}
-`
- })
+`;
+ });
}
-
// Avoid the sad sound right as we restart a new games
- combo = 1
+ combo = 1;
asyncAlert({
- allowClose: true, title, text: `
+ allowClose: true,
+ title,
+ text: `
${intro}
${unlocksInfo}
- `, textAfterButtons: `
+ `,
+ textAfterButtons: `
${getHistograms(true)}
- `
+ `,
}).then(() => restart());
}
function getHistograms(saveStats) {
-
- let runStats = ''
+ 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)
+ let runsHistory = JSON.parse(
+ localStorage.getItem("breakout_71_runs_history") || "[]",
+ );
+ runsHistory.sort((a, b) => a.score - b.score).reverse();
+ runsHistory = runsHistory.slice(0, 100);
- const nonZeroPerks = {}
+ const nonZeroPerks = {};
for (let k in perks) {
if (perks[k]) {
- nonZeroPerks[k] = perks[k]
+ nonZeroPerks[k] = perks[k];
}
}
- runsHistory.push({...runStatistics, perks: nonZeroPerks})
+ runsHistory.push({...runStatistics, perks: nonZeroPerks});
// Generate some histogram
if (saveStats) {
- localStorage.setItem('breakout_71_runs_history', JSON.stringify(runsHistory, null, 2))
+ 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)
+ 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 (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)
+ 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 = []
+ 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)
+ 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 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 `${(!v && ' ') || (vi == activeBin && lastValue + unit) || (Math.round(binsTotal[vi] / v) + unit)} `
- }
- ).join('')
+ const bars = bins
+ .map((v, vi) => {
+ const style = `height: ${(v / maxBin) * 80}px`;
+ return `${(!v && " ") || (vi == activeBin && lastValue + unit) || Math.round(binsTotal[vi] / v) + unit} `;
+ })
+ .join("");
return `${title} : ${lastValue}${unit}
${bars}
- `
- }
+ `;
+ };
-
- runStats += makeHistogram('Total score', r => r.score, '')
- runStats += makeHistogram('Catch rate', r => Math.round(r.score / r.coins_spawned * 100), '%')
- runStats += makeHistogram('Bricks broken', r => r.bricks_broken, '')
- runStats += makeHistogram('Bricks broken per minute', r => Math.round(r.bricks_broken / r.runTime * 1000 * 60), ' bpm')
- runStats += makeHistogram('Hit rate', r => Math.round((1 - r.misses / r.puck_bounces) * 100), '%')
- runStats += makeHistogram('Duration per level', r => Math.round(r.runTime / 1000 / r.levelsPlayed), 's')
- runStats += makeHistogram('Level reached', r => r.levelsPlayed, '')
- runStats += makeHistogram('Upgrades applied', r => r.upgrades_picked, '')
- runStats += makeHistogram('Balls lost', r => r.balls_lost, '')
- runStats += makeHistogram('Average combo', r => Math.round(r.coins_spawned / r.bricks_broken), '')
- runStats += makeHistogram('Max combo', r => r.max_combo, '')
+ runStats += makeHistogram("Total score", (r) => r.score, "");
+ runStats += makeHistogram(
+ "Catch rate",
+ (r) => Math.round((r.score / r.coins_spawned) * 100),
+ "%",
+ );
+ runStats += makeHistogram("Bricks broken", (r) => r.bricks_broken, "");
+ runStats += makeHistogram(
+ "Bricks broken per minute",
+ (r) => Math.round((r.bricks_broken / r.runTime) * 1000 * 60),
+ " bpm",
+ );
+ runStats += makeHistogram(
+ "Hit rate",
+ (r) => Math.round((1 - r.misses / r.puck_bounces) * 100),
+ "%",
+ );
+ runStats += makeHistogram(
+ "Duration per level",
+ (r) => Math.round(r.runTime / 1000 / r.levelsPlayed),
+ "s",
+ );
+ runStats += makeHistogram("Level reached", (r) => r.levelsPlayed, "");
+ runStats += makeHistogram("Upgrades applied", (r) => r.upgrades_picked, "");
+ runStats += makeHistogram("Balls lost", (r) => r.balls_lost, "");
+ runStats += makeHistogram(
+ "Average combo",
+ (r) => Math.round(r.coins_spawned / r.bricks_broken),
+ "",
+ );
+ runStats += makeHistogram("Max combo", (r) => r.max_combo, "");
if (runStats) {
- runStats = `Find below your run statistics compared to your ${runsHistory.length - 1} best runs.
` + runStats
+ runStats =
+ `Find below your run statistics compared to your ${runsHistory.length - 1} best runs.
` +
+ runStats;
}
} catch (e) {
- console.warn(e)
+ console.warn(e);
}
- return runStats
+ return runStats;
}
-
function explodeBrick(index, ball, isExplosion) {
-
const color = bricks[index];
if (!color) return;
- if (color === 'black') {
+ if (color === "black") {
delete bricks[index];
- const x = brickCenterX(index), y = brickCenterY(index);
+ const x = brickCenterX(index),
+ y = brickCenterY(index);
sounds.explode(ball.x);
- const col = index % gridSize
- const row = Math.floor(index / gridSize)
+ const col = index % gridSize;
+ const row = Math.floor(index / gridSize);
const size = 1 + 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 (bricks[i] && i !== -1) {
- explodeBrick(i, ball, true)
+ explodeBrick(i, ball, true);
}
}
}
@@ -1345,62 +1460,78 @@ function explodeBrick(index, ball, isExplosion) {
const dx = c.x - x;
const dy = c.y - y;
const d2 = Math.max(brickWidth, Math.abs(dx) + Math.abs(dy));
- c.vx += (dx / d2) * 10 * size / c.weight;
- c.vy += (dy / d2) * 10 * size / c.weight;
+ c.vx += ((dx / d2) * 10 * size) / c.weight;
+ c.vy += ((dy / d2) * 10 * size) / c.weight;
});
- lastexplosion = Date.now();
+ lastExplosion = Date.now();
flashes.push({
- type: "ball", duration: 150, time: levelTime, size: brickWidth * 2, color: "white", x, y,
+ type: "ball",
+ duration: 150,
+ time: levelTime,
+ size: brickWidth * 2,
+ color: "white",
+ x,
+ y,
});
- spawnExplosion(7 * (1 + perks.bigger_explosions), x, y, 'white', 150, coinSize,);
+ spawnExplosion(
+ 7 * (1 + perks.bigger_explosions),
+ x,
+ y,
+ "white",
+ 150,
+ coinSize,
+ );
ball.hitSinceBounce++;
- runStatistics.bricks_broken++
+ runStatistics.bricks_broken++;
} else if (color) {
// Even if it bounces we don't want to count that as a miss
ball.hitSinceBounce++;
if (perks.sturdy_bricks && perks.sturdy_bricks > Math.random() * 5) {
// Resist
- sounds.coinBounce(ball.x, 1)
- return
+ sounds.coinBounce(ball.x, 1);
+ return;
}
// Flashing is take care of by the tick loop
- const x = brickCenterX(index), y = brickCenterY(index);
+ const x = brickCenterX(index),
+ y = brickCenterY(index);
bricks[index] = "";
-
// coins = coins.filter((c) => !c.destroyed);
- let coinsToSpawn = combo
+ let coinsToSpawn = combo;
if (perks.sturdy_bricks) {
// +10% per level
- coinsToSpawn += Math.ceil((10 + perks.sturdy_bricks) / 10 * coinsToSpawn)
+ coinsToSpawn += Math.ceil(
+ ((10 + perks.sturdy_bricks) / 10) * coinsToSpawn,
+ );
}
levelSpawnedCoins += coinsToSpawn;
- runStatistics.coins_spawned += coinsToSpawn
- runStatistics.bricks_broken++
- const maxCoins = MAX_COINS * (isSettingOn("basic") ? 0.5 : 1)
- const spawnableCoins = coins.length > MAX_COINS ? 1 : Math.floor(maxCoins - coins.length) / 3
+ runStatistics.coins_spawned += coinsToSpawn;
+ runStatistics.bricks_broken++;
+ const maxCoins = MAX_COINS * (isSettingOn("basic") ? 0.5 : 1);
+ const spawnableCoins =
+ coins.length > MAX_COINS ? 1 : Math.floor(maxCoins - coins.length) / 3;
- const pointsPerCoin = Math.max(1, Math.ceil(coinsToSpawn / spawnableCoins))
+ const pointsPerCoin = Math.max(1, Math.ceil(coinsToSpawn / spawnableCoins));
while (coinsToSpawn > 0) {
- const points = Math.min(pointsPerCoin, coinsToSpawn)
+ const points = Math.min(pointsPerCoin, coinsToSpawn);
if (points < 0 || isNaN(points)) {
- console.error({points})
- debugger
+ console.error({points});
+ debugger;
}
- coinsToSpawn -= points
+ coinsToSpawn -= points;
const cx = x + (Math.random() - 0.5) * (brickWidth - coinSize),
cy = y + (Math.random() - 0.5) * (brickWidth - coinSize);
coins.push({
points,
- color: perks.metamorphosis ? color : 'gold',
+ color: perks.metamorphosis ? color : "gold",
x: cx,
y: cy,
previousx: cx,
@@ -1412,16 +1543,27 @@ function explodeBrick(index, ball, isExplosion) {
sy: 0,
a: Math.random() * Math.PI * 2,
sa: Math.random() - 0.5,
- weight: 0.8 + Math.random() * 0.2
+ weight: 0.8 + Math.random() * 0.2,
});
}
-
- combo += Math.max(0, perks.streak_shots + perks.compound_interest + perks.sides_are_lava + perks.top_is_lava + perks.picky_eater - Math.round(Math.random() * perks.soft_reset));
+ combo += Math.max(
+ 0,
+ perks.streak_shots +
+ perks.compound_interest +
+ perks.sides_are_lava +
+ perks.top_is_lava +
+ perks.picky_eater -
+ Math.round(Math.random() * perks.soft_reset),
+ );
if (!isExplosion) {
// color change
- if ((perks.picky_eater || perks.pierce_color) && color !== ballsColor && color) {
+ if (
+ (perks.picky_eater || perks.pierce_color) &&
+ color !== ballsColor &&
+ color
+ ) {
if (perks.picky_eater) {
resetCombo(ball.x, ball.y);
}
@@ -1433,28 +1575,33 @@ function explodeBrick(index, ball, isExplosion) {
}
flashes.push({
- type: "ball", duration: 40, time: levelTime, size: brickWidth, color: color, x, y,
+ type: "ball",
+ duration: 40,
+ time: levelTime,
+ size: brickWidth,
+ color: color,
+ x,
+ y,
});
- spawnExplosion(5 + Math.min(combo, 30), x, y, color, 100, coinSize / 2);
+ spawnExplosion(5 + Math.min(combo, 30), x, y, color, 150, coinSize / 2);
}
if (!bricks[index]) {
ball.hitItem?.push({
- index, color
- })
+ index,
+ color,
+ });
}
}
-
function max_levels() {
-
return 7 + perks.extra_levels;
}
function render() {
- if (running) needsRender = true
+ if (running) needsRender = true;
if (!needsRender) {
- return
+ return;
}
needsRender = false;
@@ -1467,24 +1614,23 @@ function render() {
scoreInfo += "🖤 ";
}
- scoreInfo += 'L' + (currentLevel + 1) + '/' + max_levels() + ' ';
- scoreInfo += '$' + score.toString();
+ scoreInfo += "L" + (currentLevel + 1) + "/" + max_levels() + " ";
+ scoreInfo += "$" + score.toString();
scoreDisplay.innerText = scoreInfo;
// Clear
if (!isSettingOn("basic") && !level.color && level.svg) {
-
// Without this the light trails everything
ctx.globalCompositeOperation = "source-over";
- ctx.globalAlpha = .4
+ ctx.globalAlpha = 0.4;
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, width, height);
-
ctx.globalCompositeOperation = "screen";
ctx.globalAlpha = 0.6;
coins.forEach((coin) => {
- if (!coin.destroyed) drawFuzzyBall(ctx, coin.color, coinSize * 2, coin.x, coin.y);
+ if (!coin.destroyed)
+ drawFuzzyBall(ctx, coin.color, coinSize * 2, coin.x, coin.y);
});
balls.forEach((ball) => {
drawFuzzyBall(ctx, ballsColor, ballSize * 2, ball.x, ball.y);
@@ -1492,8 +1638,9 @@ function render() {
ctx.globalAlpha = 0.5;
bricks.forEach((color, index) => {
if (!color) return;
- const x = brickCenterX(index), y = brickCenterY(index);
- drawFuzzyBall(ctx, color == 'black' ? '#666' : color, brickWidth, x, y);
+ const x = brickCenterX(index),
+ y = brickCenterY(index);
+ drawFuzzyBall(ctx, color == "black" ? "#666" : color, brickWidth, x, y);
});
ctx.globalAlpha = 1;
flashes.forEach((flash) => {
@@ -1506,38 +1653,37 @@ function render() {
if (type === "particle") {
drawFuzzyBall(ctx, color, size * 3, x, y);
}
-
});
// Decides how brights the bg black parts can get
- ctx.globalAlpha = .2;
+ 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 = .8;
+ 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 = canvas.width
- backgroundCanvas.height = canvas.height
- const bgctx = backgroundCanvas.getContext("2d") as CanvasRenderingContext2D
- bgctx.fillStyle = level.color || '#000'
- bgctx.fillRect(0, 0, canvas.width, canvas.height)
+ backgroundCanvas.title = level.name;
+ backgroundCanvas.width = canvas.width;
+ backgroundCanvas.height = canvas.height;
+ const bgctx = backgroundCanvas.getContext(
+ "2d",
+ ) as CanvasRenderingContext2D;
+ bgctx.fillStyle = level.color || "#000";
+ bgctx.fillRect(0, 0, canvas.width, canvas.height);
bgctx.fillStyle = ctx.createPattern(background, "repeat");
bgctx.fillRect(0, 0, width, height);
}
- ctx.drawImage(backgroundCanvas, 0, 0)
+ ctx.drawImage(backgroundCanvas, 0, 0);
} else {
// Background not loaded yes
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, width, height);
}
} else {
-
- ctx.globalAlpha = 1
+ ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
ctx.fillStyle = level.color || "#000";
ctx.fillRect(0, 0, width, height);
@@ -1554,21 +1700,26 @@ function render() {
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
- const lastExplosionDelay = Date.now() - lastexplosion + 5;
+ const lastExplosionDelay = Date.now() - lastExplosion + 5;
const shaked = lastExplosionDelay < 200;
if (shaked) {
- const amplitude = (perks.bigger_explosions + 1) * 50 / lastExplosionDelay
- ctx.translate(Math.sin(Date.now()) * amplitude, Math.sin(Date.now() + 36) * amplitude);
+ const amplitude = ((perks.bigger_explosions + 1) * 50) / lastExplosionDelay;
+ ctx.translate(
+ Math.sin(Date.now()) * amplitude,
+ Math.sin(Date.now() + 36) * amplitude,
+ );
}
ctx.globalCompositeOperation = "source-over";
renderAllBricks(ctx);
ctx.globalCompositeOperation = "screen";
- flashes = flashes.filter((f) => levelTime - f.time < f.duration && !f.destroyed,);
+ flashes = flashes.filter(
+ (f) => levelTime - f.time < f.duration && !f.destroyed,
+ );
flashes.forEach((flash) => {
- const {x, y, time, color, size, type, text, duration, points} = flash;
+ const {x, y, time, color, size, type, text, duration} = flash;
const elapsed = levelTime - time;
ctx.globalAlpha = Math.max(0, Math.min(1, 2 - (elapsed / duration) * 2));
if (type === "text") {
@@ -1585,21 +1736,29 @@ function render() {
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
coins.forEach((coin) => {
- if (!coin.destroyed) drawCoin(ctx, coin.color, coinSize, coin.x, coin.y, level.color || 'black', coin.a);
+ if (!coin.destroyed)
+ drawCoin(
+ ctx,
+ coin.color,
+ coinSize,
+ coin.x,
+ coin.y,
+ level.color || "black",
+ coin.a,
+ );
});
// Black shadow around balls
- if (coins.length > 10 && !isSettingOn('basic')) {
+ if (coins.length > 10 && !isSettingOn("basic")) {
ctx.globalAlpha = Math.min(0.8, (coins.length - 10) / 50);
balls.forEach((ball) => {
drawBall(ctx, level.color || "#000", ballSize * 6, ball.x, ball.y);
});
}
-
- ctx.globalAlpha = 1
+ ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
- const puckColor = '#FFF'
+ const puckColor = "#FFF";
balls.forEach((ball) => {
drawBall(ctx, ballsColor, ballSize, ball.x, ball.y, puckColor);
// effect
@@ -1611,33 +1770,53 @@ function render() {
}
});
// The puck
- ctx.globalAlpha = 1
+ ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
if (perks.streak_shots && combo > baseCombo()) {
-
- drawPuck(ctx, 'red', puckWidth, puckHeight, -2)
+ drawPuck(ctx, "red", puckWidth, puckHeight, -2);
}
- drawPuck(ctx, puckColor, puckWidth, puckHeight)
+ drawPuck(ctx, puckColor, puckWidth, puckHeight);
if (combo > 1) {
-
ctx.globalCompositeOperation = "source-over";
- const comboText = "x " + combo
- const comboTextWidth = comboText.length * puckHeight / 1.80
- const totalWidth = comboTextWidth + coinSize * 2
- const left = puck - totalWidth / 2
+ const comboText = "x " + combo;
+ const comboTextWidth = (comboText.length * puckHeight) / 1.8;
+ const totalWidth = comboTextWidth + coinSize * 2;
+ const left = puck - totalWidth / 2;
if (totalWidth < puckWidth) {
-
- drawCoin(ctx, 'gold', coinSize, left + coinSize / 2, gameZoneHeight - puckHeight / 2, '#FFF', 0)
- drawText(ctx, comboText, '#000', puckHeight, left + coinSize * 1.5, gameZoneHeight - puckHeight / 2, true);
+ drawCoin(
+ ctx,
+ "gold",
+ coinSize,
+ left + coinSize / 2,
+ gameZoneHeight - puckHeight / 2,
+ "#FFF",
+ 0,
+ );
+ drawText(
+ ctx,
+ comboText,
+ "#000",
+ puckHeight,
+ left + coinSize * 1.5,
+ gameZoneHeight - puckHeight / 2,
+ true,
+ );
} else {
- drawText(ctx, comboText, '#FFF', puckHeight, puck, gameZoneHeight - puckHeight / 2, false);
-
+ drawText(
+ ctx,
+ comboText,
+ "#FFF",
+ puckHeight,
+ puck,
+ gameZoneHeight - puckHeight / 2,
+ false,
+ );
}
}
// Borders
- const redSides = perks.sides_are_lava && combo > baseCombo()
- ctx.fillStyle = redSides ? 'red' : puckColor;
+ const redSides = perks.sides_are_lava && combo > baseCombo();
+ ctx.fillStyle = redSides ? "red" : puckColor;
ctx.globalCompositeOperation = "source-over";
if (offsetXRoundedDown) {
// draw outside of gaming area to avoid capturing borders in recordings
@@ -1648,23 +1827,36 @@ function render() {
ctx.fillRect(width - 1, 0, 1, height);
}
- if (perks.top_is_lava && combo > baseCombo()) drawRedSquare(ctx, offsetXRoundedDown, 0, gameZoneWidthRoundedUp, 1);
- const redBottom = perks.compound_interest && combo > baseCombo()
- ctx.fillStyle = redBottom ? 'red' : puckColor;
+ if (perks.top_is_lava && combo > baseCombo())
+ drawRedSquare(ctx, offsetXRoundedDown, 0, gameZoneWidthRoundedUp, 1);
+ const redBottom = perks.compound_interest && combo > baseCombo();
+ ctx.fillStyle = redBottom ? "red" : puckColor;
if (isSettingOn("mobile-mode")) {
ctx.fillRect(offsetXRoundedDown, gameZoneHeight, gameZoneWidthRoundedUp, 1);
if (!running) {
- drawText(ctx, "Press and hold here to play", puckColor, puckHeight, canvas.width / 2, gameZoneHeight + (canvas.height - gameZoneHeight) / 2,);
+ drawText(
+ ctx,
+ "Press and hold here to play",
+ puckColor,
+ puckHeight,
+ canvas.width / 2,
+ gameZoneHeight + (canvas.height - gameZoneHeight) / 2,
+ );
}
} else if (redBottom) {
- ctx.fillRect(offsetXRoundedDown, gameZoneHeight - 1, gameZoneWidthRoundedUp, 1);
+ ctx.fillRect(
+ offsetXRoundedDown,
+ gameZoneHeight - 1,
+ gameZoneWidthRoundedUp,
+ 1,
+ );
}
if (shaked) {
ctx.resetTransform();
}
- recordOneFrame()
+ recordOneFrame();
}
let cachedBricksRender = document.createElement("canvas");
@@ -1673,11 +1865,18 @@ let cachedBricksRenderKey = null;
function renderAllBricks(destinationCtx) {
ctx.globalAlpha = 1;
- const level = currentLevelInfo();
+ const redBorderOnBricksWithWrongColor =
+ combo > baseCombo() && perks.picky_eater;
- const redBorderOnBricksWithWrongColor = combo > baseCombo() && perks.picky_eater
-
- const newKey = gameZoneWidth + "_" + bricks.join("_") + bombSVG.complete + '_' + redBorderOnBricksWithWrongColor + '_' + ballsColor;
+ const newKey =
+ gameZoneWidth +
+ "_" +
+ bricks.join("_") +
+ bombSVG.complete +
+ "_" +
+ redBorderOnBricksWithWrongColor +
+ "_" +
+ ballsColor;
if (newKey !== cachedBricksRenderKey) {
cachedBricksRenderKey = newKey;
@@ -1688,14 +1887,18 @@ function renderAllBricks(destinationCtx) {
ctx.resetTransform();
ctx.translate(-offsetX, 0);
// Bricks
- const puckColor = '#FFF'
+ const puckColor = "#FFF";
bricks.forEach((color, index) => {
- const x = brickCenterX(index), y = brickCenterY(index);
+ const x = brickCenterX(index),
+ y = brickCenterY(index);
if (!color) return;
- const borderColor = (ballsColor === color && puckColor) || (color !== 'black' && redBorderOnBricksWithWrongColor && 'red') || color
+ const borderColor =
+ (ballsColor === color && puckColor) ||
+ (color !== "black" && redBorderOnBricksWithWrongColor && "red") ||
+ color;
drawBrick(ctx, color, borderColor, x, y);
- if (color === 'black') {
+ if (color === "black") {
ctx.globalCompositeOperation = "source-over";
drawIMG(ctx, bombSVG, brickWidth, x, y);
}
@@ -1708,8 +1911,7 @@ function renderAllBricks(destinationCtx) {
let cachedGraphics = {};
function drawPuck(ctx, color, puckWidth, puckHeight, yoffset = 0) {
-
- const key = "puck" + color + "_" + puckWidth + '_' + puckHeight;
+ const key = "puck" + color + "_" + puckWidth + "_" + puckHeight;
if (!cachedGraphics[key]) {
const can = document.createElement("canvas");
@@ -1718,23 +1920,31 @@ function drawPuck(ctx, color, puckWidth, puckHeight, yoffset = 0) {
const canctx = can.getContext("2d") as CanvasRenderingContext2D;
canctx.fillStyle = color;
-
canctx.beginPath();
- canctx.moveTo(0, puckHeight * 2)
- canctx.lineTo(0, puckHeight * 1.25)
- canctx.bezierCurveTo(0, puckHeight * .75, puckWidth, puckHeight * .75, puckWidth, puckHeight * 1.25)
- canctx.lineTo(puckWidth, puckHeight * 2)
+ 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(puck - puckWidth / 2), gameZoneHeight - puckHeight * 2 + yoffset);
-
-
+ ctx.drawImage(
+ cachedGraphics[key],
+ Math.round(puck - puckWidth / 2),
+ gameZoneHeight - puckHeight * 2 + yoffset,
+ );
}
-function drawBall(ctx, color, width, x, y, borderColor = '') {
- const key = "ball" + color + "_" + width + '_' + borderColor;
+function drawBall(ctx, color, width, x, y, borderColor = "") {
+ const key = "ball" + color + "_" + width + "_" + borderColor;
const size = Math.round(width);
if (!cachedGraphics[key]) {
@@ -1748,21 +1958,36 @@ function drawBall(ctx, color, width, x, y, borderColor = '') {
canctx.fillStyle = color;
canctx.fill();
if (borderColor) {
- canctx.lineWidth = 2
- canctx.strokeStyle = borderColor
- canctx.stroke()
+ 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),);
+ ctx.drawImage(
+ cachedGraphics[key],
+ Math.round(x - size / 2),
+ Math.round(y - size / 2),
+ );
}
-const angles = 32
+const angles = 32;
function drawCoin(ctx, color, size, x, y, bg, rawAngle) {
- const angle = (Math.round(rawAngle / Math.PI * 2 * angles) % angles + angles) % angles
- const key = "coin with halo" + "_" + color + "_" + size + '_' + bg + '_' + (color === 'gold' ? angle : 'whatever');
+ const angle =
+ ((Math.round((rawAngle / Math.PI) * 2 * angles) % angles) + angles) %
+ angles;
+ const key =
+ "coin with halo" +
+ "_" +
+ color +
+ "_" +
+ size +
+ "_" +
+ bg +
+ "_" +
+ (color === "gold" ? angle : "whatever");
if (!cachedGraphics[key]) {
const can = document.createElement("canvas");
@@ -1777,32 +2002,35 @@ function drawCoin(ctx, color, size, x, y, bg, rawAngle) {
canctx.fillStyle = color;
canctx.fill();
- if (color === 'gold') {
-
+ if (color === "gold") {
canctx.strokeStyle = bg;
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.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)
+ 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));
+ 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
+ if (!color) debugger;
const size = Math.round(width * 3);
if (!cachedGraphics[key]) {
const can = document.createElement("canvas");
@@ -1810,14 +2038,25 @@ function drawFuzzyBall(ctx, color, width, x, y) {
can.height = size;
const canctx = can.getContext("2d") as CanvasRenderingContext2D;
- const gradient = canctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2,);
+ 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),);
+ ctx.drawImage(
+ cachedGraphics[key],
+ Math.round(x - size / 2),
+ Math.round(y - size / 2),
+ );
}
function drawBrick(ctx, color, borderColor, x, y) {
@@ -1826,23 +2065,30 @@ function drawBrick(ctx, color, borderColor, x, y) {
const brx = Math.ceil(x + brickWidth / 2) - 1;
const bry = Math.ceil(y + brickWidth / 2) - 1;
- const width = brx - tlx, height = bry - tly;
- const key = "brick" + color + '_' + borderColor + "_" + width + "_" + height
+ 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 cornerRadius = 2;
const canctx = can.getContext("2d") as CanvasRenderingContext2D;
-
canctx.fillStyle = color;
canctx.strokeStyle = borderColor;
canctx.lineJoin = "round";
- canctx.lineWidth = bord
- roundRect(canctx, bord / 2, bord / 2, width - bord, height - bord, cornerRadius)
+ canctx.lineWidth = bord;
+ roundRect(
+ canctx,
+ bord / 2,
+ bord / 2,
+ width - bord,
+ height - bord,
+ cornerRadius,
+ );
canctx.fill();
canctx.stroke();
@@ -1864,17 +2110,15 @@ function roundRect(ctx, x, y, width, height, radius) {
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
-
}
function drawRedSquare(ctx, x, y, width, height) {
- ctx.fillStyle = 'red'
+ ctx.fillStyle = "red";
ctx.fillRect(x, y, width, height);
}
-
function drawIMG(ctx, img, size, x, y) {
- const key = "svg" + img + "_" + size + '_' + img.complete;
+ const key = "svg" + img + "_" + size + "_" + img.complete;
if (!cachedGraphics[key]) {
const can = document.createElement("canvas");
@@ -1890,11 +2134,15 @@ function drawIMG(ctx, img, size, x, y) {
cachedGraphics[key] = can;
}
- ctx.drawImage(cachedGraphics[key], Math.round(x - size / 2), Math.round(y - size / 2),);
+ 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;
+ const key = "text" + text + "_" + color + "_" + fontSize + "_" + left;
if (!cachedGraphics[key]) {
const can = document.createElement("canvas");
@@ -1902,7 +2150,7 @@ function drawText(ctx, text, color, fontSize, x, y, left = false) {
can.height = fontSize;
const canctx = can.getContext("2d") as CanvasRenderingContext2D;
canctx.fillStyle = color;
- canctx.textAlign = left ? 'left' : "center"
+ canctx.textAlign = left ? "left" : "center";
canctx.textBaseline = "middle";
canctx.font = fontSize + "px monospace";
@@ -1910,17 +2158,24 @@ function drawText(ctx, text, color, fontSize, x, y, left = false) {
cachedGraphics[key] = can;
}
- ctx.drawImage(cachedGraphics[key], left ? x : Math.round(x - cachedGraphics[key].width / 2), Math.round(y - cachedGraphics[key].height / 2),);
+ ctx.drawImage(
+ cachedGraphics[key],
+ left ? x : Math.round(x - cachedGraphics[key].width / 2),
+ Math.round(y - cachedGraphics[key].height / 2),
+ );
}
function pixelsToPan(pan) {
return (pan - offsetX) / gameZoneWidth;
}
-let lastComboPlayed = NaN, shepard = 6;
+let lastComboPlayed = NaN,
+ shepard = 6;
function playShepard(delta, pan, volume) {
- const shepardMax = 11, factor = 1.05945594920268, baseNote = 392;
+ const shepardMax = 11,
+ factor = 1.05945594920268,
+ baseNote = 392;
shepard += delta;
if (shepard > shepardMax) shepard = 0;
if (shepard < 0) shepard = shepardMax;
@@ -1959,19 +2214,23 @@ const sounds = {
comboDecrease() {
if (!isSettingOn("sound")) return;
playShepard(-1, 0.5, 0.5);
- }, coinBounce: (pan, volume) => {
+ },
+ coinBounce: (pan, volume) => {
if (!isSettingOn("sound")) return;
- createSingleBounceSound(1200, pixelsToPan(pan), volume, 0.1, 'triangle');
- }, explode: (pan) => {
+ createSingleBounceSound(1200, pixelsToPan(pan), volume, 0.1, "triangle");
+ },
+ explode: (pan) => {
if (!isSettingOn("sound")) return;
createExplosionSound(pixelsToPan(pan));
- }, revive: () => {
+ },
+ revive: () => {
if (!isSettingOn("sound")) return;
createRevivalSound(500);
- }, coinCatch(pan) {
+ },
+ coinCatch(pan) {
if (!isSettingOn("sound")) return;
- createSingleBounceSound(900, pixelsToPan(pan), .8, 0.1, 'triangle')
- }
+ createSingleBounceSound(900, pixelsToPan(pan), 0.8, 0.1, "triangle");
+ },
};
// How to play the code on the leftconst context = new window.AudioContext();
@@ -1980,12 +2239,18 @@ let audioContext, audioRecordingTrack;
function getAudioContext() {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
- audioRecordingTrack = audioContext.createMediaStreamDestination()
+ audioRecordingTrack = audioContext.createMediaStreamDestination();
}
return audioContext;
}
-function createSingleBounceSound(baseFreq = 800, pan = 0.5, volume = 1, duration = 0.1, type = "sine") {
+function createSingleBounceSound(
+ baseFreq = 800,
+ pan = 0.5,
+ volume = 1,
+ duration = 0.1,
+ type = "sine",
+) {
const context = getAudioContext();
// Frequency for the metal "ping"
const baseFrequency = baseFreq; // Hz
@@ -2008,7 +2273,10 @@ function createSingleBounceSound(baseFreq = 800, pan = 0.5, volume = 1, duration
// 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
+ gainNode.gain.exponentialRampToValueAtTime(
+ 0.001,
+ context.currentTime + duration,
+ ); // Quick decay
// Start the oscillator
oscillator.start(context.currentTime);
@@ -2021,7 +2289,11 @@ function createRevivalSound(baseFreq = 440) {
const context = getAudioContext();
// Create multiple oscillators for a richer sound
- const oscillators = [context.createOscillator(), context.createOscillator(), context.createOscillator(),];
+ const oscillators = [
+ context.createOscillator(),
+ context.createOscillator(),
+ context.createOscillator(),
+ ];
// Set the type and frequency for each oscillator
oscillators.forEach((osc, index) => {
@@ -2110,32 +2382,45 @@ function createExplosionSound(pan = 0.5) {
let levelTime = 0;
setInterval(() => {
- document.body.className = (running ? " running " : " paused ");
+ document.body.className = running ? " running " : " paused ";
}, 100);
window.addEventListener("visibilitychange", () => {
if (document.hidden) {
- pause(true)
+ pause(true);
}
});
const scoreDisplay = document.getElementById("score");
-let alertsOpen = 0, closeModal = null
+let alertsOpen = 0,
+ closeModal = null;
-function asyncAlert({
- title,
- text,
- actions = [{text: "OK", value: "ok", help: "", disabled: false, icon: ''}],
- allowClose = true,
- textAfterButtons = ''
- }) {
- alertsOpen++
+function asyncAlert({
+ title,
+ text,
+ actions,
+ allowClose = true,
+ textAfterButtons = "",
+ }: {
+ title?: string;
+ text?: string;
+ actions?: {
+ text?: string;
+ value?: t;
+ help?: string;
+ disabled?: boolean;
+ icon?: string;
+ }[];
+ textAfterButtons?: string;
+ allowClose?: boolean;
+}): Promise {
+ alertsOpen++;
return new Promise((resolve) => {
const popupWrap = document.createElement("div");
document.body.appendChild(popupWrap);
popupWrap.className = "popup";
- function closeWithResult(value) {
+ function closeWithResult(value: t | void) {
resolve(value);
// Doing this async lets the menu scroll persist if it's shown a second time
setTimeout(() => {
@@ -2145,16 +2430,16 @@ function asyncAlert({
if (allowClose) {
const closeButton = document.createElement("button");
- closeButton.title = "close"
- closeButton.className = "close-modale"
- closeButton.addEventListener('click', (e) => {
- e.preventDefault()
- closeWithResult(null)
- })
+ closeButton.title = "close";
+ closeButton.className = "close-modale";
+ closeButton.addEventListener("click", (e) => {
+ e.preventDefault();
+ closeWithResult(null);
+ });
closeModal = () => {
- closeWithResult(null)
- }
- popupWrap.appendChild(closeButton)
+ closeWithResult(null);
+ };
+ popupWrap.appendChild(closeButton);
}
const popup = document.createElement("div");
@@ -2171,46 +2456,51 @@ function asyncAlert({
popup.appendChild(p);
}
- actions.filter(i => i).forEach(({text, value, help, disabled, icon = ''}) => {
- const button = document.createElement("button");
+ actions
+ .filter((i) => i)
+ .forEach(({text, value, help, disabled, icon = ""}) => {
+ const button = document.createElement("button");
- button.innerHTML = `
+ button.innerHTML = `
${icon}
${text}
- ${help || ''}
+ ${help || ""}
`;
-
- if (disabled) {
- button.setAttribute("disabled", "disabled");
- } else {
- button.addEventListener("click", (e) => {
- e.preventDefault();
- closeWithResult(value)
- });
- }
- popup.appendChild(button);
- });
+ if (disabled) {
+ button.setAttribute("disabled", "disabled");
+ } else {
+ button.addEventListener("click", (e) => {
+ e.preventDefault();
+ closeWithResult(value);
+ });
+ }
+ popup.appendChild(button);
+ });
if (textAfterButtons) {
const p = document.createElement("div");
- p.className = 'textAfterButtons'
+ p.className = "textAfterButtons";
p.innerHTML = textAfterButtons;
popup.appendChild(p);
}
-
popupWrap.appendChild(popup);
- (popup.querySelector('button:not([disabled])') as HTMLButtonElement)?.focus()
- }).then((v) => {
- alertsOpen--
- closeModal=null
- return v
- }, ()=>{
- closeModal=null
- alertsOpen--
- })
+ (
+ popup.querySelector("button:not([disabled])") as HTMLButtonElement
+ )?.focus();
+ }).then(
+ (v: t | null) => {
+ alertsOpen--;
+ closeModal = null;
+ return v;
+ },
+ () => {
+ closeModal = null;
+ alertsOpen--;
+ },
+ );
}
// Settings
@@ -2219,7 +2509,9 @@ let cachedSettings = {};
function isSettingOn(key) {
if (typeof cachedSettings[key] == "undefined") {
try {
- cachedSettings[key] = JSON.parse(localStorage.getItem("breakout-settings-enable-" + key),);
+ cachedSettings[key] = JSON.parse(
+ localStorage.getItem("breakout-settings-enable-" + key),
+ );
} catch (e) {
console.warn(e);
}
@@ -2238,29 +2530,37 @@ function toggleSetting(key) {
if (options[key].afterChange) options[key].afterChange();
}
-
scoreDisplay.addEventListener("click", async (e) => {
e.preventDefault();
- openScorePanel()
+ openScorePanel();
});
async function openScorePanel() {
- pause(true)
+ pause(true);
const cb = await asyncAlert({
- title: ` ${score} points at level ${currentLevel + 1} / ${max_levels()}`, text: `
+ title: ` ${score} points at level ${currentLevel + 1} / ${max_levels()}`,
+ text: `
Upgrades picked so far :
${pickedUpgradesHTMl()}
- `, allowClose: true, actions: [{
- text: 'Resume', help: "Return to your run",
- }, {
- text: "Restart", help: "Start a brand new run.", value: () => {
- restart();
- return true;
+ `,
+ allowClose: true,
+ actions: [
+ {
+ text: "Resume",
+ help: "Return to your run",
},
- }],
+ {
+ text: "Restart",
+ help: "Start a brand new run.",
+ value: () => {
+ restart();
+ return true;
+ },
+ },
+ ],
});
if (cb) {
- await cb()
+ await cb();
}
}
@@ -2269,149 +2569,137 @@ document.getElementById("menu").addEventListener("click", (e) => {
openSettingsPanel();
});
-
const options = {
sound: {
- default: true, name: `Game sounds`, help: `Can slow down some phones.`, disabled: () => false
- }, "mobile-mode": {
+ default: true,
+ name: `Game sounds`,
+ help: `Can slow down some phones.`,
+ disabled: () => false,
+ },
+ "mobile-mode": {
default: window.innerHeight > window.innerWidth,
name: `Mobile mode`,
help: `Leaves space for your thumb.`,
afterChange() {
fitSize();
},
- disabled: () => false
- }, basic: {
- default: false, name: `Basic graphics`, help: `Better performance on older devices.`, disabled: () => false
- }, pointerLock: {
+ disabled: () => false,
+ },
+ basic: {
+ default: false,
+ name: `Basic graphics`,
+ help: `Better performance on older devices.`,
+ disabled: () => false,
+ },
+ pointerLock: {
default: false,
name: `Mouse pointer lock`,
help: `Locks and hides the mouse cursor.`,
- disabled: () => !canvas.requestPointerLock
- }, "easy": {
+ disabled: () => !canvas.requestPointerLock,
+ },
+ easy: {
default: false,
name: `Kids mode`,
help: `Start future runs with "slower ball".`,
- disabled: () => false
+ 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": {
+ record: {
default: false,
name: `Record gameplay videos`,
help: `Get a video of each level.`,
disabled() {
- return window.location.search.includes('isInWebView=true')
- }
- }
+ return window.location.search.includes("isInWebView=true");
+ },
+ },
};
async function openSettingsPanel() {
-
- pause(true)
+ pause(true);
const optionsList = [];
for (const key in options) {
- if (options[key]) optionsList.push({
- disabled: options[key].disabled(),
- icon: isSettingOn(key) ? icons['icon:checkmark_checked'] : icons['icon:checkmark_unchecked'],
- text: options[key].name,
- help: options[key].help,
- value: () => {
- toggleSetting(key)
- openSettingsPanel();
- },
- });
+ if (options[key])
+ optionsList.push({
+ disabled: options[key].disabled(),
+ icon: isSettingOn(key)
+ ? icons["icon:checkmark_checked"]
+ : icons["icon:checkmark_unchecked"],
+ text: options[key].name,
+ help: options[key].help,
+ value: () => {
+ toggleSetting(key);
+ openSettingsPanel();
+ },
+ });
}
- const cb = await asyncAlert({
- title: "Breakout 71", text: `
- `, allowClose: true, actions: [{
- text: 'Resume', help: "Return to your run", async value() {
-
- }
- }, {
- text: 'Starting perk', help: "Try perks and levels you unlocked", async value() {
- const ts = getTotalScore()
- const actions = [...upgrades
- .sort((a, b) => a.threshold - b.threshold)
- .map(({
- name, help, id, threshold, icon, fullHelp
- }) => ({
- text: name,
- help: ts >= threshold ? fullHelp || help : `Unlocks at total score ${threshold}.`,
- disabled: ts < threshold,
- value: {perks: {[id]: 1}},
- icon: icons['icon:' + id]
- }))
-
- , ...allLevels
- .sort((a, b) => a.threshold - b.threshold)
- .map((l, li) => {
- const avaliable = ts >= l.threshold
- return ({
- text: l.name,
- help: avaliable ? `A ${l.size}x${l.size} level with ${l.bricks.filter(i => i).length} bricks` : `Unlocks at total score ${l.threshold}.`,
- disabled: !avaliable,
- value: {level: l.name},
- icon: icons[l.name]
- })
- })]
-
- const tryOn = await asyncAlert({
- title: `You unlocked ${Math.round(actions.filter(a => !a.disabled).length / actions.length * 100)}% of the game.`,
- text: `
- Your total score is ${ts}. Below are all the upgrades and levels the games has to offer. They greyed out ones can be unlocked by increasing your total score.
- `,
- textAfterButtons: `
-The total score increases every time you score in game.
-Your high score is ${highScore}.
-Click an item above to start a run with it.
-
`,
- actions,
- allowClose: true,
- })
- if (tryOn) {
- if (!currentLevel || await asyncAlert({
- title: 'Restart run to try this item?',
- text: 'You\'re about to start a new run with the selected unlocked item, is that really what you wanted ? ',
- actions: [{
- value: true, text: 'Restart game to test item'
- }, {
- value: false, text: 'Cancel'
- }]
- })) nextRunOverrides = tryOn
- restart()
- }
- }
- },
+ const cb = await asyncAlert<() => void>({
+ title: "Breakout 71",
+ text: `
+ `,
+ allowClose: true,
+ actions: [
+ {
+ text: "Resume",
+ help: "Return to your run",
+ async value() {
+ },
+ },
+ {
+ text: "Starting perk",
+ help: "Try perks and levels you unlocked",
+ async value() {
+ openUnlocksList()
+ },
+ },
...optionsList,
- (document.fullscreenEnabled || document.webkitFullscreenEnabled) && (document.fullscreenElement !== null ? {
- text: "Exit Fullscreen",
- icon:icons['icon:exit_fullscreen'],
- help: "Might not work on some machines", value() {
- toggleFullScreen()
+ (document.fullscreenEnabled || document.webkitFullscreenEnabled) &&
+ (document.fullscreenElement !== null
+ ? {
+ text: "Exit Fullscreen",
+ icon: icons["icon:exit_fullscreen"],
+ help: "Might not work on some machines",
+ value() {
+ toggleFullScreen();
+ },
}
- } : {
- icon:icons['icon:fullscreen'],
- text: "Fullscreen", help: "Might not work on some machines", value() {
- toggleFullScreen()
- }
- }), {
- text: 'Reset Game', help: "Erase high score and statistics", async value() {
- if (await asyncAlert({
- title: 'Reset', actions: [{
- text: 'Yes', value: true
- }, {
- text: 'No', value: false
- }], allowClose: true,
- })) {
- localStorage.clear()
- window.location.reload()
+ : {
+ icon: icons["icon:fullscreen"],
+ text: "Fullscreen",
+ help: "Might not work on some machines",
+ value() {
+ toggleFullScreen();
+ },
+ }),
+ {
+ text: "Reset Game",
+ help: "Erase high score and statistics",
+ async value() {
+ if (
+ await asyncAlert({
+ title: "Reset",
+ actions: [
+ {
+ text: "Yes",
+ value: true,
+ },
+ {
+ text: "No",
+ value: false,
+ },
+ ],
+ allowClose: true,
+ })
+ ) {
+ localStorage.clear();
+ window.location.reload();
}
-
- }
- }], textAfterButtons: `
+ },
+ },
+ ],
+ textAfterButtons: `
Made in France by Renan LE CARO .
Privacy Policy
@@ -2423,44 +2711,115 @@ Click an item above to start a run with it.
HackerNews
v.${appVersion}
- `
- })
+ `,
+ });
if (cb) {
- cb()
+ cb();
+ }
+}
+
+async function openUnlocksList() {
+
+ const ts = getTotalScore();
+ const actions = [
+ ...upgrades
+ .sort((a, b) => a.threshold - b.threshold)
+ .map(({name, id, threshold, icon, fullHelp}) => ({
+ text: name,
+ help:
+ ts >= threshold
+ ? fullHelp
+ : `Unlocks at total score ${threshold}.`,
+ disabled: ts < threshold,
+ value: {perk: id} as RunOverrides,
+ icon,
+ })),
+ ...allLevels
+ .sort((a, b) => a.threshold - b.threshold)
+ .map((l, li) => {
+ const available = ts >= l.threshold;
+ return {
+ text: l.name,
+ help: available
+ ? `A ${l.size}x${l.size} level with ${l.bricks.filter((i) => i).length} bricks`
+ : `Unlocks at total score ${l.threshold}.`,
+ disabled: !available,
+ value: {level: l.name} as RunOverrides,
+ icon: icons[l.name],
+ };
+ }),
+ ];
+
+ const tryOn = await asyncAlert({
+ title: `You unlocked ${Math.round((actions.filter((a) => !a.disabled).length / actions.length) * 100)}% of the game.`,
+ text: `
+ Your total score is ${ts}. Below are all the upgrades and levels the games has to offer. They greyed out ones can be unlocked by increasing your total score.
+ `,
+ textAfterButtons: `
+The total score increases every time you score in game.
+Your high score is ${highScore}.
+Click an item above to start a run with it.
+
`,
+ actions,
+ allowClose: true,
+ });
+ if (tryOn) {
+ if (
+ !currentLevel ||
+ (await asyncAlert({
+ title: "Restart run to try this item?",
+ text: "You're about to start a new run with the selected unlocked item, is that really what you wanted ? ",
+ actions: [
+ {
+ value: true,
+ text: "Restart game to test item",
+ },
+ {
+ value: false,
+ text: "Cancel",
+ },
+ ],
+ }))
+ ) {
+ nextRunOverrides = tryOn;
+ restart();
+ }
}
}
function distance2(a, b) {
- return Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2)
+ 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))
+ return Math.sqrt(distance2(a, b));
}
function rainbowColor() {
- return `hsl(${Math.round((levelTime / 4)) * 2 % 360},100%,70%)`
+ return `hsl(${(Math.round(levelTime / 4) * 2) % 360},100%,70%)`;
}
function repulse(a, b, power, impactsBToo) {
-
- const distance = distanceBetween(a, b)
+ const distance = distanceBetween(a, b);
// Ensure we don't get soft locked
- const max = gameZoneWidth / 2
- if (distance > max) return
+ const max = 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, levelTime) / 500
+ 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, levelTime)) /
+ 500;
if (impactsBToo) {
- b.vx += dx * fact
- b.vy += dy * fact
+ b.vx += dx * fact;
+ b.vy += dy * fact;
}
- a.vx -= dx * fact
- a.vy -= dy * fact
+ a.vx -= dx * fact;
+ a.vy -= dy * fact;
- const speed = 10
- const rand = 2
+ const speed = 10;
+ const rand = 2;
flashes.push({
type: "particle",
duration: 100,
@@ -2472,9 +2831,8 @@ function repulse(a, b, power, impactsBToo) {
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) {
-
flashes.push({
type: "particle",
duration: 100,
@@ -2486,29 +2844,28 @@ function repulse(a, b, power, impactsBToo) {
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)
+ const distance = distanceBetween(a, b);
// Ensure we don't get soft locked
- const min = gameZoneWidth * .5
- if (distance < min) return
+ const min = gameZoneWidth * 0.5;
+ if (distance < min) return;
// Unit vector
- const dx = (a.x - b.x) / distance
- const dy = (a.y - b.y) / distance
+ const dx = (a.x - b.x) / distance;
+ const dy = (a.y - b.y) / distance;
- const fact = power * (distance - min) / min * Math.min(500, levelTime) / 500
- b.vx += dx * fact
- b.vy += dy * fact
- a.vx -= dx * fact
- a.vy -= dy * fact
+ const fact =
+ (((power * (distance - min)) / min) * Math.min(500, levelTime)) / 500;
+ b.vx += dx * fact;
+ b.vy += dy * fact;
+ a.vx -= dx * fact;
+ a.vy -= dy * fact;
- const speed = 10
- const rand = 2
+ const speed = 10;
+ const rand = 2;
flashes.push({
type: "particle",
duration: 100,
@@ -2520,7 +2877,7 @@ function attract(a, b, power) {
y: a.y,
vx: dx * speed + a.vx + (Math.random() - 0.5) * rand,
vy: dy * speed + a.vy + (Math.random() - 0.5) * rand,
- })
+ });
flashes.push({
type: "particle",
duration: 100,
@@ -2532,149 +2889,166 @@ function attract(a, b, power) {
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, recordCanvas, recordCanvasCtx
-
+let mediaRecorder, captureStream, recordCanvas, recordCanvasCtx;
function recordOneFrame() {
- if (!isSettingOn('record')) {
- return
+ if (!isSettingOn("record")) {
+ return;
}
if (!running) return;
if (!captureStream) return;
- drawMainCanvasOnSmallCanvas()
+ drawMainCanvasOnSmallCanvas();
if (captureStream.requestFrame) {
- captureStream.requestFrame()
+ captureStream.requestFrame();
} else {
- captureStream.getVideoTracks()[0].requestFrame()
+ captureStream.getVideoTracks()[0].requestFrame();
}
}
-
function drawMainCanvasOnSmallCanvas() {
- if (!recordCanvasCtx) return
- recordCanvasCtx.drawImage(canvas, offsetXRoundedDown, 0, gameZoneWidthRoundedUp, gameZoneHeight, 0, 0, recordCanvas.width, recordCanvas.height)
+ if (!recordCanvasCtx) return;
+ recordCanvasCtx.drawImage(
+ canvas,
+ offsetXRoundedDown,
+ 0,
+ gameZoneWidthRoundedUp,
+ 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'
+ // 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(score.toString(), recordCanvas.width - 12, 12)
+ recordCanvasCtx.fillText(score.toString(), recordCanvas.width - 12, 12);
recordCanvasCtx.textAlign = "left";
- recordCanvasCtx.fillText('Level ' + (currentLevel + 1) + '/' + max_levels(), 12, 12)
+ recordCanvasCtx.fillText(
+ "Level " + (currentLevel + 1) + "/" + max_levels(),
+ 12,
+ 12,
+ );
}
function startRecordingGame() {
- if (!isSettingOn('record')) {
- return
+ if (!isSettingOn("record")) {
+ return;
}
if (!recordCanvas) {
// Smaller canvas with less details
- recordCanvas = document.createElement("canvas")
- recordCanvasCtx = recordCanvas.getContext("2d", {antialias: false, alpha: false})
+ recordCanvas = document.createElement("canvas");
+ recordCanvasCtx = recordCanvas.getContext("2d", {
+ antialias: false,
+ alpha: false,
+ });
captureStream = recordCanvas.captureStream(0);
- if (isSettingOn('sound') && getAudioContext() && audioRecordingTrack) {
-
- captureStream.addTrack(audioRecordingTrack.stream.getAudioTracks()[0])
+ if (isSettingOn("sound") && getAudioContext() && audioRecordingTrack) {
+ captureStream.addTrack(audioRecordingTrack.stream.getAudioTracks()[0]);
// captureStream.addTrack(audioRecordingTrack.stream.getAudioTracks()[1])
}
}
- recordCanvas.width = gameZoneWidthRoundedUp
- recordCanvas.height = gameZoneHeight
+ recordCanvas.width = gameZoneWidthRoundedUp;
+ recordCanvas.height = gameZoneHeight;
// drawMainCanvasOnSmallCanvas()
const recordedChunks = [];
-
- const instance = new MediaRecorder(captureStream, {videoBitsPerSecond: 3500000});
- mediaRecorder = instance
+ const instance = new MediaRecorder(captureStream, {
+ videoBitsPerSecond: 3500000,
+ });
+ mediaRecorder = instance;
instance.start();
- mediaRecorder.pause()
+ 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
+ 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))
+ 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
+ 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;
// targetDiv.style.width = recordCanvas.width + 'px'
// targetDiv.style.height = recordCanvas.height + 'px'
- video.loop = true
- video.muted = true
- video.playsinline = true
+ 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 = `Download video (${(blob.size / 1000000).toFixed(2)}MB)`
- targetDiv.appendChild(video)
- targetDiv.appendChild(a)
-
- }
-
-
+ const a = document.createElement("a");
+ a.download = captureFileName("webm");
+ a.target = "_blank";
+ a.href = video.src;
+ a.textContent = `Download video (${(blob.size / 1000000).toFixed(2)}MB)`;
+ targetDiv.appendChild(video);
+ targetDiv.appendChild(a);
+ };
}
function pauseRecording() {
- if (!isSettingOn('record')) {
- return
+ if (!isSettingOn("record")) {
+ return;
}
- if (mediaRecorder?.state === 'recording') {
- mediaRecorder?.pause()
+ if (mediaRecorder?.state === "recording") {
+ mediaRecorder?.pause();
}
}
function resumeRecording() {
- if (!isSettingOn('record')) {
- return
+ if (!isSettingOn("record")) {
+ return;
}
- if (mediaRecorder?.state === 'paused') {
- mediaRecorder.resume()
+ if (mediaRecorder?.state === "paused") {
+ mediaRecorder.resume();
}
-
}
function stopRecording() {
- if (!isSettingOn('record')) {
- return
+ if (!isSettingOn("record")) {
+ return;
}
if (!mediaRecorder) return;
- mediaRecorder?.stop()
- mediaRecorder = null
+ mediaRecorder?.stop();
+ mediaRecorder = null;
}
function captureFileName(ext) {
- return "breakout-71-capture-" + new Date().toISOString().replace(/[^0-9\-]+/gi, '-') + '.' + ext
+ 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]
- }
-
+ let i = arr.length;
+ while (--i)
+ if (predicate(arr[i], i, arr)) {
+ return arr[i];
+ }
}
function toggleFullScreen() {
@@ -2686,68 +3060,80 @@ function toggleFullScreen() {
document.webkitCancelFullScreen();
}
} else {
- const docel = document.documentElement
+ const docel = document.documentElement;
if (docel.requestFullscreen) {
docel.requestFullscreen();
} else if (docel.webkitRequestFullscreen) {
docel.webkitRequestFullscreen();
}
}
-
} catch (e) {
- console.warn(e)
+ console.warn(e);
}
}
const pressed = {
- ArrowLeft: 0, ArrowRight: 0, Shift: 0
-}
+ ArrowLeft: 0,
+ ArrowRight: 0,
+ Shift: 0,
+};
function setKeyPressed(key, on) {
- pressed[key] = on
- keyboardPuckSpeed = (pressed.ArrowRight - pressed.ArrowLeft) * (1 + pressed.Shift * 2) * gameZoneWidth / 50
+ pressed[key] = on;
+ keyboardPuckSpeed =
+ ((pressed.ArrowRight - pressed.ArrowLeft) *
+ (1 + pressed.Shift * 2) *
+ gameZoneWidth) /
+ 50;
}
-document.addEventListener('keydown', e => {
- if (e.key.toLowerCase() === 'f' && !e.ctrlKey && !e.metaKey) {
- toggleFullScreen()
+document.addEventListener("keydown", (e) => {
+ if (e.key.toLowerCase() === "f" && !e.ctrlKey && !e.metaKey) {
+ toggleFullScreen();
} else if (e.key in pressed) {
- setKeyPressed(e.key, 1)
+ setKeyPressed(e.key, 1);
}
- if (e.key === ' ' && !alertsOpen) {
+ if (e.key === " " && !alertsOpen) {
if (running) {
- pause()
+ pause(true);
} else {
- play()
+ play();
}
} else {
- return
+ return;
}
- e.preventDefault()
-})
+ e.preventDefault();
+});
-document.addEventListener('keyup', e => {
+document.addEventListener("keyup", (e) => {
+ const focused = document.querySelector("button:focus")
if (e.key in pressed) {
- setKeyPressed(e.key, 0)
- } else if (e.key === 'ArrowDown' && document.querySelector('button:focus')?.nextElementSibling.tagName === 'BUTTON') {
- document.querySelector('button:focus')?.nextElementSibling?.focus()
- } else if (e.key === 'ArrowUp' && document.querySelector('button:focus')?.previousElementSibling.tagName === 'BUTTON') {
- document.querySelector('button:focus')?.previousElementSibling?.focus()
- } else if (e.key === 'Escape' && closeModal) {
- closeModal()
- } else if (e.key === 'Escape' && running) {
- pause()
- } else if (e.key.toLowerCase() === 'm' && !alertsOpen) {
- openSettingsPanel()
- } else if (e.key.toLowerCase() === 's' && !alertsOpen) {
- openScorePanel()
+ setKeyPressed(e.key, 0);
+ } else if (
+ e.key === "ArrowDown" && focused?.nextElementSibling?.tagName === "BUTTON"
+ ) {
+ (focused?.nextElementSibling as HTMLButtonElement)?.focus();
+ } else if (
+ e.key === "ArrowUp" &&
+ focused?.previousElementSibling?.tagName ===
+ "BUTTON"
+ ) {
+ (focused?.previousElementSibling as HTMLButtonElement)?.focus();
+
+ } else if (e.key === "Escape" && closeModal) {
+ closeModal();
+ } else if (e.key === "Escape" && running) {
+ pause(true);
+ } else if (e.key.toLowerCase() === "m" && !alertsOpen) {
+ openSettingsPanel().then();
+ } else if (e.key.toLowerCase() === "s" && !alertsOpen) {
+ openScorePanel().then();
} else {
- return
+ return;
}
- e.preventDefault()
-})
+ e.preventDefault();
+});
-
-fitSize()
-restart()
-tick();
\ No newline at end of file
+fitSize();
+restart();
+tick();
diff --git a/src/index.html b/src/index.html
index f4a837f..9417e26 100644
--- a/src/index.html
+++ b/src/index.html
@@ -8,18 +8,24 @@
/>
Breakout 71
-
+
-
+
-