From 27a2cd686ead2649c35a498bb5625086d22f7856 Mon Sep 17 00:00:00 2001 From: Renan LE CARO Date: Sat, 29 Mar 2025 21:28:05 +0100 Subject: [PATCH] Build --- app/build.gradle.kts | 4 +- app/src/main/assets/index.html | 2 +- dist/index.html | 36 +- src/PWA/sw-b71.js | 2 +- src/asyncAlert.ts | 2 +- src/data/version.json | 2 +- src/game.less | 13 +- src/game.ts | 1675 ++++++++--------- src/gameStateMutators.ts | 3155 ++++++++++++++++---------------- src/game_utils.ts | 18 +- src/newGameState.ts | 4 +- src/premium.ts | 1 - src/pure_functions.ts | 6 +- src/render.ts | 1736 +++++++++--------- src/types.d.ts | 14 +- src/upgrades.ts | 58 +- 16 files changed, 3396 insertions(+), 3332 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9ca1df7..324d614 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -11,8 +11,8 @@ android { applicationId = "me.lecaro.breakout" minSdk = 21 targetSdk = 34 - versionCode = 29053158 - versionName = "29053158" + versionCode = 29054664 + versionName = "29054664" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary = true diff --git a/app/src/main/assets/index.html b/app/src/main/assets/index.html index 74d72aa..342190f 100644 --- a/app/src/main/assets/index.html +++ b/app/src/main/assets/index.html @@ -1 +1 @@ -Breakout 71
\ No newline at end of file +Breakout 71
\ No newline at end of file diff --git a/dist/index.html b/dist/index.html index ff539d9..58be0a6 100644 --- a/dist/index.html +++ b/dist/index.html @@ -899,10 +899,10 @@ async function openScorePanel() { gameState.isCreativeModeRun ? `

${(0, _i18N.t)("score_panel.test_run")}

` : "", (0, _gameUtils.pickedUpgradesHTMl)(gameState), (0, _gameUtils.levelsListHTMl)(gameState), - gameState.rerolls ? (0, _i18N.t)('score_panel.rerolls_count', { + gameState.rerolls ? (0, _i18N.t)("score_panel.rerolls_count", { rerolls: gameState.rerolls - }) : '', - banned && (0, _i18N.t)('score_panel.banned', { + }) : "", + banned && (0, _i18N.t)("score_panel.banned", { banned }) ], @@ -1293,7 +1293,7 @@ function setKeyPressed(key, on) { } document.addEventListener("keydown", (e)=>{ if (e.key.toLowerCase() === "f" && !e.ctrlKey && !e.metaKey) { - (0, _options.toggleOption)('fullscreen'); + (0, _options.toggleOption)("fullscreen"); applyFullScreenChoice(); } else if (e.key in pressed) setKeyPressed(e.key, 1); if (e.key === " " && !(0, _asyncAlert.alertsOpen)) { @@ -1385,7 +1385,7 @@ const upgrades = (0, _upgrades.rawUpgrades).map((u)=>({ })); },{"./data/palette.json":"ktRBU","./data/levels.json":"8JSUc","./data/version.json":"iyP6E","./upgrades":"1u3Dx","./getLevelBackground":"7OIPf","./levelIcon":"6rQoT","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"iyP6E":[function(require,module,exports,__globalThis) { -module.exports = JSON.parse("\"29053158\""); +module.exports = JSON.parse("\"29054664\""); },{}],"1u3Dx":[function(require,module,exports,__globalThis) { var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js"); @@ -2901,7 +2901,7 @@ async function gotoNextLoop(gameState) { content: [ (0, _i18N.t)("loop.instructions"), comboText, - ...userPerks.filter((u)=>u.id !== 'instant_upgrade').map((u)=>{ + ...userPerks.filter((u)=>u.id !== "instant_upgrade").map((u)=>{ return { text: u.name + (0, _i18N.t)("level_up.upgrade_perk_to_level", { level: gameState.perks[u.id] + 1 @@ -3538,24 +3538,24 @@ function render(gameState) { else menuLabel.innerText = (0, _i18N.t)("play.menu_label"); const catchRate = gameState.levelSpawnedCoins ? (gameState.levelSpawnedCoins - gameState.levelLostCoins) / gameState.levelSpawnedCoins : 1; scoreDisplay.innerHTML = ((0, _options.isOptionOn)("show_fps") ? ` - + ${0, _game.lastMeasuredFPS} FPS / - ` : '') + ((0, _options.isOptionOn)('show_stats') ? ` - + ` : "") + ((0, _options.isOptionOn)("show_stats") ? ` + 0.9 && "good" || ""}"> ${Math.floor(catchRate * 100)}% / - + ${gameState.levelWallBounces} B / - + ${Math.ceil(gameState.levelTime / 1000)}s / - + ${gameState.levelMisses} M / - ` : '') + `$${gameState.score}`; + ` : "") + `$${gameState.score}`; scoreDisplay.className = gameState.lastScoreIncrease > gameState.levelTime - 500 ? "active" : ""; // Clear if (!(0, _options.isOptionOn)("basic") && !level.color && level.svg) { @@ -3602,14 +3602,14 @@ function render(gameState) { bgctx.fillStyle = level.color || "#000"; bgctx.fillRect(0, 0, gameState.canvasWidth, gameState.canvasHeight); if (gameState.perks.clairvoyant >= 3) { - const pageSource = document.body.innerHTML.replace(/\s+/gi, ''); + const pageSource = document.body.innerHTML.replace(/\s+/gi, ""); const lineWidth = Math.ceil(gameState.canvasWidth / 15); const lines = Math.ceil(gameState.canvasHeight / 20); const chars = lineWidth * lines; let start = Math.ceil(Math.random() * (pageSource.length - chars)); for(let i = 0; i < lines; i++){ - bgctx.fillStyle = 'white'; - bgctx.font = '20px Courier'; + bgctx.fillStyle = "white"; + bgctx.font = "20px Courier"; bgctx.fillText(pageSource.slice(start + i * lineWidth, start + (i + 1) * lineWidth), 0, i * 20, gameState.canvasWidth); } } else { @@ -3654,7 +3654,7 @@ function render(gameState) { ctx.globalCompositeOperation = "source-over"; // ctx.globalCompositeOperation = // coin.color === "gold" || level.color ? "source-over" : "screen"; - drawCoin(ctx, coin.color, coin.size, coin.x, coin.y, hasCombo && gameState.perks.asceticism && "red" || coin.color === 'gold' && 'gold' || gameState.puckColor, coin.a); + drawCoin(ctx, coin.color, coin.size, coin.x, coin.y, hasCombo && gameState.perks.asceticism && "red" || coin.color === "gold" && "gold" || gameState.puckColor, coin.a); }); // Black shadow around balls if (!(0, _options.isOptionOn)("basic")) { @@ -3928,7 +3928,7 @@ function drawBrick(ctx, color, x, y, offset = 0, borderOnly) { const brx = Math.ceil(x + (0, _game.gameState).brickWidth / 2) - 1; const bry = Math.ceil(y + (0, _game.gameState).brickWidth / 2) - 1; const width = brx - tlx, height = bry - tly; - const key = "brick" + color + "_" + "_" + width + "_" + height + "_" + offset + '_' + borderOnly; + const key = "brick" + color + "_" + "_" + width + "_" + height + "_" + offset + "_" + borderOnly; if (!cachedGraphics[key]) { const can = document.createElement("canvas"); can.width = width; diff --git a/src/PWA/sw-b71.js b/src/PWA/sw-b71.js index 9690acc..7146151 100644 --- a/src/PWA/sw-b71.js +++ b/src/PWA/sw-b71.js @@ -1,5 +1,5 @@ // The version of the cache. -const VERSION = "29053158"; +const VERSION = "29054664"; // The name of the cache const CACHE_NAME = `breakout-71-${VERSION}`; diff --git a/src/asyncAlert.ts b/src/asyncAlert.ts index a25c27e..61a7f82 100644 --- a/src/asyncAlert.ts +++ b/src/asyncAlert.ts @@ -82,7 +82,7 @@ export async function asyncAlert({ content ?.filter((i) => i) .forEach((entry, index) => { - if(!entry) return; + if (!entry) return; if (typeof entry == "string") { const p = document.createElement("div"); p.innerHTML = entry; diff --git a/src/data/version.json b/src/data/version.json index 392c901..c5e3891 100644 --- a/src/data/version.json +++ b/src/data/version.json @@ -1 +1 @@ -"29053158" +"29054664" diff --git a/src/game.less b/src/game.less index e10657f..b86650d 100644 --- a/src/game.less +++ b/src/game.less @@ -1,9 +1,10 @@ * { - font-family: Courier New, - Courier, - Lucida Sans Typewriter, - Lucida Typewriter, - monospace; + font-family: + Courier New, + Courier, + Lucida Sans Typewriter, + Lucida Typewriter, + monospace; box-sizing: border-box; } @@ -372,4 +373,4 @@ h2.histogram-title strong { // transform-origin: top left; // transform: rotate(-90deg); // -//} \ No newline at end of file +//} diff --git a/src/game.ts b/src/game.ts index 0d45177..f224aeb 100644 --- a/src/game.ts +++ b/src/game.ts @@ -1,237 +1,257 @@ -import {allLevels, appVersion, icons, upgrades} from "./loadGameData"; +import { allLevels, appVersion, icons, upgrades } from "./loadGameData"; import { - Ball, - Coin, - GameState, - LightFlash, - OptionId, - ParticleFlash, - PerkId, - RunParams, - TextFlash, - Upgrade, + Ball, + Coin, + GameState, + LightFlash, + OptionId, + ParticleFlash, + PerkId, + RunParams, + TextFlash, + Upgrade, } from "./types"; -import {getAudioContext, playPendingSounds} from "./sounds"; -import {currentLevelInfo, getRowColIndex, levelsListHTMl, max_levels, pickedUpgradesHTMl,} from "./game_utils"; +import { getAudioContext, playPendingSounds } from "./sounds"; +import { + currentLevelInfo, + getRowColIndex, + levelsListHTMl, + max_levels, + pickedUpgradesHTMl, +} from "./game_utils"; import "./PWA/sw_loader"; -import {getCurrentLang, t} from "./i18n/i18n"; +import { getCurrentLang, t } from "./i18n/i18n"; import { - cycleMaxCoins, - cycleMaxParticles, - getCurrentMaxCoins, - getCurrentMaxParticles, - getSettingValue, - getTotalScore, - setSettingValue, + cycleMaxCoins, + cycleMaxParticles, + getCurrentMaxCoins, + getCurrentMaxParticles, + getSettingValue, + getTotalScore, + setSettingValue, } from "./settings"; import { - forEachLiveOne, - gameStateTick, - liveCount, - normalizeGameState, - pickRandomUpgrades, - setLevel, - setMousePos, + forEachLiveOne, + gameStateTick, + liveCount, + normalizeGameState, + pickRandomUpgrades, + setLevel, + setMousePos, } from "./gameStateMutators"; -import {backgroundCanvas, ctx, gameCanvas, render, scoreDisplay,} from "./render"; -import {pauseRecording, recordOneFrame, resumeRecording, startRecordingGame,} from "./recording"; -import {newGameState} from "./newGameState"; -import {alertsOpen, asyncAlert, AsyncAlertAction, closeModal, requiredAsyncAlert,} from "./asyncAlert"; -import {isOptionOn, options, toggleOption} from "./options"; -import {hashCode} from "./getLevelBackground"; -import {premiumMenuEntry} from "./premium"; +import { + backgroundCanvas, + ctx, + gameCanvas, + render, + scoreDisplay, +} from "./render"; +import { + pauseRecording, + recordOneFrame, + resumeRecording, + startRecordingGame, +} from "./recording"; +import { newGameState } from "./newGameState"; +import { + alertsOpen, + asyncAlert, + AsyncAlertAction, + closeModal, + requiredAsyncAlert, +} from "./asyncAlert"; +import { isOptionOn, options, toggleOption } from "./options"; +import { hashCode } from "./getLevelBackground"; +import { premiumMenuEntry } from "./premium"; export function play() { - if (applyFullScreenChoice()) return; - if (gameState.running) return; - gameState.running = true; - gameState.ballStickToPuck = false; + if (applyFullScreenChoice()) return; + if (gameState.running) return; + gameState.running = true; + gameState.ballStickToPuck = false; - startRecordingGame(gameState); - getAudioContext()?.resume(); - resumeRecording(); - // document.body.classList[gameState.running ? 'add' : 'remove']('running') + startRecordingGame(gameState); + getAudioContext()?.resume(); + resumeRecording(); + // document.body.classList[gameState.running ? 'add' : 'remove']('running') } export function pause(playerAskedForPause: boolean) { - if (!gameState.running) return; - if (gameState.pauseTimeout) return; + if (!gameState.running) return; + if (gameState.pauseTimeout) return; - const stop = () => { - gameState.running = false; + const stop = () => { + gameState.running = false; - setTimeout(() => { - if (!gameState.running) getAudioContext()?.suspend(); - }, 1000); + setTimeout(() => { + if (!gameState.running) getAudioContext()?.suspend(); + }, 1000); - pauseRecording(); - gameState.pauseTimeout = null; - // document.body.className = gameState.running ? " running " : " paused "; - scoreDisplay.className = ""; - gameState.needsRender = true; - }; + pauseRecording(); + gameState.pauseTimeout = null; + // document.body.className = gameState.running ? " running " : " paused "; + scoreDisplay.className = ""; + gameState.needsRender = true; + }; - if (playerAskedForPause) { - // Pausing many times in a run will make pause slower - gameState.pauseUsesDuringRun++; - gameState.pauseTimeout = setTimeout( - stop, - Math.min(Math.max(0, gameState.pauseUsesDuringRun - 5) * 50, 500), - ); - } else { - stop(); - } + if (playerAskedForPause) { + // Pausing many times in a run will make pause slower + gameState.pauseUsesDuringRun++; + gameState.pauseTimeout = setTimeout( + stop, + Math.min(Math.max(0, gameState.pauseUsesDuringRun - 5) * 50, 500), + ); + } else { + stop(); + } - if (document.exitPointerLock) { - document.exitPointerLock(); - } + if (document.exitPointerLock) { + document.exitPointerLock(); + } } export const fitSize = () => { - const past_off = gameState.offsetXRoundedDown, - past_width = gameState.gameZoneWidthRoundedUp, - past_heigh = gameState.gameZoneHeight; + const past_off = gameState.offsetXRoundedDown, + past_width = gameState.gameZoneWidthRoundedUp, + past_heigh = gameState.gameZoneHeight; - const {width, height} = gameCanvas.getBoundingClientRect(); - gameState.canvasWidth = width; - gameState.canvasHeight = height; - gameCanvas.width = width; - gameCanvas.height = height; - ctx.fillStyle = currentLevelInfo(gameState)?.color || "black"; - ctx.globalAlpha = 1; - ctx.fillRect(0, 0, width, height); - backgroundCanvas.width = width; - backgroundCanvas.height = height; + const { width, height } = gameCanvas.getBoundingClientRect(); + gameState.canvasWidth = width; + gameState.canvasHeight = height; + gameCanvas.width = width; + gameCanvas.height = height; + ctx.fillStyle = currentLevelInfo(gameState)?.color || "black"; + ctx.globalAlpha = 1; + ctx.fillRect(0, 0, width, height); + backgroundCanvas.width = width; + backgroundCanvas.height = height; - gameState.gameZoneHeight = isOptionOn("mobile-mode") - ? (height * 80) / 100 - : height; - const baseWidth = Math.round( - Math.min(gameState.canvasWidth, gameState.gameZoneHeight * 0.73), - ); - gameState.brickWidth = Math.floor(baseWidth / gameState.gridSize / 2) * 2; - gameState.gameZoneWidth = gameState.brickWidth * gameState.gridSize; - gameState.offsetX = Math.floor( - (gameState.canvasWidth - gameState.gameZoneWidth) / 2, - ); - gameState.offsetXRoundedDown = gameState.offsetX; - if (gameState.offsetX < gameState.ballSize) gameState.offsetXRoundedDown = 0; - gameState.gameZoneWidthRoundedUp = width - 2 * gameState.offsetXRoundedDown; - backgroundCanvas.title = "resized"; - // Ensure puck stays within bounds - setMousePos(gameState, gameState.puckPosition); + gameState.gameZoneHeight = isOptionOn("mobile-mode") + ? (height * 80) / 100 + : height; + const baseWidth = Math.round( + Math.min(gameState.canvasWidth, gameState.gameZoneHeight * 0.73), + ); + gameState.brickWidth = Math.floor(baseWidth / gameState.gridSize / 2) * 2; + gameState.gameZoneWidth = gameState.brickWidth * gameState.gridSize; + gameState.offsetX = Math.floor( + (gameState.canvasWidth - gameState.gameZoneWidth) / 2, + ); + gameState.offsetXRoundedDown = gameState.offsetX; + if (gameState.offsetX < gameState.ballSize) gameState.offsetXRoundedDown = 0; + gameState.gameZoneWidthRoundedUp = width - 2 * gameState.offsetXRoundedDown; + backgroundCanvas.title = "resized"; + // Ensure puck stays within bounds + setMousePos(gameState, gameState.puckPosition); - function mapXY(item: ParticleFlash | TextFlash | LightFlash) { - item.x = - gameState.offsetXRoundedDown + - ((item.x - past_off) / past_width) * gameState.gameZoneWidthRoundedUp; - item.y = (item.y / past_heigh) * gameState.gameZoneHeight; - } + function mapXY(item: ParticleFlash | TextFlash | LightFlash) { + item.x = + gameState.offsetXRoundedDown + + ((item.x - past_off) / past_width) * gameState.gameZoneWidthRoundedUp; + item.y = (item.y / past_heigh) * gameState.gameZoneHeight; + } - function mapXYPastCoord(coin: Coin | Ball) { - coin.x = - gameState.offsetXRoundedDown + - ((coin.x - past_off) / past_width) * gameState.gameZoneWidthRoundedUp; - coin.y = (coin.y / past_heigh) * gameState.gameZoneHeight; - coin.previousX = coin.x; - coin.previousY = coin.y; - } + function mapXYPastCoord(coin: Coin | Ball) { + coin.x = + gameState.offsetXRoundedDown + + ((coin.x - past_off) / past_width) * gameState.gameZoneWidthRoundedUp; + coin.y = (coin.y / past_heigh) * gameState.gameZoneHeight; + coin.previousX = coin.x; + coin.previousY = coin.y; + } - gameState.balls.forEach(mapXYPastCoord); - forEachLiveOne(gameState.coins, mapXYPastCoord); - forEachLiveOne(gameState.particles, mapXY); - forEachLiveOne(gameState.texts, mapXY); - forEachLiveOne(gameState.lights, mapXY); - pause(true); - // For safari mobile https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ - document.documentElement.style.setProperty( - "--vh", - `${window.innerHeight * 0.01}px`, - ); + gameState.balls.forEach(mapXYPastCoord); + forEachLiveOne(gameState.coins, mapXYPastCoord); + forEachLiveOne(gameState.particles, mapXY); + forEachLiveOne(gameState.texts, mapXY); + forEachLiveOne(gameState.lights, mapXY); + pause(true); + // For safari mobile https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ + document.documentElement.style.setProperty( + "--vh", + `${window.innerHeight * 0.01}px`, + ); }; window.addEventListener("resize", fitSize); window.addEventListener("fullscreenchange", fitSize); setInterval(() => { - // Sometimes, the page changes size without triggering the event (when switching to fullscreen, closing debug panel...) - const {width, height} = gameCanvas.getBoundingClientRect(); - if (width !== gameState.canvasWidth || height !== gameState.canvasHeight) - fitSize(); + // Sometimes, the page changes size without triggering the event (when switching to fullscreen, closing debug panel...) + const { width, height } = gameCanvas.getBoundingClientRect(); + if (width !== gameState.canvasWidth || height !== gameState.canvasHeight) + fitSize(); }, 1000); export async function openUpgradesPicker(gameState: GameState) { + const catchRate = + (gameState.score - gameState.levelStartScore) / + (gameState.levelSpawnedCoins || 1); - const catchRate = - (gameState.score - gameState.levelStartScore) / - (gameState.levelSpawnedCoins || 1); + let repeats = 1; + let timeGain = "", + catchGain = "", + wallHitsGain = "", + missesGain = ""; + if (gameState.levelWallBounces == 0) { + repeats++; + gameState.rerolls++; + wallHitsGain = t("level_up.plus_one_upgrade"); + } else if (gameState.levelWallBounces < 5) { + gameState.rerolls++; + wallHitsGain = t("level_up.plus_one_choice"); + } + if (gameState.levelTime < 30 * 1000) { + repeats++; + gameState.rerolls++; + timeGain = t("level_up.plus_one_upgrade"); + } else if (gameState.levelTime < 60 * 1000) { + gameState.rerolls++; + timeGain = t("level_up.plus_one_choice"); + } + if (catchRate === 1) { + repeats++; + gameState.rerolls++; + catchGain = t("level_up.plus_one_upgrade"); + } else if (catchRate > 0.9) { + gameState.rerolls++; + catchGain = t("level_up.plus_one_choice"); + } + if (gameState.levelMisses === 0) { + repeats++; + gameState.rerolls++; + missesGain = t("level_up.plus_one_upgrade"); + } else if (gameState.levelMisses <= 3) { + gameState.rerolls++; + missesGain = t("level_up.plus_one_choice"); + } - let repeats = 1; + while (repeats--) { + const actions: Array<{ + text: string; + icon: string; + value: PerkId | "reroll"; + help: string; + }> = pickRandomUpgrades( + gameState, + 3 + gameState.perks.one_more_choice - gameState.perks.instant_upgrade, + ); + if (!actions.length) break; - let timeGain = "", - catchGain = "", - wallHitsGain = "", - missesGain = ""; + if (gameState.rerolls) + actions.push({ + text: t("level_up.reroll", { count: gameState.rerolls }), + help: t("level_up.reroll_help"), + value: "reroll" as const, + icon: icons["icon:reroll"], + }); - if (gameState.levelWallBounces == 0) { - repeats++; - gameState.rerolls++; - wallHitsGain = t("level_up.plus_one_upgrade"); - } else if (gameState.levelWallBounces < 5) { - gameState.rerolls++; - wallHitsGain = t("level_up.plus_one_choice"); - } - if (gameState.levelTime < 30 * 1000) { - repeats++; - gameState.rerolls++; - timeGain = t("level_up.plus_one_upgrade"); - } else if (gameState.levelTime < 60 * 1000) { - gameState.rerolls++; - timeGain = t("level_up.plus_one_choice"); - } - if (catchRate === 1) { - repeats++; - gameState.rerolls++; - catchGain = t("level_up.plus_one_upgrade"); - } else if (catchRate > 0.9) { - gameState.rerolls++; - catchGain = t("level_up.plus_one_choice"); - } - if (gameState.levelMisses === 0) { - repeats++; - gameState.rerolls++; - missesGain = t("level_up.plus_one_upgrade"); - } else if (gameState.levelMisses <= 3) { - gameState.rerolls++; - missesGain = t("level_up.plus_one_choice"); - } - - while (repeats--) { - const actions: Array<{ - text: string; - icon: string; - value: PerkId | "reroll"; - help: string; - }> = pickRandomUpgrades( - gameState, - 3 + gameState.perks.one_more_choice - gameState.perks.instant_upgrade, - ); - if (!actions.length) break; - - if (gameState.rerolls) - actions.push({ - text: t("level_up.reroll", {count: gameState.rerolls}), - help: t("level_up.reroll_help"), - value: "reroll" as const, - icon: icons["icon:reroll"], - }); - - let textAfterButtons = ` + let textAfterButtons = `

${t("level_up.after_buttons", { - level: gameState.currentLevel + 1, - max: max_levels(gameState), + level: gameState.currentLevel + 1, + max: max_levels(gameState), })}

${pickedUpgradesHTMl(gameState)} @@ -239,755 +259,748 @@ export async function openUpgradesPicker(gameState: GameState) { `; - const compliment = - (timeGain && - catchGain && - missesGain && - wallHitsGain && - t("level_up.compliment_perfect")) || - ((timeGain || catchGain || missesGain || wallHitsGain) && - t("level_up.compliment_good")) || - t("level_up.compliment_advice"); + const compliment = + (timeGain && + catchGain && + missesGain && + wallHitsGain && + t("level_up.compliment_perfect")) || + ((timeGain || catchGain || missesGain || wallHitsGain) && + t("level_up.compliment_good")) || + t("level_up.compliment_advice"); - const upgradeId = await requiredAsyncAlert({ - title: - t("level_up.pick_upgrade_title") + - (repeats ? " (" + (repeats + 1) + ")" : ""), - content: [ - `

${t("level_up.before_buttons", { - score: gameState.score - gameState.levelStartScore, - catchGain, - levelSpawnedCoins: gameState.levelSpawnedCoins, - time: Math.round(gameState.levelTime / 1000), - timeGain, - levelMisses: gameState.levelMisses, - missesGain, - levelWallBounces: gameState.levelWallBounces, - wallHitsGain, - compliment, - })} + const upgradeId = await requiredAsyncAlert({ + title: + t("level_up.pick_upgrade_title") + + (repeats ? " (" + (repeats + 1) + ")" : ""), + content: [ + `

${t("level_up.before_buttons", { + score: gameState.score - gameState.levelStartScore, + catchGain, + levelSpawnedCoins: gameState.levelSpawnedCoins, + time: Math.round(gameState.levelTime / 1000), + timeGain, + levelMisses: gameState.levelMisses, + missesGain, + levelWallBounces: gameState.levelWallBounces, + wallHitsGain, + compliment, + })}

${levelsListHTMl(gameState)}

`, - ...actions, - textAfterButtons, - ], - }); + ...actions, + textAfterButtons, + ], + }); - if (upgradeId === "reroll") { - repeats++; - gameState.rerolls--; - } else { - gameState.perks[upgradeId]++; - if (upgradeId === "instant_upgrade") { - repeats += 2; - } - gameState.runStatistics.upgrades_picked++; - } + if (upgradeId === "reroll") { + repeats++; + gameState.rerolls--; + } else { + gameState.perks[upgradeId]++; + if (upgradeId === "instant_upgrade") { + repeats += 2; + } + gameState.runStatistics.upgrades_picked++; } + } } gameCanvas.addEventListener("mouseup", (e) => { - if (e.button !== 0) return; - if (gameState.running) { - pause(true); - } else { - play(); - if (isOptionOn("pointerLock") && gameCanvas.requestPointerLock) { - gameCanvas.requestPointerLock().then(); - } + if (e.button !== 0) return; + if (gameState.running) { + pause(true); + } else { + play(); + if (isOptionOn("pointerLock") && gameCanvas.requestPointerLock) { + gameCanvas.requestPointerLock().then(); } + } }); gameCanvas.addEventListener("mousemove", (e) => { - if (document.pointerLockElement === gameCanvas) { - setMousePos(gameState, gameState.puckPosition + e.movementX); - } else { - setMousePos(gameState, e.x); - } + if (document.pointerLockElement === gameCanvas) { + setMousePos(gameState, gameState.puckPosition + e.movementX); + } else { + setMousePos(gameState, e.x); + } }); gameCanvas.addEventListener("touchstart", (e) => { - e.preventDefault(); - if (!e.touches?.length) return; + e.preventDefault(); + if (!e.touches?.length) return; - setMousePos(gameState, e.touches[0].pageX); - normalizeGameState(gameState); - play(); + setMousePos(gameState, e.touches[0].pageX); + normalizeGameState(gameState); + play(); }); gameCanvas.addEventListener("touchend", (e) => { - e.preventDefault(); - pause(true); + e.preventDefault(); + pause(true); }); gameCanvas.addEventListener("touchcancel", (e) => { - e.preventDefault(); - pause(true); + e.preventDefault(); + pause(true); }); gameCanvas.addEventListener("touchmove", (e) => { - if (!e.touches?.length) return; - setMousePos(gameState, e.touches[0].pageX); + if (!e.touches?.length) return; + setMousePos(gameState, e.touches[0].pageX); }); export function brickIndex(x: number, y: number) { - return getRowColIndex( - gameState, - Math.floor(y / gameState.brickWidth), - Math.floor((x - gameState.offsetX) / gameState.brickWidth), - ); + return getRowColIndex( + gameState, + Math.floor(y / gameState.brickWidth), + Math.floor((x - gameState.offsetX) / gameState.brickWidth), + ); } export function hasBrick(index: number): number | undefined { - if (gameState.bricks[index]) return index; + if (gameState.bricks[index]) return index; } export function hitsSomething(x: number, y: number, radius: number) { - 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)) + ); } export function tick() { - const currentTick = performance.now(); - const timeDeltaMs = currentTick - gameState.lastTick; - gameState.lastTick = currentTick; + const currentTick = performance.now(); + const timeDeltaMs = currentTick - gameState.lastTick; + gameState.lastTick = currentTick; - const frames = Math.min(4, timeDeltaMs / (1000 / 60)); + const frames = Math.min(4, timeDeltaMs / (1000 / 60)); - if (gameState.keyboardPuckSpeed) { - setMousePos( - gameState, - gameState.puckPosition + gameState.keyboardPuckSpeed, - ); - } - normalizeGameState(gameState); + if (gameState.keyboardPuckSpeed) { + setMousePos( + gameState, + gameState.puckPosition + gameState.keyboardPuckSpeed, + ); + } + normalizeGameState(gameState); - if (gameState.running) { - gameState.levelTime += timeDeltaMs; - gameState.runStatistics.runTime += timeDeltaMs; - gameStateTick(gameState, frames); - } - if (gameState.running || gameState.needsRender) { - gameState.needsRender = false; - render(gameState); - } - if (gameState.running) { - recordOneFrame(gameState); - } - if (isOptionOn("sound")) { - playPendingSounds(gameState); - } + if (gameState.running) { + gameState.levelTime += timeDeltaMs; + gameState.runStatistics.runTime += timeDeltaMs; + gameStateTick(gameState, frames); + } + if (gameState.running || gameState.needsRender) { + gameState.needsRender = false; + render(gameState); + } + if (gameState.running) { + recordOneFrame(gameState); + } + if (isOptionOn("sound")) { + playPendingSounds(gameState); + } - requestAnimationFrame(tick); - FPSCounter++; + requestAnimationFrame(tick); + FPSCounter++; } let FPSCounter = 0; export let lastMeasuredFPS = 60; setInterval(() => { - lastMeasuredFPS = FPSCounter - FPSCounter = 0; + lastMeasuredFPS = FPSCounter; + FPSCounter = 0; }, 1000); window.addEventListener("visibilitychange", () => { - if (document.hidden) { - pause(true); - } + if (document.hidden) { + pause(true); + } }); scoreDisplay.addEventListener("click", (e) => { - e.preventDefault(); - if (!alertsOpen) { - openScorePanel(); - } + e.preventDefault(); + if (!alertsOpen) { + openScorePanel(); + } }); document.addEventListener("visibilitychange", () => { - if (document.hidden) { - pause(true); - } + if (document.hidden) { + pause(true); + } }); async function openScorePanel() { - pause(true); + pause(true); - const banned = upgrades - .filter((u) => gameState.bannedPerks[u.id]) - .map((u) => u.name) - .join(", "); + const banned = upgrades + .filter((u) => gameState.bannedPerks[u.id]) + .map((u) => u.name) + .join(", "); + const cb = await asyncAlert({ + title: gameState.loop + ? t("score_panel.title_looped", { + loop: gameState.loop, + score: gameState.score, + level: gameState.currentLevel + 1, + max: max_levels(gameState), + }) + : t("score_panel.title", { + score: gameState.score, + level: gameState.currentLevel + 1, + max: max_levels(gameState), + }), - const cb = await asyncAlert({ - title: gameState.loop - ? t("score_panel.title_looped", { - loop: gameState.loop, - score: gameState.score, - level: gameState.currentLevel + 1, - max: max_levels(gameState), - }) - : t("score_panel.title", { - score: gameState.score, - level: gameState.currentLevel + 1, - max: max_levels(gameState), - }), - - content: [ - gameState.isCreativeModeRun ? `

${t("score_panel.test_run")}

` : "", - pickedUpgradesHTMl(gameState), - levelsListHTMl(gameState), - gameState.rerolls ? - t('score_panel.rerolls_count', {rerolls: gameState.rerolls}) : '', - banned && t('score_panel.banned', {banned}) - ], - allowClose: true, - }); + content: [ + gameState.isCreativeModeRun ? `

${t("score_panel.test_run")}

` : "", + pickedUpgradesHTMl(gameState), + levelsListHTMl(gameState), + gameState.rerolls + ? t("score_panel.rerolls_count", { rerolls: gameState.rerolls }) + : "", + banned && t("score_panel.banned", { banned }), + ], + allowClose: true, + }); } (document.getElementById("menu") as HTMLButtonElement).addEventListener( - "click", - (e) => { - e.preventDefault(); - if (!alertsOpen) { - openMainMenu(); - } - }, + "click", + (e) => { + e.preventDefault(); + if (!alertsOpen) { + openMainMenu(); + } + }, ); export async function openMainMenu() { - pause(true); + pause(true); - const creativeModeThreshold = Math.max(...upgrades.map((u) => u.threshold)); - const actions: AsyncAlertAction<() => void>[] = [ - { - icon: icons["icon:7_levels_run"], - text: t("main_menu.normal"), - help: t("main_menu.normal_help"), - value: () => { - restart({levelToAvoid: currentLevelInfo(gameState).name}); - }, - }, - { - icon: icons["icon:unlocks"], - text: t("main_menu.unlocks"), - help: t("main_menu.unlocks_help"), - value() { - openUnlocksList(); - }, - }, - { - icon: icons["icon:sandbox"], - text: t("sandbox.title"), - help: - getTotalScore() < creativeModeThreshold - ? t("sandbox.unlocks_at", {score: creativeModeThreshold}) - : t("sandbox.help"), - disabled: getTotalScore() < creativeModeThreshold, - async value() { - let creativeModePerks: Partial<{ [id in PerkId]: number }> = - getSettingValue("creativeModePerks", {}), - choice: "start" | Upgrade | void; + const creativeModeThreshold = Math.max(...upgrades.map((u) => u.threshold)); + const actions: AsyncAlertAction<() => void>[] = [ + { + icon: icons["icon:7_levels_run"], + text: t("main_menu.normal"), + help: t("main_menu.normal_help"), + value: () => { + restart({ levelToAvoid: currentLevelInfo(gameState).name }); + }, + }, + { + icon: icons["icon:unlocks"], + text: t("main_menu.unlocks"), + help: t("main_menu.unlocks_help"), + value() { + openUnlocksList(); + }, + }, + { + icon: icons["icon:sandbox"], + text: t("sandbox.title"), + help: + getTotalScore() < creativeModeThreshold + ? t("sandbox.unlocks_at", { score: creativeModeThreshold }) + : t("sandbox.help"), + disabled: getTotalScore() < creativeModeThreshold, + async value() { + let creativeModePerks: Partial<{ [id in PerkId]: number }> = + getSettingValue("creativeModePerks", {}), + choice: "start" | Upgrade | void; - while ( - (choice = await asyncAlert<"start" | Upgrade>({ - title: t("sandbox.title"), - actionsAsGrid: true, - content: [ - t("sandbox.instructions"), - ...upgrades.map((u) => ({ - icon: u.icon, - text: u.name, - help: (creativeModePerks[u.id] || 0) + "/" + u.max, - value: u, - className: creativeModePerks[u.id] - ? "" - : "grey-out-unless-hovered", - })), - { - text: t("sandbox.start"), - value: "start", - icon: icons["icon:continue"], - }, - ], - })) - ) { - if (choice === "start") { - restart({perks: creativeModePerks}); - break; - } else if (choice) { - creativeModePerks[choice.id] = - ((creativeModePerks[choice.id] || 0) + 1) % (choice.max + 1); - setSettingValue("creativeModePerks", creativeModePerks); - } - } - }, - }, + while ( + (choice = await asyncAlert<"start" | Upgrade>({ + title: t("sandbox.title"), + actionsAsGrid: true, + content: [ + t("sandbox.instructions"), + ...upgrades.map((u) => ({ + icon: u.icon, + text: u.name, + help: (creativeModePerks[u.id] || 0) + "/" + u.max, + value: u, + className: creativeModePerks[u.id] + ? "" + : "grey-out-unless-hovered", + })), + { + text: t("sandbox.start"), + value: "start", + icon: icons["icon:continue"], + }, + ], + })) + ) { + if (choice === "start") { + restart({ perks: creativeModePerks }); + break; + } else if (choice) { + creativeModePerks[choice.id] = + ((creativeModePerks[choice.id] || 0) + 1) % (choice.max + 1); + setSettingValue("creativeModePerks", creativeModePerks); + } + } + }, + }, - premiumMenuEntry(gameState), - { - text: t("main_menu.settings_title"), - help: t("main_menu.settings_help"), - icon: icons["icon:settings"], - value() { - openSettingsMenu(); - }, - }, - ]; + premiumMenuEntry(gameState), + { + text: t("main_menu.settings_title"), + help: t("main_menu.settings_help"), + icon: icons["icon:settings"], + value() { + openSettingsMenu(); + }, + }, + ]; - const cb = await asyncAlert<() => void>({ - title: t("main_menu.title"), - content: [...actions, t("main_menu.footer_html", {appVersion})], - allowClose: true, - }); - if (cb) { - cb(); - gameState.needsRender = true; - } + const cb = await asyncAlert<() => void>({ + title: t("main_menu.title"), + content: [...actions, t("main_menu.footer_html", { appVersion })], + allowClose: true, + }); + if (cb) { + cb(); + gameState.needsRender = true; + } } async function openSettingsMenu() { - pause(true); + pause(true); - const actions: AsyncAlertAction<() => void>[] = []; + const actions: AsyncAlertAction<() => void>[] = []; - for (const key of Object.keys(options) as OptionId[]) { - if (options[key]) - actions.push({ - icon: isOptionOn(key) - ? icons["icon:checkmark_checked"] - : icons["icon:checkmark_unchecked"], - text: options[key].name, - help: options[key].help, - value: () => { - toggleOption(key); - fitSize(); - applyFullScreenChoice() - openSettingsMenu(); - }, - }); - } - actions.push({ - text: t("main_menu.reset"), - help: t("main_menu.reset_help"), - async value() { - if ( - await asyncAlert({ - title: t("main_menu.reset"), - content: [ - t("main_menu.reset_instruction"), - { - text: t("main_menu.reset_confirm"), - value: true, - }, - { - text: t("main_menu.reset_cancel"), - value: false, - }, - ], - allowClose: true, - }) - ) { - localStorage.clear(); - window.location.reload(); - } + for (const key of Object.keys(options) as OptionId[]) { + if (options[key]) + actions.push({ + icon: isOptionOn(key) + ? icons["icon:checkmark_checked"] + : icons["icon:checkmark_unchecked"], + text: options[key].name, + help: options[key].help, + value: () => { + toggleOption(key); + fitSize(); + applyFullScreenChoice(); + openSettingsMenu(); }, - }); - - actions.push({ - text: t("main_menu.download_save_file"), - help: t("main_menu.download_save_file_help"), - async value() { - const localStorageContent: Record = {}; - - for (let i = 0; i < localStorage.length; i++) { - const key = localStorage.key(i) as string; - const value = localStorage.getItem(key) as string; - - // Store the key-value pair in the object - localStorageContent[key] = value; - } - - const signedPayload = JSON.stringify(localStorageContent); - const dlLink = document.createElement("a"); - - dlLink.setAttribute( - "href", - "data:application/json;base64," + - btoa( - JSON.stringify({ - fileType: "B71-save-file", - appVersion, - signedPayload, - key: hashCode( - "Security by obscurity, but really the game is oss so eh" + - signedPayload, - ), - }), - ), - ); - - dlLink.setAttribute( - "download", - "b71-save-" + - new Date() - .toISOString() - .slice(0, 19) - .replace(/[^0-9]+/gi, "-") + - ".b71", - ); - document.body.appendChild(dlLink); - dlLink.click(); - setTimeout(() => document.body.removeChild(dlLink), 1000); - }, - }); - - actions.push({ - text: t("main_menu.load_save_file"), - help: t("main_menu.load_save_file_help"), - async value() { - if (!document.getElementById("save_file_picker")) { - let input: HTMLInputElement = document.createElement("input"); - input.setAttribute("type", "file"); - input.setAttribute("id", "save_file_picker"); - input.setAttribute("accept", ".b71,.json"); - input.style.position = "absolute"; - input.style.left = "-1000px"; - input.addEventListener("change", async (e) => { - try { - const file = input && input.files?.item(0); - if (file) { - const content = await new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = function () { - resolve(reader.result?.toString() || ""); - }; - reader.onerror = function () { - reject(reader.error); - }; - - // Read the file as a text string - - reader.readAsText(file); - }); - const { - fileType, - appVersion: fileVersion, - signedPayload, - key, - } = JSON.parse(content); - if (fileType !== "B71-save-file") - throw new Error("Not a B71 save file"); - if (fileVersion > appVersion) - throw new Error( - "Please update your app first, this file is for version " + - fileVersion + - " or newer.", - ); - - if ( - key !== - hashCode( - "Security by obscurity, but really the game is oss so eh" + - signedPayload, - ) - ) { - throw new Error("Key does not match content."); - } - - const localStorageContent = JSON.parse(signedPayload); - localStorage.clear(); - for (let key in localStorageContent) { - localStorage.setItem(key, localStorageContent[key]); - } - await asyncAlert({ - title: t("main_menu.save_file_loaded"), - content: [ - t("main_menu.save_file_loaded_help"), - {text: t("main_menu.save_file_loaded_ok")}, - ], - }); - window.location.reload(); - } - } catch (e: any) { - await asyncAlert({ - title: t("main_menu.save_file_error"), - content: [ - e.message, - {text: t("main_menu.save_file_loaded_ok")}, - ], - }); - } - input.value = ""; - }); - document.body.appendChild(input); - } - document.getElementById("save_file_picker")?.click(); - }, - }); - - actions.push({ - text: t("main_menu.language"), - help: t("main_menu.language_help"), - async value() { - const pick = await asyncAlert({ - title: t("main_menu.language"), - content: [ - t("main_menu.language_help"), - { - text: "English", - value: "en", - }, - { - text: "Français", - value: "fr", - }, - ], - allowClose: true, - }); - if ( - pick && - pick !== getCurrentLang() && - (await confirmRestart(gameState)) - ) { - setSettingValue("lang", pick); - window.location.reload(); - } - }, - }); - - actions.push({ - text: t("main_menu.max_coins", {max: getCurrentMaxCoins()}), - help: t("main_menu.max_coins_help"), - async value() { - cycleMaxCoins(); - await openSettingsMenu(); - }, - }); - actions.push({ - text: t("main_menu.max_particles", {max: getCurrentMaxParticles()}), - help: t("main_menu.max_particles_help"), - async value() { - cycleMaxParticles(); - await openSettingsMenu(); - }, - }); - - const cb = await asyncAlert<() => void>({ - title: t("main_menu.settings_title"), - content: [t("main_menu.settings_help"), ...actions], - allowClose: true, - }); - if (cb) { - cb(); - gameState.needsRender = true; - } -} - - -function applyFullScreenChoice(): boolean { - - try { - if (!(document.fullscreenEnabled || document.webkitFullscreenEnabled)) { - return false - } - - if (document.fullscreenElement !== null && !isOptionOn("fullscreen")) { - if (document.exitFullscreen) { - document.exitFullscreen(); - return true - } else if (document.webkitCancelFullScreen) { - document.webkitCancelFullScreen(); - return true - } - } else if (isOptionOn("fullscreen") && !document.fullscreenElement) { - const docel = document.documentElement; - if (docel.requestFullscreen) { - docel.requestFullscreen(); - return true - } else if (docel.webkitRequestFullscreen) { - docel.webkitRequestFullscreen(); - return true - } - } - } catch (e) { - console.warn(e); - - } - return false -} - - -async function openUnlocksList() { - const ts = getTotalScore(); - const upgradeActions = upgrades - .sort((a, b) => a.threshold - b.threshold) - .map(({name, id, threshold, icon, help}) => ({ - text: name, - // help: - // ts >= threshold ? help(1) : t("unlocks.unlocks_at", { threshold }), - disabled: ts < threshold, - value: {perks: {[id]: 1}} as RunParams, - icon, - })) - - const levelActions = allLevels - .sort((a, b) => a.threshold - b.threshold) - .map((l) => { - const available = ts >= l.threshold; - return { - text: l.name, - // help: available - // ? t("unlocks.level_description", { - // size: l.size, - // bricks: l.bricks.filter((i) => i).length, - // }) - // : t("unlocks.unlocks_at", { threshold: l.threshold }), - disabled: !available, - value: {level: l.name} as RunParams, - icon: icons[l.name], - }; + }); + } + actions.push({ + text: t("main_menu.reset"), + help: t("main_menu.reset_help"), + async value() { + if ( + await asyncAlert({ + title: t("main_menu.reset"), + content: [ + t("main_menu.reset_instruction"), + { + text: t("main_menu.reset_confirm"), + value: true, + }, + { + text: t("main_menu.reset_cancel"), + value: false, + }, + ], + allowClose: true, }) + ) { + localStorage.clear(); + window.location.reload(); + } + }, + }); - const percentUnlock = Math.round( - ([...upgradeActions, ...levelActions].filter((a) => !a.disabled).length / (upgradeActions.length + - levelActions.length)) * 100, - ); - const tryOn = await asyncAlert({ - title: t("unlocks.title", {percentUnlock}), + actions.push({ + text: t("main_menu.download_save_file"), + help: t("main_menu.download_save_file_help"), + async value() { + const localStorageContent: Record = {}; + + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i) as string; + const value = localStorage.getItem(key) as string; + + // Store the key-value pair in the object + localStorageContent[key] = value; + } + + const signedPayload = JSON.stringify(localStorageContent); + const dlLink = document.createElement("a"); + + dlLink.setAttribute( + "href", + "data:application/json;base64," + + btoa( + JSON.stringify({ + fileType: "B71-save-file", + appVersion, + signedPayload, + key: hashCode( + "Security by obscurity, but really the game is oss so eh" + + signedPayload, + ), + }), + ), + ); + + dlLink.setAttribute( + "download", + "b71-save-" + + new Date() + .toISOString() + .slice(0, 19) + .replace(/[^0-9]+/gi, "-") + + ".b71", + ); + document.body.appendChild(dlLink); + dlLink.click(); + setTimeout(() => document.body.removeChild(dlLink), 1000); + }, + }); + + actions.push({ + text: t("main_menu.load_save_file"), + help: t("main_menu.load_save_file_help"), + async value() { + if (!document.getElementById("save_file_picker")) { + let input: HTMLInputElement = document.createElement("input"); + input.setAttribute("type", "file"); + input.setAttribute("id", "save_file_picker"); + input.setAttribute("accept", ".b71,.json"); + input.style.position = "absolute"; + input.style.left = "-1000px"; + input.addEventListener("change", async (e) => { + try { + const file = input && input.files?.item(0); + if (file) { + const content = await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = function () { + resolve(reader.result?.toString() || ""); + }; + reader.onerror = function () { + reject(reader.error); + }; + + // Read the file as a text string + + reader.readAsText(file); + }); + const { + fileType, + appVersion: fileVersion, + signedPayload, + key, + } = JSON.parse(content); + if (fileType !== "B71-save-file") + throw new Error("Not a B71 save file"); + if (fileVersion > appVersion) + throw new Error( + "Please update your app first, this file is for version " + + fileVersion + + " or newer.", + ); + + if ( + key !== + hashCode( + "Security by obscurity, but really the game is oss so eh" + + signedPayload, + ) + ) { + throw new Error("Key does not match content."); + } + + const localStorageContent = JSON.parse(signedPayload); + localStorage.clear(); + for (let key in localStorageContent) { + localStorage.setItem(key, localStorageContent[key]); + } + await asyncAlert({ + title: t("main_menu.save_file_loaded"), + content: [ + t("main_menu.save_file_loaded_help"), + { text: t("main_menu.save_file_loaded_ok") }, + ], + }); + window.location.reload(); + } + } catch (e: any) { + await asyncAlert({ + title: t("main_menu.save_file_error"), + content: [ + e.message, + { text: t("main_menu.save_file_loaded_ok") }, + ], + }); + } + input.value = ""; + }); + document.body.appendChild(input); + } + document.getElementById("save_file_picker")?.click(); + }, + }); + + actions.push({ + text: t("main_menu.language"), + help: t("main_menu.language_help"), + async value() { + const pick = await asyncAlert({ + title: t("main_menu.language"), content: [ - `

${t("unlocks.intro", {ts, highScore: gameState.highScore})} - ${percentUnlock < 100 ? t("unlocks.greyed_out_help") : ""}

`, - ...upgradeActions, - t("unlocks.level"), - ...levelActions, - + t("main_menu.language_help"), + { + text: "English", + value: "en", + }, + { + text: "Français", + value: "fr", + }, ], allowClose: true, - actionsAsGrid: true, - }); - if (tryOn) { - if (await confirmRestart(gameState)) { - restart(tryOn); - } + }); + if ( + pick && + pick !== getCurrentLang() && + (await confirmRestart(gameState)) + ) { + setSettingValue("lang", pick); + window.location.reload(); + } + }, + }); + + actions.push({ + text: t("main_menu.max_coins", { max: getCurrentMaxCoins() }), + help: t("main_menu.max_coins_help"), + async value() { + cycleMaxCoins(); + await openSettingsMenu(); + }, + }); + actions.push({ + text: t("main_menu.max_particles", { max: getCurrentMaxParticles() }), + help: t("main_menu.max_particles_help"), + async value() { + cycleMaxParticles(); + await openSettingsMenu(); + }, + }); + + const cb = await asyncAlert<() => void>({ + title: t("main_menu.settings_title"), + content: [t("main_menu.settings_help"), ...actions], + allowClose: true, + }); + if (cb) { + cb(); + gameState.needsRender = true; + } +} + +function applyFullScreenChoice(): boolean { + try { + if (!(document.fullscreenEnabled || document.webkitFullscreenEnabled)) { + return false; } + + if (document.fullscreenElement !== null && !isOptionOn("fullscreen")) { + if (document.exitFullscreen) { + document.exitFullscreen(); + return true; + } else if (document.webkitCancelFullScreen) { + document.webkitCancelFullScreen(); + return true; + } + } else if (isOptionOn("fullscreen") && !document.fullscreenElement) { + const docel = document.documentElement; + if (docel.requestFullscreen) { + docel.requestFullscreen(); + return true; + } else if (docel.webkitRequestFullscreen) { + docel.webkitRequestFullscreen(); + return true; + } + } + } catch (e) { + console.warn(e); + } + return false; +} + +async function openUnlocksList() { + const ts = getTotalScore(); + const upgradeActions = upgrades + .sort((a, b) => a.threshold - b.threshold) + .map(({ name, id, threshold, icon, help }) => ({ + text: name, + // help: + // ts >= threshold ? help(1) : t("unlocks.unlocks_at", { threshold }), + disabled: ts < threshold, + value: { perks: { [id]: 1 } } as RunParams, + icon, + })); + + const levelActions = allLevels + .sort((a, b) => a.threshold - b.threshold) + .map((l) => { + const available = ts >= l.threshold; + return { + text: l.name, + // help: available + // ? t("unlocks.level_description", { + // size: l.size, + // bricks: l.bricks.filter((i) => i).length, + // }) + // : t("unlocks.unlocks_at", { threshold: l.threshold }), + disabled: !available, + value: { level: l.name } as RunParams, + icon: icons[l.name], + }; + }); + + const percentUnlock = Math.round( + ([...upgradeActions, ...levelActions].filter((a) => !a.disabled).length / + (upgradeActions.length + levelActions.length)) * + 100, + ); + const tryOn = await asyncAlert({ + title: t("unlocks.title", { percentUnlock }), + content: [ + `

${t("unlocks.intro", { ts, highScore: gameState.highScore })} + ${percentUnlock < 100 ? t("unlocks.greyed_out_help") : ""}

`, + ...upgradeActions, + t("unlocks.level"), + ...levelActions, + ], + allowClose: true, + actionsAsGrid: true, + }); + if (tryOn) { + if (await confirmRestart(gameState)) { + restart(tryOn); + } + } } export async function confirmRestart(gameState) { - if (!gameState.currentLevel) return true; + if (!gameState.currentLevel) return true; - return asyncAlert({ - title: t("confirmRestart.title"), - content: [ - t("confirmRestart.text"), - { - value: true, - text: t("confirmRestart.yes"), - }, - { - value: false, - text: t("confirmRestart.no"), - }, - ], - }); + return asyncAlert({ + title: t("confirmRestart.title"), + content: [ + t("confirmRestart.text"), + { + value: true, + text: t("confirmRestart.yes"), + }, + { + value: false, + text: t("confirmRestart.no"), + }, + ], + }); } - const pressed: { [k: string]: number } = { - ArrowLeft: 0, - ArrowRight: 0, - Shift: 0, + ArrowLeft: 0, + ArrowRight: 0, + Shift: 0, }; export function setKeyPressed(key: string, on: 0 | 1) { - pressed[key] = on; - gameState.keyboardPuckSpeed = - ((pressed.ArrowRight - pressed.ArrowLeft) * - (1 + pressed.Shift * 2) * - gameState.gameZoneWidth) / - 50; + pressed[key] = on; + gameState.keyboardPuckSpeed = + ((pressed.ArrowRight - pressed.ArrowLeft) * + (1 + pressed.Shift * 2) * + gameState.gameZoneWidth) / + 50; } document.addEventListener("keydown", (e) => { - if (e.key.toLowerCase() === "f" && !e.ctrlKey && !e.metaKey) { - toggleOption('fullscreen'); - applyFullScreenChoice() - } else if (e.key in pressed) { - setKeyPressed(e.key, 1); - } - if (e.key === " " && !alertsOpen) { - if (gameState.running) { - pause(true); - } else { - play(); - } + if (e.key.toLowerCase() === "f" && !e.ctrlKey && !e.metaKey) { + toggleOption("fullscreen"); + applyFullScreenChoice(); + } else if (e.key in pressed) { + setKeyPressed(e.key, 1); + } + if (e.key === " " && !alertsOpen) { + if (gameState.running) { + pause(true); } else { - return; + play(); } - e.preventDefault(); + } else { + return; + } + e.preventDefault(); }); document.addEventListener("keyup", async (e) => { - const focused = document.querySelector("button:focus"); - if (e.key in pressed) { - setKeyPressed(e.key, 0); - } else if ( - e.key === "ArrowDown" && - focused?.nextElementSibling?.tagName === "BUTTON" - ) { - (focused?.nextElementSibling 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" && gameState.running) { - pause(true); - } else if (e.key.toLowerCase() === "m" && !alertsOpen) { - openMainMenu().then(); - } else if (e.key.toLowerCase() === "s" && !alertsOpen) { - openScorePanel().then(); - } else if (e.key.toLowerCase() === "r" && !alertsOpen) { - if (await confirmRestart(gameState)) { - restart({levelToAvoid: currentLevelInfo(gameState).name}); - } - } else { - return; + const focused = document.querySelector("button:focus"); + if (e.key in pressed) { + setKeyPressed(e.key, 0); + } else if ( + e.key === "ArrowDown" && + focused?.nextElementSibling?.tagName === "BUTTON" + ) { + (focused?.nextElementSibling 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" && gameState.running) { + pause(true); + } else if (e.key.toLowerCase() === "m" && !alertsOpen) { + openMainMenu().then(); + } else if (e.key.toLowerCase() === "s" && !alertsOpen) { + openScorePanel().then(); + } else if (e.key.toLowerCase() === "r" && !alertsOpen) { + if (await confirmRestart(gameState)) { + restart({ levelToAvoid: currentLevelInfo(gameState).name }); } - e.preventDefault(); + } else { + return; + } + e.preventDefault(); }); export const gameState = newGameState({}); export function restart(params: RunParams) { - fitSize(); - Object.assign(gameState, newGameState(params)); - pauseRecording(); - setLevel(gameState, 0); + fitSize(); + Object.assign(gameState, newGameState(params)); + pauseRecording(); + setLevel(gameState, 0); } - restart( - (window.location.search.includes("stressTest") && { - level: "Bird", - perks: { - shocks:10, - multiball:6, - telekinesis:2, - ghost_coins:1, - pierce:4, - clairvoyant:3, - bigger_explosions:2, - sapper:2, - unbounded:1 - - }, - levelsPerLoop: 2, - }) || + (window.location.search.includes("stressTest") && { + level: "Bird", + perks: { + shocks: 10, + multiball: 6, + telekinesis: 2, + ghost_coins: 1, + pierce: 4, + clairvoyant: 3, + bigger_explosions: 2, + sapper: 2, + unbounded: 1, + }, + levelsPerLoop: 2, + }) || {}, ); diff --git a/src/gameStateMutators.ts b/src/gameStateMutators.ts index 4d20039..94796ce 100644 --- a/src/gameStateMutators.ts +++ b/src/gameStateMutators.ts @@ -1,1815 +1,1834 @@ import { - Ball, - BallLike, - Coin, - colorString, - GameState, - LightFlash, - ParticleFlash, - PerkId, - ReusableArray, - TextFlash, + Ball, + BallLike, + Coin, + colorString, + GameState, + LightFlash, + ParticleFlash, + PerkId, + ReusableArray, + TextFlash, } from "./types"; import { - brickCenterX, - brickCenterY, - countBricksAbove, - countBricksBelow, - currentLevelInfo, - distance2, - distanceBetween, - getMajorityValue, - getPossibleUpgrades, - getRowColIndex, - isTelekinesisActive, - isYoyoActive, - makeEmptyPerksMap, - max_levels, - shouldPierceByColor, + brickCenterX, + brickCenterY, + countBricksAbove, + countBricksBelow, + currentLevelInfo, + distance2, + distanceBetween, + getMajorityValue, + getPossibleUpgrades, + getRowColIndex, + isTelekinesisActive, + isYoyoActive, + makeEmptyPerksMap, + max_levels, + shouldPierceByColor, } from "./game_utils"; -import {t} from "./i18n/i18n"; -import {icons, upgrades} from "./loadGameData"; +import { t } from "./i18n/i18n"; +import { icons, upgrades } from "./loadGameData"; -import {addToTotalScore, getCurrentMaxCoins, getCurrentMaxParticles,} from "./settings"; -import {background} from "./render"; -import {gameOver} from "./gameOver"; -import {brickIndex, fitSize, gameState, hasBrick, hitsSomething, openUpgradesPicker, pause,} from "./game"; -import {stopRecording} from "./recording"; -import {isOptionOn} from "./options"; -import {isPremium} from "./premium"; -import {getRunLevels} from "./newGameState"; -import {requiredAsyncAlert} from "./asyncAlert"; -import {clamp, comboKeepingRate} from "./pure_functions"; +import { + addToTotalScore, + getCurrentMaxCoins, + getCurrentMaxParticles, +} from "./settings"; +import { background } from "./render"; +import { gameOver } from "./gameOver"; +import { + brickIndex, + fitSize, + gameState, + hasBrick, + hitsSomething, + openUpgradesPicker, + pause, +} from "./game"; +import { stopRecording } from "./recording"; +import { isOptionOn } from "./options"; +import { isPremium } from "./premium"; +import { getRunLevels } from "./newGameState"; +import { requiredAsyncAlert } from "./asyncAlert"; +import { clamp, comboKeepingRate } from "./pure_functions"; export function setMousePos(gameState: GameState, x: number) { - gameState.puckPosition = x; - // Sets the puck position, and updates the ball position if they are supposed to follow it - gameState.needsRender = true; + gameState.puckPosition = x; + // Sets the puck position, and updates the ball position if they are supposed to follow it + gameState.needsRender = true; } function getBallDefaultVx(gameState: GameState) { - return ( - (gameState.perks.concave_puck ? 0 : 1) * - (Math.random() > 0.5 ? gameState.baseSpeed : -gameState.baseSpeed) - ); + return ( + (gameState.perks.concave_puck ? 0 : 1) * + (Math.random() > 0.5 ? gameState.baseSpeed : -gameState.baseSpeed) + ); } export function resetBalls(gameState: GameState) { - // Always compute speed first - normalizeGameState(gameState); - const count = 1 + (gameState.perks?.multiball || 0); - const perBall = gameState.puckWidth / (count + 1); - gameState.balls = []; - gameState.ballsColor = "#FFF"; - if (gameState.perks.picky_eater || gameState.perks.pierce_color) { - gameState.ballsColor = - getMajorityValue(gameState.bricks.filter((i) => i)) || "#FFF"; - } - for (let i = 0; i < count; i++) { - const x = - gameState.puckPosition - gameState.puckWidth / 2 + perBall * (i + 1); - const vx = getBallDefaultVx(gameState); + // Always compute speed first + normalizeGameState(gameState); + const count = 1 + (gameState.perks?.multiball || 0); + const perBall = gameState.puckWidth / (count + 1); + gameState.balls = []; + gameState.ballsColor = "#FFF"; + if (gameState.perks.picky_eater || gameState.perks.pierce_color) { + gameState.ballsColor = + getMajorityValue(gameState.bricks.filter((i) => i)) || "#FFF"; + } + for (let i = 0; i < count; i++) { + const x = + gameState.puckPosition - gameState.puckWidth / 2 + perBall * (i + 1); + const vx = getBallDefaultVx(gameState); - gameState.balls.push({ - x, - previousX: x, - y: gameState.gameZoneHeight - 1.5 * gameState.ballSize, - previousY: gameState.gameZoneHeight - 1.5 * gameState.ballSize, - vx, - previousVX: vx, - vy: -gameState.baseSpeed, - previousVY: -gameState.baseSpeed, - piercePoints: gameState.perks.pierce * 3, - hitSinceBounce: 0, - brokenSinceBounce: 0, - sapperUses: 0, - }); - } - gameState.ballStickToPuck = true; + gameState.balls.push({ + x, + previousX: x, + y: gameState.gameZoneHeight - 1.5 * gameState.ballSize, + previousY: gameState.gameZoneHeight - 1.5 * gameState.ballSize, + vx, + previousVX: vx, + vy: -gameState.baseSpeed, + previousVY: -gameState.baseSpeed, + piercePoints: gameState.perks.pierce * 3, + hitSinceBounce: 0, + brokenSinceBounce: 0, + sapperUses: 0, + }); + } + gameState.ballStickToPuck = true; } export function putBallsAtPuck(gameState: GameState) { - // This reset could be abused to cheat quite easily - const count = gameState.balls.length; - const perBall = gameState.puckWidth / (count + 1); - // const vx = getBallDefaultVx(gameState); - gameState.balls.forEach((ball, i) => { - const x = - gameState.puckPosition - gameState.puckWidth / 2 + perBall * (i + 1); + // This reset could be abused to cheat quite easily + const count = gameState.balls.length; + const perBall = gameState.puckWidth / (count + 1); + // const vx = getBallDefaultVx(gameState); + gameState.balls.forEach((ball, i) => { + const x = + gameState.puckPosition - gameState.puckWidth / 2 + perBall * (i + 1); - ball.x = x; - ball.previousX = x; - ball.y = gameState.gameZoneHeight - 1.5 * gameState.ballSize; - ball.previousY = ball.y; - ball.hitSinceBounce = 0; - ball.brokenSinceBounce = 0; - ball.piercePoints = gameState.perks.pierce * 3; - }); + ball.x = x; + ball.previousX = x; + ball.y = gameState.gameZoneHeight - 1.5 * gameState.ballSize; + ball.previousY = ball.y; + ball.hitSinceBounce = 0; + ball.brokenSinceBounce = 0; + ball.piercePoints = gameState.perks.pierce * 3; + }); } export function normalizeGameState(gameState: GameState) { - // This function resets most parameters on the state to correct values, and should be used even when the game is paused + // This function resets most parameters on the state to correct values, and should be used even when the game is paused - gameState.baseSpeed = Math.max( - 3, - gameState.gameZoneWidth / 12 / 10 + - gameState.currentLevel / 3 + - gameState.levelTime / (30 * 1000) - - gameState.perks.slow_down * 2, - ); + gameState.baseSpeed = Math.max( + 3, + gameState.gameZoneWidth / 12 / 10 + + gameState.currentLevel / 3 + + gameState.levelTime / (30 * 1000) - + gameState.perks.slow_down * 2, + ); - gameState.puckWidth = Math.max(gameState.ballSize, - (gameState.gameZoneWidth / 12) * - Math.min(12, 3 - gameState.perks.smaller_puck + gameState.perks.bigger_puck)); + gameState.puckWidth = Math.max( + gameState.ballSize, + (gameState.gameZoneWidth / 12) * + Math.min( + 12, + 3 - gameState.perks.smaller_puck + gameState.perks.bigger_puck, + ), + ); - const corner = gameState.levelTime ? gameState.perks.corner_shot : 0 + const corner = gameState.levelTime ? gameState.perks.corner_shot : 0; - let minX = gameState.offsetXRoundedDown + gameState.puckWidth / 2 - gameState.puckWidth * corner + let minX = + gameState.offsetXRoundedDown + + gameState.puckWidth / 2 - + gameState.puckWidth * corner; - let maxX = gameState.offsetXRoundedDown + - gameState.gameZoneWidthRoundedUp - - gameState.puckWidth / 2 + gameState.puckWidth * corner; + let maxX = + gameState.offsetXRoundedDown + + gameState.gameZoneWidthRoundedUp - + gameState.puckWidth / 2 + + gameState.puckWidth * corner; + gameState.puckPosition = clamp(gameState.puckPosition, minX, maxX); - gameState.puckPosition = clamp(gameState.puckPosition, minX, maxX); + if (gameState.ballStickToPuck) { + putBallsAtPuck(gameState); + } - if (gameState.ballStickToPuck) { - putBallsAtPuck(gameState); - } - - if ( - Math.abs(gameState.lastPuckPosition - gameState.puckPosition) > 1 && - gameState.running - ) { - gameState.lastPuckMove = gameState.levelTime; - } - gameState.lastPuckPosition = gameState.puckPosition; + if ( + Math.abs(gameState.lastPuckPosition - gameState.puckPosition) > 1 && + gameState.running + ) { + gameState.lastPuckMove = gameState.levelTime; + } + gameState.lastPuckPosition = gameState.puckPosition; } export function baseCombo(gameState: GameState) { - return ( - gameState.baseCombo + - gameState.perks.base_combo * 3 + - gameState.perks.smaller_puck * 5 - ); + return ( + gameState.baseCombo + + gameState.perks.base_combo * 3 + + gameState.perks.smaller_puck * 5 + ); } export function resetCombo( - gameState: GameState, - x: number | undefined, - y: number | undefined, + gameState: GameState, + x: number | undefined, + y: number | undefined, ) { - const prev = gameState.combo; - gameState.combo = baseCombo(gameState); + const prev = gameState.combo; + gameState.combo = baseCombo(gameState); - if (prev > gameState.combo && gameState.perks.soft_reset) { - gameState.combo += Math.floor( - (prev - gameState.combo) * comboKeepingRate(gameState.perks.soft_reset) - ); + if (prev > gameState.combo && gameState.perks.soft_reset) { + gameState.combo += Math.floor( + (prev - gameState.combo) * comboKeepingRate(gameState.perks.soft_reset), + ); + } + const lost = Math.max(0, prev - gameState.combo); + if (lost) { + for (let i = 0; i < lost && i < 8; i++) { + setTimeout( + () => schedulGameSound(gameState, "comboDecrease", x, 1), + i * 100, + ); } - const lost = Math.max(0, prev - gameState.combo); - if (lost) { - for (let i = 0; i < lost && i < 8; i++) { - setTimeout( - () => schedulGameSound(gameState, "comboDecrease", x, 1), - i * 100, - ); - } - if (typeof x !== "undefined" && typeof y !== "undefined") { - - makeText(gameState, x, y, "red", "-" + lost, 20, 500 + clamp(lost, 0, 500)); - } + if (typeof x !== "undefined" && typeof y !== "undefined") { + makeText( + gameState, + x, + y, + "red", + "-" + lost, + 20, + 500 + clamp(lost, 0, 500), + ); } - return lost; + } + return lost; } export function decreaseCombo( - gameState: GameState, - by: number, - x: number, - y: number, + gameState: GameState, + by: number, + x: number, + y: number, ) { - const prev = gameState.combo; - gameState.combo = Math.max(baseCombo(gameState), gameState.combo - by); - const lost = Math.max(0, prev - gameState.combo); + const prev = gameState.combo; + gameState.combo = Math.max(baseCombo(gameState), gameState.combo - by); + const lost = Math.max(0, prev - gameState.combo); - if (lost) { - schedulGameSound(gameState, "comboDecrease", x, 1); - if (typeof x !== "undefined" && typeof y !== "undefined") { - makeText(gameState, x, y, "red", "-" + lost, 20, 400 + lost); - } + if (lost) { + schedulGameSound(gameState, "comboDecrease", x, 1); + if (typeof x !== "undefined" && typeof y !== "undefined") { + makeText(gameState, x, y, "red", "-" + lost, 20, 400 + lost); } + } } export function spawnExplosion( - gameState: GameState, - count: number, - x: number, - y: number, - color: string, + gameState: GameState, + count: number, + x: number, + y: number, + color: string, ) { - if (!!isOptionOn("basic")) return; + if (!!isOptionOn("basic")) return; - if (liveCount(gameState.particles) > getCurrentMaxParticles()) { - // Avoid freezing when lots of explosion happen at once - count = 1; - } - for (let i = 0; i < count; i++) { - makeParticle( - gameState, + if (liveCount(gameState.particles) > getCurrentMaxParticles()) { + // Avoid freezing when lots of explosion happen at once + count = 1; + } + for (let i = 0; i < count; i++) { + makeParticle( + gameState, - x + ((Math.random() - 0.5) * gameState.brickWidth) / 2, - y + ((Math.random() - 0.5) * gameState.brickWidth) / 2, - (Math.random() - 0.5) * 30, - (Math.random() - 0.5) * 30, - color, - false, - ); - } + x + ((Math.random() - 0.5) * gameState.brickWidth) / 2, + y + ((Math.random() - 0.5) * gameState.brickWidth) / 2, + (Math.random() - 0.5) * 30, + (Math.random() - 0.5) * 30, + color, + false, + ); + } } export function spawnImplosion( - gameState: GameState, - count: number, - x: number, - y: number, - color: string, + gameState: GameState, + count: number, + x: number, + y: number, + color: string, ) { - if (!!isOptionOn("basic")) return; + if (!!isOptionOn("basic")) return; - if (liveCount(gameState.particles) > getCurrentMaxParticles()) { - // Avoid freezing when lots of explosion happen at once - count = 1; - } - for (let i = 0; i < count; i++) { - const dx = ((Math.random() - 0.5) * gameState.brickWidth) / 2; - const dy = ((Math.random() - 0.5) * gameState.brickWidth) / 2; - makeParticle(gameState, x - dx * 10, y - dy * 10, dx, dy, color, false); - } + if (liveCount(gameState.particles) > getCurrentMaxParticles()) { + // Avoid freezing when lots of explosion happen at once + count = 1; + } + for (let i = 0; i < count; i++) { + const dx = ((Math.random() - 0.5) * gameState.brickWidth) / 2; + const dy = ((Math.random() - 0.5) * gameState.brickWidth) / 2; + makeParticle(gameState, x - dx * 10, y - dy * 10, dx, dy, color, false); + } } export function explosionAt( - gameState: GameState, - index: number, - x: number, - y: number, - ball: Ball, - extraSize: number = 0 + gameState: GameState, + index: number, + x: number, + y: number, + ball: Ball, + extraSize: number = 0, ) { - const size = 1 + gameState.perks.bigger_explosions + - Math.max(0, gameState.perks.implosions - 1) + extraSize - ; - schedulGameSound(gameState, "explode", ball.x, 1); - if (index !== -1) { - const col = index % gameState.gridSize; - const row = Math.floor(index / gameState.gridSize); - // Break bricks around - for (let dx = -size; dx <= size; dx++) { - for (let dy = -size; dy <= size; dy++) { - const i = getRowColIndex(gameState, row + dy, col + dx); - if (gameState.bricks[i] && i !== -1) { - // Study bricks resist explosions too - gameState.brickHP[i]--; - if (gameState.brickHP[i] <= 0) { - explodeBrick(gameState, i, ball, true); - } - } - } + const size = + 1 + + gameState.perks.bigger_explosions + + Math.max(0, gameState.perks.implosions - 1) + + extraSize; + schedulGameSound(gameState, "explode", ball.x, 1); + if (index !== -1) { + const col = index % gameState.gridSize; + const row = Math.floor(index / gameState.gridSize); + // Break bricks around + for (let dx = -size; dx <= size; dx++) { + for (let dy = -size; dy <= size; dy++) { + const i = getRowColIndex(gameState, row + dy, col + dx); + if (gameState.bricks[i] && i !== -1) { + // Study bricks resist explosions too + gameState.brickHP[i]--; + if (gameState.brickHP[i] <= 0) { + explodeBrick(gameState, i, ball, true); + } } + } } + } - const factor = gameState.perks.implosions ? -1 : 1; - // Blow nearby coins - forEachLiveOne(gameState.coins, (c) => { - const dx = c.x - x; - const dy = c.y - y; - const d2 = Math.max(gameState.brickWidth, Math.abs(dx) + Math.abs(dy)); - c.vx += (((dx / d2) * 10 * size) / c.weight) * factor; - c.vy += (((dy / d2) * 10 * size) / c.weight) * factor; - }); - gameState.lastExplosion = Date.now(); + const factor = gameState.perks.implosions ? -1 : 1; + // Blow nearby coins + forEachLiveOne(gameState.coins, (c) => { + const dx = c.x - x; + const dy = c.y - y; + const d2 = Math.max(gameState.brickWidth, Math.abs(dx) + Math.abs(dy)); + c.vx += (((dx / d2) * 10 * size) / c.weight) * factor; + c.vy += (((dy / d2) * 10 * size) / c.weight) * factor; + }); + gameState.lastExplosion = Date.now(); - // makeLight(gameState, x, y, "white", gameState.brickWidth * 2, 150); - if (gameState.perks.implosions) { - spawnImplosion( - gameState, - 7 * size, - x, - y, - "white", - ); - } else { - spawnExplosion( - gameState, - 7 * size, - x, - y, - "white", - ); - } + // makeLight(gameState, x, y, "white", gameState.brickWidth * 2, 150); + if (gameState.perks.implosions) { + spawnImplosion(gameState, 7 * size, x, y, "white"); + } else { + spawnExplosion(gameState, 7 * size, x, y, "white"); + } - gameState.runStatistics.bricks_broken++; + gameState.runStatistics.bricks_broken++; - if (gameState.perks.zen) { - resetCombo(gameState, x, y); - } + if (gameState.perks.zen) { + resetCombo(gameState, x, y); + } } export function explodeBrick( - gameState: GameState, - index: number, - ball: Ball, - isExplosion: boolean, + gameState: GameState, + index: number, + ball: Ball, + isExplosion: boolean, ) { - const color = gameState.bricks[index]; - if (!color) return; + const color = gameState.bricks[index]; + if (!color) return; - if (color === "black") { - const x = brickCenterX(gameState, index), - y = brickCenterY(gameState, index); + if (color === "black") { + const x = brickCenterX(gameState, index), + y = brickCenterY(gameState, index); - // if (color === "transparent") { - // schedulGameSound(gameState, "void", x, 1); - // resetCombo(gameState, x, y); - // } - setBrick(gameState, index, ""); - explosionAt(gameState, index, x, y, ball, 0); - } else if (color) { - // Even if it bounces we don't want to count that as a miss + // if (color === "transparent") { + // schedulGameSound(gameState, "void", x, 1); + // resetCombo(gameState, x, y); + // } + setBrick(gameState, index, ""); + explosionAt(gameState, index, x, y, ball, 0); + } else if (color) { + // Even if it bounces we don't want to count that as a miss - // Flashing is take care of by the tick loop - const x = brickCenterX(gameState, index), - y = brickCenterY(gameState, index); + // Flashing is take care of by the tick loop + const x = brickCenterX(gameState, index), + y = brickCenterY(gameState, index); - setBrick(gameState, index, ""); + setBrick(gameState, index, ""); - let coinsToSpawn = gameState.combo; - if (gameState.perks.sturdy_bricks) { - // +10% per level - coinsToSpawn += Math.ceil( - ((10 + gameState.perks.sturdy_bricks) / 10) * coinsToSpawn, - ); - } - - gameState.levelSpawnedCoins += coinsToSpawn; - gameState.runStatistics.coins_spawned += coinsToSpawn; - gameState.runStatistics.bricks_broken++; - const maxCoins = getCurrentMaxCoins() * (isOptionOn("basic") ? 0.5 : 1); - const spawnableCoins = - liveCount(gameState.coins) > getCurrentMaxCoins() - ? 1 - : Math.floor(maxCoins - liveCount(gameState.coins)) / 3; - - const pointsPerCoin = Math.max(1, Math.ceil(coinsToSpawn / spawnableCoins)); - - while (coinsToSpawn > 0) { - const points = Math.min(pointsPerCoin, coinsToSpawn); - if (points < 0 || isNaN(points)) { - console.error({points}); - debugger; - } - - coinsToSpawn -= points; - - const cx = - x + - (Math.random() - 0.5) * (gameState.brickWidth - gameState.coinSize), - cy = - y + - (Math.random() - 0.5) * (gameState.brickWidth - gameState.coinSize); - makeCoin( - gameState, - cx, - cy, - ball.previousVX * (0.5 + Math.random()), - ball.previousVY * (0.5 + Math.random()), - gameState.perks.metamorphosis || isOptionOn("colorful_coins") - ? color - : "gold", - points, - ); - } - - gameState.combo += - gameState.perks.streak_shots + - gameState.perks.compound_interest + - gameState.perks.left_is_lava + - gameState.perks.right_is_lava + - gameState.perks.top_is_lava + - gameState.perks.picky_eater + - gameState.perks.asceticism * 3 + - gameState.perks.zen + - gameState.perks.passive_income + - gameState.perks.nbricks + - gameState.perks.unbounded; - - if (gameState.perks.side_kick) { - if (Math.abs(ball.vx) > Math.abs(ball.vy)) { - gameState.combo += gameState.perks.side_kick; - } else { - decreaseCombo(gameState, gameState.perks.side_kick, ball.x, ball.y); - } - } - - if (gameState.perks.reach) { - if ( - countBricksAbove(gameState, index) && - !countBricksBelow(gameState, index) - ) { - resetCombo(gameState, x, y); - } else { - gameState.combo += gameState.perks.reach; - } - } - - if ( - gameState.lastPuckMove && - gameState.perks.passive_income && - gameState.lastPuckMove > - gameState.levelTime - 250 * gameState.perks.passive_income - ) { - resetCombo(gameState, x, y); - } - - if ( - gameState.perks.nbricks && - ball.brokenSinceBounce > gameState.perks.nbricks - ) { - // We need to reset at each hit, otherwise it's just an OP version of single puck hit streak - resetCombo(gameState, ball.x, ball.y); - } - - if (!isExplosion) { - // color change - if ( - (gameState.perks.picky_eater || gameState.perks.pierce_color) && - color !== gameState.ballsColor && - color - ) { - if (gameState.perks.picky_eater) { - resetCombo(gameState, ball.x, ball.y); - } - schedulGameSound(gameState, "colorChange", ball.x, 0.8); - gameState.lastExplosion = gameState.levelTime; - gameState.ballsColor = color; - if (!isOptionOn("basic")) { - gameState.balls.forEach((ball) => { - spawnExplosion(gameState, 7, ball.previousX, ball.previousY, color); - }); - } - } else { - schedulGameSound(gameState, "comboIncreaseMaybe", ball.x, 1); - } - } - // makeLight(gameState, x, y, color, gameState.brickWidth, 40); - - spawnExplosion(gameState, 5 + Math.min(gameState.combo, 30), x, y, color); + let coinsToSpawn = gameState.combo; + if (gameState.perks.sturdy_bricks) { + // +10% per level + coinsToSpawn += Math.ceil( + ((10 + gameState.perks.sturdy_bricks) / 10) * coinsToSpawn, + ); } - if (gameState.perks.respawn && color !== "black" && !gameState.bricks[index]) { - if (Math.random() < comboKeepingRate(gameState.perks.respawn)) { - append(gameState.respawns, b => { - b.color = color - b.index = index - b.time = gameState.levelTime + 3 * 1000 / gameState.perks.respawn - }) - } + gameState.levelSpawnedCoins += coinsToSpawn; + gameState.runStatistics.coins_spawned += coinsToSpawn; + gameState.runStatistics.bricks_broken++; + const maxCoins = getCurrentMaxCoins() * (isOptionOn("basic") ? 0.5 : 1); + const spawnableCoins = + liveCount(gameState.coins) > getCurrentMaxCoins() + ? 1 + : Math.floor(maxCoins - liveCount(gameState.coins)) / 3; + + const pointsPerCoin = Math.max(1, Math.ceil(coinsToSpawn / spawnableCoins)); + + while (coinsToSpawn > 0) { + const points = Math.min(pointsPerCoin, coinsToSpawn); + if (points < 0 || isNaN(points)) { + console.error({ points }); + debugger; + } + + coinsToSpawn -= points; + + const cx = + x + + (Math.random() - 0.5) * (gameState.brickWidth - gameState.coinSize), + cy = + y + + (Math.random() - 0.5) * (gameState.brickWidth - gameState.coinSize); + makeCoin( + gameState, + cx, + cy, + ball.previousVX * (0.5 + Math.random()), + ball.previousVY * (0.5 + Math.random()), + gameState.perks.metamorphosis || isOptionOn("colorful_coins") + ? color + : "gold", + points, + ); } + + gameState.combo += + gameState.perks.streak_shots + + gameState.perks.compound_interest + + gameState.perks.left_is_lava + + gameState.perks.right_is_lava + + gameState.perks.top_is_lava + + gameState.perks.picky_eater + + gameState.perks.asceticism * 3 + + gameState.perks.zen + + gameState.perks.passive_income + + gameState.perks.nbricks + + gameState.perks.unbounded; + + if (gameState.perks.side_kick) { + if (Math.abs(ball.vx) > Math.abs(ball.vy)) { + gameState.combo += gameState.perks.side_kick; + } else { + decreaseCombo(gameState, gameState.perks.side_kick, ball.x, ball.y); + } + } + + if (gameState.perks.reach) { + if ( + countBricksAbove(gameState, index) && + !countBricksBelow(gameState, index) + ) { + resetCombo(gameState, x, y); + } else { + gameState.combo += gameState.perks.reach; + } + } + + if ( + gameState.lastPuckMove && + gameState.perks.passive_income && + gameState.lastPuckMove > + gameState.levelTime - 250 * gameState.perks.passive_income + ) { + resetCombo(gameState, x, y); + } + + if ( + gameState.perks.nbricks && + ball.brokenSinceBounce > gameState.perks.nbricks + ) { + // We need to reset at each hit, otherwise it's just an OP version of single puck hit streak + resetCombo(gameState, ball.x, ball.y); + } + + if (!isExplosion) { + // color change + if ( + (gameState.perks.picky_eater || gameState.perks.pierce_color) && + color !== gameState.ballsColor && + color + ) { + if (gameState.perks.picky_eater) { + resetCombo(gameState, ball.x, ball.y); + } + schedulGameSound(gameState, "colorChange", ball.x, 0.8); + gameState.lastExplosion = gameState.levelTime; + gameState.ballsColor = color; + if (!isOptionOn("basic")) { + gameState.balls.forEach((ball) => { + spawnExplosion(gameState, 7, ball.previousX, ball.previousY, color); + }); + } + } else { + schedulGameSound(gameState, "comboIncreaseMaybe", ball.x, 1); + } + } + // makeLight(gameState, x, y, color, gameState.brickWidth, 40); + + spawnExplosion(gameState, 5 + Math.min(gameState.combo, 30), x, y, color); + } + + if ( + gameState.perks.respawn && + color !== "black" && + !gameState.bricks[index] + ) { + if (Math.random() < comboKeepingRate(gameState.perks.respawn)) { + append(gameState.respawns, (b) => { + b.color = color; + b.index = index; + b.time = gameState.levelTime + (3 * 1000) / gameState.perks.respawn; + }); + } + } } export function dontOfferTooSoon(gameState: GameState, id: PerkId) { - gameState.lastOffered[id] = Math.round(Date.now() / 1000); + gameState.lastOffered[id] = Math.round(Date.now() / 1000); } export function pickRandomUpgrades(gameState: GameState, count: number) { - let list = getPossibleUpgrades(gameState) - .map((u) => ({ - ...u, - score: Math.random() + (gameState.lastOffered[u.id] || 0), - })) - .sort((a, b) => a.score - b.score) - .filter((u) => gameState.perks[u.id] < u.max) - .filter((u) => !gameState.bannedPerks[u.id]) - .slice(0, count) - .sort((a, b) => (a.id > b.id ? 1 : -1)); + let list = getPossibleUpgrades(gameState) + .map((u) => ({ + ...u, + score: Math.random() + (gameState.lastOffered[u.id] || 0), + })) + .sort((a, b) => a.score - b.score) + .filter((u) => gameState.perks[u.id] < u.max) + .filter((u) => !gameState.bannedPerks[u.id]) + .slice(0, count) + .sort((a, b) => (a.id > b.id ? 1 : -1)); - list.forEach((u) => { - dontOfferTooSoon(gameState, u.id); - }); + list.forEach((u) => { + dontOfferTooSoon(gameState, u.id); + }); - return list.map((u) => ({ - text: - u.name + - (gameState.perks[u.id] - ? t("level_up.upgrade_perk_to_level", { - level: gameState.perks[u.id] + 1, - }) - : ""), - icon: icons["icon:" + u.id], - value: u.id as PerkId, - help: u.help(gameState.perks[u.id] + 1), - })); + return list.map((u) => ({ + text: + u.name + + (gameState.perks[u.id] + ? t("level_up.upgrade_perk_to_level", { + level: gameState.perks[u.id] + 1, + }) + : ""), + icon: icons["icon:" + u.id], + value: u.id as PerkId, + help: u.help(gameState.perks[u.id] + 1), + })); } export function schedulGameSound( - gameState: GameState, - sound: keyof GameState["aboutToPlaySound"], - x: number | void, - vol: number, + gameState: GameState, + sound: keyof GameState["aboutToPlaySound"], + x: number | void, + vol: number, ) { - if (!vol) return; - x ??= gameState.offsetX + gameState.gameZoneWidth / 2; - const ex = gameState.aboutToPlaySound[sound] as { vol: number; x: number }; + if (!vol) return; + x ??= gameState.offsetX + gameState.gameZoneWidth / 2; + const ex = gameState.aboutToPlaySound[sound] as { vol: number; x: number }; - ex.x = (x * vol + ex.x * ex.vol) / (vol + ex.vol); - ex.vol += vol; + ex.x = (x * vol + ex.x * ex.vol) / (vol + ex.vol); + ex.vol += vol; } export function addToScore(gameState: GameState, coin: Coin) { - gameState.score += coin.points; - gameState.lastScoreIncrease = gameState.levelTime; - addToTotalScore(gameState, coin.points); - if (gameState.score > gameState.highScore && !gameState.isCreativeModeRun) { - gameState.highScore = gameState.score; - localStorage.setItem("breakout-3-hs", gameState.score.toString()); - } - if (!isOptionOn("basic")) { - makeParticle( - gameState, - coin.previousX, - coin.previousY, - (gameState.canvasWidth - coin.x) / 100, - -coin.y / 100, - coin.color, - true, - gameState.coinSize / 2, - 100 + Math.random() * 50, - ); - } + gameState.score += coin.points; + gameState.lastScoreIncrease = gameState.levelTime; + addToTotalScore(gameState, coin.points); + if (gameState.score > gameState.highScore && !gameState.isCreativeModeRun) { + gameState.highScore = gameState.score; + localStorage.setItem("breakout-3-hs", gameState.score.toString()); + } + if (!isOptionOn("basic")) { + makeParticle( + gameState, + coin.previousX, + coin.previousY, + (gameState.canvasWidth - coin.x) / 100, + -coin.y / 100, + coin.color, + true, + gameState.coinSize / 2, + 100 + Math.random() * 50, + ); + } - schedulGameSound(gameState, "coinCatch", coin.x, 1); - gameState.runStatistics.score += coin.points; - if (gameState.perks.asceticism) { - resetCombo(gameState, coin.x, coin.y); - } + schedulGameSound(gameState, "coinCatch", coin.x, 1); + gameState.runStatistics.score += coin.points; + if (gameState.perks.asceticism) { + resetCombo(gameState, coin.x, coin.y); + } } export async function gotoNextLoop(gameState: GameState) { - pause(false); - gameState.loop++; - gameState.runStatistics.loops++; - gameState.runLevels = getRunLevels(gameState.totalScoreAtRunStart, {}); - gameState.upgradesOfferedFor = -1; + pause(false); + gameState.loop++; + gameState.runStatistics.loops++; + gameState.runLevels = getRunLevels(gameState.totalScoreAtRunStart, {}); + gameState.upgradesOfferedFor = -1; - let comboText = ""; - if (gameState.rerolls) { - comboText = t("loop.converted_rerolls", {n: gameState.rerolls}); - gameState.baseCombo += gameState.rerolls; - gameState.rerolls = 0; - } else { - comboText = t("loop.no_rerolls"); + let comboText = ""; + if (gameState.rerolls) { + comboText = t("loop.converted_rerolls", { n: gameState.rerolls }); + gameState.baseCombo += gameState.rerolls; + gameState.rerolls = 0; + } else { + comboText = t("loop.no_rerolls"); + } + + const userPerks = upgrades.filter((u) => gameState.perks[u.id]); + + const keep = await requiredAsyncAlert({ + title: t("loop.title", { loop: gameState.loop }), + content: [ + t("loop.instructions"), + comboText, + ...userPerks + .filter((u) => u.id !== "instant_upgrade") + .map((u) => { + return { + text: + u.name + + t("level_up.upgrade_perk_to_level", { + level: gameState.perks[u.id] + 1, + }), + icon: u.icon, + value: u.id, + help: u.help(gameState.perks[u.id] + 1), + }; + }), + ], + }); + + userPerks.forEach((u) => { + if (u.id !== keep) { + gameState.bannedPerks[u.id] = 1; } + }); - const userPerks = upgrades.filter((u) => gameState.perks[u.id]); + Object.assign(gameState.perks, makeEmptyPerksMap(upgrades), { + [keep]: gameState.perks[keep], + }); - const keep = await requiredAsyncAlert({ - title: t("loop.title", {loop: gameState.loop}), - content: [ - t("loop.instructions"), - comboText, - ...userPerks - .filter(u => u.id !== 'instant_upgrade') - .map((u) => { - return { - text: - u.name + - t("level_up.upgrade_perk_to_level", { - level: gameState.perks[u.id] + 1, - }), - icon: u.icon, - value: u.id, - help: u.help(gameState.perks[u.id] + 1), - }; - }), - ], - }); - - userPerks.forEach(u => { - if (u.id !== keep) { - gameState.bannedPerks[u.id] = 1 - } - }) - - Object.assign(gameState.perks, makeEmptyPerksMap(upgrades), { - [keep]: gameState.perks[keep], - }); - - await setLevel(gameState, 0); + await setLevel(gameState, 0); } export async function setLevel(gameState: GameState, l: number) { - // Here to alleviate double upgrades issues - if (gameState.upgradesOfferedFor >= l) { - debugger; - return console.warn("Extra upgrade request ignored "); - } - gameState.upgradesOfferedFor = l; - pause(false); - stopRecording(); - if (l > 0) { - await openUpgradesPicker(gameState); - } - gameState.currentLevel = l; + // Here to alleviate double upgrades issues + if (gameState.upgradesOfferedFor >= l) { + debugger; + return console.warn("Extra upgrade request ignored "); + } + gameState.upgradesOfferedFor = l; + pause(false); + stopRecording(); + if (l > 0) { + await openUpgradesPicker(gameState); + } + gameState.currentLevel = l; - gameState.level = gameState.runLevels[l]; + gameState.level = gameState.runLevels[l]; - gameState.levelTime = 0; - gameState.winAt = 0; - gameState.levelWallBounces = 0; - gameState.autoCleanUses = 0; - gameState.lastTickDown = gameState.levelTime; - gameState.levelStartScore = gameState.score; - gameState.levelSpawnedCoins = 0; - gameState.levelLostCoins = 0; - gameState.levelMisses = 0; - gameState.runStatistics.levelsPlayed++; + gameState.levelTime = 0; + gameState.winAt = 0; + gameState.levelWallBounces = 0; + gameState.autoCleanUses = 0; + gameState.lastTickDown = gameState.levelTime; + gameState.levelStartScore = gameState.score; + gameState.levelSpawnedCoins = 0; + gameState.levelLostCoins = 0; + gameState.levelMisses = 0; + gameState.runStatistics.levelsPlayed++; - // Reset combo silently - const finalCombo = gameState.combo; - gameState.combo = baseCombo(gameState); - if (gameState.perks.shunt) { - gameState.combo += Math.round( - Math.max( - 0, - (finalCombo - gameState.combo) * comboKeepingRate(gameState.perks.shunt), - ), - ); - } - gameState.combo += gameState.perks.hot_start * 15; + // Reset combo silently + const finalCombo = gameState.combo; + gameState.combo = baseCombo(gameState); + if (gameState.perks.shunt) { + gameState.combo += Math.round( + Math.max( + 0, + (finalCombo - gameState.combo) * + comboKeepingRate(gameState.perks.shunt), + ), + ); + } + gameState.combo += gameState.perks.hot_start * 15; - const lvl = currentLevelInfo(gameState); - if (lvl.size !== gameState.gridSize) { - gameState.gridSize = lvl.size; - fitSize(); - } - gameState.levelLostCoins += empty(gameState.coins); - empty(gameState.particles); - empty(gameState.lights); - empty(gameState.texts); - empty(gameState.respawns); - gameState.bricks = []; - for (let i = 0; i < lvl.size * lvl.size; i++) { - setBrick(gameState, i, lvl.bricks[i]); - } + const lvl = currentLevelInfo(gameState); + if (lvl.size !== gameState.gridSize) { + gameState.gridSize = lvl.size; + fitSize(); + } + gameState.levelLostCoins += empty(gameState.coins); + empty(gameState.particles); + empty(gameState.lights); + empty(gameState.texts); + empty(gameState.respawns); + gameState.bricks = []; + for (let i = 0; i < lvl.size * lvl.size; i++) { + setBrick(gameState, i, lvl.bricks[i]); + } - // Balls color will depend on most common brick color sometimes - resetBalls(gameState); - gameState.needsRender = true; - // This caused problems with accented characters like the ô of côte d'ivoire for odd reasons - // background.src = 'data:image/svg+xml;base64,' + btoa(lvl.svg) - background.src = "data:image/svg+xml;UTF8," + lvl.svg; + // Balls color will depend on most common brick color sometimes + resetBalls(gameState); + gameState.needsRender = true; + // This caused problems with accented characters like the ô of côte d'ivoire for odd reasons + // background.src = 'data:image/svg+xml;base64,' + btoa(lvl.svg) + background.src = "data:image/svg+xml;UTF8," + lvl.svg; } function setBrick(gameState: GameState, index: number, color: string) { - gameState.bricks[index] = color || ""; - gameState.brickHP[index] = - (color === "black" && 1) || - (color && - 1 + gameState.perks.sturdy_bricks) || - 0; + gameState.bricks[index] = color || ""; + gameState.brickHP[index] = + (color === "black" && 1) || + (color && 1 + gameState.perks.sturdy_bricks) || + 0; } export function rainbowColor(): colorString { - return `hsl(${(Math.round(gameState.levelTime / 4) * 2) % 360},100%,70%)`; + return `hsl(${(Math.round(gameState.levelTime / 4) * 2) % 360},100%,70%)`; } export function repulse( - gameState: GameState, - a: Ball, - b: BallLike, - power: number, - impactsBToo: boolean, + gameState: GameState, + a: Ball, + b: BallLike, + power: number, + impactsBToo: boolean, ) { - const distance = distanceBetween(a, b); - // Ensure we don't get soft locked - const max = gameState.gameZoneWidth / 4; - if (distance > max) return; - // Unit vector - const dx = (a.x - b.x) / distance; - const dy = (a.y - b.y) / distance; - const fact = - (((-power * (max - distance)) / (max * 1.2) / 3) * - Math.min(500, gameState.levelTime)) / - 500; - if ( - impactsBToo && - typeof b.vx !== "undefined" && - typeof b.vy !== "undefined" - ) { - b.vx += dx * fact; - b.vy += dy * fact; - } - a.vx -= dx * fact; - a.vy -= dy * fact; + const distance = distanceBetween(a, b); + // Ensure we don't get soft locked + const max = gameState.gameZoneWidth / 4; + if (distance > max) return; + // Unit vector + const dx = (a.x - b.x) / distance; + const dy = (a.y - b.y) / distance; + const fact = + (((-power * (max - distance)) / (max * 1.2) / 3) * + Math.min(500, gameState.levelTime)) / + 500; + if ( + impactsBToo && + typeof b.vx !== "undefined" && + typeof b.vy !== "undefined" + ) { + b.vx += dx * fact; + b.vy += dy * fact; + } + a.vx -= dx * fact; + a.vy -= dy * fact; - const speed = 10; - const rand = 2; + const speed = 10; + const rand = 2; + makeParticle( + gameState, + a.x, + a.y, + -dx * speed + a.vx + (Math.random() - 0.5) * rand, + -dy * speed + a.vy + (Math.random() - 0.5) * rand, + rainbowColor(), + true, + gameState.coinSize / 2, + 100, + ); + if ( + impactsBToo && + typeof b.vx !== "undefined" && + typeof b.vy !== "undefined" + ) { makeParticle( - gameState, - a.x, - a.y, - -dx * speed + a.vx + (Math.random() - 0.5) * rand, - -dy * speed + a.vy + (Math.random() - 0.5) * rand, - rainbowColor(), - true, - gameState.coinSize / 2, - 100, + gameState, + b.x, + b.y, + dx * speed + b.vx + (Math.random() - 0.5) * rand, + dy * speed + b.vy + (Math.random() - 0.5) * rand, + rainbowColor(), + true, + gameState.coinSize / 2, + 100, ); - if ( - impactsBToo && - typeof b.vx !== "undefined" && - typeof b.vy !== "undefined" - ) { - makeParticle( - gameState, - b.x, - b.y, - dx * speed + b.vx + (Math.random() - 0.5) * rand, - dy * speed + b.vy + (Math.random() - 0.5) * rand, - rainbowColor(), - true, - gameState.coinSize / 2, - 100, - ); - } + } } export function attract(gameState: GameState, a: Ball, b: Ball, power: number) { - const distance = distanceBetween(a, b); - // Ensure we don't get soft locked - const min = (gameState.gameZoneWidth * 3) / 4; - if (distance < min) return; - // Unit vector - const dx = (a.x - b.x) / distance; - const dy = (a.y - b.y) / distance; + const distance = distanceBetween(a, b); + // Ensure we don't get soft locked + const min = (gameState.gameZoneWidth * 3) / 4; + if (distance < min) return; + // Unit vector + const dx = (a.x - b.x) / distance; + const dy = (a.y - b.y) / distance; - const fact = - (((power * (distance - min)) / min) * Math.min(500, gameState.levelTime)) / - 500; - b.vx += dx * fact; - b.vy += dy * fact; - a.vx -= dx * fact; - a.vy -= dy * fact; + const fact = + (((power * (distance - min)) / min) * Math.min(500, gameState.levelTime)) / + 500; + b.vx += dx * fact; + b.vy += dy * fact; + a.vx -= dx * fact; + a.vy -= dy * fact; - const speed = 10; - const rand = 2; + const speed = 10; + const rand = 2; - makeParticle( - gameState, - a.x, - a.y, - dx * speed + a.vx + (Math.random() - 0.5) * rand, - dy * speed + a.vy + (Math.random() - 0.5) * rand, - rainbowColor(), - true, - gameState.coinSize / 2, - 100, - ); - makeParticle( - gameState, - b.x, - b.y, - -dx * speed + b.vx + (Math.random() - 0.5) * rand, - -dy * speed + b.vy + (Math.random() - 0.5) * rand, - rainbowColor(), - true, - gameState.coinSize / 2, - 100, - ); + makeParticle( + gameState, + a.x, + a.y, + dx * speed + a.vx + (Math.random() - 0.5) * rand, + dy * speed + a.vy + (Math.random() - 0.5) * rand, + rainbowColor(), + true, + gameState.coinSize / 2, + 100, + ); + makeParticle( + gameState, + b.x, + b.y, + -dx * speed + b.vx + (Math.random() - 0.5) * rand, + -dy * speed + b.vy + (Math.random() - 0.5) * rand, + rainbowColor(), + true, + gameState.coinSize / 2, + 100, + ); } export function coinBrickHitCheck(gameState: GameState, coin: Coin) { - // Make ball/coin bonce, and return bricks that were hit - const radius = coin.size / 2; - const {x, y, previousX, previousY} = coin; + // Make ball/coin bonce, and return bricks that were hit + const radius = coin.size / 2; + const { x, y, previousX, previousY } = coin; - const vhit = hitsSomething(previousX, y, radius); - const hhit = hitsSomething(x, previousY, radius); - const chit = - (typeof vhit == "undefined" && - typeof hhit == "undefined" && - hitsSomething(x, y, radius)) || - undefined; + const vhit = hitsSomething(previousX, y, radius); + const hhit = hitsSomething(x, previousY, radius); + const chit = + (typeof vhit == "undefined" && + typeof hhit == "undefined" && + hitsSomething(x, y, radius)) || + undefined; - if (gameState.perks.ghost_coins) { - // slow down - if (typeof (vhit ?? hhit ?? chit) !== "undefined") { - - coin.vy *= 1 - 0.2 / gameState.perks.ghost_coins; - coin.vx *= 1 - 0.2 / gameState.perks.ghost_coins; - } - - } else { - if (typeof vhit !== "undefined" || typeof chit !== "undefined") { - coin.y = coin.previousY; - coin.vy *= -1; - - // Roll on corners - const leftHit = gameState.bricks[brickIndex(x - radius, y + radius)]; - const rightHit = gameState.bricks[brickIndex(x + radius, y + radius)]; - - if (leftHit && !rightHit) { - coin.vx += 1; - coin.sa -= 1; - } - if (!leftHit && rightHit) { - coin.vx -= 1; - coin.sa += 1; - } - } - if (typeof hhit !== "undefined" || typeof chit !== "undefined") { - coin.x = coin.previousX; - coin.vx *= -1; - } + if (gameState.perks.ghost_coins) { + // slow down + if (typeof (vhit ?? hhit ?? chit) !== "undefined") { + coin.vy *= 1 - 0.2 / gameState.perks.ghost_coins; + coin.vx *= 1 - 0.2 / gameState.perks.ghost_coins; } - return vhit ?? hhit ?? chit; + } else { + if (typeof vhit !== "undefined" || typeof chit !== "undefined") { + coin.y = coin.previousY; + coin.vy *= -1; + + // Roll on corners + const leftHit = gameState.bricks[brickIndex(x - radius, y + radius)]; + const rightHit = gameState.bricks[brickIndex(x + radius, y + radius)]; + + if (leftHit && !rightHit) { + coin.vx += 1; + coin.sa -= 1; + } + if (!leftHit && rightHit) { + coin.vx -= 1; + coin.sa += 1; + } + } + if (typeof hhit !== "undefined" || typeof chit !== "undefined") { + coin.x = coin.previousX; + coin.vx *= -1; + } + } + return vhit ?? hhit ?? chit; } export function bordersHitCheck( - gameState: GameState, - coin: Coin | Ball, - radius: number, - delta: number, + gameState: GameState, + coin: Coin | Ball, + radius: number, + delta: number, ) { - if (coin.destroyed) return; - coin.previousX = coin.x; - coin.previousY = coin.y; - coin.x += coin.vx * delta; - coin.y += coin.vy * delta; + if (coin.destroyed) return; + coin.previousX = coin.x; + coin.previousY = coin.y; + coin.x += coin.vx * delta; + coin.y += coin.vy * delta; - if (gameState.perks.wind) { - coin.vx += - ((gameState.puckPosition - - (gameState.offsetX + gameState.gameZoneWidth / 2)) / - gameState.gameZoneWidth) * - gameState.perks.wind * - 0.5; - } + if (gameState.perks.wind) { + coin.vx += + ((gameState.puckPosition - + (gameState.offsetX + gameState.gameZoneWidth / 2)) / + gameState.gameZoneWidth) * + gameState.perks.wind * + 0.5; + } - let vhit = 0, - hhit = 0; + let vhit = 0, + hhit = 0; - if ( - coin.x < gameState.offsetXRoundedDown + radius && - !gameState.perks.unbounded - ) { - coin.x = - gameState.offsetXRoundedDown + - radius + - (gameState.offsetXRoundedDown + radius - coin.x); - coin.vx *= -1; - hhit = 1; - } - if (coin.y < radius && gameState.perks.unbounded < 2) { - coin.y = radius + (radius - coin.y); - coin.vy *= -1; - vhit = 1; - } - if ( - coin.x > gameState.canvasWidth - gameState.offsetXRoundedDown - radius && - !gameState.perks.unbounded - ) { - coin.x = - gameState.canvasWidth - - gameState.offsetXRoundedDown - - radius - - (coin.x - - (gameState.canvasWidth - gameState.offsetXRoundedDown - radius)); - coin.vx *= -1; - hhit = 1; - } + if ( + coin.x < gameState.offsetXRoundedDown + radius && + !gameState.perks.unbounded + ) { + coin.x = + gameState.offsetXRoundedDown + + radius + + (gameState.offsetXRoundedDown + radius - coin.x); + coin.vx *= -1; + hhit = 1; + } + if (coin.y < radius && gameState.perks.unbounded < 2) { + coin.y = radius + (radius - coin.y); + coin.vy *= -1; + vhit = 1; + } + if ( + coin.x > gameState.canvasWidth - gameState.offsetXRoundedDown - radius && + !gameState.perks.unbounded + ) { + coin.x = + gameState.canvasWidth - + gameState.offsetXRoundedDown - + radius - + (coin.x - + (gameState.canvasWidth - gameState.offsetXRoundedDown - radius)); + coin.vx *= -1; + hhit = 1; + } - return hhit + vhit * 2; + return hhit + vhit * 2; } export function gameStateTick( - gameState: GameState, - // How many frames to compute at once, can go above 1 to compensate lag - frames = 1, + gameState: GameState, + // How many frames to compute at once, can go above 1 to compensate lag + frames = 1, ) { - gameState.runStatistics.max_combo = Math.max( - gameState.runStatistics.max_combo, - gameState.combo, + gameState.runStatistics.max_combo = Math.max( + gameState.runStatistics.max_combo, + gameState.combo, + ); + + gameState.balls = gameState.balls.filter((ball) => !ball.destroyed); + + const remainingBricks = gameState.bricks.filter( + (b) => b && b !== "black", + ).length; + + if ( + gameState.levelTime > gameState.lastTickDown + 1000 && + gameState.perks.hot_start + ) { + gameState.lastTickDown = gameState.levelTime; + decreaseCombo( + gameState, + gameState.perks.hot_start, + gameState.puckPosition, + gameState.gameZoneHeight - 2 * gameState.puckHeight, ); + } - gameState.balls = gameState.balls.filter((ball) => !ball.destroyed); + if ( + remainingBricks <= gameState.perks.skip_last && + !gameState.autoCleanUses + ) { + gameState.bricks.forEach((type, index) => { + if (type) { + explodeBrick(gameState, index, gameState.balls[0], true); + } + }); + gameState.autoCleanUses++; + } - const remainingBricks = gameState.bricks.filter( - (b) => b && b !== "black", - ).length; + const hasPendingBricks = liveCount(gameState.respawns); - if ( - gameState.levelTime > gameState.lastTickDown + 1000 && - gameState.perks.hot_start - ) { - gameState.lastTickDown = gameState.levelTime; - decreaseCombo( - gameState, - gameState.perks.hot_start, - gameState.puckPosition, - gameState.gameZoneHeight - 2 * gameState.puckHeight, - ); + if (gameState.running && !remainingBricks && !hasPendingBricks) { + if (!gameState.winAt) { + gameState.winAt = gameState.levelTime + 5000; } + } else { + gameState.winAt = 0; + } - if ( - remainingBricks <= gameState.perks.skip_last && - !gameState.autoCleanUses - ) { - gameState.bricks.forEach((type, index) => { - if (type) { - explodeBrick(gameState, index, gameState.balls[0], true); - } - }); - gameState.autoCleanUses++; - } - - const hasPendingBricks = liveCount(gameState.respawns) - - if (gameState.running && !remainingBricks && !hasPendingBricks) { - if (!gameState.winAt) { - gameState.winAt = gameState.levelTime + 5000; - } + if ( + (gameState.running && + // Delayed win when coins are still flying + gameState.winAt && + gameState.levelTime > gameState.winAt) || + // instant win condition + (gameState.levelTime && !remainingBricks && !liveCount(gameState.coins)) + ) { + if (gameState.currentLevel + 1 < max_levels(gameState)) { + setLevel(gameState, gameState.currentLevel + 1); } else { - gameState.winAt = 0; + if (isPremium()) { + gotoNextLoop(gameState); + } else { + gameOver( + t("gameOver.win.title"), + t("gameOver.win.summary", { score: gameState.score }), + ); + } } + } else if (gameState.running || gameState.levelTime) { + const coinRadius = Math.round(gameState.coinSize / 2); - if ( - (gameState.running && - // Delayed win when coins are still flying - gameState.winAt && - gameState.levelTime > gameState.winAt) || - // instant win condition - (gameState.levelTime && !remainingBricks && !liveCount(gameState.coins)) - ) { - if (gameState.currentLevel + 1 < max_levels(gameState)) { - setLevel(gameState, gameState.currentLevel + 1); - } else { - if (isPremium()) { - gotoNextLoop(gameState); - } else { - gameOver( - t("gameOver.win.title"), - t("gameOver.win.summary", {score: gameState.score}), - ); - } - } - } else if (gameState.running || gameState.levelTime) { - const coinRadius = Math.round(gameState.coinSize / 2); + forEachLiveOne(gameState.coins, (coin, coinIndex) => { + if (gameState.perks.coin_magnet) { + const strength = + (100 / + (100 + + Math.pow(coin.y - gameState.gameZoneHeight, 2) + + Math.pow(coin.x - gameState.puckPosition, 2))) * + gameState.perks.coin_magnet; - forEachLiveOne(gameState.coins, (coin, coinIndex) => { - if (gameState.perks.coin_magnet) { - const strength = - (100 / - (100 + - Math.pow(coin.y - gameState.gameZoneHeight, 2) + - Math.pow(coin.x - gameState.puckPosition, 2))) * - gameState.perks.coin_magnet; + const attractionX = + frames * (gameState.puckPosition - coin.x) * strength; - const attractionX = - frames * (gameState.puckPosition - coin.x) * strength; + coin.vx += attractionX; + coin.vy += + (frames * (gameState.gameZoneHeight - coin.y) * strength) / 2; + coin.sa -= attractionX / 10; + } - coin.vx += attractionX; - coin.vy += - (frames * (gameState.gameZoneHeight - coin.y) * strength) / 2; - coin.sa -= attractionX / 10; - } - - if (gameState.perks.ball_attracts_coins) { - gameState.balls.forEach((ball) => { - const d2 = distance2(ball, coin); - coin.vx += - ((ball.x - coin.x) / d2) * 50 * gameState.perks.ball_attracts_coins; - coin.vy += - ((ball.y - coin.y) / d2) * 50 * gameState.perks.ball_attracts_coins; - }); - } - - const ratio = - 1 - - (gameState.perks.viscosity * - 0.03 + - 0.005) * - frames / (1 + gameState.perks.etherealcoins); - - coin.vy *= ratio; - coin.vx *= ratio; - if (coin.vx > 7 * gameState.baseSpeed) coin.vx = 7 * gameState.baseSpeed; - if (coin.vx < -7 * gameState.baseSpeed) - coin.vx = -7 * gameState.baseSpeed; - if (coin.vy > 7 * gameState.baseSpeed) coin.vy = 7 * gameState.baseSpeed; - if (coin.vy < -7 * gameState.baseSpeed) - coin.vy = -7 * gameState.baseSpeed; - coin.a += coin.sa; - - // Gravity - if (!gameState.perks.etherealcoins) { - const flip = - gameState.perks.helium > 0 && - Math.abs(coin.x - gameState.puckPosition) * 2 > - gameState.puckWidth + coin.size; - coin.vy += frames * coin.weight * 0.8 * (flip ? -gameState.perks.helium : 1); - if (flip && !isOptionOn("basic") && Math.random() < 0.1) { - makeParticle( - gameState, - coin.x, - coin.y, - 0, - gameState.baseSpeed, - coin.color, - true, - 5, - 250, - ); - } - } - - - const speed = (Math.abs(coin.vx) + Math.abs(coin.vy)) * 10; - const hitBorder = bordersHitCheck(gameState, coin, coin.size / 2, frames); - - if ( - coin.y > gameState.gameZoneHeight - coinRadius - gameState.puckHeight && - coin.y < gameState.gameZoneHeight + gameState.puckHeight + coin.vy && - Math.abs(coin.x - gameState.puckPosition) < - coinRadius + - gameState.puckWidth / 2 + - // a bit of margin to be nice , negative in case it's a negative coin - gameState.puckHeight * (coin.points ? 1 : -1) - ) { - - addToScore(gameState, coin); - - destroy(gameState.coins, coinIndex); - } else if (coin.y > gameState.canvasHeight + coinRadius) { - gameState.levelLostCoins+=coin.points - destroy(gameState.coins, coinIndex); - if (gameState.perks.compound_interest) { - resetCombo(gameState, coin.x, coin.y); - } - } else if ( - gameState.perks.unbounded && - (coin.x < -gameState.gameZoneWidth / 2 || - coin.x > gameState.canvasWidth + gameState.gameZoneWidth / 2 - || coin.y < -gameState.gameZoneWidth - ) - ) { - // Out of bound on sides - gameState.levelLostCoins+=coin.points - destroy(gameState.coins, coinIndex); - } - - const hitBrick = coinBrickHitCheck(gameState, coin); - if (gameState.perks.metamorphosis && typeof hitBrick !== "undefined") { - if ( - gameState.bricks[hitBrick] && - coin.color !== gameState.bricks[hitBrick] && - gameState.bricks[hitBrick] !== "black" && - coin.metamorphosisPoints - ) { - // Not using setbrick because we don't want to reset HP - gameState.bricks[hitBrick] = coin.color; - coin.metamorphosisPoints--; - - schedulGameSound(gameState, "colorChange", coin.x, 0.3); - } - } - - if ( - (!gameState.perks.ghost_coins && typeof hitBrick !== "undefined") || - hitBorder - ) { - coin.vx *= 0.8; - coin.vy *= 0.8; - coin.sa *= 0.9; - if (speed > 20 && !coin.collidedLastFrame) { - schedulGameSound(gameState, "coinBounce", coin.x, 0.2); - } - coin.collidedLastFrame = true; - - if (Math.abs(coin.vy) < 3) { - coin.vy = 0; - } - } else { - coin.collidedLastFrame = false; - } + if (gameState.perks.ball_attracts_coins) { + gameState.balls.forEach((ball) => { + const d2 = distance2(ball, coin); + coin.vx += + ((ball.x - coin.x) / d2) * 50 * gameState.perks.ball_attracts_coins; + coin.vy += + ((ball.y - coin.y) / d2) * 50 * gameState.perks.ball_attracts_coins; }); + } - gameState.balls.forEach((ball) => ballTick(gameState, ball, frames)); + const ratio = + 1 - + ((gameState.perks.viscosity * 0.03 + 0.005) * frames) / + (1 + gameState.perks.etherealcoins); - if (gameState.perks.shocks) { - gameState.balls.forEach((a, ai) => - gameState.balls.forEach((b, bi) => { - if ( - ai < bi && - !a.destroyed && - !b.destroyed && - distance2(a, b) < gameState.ballSize * gameState.ballSize - ) { - let tempVx = a.vx; - let tempVy = a.vy; - a.vx = b.vx; - a.vy = b.vy; - b.vx = tempVx; - b.vy = tempVy; + coin.vy *= ratio; + coin.vx *= ratio; + if (coin.vx > 7 * gameState.baseSpeed) coin.vx = 7 * gameState.baseSpeed; + if (coin.vx < -7 * gameState.baseSpeed) + coin.vx = -7 * gameState.baseSpeed; + if (coin.vy > 7 * gameState.baseSpeed) coin.vy = 7 * gameState.baseSpeed; + if (coin.vy < -7 * gameState.baseSpeed) + coin.vy = -7 * gameState.baseSpeed; + coin.a += coin.sa; - let x = (a.x + b.x) / 2; - let y = (a.y + b.y) / 2; - const limit = gameState.baseSpeed; - a.vx += - clamp(a.x - x, -limit, limit) + - ((Math.random() - 0.5) * limit) / 3; - a.vy += - clamp(a.y - y, -limit, limit) + - ((Math.random() - 0.5) * limit) / 3; - b.vx += - clamp(b.x - x, -limit, limit) + - ((Math.random() - 0.5) * limit) / 3; - b.vy += - clamp(b.y - y, -limit, limit) + - ((Math.random() - 0.5) * limit) / 3; - - let index = brickIndex(x, y); - explosionAt(gameState, index, x, y, a, Math.max(0, gameState.perks.shocks - 1)); - } - }), - ); + // Gravity + if (!gameState.perks.etherealcoins) { + const flip = + gameState.perks.helium > 0 && + Math.abs(coin.x - gameState.puckPosition) * 2 > + gameState.puckWidth + coin.size; + coin.vy += + frames * coin.weight * 0.8 * (flip ? -gameState.perks.helium : 1); + if (flip && !isOptionOn("basic") && Math.random() < 0.1) { + makeParticle( + gameState, + coin.x, + coin.y, + 0, + gameState.baseSpeed, + coin.color, + true, + 5, + 250, + ); } + } - if (gameState.perks.wind) { - const windD = - ((gameState.puckPosition - - (gameState.offsetX + gameState.gameZoneWidth / 2)) / - gameState.gameZoneWidth) * - 2 * - gameState.perks.wind; - for (let i = 0; i < gameState.perks.wind; i++) { - if (Math.random() * Math.abs(windD) > 0.5) { - makeParticle( - gameState, - gameState.offsetXRoundedDown + - Math.random() * gameState.gameZoneWidthRoundedUp, - Math.random() * gameState.gameZoneHeight, - windD * 8, - 0, - rainbowColor(), - true, - gameState.coinSize / 2, - 150, - ); - } - } - } - forEachLiveOne(gameState.particles, (flash, index) => { - flash.x += flash.vx * frames; - flash.y += flash.vy * frames; - if (!flash.ethereal) { - flash.vy += 0.5; - if (hasBrick(brickIndex(flash.x, flash.y))) { - destroy(gameState.particles, index); - } - } - }); - } + const speed = (Math.abs(coin.vx) + Math.abs(coin.vy)) * 10; + const hitBorder = bordersHitCheck(gameState, coin, coin.size / 2, frames); - if ( - gameState.combo > baseCombo(gameState) && - !isOptionOn("basic") && - (gameState.combo - baseCombo(gameState)) * Math.random() > 5 - ) { - // The red should still be visible on a white bg - - if (gameState.perks.top_is_lava) { - makeParticle( - gameState, - gameState.offsetXRoundedDown + - Math.random() * gameState.gameZoneWidthRoundedUp, - 0, - (Math.random() - 0.5) * 10, - 5, - "red", - true, - gameState.coinSize / 2, - 100 * (Math.random() + 1), - ); - } - - if (gameState.perks.left_is_lava) { - makeParticle( - gameState, - gameState.offsetXRoundedDown, - Math.random() * gameState.gameZoneHeight, - 5, - (Math.random() - 0.5) * 10, - "red", - true, - gameState.coinSize / 2, - 100 * (Math.random() + 1), - ); - } - - if (gameState.perks.right_is_lava) { - makeParticle( - gameState, - gameState.offsetXRoundedDown + gameState.gameZoneWidthRoundedUp, - Math.random() * gameState.gameZoneHeight, - -5, - (Math.random() - 0.5) * 10, - "red", - true, - gameState.coinSize / 2, - 100 * (Math.random() + 1), - ); - } + if ( + coin.y > gameState.gameZoneHeight - coinRadius - gameState.puckHeight && + coin.y < gameState.gameZoneHeight + gameState.puckHeight + coin.vy && + Math.abs(coin.x - gameState.puckPosition) < + coinRadius + + gameState.puckWidth / 2 + + // a bit of margin to be nice , negative in case it's a negative coin + gameState.puckHeight * (coin.points ? 1 : -1) + ) { + addToScore(gameState, coin); + destroy(gameState.coins, coinIndex); + } else if (coin.y > gameState.canvasHeight + coinRadius) { + gameState.levelLostCoins += coin.points; + destroy(gameState.coins, coinIndex); if (gameState.perks.compound_interest) { - let x = gameState.puckPosition, - attemps = 0; - do { - x = - gameState.offsetXRoundedDown + - gameState.gameZoneWidthRoundedUp * Math.random(); - attemps++; - } while ( - Math.abs(x - gameState.puckPosition) < gameState.puckWidth / 2 && - attemps < 10 - ); + resetCombo(gameState, coin.x, coin.y); + } + } else if ( + gameState.perks.unbounded && + (coin.x < -gameState.gameZoneWidth / 2 || + coin.x > gameState.canvasWidth + gameState.gameZoneWidth / 2 || + coin.y < -gameState.gameZoneWidth) + ) { + // Out of bound on sides + gameState.levelLostCoins += coin.points; + destroy(gameState.coins, coinIndex); + } - makeParticle( - gameState, - x, - gameState.gameZoneHeight, - (Math.random() - 0.5) * 10, - -5, - "red", - true, - gameState.coinSize / 2, - 100 * (Math.random() + 1), - ); + const hitBrick = coinBrickHitCheck(gameState, coin); + if (gameState.perks.metamorphosis && typeof hitBrick !== "undefined") { + if ( + gameState.bricks[hitBrick] && + coin.color !== gameState.bricks[hitBrick] && + gameState.bricks[hitBrick] !== "black" && + coin.metamorphosisPoints + ) { + // Not using setbrick because we don't want to reset HP + gameState.bricks[hitBrick] = coin.color; + coin.metamorphosisPoints--; + + schedulGameSound(gameState, "colorChange", coin.x, 0.3); } - if (gameState.perks.streak_shots) { - const pos = 0.5 - Math.random(); - makeParticle( - gameState, - gameState.puckPosition + gameState.puckWidth * pos, - gameState.gameZoneHeight - gameState.puckHeight, - pos * 10, - -5, - "red", - true, - gameState.coinSize / 2, - 100 * (Math.random() + 1), - ); + } + + if ( + (!gameState.perks.ghost_coins && typeof hitBrick !== "undefined") || + hitBorder + ) { + coin.vx *= 0.8; + coin.vy *= 0.8; + coin.sa *= 0.9; + if (speed > 20 && !coin.collidedLastFrame) { + schedulGameSound(gameState, "coinBounce", coin.x, 0.2); } + coin.collidedLastFrame = true; + + if (Math.abs(coin.vy) < 3) { + coin.vy = 0; + } + } else { + coin.collidedLastFrame = false; + } + }); + + gameState.balls.forEach((ball) => ballTick(gameState, ball, frames)); + + if (gameState.perks.shocks) { + gameState.balls.forEach((a, ai) => + gameState.balls.forEach((b, bi) => { + if ( + ai < bi && + !a.destroyed && + !b.destroyed && + distance2(a, b) < gameState.ballSize * gameState.ballSize + ) { + let tempVx = a.vx; + let tempVy = a.vy; + a.vx = b.vx; + a.vy = b.vy; + b.vx = tempVx; + b.vy = tempVy; + + let x = (a.x + b.x) / 2; + let y = (a.y + b.y) / 2; + const limit = gameState.baseSpeed; + a.vx += + clamp(a.x - x, -limit, limit) + + ((Math.random() - 0.5) * limit) / 3; + a.vy += + clamp(a.y - y, -limit, limit) + + ((Math.random() - 0.5) * limit) / 3; + b.vx += + clamp(b.x - x, -limit, limit) + + ((Math.random() - 0.5) * limit) / 3; + b.vy += + clamp(b.y - y, -limit, limit) + + ((Math.random() - 0.5) * limit) / 3; + + let index = brickIndex(x, y); + explosionAt( + gameState, + index, + x, + y, + a, + Math.max(0, gameState.perks.shocks - 1), + ); + } + }), + ); } - // Respawn what's needed, show particles - forEachLiveOne(gameState.respawns, (r, ri) => { - if (gameState.bricks[r.index]) { - destroy(gameState.respawns, ri) - } else if (gameState.levelTime > r.time) { - setBrick(gameState, r.index, r.color) - destroy(gameState.respawns, ri) - } else if (!isOptionOn("basic")) { - const {index, color} = r; - const vertical = Math.random() > 0.5; - const dx = Math.random() > 0.5 ? 1 : -1; - const dy = Math.random() > 0.5 ? 1 : -1; - - makeParticle( - gameState, - brickCenterX(gameState, index) + (dx * gameState.brickWidth) / 2, - brickCenterY(gameState, index) + (dy * gameState.brickWidth) / 2, - vertical ? 0 : -dx * gameState.baseSpeed, - vertical ? -dy * gameState.baseSpeed : 0, - color, - true, - gameState.coinSize / 2, - 250, - ); + if (gameState.perks.wind) { + const windD = + ((gameState.puckPosition - + (gameState.offsetX + gameState.gameZoneWidth / 2)) / + gameState.gameZoneWidth) * + 2 * + gameState.perks.wind; + for (let i = 0; i < gameState.perks.wind; i++) { + if (Math.random() * Math.abs(windD) > 0.5) { + makeParticle( + gameState, + gameState.offsetXRoundedDown + + Math.random() * gameState.gameZoneWidthRoundedUp, + Math.random() * gameState.gameZoneHeight, + windD * 8, + 0, + rainbowColor(), + true, + gameState.coinSize / 2, + 150, + ); } - }) - - - forEachLiveOne(gameState.particles, (p, pi) => { - if (gameState.levelTime > p.time + p.duration) { - destroy(gameState.particles, pi); - } - }); - forEachLiveOne(gameState.texts, (p, pi) => { - if (gameState.levelTime > p.time + p.duration) { - destroy(gameState.texts, pi); - } - }); - forEachLiveOne(gameState.lights, (p, pi) => { - if (gameState.levelTime > p.time + p.duration) { - destroy(gameState.lights, pi); + } + } + forEachLiveOne(gameState.particles, (flash, index) => { + flash.x += flash.vx * frames; + flash.y += flash.vy * frames; + if (!flash.ethereal) { + flash.vy += 0.5; + if (hasBrick(brickIndex(flash.x, flash.y))) { + destroy(gameState.particles, index); } + } }); + } + + if ( + gameState.combo > baseCombo(gameState) && + !isOptionOn("basic") && + (gameState.combo - baseCombo(gameState)) * Math.random() > 5 + ) { + // The red should still be visible on a white bg + + if (gameState.perks.top_is_lava) { + makeParticle( + gameState, + gameState.offsetXRoundedDown + + Math.random() * gameState.gameZoneWidthRoundedUp, + 0, + (Math.random() - 0.5) * 10, + 5, + "red", + true, + gameState.coinSize / 2, + 100 * (Math.random() + 1), + ); + } + + if (gameState.perks.left_is_lava) { + makeParticle( + gameState, + gameState.offsetXRoundedDown, + Math.random() * gameState.gameZoneHeight, + 5, + (Math.random() - 0.5) * 10, + "red", + true, + gameState.coinSize / 2, + 100 * (Math.random() + 1), + ); + } + + if (gameState.perks.right_is_lava) { + makeParticle( + gameState, + gameState.offsetXRoundedDown + gameState.gameZoneWidthRoundedUp, + Math.random() * gameState.gameZoneHeight, + -5, + (Math.random() - 0.5) * 10, + "red", + true, + gameState.coinSize / 2, + 100 * (Math.random() + 1), + ); + } + + if (gameState.perks.compound_interest) { + let x = gameState.puckPosition, + attemps = 0; + do { + x = + gameState.offsetXRoundedDown + + gameState.gameZoneWidthRoundedUp * Math.random(); + attemps++; + } while ( + Math.abs(x - gameState.puckPosition) < gameState.puckWidth / 2 && + attemps < 10 + ); + + makeParticle( + gameState, + x, + gameState.gameZoneHeight, + (Math.random() - 0.5) * 10, + -5, + "red", + true, + gameState.coinSize / 2, + 100 * (Math.random() + 1), + ); + } + if (gameState.perks.streak_shots) { + const pos = 0.5 - Math.random(); + makeParticle( + gameState, + gameState.puckPosition + gameState.puckWidth * pos, + gameState.gameZoneHeight - gameState.puckHeight, + pos * 10, + -5, + "red", + true, + gameState.coinSize / 2, + 100 * (Math.random() + 1), + ); + } + } + + // Respawn what's needed, show particles + forEachLiveOne(gameState.respawns, (r, ri) => { + if (gameState.bricks[r.index]) { + destroy(gameState.respawns, ri); + } else if (gameState.levelTime > r.time) { + setBrick(gameState, r.index, r.color); + destroy(gameState.respawns, ri); + } else if (!isOptionOn("basic")) { + const { index, color } = r; + const vertical = Math.random() > 0.5; + const dx = Math.random() > 0.5 ? 1 : -1; + const dy = Math.random() > 0.5 ? 1 : -1; + + makeParticle( + gameState, + brickCenterX(gameState, index) + (dx * gameState.brickWidth) / 2, + brickCenterY(gameState, index) + (dy * gameState.brickWidth) / 2, + vertical ? 0 : -dx * gameState.baseSpeed, + vertical ? -dy * gameState.baseSpeed : 0, + color, + true, + gameState.coinSize / 2, + 250, + ); + } + }); + + forEachLiveOne(gameState.particles, (p, pi) => { + if (gameState.levelTime > p.time + p.duration) { + destroy(gameState.particles, pi); + } + }); + forEachLiveOne(gameState.texts, (p, pi) => { + if (gameState.levelTime > p.time + p.duration) { + destroy(gameState.texts, pi); + } + }); + forEachLiveOne(gameState.lights, (p, pi) => { + if (gameState.levelTime > p.time + p.duration) { + destroy(gameState.lights, pi); + } + }); } export function ballTick(gameState: GameState, ball: Ball, delta: number) { - ball.previousVX = ball.vx; - ball.previousVY = ball.vy; + ball.previousVX = ball.vx; + ball.previousVY = ball.vy; - let speedLimitDampener = - 1 + - gameState.perks.telekinesis + - gameState.perks.ball_repulse_ball + - gameState.perks.puck_repulse_ball + - gameState.perks.ball_attract_ball; + let speedLimitDampener = + 1 + + gameState.perks.telekinesis + + gameState.perks.ball_repulse_ball + + gameState.perks.puck_repulse_ball + + gameState.perks.ball_attract_ball; - if (isTelekinesisActive(gameState, ball)) { - speedLimitDampener += 3; - ball.vx += - ((gameState.puckPosition - ball.x) / 1000) * - delta * - gameState.perks.telekinesis - } - if (isYoyoActive(gameState, ball)) { - speedLimitDampener += 3; - ball.vx += - ((gameState.puckPosition - ball.x) / 1000) * - delta * - gameState.perks.yoyo - } - if ( - ball.vx * ball.vx + ball.vy * ball.vy < - gameState.baseSpeed * gameState.baseSpeed * 2 - ) { - ball.vx *= 1 + 0.02 / speedLimitDampener; - ball.vy *= 1 + 0.02 / speedLimitDampener; - } else { - ball.vx *= 1 - 0.02 / speedLimitDampener; - ball.vy *= 1 - 0.02 / speedLimitDampener; - } - // Ball could get stuck horizontally because of ball-ball interactions in repulse/attract - if (Math.abs(ball.vy) < 0.2 * gameState.baseSpeed) { - ball.vy += ((ball.vy > 0 ? 1 : -1) * 0.02) / speedLimitDampener; - } + if (isTelekinesisActive(gameState, ball)) { + speedLimitDampener += 3; + ball.vx += + ((gameState.puckPosition - ball.x) / 1000) * + delta * + gameState.perks.telekinesis; + } + if (isYoyoActive(gameState, ball)) { + speedLimitDampener += 3; + ball.vx += + ((gameState.puckPosition - ball.x) / 1000) * delta * gameState.perks.yoyo; + } + if ( + ball.vx * ball.vx + ball.vy * ball.vy < + gameState.baseSpeed * gameState.baseSpeed * 2 + ) { + ball.vx *= 1 + 0.02 / speedLimitDampener; + ball.vy *= 1 + 0.02 / speedLimitDampener; + } else { + ball.vx *= 1 - 0.02 / speedLimitDampener; + ball.vy *= 1 - 0.02 / speedLimitDampener; + } + // Ball could get stuck horizontally because of ball-ball interactions in repulse/attract + if (Math.abs(ball.vy) < 0.2 * gameState.baseSpeed) { + ball.vy += ((ball.vy > 0 ? 1 : -1) * 0.02) / speedLimitDampener; + } - if (gameState.perks.ball_repulse_ball) { - for (let b2 of gameState.balls) { - // avoid computing this twice, and repulsing itself - if (b2.x >= ball.x) continue; - repulse(gameState, ball, b2, gameState.perks.ball_repulse_ball, true); - } + if (gameState.perks.ball_repulse_ball) { + for (let b2 of gameState.balls) { + // avoid computing this twice, and repulsing itself + if (b2.x >= ball.x) continue; + repulse(gameState, ball, b2, gameState.perks.ball_repulse_ball, true); } - if (gameState.perks.ball_attract_ball) { - for (let b2 of gameState.balls) { - // avoid computing this twice, and repulsing itself - if (b2.x >= ball.x) continue; - attract(gameState, ball, b2, gameState.perks.ball_attract_ball); - } + } + if (gameState.perks.ball_attract_ball) { + for (let b2 of gameState.balls) { + // avoid computing this twice, and repulsing itself + if (b2.x >= ball.x) continue; + attract(gameState, ball, b2, gameState.perks.ball_attract_ball); } - if ( - gameState.perks.puck_repulse_ball && - Math.abs(ball.x - gameState.puckPosition) < - gameState.puckWidth / 2 + + } + if ( + gameState.perks.puck_repulse_ball && + Math.abs(ball.x - gameState.puckPosition) < + gameState.puckWidth / 2 + (gameState.ballSize * (9 + gameState.perks.puck_repulse_ball)) / 10 - ) { - repulse( - gameState, - ball, - { - x: gameState.puckPosition, - y: gameState.gameZoneHeight, - }, - gameState.perks.puck_repulse_ball + 1, - false, - ); - } - - - const borderHitCode = bordersHitCheck( - gameState, - ball, - gameState.ballSize / 2, - delta, + ) { + repulse( + gameState, + ball, + { + x: gameState.puckPosition, + y: gameState.gameZoneHeight, + }, + gameState.perks.puck_repulse_ball + 1, + false, ); - if (borderHitCode) { - if ( - gameState.perks.left_is_lava && - borderHitCode % 2 && - ball.x < gameState.offsetX + gameState.gameZoneWidth / 2 - ) { - resetCombo(gameState, ball.x, ball.y); - } + } - if ( - gameState.perks.right_is_lava && - borderHitCode % 2 && - ball.x > gameState.offsetX + gameState.gameZoneWidth / 2 - ) { - resetCombo(gameState, ball.x, ball.y); - } - - if (gameState.perks.top_is_lava && borderHitCode >= 2) { - resetCombo(gameState, ball.x, ball.y + gameState.ballSize); - } - if (gameState.perks.trampoline && borderHitCode >= 2) { - decreaseCombo( - gameState, - gameState.perks.trampoline, - ball.x, - ball.y + gameState.ballSize, - ); - } - - schedulGameSound(gameState, "wallBeep", ball.x, 1); - gameState.levelWallBounces++; - gameState.runStatistics.wall_bounces++; - } - - // Puck collision - const ylimit = - gameState.gameZoneHeight - gameState.puckHeight - gameState.ballSize / 2; - const ballIsUnderPuck = - Math.abs(ball.x - gameState.puckPosition) < - gameState.ballSize / 2 + gameState.puckWidth / 2; + const borderHitCode = bordersHitCheck( + gameState, + ball, + gameState.ballSize / 2, + delta, + ); + if (borderHitCode) { if ( - ball.y > ylimit && - ball.vy > 0 && - (ballIsUnderPuck || - (gameState.perks.extra_life && - ball.y > ylimit + gameState.puckHeight / 2)) + gameState.perks.left_is_lava && + borderHitCode % 2 && + ball.x < gameState.offsetX + gameState.gameZoneWidth / 2 ) { - if (ballIsUnderPuck) { - const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy); - const angle = Math.atan2( - -gameState.puckWidth / 2, - (ball.x - gameState.puckPosition) * - (gameState.perks.concave_puck ? -1 / (1 + gameState.perks.concave_puck) : 1), - ); - ball.vx = speed * Math.cos(angle); - ball.vy = speed * Math.sin(angle); - schedulGameSound(gameState, "wallBeep", ball.x, 1); - } else { - ball.vy *= -1; - justLostALife(gameState, ball, ball.x, ball.y); - } - if (gameState.perks.streak_shots) { - resetCombo(gameState, ball.x, ball.y); - } - if (gameState.perks.trampoline) { - gameState.combo += gameState.perks.trampoline; - } - if ( - gameState.perks.nbricks && - ball.brokenSinceBounce < gameState.perks.nbricks - ) { - resetCombo(gameState, ball.x, ball.y); - } - - - if (!ball.hitSinceBounce && gameState.bricks.find((i) => i)) { - gameState.runStatistics.misses++; - if (gameState.perks.forgiving) { - const loss = Math.floor( - (gameState.levelMisses / 10 / gameState.perks.forgiving) * - (gameState.combo - baseCombo(gameState)), - ); - decreaseCombo(gameState, loss, ball.x, ball.y - gameState.ballSize); - } else { - resetCombo(gameState, ball.x, ball.y); - } - gameState.levelMisses++; - makeText( - gameState, - gameState.puckPosition, - gameState.gameZoneHeight - gameState.puckHeight * 2, - "red", - t("play.missed_ball"), - gameState.puckHeight, - 500, - ); - } - gameState.runStatistics.puck_bounces++; - ball.hitSinceBounce = 0; - ball.brokenSinceBounce = 0; - ball.sapperUses = 0; - ball.piercePoints = gameState.perks.pierce * 3; + resetCombo(gameState, ball.x, ball.y); } - const lostOnSides = - (gameState.perks.unbounded && ball.x < -gameState.gameZoneWidth / 2) || - ball.x > gameState.canvasWidth + gameState.gameZoneWidth / 2; - - const lostInTheSky = (gameState.perks.unbounded > 1 && - ball.y < -gameState.gameZoneWidth / 2 - ) - if ( - gameState.running && - (ball.y > gameState.gameZoneHeight + gameState.ballSize / 2 || lostOnSides - || lostInTheSky - ) + gameState.perks.right_is_lava && + borderHitCode % 2 && + ball.x > gameState.offsetX + gameState.gameZoneWidth / 2 ) { - ball.destroyed = true; - gameState.runStatistics.balls_lost++; - if (!gameState.balls.find((b) => !b.destroyed)) { - gameOver( - t("gameOver.lost.title"), - t("gameOver.lost.summary", {score: gameState.score}), - ); - } + resetCombo(gameState, ball.x, ball.y); } - const radius = gameState.ballSize / 2; - // Make ball/coin bonce, and return bricks that were hit - const {x, y, previousX, previousY} = ball; - const vhit = hitsSomething(previousX, y, radius); - const hhit = hitsSomething(x, previousY, radius); - const chit = - (typeof vhit == "undefined" && - typeof hhit == "undefined" && - hitsSomething(x, y, radius)) || - undefined; + if (gameState.perks.top_is_lava && borderHitCode >= 2) { + resetCombo(gameState, ball.x, ball.y + gameState.ballSize); + } + if (gameState.perks.trampoline && borderHitCode >= 2) { + decreaseCombo( + gameState, + gameState.perks.trampoline, + ball.x, + ball.y + gameState.ballSize, + ); + } - const hitBrick = vhit ?? hhit ?? chit; + schedulGameSound(gameState, "wallBeep", ball.x, 1); + gameState.levelWallBounces++; + gameState.runStatistics.wall_bounces++; + } - if (typeof hitBrick !== "undefined") { - ball.hitSinceBounce++; - let pierce = false; - let damage = - 1 + - (shouldPierceByColor(gameState, vhit, hhit, chit) - ? gameState.perks.pierce_color - : 0); + // Puck collision + const ylimit = + gameState.gameZoneHeight - gameState.puckHeight - gameState.ballSize / 2; + const ballIsUnderPuck = + Math.abs(ball.x - gameState.puckPosition) < + gameState.ballSize / 2 + gameState.puckWidth / 2; + if ( + ball.y > ylimit && + ball.vy > 0 && + (ballIsUnderPuck || + (gameState.perks.extra_life && + ball.y > ylimit + gameState.puckHeight / 2)) + ) { + if (ballIsUnderPuck) { + const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy); + const angle = Math.atan2( + -gameState.puckWidth / 2, + (ball.x - gameState.puckPosition) * + (gameState.perks.concave_puck + ? -1 / (1 + gameState.perks.concave_puck) + : 1), + ); + ball.vx = speed * Math.cos(angle); + ball.vy = speed * Math.sin(angle); + schedulGameSound(gameState, "wallBeep", ball.x, 1); + } else { + ball.vy *= -1; + justLostALife(gameState, ball, ball.x, ball.y); + } + if (gameState.perks.streak_shots) { + resetCombo(gameState, ball.x, ball.y); + } + if (gameState.perks.trampoline) { + gameState.combo += gameState.perks.trampoline; + } + if ( + gameState.perks.nbricks && + ball.brokenSinceBounce < gameState.perks.nbricks + ) { + resetCombo(gameState, ball.x, ball.y); + } - gameState.brickHP[hitBrick] -= damage; - - const used = Math.min( - ball.piercePoints, - Math.max(1, gameState.brickHP[hitBrick]), + if (!ball.hitSinceBounce && gameState.bricks.find((i) => i)) { + gameState.runStatistics.misses++; + if (gameState.perks.forgiving) { + const loss = Math.floor( + (gameState.levelMisses / 10 / gameState.perks.forgiving) * + (gameState.combo - baseCombo(gameState)), ); - gameState.brickHP[hitBrick] -= used; - ball.piercePoints -= used; + decreaseCombo(gameState, loss, ball.x, ball.y - gameState.ballSize); + } else { + resetCombo(gameState, ball.x, ball.y); + } + gameState.levelMisses++; + makeText( + gameState, + gameState.puckPosition, + gameState.gameZoneHeight - gameState.puckHeight * 2, + "red", + t("play.missed_ball"), + gameState.puckHeight, + 500, + ); + } + gameState.runStatistics.puck_bounces++; + ball.hitSinceBounce = 0; + ball.brokenSinceBounce = 0; + ball.sapperUses = 0; + ball.piercePoints = gameState.perks.pierce * 3; + } - if (gameState.brickHP[hitBrick] < 0) { - gameState.brickHP[hitBrick] = 0; - pierce = true; - } - if (typeof vhit !== "undefined" || typeof chit !== "undefined") { - if (!pierce) { - ball.y = ball.previousY; - ball.vy *= -1; - } - } - if (typeof hhit !== "undefined" || typeof chit !== "undefined") { - if (!pierce) { - ball.x = ball.previousX; - ball.vx *= -1; - } - } + const lostOnSides = + (gameState.perks.unbounded && ball.x < -gameState.gameZoneWidth / 2) || + ball.x > gameState.canvasWidth + gameState.gameZoneWidth / 2; - if (!gameState.brickHP[hitBrick]) { - const initialBrickColor = gameState.bricks[hitBrick]; - ball.brokenSinceBounce++; + const lostInTheSky = + gameState.perks.unbounded > 1 && ball.y < -gameState.gameZoneWidth / 2; - explodeBrick(gameState, hitBrick, ball, false); - if ( - ball.sapperUses < gameState.perks.sapper && - initialBrickColor !== "black" && // don't replace a brick that bounced with sturdy_bricks - !gameState.bricks[hitBrick] - ) { - setBrick(gameState, hitBrick, "black"); - ball.sapperUses++; - } - } else { - schedulGameSound(gameState, "wallBeep", x, 1); - makeLight( - gameState, - brickCenterX(gameState, hitBrick), - brickCenterY(gameState, hitBrick), - "white", - gameState.brickWidth + 2, - 50 * gameState.brickHP[hitBrick], - ); - } + if ( + gameState.running && + (ball.y > gameState.gameZoneHeight + gameState.ballSize / 2 || + lostOnSides || + lostInTheSky) + ) { + ball.destroyed = true; + gameState.runStatistics.balls_lost++; + if (!gameState.balls.find((b) => !b.destroyed)) { + gameOver( + t("gameOver.lost.title"), + t("gameOver.lost.summary", { score: gameState.score }), + ); + } + } + const radius = gameState.ballSize / 2; + // Make ball/coin bonce, and return bricks that were hit + const { x, y, previousX, previousY } = ball; + + const vhit = hitsSomething(previousX, y, radius); + const hhit = hitsSomething(x, previousY, radius); + const chit = + (typeof vhit == "undefined" && + typeof hhit == "undefined" && + hitsSomething(x, y, radius)) || + undefined; + + const hitBrick = vhit ?? hhit ?? chit; + + if (typeof hitBrick !== "undefined") { + ball.hitSinceBounce++; + let pierce = false; + let damage = + 1 + + (shouldPierceByColor(gameState, vhit, hhit, chit) + ? gameState.perks.pierce_color + : 0); + + gameState.brickHP[hitBrick] -= damage; + + const used = Math.min( + ball.piercePoints, + Math.max(1, gameState.brickHP[hitBrick]), + ); + gameState.brickHP[hitBrick] -= used; + ball.piercePoints -= used; + + if (gameState.brickHP[hitBrick] < 0) { + gameState.brickHP[hitBrick] = 0; + pierce = true; + } + if (typeof vhit !== "undefined" || typeof chit !== "undefined") { + if (!pierce) { + ball.y = ball.previousY; + ball.vy *= -1; + } + } + if (typeof hhit !== "undefined" || typeof chit !== "undefined") { + if (!pierce) { + ball.x = ball.previousX; + ball.vx *= -1; + } } - if (!isOptionOn("basic")) { - const remainingPierce = ball.piercePoints; - const remainingSapper = ball.sapperUses < gameState.perks.sapper; - const extraCombo = gameState.combo - 1; - if ( - (extraCombo && Math.random() > 0.1 / (1 + extraCombo)) || - (remainingSapper && Math.random() > 0.1 / (1 + remainingSapper)) || - (extraCombo && Math.random() > 0.1 / (1 + extraCombo)) - ) { - const color = remainingSapper - ? Math.random() > 0.5 - ? "orange" - : "red" - : gameState.ballsColor; + if (!gameState.brickHP[hitBrick]) { + const initialBrickColor = gameState.bricks[hitBrick]; + ball.brokenSinceBounce++; - makeParticle( - gameState, - ball.x, - ball.y, - gameState.perks.pierce_color || remainingPierce - ? -ball.vx + ((Math.random() - 0.5) * gameState.baseSpeed) / 3 - : (Math.random() - 0.5) * gameState.baseSpeed, - gameState.perks.pierce_color || remainingPierce - ? -ball.vy + ((Math.random() - 0.5) * gameState.baseSpeed) / 3 - : (Math.random() - 0.5) * gameState.baseSpeed, - color, - true, - gameState.coinSize / 2, - 100, - ); - } + explodeBrick(gameState, hitBrick, ball, false); + if ( + ball.sapperUses < gameState.perks.sapper && + initialBrickColor !== "black" && // don't replace a brick that bounced with sturdy_bricks + !gameState.bricks[hitBrick] + ) { + setBrick(gameState, hitBrick, "black"); + ball.sapperUses++; + } + } else { + schedulGameSound(gameState, "wallBeep", x, 1); + makeLight( + gameState, + brickCenterX(gameState, hitBrick), + brickCenterY(gameState, hitBrick), + "white", + gameState.brickWidth + 2, + 50 * gameState.brickHP[hitBrick], + ); } + } + + if (!isOptionOn("basic")) { + const remainingPierce = ball.piercePoints; + const remainingSapper = ball.sapperUses < gameState.perks.sapper; + const extraCombo = gameState.combo - 1; + if ( + (extraCombo && Math.random() > 0.1 / (1 + extraCombo)) || + (remainingSapper && Math.random() > 0.1 / (1 + remainingSapper)) || + (extraCombo && Math.random() > 0.1 / (1 + extraCombo)) + ) { + const color = remainingSapper + ? Math.random() > 0.5 + ? "orange" + : "red" + : gameState.ballsColor; + + makeParticle( + gameState, + ball.x, + ball.y, + gameState.perks.pierce_color || remainingPierce + ? -ball.vx + ((Math.random() - 0.5) * gameState.baseSpeed) / 3 + : (Math.random() - 0.5) * gameState.baseSpeed, + gameState.perks.pierce_color || remainingPierce + ? -ball.vy + ((Math.random() - 0.5) * gameState.baseSpeed) / 3 + : (Math.random() - 0.5) * gameState.baseSpeed, + color, + true, + gameState.coinSize / 2, + 100, + ); + } + } } function justLostALife(gameState: GameState, ball: Ball, x: number, y: number) { - gameState.perks.extra_life -= 1; - if (gameState.perks.extra_life < 0) { - gameState.perks.extra_life = 0; - } else if (gameState.perks.sacrifice) { - gameState.combo *= gameState.perks.sacrifice - gameState.bricks.forEach( - (color, index) => color && explodeBrick(gameState, index, ball, true), - ); - } + gameState.perks.extra_life -= 1; + if (gameState.perks.extra_life < 0) { + gameState.perks.extra_life = 0; + } else if (gameState.perks.sacrifice) { + gameState.combo *= gameState.perks.sacrifice; + gameState.bricks.forEach( + (color, index) => color && explodeBrick(gameState, index, ball, true), + ); + } - schedulGameSound(gameState, "lifeLost", ball.x, 1); + schedulGameSound(gameState, "lifeLost", ball.x, 1); - if (!isOptionOn("basic")) { - for (let i = 0; i < 10; i++) - makeParticle( - gameState, - x, - y, - Math.random() * gameState.baseSpeed * 3, - gameState.baseSpeed * 3, - "red", - false, - gameState.coinSize / 2, - 150, - ); - } + if (!isOptionOn("basic")) { + for (let i = 0; i < 10; i++) + makeParticle( + gameState, + x, + y, + Math.random() * gameState.baseSpeed * 3, + gameState.baseSpeed * 3, + "red", + false, + gameState.coinSize / 2, + 150, + ); + } } function makeCoin( - gameState: GameState, - x: number, - y: number, - vx: number, - vy: number, - color = "gold", - points = 1, + gameState: GameState, + x: number, + y: number, + vx: number, + vy: number, + color = "gold", + points = 1, ) { - let weight = 0.8 + Math.random() * 0.2 + Math.min(2, points * 0.01) - weight *= 5 / (5 + gameState.perks.etherealcoins) + let weight = 0.8 + Math.random() * 0.2 + Math.min(2, points * 0.01); + weight *= 5 / (5 + gameState.perks.etherealcoins); - append(gameState.coins, (p: Partial) => { - p.x = x; - p.y = y; - p.collidedLastFrame = true; - p.size = gameState.coinSize; - p.previousX = x; - p.previousY = y; - p.vx = vx; - p.vy = vy; - // p.sx = 0; - // p.sy = 0; - p.color = color; - p.a = Math.random() * Math.PI * 2; - p.sa = Math.random() - 0.5; - p.points = points; - p.weight = weight; - p.metamorphosisPoints = gameState.perks.metamorphosis - }); + append(gameState.coins, (p: Partial) => { + p.x = x; + p.y = y; + p.collidedLastFrame = true; + p.size = gameState.coinSize; + p.previousX = x; + p.previousY = y; + p.vx = vx; + p.vy = vy; + // p.sx = 0; + // p.sy = 0; + p.color = color; + p.a = Math.random() * Math.PI * 2; + p.sa = Math.random() - 0.5; + p.points = points; + p.weight = weight; + p.metamorphosisPoints = gameState.perks.metamorphosis; + }); } function makeParticle( - gameState: GameState, - x: number, - y: number, - vx: number, - vy: number, - color: colorString, - ethereal = false, - size = 8, - duration = 150, + gameState: GameState, + x: number, + y: number, + vx: number, + vy: number, + color: colorString, + ethereal = false, + size = 8, + duration = 150, ) { - append(gameState.particles, (p: Partial) => { - p.time = gameState.levelTime; - p.x = x; - p.y = y; - p.vx = vx; - p.vy = vy; - p.color = color; - p.size = size; - p.duration = duration; - p.ethereal = ethereal; - }); + append(gameState.particles, (p: Partial) => { + p.time = gameState.levelTime; + p.x = x; + p.y = y; + p.vx = vx; + p.vy = vy; + p.color = color; + p.size = size; + p.duration = duration; + p.ethereal = ethereal; + }); } function makeText( - gameState: GameState, - x: number, - y: number, - color: colorString, - text: string, - size = 20, - duration = 500, + gameState: GameState, + x: number, + y: number, + color: colorString, + text: string, + size = 20, + duration = 500, ) { - append(gameState.texts, (p: Partial) => { - p.time = gameState.levelTime; - p.x = x; - p.y = y; - p.color = color; - p.size = size; - p.duration = clamp(duration, 400, 2000); - p.text = text; - }); + append(gameState.texts, (p: Partial) => { + p.time = gameState.levelTime; + p.x = x; + p.y = y; + p.color = color; + p.size = size; + p.duration = clamp(duration, 400, 2000); + p.text = text; + }); } function makeLight( - gameState: GameState, - x: number, - y: number, - color: colorString, - size = 8, - duration = 150, + gameState: GameState, + x: number, + y: number, + color: colorString, + size = 8, + duration = 150, ) { - append(gameState.lights, (p: Partial) => { - p.time = gameState.levelTime; - p.x = x; - p.y = y; - p.color = color; - p.size = size; - p.duration = duration; - }); + append(gameState.lights, (p: Partial) => { + p.time = gameState.levelTime; + p.x = x; + p.y = y; + p.color = color; + p.size = size; + p.duration = duration; + }); } export function append( - where: ReusableArray, - makeItem: (match: Partial) => void, + where: ReusableArray, + makeItem: (match: Partial) => void, ) { - while ( - where.list[where.indexMin] && - !where.list[where.indexMin].destroyed && - where.indexMin < where.list.length - ) { - where.indexMin++; - } - if (where.indexMin < where.list.length) { - where.list[where.indexMin].destroyed = false; - makeItem(where.list[where.indexMin]); - where.indexMin++; - } else { - const p = {destroyed: false}; - makeItem(p); - where.list.push(p); - } - where.total++; + while ( + where.list[where.indexMin] && + !where.list[where.indexMin].destroyed && + where.indexMin < where.list.length + ) { + where.indexMin++; + } + if (where.indexMin < where.list.length) { + where.list[where.indexMin].destroyed = false; + makeItem(where.list[where.indexMin]); + where.indexMin++; + } else { + const p = { destroyed: false }; + makeItem(p); + where.list.push(p); + } + where.total++; } export function destroy(where: ReusableArray, index: number) { - if (where.list[index].destroyed) return; - where.list[index].destroyed = true; - where.indexMin = Math.min(where.indexMin, index); - where.total--; + if (where.list[index].destroyed) return; + where.list[index].destroyed = true; + where.indexMin = Math.min(where.indexMin, index); + where.total--; } export function liveCount(where: ReusableArray) { - return where.total; + return where.total; } export function empty(where: ReusableArray) { - let destroyed=0 - where.total = 0; - where.indexMin = 0; - where.list.forEach((i) => { - if(!i.destroyed) { - i.destroyed = true - destroyed++ - } - }); - return destroyed + let destroyed = 0; + where.total = 0; + where.indexMin = 0; + where.list.forEach((i) => { + if (!i.destroyed) { + i.destroyed = true; + destroyed++; + } + }); + return destroyed; } export function forEachLiveOne( - where: ReusableArray, - cb: (t: T, index: number) => void, + where: ReusableArray, + cb: (t: T, index: number) => void, ) { - where.list.forEach((item: T, index: number) => { - if (item && !item.destroyed) { - cb(item, index); - } - }); + where.list.forEach((item: T, index: number) => { + if (item && !item.destroyed) { + cb(item, index); + } + }); } diff --git a/src/game_utils.ts b/src/game_utils.ts index 6fec614..9fc5114 100644 --- a/src/game_utils.ts +++ b/src/game_utils.ts @@ -1,6 +1,6 @@ -import {Ball, GameState, PerkId, PerksMap} from "./types"; -import {icons, upgrades} from "./loadGameData"; -import {t} from "./i18n/i18n"; +import { Ball, GameState, PerkId, PerksMap } from "./types"; +import { icons, upgrades } from "./loadGameData"; +import { t } from "./i18n/i18n"; export function getMajorityValue(arr: string[]): string { const count: { [k: string]: number } = {}; @@ -14,10 +14,8 @@ export function sample(arr: T[]): T { return arr[Math.floor(arr.length * Math.random())]; } -export function sampleN(arr: T[],n:number): T[] { - - return [...arr].sort(()=>Math.random()-0.5) - .slice(0,n) +export function sampleN(arr: T[], n: number): T[] { + return [...arr].sort(() => Math.random() - 0.5).slice(0, n); } export function sumOfValues(obj: { [key: string]: number } | undefined | null) { @@ -55,7 +53,10 @@ export function getRowColIndex(gameState: GameState, row: number, col: number) { export function getPossibleUpgrades(gameState: GameState) { return upgrades - .filter((u) => gameState.totalScoreAtRunStart >= u.threshold || gameState.loop>0) + .filter( + (u) => + gameState.totalScoreAtRunStart >= u.threshold || gameState.loop > 0, + ) .filter((u) => !u?.requires || gameState.perks[u?.requires]); } @@ -186,4 +187,3 @@ export function countBricksBelow(gameState: GameState, index: number) { } return count; } - diff --git a/src/newGameState.ts b/src/newGameState.ts index f3ce899..e8cc458 100644 --- a/src/newGameState.ts +++ b/src/newGameState.ts @@ -1,4 +1,4 @@ -import { GameState, RunParams } from "./types"; +import { GameState, RunParams } from "./types"; import { getTotalScore } from "./settings"; import { allLevels, upgrades } from "./loadGameData"; import { @@ -129,5 +129,3 @@ export function newGameState(params: RunParams): GameState { } return gameState; } - - diff --git a/src/premium.ts b/src/premium.ts index 5aa7ac8..449d253 100644 --- a/src/premium.ts +++ b/src/premium.ts @@ -115,7 +115,6 @@ export function premiumMenuEntry(gameState: GameState) { text = t("premium.per_hours", args); help = t("premium.per_hours_help", args); } - } } catch (e) { console.warn(e); diff --git a/src/pure_functions.ts b/src/pure_functions.ts index 2794f93..90832f3 100644 --- a/src/pure_functions.ts +++ b/src/pure_functions.ts @@ -1,7 +1,7 @@ export function clamp(value: number, min: number, max: number) { - return Math.max(min, Math.min(value, max)); + return Math.max(min, Math.min(value, max)); } export function comboKeepingRate(level: number) { - return clamp(1 - 1 / (1 + level) * 1.5, 0, 1) -} \ No newline at end of file + return clamp(1 - (1 / (1 + level)) * 1.5, 0, 1); +} diff --git a/src/render.ts b/src/render.ts index dc6582b..f6ddc20 100644 --- a/src/render.ts +++ b/src/render.ts @@ -1,1000 +1,1020 @@ -import {baseCombo, forEachLiveOne, liveCount,} from "./gameStateMutators"; +import { baseCombo, forEachLiveOne, liveCount } from "./gameStateMutators"; import { - brickCenterX, - brickCenterY, - countBricksAbove, - countBricksBelow, - currentLevelInfo, - isTelekinesisActive, - isYoyoActive, - max_levels, + brickCenterX, + brickCenterY, + countBricksAbove, + countBricksBelow, + currentLevelInfo, + isTelekinesisActive, + isYoyoActive, + max_levels, } from "./game_utils"; -import {colorString, GameState} from "./types"; -import {t} from "./i18n/i18n"; -import {gameState, lastMeasuredFPS} from "./game"; -import {isOptionOn} from "./options"; +import { colorString, GameState } from "./types"; +import { t } from "./i18n/i18n"; +import { gameState, lastMeasuredFPS } from "./game"; +import { isOptionOn } from "./options"; export const gameCanvas = document.getElementById("game") as HTMLCanvasElement; export const ctx = gameCanvas.getContext("2d", { - alpha: false, + alpha: false, }) as CanvasRenderingContext2D; export const bombSVG = document.createElement("img"); bombSVG.src = - "data:image/svg+xml;base64," + - btoa(` + "data:image/svg+xml;base64," + + btoa(` `); -bombSVG.onload = () => gameState.needsRender = true +bombSVG.onload = () => (gameState.needsRender = true); export const background = document.createElement("img"); export const backgroundCanvas = document.createElement("canvas"); export function render(gameState: GameState) { - const level = currentLevelInfo(gameState); + const level = currentLevelInfo(gameState); - const hasCombo = gameState.combo > baseCombo(gameState); - const {width, height} = gameCanvas; - if (!width || !height) return; + const hasCombo = gameState.combo > baseCombo(gameState); + const { width, height } = gameCanvas; + if (!width || !height) return; - if (gameState.currentLevel || gameState.levelTime) { - menuLabel.innerText = gameState.loop - ? t("play.current_lvl_loop", { - level: gameState.currentLevel + 1, - max: max_levels(gameState), - loop: gameState.loop, - }) - : t("play.current_lvl", { - level: gameState.currentLevel + 1, - max: max_levels(gameState), - }); - } else { - menuLabel.innerText = t("play.menu_label"); - } + if (gameState.currentLevel || gameState.levelTime) { + menuLabel.innerText = gameState.loop + ? t("play.current_lvl_loop", { + level: gameState.currentLevel + 1, + max: max_levels(gameState), + loop: gameState.loop, + }) + : t("play.current_lvl", { + level: gameState.currentLevel + 1, + max: max_levels(gameState), + }); + } else { + menuLabel.innerText = t("play.menu_label"); + } - const catchRate = gameState.levelSpawnedCoins ? - (gameState.levelSpawnedCoins - gameState.levelLostCoins)/gameState.levelSpawnedCoins :1 + const catchRate = gameState.levelSpawnedCoins + ? (gameState.levelSpawnedCoins - gameState.levelLostCoins) / + gameState.levelSpawnedCoins + : 1; - scoreDisplay.innerHTML= - (isOptionOn("show_fps") ? ` - + scoreDisplay.innerHTML = + (isOptionOn("show_fps") + ? ` + ${lastMeasuredFPS} FPS / - `:'')+ - - - - (isOptionOn('show_stats') ? ` - - ${Math.floor(catchRate*100)}% + ` + : "") + + (isOptionOn("show_stats") + ? ` + 0.9 && "good") || ""}"> + ${Math.floor(catchRate * 100)}% / - + ${gameState.levelWallBounces} B / - - ${Math.ceil(gameState.levelTime/1000)}s + + ${Math.ceil(gameState.levelTime / 1000)}s / - + ${gameState.levelMisses} M / - `: '' )+ `$${gameState.score}`; + ` + : "") + + `$${gameState.score}`; + scoreDisplay.className = + gameState.lastScoreIncrease > gameState.levelTime - 500 ? "active" : ""; - scoreDisplay.className = - gameState.lastScoreIncrease > gameState.levelTime - 500 ? "active" : ""; - - // Clear - if (!isOptionOn("basic") && !level.color && level.svg) { - // Without this the light trails everything - ctx.globalCompositeOperation = "source-over"; - ctx.globalAlpha = 1; - ctx.fillStyle = "#000"; - ctx.fillRect(0, 0, width, height); - - ctx.globalCompositeOperation = "screen"; - ctx.globalAlpha = 0.6; - - forEachLiveOne(gameState.coins, (coin) => { - drawFuzzyBall(ctx, coin.color, gameState.coinSize * 2, coin.x, coin.y); - }); - gameState.balls.forEach((ball) => { - drawFuzzyBall( - ctx, - gameState.ballsColor, - gameState.ballSize * 2, - ball.x, - ball.y, - ); - }); - ctx.globalAlpha = 0.5; - gameState.bricks.forEach((color, index) => { - if (!color) return; - const x = brickCenterX(gameState, index), - y = brickCenterY(gameState, index); - drawFuzzyBall( - ctx, - color == "black" ? "#666" : color, - gameState.brickWidth, - x, - y, - ); - }); - ctx.globalAlpha = 1; - - forEachLiveOne(gameState.particles, (flash) => { - const {x, y, time, color, size, duration} = flash; - const elapsed = gameState.levelTime - time; - ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2); - drawFuzzyBall(ctx, color, size * 3, x, y); - }); - - // Decides how brights the bg black parts can get - ctx.globalAlpha = 0.2; - ctx.globalCompositeOperation = "multiply"; - ctx.fillStyle = "black"; - ctx.fillRect(0, 0, width, height); - // Decides how dark the background black parts are when lit (1=black) - ctx.globalAlpha = 0.8; - ctx.globalCompositeOperation = "multiply"; - if (level.svg && background.width && background.complete) { - if (backgroundCanvas.title !== level.name) { - backgroundCanvas.title = level.name; - backgroundCanvas.width = gameState.canvasWidth; - backgroundCanvas.height = gameState.canvasHeight; - const bgctx = backgroundCanvas.getContext( - "2d", - ) as CanvasRenderingContext2D; - bgctx.fillStyle = level.color || "#000"; - bgctx.fillRect(0, 0, gameState.canvasWidth, gameState.canvasHeight); - if (gameState.perks.clairvoyant >= 3) { - const pageSource = document.body.innerHTML.replace(/\s+/gi, '') - const lineWidth = Math.ceil(gameState.canvasWidth / 15) - const lines = Math.ceil(gameState.canvasHeight / 20) - const chars = lineWidth * lines - let start = Math.ceil(Math.random() * (pageSource.length - chars)) - for (let i = 0; i < lines; i++) { - bgctx.fillStyle = 'white' - bgctx.font = '20px Courier' - bgctx.fillText(pageSource.slice( - start + i * lineWidth, - start + (i + 1) * lineWidth), - 0, - i * 20, - gameState.canvasWidth - ) - } - } else { - - const pattern = ctx.createPattern(background, "repeat"); - if (pattern) { - bgctx.fillStyle = pattern; - bgctx.fillRect(0, 0, width, height); - } - } - } - - ctx.drawImage(backgroundCanvas, 0, 0); - } else { - // Background not loaded yes - ctx.fillStyle = "#000"; - ctx.fillRect(0, 0, width, height); - } - } else { - ctx.globalAlpha = 1; - ctx.globalCompositeOperation = "source-over"; - ctx.fillStyle = level.color || "#000"; - ctx.fillRect(0, 0, width, height); - forEachLiveOne(gameState.particles, (flash) => { - const {x, y, time, color, size, duration} = flash; - const elapsed = gameState.levelTime - time; - ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2); - drawBall(ctx, color, size, x, y); - }); - } - - ctx.globalAlpha = 1; + // Clear + if (!isOptionOn("basic") && !level.color && level.svg) { + // Without this the light trails everything ctx.globalCompositeOperation = "source-over"; - const lastExplosionDelay = Date.now() - gameState.lastExplosion + 5; - const shaked = lastExplosionDelay < 200 && !isOptionOn("basic"); - if (shaked) { - const amplitude = - ((gameState.perks.bigger_explosions + 1) * 50) / lastExplosionDelay; - ctx.translate( - Math.sin(Date.now()) * amplitude, - Math.sin(Date.now() + 36) * amplitude, - ); - } - if (gameState.perks.bigger_explosions && !isOptionOn("basic") && shaked) { - gameCanvas.style.filter = - "brightness(" + (1 + 100 / (1 + lastExplosionDelay)) + ")"; - } else { - gameCanvas.style.filter = ""; - } - // Coins ctx.globalAlpha = 1; + ctx.fillStyle = "#000"; + ctx.fillRect(0, 0, width, height); + + ctx.globalCompositeOperation = "screen"; + ctx.globalAlpha = 0.6; + forEachLiveOne(gameState.coins, (coin) => { - ctx.globalCompositeOperation = "source-over"; - // ctx.globalCompositeOperation = - // coin.color === "gold" || level.color ? "source-over" : "screen"; - drawCoin( - ctx, - coin.color, - coin.size, - coin.x, - coin.y, - (hasCombo && gameState.perks.asceticism && "red") || - (coin.color==='gold' && 'gold')|| - gameState.puckColor, - coin.a, - ); + drawFuzzyBall(ctx, coin.color, gameState.coinSize * 2, coin.x, coin.y); }); - - // Black shadow around balls - if (!isOptionOn("basic")) { - ctx.globalCompositeOperation = "source-over"; - ctx.globalAlpha = Math.min(0.8, liveCount(gameState.coins) / 20); - gameState.balls.forEach((ball) => { - drawBall( - ctx, - level.color || "#000", - gameState.ballSize * 6, - ball.x, - ball.y, - ); - }); - - } - - ctx.globalCompositeOperation = "source-over"; - renderAllBricks(); - - ctx.globalCompositeOperation = "screen"; - forEachLiveOne(gameState.lights, (flash) => { - const {x, y, time, color, size, duration} = flash; - const elapsed = gameState.levelTime - time; - ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2) * 0.5; - drawBrick(ctx, color, x, y, -1, gameState.perks.clairvoyant >= 2); - }); - - ctx.globalCompositeOperation = "screen"; - forEachLiveOne(gameState.texts, (flash) => { - const {x, y, time, color, size, duration} = flash; - const elapsed = gameState.levelTime - time; - ctx.globalAlpha = Math.max(0, Math.min(1, 2 - (elapsed / duration) * 2)); - ctx.globalCompositeOperation = "source-over"; - drawText(ctx, flash.text, color, size, x, y - elapsed / 10); - }); - - forEachLiveOne(gameState.particles, (particle) => { - const {x, y, time, color, size, duration} = particle; - const elapsed = gameState.levelTime - time; - ctx.globalAlpha = Math.max(0, Math.min(1, 2 - (elapsed / duration) * 2)); - ctx.globalCompositeOperation = "screen"; - drawBall(ctx, color, size, x, y); - drawFuzzyBall(ctx, color, size, x, y); - }); - - if (gameState.perks.extra_life) { - ctx.globalAlpha = 1; - ctx.globalCompositeOperation = "source-over"; - ctx.fillStyle = gameState.puckColor; - for (let i = 0; i < gameState.perks.extra_life; i++) { - ctx.fillRect( - gameState.perks.unbounded ? 0 : gameState.offsetXRoundedDown, - gameState.gameZoneHeight - gameState.puckHeight / 2 + 2 * i, - gameState.perks.unbounded - ? gameState.canvasWidth - : gameState.gameZoneWidthRoundedUp, - 1, - ); - } - } - - ctx.globalAlpha = 1; - ctx.globalCompositeOperation = "source-over"; - gameState.balls.forEach((ball) => { - const drawingColor = gameState.ballsColor; - - // The white border around is to distinguish colored balls from coins/bg - drawBall( - ctx, - drawingColor, - gameState.ballSize, - ball.x, - ball.y, - gameState.puckColor, - ); - - if (isTelekinesisActive(gameState, ball) || isYoyoActive(gameState, ball)) { - ctx.beginPath(); - ctx.moveTo(gameState.puckPosition, gameState.gameZoneHeight); - - ctx.strokeStyle = gameState.puckColor; - ctx.bezierCurveTo( - gameState.puckPosition, - gameState.gameZoneHeight, - gameState.puckPosition, - ball.y, - ball.x, - ball.y, - ); - ctx.stroke(); - - ctx.lineWidth = 2; - ctx.setLineDash(emptyArray); - } - if (gameState.perks.clairvoyant && gameState.ballStickToPuck) { - ctx.strokeStyle = gameState.ballsColor; - ctx.beginPath(); - ctx.moveTo(ball.x, ball.y); - ctx.lineTo(ball.x + ball.vx * 10, ball.y + ball.vy * 10); - ctx.stroke(); - } + drawFuzzyBall( + ctx, + gameState.ballsColor, + gameState.ballSize * 2, + ball.x, + ball.y, + ); }); - // The puck + ctx.globalAlpha = 0.5; + gameState.bricks.forEach((color, index) => { + if (!color) return; + const x = brickCenterX(gameState, index), + y = brickCenterY(gameState, index); + drawFuzzyBall( + ctx, + color == "black" ? "#666" : color, + gameState.brickWidth, + x, + y, + ); + }); + ctx.globalAlpha = 1; + + forEachLiveOne(gameState.particles, (flash) => { + const { x, y, time, color, size, duration } = flash; + const elapsed = gameState.levelTime - time; + ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2); + drawFuzzyBall(ctx, color, size * 3, x, y); + }); + + // Decides how brights the bg black parts can get + ctx.globalAlpha = 0.2; + ctx.globalCompositeOperation = "multiply"; + ctx.fillStyle = "black"; + ctx.fillRect(0, 0, width, height); + // Decides how dark the background black parts are when lit (1=black) + ctx.globalAlpha = 0.8; + ctx.globalCompositeOperation = "multiply"; + if (level.svg && background.width && background.complete) { + if (backgroundCanvas.title !== level.name) { + backgroundCanvas.title = level.name; + backgroundCanvas.width = gameState.canvasWidth; + backgroundCanvas.height = gameState.canvasHeight; + const bgctx = backgroundCanvas.getContext( + "2d", + ) as CanvasRenderingContext2D; + bgctx.fillStyle = level.color || "#000"; + bgctx.fillRect(0, 0, gameState.canvasWidth, gameState.canvasHeight); + if (gameState.perks.clairvoyant >= 3) { + const pageSource = document.body.innerHTML.replace(/\s+/gi, ""); + const lineWidth = Math.ceil(gameState.canvasWidth / 15); + const lines = Math.ceil(gameState.canvasHeight / 20); + const chars = lineWidth * lines; + let start = Math.ceil(Math.random() * (pageSource.length - chars)); + for (let i = 0; i < lines; i++) { + bgctx.fillStyle = "white"; + bgctx.font = "20px Courier"; + bgctx.fillText( + pageSource.slice( + start + i * lineWidth, + start + (i + 1) * lineWidth, + ), + 0, + i * 20, + gameState.canvasWidth, + ); + } + } else { + const pattern = ctx.createPattern(background, "repeat"); + if (pattern) { + bgctx.fillStyle = pattern; + bgctx.fillRect(0, 0, width, height); + } + } + } + + ctx.drawImage(backgroundCanvas, 0, 0); + } else { + // Background not loaded yes + ctx.fillStyle = "#000"; + ctx.fillRect(0, 0, width, height); + } + } else { ctx.globalAlpha = 1; ctx.globalCompositeOperation = "source-over"; + ctx.fillStyle = level.color || "#000"; + ctx.fillRect(0, 0, width, height); + forEachLiveOne(gameState.particles, (flash) => { + const { x, y, time, color, size, duration } = flash; + const elapsed = gameState.levelTime - time; + ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2); + drawBall(ctx, color, size, x, y); + }); + } - drawPuck( - ctx, - gameState.puckColor, - gameState.puckWidth, - gameState.puckHeight, - 0, - gameState.perks.concave_puck, - gameState.perks.streak_shots && hasCombo ? getDashOffset(gameState) : -1, + ctx.globalAlpha = 1; + ctx.globalCompositeOperation = "source-over"; + const lastExplosionDelay = Date.now() - gameState.lastExplosion + 5; + const shaked = lastExplosionDelay < 200 && !isOptionOn("basic"); + if (shaked) { + const amplitude = + ((gameState.perks.bigger_explosions + 1) * 50) / lastExplosionDelay; + ctx.translate( + Math.sin(Date.now()) * amplitude, + Math.sin(Date.now() + 36) * amplitude, ); - - if (gameState.combo > 1) { - ctx.globalCompositeOperation = "source-over"; - const comboText = "x " + gameState.combo; - const comboTextWidth = (comboText.length * gameState.puckHeight) / 1.8; - const totalWidth = comboTextWidth + gameState.coinSize * 2; - const left = gameState.puckPosition - totalWidth / 2; - if (totalWidth < gameState.puckWidth) { - drawCoin( - ctx, - "gold", - gameState.coinSize, - left + gameState.coinSize / 2, - gameState.gameZoneHeight - gameState.puckHeight / 2, - gameState.puckColor, - 0, - ); - drawText( - ctx, - comboText, - "#000", - gameState.puckHeight, - left + gameState.coinSize * 1.5, - gameState.gameZoneHeight - gameState.puckHeight / 2, - true, - ); - } else { - drawText( - ctx, - comboTextWidth > gameState.puckWidth - ? gameState.combo.toString() - : comboText, - "#000", - comboTextWidth > gameState.puckWidth ? 12 : 20, - gameState.puckPosition, - gameState.gameZoneHeight - gameState.puckHeight / 2, - false, - ); - } - } - // Borders - + } + if (gameState.perks.bigger_explosions && !isOptionOn("basic") && shaked) { + gameCanvas.style.filter = + "brightness(" + (1 + 100 / (1 + lastExplosionDelay)) + ")"; + } else { + gameCanvas.style.filter = ""; + } + // Coins + ctx.globalAlpha = 1; + forEachLiveOne(gameState.coins, (coin) => { ctx.globalCompositeOperation = "source-over"; - ctx.globalAlpha = gameState.perks.unbounded ? 0.1 : 1; + // ctx.globalCompositeOperation = + // coin.color === "gold" || level.color ? "source-over" : "screen"; + drawCoin( + ctx, + coin.color, + coin.size, + coin.x, + coin.y, + (hasCombo && gameState.perks.asceticism && "red") || + (coin.color === "gold" && "gold") || + gameState.puckColor, + coin.a, + ); + }); - if (gameState.offsetXRoundedDown) { - // draw outside of gaming area to avoid capturing borders in recordings - ctx.fillStyle = - hasCombo && gameState.perks.left_is_lava ? "red" : gameState.puckColor; + // Black shadow around balls + if (!isOptionOn("basic")) { + ctx.globalCompositeOperation = "source-over"; + ctx.globalAlpha = Math.min(0.8, liveCount(gameState.coins) / 20); + gameState.balls.forEach((ball) => { + drawBall( + ctx, + level.color || "#000", + gameState.ballSize * 6, + ball.x, + ball.y, + ); + }); + } - drawStraightLine( - ctx, - gameState, - (hasCombo && - gameState.perks.left_is_lava && - !gameState.perks.unbounded && - "red") || - "white", - gameState.offsetX - 1, - 0, - gameState.offsetX - 1, - height, - gameState.perks.unbounded ? 0.1 : 1, - ); + ctx.globalCompositeOperation = "source-over"; + renderAllBricks(); - drawStraightLine( - ctx, - gameState, - (hasCombo && - gameState.perks.right_is_lava && - !gameState.perks.unbounded && - "red") || - "white", - width - gameState.offsetX + 1, - 0, - width - gameState.offsetX + 1, - height, - gameState.perks.unbounded ? 0.1 : 1, - ); + ctx.globalCompositeOperation = "screen"; + forEachLiveOne(gameState.lights, (flash) => { + const { x, y, time, color, size, duration } = flash; + const elapsed = gameState.levelTime - time; + ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2) * 0.5; + drawBrick(ctx, color, x, y, -1, gameState.perks.clairvoyant >= 2); + }); + + ctx.globalCompositeOperation = "screen"; + forEachLiveOne(gameState.texts, (flash) => { + const { x, y, time, color, size, duration } = flash; + const elapsed = gameState.levelTime - time; + ctx.globalAlpha = Math.max(0, Math.min(1, 2 - (elapsed / duration) * 2)); + ctx.globalCompositeOperation = "source-over"; + drawText(ctx, flash.text, color, size, x, y - elapsed / 10); + }); + + forEachLiveOne(gameState.particles, (particle) => { + const { x, y, time, color, size, duration } = particle; + const elapsed = gameState.levelTime - time; + ctx.globalAlpha = Math.max(0, Math.min(1, 2 - (elapsed / duration) * 2)); + ctx.globalCompositeOperation = "screen"; + drawBall(ctx, color, size, x, y); + drawFuzzyBall(ctx, color, size, x, y); + }); + + if (gameState.perks.extra_life) { + ctx.globalAlpha = 1; + ctx.globalCompositeOperation = "source-over"; + ctx.fillStyle = gameState.puckColor; + for (let i = 0; i < gameState.perks.extra_life; i++) { + ctx.fillRect( + gameState.perks.unbounded ? 0 : gameState.offsetXRoundedDown, + gameState.gameZoneHeight - gameState.puckHeight / 2 + 2 * i, + gameState.perks.unbounded + ? gameState.canvasWidth + : gameState.gameZoneWidthRoundedUp, + 1, + ); + } + } + + ctx.globalAlpha = 1; + ctx.globalCompositeOperation = "source-over"; + + gameState.balls.forEach((ball) => { + const drawingColor = gameState.ballsColor; + + // The white border around is to distinguish colored balls from coins/bg + drawBall( + ctx, + drawingColor, + gameState.ballSize, + ball.x, + ball.y, + gameState.puckColor, + ); + + if (isTelekinesisActive(gameState, ball) || isYoyoActive(gameState, ball)) { + ctx.beginPath(); + ctx.moveTo(gameState.puckPosition, gameState.gameZoneHeight); + + ctx.strokeStyle = gameState.puckColor; + ctx.bezierCurveTo( + gameState.puckPosition, + gameState.gameZoneHeight, + gameState.puckPosition, + ball.y, + ball.x, + ball.y, + ); + ctx.stroke(); + + ctx.lineWidth = 2; + ctx.setLineDash(emptyArray); + } + if (gameState.perks.clairvoyant && gameState.ballStickToPuck) { + ctx.strokeStyle = gameState.ballsColor; + ctx.beginPath(); + ctx.moveTo(ball.x, ball.y); + ctx.lineTo(ball.x + ball.vx * 10, ball.y + ball.vy * 10); + ctx.stroke(); + } + }); + // The puck + ctx.globalAlpha = 1; + ctx.globalCompositeOperation = "source-over"; + + drawPuck( + ctx, + gameState.puckColor, + gameState.puckWidth, + gameState.puckHeight, + 0, + gameState.perks.concave_puck, + gameState.perks.streak_shots && hasCombo ? getDashOffset(gameState) : -1, + ); + + if (gameState.combo > 1) { + ctx.globalCompositeOperation = "source-over"; + const comboText = "x " + gameState.combo; + const comboTextWidth = (comboText.length * gameState.puckHeight) / 1.8; + const totalWidth = comboTextWidth + gameState.coinSize * 2; + const left = gameState.puckPosition - totalWidth / 2; + if (totalWidth < gameState.puckWidth) { + drawCoin( + ctx, + "gold", + gameState.coinSize, + left + gameState.coinSize / 2, + gameState.gameZoneHeight - gameState.puckHeight / 2, + gameState.puckColor, + 0, + ); + drawText( + ctx, + comboText, + "#000", + gameState.puckHeight, + left + gameState.coinSize * 1.5, + gameState.gameZoneHeight - gameState.puckHeight / 2, + true, + ); } else { - ctx.fillStyle = "red"; - - drawStraightLine( - ctx, - gameState, - (hasCombo && - gameState.perks.left_is_lava && - !gameState.perks.unbounded && - "red") || - "", - 0, - 0, - 0, - height, - 1, - ); - - drawStraightLine( - ctx, - gameState, - (hasCombo && - gameState.perks.right_is_lava && - !gameState.perks.unbounded && - "red") || - "", - width - 1, - 0, - width - 1, - height, - 1, - ); - } - - ctx.globalAlpha = gameState.perks.unbounded > 1 ? 0.1 : 1 - drawStraightLine( + drawText( ctx, - gameState, - (hasCombo && gameState.perks.top_is_lava && "red") || "", - gameState.offsetXRoundedDown, - 1, - width - gameState.offsetXRoundedDown, - 1, - 1, + comboTextWidth > gameState.puckWidth + ? gameState.combo.toString() + : comboText, + "#000", + comboTextWidth > gameState.puckWidth ? 12 : 20, + gameState.puckPosition, + gameState.gameZoneHeight - gameState.puckHeight / 2, + false, + ); + } + } + // Borders + + ctx.globalCompositeOperation = "source-over"; + ctx.globalAlpha = gameState.perks.unbounded ? 0.1 : 1; + + if (gameState.offsetXRoundedDown) { + // draw outside of gaming area to avoid capturing borders in recordings + ctx.fillStyle = + hasCombo && gameState.perks.left_is_lava ? "red" : gameState.puckColor; + + drawStraightLine( + ctx, + gameState, + (hasCombo && + gameState.perks.left_is_lava && + !gameState.perks.unbounded && + "red") || + "white", + gameState.offsetX - 1, + 0, + gameState.offsetX - 1, + height, + gameState.perks.unbounded ? 0.1 : 1, ); - ctx.globalAlpha = 1 drawStraightLine( - ctx, - gameState, - (hasCombo && gameState.perks.compound_interest && "red") || - (isOptionOn("mobile-mode") && "white") || + ctx, + gameState, + (hasCombo && + gameState.perks.right_is_lava && + !gameState.perks.unbounded && + "red") || + "white", + width - gameState.offsetX + 1, + 0, + width - gameState.offsetX + 1, + height, + gameState.perks.unbounded ? 0.1 : 1, + ); + } else { + ctx.fillStyle = "red"; + + drawStraightLine( + ctx, + gameState, + (hasCombo && + gameState.perks.left_is_lava && + !gameState.perks.unbounded && + "red") || "", - gameState.offsetXRoundedDown, - gameState.gameZoneHeight, - width - gameState.offsetXRoundedDown, - gameState.gameZoneHeight, - 1, + 0, + 0, + 0, + height, + 1, ); - if (isOptionOn("mobile-mode") && !gameState.running) { - drawText( - ctx, - t("play.mobile_press_to_play"), - gameState.puckColor, - gameState.puckHeight, - gameState.canvasWidth / 2, - gameState.gameZoneHeight + - (gameState.canvasHeight - gameState.gameZoneHeight) / 2, - ); - } + drawStraightLine( + ctx, + gameState, + (hasCombo && + gameState.perks.right_is_lava && + !gameState.perks.unbounded && + "red") || + "", + width - 1, + 0, + width - 1, + height, + 1, + ); + } + ctx.globalAlpha = gameState.perks.unbounded > 1 ? 0.1 : 1; + drawStraightLine( + ctx, + gameState, + (hasCombo && gameState.perks.top_is_lava && "red") || "", + gameState.offsetXRoundedDown, + 1, + width - gameState.offsetXRoundedDown, + 1, + 1, + ); - if (shaked) { - ctx.resetTransform(); - } + ctx.globalAlpha = 1; + drawStraightLine( + ctx, + gameState, + (hasCombo && gameState.perks.compound_interest && "red") || + (isOptionOn("mobile-mode") && "white") || + "", + gameState.offsetXRoundedDown, + gameState.gameZoneHeight, + width - gameState.offsetXRoundedDown, + gameState.gameZoneHeight, + 1, + ); + + if (isOptionOn("mobile-mode") && !gameState.running) { + drawText( + ctx, + t("play.mobile_press_to_play"), + gameState.puckColor, + gameState.puckHeight, + gameState.canvasWidth / 2, + gameState.gameZoneHeight + + (gameState.canvasHeight - gameState.gameZoneHeight) / 2, + ); + } + + if (shaked) { + ctx.resetTransform(); + } } function drawStraightLine( - ctx: CanvasRenderingContext2D, - gameState: GameState, - mode: "white" | "" | "red", - x1, - y1, - x2, - y2, - alpha = 1, + ctx: CanvasRenderingContext2D, + gameState: GameState, + mode: "white" | "" | "red", + x1, + y1, + x2, + y2, + alpha = 1, ) { - ctx.globalAlpha = alpha; - if (!mode) return; - if (mode == "red") { - ctx.strokeStyle = "red"; - ctx.lineDashOffset = getDashOffset(gameState); - ctx.lineWidth = 2; - ctx.setLineDash(redBorderDash); - } else { - ctx.strokeStyle = "white"; - ctx.lineWidth = 1; - } - ctx.beginPath(); - ctx.moveTo(x1, y1); - ctx.lineTo(x2, y2); - ctx.stroke(); - if (mode == "red") { - ctx.setLineDash(emptyArray); - ctx.lineWidth = 1; - } - ctx.globalAlpha = 1; + ctx.globalAlpha = alpha; + if (!mode) return; + if (mode == "red") { + ctx.strokeStyle = "red"; + ctx.lineDashOffset = getDashOffset(gameState); + ctx.lineWidth = 2; + ctx.setLineDash(redBorderDash); + } else { + ctx.strokeStyle = "white"; + ctx.lineWidth = 1; + } + ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.stroke(); + if (mode == "red") { + ctx.setLineDash(emptyArray); + ctx.lineWidth = 1; + } + ctx.globalAlpha = 1; } let cachedBricksRender = document.createElement("canvas"); let cachedBricksRenderKey = ""; export function renderAllBricks() { - ctx.globalAlpha = 1; + ctx.globalAlpha = 1; - const hasCombo = gameState.combo > baseCombo(gameState); - const redBorderOnBricksWithWrongColor = - hasCombo && gameState.perks.picky_eater && !isOptionOn("basic"); + const hasCombo = gameState.combo > baseCombo(gameState); + const redBorderOnBricksWithWrongColor = + hasCombo && gameState.perks.picky_eater && !isOptionOn("basic"); - const redColorOnAllBricks = !!( - gameState.lastPuckMove && - gameState.perks.passive_income && - hasCombo && - gameState.lastPuckMove > - gameState.levelTime - 250 * gameState.perks.passive_income - ); + const redColorOnAllBricks = !!( + gameState.lastPuckMove && + gameState.perks.passive_income && + hasCombo && + gameState.lastPuckMove > + gameState.levelTime - 250 * gameState.perks.passive_income + ); - let offset = getDashOffset(gameState); - if ( - !( - redBorderOnBricksWithWrongColor || - redColorOnAllBricks || - gameState.perks.reach || - gameState.perks.zen - ) - ) { - offset = 0; - } + let offset = getDashOffset(gameState); + if ( + !( + redBorderOnBricksWithWrongColor || + redColorOnAllBricks || + gameState.perks.reach || + gameState.perks.zen + ) + ) { + offset = 0; + } - const clairVoyance = - gameState.perks.clairvoyant && gameState.brickHP.reduce((a, b) => a + b, 0); + const clairVoyance = + gameState.perks.clairvoyant && gameState.brickHP.reduce((a, b) => a + b, 0); - const newKey = - gameState.gameZoneWidth + - "_" + - gameState.bricks.join("_") + - bombSVG.complete + - "_" + - redBorderOnBricksWithWrongColor + - "_" + - redColorOnAllBricks + - "_" + - gameState.ballsColor + - "_" + - gameState.perks.pierce_color + - "_" + - clairVoyance + - "_" + - offset; + const newKey = + gameState.gameZoneWidth + + "_" + + gameState.bricks.join("_") + + bombSVG.complete + + "_" + + redBorderOnBricksWithWrongColor + + "_" + + redColorOnAllBricks + + "_" + + gameState.ballsColor + + "_" + + gameState.perks.pierce_color + + "_" + + clairVoyance + + "_" + + offset; - if (newKey !== cachedBricksRenderKey) { - cachedBricksRenderKey = newKey; + if (newKey !== cachedBricksRenderKey) { + cachedBricksRenderKey = newKey; - cachedBricksRender.width = gameState.gameZoneWidth; - cachedBricksRender.height = gameState.gameZoneWidth + 1; - const canctx = cachedBricksRender.getContext( - "2d", - ) as CanvasRenderingContext2D; - canctx.clearRect(0, 0, gameState.gameZoneWidth, gameState.gameZoneWidth); - canctx.resetTransform(); - canctx.translate(-gameState.offsetX, 0); - // Bricks - gameState.bricks.forEach((color, index) => { - const x = brickCenterX(gameState, index), - y = brickCenterY(gameState, index); + cachedBricksRender.width = gameState.gameZoneWidth; + cachedBricksRender.height = gameState.gameZoneWidth + 1; + const canctx = cachedBricksRender.getContext( + "2d", + ) as CanvasRenderingContext2D; + canctx.clearRect(0, 0, gameState.gameZoneWidth, gameState.gameZoneWidth); + canctx.resetTransform(); + canctx.translate(-gameState.offsetX, 0); + // Bricks + gameState.bricks.forEach((color, index) => { + const x = brickCenterX(gameState, index), + y = brickCenterY(gameState, index); - if (!color) return; + if (!color) return; - let redBecauseOfReach = - gameState.perks.reach && - countBricksAbove(gameState, index) && - !countBricksBelow(gameState, index); + let redBecauseOfReach = + gameState.perks.reach && + countBricksAbove(gameState, index) && + !countBricksBelow(gameState, index); - let redBorder = (gameState.ballsColor !== color && - color !== "black" && - redBorderOnBricksWithWrongColor) || - (hasCombo && gameState.perks.zen && color === "black") || - redBecauseOfReach || - redColorOnAllBricks; + let redBorder = + (gameState.ballsColor !== color && + color !== "black" && + redBorderOnBricksWithWrongColor) || + (hasCombo && gameState.perks.zen && color === "black") || + redBecauseOfReach || + redColorOnAllBricks; - canctx.globalCompositeOperation = "source-over"; - drawBrick(canctx, - color, x, y, redBorder ? offset : -1, gameState.perks.clairvoyant >= 2); - if (gameState.brickHP[index] > 1 && gameState.perks.clairvoyant) { - canctx.globalCompositeOperation = "destination-out"; - drawText( - canctx, - gameState.brickHP[index].toString(), - "white", - gameState.puckHeight, - x, - y, - ); - } + canctx.globalCompositeOperation = "source-over"; + drawBrick( + canctx, + color, + x, + y, + redBorder ? offset : -1, + gameState.perks.clairvoyant >= 2, + ); + if (gameState.brickHP[index] > 1 && gameState.perks.clairvoyant) { + canctx.globalCompositeOperation = "destination-out"; + drawText( + canctx, + gameState.brickHP[index].toString(), + "white", + gameState.puckHeight, + x, + y, + ); + } - if (color === "black") { - canctx.globalCompositeOperation = "source-over"; - drawIMG(canctx, bombSVG, gameState.brickWidth, x, y); - } - }); - } + if (color === "black") { + canctx.globalCompositeOperation = "source-over"; + drawIMG(canctx, bombSVG, gameState.brickWidth, x, y); + } + }); + } - ctx.drawImage(cachedBricksRender, gameState.offsetX, 0); + ctx.drawImage(cachedBricksRender, gameState.offsetX, 0); } let cachedGraphics: { [k: string]: HTMLCanvasElement } = {}; export function drawPuck( - ctx: CanvasRenderingContext2D, - color: colorString, - puckWidth: number, - puckHeight: number, - yOffset = 0, - concave_puck: number, - redBorderOffset: number, + ctx: CanvasRenderingContext2D, + color: colorString, + puckWidth: number, + puckHeight: number, + yOffset = 0, + concave_puck: number, + redBorderOffset: number, ) { - const key = - "puck" + - color + - "_" + - puckWidth + - "_" + - puckHeight + - "_" + - concave_puck + - "_" + - redBorderOffset; + const key = + "puck" + + color + + "_" + + puckWidth + + "_" + + puckHeight + + "_" + + concave_puck + + "_" + + redBorderOffset; - if (!cachedGraphics[key]) { - const can = document.createElement("canvas"); - can.width = puckWidth; - can.height = puckHeight * 2; - const canctx = can.getContext("2d") as CanvasRenderingContext2D; - canctx.fillStyle = color; + if (!cachedGraphics[key]) { + const can = document.createElement("canvas"); + can.width = puckWidth; + can.height = puckHeight * 2; + const canctx = can.getContext("2d") as CanvasRenderingContext2D; + canctx.fillStyle = color; - canctx.beginPath(); - canctx.moveTo(0, puckHeight * 2); + canctx.beginPath(); + canctx.moveTo(0, puckHeight * 2); - if (concave_puck) { - canctx.lineTo(0, puckHeight * 0.75); - canctx.bezierCurveTo( - puckWidth / 2, - puckHeight * (2 + concave_puck) / 3, - puckWidth / 2, - puckHeight * (2 + concave_puck) / 3, - puckWidth, - puckHeight * 0.75, - ); - canctx.lineTo(puckWidth, puckHeight * 2); - } else { - canctx.lineTo(0, puckHeight * 1.25); - canctx.bezierCurveTo( - 0, - puckHeight * 0.75, - puckWidth, - puckHeight * 0.75, - puckWidth, - puckHeight * 1.25, - ); - canctx.lineTo(puckWidth, puckHeight * 2); - } - - canctx.fill(); - - if (redBorderOffset !== -1) { - canctx.strokeStyle = "red"; - canctx.lineWidth = 4; - canctx.setLineDash(redBorderDash); - canctx.lineDashOffset = redBorderOffset; - canctx.stroke(); - } - - cachedGraphics[key] = can; + if (concave_puck) { + canctx.lineTo(0, puckHeight * 0.75); + canctx.bezierCurveTo( + puckWidth / 2, + (puckHeight * (2 + concave_puck)) / 3, + puckWidth / 2, + (puckHeight * (2 + concave_puck)) / 3, + puckWidth, + puckHeight * 0.75, + ); + canctx.lineTo(puckWidth, puckHeight * 2); + } else { + canctx.lineTo(0, puckHeight * 1.25); + canctx.bezierCurveTo( + 0, + puckHeight * 0.75, + puckWidth, + puckHeight * 0.75, + puckWidth, + puckHeight * 1.25, + ); + canctx.lineTo(puckWidth, puckHeight * 2); } - ctx.drawImage( - cachedGraphics[key], - Math.round(gameState.puckPosition - puckWidth / 2), - gameState.gameZoneHeight - puckHeight * 2 + yOffset, - ); + canctx.fill(); + + if (redBorderOffset !== -1) { + canctx.strokeStyle = "red"; + canctx.lineWidth = 4; + canctx.setLineDash(redBorderDash); + canctx.lineDashOffset = redBorderOffset; + canctx.stroke(); + } + + cachedGraphics[key] = can; + } + + ctx.drawImage( + cachedGraphics[key], + Math.round(gameState.puckPosition - puckWidth / 2), + gameState.gameZoneHeight - puckHeight * 2 + yOffset, + ); } export function drawBall( - ctx: CanvasRenderingContext2D, - color: colorString, - width: number, - x: number, - y: number, - borderColor = "", + ctx: CanvasRenderingContext2D, + color: colorString, + width: number, + x: number, + y: number, + borderColor = "", ) { - const key = "ball" + color + "_" + width + "_" + borderColor; + const key = "ball" + color + "_" + width + "_" + borderColor; - const size = Math.round(width); - if (!cachedGraphics[key]) { - const can = document.createElement("canvas"); - can.width = size; - can.height = size; + const size = Math.round(width); + if (!cachedGraphics[key]) { + const can = document.createElement("canvas"); + can.width = size; + can.height = size; - const canctx = can.getContext("2d") as CanvasRenderingContext2D; - canctx.beginPath(); - canctx.arc(size / 2, size / 2, Math.round(size / 2) - 1, 0, 2 * Math.PI); - canctx.fillStyle = color; - canctx.fill(); - if (borderColor) { - canctx.lineWidth = 2; - canctx.strokeStyle = borderColor; - canctx.stroke(); - } - - cachedGraphics[key] = can; + const canctx = can.getContext("2d") as CanvasRenderingContext2D; + canctx.beginPath(); + canctx.arc(size / 2, size / 2, Math.round(size / 2) - 1, 0, 2 * Math.PI); + canctx.fillStyle = color; + canctx.fill(); + if (borderColor) { + canctx.lineWidth = 2; + canctx.strokeStyle = borderColor; + canctx.stroke(); } - ctx.drawImage( - cachedGraphics[key], - Math.round(x - size / 2), - Math.round(y - size / 2), - ); + + cachedGraphics[key] = can; + } + ctx.drawImage( + cachedGraphics[key], + Math.round(x - size / 2), + Math.round(y - size / 2), + ); } const angles = 32; export function drawCoin( - ctx: CanvasRenderingContext2D, - color: colorString, - size: number, - x: number, - y: number, - borderColor: colorString, - rawAngle: number, + ctx: CanvasRenderingContext2D, + color: colorString, + size: number, + x: number, + y: number, + borderColor: colorString, + rawAngle: number, ) { - const angle = - ((Math.round((rawAngle / Math.PI) * 2 * angles) % angles) + angles) % - angles; - const key = - "coin with halo" + - "_" + - color + - "_" + - size + - "_" + - borderColor + - "_" + - (color === "gold" ? angle : "whatever"); + const angle = + ((Math.round((rawAngle / Math.PI) * 2 * angles) % angles) + angles) % + angles; + const key = + "coin with halo" + + "_" + + color + + "_" + + size + + "_" + + borderColor + + "_" + + (color === "gold" ? angle : "whatever"); - if (!cachedGraphics[key]) { - const can = document.createElement("canvas"); - can.width = size; - can.height = size; + if (!cachedGraphics[key]) { + const can = document.createElement("canvas"); + can.width = size; + can.height = size; - const canctx = can.getContext("2d") as CanvasRenderingContext2D; + const canctx = can.getContext("2d") as CanvasRenderingContext2D; - // coin - canctx.beginPath(); - canctx.arc(size / 2, size / 2, size / 2, 0, 2 * Math.PI); - canctx.fillStyle = color; - canctx.fill(); + // coin + canctx.beginPath(); + canctx.arc(size / 2, size / 2, size / 2, 0, 2 * Math.PI); + canctx.fillStyle = color; + canctx.fill(); - canctx.strokeStyle = borderColor; - if (borderColor == "red") { - canctx.lineWidth = 2; - canctx.setLineDash(redBorderDash); - } - canctx.stroke(); - - if (color === "gold") { - // Fill in - canctx.beginPath(); - canctx.arc(size / 2, size / 2, (size / 2) * 0.6, 0, 2 * Math.PI); - canctx.fillStyle = "rgba(255,255,255,0.5)"; - canctx.fill(); - - canctx.translate(size / 2, size / 2); - canctx.rotate(angle / 16); - canctx.translate(-size / 2, -size / 2); - - canctx.globalCompositeOperation = "multiply"; - drawText(canctx, "$", color, size - 2, size / 2, size / 2 + 1); - drawText(canctx, "$", color, size - 2, size / 2, size / 2 + 1); - } - cachedGraphics[key] = can; + canctx.strokeStyle = borderColor; + if (borderColor == "red") { + canctx.lineWidth = 2; + canctx.setLineDash(redBorderDash); } - ctx.drawImage( - cachedGraphics[key], - Math.round(x - size / 2), - Math.round(y - size / 2), - ); + canctx.stroke(); + + if (color === "gold") { + // Fill in + canctx.beginPath(); + canctx.arc(size / 2, size / 2, (size / 2) * 0.6, 0, 2 * Math.PI); + canctx.fillStyle = "rgba(255,255,255,0.5)"; + canctx.fill(); + + canctx.translate(size / 2, size / 2); + canctx.rotate(angle / 16); + canctx.translate(-size / 2, -size / 2); + + canctx.globalCompositeOperation = "multiply"; + drawText(canctx, "$", color, size - 2, size / 2, size / 2 + 1); + drawText(canctx, "$", color, size - 2, size / 2, size / 2 + 1); + } + cachedGraphics[key] = can; + } + ctx.drawImage( + cachedGraphics[key], + Math.round(x - size / 2), + Math.round(y - size / 2), + ); } export function drawFuzzyBall( - ctx: CanvasRenderingContext2D, - color: colorString, - width: number, - x: number, - y: number, + ctx: CanvasRenderingContext2D, + color: colorString, + width: number, + x: number, + y: number, ) { - const key = "fuzzy-circle" + color + "_" + width; - if (!color) debugger; - const size = Math.round(width * 3); - if (!cachedGraphics[key]) { - const can = document.createElement("canvas"); - can.width = size; - can.height = size; + const key = "fuzzy-circle" + color + "_" + width; + if (!color) debugger; + const size = Math.round(width * 3); + if (!cachedGraphics[key]) { + const can = document.createElement("canvas"); + can.width = size; + can.height = size; - const canctx = can.getContext("2d") as CanvasRenderingContext2D; - 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), + const canctx = can.getContext("2d") as CanvasRenderingContext2D; + 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), + ); } export function drawBrick( - ctx: CanvasRenderingContext2D, - color: colorString, - x: number, - y: number, - offset: number = 0, - borderOnly: boolean + ctx: CanvasRenderingContext2D, + color: colorString, + x: number, + y: number, + offset: number = 0, + borderOnly: boolean, ) { - const tlx = Math.ceil(x - gameState.brickWidth / 2); - const tly = Math.ceil(y - gameState.brickWidth / 2); - const brx = Math.ceil(x + gameState.brickWidth / 2) - 1; - const bry = Math.ceil(y + gameState.brickWidth / 2) - 1; + const tlx = Math.ceil(x - gameState.brickWidth / 2); + const tly = Math.ceil(y - gameState.brickWidth / 2); + const brx = Math.ceil(x + gameState.brickWidth / 2) - 1; + const bry = Math.ceil(y + gameState.brickWidth / 2) - 1; - const width = brx - tlx, - height = bry - tly; - const key = "brick" + color + "_" + "_" + width + "_" + height + "_" + offset + '_' + borderOnly; + const width = brx - tlx, + height = bry - tly; + const key = + "brick" + + color + + "_" + + "_" + + width + + "_" + + height + + "_" + + offset + + "_" + + borderOnly; - if (!cachedGraphics[key]) { - const can = document.createElement("canvas"); - can.width = width; - can.height = height; - const bord = 4; - const cornerRadius = 2; - const canctx = can.getContext("2d") as CanvasRenderingContext2D; + if (!cachedGraphics[key]) { + const can = document.createElement("canvas"); + can.width = width; + can.height = height; + const bord = 4; + const cornerRadius = 2; + const canctx = can.getContext("2d") as CanvasRenderingContext2D; - canctx.fillStyle = color; + canctx.fillStyle = color; - canctx.setLineDash(offset !== -1 ? redBorderDash : emptyArray); - canctx.lineDashOffset = offset; - canctx.strokeStyle = offset !== -1 ? "red" : color; - canctx.lineJoin = "round"; - canctx.lineWidth = bord; - roundRect( - canctx, - bord / 2, - bord / 2, - width - bord, - height - bord, - cornerRadius, - ); - if (!borderOnly) { - canctx.fill(); - } - canctx.stroke(); - - cachedGraphics[key] = can; + canctx.setLineDash(offset !== -1 ? redBorderDash : emptyArray); + canctx.lineDashOffset = offset; + canctx.strokeStyle = offset !== -1 ? "red" : color; + canctx.lineJoin = "round"; + canctx.lineWidth = bord; + roundRect( + canctx, + bord / 2, + bord / 2, + width - bord, + height - bord, + cornerRadius, + ); + if (!borderOnly) { + canctx.fill(); } - ctx.drawImage(cachedGraphics[key], tlx, tly, width, height); - // It's not easy to have a 1px gap between bricks without antialiasing + canctx.stroke(); + + cachedGraphics[key] = can; + } + ctx.drawImage(cachedGraphics[key], tlx, tly, width, height); + // It's not easy to have a 1px gap between bricks without antialiasing } export function roundRect( - ctx: CanvasRenderingContext2D, - x: number, - y: number, - width: number, - height: number, - radius: number, + ctx: CanvasRenderingContext2D, + x: number, + y: number, + width: number, + height: number, + radius: number, ) { - ctx.beginPath(); - ctx.moveTo(x + radius, y); - ctx.lineTo(x + width - radius, y); - ctx.quadraticCurveTo(x + width, y, x + width, y + radius); - ctx.lineTo(x + width, y + height - radius); - ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); - ctx.lineTo(x + radius, y + height); - ctx.quadraticCurveTo(x, y + height, x, y + height - radius); - ctx.lineTo(x, y + radius); - ctx.quadraticCurveTo(x, y, x + radius, y); - ctx.closePath(); + ctx.beginPath(); + ctx.moveTo(x + radius, y); + ctx.lineTo(x + width - radius, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + radius); + ctx.lineTo(x + width, y + height - radius); + ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); + ctx.lineTo(x + radius, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - radius); + ctx.lineTo(x, y + radius); + ctx.quadraticCurveTo(x, y, x + radius, y); + ctx.closePath(); } export function drawIMG( - ctx: CanvasRenderingContext2D, - img: HTMLImageElement, - size: number, - x: number, - y: number, + ctx: CanvasRenderingContext2D, + img: HTMLImageElement, + size: number, + x: number, + y: number, ) { - const key = "svg" + img + "_" + size + "_" + img.complete; + const key = "svg" + img + "_" + size + "_" + img.complete; - if (!cachedGraphics[key]) { - const can = document.createElement("canvas"); - can.width = size; - can.height = size; + if (!cachedGraphics[key]) { + const can = document.createElement("canvas"); + can.width = size; + can.height = size; - const canctx = can.getContext("2d") as CanvasRenderingContext2D; + const canctx = can.getContext("2d") as CanvasRenderingContext2D; - const ratio = size / Math.max(img.width, img.height); - const w = img.width * ratio; - const h = img.height * ratio; - canctx.drawImage(img, (size - w) / 2, (size - h) / 2, w, h); + const ratio = size / Math.max(img.width, img.height); + const w = img.width * ratio; + const h = img.height * ratio; + canctx.drawImage(img, (size - w) / 2, (size - h) / 2, w, h); - cachedGraphics[key] = can; - } - ctx.drawImage( - cachedGraphics[key], - Math.round(x - size / 2), - Math.round(y - size / 2), - ); + cachedGraphics[key] = can; + } + ctx.drawImage( + cachedGraphics[key], + Math.round(x - size / 2), + Math.round(y - size / 2), + ); } export function drawText( - ctx: CanvasRenderingContext2D, - text: string, - color: colorString, - fontSize: number, - x: number, - y: number, - left = false, + ctx: CanvasRenderingContext2D, + text: string, + color: colorString, + fontSize: number, + x: number, + y: number, + left = false, ) { - const key = "text" + text + "_" + color + "_" + fontSize + "_" + left; + const key = "text" + text + "_" + color + "_" + fontSize + "_" + left; - if (!cachedGraphics[key]) { - const can = document.createElement("canvas"); - can.width = fontSize * text.length; - can.height = fontSize; - const canctx = can.getContext("2d") as CanvasRenderingContext2D; - canctx.fillStyle = color; - canctx.textAlign = left ? "left" : "center"; - canctx.textBaseline = "middle"; - canctx.font = fontSize + "px monospace"; + if (!cachedGraphics[key]) { + const can = document.createElement("canvas"); + can.width = fontSize * text.length; + can.height = fontSize; + const canctx = can.getContext("2d") as CanvasRenderingContext2D; + canctx.fillStyle = color; + canctx.textAlign = left ? "left" : "center"; + canctx.textBaseline = "middle"; + canctx.font = fontSize + "px monospace"; - canctx.fillText(text, left ? 0 : can.width / 2, can.height / 2, can.width); + canctx.fillText(text, left ? 0 : can.width / 2, can.height / 2, can.width); - cachedGraphics[key] = can; - } - ctx.drawImage( - cachedGraphics[key], - left ? x : Math.round(x - cachedGraphics[key].width / 2), - Math.round(y - cachedGraphics[key].height / 2), - ); + cachedGraphics[key] = can; + } + ctx.drawImage( + cachedGraphics[key], + left ? x : Math.round(x - cachedGraphics[key].width / 2), + Math.round(y - cachedGraphics[key].height / 2), + ); } export const scoreDisplay = document.getElementById( - "score", + "score", ) as HTMLButtonElement; const menuLabel = document.getElementById("menuLabel") as HTMLButtonElement; @@ -1002,8 +1022,8 @@ const emptyArray = []; const redBorderDash = [5, 5]; export function getDashOffset(gameState: GameState) { - if (isOptionOn("basic")) { - return 0; - } - return Math.floor(((gameState.levelTime % 500) / 500) * 10) % 10; + if (isOptionOn("basic")) { + return 0; + } + return Math.floor(((gameState.levelTime % 500) / 500) * 10) % 10; } diff --git a/src/types.d.ts b/src/types.d.ts index fd09e02..36df8f7 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -83,7 +83,7 @@ export type Coin = { weight: number; destroyed?: boolean; collidedLastFrame?: boolean; - metamorphosisPoints:number; + metamorphosisPoints: number; }; export type Ball = { x: number; @@ -238,8 +238,12 @@ export type GameState = { coins: ReusableArray; // Bricks that should respawn destroyed - respawns: ReusableArray<{ index: number; color: string ; time:number; - destroyed?: boolean;}>; + respawns: ReusableArray<{ + index: number; + color: string; + time: number; + destroyed?: boolean; + }>; levelStartScore: number; levelMisses: number; @@ -280,14 +284,14 @@ export type GameState = { rerolls: number; loop: number; baseCombo: number; - levelsPerLoop:number; + levelsPerLoop: number; }; export type RunParams = { level?: string; levelToAvoid?: string; perks?: Partial; - levelsPerLoop?:number; + levelsPerLoop?: number; }; export type OptionDef = { default: boolean; diff --git a/src/upgrades.ts b/src/upgrades.ts index 70f61b1..a69012b 100644 --- a/src/upgrades.ts +++ b/src/upgrades.ts @@ -1,6 +1,6 @@ import { t } from "./i18n/i18n"; -import {comboKeepingRate} from "./pure_functions"; +import { comboKeepingRate } from "./pure_functions"; export const rawUpgrades = [ { @@ -49,7 +49,7 @@ export const rawUpgrades = [ id: "slow_down", max: 2, name: t("upgrades.slow_down.name"), - help: (lvl:number) => t("upgrades.slow_down.help",{ lvl }), + help: (lvl: number) => t("upgrades.slow_down.help", { lvl }), fullHelp: t("upgrades.slow_down.fullHelp"), }, { @@ -84,7 +84,7 @@ export const rawUpgrades = [ max: 1, name: t("upgrades.left_is_lava.name"), - help: (lvl:number) => t("upgrades.left_is_lava.help",{ lvl }), + help: (lvl: number) => t("upgrades.left_is_lava.help", { lvl }), fullHelp: t("upgrades.left_is_lava.fullHelp"), }, { @@ -95,7 +95,7 @@ export const rawUpgrades = [ giftable: true, max: 1, name: t("upgrades.right_is_lava.name"), - help: (lvl:number) => t("upgrades.right_is_lava.help",{ lvl }), + help: (lvl: number) => t("upgrades.right_is_lava.help", { lvl }), fullHelp: t("upgrades.right_is_lava.fullHelp"), }, { @@ -106,7 +106,7 @@ export const rawUpgrades = [ giftable: true, max: 1, name: t("upgrades.top_is_lava.name"), - help: (lvl:number) => t("upgrades.top_is_lava.help",{ lvl }), + help: (lvl: number) => t("upgrades.top_is_lava.help", { lvl }), fullHelp: t("upgrades.top_is_lava.fullHelp"), }, { @@ -195,7 +195,7 @@ export const rawUpgrades = [ giftable: true, max: 1, name: t("upgrades.picky_eater.name"), - help: (lvl: number) => t("upgrades.picky_eater.help",{lvl}), + help: (lvl: number) => t("upgrades.picky_eater.help", { lvl }), fullHelp: t("upgrades.picky_eater.fullHelp"), }, { @@ -206,7 +206,7 @@ export const rawUpgrades = [ id: "metamorphosis", max: 1, name: t("upgrades.metamorphosis.name"), - help: (lvl: number) => t("upgrades.metamorphosis.help",{lvl}), + help: (lvl: number) => t("upgrades.metamorphosis.help", { lvl }), fullHelp: t("upgrades.metamorphosis.fullHelp"), }, { @@ -217,7 +217,7 @@ export const rawUpgrades = [ giftable: true, max: 1, name: t("upgrades.compound_interest.name"), - help: (lvl: number) => t("upgrades.compound_interest.help",{lvl}), + help: (lvl: number) => t("upgrades.compound_interest.help", { lvl }), fullHelp: t("upgrades.compound_interest.fullHelp"), }, { @@ -289,7 +289,10 @@ export const rawUpgrades = [ id: "soft_reset", max: 3, name: t("upgrades.soft_reset.name"), - help: (lvl: number) => t("upgrades.soft_reset.help", { percent: Math.round(comboKeepingRate(lvl) * 100)}), + help: (lvl: number) => + t("upgrades.soft_reset.help", { + percent: Math.round(comboKeepingRate(lvl) * 100), + }), fullHelp: t("upgrades.soft_reset.fullHelp"), }, { @@ -354,9 +357,9 @@ export const rawUpgrades = [ name: t("upgrades.sturdy_bricks.name"), help: (lvl: number) => // lvl == 1 - t("upgrades.sturdy_bricks.help",{lvl, percent:lvl*10}), - // ? - // : t("upgrades.sturdy_bricks.help_plural"), + t("upgrades.sturdy_bricks.help", { lvl, percent: lvl * 10 }), + // ? + // : t("upgrades.sturdy_bricks.help_plural"), fullHelp: t("upgrades.sturdy_bricks.fullHelp"), }, { @@ -368,7 +371,10 @@ export const rawUpgrades = [ max: 4, name: t("upgrades.respawn.name"), help: (lvl: number) => - t("upgrades.respawn.help",{percent:Math.floor(100*comboKeepingRate(lvl)),delay:(3/lvl).toFixed(2)}), + t("upgrades.respawn.help", { + percent: Math.floor(100 * comboKeepingRate(lvl)), + delay: (3 / lvl).toFixed(2), + }), fullHelp: t("upgrades.respawn.fullHelp"), }, { @@ -378,7 +384,7 @@ export const rawUpgrades = [ id: "one_more_choice", max: 3, name: t("upgrades.one_more_choice.name"), - help: (lvl: number) => t("upgrades.one_more_choice.help", {lvl}), + help: (lvl: number) => t("upgrades.one_more_choice.help", { lvl }), fullHelp: t("upgrades.one_more_choice.fullHelp"), }, { @@ -390,7 +396,7 @@ export const rawUpgrades = [ max: 2, adventure: false, name: t("upgrades.instant_upgrade.name"), - help: (lvl: number) => t("upgrades.instant_upgrade.help",{lvl}), + help: (lvl: number) => t("upgrades.instant_upgrade.help", { lvl }), fullHelp: t("upgrades.instant_upgrade.fullHelp"), }, { @@ -422,7 +428,7 @@ export const rawUpgrades = [ id: "asceticism", max: 1, name: t("upgrades.asceticism.name"), - help: (lvl: number) => t("upgrades.asceticism.help",{combo:lvl*3}), + help: (lvl: number) => t("upgrades.asceticism.help", { combo: lvl * 3 }), fullHelp: t("upgrades.asceticism.fullHelp"), }, { @@ -433,9 +439,10 @@ export const rawUpgrades = [ id: "unbounded", max: 1, name: t("upgrades.unbounded.name"), - help: (lvl: number) => lvl > 1 ? - t("upgrades.unbounded.help_no_ceiling",{lvl}): - t("upgrades.unbounded.help",{lvl}), + help: (lvl: number) => + lvl > 1 + ? t("upgrades.unbounded.help_no_ceiling", { lvl }) + : t("upgrades.unbounded.help", { lvl }), fullHelp: t("upgrades.unbounded.fullHelp"), }, { @@ -446,7 +453,10 @@ export const rawUpgrades = [ id: "shunt", max: 3, name: t("upgrades.shunt.name"), - help: (lvl: number) => t("upgrades.shunt.help", { percent: Math.round(comboKeepingRate(lvl) * 100) }), + help: (lvl: number) => + t("upgrades.shunt.help", { + percent: Math.round(comboKeepingRate(lvl) * 100), + }), fullHelp: t("upgrades.shunt.fullHelp"), }, { @@ -499,7 +509,7 @@ export const rawUpgrades = [ id: "zen", max: 1, name: t("upgrades.zen.name"), - help: (lvl: number) => t("upgrades.zen.help",{lvl}), + help: (lvl: number) => t("upgrades.zen.help", { lvl }), fullHelp: t("upgrades.zen.fullHelp"), }, { @@ -510,9 +520,9 @@ export const rawUpgrades = [ max: 1, name: t("upgrades.sacrifice.name"), help: (lvl: number) => - lvl==1 ? - t("upgrades.sacrifice.help_l1"): - t("upgrades.sacrifice.help_over",{lvl}), + lvl == 1 + ? t("upgrades.sacrifice.help_l1") + : t("upgrades.sacrifice.help_over", { lvl }), fullHelp: t("upgrades.sacrifice.fullHelp"), },