From 3cb662bc92a0501b97409dcda1f88985b9c2d9bf Mon Sep 17 00:00:00 2001 From: Renan LE CARO Date: Thu, 6 Mar 2025 16:46:25 +0100 Subject: [PATCH] Tried to use ts to catch bugs, it's pretty useless for now. --- Readme.md | 13 +- deploy.sh | 2 + dist/index.html | 1213 ++++++++++++------------ src/game.ts | 2188 +++++++++++++++++++++++++------------------ src/index.html | 14 +- src/levels.json | 8 +- src/loadGameData.ts | 122 ++- src/palette.json | 2 +- src/rawUpgrades.ts | 668 +++++++------ src/style.css | 398 ++++---- src/types.d.ts | 119 ++- 11 files changed, 2631 insertions(+), 2116 deletions(-) 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 - + - + - diff --git a/src/levels.json b/src/levels.json index 1978857..5997153 100644 --- a/src/levels.json +++ b/src/levels.json @@ -807,14 +807,14 @@ }, { "name": "icon:checkmark_checked", - "size": 4, - "bricks": "WWWWWttWWttWWWWW", + "size": 6, + "bricks": "_WWWWGWBBBGGGGBGGWWGGGBWWBGBBW_WWWW_", "svg": "" }, { "name": "icon:checkmark_unchecked", - "size": 4, - "bricks": "WWWWW__WW__WWWWW", + "size": 6, + "bricks": "_WWWW_WBBBBWWBBBBWWBBBBWWBBBBW_WWWW_", "svg": "" }, { diff --git a/src/loadGameData.ts b/src/loadGameData.ts index 3ad8407..9da904a 100644 --- a/src/loadGameData.ts +++ b/src/loadGameData.ts @@ -1,74 +1,96 @@ -import {Level, Palette, RawLevel, Upgrade} from "./types"; +import { Level, Palette, RawLevel, Upgrade } from "./types"; import _palette from "./palette.json"; import _rawLevelsList from "./levels.json"; import _appVersion from "./version.json"; -import {rawUpgrades} from "./rawUpgrades"; +import { rawUpgrades } from "./rawUpgrades"; const palette = _palette as Palette; -const rawLevelsList = _rawLevelsList as RawLevel[] +const rawLevelsList = _rawLevelsList as RawLevel[]; export const appVersion = _appVersion as string; +const randomPatterns = [ + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, +]; -const randomPatterns = [``, ``, ``, ``, ``, ``, ``, ``, ``, ``, ``,] +let attributed = 0; -let attributed = 0 - - -let levelIconHTMLCanvas = document.createElement('canvas') +let levelIconHTMLCanvas = document.createElement("canvas"); const levelIconHTMLCanvasCtx = levelIconHTMLCanvas.getContext("2d", { - antialias: false, - alpha: true + antialias: false, + alpha: true, }) as CanvasRenderingContext2D; +function levelIconHTML( + bricks: string[], + levelSize: number, + levelName: string, + color: string, +) { + const size = 40; + const c = levelIconHTMLCanvas; + const ctx = levelIconHTMLCanvasCtx; + c.width = size; + c.height = size; -function levelIconHTML(bricks: string[], levelSize: number, levelName: string, color: string) { - const size = 40 - const c = levelIconHTMLCanvas - const ctx = levelIconHTMLCanvasCtx - c.width = size - c.height = size - - if (color) { - ctx.fillStyle = color - ctx.fillRect(0, 0, size, size) - } else { - ctx.clearRect(0, 0, size, size) + if (color) { + ctx.fillStyle = color; + ctx.fillRect(0, 0, size, size); + } else { + ctx.clearRect(0, 0, size, size); + } + const pxSize = size / levelSize; + for (let x = 0; x < levelSize; x++) { + for (let y = 0; y < levelSize; y++) { + const c = bricks[y * levelSize + x]; + if (c) { + ctx.fillStyle = c; + ctx.fillRect( + Math.floor(pxSize * x), + Math.floor(pxSize * y), + Math.ceil(pxSize), + Math.ceil(pxSize), + ); + } } - const pxSize = size / levelSize - for (let x = 0; x < levelSize; x++) { - for (let y = 0; y < levelSize; y++) { - const c = bricks[y * levelSize + x] - if (c) { - ctx.fillStyle = c - ctx.fillRect(Math.floor(pxSize * x), Math.floor(pxSize * y), Math.ceil(pxSize), Math.ceil(pxSize)) - } - } - } - // I don't think many blind people will benefit for this but it's nice to have something to put in "alt" - return `${levelName}` + } + // I don't think many blind people will benefit for this but it's nice to have something to put in "alt" + return `${levelName}`; } -export const icons = {} +export const icons = {}; -export const allLevels = rawLevelsList.map(level => { - const bricks = level.bricks.split('').map(c => palette[c]) - const icon = levelIconHTML(bricks, level.size, level.name, level.color) - icons[level.name] = icon +export const allLevels = rawLevelsList + .map((level) => { + const bricks = level.bricks.split("").map((c) => palette[c]); + const icon = levelIconHTML(bricks, level.size, level.name, level.color); + icons[level.name] = icon; let svg = level.svg; if (!level.color && !svg) { - svg = randomPatterns[attributed % randomPatterns.length] - attributed++ + svg = randomPatterns[attributed % randomPatterns.length]; + attributed++; } return { - ...level, - bricks, - icon, - svg - } + ...level, + bricks, + icon, + svg, + }; + }) + .filter((l) => !l.name.startsWith("icon:")) as Level[]; -}).filter(l => !l.name.startsWith('icon:')) as Level[] - - -export const upgrades = rawUpgrades.map(u => ({...u, icon: icons['icon:' + u.id]})) as Upgrade[] +export const upgrades = rawUpgrades.map((u) => ({ + ...u, + icon: icons["icon:" + u.id], +})) as Upgrade[]; diff --git a/src/palette.json b/src/palette.json index afd81d7..a3c33f0 100644 --- a/src/palette.json +++ b/src/palette.json @@ -21,4 +21,4 @@ "k": "#618227", "e": "#e1c8b4", "l": "#9b9fa4" -} \ No newline at end of file +} diff --git a/src/rawUpgrades.ts b/src/rawUpgrades.ts index 3244262..6108c8a 100644 --- a/src/rawUpgrades.ts +++ b/src/rawUpgrades.ts @@ -1,344 +1,402 @@ -export const rawUpgrades = [{ - requires: '', - "threshold": 0, +export const rawUpgrades = [ + { + requires: "", + threshold: 0, giftable: false, - "id": "extra_life", - "name": "+1 life", - "max": 7, - help: lvl => `Survive dropping the ball ${lvl} time${lvl > 1 ? 's' : ''}.`, + id: "extra_life", + name: "+1 life", + max: 7, + help: (lvl) => + `Survive dropping the ball ${lvl} time${lvl > 1 ? "s" : ""}.`, fullHelp: `Normally, you just have one life, and the run is over as soon as you drop it. - With this perk, you can survive dropping the ball once. A heart in the top right corner will remind you of how many extra lives you have. ` -}, { - requires: '', - "threshold": 0, - "id": "streak_shots", - "giftable": true, - "name": "Single puck hit streak", - "max": 1, - help: lvl => `More coins if you break many bricks at once`, + With this perk, you can survive dropping the ball once. A heart in the top right corner will remind you of how many extra lives you have. `, + }, + { + requires: "", + threshold: 0, + id: "streak_shots", + giftable: true, + name: "Single puck hit streak", + max: 1, + help: (lvl) => `More coins if you break many bricks at once`, fullHelp: `Every time you break a brick, your combo (number of coins per bricks) increases by one. However, as soon as the ball touches your puck, the combo is reset to its default value, and you'll just get one coin per brick. So you should try to hit many bricks in one go for more score. Once your combo rises above the base value, your puck will become red to remind you that it will destroy your combo to touch it with the ball. - This can stack with other combo related perks, the combo will rise faster but reset more easily as any of the conditions is enough to reset it. ` -}, + This can stack with other combo related perks, the combo will rise faster but reset more easily as any of the conditions is enough to reset it. `, + }, - { - requires: '', - "threshold": 0, - "id": "base_combo", - "giftable": true, - "name": "+3 base combo", - "max": 7, - help: lvl => `Every brick drops at least ${1 + lvl * 3} coins.`, - fullHelp: `Your combo (number of coins per bricks) normally starts at 1 at the beginning of the level, and resets to one when you bounce around without hitting anything. + { + requires: "", + threshold: 0, + id: "base_combo", + giftable: true, + name: "+3 base combo", + max: 7, + help: (lvl) => `Every brick drops at least ${1 + lvl * 3} coins.`, + fullHelp: `Your combo (number of coins per bricks) normally starts at 1 at the beginning of the level, and resets to one when you bounce around without hitting anything. With this perk, the combo starts 3 points higher, so you'll always get at least 4 coins per brick. Whenever your combo reset, it will go back to 4 and not 1. - Your ball will glitter a bit to indicate that its combo is higher than one.` - }, { - requires: '', - "threshold": 0, - giftable: false, - "id": "slow_down", - "name": "Slower ball", - "max": 2, - help: lvl => `Ball moves ${lvl > 1 ? 'even' : ''} more slowly.`, + Your ball will glitter a bit to indicate that its combo is higher than one.`, + }, + { + requires: "", + threshold: 0, + giftable: false, + id: "slow_down", + name: "Slower ball", + max: 2, + help: (lvl) => `Ball moves ${lvl > 1 ? "even" : ""} more slowly.`, - fullHelp: `The ball starts relatively slow, but every level of your run it will start a bit faster, and it will also accelerate if you spend a lot of time in one level. This perk makes it - more manageable. You can get it at the start every time by enabling kid mode in the menu.` - }, { - requires: '', - "threshold": 0, - giftable: false, - "id": "bigger_puck", - "name": "Bigger puck", - "max": 2, - help: lvl => `Easily catch ${lvl > 1 ? 'even' : ''} more coins.`, - fullHelp: `A bigger puck makes it easier to never miss the ball and to catch more coins, and also to precisely angle the bounces (the ball's angle only depends on where it hits the puck). - However, a large puck is harder to use around the sides of the level, and will make it sometimes unavoidable to miss (not hit anything) which comes with downsides. ` - }, { - requires: '', - "threshold": 0, - giftable: false, - "id": "viscosity", - "name": "Viscosity", - "max": 3, - help: lvl => `${lvl > 1 ? 'Even slower' : 'Slower'} coins fall.`, + fullHelp: `The ball starts relatively slow, but every level of your run it will start a bit faster, and it will also accelerate if you spend a lot of time in one level. This perk makes it + more manageable. You can get it at the start every time by enabling kid mode in the menu.`, + }, + { + requires: "", + threshold: 0, + giftable: false, + id: "bigger_puck", + name: "Bigger puck", + max: 2, + help: (lvl) => `Easily catch ${lvl > 1 ? "even" : ""} more coins.`, + fullHelp: `A bigger puck makes it easier to never miss the ball and to catch more coins, and also to precisely angle the bounces (the ball's angle only depends on where it hits the puck). + However, a large puck is harder to use around the sides of the level, and will make it sometimes unavoidable to miss (not hit anything) which comes with downsides. `, + }, + { + requires: "", + threshold: 0, + giftable: false, + id: "viscosity", + name: "Viscosity", + max: 3, + help: (lvl) => `${lvl > 1 ? "Even slower" : "Slower"} coins fall.`, - fullHelp: `Coins normally accelerate with gravity and explosions to pretty high speeds. This perk constantly makes them slow down, as if they were in some sort of viscous liquid. - This makes catching them easier, and combines nicely with perks that influence the coin's movement. ` - }, { - requires: '', - "threshold": 0, - "id": "sides_are_lava", - "giftable": true, - "name": "Shoot straight", - "max": 1, - help: lvl => `More coins if you don't touch the sides.`, + fullHelp: `Coins normally accelerate with gravity and explosions to pretty high speeds. This perk constantly makes them slow down, as if they were in some sort of viscous liquid. + This makes catching them easier, and combines nicely with perks that influence the coin's movement. `, + }, + { + requires: "", + threshold: 0, + id: "sides_are_lava", + giftable: true, + name: "Shoot straight", + max: 1, + help: (lvl) => `More coins if you don't touch the sides.`, - fullHelp: `Whenever you break a brick, your combo will increase by one, so you'll get one more coin all the next bricks you break. + fullHelp: `Whenever you break a brick, your combo will increase by one, so you'll get one more coin all the next bricks you break. However, your combo will reset as soon as your ball hits the left or right side. As soon as your combo rises, the sides become red to remind you that you should avoid hitting them. The effect stacks with other combo perks, combo rises faster with more upgrades but will also reset if any - of the reset conditions are met.` - }, { - requires: '', - "threshold": 0, - "id": "top_is_lava", - "giftable": true, - "name": "Sky is the limit", - "max": 1, - help: lvl => `More coins if you don't touch the top.`, + of the reset conditions are met.`, + }, + { + requires: "", + threshold: 0, + id: "top_is_lava", + giftable: true, + name: "Sky is the limit", + max: 1, + help: (lvl) => `More coins if you don't touch the top.`, - fullHelp: `Whenever you break a brick, your combo will increase by one. However, your combo will reset as soon as your ball hit the top of the screen. + fullHelp: `Whenever you break a brick, your combo will increase by one. However, your combo will reset as soon as your ball hit the top of the screen. When your combo is above the minimum, a red bar will appear at the top to remind you that you should avoid hitting it. - The effect stacks with other combo perks.` - }, { - requires: '', - "threshold": 0, - giftable: false, - "id": "skip_last", - "name": "Easy Cleanup", - "max": 7, - help: lvl => `The last ${lvl > 1 ? lvl + ' bricks' : 'brick'} left will self-destruct.`, - fullHelp: `You need to break all bricks to go to the next level. However, it can be hard to get the last ones. + The effect stacks with other combo perks.`, + }, + { + requires: "", + threshold: 0, + giftable: false, + id: "skip_last", + name: "Easy Cleanup", + max: 7, + help: (lvl) => + `The last ${lvl > 1 ? lvl + " bricks" : "brick"} left will self-destruct.`, + fullHelp: `You need to break all bricks to go to the next level. However, it can be hard to get the last ones. Clearing a level early brings extra choices when upgrading. Never missing the bricks is also very beneficial. - So if you find it difficult to break the last bricks, getting this perk a few time can help.` - }, { - requires: '', - "threshold": 500, - "id": "telekinesis", - "giftable": true, - "name": "Puck controls ball", - "max": 2, - help: lvl => lvl == 1 ? `Control the ball's trajectory.` : `Stronger effect on the ball`, - fullHelp: `Right after the ball hits your puck, you'll be able to direct it left and right by moving your puck. - The effect stops when the ball hits a brick and resets the next time it touches the puck. It also does nothing when the ball is going downward after bouncing at the top. ` - }, { - requires: '', - "threshold": 1000, - giftable: false, - "id": "coin_magnet", - "name": "Coins magnet", - "max": 3, - help: lvl => lvl == 1 ? `Puck attracts coins.` : `Stronger effect on the coins`, + So if you find it difficult to break the last bricks, getting this perk a few time can help.`, + }, + { + requires: "", + threshold: 500, + id: "telekinesis", + giftable: true, + name: "Puck controls ball", + max: 2, + help: (lvl) => + lvl == 1 + ? `Control the ball's trajectory.` + : `Stronger effect on the ball`, + fullHelp: `Right after the ball hits your puck, you'll be able to direct it left and right by moving your puck. + The effect stops when the ball hits a brick and resets the next time it touches the puck. It also does nothing when the ball is going downward after bouncing at the top. `, + }, + { + requires: "", + threshold: 1000, + giftable: false, + id: "coin_magnet", + name: "Coins magnet", + max: 3, + help: (lvl) => + lvl == 1 ? `Puck attracts coins.` : `Stronger effect on the coins`, - fullHelp: `Directs the coins to the puck. The effect is stronger if the coin is close to it already. Catching 90% or 100% of coins bring special bonuses in the game. - Another way to catch more coins is to hit bricks from the bottom. The ball's speed and direction impacts the spawned coin's velocity. ` - }, { - requires: '', - "threshold": 1500, - "id": "multiball", - "giftable": true, - "name": "+1 ball", - "max": 6, - help: lvl => `Start every levels with ${lvl + 1} balls.`, - fullHelp: `As soon as you drop the ball in Breakout 71, you loose. With this perk, you get two balls, and so you can afford to lose one. + fullHelp: `Directs the coins to the puck. The effect is stronger if the coin is close to it already. Catching 90% or 100% of coins bring special bonuses in the game. + Another way to catch more coins is to hit bricks from the bottom. The ball's speed and direction impacts the spawned coin's velocity. `, + }, + { + requires: "", + threshold: 1500, + id: "multiball", + giftable: true, + name: "+1 ball", + max: 6, + help: (lvl) => `Start every levels with ${lvl + 1} balls.`, + fullHelp: `As soon as you drop the ball in Breakout 71, you loose. With this perk, you get two balls, and so you can afford to lose one. The lost balls come back on the next level or whenever you use one of your extra lives, if you picked that perk. Having more than one balls makes - some further perks available, and of course clears the level faster.` - }, { - requires: '', - "threshold": 2000, - giftable: false, - "id": "smaller_puck", - "name": "Smaller puck", - "max": 2, - help: lvl => lvl == 1 ? `Also gives +5 base combo.` : `Even smaller puck and higher base combo`, - fullHelp: `This makes the puck smaller, which in theory makes some corner shots easier, but really just raises the difficulty. - That's why you also get a nice bonus of +5 coins per brick for all bricks you'll break after picking this. ` - }, { - requires: '', - "threshold": 3000, - "id": "pierce", - "giftable": true, - "name": "Piercing", - "max": 3, - help: lvl => `Ball pierces ${3 * lvl} bricks after a puck bounce.`, - fullHelp: `The ball normally bounces as soon as it touches something. With this perk, it will continue its trajectory for up to 3 bricks broken. - After that, it will bounce on the 4th brick, and you'll need to touch the puck to reset the counter. This combines particularly well with Sapper. ` - }, { - requires: '', - "threshold": 4000, - "id": "picky_eater", - "giftable": true, - "name": "Picky eater", - "max": 1, - help: lvl => `More coins if you break bricks color by color.`, + some further perks available, and of course clears the level faster.`, + }, + { + requires: "", + threshold: 2000, + giftable: false, + id: "smaller_puck", + name: "Smaller puck", + max: 2, + help: (lvl) => + lvl == 1 + ? `Also gives +5 base combo.` + : `Even smaller puck and higher base combo`, + fullHelp: `This makes the puck smaller, which in theory makes some corner shots easier, but really just raises the difficulty. + That's why you also get a nice bonus of +5 coins per brick for all bricks you'll break after picking this. `, + }, + { + requires: "", + threshold: 3000, + id: "pierce", + giftable: true, + name: "Piercing", + max: 3, + help: (lvl) => `Ball pierces ${3 * lvl} bricks after a puck bounce.`, + fullHelp: `The ball normally bounces as soon as it touches something. With this perk, it will continue its trajectory for up to 3 bricks broken. + After that, it will bounce on the 4th brick, and you'll need to touch the puck to reset the counter. This combines particularly well with Sapper. `, + }, + { + requires: "", + threshold: 4000, + id: "picky_eater", + giftable: true, + name: "Picky eater", + max: 1, + help: (lvl) => `More coins if you break bricks color by color.`, - fullHelp: `Whenever you break a brick the same color as your ball, your combo increases by one. + fullHelp: `Whenever you break a brick the same color as your ball, your combo increases by one. If it's a different color, the ball takes that new color, but the combo resets. The bricks with the right color will get a white border. Once you get a combo higher than your minimum, the bricks of the wrong color will get a red halo. If you have more than one ball, they all change color whenever one of them hits a brick. - ` - }, { - requires: '', - "threshold": 5000, - giftable: false, - "id": "metamorphosis", - "name": "Stain", - "max": 1, - help: lvl => `Coins color the bricks they touch.`, + `, + }, + { + requires: "", + threshold: 5000, + giftable: false, + id: "metamorphosis", + name: "Stain", + max: 1, + help: (lvl) => `Coins color the bricks they touch.`, - fullHelp: `With this perk, coins will be of the color of the brick they come from, and will color the first brick they touch in the same color. Coins spawn with the speed + fullHelp: `With this perk, coins will be of the color of the brick they come from, and will color the first brick they touch in the same color. Coins spawn with the speed of the ball that broke them, which means you can aim a bit in the direction of the bricks you want to "paint". - ` - }, { - requires: '', - "threshold": 6000, - "id": "compound_interest", - "giftable": true, - "name": "Compound interest", - "max": 3, - help: lvl => `+${lvl} combo / brick broken, -${lvl} combo per coin lost`, + `, + }, + { + requires: "", + threshold: 6000, + id: "compound_interest", + giftable: true, + name: "Compound interest", + max: 3, + help: (lvl) => `+${lvl} combo / brick broken, -${lvl} combo per coin lost`, - fullHelp: `Your combo will grow by one every time you break a brick, spawning more and more coin with every brick you break. Be sure however to catch every one of those coins + fullHelp: `Your combo will grow by one every time you break a brick, spawning more and more coin with every brick you break. Be sure however to catch every one of those coins with your puck, as any lost coin will decrease your combo by one point. One your combo is above the minimum, the bottom of the play area will have a red line to remind you that coins should not go there. This perk combines with other combo perks, the combo will rise faster but reset more easily. - ` - }, { - requires: '', - "threshold": 7000, - "id": "hot_start", - "giftable": true, - "name": "Hot start", - "max": 3, - help: lvl => `Start at combo ${lvl * 15 + 1}, -${lvl} combo per second`, - fullHelp: `At the start of every level, your combo will start at +15 points, but then every second it will be decreased by one. This means the first 15 seconds in a level will spawn + `, + }, + { + requires: "", + threshold: 7000, + id: "hot_start", + giftable: true, + name: "Hot start", + max: 3, + help: (lvl) => `Start at combo ${lvl * 15 + 1}, -${lvl} combo per second`, + fullHelp: `At the start of every level, your combo will start at +15 points, but then every second it will be decreased by one. This means the first 15 seconds in a level will spawn many more coins than the following ones, and you should make sure that you clear the level quickly. The effect stacks with other combo related perks, so you might be able to raise the combo after the 15s timeout, but it will keep ticking down. Every time you take the perk again, the effect will be more dramatic. - ` - }, { - requires: '', - "threshold": 9000, - "id": "sapper", - "giftable": true, - "name": "Sapper", - "max": 7, - help: lvl => lvl === 1 ? 'The first brick broken becomes a bomb.' : `The first ${lvl} bricks broken become bombs.`, - fullHelp: `Instead of just disappearing, the first brick you break will be replaced by a bomb brick. Bouncing the ball on the puck re-arms the effect. "Piercing" will instantly + `, + }, + { + requires: "", + threshold: 9000, + id: "sapper", + giftable: true, + name: "Sapper", + max: 7, + help: (lvl) => + lvl === 1 + ? "The first brick broken becomes a bomb." + : `The first ${lvl} bricks broken become bombs.`, + fullHelp: `Instead of just disappearing, the first brick you break will be replaced by a bomb brick. Bouncing the ball on the puck re-arms the effect. "Piercing" will instantly detonate the bomb that was just placed. Leveling-up this perk will allow you to place more bombs. Remember that bombs impact the velocity of nearby coins, so too many explosions could make it hard to catch the fruits of your hard work. - ` - }, { - requires: '', - "threshold": 11000, - "id": "bigger_explosions", - "name": "Kaboom", - "max": 1, - giftable: false, + `, + }, + { + requires: "", + threshold: 11000, + id: "bigger_explosions", + name: "Kaboom", + max: 1, + giftable: false, - help: lvl => 'Bigger explosions', + help: (lvl) => "Bigger explosions", - fullHelp: `The default explosion clears a 3x3 square, with this it becomes a 5x5 square, and the blowback on the coins is also significantly stronger. ` - }, { - requires: '', - "threshold": 13000, - giftable: false, - "id": "extra_levels", - "name": "+1 level", - "max": 3, - help: lvl => `Play ${lvl + 7} levels instead of 7`, - fullHelp: `The default run can last a max of 7 levels, after which the game is over and whatever score you reached is your run score. - Each level of this perk lets you go one level higher. The last levels are often the ones where you make the most score, so the difference can be dramatic.` - }, { - requires: '', - "threshold": 15000, - giftable: false, - "id": "pierce_color", - "name": "Color pierce", - "max": 1, - help: lvl => `Balls pierce bricks of their color.`, - fullHelp: `Whenever a ball hits a brick of the same color, it will just go through unimpeded. - Once it reaches a brick of a different color, it will break it, take its color and bounce.` - }, { - requires: '', - "threshold": 18000, - giftable: false, - "id": "soft_reset", - "name": "Soft reset", - "max": 2, - help: lvl => `Combo grows ${lvl > 1 ? 'even' : ''} slower but resets less.`, - fullHelp: `The combo normally climbs every time you break a brick. This will sometimes cancel that climb, but also limit the impact of a combo reset.` - }, { - requires: 'multiball', - "threshold": 21000, - giftable: false, - "id": "ball_repulse_ball", - "name": "Personal space", - "max": 3, - help: lvl => lvl === 1 ? `Balls repulse balls.` : 'Stronger repulsion force', + fullHelp: `The default explosion clears a 3x3 square, with this it becomes a 5x5 square, and the blowback on the coins is also significantly stronger. `, + }, + { + requires: "", + threshold: 13000, + giftable: false, + id: "extra_levels", + name: "+1 level", + max: 3, + help: (lvl) => `Play ${lvl + 7} levels instead of 7`, + fullHelp: `The default run can last a max of 7 levels, after which the game is over and whatever score you reached is your run score. + Each level of this perk lets you go one level higher. The last levels are often the ones where you make the most score, so the difference can be dramatic.`, + }, + { + requires: "", + threshold: 15000, + giftable: false, + id: "pierce_color", + name: "Color pierce", + max: 1, + help: (lvl) => `Balls pierce bricks of their color.`, + fullHelp: `Whenever a ball hits a brick of the same color, it will just go through unimpeded. + Once it reaches a brick of a different color, it will break it, take its color and bounce.`, + }, + { + requires: "", + threshold: 18000, + giftable: false, + id: "soft_reset", + name: "Soft reset", + max: 2, + help: (lvl) => + `Combo grows ${lvl > 1 ? "even" : ""} slower but resets less.`, + fullHelp: `The combo normally climbs every time you break a brick. This will sometimes cancel that climb, but also limit the impact of a combo reset.`, + }, + { + requires: "multiball", + threshold: 21000, + giftable: false, + id: "ball_repulse_ball", + name: "Personal space", + max: 3, + help: (lvl) => + lvl === 1 ? `Balls repulse balls.` : "Stronger repulsion force", - fullHelp: `Balls that are less than half a screen width away will start repulsing each other. The repulsion force is stronger if they are close to each other. - Particles will jet out to symbolize this force being applied. This perk is only offered if you have more than one ball already.` - }, { - requires: 'multiball', - "threshold": 25000, - giftable: false, - "id": "ball_attract_ball", - "name": "Gravity", - "max": 3, - help: lvl => lvl === 1 ? `Balls attract balls.` : 'Stronger attraction force', + fullHelp: `Balls that are less than half a screen width away will start repulsing each other. The repulsion force is stronger if they are close to each other. + Particles will jet out to symbolize this force being applied. This perk is only offered if you have more than one ball already.`, + }, + { + requires: "multiball", + threshold: 25000, + giftable: false, + id: "ball_attract_ball", + name: "Gravity", + max: 3, + help: (lvl) => + lvl === 1 ? `Balls attract balls.` : "Stronger attraction force", - fullHelp: `Balls that are more than half a screen width away will start attracting each other. The attraction force is stronger when they are furthest away from each other. - Rainbow particles will fly to symbolize the attraction force. This perk is only offered if you have more than one ball already.` - }, { - requires: '', - "threshold": 30000, - giftable: false, - "id": "puck_repulse_ball", - "name": "Soft landing", - "max": 3, - help: lvl => lvl === 1 ? `Puck repulses balls.` : 'Stronger repulsion force', - fullHelp: `When a ball gets close to the puck, it will start slowing down, and even potentially bouncing without touching the puck.` - }, { - requires: '', - "threshold": 35000, - giftable: false, - "id": "wind", - "name": "Wind", - "max": 3, - help: lvl => lvl === 1 ? `Puck position creates wind.` : 'Stronger wind force', - fullHelp: `The wind depends on where your puck is, if it's in the center of the screen nothing happens, if it's on the left it will blow leftwise, if it's on the right of the screen - then it will blow rightwise. The wind affects both the balls and coins.` - }, { - requires: '', - "threshold": 40000, - giftable: false, - "id": "sturdy_bricks", - "name": "Sturdy bricks", - "max": 4, - help: lvl => lvl === 1 ? `Bricks sometimes resist hits but drop more coins.` : 'Bricks resist more and drop more coins', - fullHelp: `With level one of this perk, the ball has a 20% chance to bounce harmlessly on bricks, + fullHelp: `Balls that are more than half a screen width away will start attracting each other. The attraction force is stronger when they are furthest away from each other. + Rainbow particles will fly to symbolize the attraction force. This perk is only offered if you have more than one ball already.`, + }, + { + requires: "", + threshold: 30000, + giftable: false, + id: "puck_repulse_ball", + name: "Soft landing", + max: 3, + help: (lvl) => + lvl === 1 ? `Puck repulses balls.` : "Stronger repulsion force", + fullHelp: `When a ball gets close to the puck, it will start slowing down, and even potentially bouncing without touching the puck.`, + }, + { + requires: "", + threshold: 35000, + giftable: false, + id: "wind", + name: "Wind", + max: 3, + help: (lvl) => + lvl === 1 ? `Puck position creates wind.` : "Stronger wind force", + fullHelp: `The wind depends on where your puck is, if it's in the center of the screen nothing happens, if it's on the left it will blow leftwise, if it's on the right of the screen + then it will blow rightwise. The wind affects both the balls and coins.`, + }, + { + requires: "", + threshold: 40000, + giftable: false, + id: "sturdy_bricks", + name: "Sturdy bricks", + max: 4, + help: (lvl) => + lvl === 1 + ? `Bricks sometimes resist hits but drop more coins.` + : "Bricks resist more and drop more coins", + fullHelp: `With level one of this perk, the ball has a 20% chance to bounce harmlessly on bricks, but generates 10% more coins when it does break one. - This +10% is not shown in the combo number. At level 4 the ball has 80% chance of bouncing and brings 40% more coins.` - }, { - requires: '', - "threshold": 45000, - giftable: false, - "id": "respawn", - "name": "Respawn", - "max": 4, - help: lvl => lvl === 1 ? `The first brick hit of two+ will respawn.` : 'More bricks can respawn', - fullHelp: `After breaking two or more bricks, when the ball hits the puck, the first brick will be put back in place, provided that space is free and the brick wasn't a bomb. + This +10% is not shown in the combo number. At level 4 the ball has 80% chance of bouncing and brings 40% more coins.`, + }, + { + requires: "", + threshold: 45000, + giftable: false, + id: "respawn", + name: "Respawn", + max: 4, + help: (lvl) => + lvl === 1 + ? `The first brick hit of two+ will respawn.` + : "More bricks can respawn", + fullHelp: `After breaking two or more bricks, when the ball hits the puck, the first brick will be put back in place, provided that space is free and the brick wasn't a bomb. Some particle effect will let you know where bricks will appear. Levelling this up lets you respawn up to 4 bricks at a time, but there should always be at least one destroyed. - ` - }, { - requires: '', - "threshold": 50000, - giftable: false, - "id": "one_more_choice", - "name": "+1 choice until run end", - "max": 3, - help: lvl => lvl === 1 ? `Further level ups will offer one more option in the list.` : 'Even more options', - fullHelp: `Every upgrade menu will have one more option. + `, + }, + { + requires: "", + threshold: 50000, + giftable: false, + id: "one_more_choice", + name: "+1 choice until run end", + max: 3, + help: (lvl) => + lvl === 1 + ? `Further level ups will offer one more option in the list.` + : "Even more options", + fullHelp: `Every upgrade menu will have one more option. Doesn't increase the number of upgrades you can pick. - ` - }, { - requires: '', - "threshold": 55000, - giftable: false, - "id": "instant_upgrade", - "name": "+2 upgrades now", - "max": 2, - help: lvl => lvl === 1 ? `-1 choice until run end.` : 'Even fewer options', - fullHelp: `Immediately pick two upgrades, so that you get one free one and one to repay the one used to get this perk. Every further menu to pick upgrades will have fewer options to choose from.` - }] as const; \ No newline at end of file + `, + }, + { + requires: "", + threshold: 55000, + giftable: false, + id: "instant_upgrade", + name: "+2 upgrades now", + max: 2, + help: (lvl) => + lvl === 1 ? `-1 choice until run end.` : "Even fewer options", + fullHelp: `Immediately pick two upgrades, so that you get one free one and one to repay the one used to get this perk. Every further menu to pick upgrades will have fewer options to choose from.`, + }, +] as const; diff --git a/src/style.css b/src/style.css index 6f96029..31b8a92 100644 --- a/src/style.css +++ b/src/style.css @@ -1,333 +1,329 @@ * { - font-family: Courier New, + font-family: + Courier New, Courier, Lucida Sans Typewriter, Lucida Typewriter, monospace; - box-sizing: border-box; + box-sizing: border-box; } body { - margin: 0; - padding: 0; - overflow: hidden; - width: 100vw; - height: 100vh; - height: calc(var(--vh, 1vh) * 100); - color: white; - background-size: 120px 120px; - background-color: var(--background1); - --background1: #030c23; - --background2: #03112a; + margin: 0; + padding: 0; + overflow: hidden; + width: 100vw; + height: 100vh; + height: calc(var(--vh, 1vh) * 100); + color: white; + background-size: 120px 120px; + background-color: var(--background1); + --background1: #030c23; + --background2: #03112a; } #game { - position: absolute; - top: 0; - left: 0; - height: 100vh; - height: calc(var(--vh, 1vh) * 100); - width: 100vw; + position: absolute; + top: 0; + left: 0; + height: 100vh; + height: calc(var(--vh, 1vh) * 100); + width: 100vw; } #score, #menu { - position: absolute; - top: 0; - z-index: 1; - padding: 10px; - appearance: none; - background: none; - border: none; - font: inherit; - color: white; - min-width: 40px; - min-height: 40px; - line-height: 20px; + position: absolute; + top: 0; + z-index: 1; + padding: 10px; + appearance: none; + background: none; + border: none; + font: inherit; + color: white; + min-width: 40px; + min-height: 40px; + line-height: 20px; } - #score:hover, #score:focus, #menu:hover, #menu:focus { - background: rgba(0, 0, 0, 0.3); - cursor: pointer; + background: rgba(0, 0, 0, 0.3); + cursor: pointer; } #score { - right: 0; + right: 0; } #menu { - left: 0; + left: 0; } @media screen and (orientation: portrait) { - #menu > span { - display: none; - } + #menu > span { + display: none; + } } .popup { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.9); - z-index: 10; - display: flex; - overflow: auto; + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.9); + z-index: 10; + display: flex; + overflow: auto; } .popup > div { - margin: auto; - padding: 20px 10px; - /*border: 1px solid white;*/ - transform-origin: center; - display: flex; - flex-direction: column; - align-items: stretch; - width: 100%; - max-width: 450px; + margin: auto; + padding: 20px 10px; + /*border: 1px solid white;*/ + transform-origin: center; + display: flex; + flex-direction: column; + align-items: stretch; + width: 100%; + max-width: 450px; } .popup > div > * { - padding: 0; - margin: 0; + padding: 0; + margin: 0; } .popup > div > h2, .popup > div > p { - margin-bottom: 20px; + margin-bottom: 20px; } .popup > div > button { - font: inherit; - background: rgba(0, 0, 0, 0.8); - color: white; - padding: 10px; - cursor: pointer; - border: 1px solid white; - text-align: left; - display: flex; - gap: 10px; - margin-top: -1px; + font: inherit; + background: rgba(0, 0, 0, 0.8); + color: white; + padding: 10px; + cursor: pointer; + border: 1px solid white; + text-align: left; + display: flex; + gap: 10px; + margin-top: -1px; } .popup > div > button:not([disabled]):hover, .popup > div > button:not([disabled]):focus { - border-color: #f1d33b; - position: relative; - z-index: 1; + border-color: #f1d33b; + position: relative; + z-index: 1; } - .popup button.close-modale { - color: white; - position: absolute; - top: 0; - right: 0; - width: 60px; - height: 60px; - background: transparent; - border: none; - cursor: pointer; - overflow: hidden; + color: white; + position: absolute; + top: 0; + right: 0; + width: 60px; + height: 60px; + background: transparent; + border: none; + cursor: pointer; + overflow: hidden; } .popup button.close-modale:before { - content: "+"; - position: absolute; - transform: translate(-50%, -50%) rotate(45deg); - font-size: 80px; - display: inline-block; - top: 34px; - left: 26px; + content: "+"; + position: absolute; + transform: translate(-50%, -50%) rotate(45deg); + font-size: 80px; + display: inline-block; + top: 34px; + left: 26px; } .popup button.close-modale:hover { - font-weight: bold; - background: black; + font-weight: bold; + background: black; } .popup > div > button[disabled] { - /*border: 1px solid #666;*/ - opacity: 0.5; - filter: saturate(0); - pointer-events: none; + /*border: 1px solid #666;*/ + opacity: 0.5; + filter: saturate(0); + pointer-events: none; } .popup > div > button > div { - flex-grow: 1; + flex-grow: 1; } .popup > div > button > div > em { - display: block; - opacity: 0.8; + display: block; + opacity: 0.8; } .popup > div > button > span.checks { - width: 40px; - height: 40px; - display: inline-flex; - gap: 5px; - flex-grow: 0; - flex-shrink: 0; + width: 40px; + height: 40px; + display: inline-flex; + gap: 5px; + flex-grow: 0; + flex-shrink: 0; } .popup > div > button > span.checks > span { - flex-basis: 10px; - flex-grow: 1; - flex-shrink: 1; - /*border: 1px solid white;*/ - background: white; - opacity: 0.1; - border-radius: 4px; - align-self: stretch; + flex-basis: 10px; + flex-grow: 1; + flex-shrink: 1; + /*border: 1px solid white;*/ + background: white; + opacity: 0.1; + border-radius: 4px; + align-self: stretch; } .popup > div > button > span.checks > span.checked { - opacity: 1; + opacity: 1; } .popup .textAfterButtons { - color: rgba(255, 255, 255, 0.58); + color: rgba(255, 255, 255, 0.58); } .popup a[href] { - color: inherit; + color: inherit; } .popup a[href]:hover, .popup a[href]:focus { - color: white; + color: white; } /*Unlocks progress bar*/ .progress { - display: block; - padding: 5px 10px; - background: #1c1c2f; - color: #fff; - box-shadow: inset 3px 3px 5px rgba(0, 0, 0, 0.5); - border-radius: 5px; - text-align: center; - position: relative; - overflow: hidden; + display: block; + padding: 5px 10px; + background: #1c1c2f; + color: #fff; + box-shadow: inset 3px 3px 5px rgba(0, 0, 0, 0.5); + border-radius: 5px; + text-align: center; + position: relative; + overflow: hidden; } .progress > .progress_bar_part { - display: block; - background: #4049ca; - box-shadow: inset 3px 3px 5px rgba(0, 0, 0, 0.5); - left: 0; - position: absolute; - right: 0; - top: 0; - bottom: 0; - transform-origin: top left; - animation: grow 1s both ease-out; - z-index: 1; + display: block; + background: #4049ca; + box-shadow: inset 3px 3px 5px rgba(0, 0, 0, 0.5); + left: 0; + position: absolute; + right: 0; + top: 0; + bottom: 0; + transform-origin: top left; + animation: grow 1s both ease-out; + z-index: 1; } .progress > span { - display: block; - position: relative; - z-index: 2; + display: block; + position: relative; + z-index: 2; } @keyframes grow { - 0% { - transform: scale(0, 1); - } + 0% { + transform: scale(0, 1); + } } #level-recording-container { - max-width: 400px; - text-align: center; - margin: 40px; + max-width: 400px; + text-align: center; + margin: 40px; } -#level-recording-container img, #level-recording-container video { - max-width: 100%; - height: auto; +#level-recording-container img, +#level-recording-container video { + max-width: 100%; + height: auto; } #level-recording-container a { - display: block; + display: block; } #level-recording-container a video { - border-radius: 10px; - display: block; - outline: 1px solid white; - box-shadow: 2px 2px 5px black; - margin: 20px auto; + border-radius: 10px; + display: block; + outline: 1px solid white; + box-shadow: 2px 2px 5px black; + margin: 20px auto; } @media (min-width: 1200px) { - - #level-recording-container { - position: absolute; - top: 40px; - left: 40px; - max-width: calc((100vw - 450px )/2 - 80px); - - } + #level-recording-container { + position: absolute; + top: 40px; + left: 40px; + max-width: calc((100vw - 450px) / 2 - 80px); + } } .histogram { - display: flex; - gap: 10px; - align-items: stretch; - margin: 10px 0 40px 0; + display: flex; + gap: 10px; + align-items: stretch; + margin: 10px 0 40px 0; } - .histogram > span { -/* Hover zone */ - flex-grow: 1; - width: 10px; - position: relative; - display: flex; - flex-direction: column; - justify-content: flex-end; + /* Hover zone */ + flex-grow: 1; + width: 10px; + position: relative; + display: flex; + flex-direction: column; + justify-content: flex-end; } -.histogram > span.active > span{ - background: #4049ca; +.histogram > span.active > span { + background: #4049ca; } -.histogram > span > span{ - /*Visible bar*/ - background: #1c1c2f; - width: 100%; - display: block; - border-radius: 5px; - min-height: 1px; +.histogram > span > span { + /*Visible bar*/ + background: #1c1c2f; + width: 100%; + display: block; + border-radius: 5px; + min-height: 1px; } -.histogram > span > span> span { - /*label */ - position: absolute; - bottom: -20px; - pointer-events: none; - white-space: nowrap; - transform-origin: bottom left; - font-size: 13px; - text-align: center; - display: block; - left: 50%; - transform: translate(-50%,0); - +.histogram > span > span > span { + /*label */ + position: absolute; + bottom: -20px; + pointer-events: none; + white-space: nowrap; + transform-origin: bottom left; + font-size: 13px; + text-align: center; + display: block; + left: 50%; + transform: translate(-50%, 0); } -.histogram > span:not(:hover):not(.active) > span> span { - opacity: 0; +.histogram > span:not(:hover):not(.active) > span > span { + opacity: 0; } h2.histogram-title { - color: #3b3f75; - font-size: 18px; + color: #3b3f75; + font-size: 18px; } h2.histogram-title strong { - color: #4049ca; -} \ No newline at end of file + color: #4049ca; +} diff --git a/src/types.d.ts b/src/types.d.ts index a472653..84c4af7 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,38 +1,103 @@ -import {rawUpgrades} from "./rawUpgrades"; +import { rawUpgrades } from "./rawUpgrades"; + +export type colorString = string; export type RawLevel = { - name: string; - size: number; - bricks: string; - svg: string; - color: string; + name: string; + size: number; + bricks: string; + svg: string; + color: string; }; export type Level = { - name: string; - size: number; - bricks: string[]; - svg: string; - color: string; - threshold?: number; - sortKey?: number; + name: string; + size: number; + bricks: colorString[]; + svg: string; + color: string; + threshold?: number; + sortKey?: number; }; -export type Palette = { [k: string]: string } +export type Palette = { [k: string]: string }; -export type Upgrade={ - threshold: number; - giftable: boolean; - "id": string; - "name": string; - "max": number; - help: (lvl:string) => string; - fullHelp: string; - requires:PerkId|'' +export type Upgrade = { + threshold: number; + giftable: boolean; + id: string; + name: string; + icon: string; + max: number; + help: (lvl: string) => string; + fullHelp: string; + requires: PerkId | ""; +}; + +export type PerkId = (typeof rawUpgrades)[number]["id"]; + +declare global { + interface Window { + webkitAudioContext?: typeof AudioContext; + } + interface Document { + webkitFullscreenEnabled?: boolean; + webkitCancelFullScreen?: ()=>void; + } + interface Element { + webkitRequestFullscreen: typeof Element.requestFullscreen + } +} + +export type Coin={ + points:number; + color: colorString; + x:number; + y:number; + previousx:number; + previousy:number; + vx:number; + vy:number; + sx:number; + sy:number; + a:number; + sa:number; + weight:number; + destroyed?:boolean; + coloredABrick?:boolean; +} +export type Ball = { + x:number; + previousx:number; + y:number; + previousy:number; + vx:number; + vy:number; + sx:number; + sy:number; + sparks:number; + piercedSinceBounce:number; + hitSinceBounce:number; + hitItem: {index:number, color:string}[], + sapperUses:number; + destroyed?:boolean; } -export type PerkId = typeof rawUpgrades[number]['id'] +export type FlashTypes= "text"|"particle"|'ball' + +export type Flash = { + type: FlashTypes; + text?:string; + time:number; + color:colorString; + x:number; + y:number; + duration:number; + size:number; + vx?:number; + vy?:number; + ethereal?:boolean; + destroyed?:boolean; + +} -declare global { - interface Window { webkitAudioContext: typeof AudioContext; } -} \ No newline at end of file