From 9731d694f3cfea7ac042eb0d72cf142a815be8df Mon Sep 17 00:00:00 2001 From: Renan LE CARO Date: Sat, 3 May 2025 19:41:45 +0200 Subject: [PATCH] wip --- Readme.md | 2 + dist/index.html | 1392 +++++++++++++++++++------------------- src/game.less | 33 +- src/game.ts | 6 +- src/gameStateMutators.ts | 41 +- src/game_utils.ts | 12 + src/render.ts | 14 +- src/toast.ts | 13 +- 8 files changed, 791 insertions(+), 722 deletions(-) diff --git a/Readme.md b/Readme.md index 2896eb5..0084314 100644 --- a/Readme.md +++ b/Readme.md @@ -14,6 +14,8 @@ Break colourful bricks, catch bouncing coins and select powerful upgrades ! # Changelog ## To do +- loosing ball is ok if in win timeout period +- ## Done - delayed start on mobile diff --git a/dist/index.html b/dist/index.html index 065c5f0..0cae984 100644 --- a/dist/index.html +++ b/dist/index.html @@ -607,12 +607,15 @@ h2.histogram-title strong { } .toast.big { - opacity: .9; + opacity: .8; + text-shadow: 2px 0 #000, -2px 0 #000, 0 2px #000, 0 -2px #000, 1px 1px #000, -1px -1px #000, 1px -1px #000, -1px 1px #000; background: none; border: none; font-size: 60px; + font-weight: bold; top: 50vh; left: 50vw; + transform: translate(-50%, -50%); } .gridEdit > div > span, .palette > span { @@ -1011,8 +1014,11 @@ function startPlayCountDown() { (0, _toast.toast)("GO", "big"); play(); }, 3000)); + timers.push(setTimeout(()=>(0, _toast.clearToasts)(), 3500)); } function stopPlayCountDown() { + if (!timers.length) return; + (0, _toast.clearToasts)(); timers.forEach((id)=>clearTimeout(id)); timers.length = 0; } @@ -2949,18 +2955,23 @@ async function askForPersistentStorage() { var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js"); parcelHelpers.defineInteropFlag(exports); parcelHelpers.export(exports, "toast", ()=>toast); +parcelHelpers.export(exports, "clearToasts", ()=>clearToasts); let div = document.createElement("div"); div.classList = "hidden toast"; document.body.appendChild(div); let timeout; function toast(html, className = "") { + clearToasts(); div.classList = "toast visible " + className; div.innerHTML = html; - if (timeout) clearTimeout(timeout); - timeout = setTimeout(()=>{ + timeout = setTimeout(clearToasts, 1500); +} +function clearToasts() { + if (timeout) { + clearTimeout(timeout); timeout = undefined; - div.classList = "hidden toast"; - }, 1500); + } + div.classList = "hidden toast"; } },{"@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"gkKU3":[function(require,module,exports,__globalThis) { @@ -3512,11 +3523,15 @@ parcelHelpers.export(exports, "getCornerOffset", ()=>getCornerOffset); parcelHelpers.export(exports, "isInWebView", ()=>isInWebView); parcelHelpers.export(exports, "hoursSpentPlaying", ()=>hoursSpentPlaying); parcelHelpers.export(exports, "escapeAttribute", ()=>escapeAttribute); +parcelHelpers.export(exports, "canvasCenterX", ()=>canvasCenterX); +parcelHelpers.export(exports, "zoneLeftBorderX", ()=>zoneLeftBorderX); +parcelHelpers.export(exports, "zoneRightBorderX", ()=>zoneRightBorderX); var _loadGameData = require("./loadGameData"); var _i18N = require("./i18n/i18n"); var _pureFunctions = require("./pure_functions"); var _settings = require("./settings"); var _options = require("./options"); +var _render = require("./render"); function describeLevel(level) { let bricks = 0, colors = new Set(), bombs = 0; level.bricks.forEach((color)=>{ @@ -3755,55 +3770,643 @@ function hoursSpentPlaying() { function escapeAttribute(str) { return str.replace(/&/gi, "&").replace(/gameCanvas); +parcelHelpers.export(exports, "ctx", ()=>ctx); +parcelHelpers.export(exports, "bombSVG", ()=>bombSVG); +parcelHelpers.export(exports, "background", ()=>background); +parcelHelpers.export(exports, "backgroundCanvas", ()=>backgroundCanvas); +parcelHelpers.export(exports, "haloCanvas", ()=>haloCanvas); +parcelHelpers.export(exports, "getHaloScale", ()=>getHaloScale); +parcelHelpers.export(exports, "render", ()=>render); +parcelHelpers.export(exports, "renderAllBricks", ()=>renderAllBricks); +parcelHelpers.export(exports, "drawPuck", ()=>drawPuck); +parcelHelpers.export(exports, "drawBall", ()=>drawBall); +parcelHelpers.export(exports, "drawCoin", ()=>drawCoin); +parcelHelpers.export(exports, "drawFuzzyBall", ()=>drawFuzzyBall); +parcelHelpers.export(exports, "drawBrick", ()=>drawBrick); +parcelHelpers.export(exports, "roundRect", ()=>roundRect); +parcelHelpers.export(exports, "drawIMG", ()=>drawIMG); +parcelHelpers.export(exports, "drawText", ()=>drawText); +parcelHelpers.export(exports, "scoreDisplay", ()=>scoreDisplay); +parcelHelpers.export(exports, "getDashOffset", ()=>getDashOffset); +var _gameStateMutators = require("./gameStateMutators"); +var _gameUtils = require("./game_utils"); +var _i18N = require("./i18n/i18n"); +var _game = require("./game"); +var _options = require("./options"); +var _pureFunctions = require("./pure_functions"); +const gameCanvas = document.getElementById("game"); +const ctx = gameCanvas.getContext("2d", { + alpha: false +}); +const bombSVG = document.createElement("img"); +bombSVG.src = "data:image/svg+xml;base64," + btoa(` + +`); +bombSVG.onload = ()=>(0, _game.gameState).needsRender = true; +const background = document.createElement("img"); +background.onload = ()=>(0, _game.gameState).needsRender = true; +const backgroundCanvas = document.createElement("canvas"); +const haloCanvas = document.createElement("canvas"); +const haloCanvasCtx = haloCanvas.getContext("2d", { + alpha: false +}); +function getHaloScale() { + return 16 * ((0, _options.isOptionOn)("precise_lighting") ? 1 : 2); } - -},{"b04459cc43e56e8c":"17ciJ","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"17ciJ":[function(require,module,exports,__globalThis) { -module.exports = require("9c7c7951fd7c4db6").getBundleURL('arAGi') + "sw-b71.41cdff1b.js"; - -},{"9c7c7951fd7c4db6":"lgJ39"}],"lgJ39":[function(require,module,exports,__globalThis) { -"use strict"; -var bundleURL = {}; -function getBundleURLCached(id) { - var value = bundleURL[id]; - if (!value) { - value = getBundleURL(); - bundleURL[id] = value; +let framesCounter = 0; +function render(gameState) { + framesCounter++; + (0, _game.startWork)("render:init"); + const level = (0, _gameUtils.currentLevelInfo)(gameState); + const hasCombo = gameState.combo > (0, _gameStateMutators.baseCombo)(gameState); + const { width, height } = gameCanvas; + if (!width || !height) return; + if (gameState.currentLevel || gameState.levelTime) menuLabel.innerText = (0, _i18N.t)("play.current_lvl", { + level: gameState.currentLevel + 1, + max: (0, _gameUtils.max_levels)(gameState) + }); + else menuLabel.innerText = (0, _i18N.t)("play.menu_label"); + const catchRate = gameState.levelSpawnedCoins ? gameState.levelCoughtCoins / (gameState.levelSpawnedCoins || 1) : // gameState.levelSpawnedCoins + 1; + (0, _game.startWork)("render:scoreDisplay"); + scoreDisplay.innerHTML = ((0, _options.isOptionOn)("show_fps") || gameState.startParams.computer_controlled ? ` + + ${0, _game.lastMeasuredFPS} FPS + / + ` : "") + ((0, _options.isOptionOn)("show_stats") ? ` + (0, _pureFunctions.catchRateGood) / 100 && "good" || ""}" data-tooltip="${(0, _i18N.t)("play.stats.coins_catch_rate")}"> + ${Math.floor(catchRate * 100)}% + / + + ${Math.ceil(gameState.levelTime / 1000)}s + / + + ${gameState.levelMisses} M + / + ` : "") + `$${gameState.score}`; + scoreDisplay.classList[gameState.startParams.computer_controlled ? "add" : "remove"]("computer_controlled"); + scoreDisplay.classList[gameState.lastScoreIncrease > gameState.levelTime - 500 ? "add" : "remove"]("active"); + // Clear + if (!(0, _options.isOptionOn)("basic") && level.svg && level.color === "#000000") { + const skipN = (0, _options.isOptionOn)("probabilistic_lighting") && (0, _gameStateMutators.liveCount)(gameState.coins) > 150 ? 3 : 0; + const shouldSkip = (index)=>skipN ? (framesCounter + index) % (skipN + 1) !== 0 : false; + const haloScale = getHaloScale(); + (0, _game.startWork)("render:halo:clear"); + haloCanvasCtx.globalCompositeOperation = "source-over"; + haloCanvasCtx.globalAlpha = skipN ? 0.1 : 0.99; + haloCanvasCtx.fillStyle = level.color; + haloCanvasCtx.fillRect(0, 0, width / haloScale, height / haloScale); + const brightness = (0, _options.isOptionOn)("extra_bright") ? 3 : 1; + haloCanvasCtx.globalCompositeOperation = "lighten"; + haloCanvasCtx.globalAlpha = 0.1 + 5 / ((0, _gameStateMutators.liveCount)(gameState.coins) + 10); + (0, _game.startWork)("render:halo:coins"); + (0, _gameStateMutators.forEachLiveOne)(gameState.coins, (coin, index)=>{ + if (shouldSkip(index)) return; + const color = (0, _gameUtils.getCoinRenderColor)(gameState, coin); + drawFuzzyBall(haloCanvasCtx, color, gameState.coinSize * 2 * brightness / haloScale, coin.x / haloScale, coin.y / haloScale); + }); + (0, _game.startWork)("render:halo:balls"); + gameState.balls.forEach((ball, index)=>{ + if (shouldSkip(index)) return; + haloCanvasCtx.globalAlpha = 0.3 * (1 - (0, _pureFunctions.ballTransparency)(ball, gameState)); + drawFuzzyBall(haloCanvasCtx, gameState.ballsColor, gameState.ballSize * 2 * brightness / haloScale, ball.x / haloScale, ball.y / haloScale); + }); + (0, _game.startWork)("render:halo:bricks"); + haloCanvasCtx.globalAlpha = 0.05; + gameState.bricks.forEach((color, index)=>{ + if (!color) return; + if (shouldSkip(index)) return; + const x = (0, _gameUtils.brickCenterX)(gameState, index), y = (0, _gameUtils.brickCenterY)(gameState, index); + drawFuzzyBall(haloCanvasCtx, color == "black" ? "#666666" : color, // Perf could really go down there because of the size of the halo + Math.min(200, gameState.brickWidth * 1.5 * brightness) / haloScale, x / haloScale, y / haloScale); + }); + (0, _game.startWork)("render:halo:particles"); + haloCanvasCtx.globalCompositeOperation = "screen"; + (0, _gameStateMutators.forEachLiveOne)(gameState.particles, (flash, index)=>{ + if (shouldSkip(index)) return; + const { x, y, time, color, size, duration } = flash; + const elapsed = gameState.levelTime - time; + haloCanvasCtx.globalAlpha = 0.1 * Math.min(1, 2 - elapsed / duration * 2); + drawFuzzyBall(haloCanvasCtx, color, size * 3 * brightness / haloScale, x / haloScale, y / haloScale); + }); + (0, _game.startWork)("render:halo:scale_up"); + ctx.globalAlpha = 1; + ctx.globalCompositeOperation = "source-over"; + ctx.imageSmoothingQuality = "high"; + ctx.imageSmoothingEnabled = (0, _options.isOptionOn)("smooth_lighting") || false; + ctx.drawImage(haloCanvas, 0, 0, width, height); + ctx.imageSmoothingEnabled = false; + (0, _game.startWork)("render:halo:pattern"); + ctx.globalAlpha = 1; + ctx.globalCompositeOperation = "multiply"; + if (level.svg && background.width && background.complete) { + if (backgroundCanvas.title !== level.name) { + backgroundCanvas.title = level.name; + backgroundCanvas.width = gameState.canvasWidth; + backgroundCanvas.height = gameState.canvasHeight; + const bgctx = backgroundCanvas.getContext("2d"); + bgctx.globalCompositeOperation = "source-over"; + 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 = "#FFFFFF"; + 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.globalCompositeOperation = "screen"; + bgctx.fillStyle = pattern; + bgctx.fillRect(0, 0, width, height); + } + } + } + ctx.globalCompositeOperation = "darken"; + ctx.drawImage(backgroundCanvas, 0, 0); + } else { + // Background not loaded yes + ctx.fillStyle = "#000"; + ctx.fillRect(0, 0, width, height); + } + } else { + (0, _game.startWork)("render:halo-basic"); + ctx.globalAlpha = 1; + ctx.globalCompositeOperation = "source-over"; + ctx.fillStyle = level.color || "#000"; + ctx.fillRect(0, 0, width, height); + (0, _gameStateMutators.forEachLiveOne)(gameState.particles, (flash)=>{ + const { x, y, time, color, size, duration } = flash; + const elapsed = gameState.levelTime - time; + ctx.globalAlpha = Math.min(1, 2 - elapsed / duration * 2); + drawBall(ctx, color, size, x, y); + }); } - return value; -} -function getBundleURL() { - try { - throw new Error(); - } catch (err) { - var matches = ('' + err.stack).match(/(https?|file|ftp|(chrome|moz|safari-web)-extension):\/\/[^)\n]+/g); - if (matches) // The first two stack frames will be this function and getBundleURLCached. - // Use the 3rd one, which will be a runtime in the original bundle. - return getBaseURL(matches[2]); + (0, _game.startWork)("render:explosionshake"); + ctx.globalAlpha = 1; + ctx.globalCompositeOperation = "source-over"; + const lastExplosionDelay = gameState.levelTime - gameState.lastExplosion + 5; + const shaked = lastExplosionDelay < 200 && !(0, _options.isOptionOn)("basic") && // Otherwise, if you pause after an explosion, moving the mouses shakes the picture + gameState.running; + if (shaked) { + const amplitude = (gameState.perks.bigger_explosions + 1) * 50 / lastExplosionDelay; + ctx.translate(Math.sin(Date.now()) * amplitude, Math.sin(Date.now() + 36) * amplitude); } - return '/'; + (0, _game.startWork)("render:coins"); + // Coins + ctx.globalAlpha = 1; + (0, _gameStateMutators.forEachLiveOne)(gameState.coins, (coin)=>{ + const color = (0, _gameUtils.getCoinRenderColor)(gameState, coin); + const hollow = gameState.perks.metamorphosis && !coin.metamorphosisPoints; + ctx.globalCompositeOperation = "source-over"; + drawCoin(ctx, hollow ? "transparent" : color, coin.size, coin.x, coin.y, // Red border around coins with asceticism + hasCombo && gameState.perks.asceticism && "#FF0000" || // Gold coins + // (color === "#ffd300" && "#ffd300") || + hollow && color || gameState.level.color, coin.a); + }); + (0, _game.startWork)("render:ball shade"); + // Black shadow around balls + ctx.globalCompositeOperation = "source-over"; + gameState.balls.forEach((ball)=>{ + ctx.globalAlpha = Math.min(0.8, (0, _gameStateMutators.liveCount)(gameState.coins) / 20) * (1 - (0, _pureFunctions.ballTransparency)(ball, gameState)); + drawBall(ctx, level.color || "#000", gameState.ballSize * 6, ball.x, ball.y); + }); + (0, _game.startWork)("render:bricks"); + ctx.globalCompositeOperation = "source-over"; + renderAllBricks(); + (0, _game.startWork)("render:lights"); + ctx.globalCompositeOperation = "screen"; + (0, _gameStateMutators.forEachLiveOne)(gameState.lights, (flash)=>{ + const { x, y, time, color, size, duration } = flash; + const elapsed = gameState.levelTime - time; + ctx.globalAlpha = Math.min(1, 2 - elapsed / duration * 2) * 0.5; + drawBrick(gameState, ctx, color, x, y, -1, gameState.perks.clairvoyant >= 2); + }); + (0, _game.startWork)("render:texts"); + ctx.globalCompositeOperation = "screen"; + (0, _gameStateMutators.forEachLiveOne)(gameState.texts, (flash)=>{ + const { x, y, time, color, size, duration } = flash; + const elapsed = gameState.levelTime - time; + ctx.globalAlpha = Math.max(0, Math.min(1, 2 - elapsed / duration * 2)); + ctx.globalCompositeOperation = "source-over"; + drawText(ctx, flash.text, color, size, x, y - elapsed / 10); + }); + (0, _game.startWork)("render:particles"); + (0, _gameStateMutators.forEachLiveOne)(gameState.particles, (particle)=>{ + const { x, y, time, color, size, duration } = particle; + const elapsed = gameState.levelTime - time; + ctx.globalAlpha = Math.max(0, Math.min(1, 2 - elapsed / duration * 2)); + ctx.globalCompositeOperation = "screen"; + drawBall(ctx, color, size, x, y); + }); + // + (0, _game.startWork)("render:extra_life"); + if (gameState.perks.extra_life) { + ctx.globalAlpha = gameState.balls.length > 1 ? 0.2 : 1; + ctx.globalCompositeOperation = "source-over"; + ctx.fillStyle = gameState.puckColor; + for(let i = 0; i < gameState.perks.extra_life; i++)ctx.fillRect(gameState.offsetXRoundedDown, gameState.gameZoneHeight - gameState.puckHeight / 2 + 2 * i, gameState.gameZoneWidthRoundedUp, 1); + } + (0, _game.startWork)("render:balls"); + ctx.globalAlpha = 1; + ctx.globalCompositeOperation = "source-over"; + gameState.balls.forEach((ball)=>{ + const drawingColor = gameState.ballsColor; + const ballAlpha = 1 - (0, _pureFunctions.ballTransparency)(ball, gameState); + ctx.globalAlpha = ballAlpha; + // The white border around is to distinguish colored balls from coins/bg + drawBall(ctx, drawingColor, gameState.ballSize, ball.x, ball.y, gameState.puckColor); + if ((0, _gameUtils.telekinesisEffectRate)(gameState, ball) || (0, _gameUtils.yoyoEffectRate)(gameState, ball)) { + ctx.beginPath(); + ctx.moveTo(gameState.puckPosition, gameState.gameZoneHeight); + ctx.globalAlpha = (0, _pureFunctions.clamp)(Math.max((0, _gameUtils.telekinesisEffectRate)(gameState, ball), (0, _gameUtils.yoyoEffectRate)(gameState, ball)) * ballAlpha, 0, 1); + 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); + } + ctx.globalAlpha = 1; + if (gameState.perks.clairvoyant && gameState.ballStickToPuck || gameState.perks.steering > 1 && !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(); + } + }); + (0, _game.startWork)("render:puck"); + ctx.globalAlpha = (0, _gameUtils.isMovingWhilePassiveIncome)(gameState) ? 0.2 : 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); + (0, _game.startWork)("render:combotext"); + const spawns = (0, _pureFunctions.coinsBoostedCombo)(gameState); + if (spawns > 1 && !(0, _gameUtils.isMovingWhilePassiveIncome)(gameState)) { + ctx.globalCompositeOperation = "source-over"; + ctx.globalAlpha = 1; + const comboText = spawns.toString(); + const comboTextWidth = comboText.length * gameState.puckHeight / 1.8; + const totalWidth = comboTextWidth + gameState.coinSize * 2; + const left = gameState.puckPosition - totalWidth / 2; + ctx.globalAlpha = gameState.combo > (0, _gameStateMutators.baseCombo)(gameState) ? 1 : 0.3; + if (totalWidth < gameState.puckWidth) { + drawText(ctx, comboText, "#000", gameState.puckHeight, left + gameState.coinSize * 1.5, gameState.gameZoneHeight - gameState.puckHeight / 2, true); + ctx.globalAlpha = 1; + drawCoin(ctx, "#ffd300", gameState.coinSize, left + gameState.coinSize / 2, gameState.gameZoneHeight - gameState.puckHeight / 2, "#ffd300", 0); + } else drawText(ctx, comboTextWidth > gameState.puckWidth ? gameState.combo.toString() : comboText, "#000", comboTextWidth > gameState.puckWidth ? 12 : 20, gameState.puckPosition, gameState.gameZoneHeight - gameState.puckHeight / 2, false); + } + (0, _game.startWork)("render:borders"); + // Borders + ctx.globalCompositeOperation = "source-over"; + ctx.globalAlpha = 1; + let redLeftSide = hasCombo && (gameState.perks.left_is_lava || gameState.perks.trampoline); + let redRightSide = hasCombo && (gameState.perks.right_is_lava || gameState.perks.trampoline); + let redTop = hasCombo && (gameState.perks.top_is_lava || gameState.perks.trampoline); + if (gameState.offsetXRoundedDown) { + // draw outside of gaming area to avoid capturing borders in recordings + if (gameState.perks.left_is_lava < 2) drawStraightLine(ctx, gameState, redLeftSide && "#FF0000" || "#FFFFFF", (0, _gameUtils.zoneLeftBorderX)(gameState), 0, (0, _gameUtils.zoneLeftBorderX)(gameState), height, 1); + if (gameState.perks.right_is_lava < 2) drawStraightLine(ctx, gameState, redRightSide && "#FF0000" || "#FFFFFF", (0, _gameUtils.zoneRightBorderX)(gameState), 0, (0, _gameUtils.zoneRightBorderX)(gameState), height, 1); + } else { + if (gameState.perks.left_is_lava < 2) drawStraightLine(ctx, gameState, redLeftSide && "#FF0000" || "", 0, 0, 0, height, 1); + if (gameState.perks.right_is_lava < 2) drawStraightLine(ctx, gameState, redRightSide && "#FF0000" || "", width - 1, 0, width - 1, height, 1); + } + if (redTop && gameState.perks.top_is_lava < 2) drawStraightLine(ctx, gameState, "#FF0000", (0, _gameUtils.zoneLeftBorderX)(gameState), 1, (0, _gameUtils.zoneRightBorderX)(gameState), 1, 1); + (0, _game.startWork)("render:bottom_line"); + ctx.globalAlpha = 1; + const corner = (0, _gameUtils.getCornerOffset)(gameState); + const bottomLineIsRed = hasCombo && gameState.perks.compound_interest; + drawStraightLine(ctx, gameState, bottomLineIsRed && "#FF0000" || (0, _options.isOptionOn)("mobile-mode") && "#FFFFFF" || corner && "#FFFFFF" || "", gameState.offsetXRoundedDown - corner, gameState.gameZoneHeight - 1, width - gameState.offsetXRoundedDown + corner, gameState.gameZoneHeight - 1, bottomLineIsRed ? 1 : 0.5); + (0, _game.startWork)("render:contrast"); + if (!(0, _options.isOptionOn)("basic") && (0, _options.isOptionOn)("contrast") && level.svg && level.color === "#000000") { + ctx.imageSmoothingEnabled = (0, _options.isOptionOn)("smooth_lighting") || false; + if ((0, _options.isOptionOn)("probabilistic_lighting")) { + ctx.globalAlpha = 1; + ctx.globalCompositeOperation = "soft-light"; + } else { + haloCanvasCtx.fillStyle = "#FFFFFF"; + haloCanvasCtx.globalAlpha = 0.25; + haloCanvasCtx.globalCompositeOperation = "screen"; + haloCanvasCtx.fillRect(0, 0, haloCanvas.width, haloCanvas.height); + ctx.globalAlpha = 1; + ctx.globalCompositeOperation = "overlay"; + } + ctx.drawImage(haloCanvas, 0, 0, width, height); + ctx.imageSmoothingEnabled = false; + } + (0, _game.startWork)("render:text_under_puck"); + ctx.globalCompositeOperation = "source-over"; + ctx.globalAlpha = 1; + if ((0, _options.isOptionOn)("mobile-mode") && gameState.startParams.computer_controlled) drawText(ctx, "breakout.lecaro.me?autoplay", gameState.puckColor, gameState.puckHeight, gameState.canvasWidth / 2, gameState.gameZoneHeight + (gameState.canvasHeight - gameState.gameZoneHeight) / 2); + if ((0, _options.isOptionOn)("mobile-mode") && !gameState.running) drawText(ctx, (0, _i18N.t)("play.mobile_press_to_play"), gameState.puckColor, gameState.puckHeight, gameState.canvasWidth / 2, gameState.gameZoneHeight + (gameState.canvasHeight - gameState.gameZoneHeight) / 2); + (0, _game.startWork)("render:askForWakeLock"); + askForWakeLock(gameState); + (0, _game.startWork)("render:resetTransform"); + if (shaked) ctx.resetTransform(); } -function getBaseURL(url) { - return ('' + url).replace(/^((?:https?|file|ftp|(chrome|moz|safari-web)-extension):\/\/.+)\/[^/]+$/, '$1') + '/'; +function drawStraightLine(ctx, gameState, mode, x1, y1, x2, y2, alpha = 1) { + ctx.globalAlpha = alpha; + if (!mode) return; + x1 = Math.round(x1); + y1 = Math.round(y1); + x2 = Math.round(x2); + y2 = Math.round(y2); + if (mode == "#FF0000") { + ctx.strokeStyle = "red"; + ctx.lineDashOffset = getDashOffset(gameState); + ctx.lineWidth = 2; + ctx.setLineDash(redBorderDash); + ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.stroke(); + ctx.setLineDash(emptyArray); + ctx.lineWidth = 1; + } else { + ctx.fillStyle = mode; + ctx.fillRect(Math.min(x1, x2), Math.min(y1, y2), Math.max(1, Math.abs(x1 - x2)), Math.max(1, Math.abs(y1 - y2))); + } + mode; + ctx.globalAlpha = 1; } -// TODO: Replace uses with `new URL(url).origin` when ie11 is no longer supported. -function getOrigin(url) { - var matches = ('' + url).match(/(https?|file|ftp|(chrome|moz|safari-web)-extension):\/\/[^/]+/); - if (!matches) throw new Error('Origin not found'); - return matches[0]; +let cachedBricksRender = document.createElement("canvas"); +let cachedBricksRenderKey = ""; +function renderAllBricks() { + ctx.globalAlpha = 1; + const hasCombo = (0, _game.gameState).combo > (0, _gameStateMutators.baseCombo)((0, _game.gameState)); + const redBorderOnBricksWithWrongColor = hasCombo && (0, _game.gameState).perks.picky_eater && (0, _gameUtils.isPickyEatingPossible)((0, _game.gameState)); + const redRowReach = (0, _gameUtils.reachRedRowIndex)((0, _game.gameState)); + const { clairvoyant } = (0, _game.gameState).perks; + let offset = getDashOffset((0, _game.gameState)); + if (!(redBorderOnBricksWithWrongColor || redRowReach !== -1 || (0, _game.gameState).perks.zen)) offset = 0; + const clairVoyance = clairvoyant && (0, _game.gameState).brickHP.reduce((a, b)=>a + b, 0); + const newKey = (0, _game.gameState).gameZoneWidth + "_" + (0, _game.gameState).bricks.join("_") + bombSVG.complete + "_" + redRowReach + "_" + redBorderOnBricksWithWrongColor + "_" + (0, _game.gameState).ballsColor + "_" + (0, _game.gameState).perks.pierce_color + "_" + clairVoyance + "_" + offset; + if (newKey !== cachedBricksRenderKey) { + cachedBricksRenderKey = newKey; + cachedBricksRender.width = (0, _game.gameState).gameZoneWidth; + cachedBricksRender.height = (0, _game.gameState).gameZoneWidth + 1; + const canctx = cachedBricksRender.getContext("2d"); + canctx.clearRect(0, 0, (0, _game.gameState).gameZoneWidth, (0, _game.gameState).gameZoneWidth); + canctx.resetTransform(); + canctx.translate(-(0, _game.gameState).offsetX, 0); + // Bricks + (0, _game.gameState).bricks.forEach((color, index)=>{ + const x = (0, _gameUtils.brickCenterX)((0, _game.gameState), index), y = (0, _gameUtils.brickCenterY)((0, _game.gameState), index); + if (!color) return; + let redBecauseOfReach = redRowReach === Math.floor(index / (0, _game.gameState).level.size); + let redBorder = (0, _game.gameState).ballsColor !== color && color !== "black" && redBorderOnBricksWithWrongColor || hasCombo && (0, _game.gameState).perks.zen && color === "black" || redBecauseOfReach; + canctx.globalCompositeOperation = "source-over"; + drawBrick((0, _game.gameState), canctx, color, x, y, redBorder ? offset : -1, clairvoyant >= 2); + if ((0, _game.gameState).brickHP[index] > 1 && clairvoyant) { + canctx.globalCompositeOperation = "source-over"; + drawText(canctx, (0, _game.gameState).brickHP[index].toString(), clairvoyant >= 2 ? color : (0, _game.gameState).level.color, (0, _game.gameState).puckHeight, x, y); + } + if (color === "black") { + canctx.globalCompositeOperation = "source-over"; + drawIMG(canctx, bombSVG, (0, _game.gameState).brickWidth, x, y); + } + }); + } + ctx.drawImage(cachedBricksRender, (0, _game.gameState).offsetX, 0); +} +let cachedGraphics = {}; +function drawPuck(ctx, color, puckWidth, puckHeight, yOffset = 0, 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"); + canctx.fillStyle = color; + 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 = "#FF0000"; + canctx.lineWidth = 4; + canctx.setLineDash(redBorderDash); + canctx.lineDashOffset = redBorderOffset; + canctx.stroke(); + } + cachedGraphics[key] = can; + } + ctx.drawImage(cachedGraphics[key], Math.round((0, _game.gameState).puckPosition - puckWidth / 2), (0, _game.gameState).gameZoneHeight - puckHeight * 2 + yOffset); +} +function drawBall(ctx, color, width, x, y, borderColor = "") { + const key = "ball" + color + "_" + width + "_" + borderColor; + const size = Math.round(width); + if (!cachedGraphics[key]) { + const can = document.createElement("canvas"); + can.width = size; + can.height = size; + const canctx = can.getContext("2d"); + canctx.beginPath(); + canctx.arc(size / 2, size / 2, Math.round(size / 2) - 1, 0, 2 * Math.PI); + canctx.fillStyle = color; + canctx.fill(); + if (borderColor) { + canctx.lineWidth = 2; + canctx.strokeStyle = borderColor; + canctx.stroke(); + } + cachedGraphics[key] = can; + } + ctx.drawImage(cachedGraphics[key], Math.round(x - size / 2), Math.round(y - size / 2)); +} +const angles = 32; +function drawCoin(ctx, color, size, x, y, borderColor, rawAngle) { + const angle = (Math.round(rawAngle / Math.PI * 2 * angles) % angles + angles) % angles; + const key = "coin with halo_" + color + "_" + size + "_" + borderColor + "_" + (color === "#ffd300" ? angle : "whatever"); + if (!cachedGraphics[key]) { + const can = document.createElement("canvas"); + can.width = size; + can.height = size; + const canctx = can.getContext("2d"); + // coin + canctx.beginPath(); + canctx.arc(size / 2, size / 2, size / 2, 0, 2 * Math.PI); + canctx.fillStyle = color; + canctx.fill(); + canctx.strokeStyle = borderColor; + if (borderColor == "#FF0000") { + canctx.lineWidth = 2; + canctx.setLineDash(redBorderDash); + } + if (color === "transparent") canctx.lineWidth = 2; + canctx.stroke(); + if (color === "#ffd300") { + // 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)); +} +function drawFuzzyBall(ctx, color, width, x, y) { + width = Math.max(width, 2); + const key = "fuzzy-circle" + color + "_" + width; + if (!color?.startsWith("#")) debugger; + const size = Math.round(width * 3); + if (!size || isNaN(size)) { + debugger; + return; + } + if (!cachedGraphics[key]) { + const can = document.createElement("canvas"); + can.width = size; + can.height = size; + const canctx = can.getContext("2d"); + const gradient = canctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2); + gradient.addColorStop(0, color); + gradient.addColorStop(0.3, color + "88"); + gradient.addColorStop(0.6, color + "22"); + gradient.addColorStop(1, "transparent"); + canctx.fillStyle = gradient; + canctx.fillRect(0, 0, size, size); + cachedGraphics[key] = can; + } + ctx.drawImage(cachedGraphics[key], Math.round(x - size / 2), Math.round(y - size / 2)); +} +function drawBrick(gameState, ctx, color, x, y, offset = 0, borderOnly) { + const tlx = Math.ceil(x - gameState.brickWidth / 2); + const tly = Math.ceil(y - gameState.brickWidth / 2); + const brx = Math.ceil(x + gameState.brickWidth / 2) - 1; + const bry = Math.ceil(y + gameState.brickWidth / 2) - 1; + 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"); + canctx.fillStyle = color; + canctx.setLineDash(offset !== -1 ? redBorderDash : emptyArray); + canctx.lineDashOffset = offset; + canctx.strokeStyle = offset !== -1 && "#FF000033" || 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; + } + ctx.drawImage(cachedGraphics[key], tlx, tly, width, height); +// It's not easy to have a 1px gap between bricks without antialiasing +} +function roundRect(ctx, x, y, width, height, radius) { + ctx.beginPath(); + ctx.moveTo(x + radius, y); + ctx.lineTo(x + width - radius, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + radius); + ctx.lineTo(x + width, y + height - radius); + ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); + ctx.lineTo(x + radius, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - radius); + ctx.lineTo(x, y + radius); + ctx.quadraticCurveTo(x, y, x + radius, y); + ctx.closePath(); +} +function drawIMG(ctx, img, size, x, y) { + const key = "svg" + img + "_" + size + "_" + img.complete; + if (!cachedGraphics[key]) { + const can = document.createElement("canvas"); + can.width = size; + can.height = size; + const canctx = can.getContext("2d"); + const ratio = size / Math.max(img.width, img.height); + const w = img.width * ratio; + const h = img.height * ratio; + canctx.drawImage(img, (size - w) / 2, (size - h) / 2, w, h); + cachedGraphics[key] = can; + } + ctx.drawImage(cachedGraphics[key], Math.round(x - size / 2), Math.round(y - size / 2)); +} +function drawText(ctx, text, color, fontSize, x, y, left = false) { + const key = "text" + text + "_" + color + "_" + fontSize + "_" + left; + if (!cachedGraphics[key]) { + const can = document.createElement("canvas"); + can.width = fontSize * text.length; + can.height = fontSize; + const canctx = can.getContext("2d"); + canctx.fillStyle = color; + canctx.textAlign = left ? "left" : "center"; + canctx.textBaseline = "middle"; + canctx.font = fontSize + "px monospace"; + canctx.fillText(text, left ? 0 : can.width / 2, can.height / 2, can.width); + cachedGraphics[key] = can; + } + ctx.drawImage(cachedGraphics[key], left ? x : Math.round(x - cachedGraphics[key].width / 2), Math.round(y - cachedGraphics[key].height / 2)); +} +const scoreDisplay = document.getElementById("score"); +const menuLabel = document.getElementById("menuLabel"); +const emptyArray = []; +const redBorderDash = [ + 5, + 5 +]; +function getDashOffset(gameState) { + if ((0, _options.isOptionOn)("basic")) return 0; + return Math.floor(gameState.levelTime % 500 / 500 * 10) % 10; +} +let wakeLock = null, wakeLockPending = false; +function askForWakeLock(gameState) { + if (gameState.startParams.computer_controlled && !wakeLock && !wakeLockPending) { + wakeLockPending = true; + try { + navigator.wakeLock.request("screen").then((lock)=>{ + wakeLock = lock; + wakeLockPending = false; + lock.addEventListener("release", ()=>{ + // the wake lock has been released + wakeLock = null; + }); + }); + } catch (e) { + console.warn("askForWakeLock error", e); + } + } } -exports.getBundleURL = getBundleURLCached; -exports.getBaseURL = getBaseURL; -exports.getOrigin = getOrigin; -},{}],"9ZeQl":[function(require,module,exports,__globalThis) { +},{"./gameStateMutators":"9ZeQl","./game_utils":"cEeac","./i18n/i18n":"eNPRm","./game":"edeGs","./options":"d5NoS","./pure_functions":"6pQh7","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"9ZeQl":[function(require,module,exports,__globalThis) { var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js"); parcelHelpers.defineInteropFlag(exports); parcelHelpers.export(exports, "setMousePos", ()=>setMousePos); @@ -4384,7 +4987,7 @@ frames = 1) { spawnParticlesExplosion(gameState, 3, coin.x, coin.y, "#6262EA"); spawnParticlesImplosion(gameState, 3, coin.previousX, coin.previousY, "#6262EA"); } - if (gameState.perks.wrap_right > 1 && hitBorder % 2 && coin.previousX > gameState.offsetX + gameState.gameZoneWidth / 2) { + if (gameState.perks.wrap_right > 1 && hitBorder % 2 && coin.previousX > (0, _gameUtils.canvasCenterX)(gameState)) { schedulGameSound(gameState, "plouf", coin.x, 1); coin.x = gameState.offsetX + gameState.coinSize; if (coin.vx < 0) coin.vx *= -1; @@ -4477,7 +5080,7 @@ frames = 1) { } })); if (gameState.perks.wind) { - const windD = (gameState.puckPosition - (gameState.offsetX + gameState.gameZoneWidth / 2)) / gameState.gameZoneWidth * 2 * gameState.perks.wind; + const windD = (gameState.puckPosition - (0, _gameUtils.canvasCenterX)(gameState)) / 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)=>{ @@ -4507,8 +5110,8 @@ frames = 1) { makeParticle(gameState, gameState.puckPosition + gameState.puckWidth * pos, gameState.gameZoneHeight - gameState.puckHeight, pos * 10, -5, "#FF0000", true, gameState.coinSize / 2, 100 * (Math.random() + 1)); } } - if (gameState.perks.wrap_left && gameState.perks.left_is_lava < 2 && Math.random() * frames > 0.1) makeParticle(gameState, gameState.offsetXRoundedDown, Math.random() * gameState.gameZoneHeight, 5, (Math.random() - 0.5) * 10, "#6262EA", true, gameState.coinSize / 2, 100 * (Math.random() + 1)); - if (gameState.perks.wrap_right && gameState.perks.right_is_lava < 2 && Math.random() * frames > 0.1) makeParticle(gameState, gameState.offsetXRoundedDown + gameState.gameZoneWidth, Math.random() * gameState.gameZoneHeight, -5, (Math.random() - 0.5) * 10, "#6262EA", true, gameState.coinSize / 2, 100 * (Math.random() + 1)); + if (gameState.perks.wrap_left && gameState.perks.left_is_lava < 2 && Math.random() * frames > 0.1) makeParticle(gameState, (0, _gameUtils.zoneLeftBorderX)(gameState), Math.random() * gameState.gameZoneHeight, 5, (Math.random() - 0.5) * 10, "#6262EA", true, gameState.coinSize / 2, 100 * (Math.random() + 1)); + if (gameState.perks.wrap_right && gameState.perks.right_is_lava < 2 && Math.random() * frames > 0.1) makeParticle(gameState, (0, _gameUtils.zoneRightBorderX)(gameState), Math.random() * gameState.gameZoneHeight, -5, (Math.random() - 0.5) * 10, "#6262EA", 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); @@ -4572,15 +5175,13 @@ function ballTick(gameState, ball, frames) { }, gameState.perks.puck_repulse_ball + 1, false); if (gameState.perks.steering) { const delta = gameState.puckPosition - gameState.lastPuckPosition; - const angle = Math.atan2(ball.vy, ball.vx) + delta / gameState.gameZoneWidth * Math.PI / 2 * gameState.perks.steering * frames / 2; - const d = Math.sqrt(ball.vy * ball.vy + ball.vx * ball.vx); - ball.vy = Math.sin(angle) * d; - ball.vx = Math.cos(angle) * d; - console.log({ - delta, - angle, - d - }); + if (Math.abs(delta) > 1) { + const angle = Math.atan2(ball.vy, ball.vx) + delta / gameState.gameZoneWidth * Math.PI / 2 * gameState.perks.steering * frames / 2; + const d = Math.sqrt(ball.vy * ball.vy + ball.vx * ball.vx); + ball.vy = Math.sin(angle) * d; + ball.vx = Math.cos(angle) * d; + if (Math.random() < frames && !(0, _options.isOptionOn)('basic')) makeParticle(gameState, ball.x, ball.y, -ball.vx / 10, -ball.vy / 10, '#6262EA', true, 8, 500); + } } // Bounces const borderHitCode = bordersHitCheck(gameState, ball, gameState.ballSize / 2, frames); @@ -4882,633 +5483,7 @@ function zenTick(gameState) { } } -},{"./game_utils":"cEeac","./i18n/i18n":"eNPRm","./settings":"5blfu","./render":"9AS2t","./gameOver":"caCAf","./game":"edeGs","./recording":"godmD","./options":"d5NoS","./pure_functions":"6pQh7","./addToTotalScore":"ka4dG","./getLevelBackground":"7OIPf","./openUpgradesPicker":"2fQt0","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"9AS2t":[function(require,module,exports,__globalThis) { -var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js"); -parcelHelpers.defineInteropFlag(exports); -parcelHelpers.export(exports, "gameCanvas", ()=>gameCanvas); -parcelHelpers.export(exports, "ctx", ()=>ctx); -parcelHelpers.export(exports, "bombSVG", ()=>bombSVG); -parcelHelpers.export(exports, "background", ()=>background); -parcelHelpers.export(exports, "backgroundCanvas", ()=>backgroundCanvas); -parcelHelpers.export(exports, "haloCanvas", ()=>haloCanvas); -parcelHelpers.export(exports, "getHaloScale", ()=>getHaloScale); -parcelHelpers.export(exports, "render", ()=>render); -parcelHelpers.export(exports, "renderAllBricks", ()=>renderAllBricks); -parcelHelpers.export(exports, "drawPuck", ()=>drawPuck); -parcelHelpers.export(exports, "drawBall", ()=>drawBall); -parcelHelpers.export(exports, "drawCoin", ()=>drawCoin); -parcelHelpers.export(exports, "drawFuzzyBall", ()=>drawFuzzyBall); -parcelHelpers.export(exports, "drawBrick", ()=>drawBrick); -parcelHelpers.export(exports, "roundRect", ()=>roundRect); -parcelHelpers.export(exports, "drawIMG", ()=>drawIMG); -parcelHelpers.export(exports, "drawText", ()=>drawText); -parcelHelpers.export(exports, "scoreDisplay", ()=>scoreDisplay); -parcelHelpers.export(exports, "getDashOffset", ()=>getDashOffset); -var _gameStateMutators = require("./gameStateMutators"); -var _gameUtils = require("./game_utils"); -var _i18N = require("./i18n/i18n"); -var _game = require("./game"); -var _options = require("./options"); -var _pureFunctions = require("./pure_functions"); -const gameCanvas = document.getElementById("game"); -const ctx = gameCanvas.getContext("2d", { - alpha: false -}); -const bombSVG = document.createElement("img"); -bombSVG.src = "data:image/svg+xml;base64," + btoa(` - -`); -bombSVG.onload = ()=>(0, _game.gameState).needsRender = true; -const background = document.createElement("img"); -background.onload = ()=>(0, _game.gameState).needsRender = true; -const backgroundCanvas = document.createElement("canvas"); -const haloCanvas = document.createElement("canvas"); -const haloCanvasCtx = haloCanvas.getContext("2d", { - alpha: false -}); -function getHaloScale() { - return 16 * ((0, _options.isOptionOn)("precise_lighting") ? 1 : 2); -} -let framesCounter = 0; -function render(gameState) { - framesCounter++; - (0, _game.startWork)("render:init"); - const level = (0, _gameUtils.currentLevelInfo)(gameState); - const hasCombo = gameState.combo > (0, _gameStateMutators.baseCombo)(gameState); - const { width, height } = gameCanvas; - if (!width || !height) return; - if (gameState.currentLevel || gameState.levelTime) menuLabel.innerText = (0, _i18N.t)("play.current_lvl", { - level: gameState.currentLevel + 1, - max: (0, _gameUtils.max_levels)(gameState) - }); - else menuLabel.innerText = (0, _i18N.t)("play.menu_label"); - const catchRate = gameState.levelSpawnedCoins ? gameState.levelCoughtCoins / (gameState.levelSpawnedCoins || 1) : // gameState.levelSpawnedCoins - 1; - (0, _game.startWork)("render:scoreDisplay"); - scoreDisplay.innerHTML = ((0, _options.isOptionOn)("show_fps") || gameState.startParams.computer_controlled ? ` - - ${0, _game.lastMeasuredFPS} FPS - / - ` : "") + ((0, _options.isOptionOn)("show_stats") ? ` - (0, _pureFunctions.catchRateGood) / 100 && "good" || ""}" data-tooltip="${(0, _i18N.t)("play.stats.coins_catch_rate")}"> - ${Math.floor(catchRate * 100)}% - / - - ${Math.ceil(gameState.levelTime / 1000)}s - / - - ${gameState.levelMisses} M - / - ` : "") + `$${gameState.score}`; - scoreDisplay.classList[gameState.startParams.computer_controlled ? "add" : "remove"]("computer_controlled"); - scoreDisplay.classList[gameState.lastScoreIncrease > gameState.levelTime - 500 ? "add" : "remove"]("active"); - // Clear - if (!(0, _options.isOptionOn)("basic") && level.svg && level.color === "#000000") { - const skipN = (0, _options.isOptionOn)("probabilistic_lighting") && (0, _gameStateMutators.liveCount)(gameState.coins) > 150 ? 3 : 0; - const shouldSkip = (index)=>skipN ? (framesCounter + index) % (skipN + 1) !== 0 : false; - const haloScale = getHaloScale(); - (0, _game.startWork)("render:halo:clear"); - haloCanvasCtx.globalCompositeOperation = "source-over"; - haloCanvasCtx.globalAlpha = skipN ? 0.1 : 0.99; - haloCanvasCtx.fillStyle = level.color; - haloCanvasCtx.fillRect(0, 0, width / haloScale, height / haloScale); - const brightness = (0, _options.isOptionOn)("extra_bright") ? 3 : 1; - haloCanvasCtx.globalCompositeOperation = "lighten"; - haloCanvasCtx.globalAlpha = 0.1 + 5 / ((0, _gameStateMutators.liveCount)(gameState.coins) + 10); - (0, _game.startWork)("render:halo:coins"); - (0, _gameStateMutators.forEachLiveOne)(gameState.coins, (coin, index)=>{ - if (shouldSkip(index)) return; - const color = (0, _gameUtils.getCoinRenderColor)(gameState, coin); - drawFuzzyBall(haloCanvasCtx, color, gameState.coinSize * 2 * brightness / haloScale, coin.x / haloScale, coin.y / haloScale); - }); - (0, _game.startWork)("render:halo:balls"); - gameState.balls.forEach((ball, index)=>{ - if (shouldSkip(index)) return; - haloCanvasCtx.globalAlpha = 0.3 * (1 - (0, _pureFunctions.ballTransparency)(ball, gameState)); - drawFuzzyBall(haloCanvasCtx, gameState.ballsColor, gameState.ballSize * 2 * brightness / haloScale, ball.x / haloScale, ball.y / haloScale); - }); - (0, _game.startWork)("render:halo:bricks"); - haloCanvasCtx.globalAlpha = 0.05; - gameState.bricks.forEach((color, index)=>{ - if (!color) return; - if (shouldSkip(index)) return; - const x = (0, _gameUtils.brickCenterX)(gameState, index), y = (0, _gameUtils.brickCenterY)(gameState, index); - drawFuzzyBall(haloCanvasCtx, color == "black" ? "#666666" : color, // Perf could really go down there because of the size of the halo - Math.min(200, gameState.brickWidth * 1.5 * brightness) / haloScale, x / haloScale, y / haloScale); - }); - (0, _game.startWork)("render:halo:particles"); - haloCanvasCtx.globalCompositeOperation = "screen"; - (0, _gameStateMutators.forEachLiveOne)(gameState.particles, (flash, index)=>{ - if (shouldSkip(index)) return; - const { x, y, time, color, size, duration } = flash; - const elapsed = gameState.levelTime - time; - haloCanvasCtx.globalAlpha = 0.1 * Math.min(1, 2 - elapsed / duration * 2); - drawFuzzyBall(haloCanvasCtx, color, size * 3 * brightness / haloScale, x / haloScale, y / haloScale); - }); - (0, _game.startWork)("render:halo:scale_up"); - ctx.globalAlpha = 1; - ctx.globalCompositeOperation = "source-over"; - ctx.imageSmoothingQuality = "high"; - ctx.imageSmoothingEnabled = (0, _options.isOptionOn)("smooth_lighting") || false; - ctx.drawImage(haloCanvas, 0, 0, width, height); - ctx.imageSmoothingEnabled = false; - (0, _game.startWork)("render:halo:pattern"); - ctx.globalAlpha = 1; - ctx.globalCompositeOperation = "multiply"; - if (level.svg && background.width && background.complete) { - if (backgroundCanvas.title !== level.name) { - backgroundCanvas.title = level.name; - backgroundCanvas.width = gameState.canvasWidth; - backgroundCanvas.height = gameState.canvasHeight; - const bgctx = backgroundCanvas.getContext("2d"); - bgctx.globalCompositeOperation = "source-over"; - 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 = "#FFFFFF"; - 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.globalCompositeOperation = "screen"; - bgctx.fillStyle = pattern; - bgctx.fillRect(0, 0, width, height); - } - } - } - ctx.globalCompositeOperation = "darken"; - ctx.drawImage(backgroundCanvas, 0, 0); - } else { - // Background not loaded yes - ctx.fillStyle = "#000"; - ctx.fillRect(0, 0, width, height); - } - } else { - (0, _game.startWork)("render:halo-basic"); - ctx.globalAlpha = 1; - ctx.globalCompositeOperation = "source-over"; - ctx.fillStyle = level.color || "#000"; - ctx.fillRect(0, 0, width, height); - (0, _gameStateMutators.forEachLiveOne)(gameState.particles, (flash)=>{ - const { x, y, time, color, size, duration } = flash; - const elapsed = gameState.levelTime - time; - ctx.globalAlpha = Math.min(1, 2 - elapsed / duration * 2); - drawBall(ctx, color, size, x, y); - }); - } - (0, _game.startWork)("render:explosionshake"); - ctx.globalAlpha = 1; - ctx.globalCompositeOperation = "source-over"; - const lastExplosionDelay = gameState.levelTime - gameState.lastExplosion + 5; - const shaked = lastExplosionDelay < 200 && !(0, _options.isOptionOn)("basic") && // Otherwise, if you pause after an explosion, moving the mouses shakes the picture - gameState.running; - if (shaked) { - const amplitude = (gameState.perks.bigger_explosions + 1) * 50 / lastExplosionDelay; - ctx.translate(Math.sin(Date.now()) * amplitude, Math.sin(Date.now() + 36) * amplitude); - } - (0, _game.startWork)("render:coins"); - // Coins - ctx.globalAlpha = 1; - (0, _gameStateMutators.forEachLiveOne)(gameState.coins, (coin)=>{ - const color = (0, _gameUtils.getCoinRenderColor)(gameState, coin); - const hollow = gameState.perks.metamorphosis && !coin.metamorphosisPoints; - ctx.globalCompositeOperation = "source-over"; - drawCoin(ctx, hollow ? "transparent" : color, coin.size, coin.x, coin.y, // Red border around coins with asceticism - hasCombo && gameState.perks.asceticism && "#FF0000" || // Gold coins - // (color === "#ffd300" && "#ffd300") || - hollow && color || gameState.level.color, coin.a); - }); - (0, _game.startWork)("render:ball shade"); - // Black shadow around balls - ctx.globalCompositeOperation = "source-over"; - gameState.balls.forEach((ball)=>{ - ctx.globalAlpha = Math.min(0.8, (0, _gameStateMutators.liveCount)(gameState.coins) / 20) * (1 - (0, _pureFunctions.ballTransparency)(ball, gameState)); - drawBall(ctx, level.color || "#000", gameState.ballSize * 6, ball.x, ball.y); - }); - (0, _game.startWork)("render:bricks"); - ctx.globalCompositeOperation = "source-over"; - renderAllBricks(); - (0, _game.startWork)("render:lights"); - ctx.globalCompositeOperation = "screen"; - (0, _gameStateMutators.forEachLiveOne)(gameState.lights, (flash)=>{ - const { x, y, time, color, size, duration } = flash; - const elapsed = gameState.levelTime - time; - ctx.globalAlpha = Math.min(1, 2 - elapsed / duration * 2) * 0.5; - drawBrick(gameState, ctx, color, x, y, -1, gameState.perks.clairvoyant >= 2); - }); - (0, _game.startWork)("render:texts"); - ctx.globalCompositeOperation = "screen"; - (0, _gameStateMutators.forEachLiveOne)(gameState.texts, (flash)=>{ - const { x, y, time, color, size, duration } = flash; - const elapsed = gameState.levelTime - time; - ctx.globalAlpha = Math.max(0, Math.min(1, 2 - elapsed / duration * 2)); - ctx.globalCompositeOperation = "source-over"; - drawText(ctx, flash.text, color, size, x, y - elapsed / 10); - }); - (0, _game.startWork)("render:particles"); - (0, _gameStateMutators.forEachLiveOne)(gameState.particles, (particle)=>{ - const { x, y, time, color, size, duration } = particle; - const elapsed = gameState.levelTime - time; - ctx.globalAlpha = Math.max(0, Math.min(1, 2 - elapsed / duration * 2)); - ctx.globalCompositeOperation = "screen"; - drawBall(ctx, color, size, x, y); - }); - // - (0, _game.startWork)("render:extra_life"); - if (gameState.perks.extra_life) { - ctx.globalAlpha = gameState.balls.length > 1 ? 0.2 : 1; - ctx.globalCompositeOperation = "source-over"; - ctx.fillStyle = gameState.puckColor; - for(let i = 0; i < gameState.perks.extra_life; i++)ctx.fillRect(gameState.offsetXRoundedDown, gameState.gameZoneHeight - gameState.puckHeight / 2 + 2 * i, gameState.gameZoneWidthRoundedUp, 1); - } - (0, _game.startWork)("render:balls"); - ctx.globalAlpha = 1; - ctx.globalCompositeOperation = "source-over"; - gameState.balls.forEach((ball)=>{ - const drawingColor = gameState.ballsColor; - const ballAlpha = 1 - (0, _pureFunctions.ballTransparency)(ball, gameState); - ctx.globalAlpha = ballAlpha; - // The white border around is to distinguish colored balls from coins/bg - drawBall(ctx, drawingColor, gameState.ballSize, ball.x, ball.y, gameState.puckColor); - if ((0, _gameUtils.telekinesisEffectRate)(gameState, ball) || (0, _gameUtils.yoyoEffectRate)(gameState, ball)) { - ctx.beginPath(); - ctx.moveTo(gameState.puckPosition, gameState.gameZoneHeight); - ctx.globalAlpha = (0, _pureFunctions.clamp)(Math.max((0, _gameUtils.telekinesisEffectRate)(gameState, ball), (0, _gameUtils.yoyoEffectRate)(gameState, ball)) * ballAlpha, 0, 1); - 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); - } - ctx.globalAlpha = 1; - if (gameState.perks.clairvoyant && gameState.ballStickToPuck || gameState.perks.steering > 1 && !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(); - } - }); - (0, _game.startWork)("render:puck"); - ctx.globalAlpha = (0, _gameUtils.isMovingWhilePassiveIncome)(gameState) ? 0.2 : 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); - (0, _game.startWork)("render:combotext"); - const spawns = (0, _pureFunctions.coinsBoostedCombo)(gameState); - if (spawns > 1 && !(0, _gameUtils.isMovingWhilePassiveIncome)(gameState)) { - ctx.globalCompositeOperation = "source-over"; - ctx.globalAlpha = 1; - const comboText = spawns.toString(); - const comboTextWidth = comboText.length * gameState.puckHeight / 1.8; - const totalWidth = comboTextWidth + gameState.coinSize * 2; - const left = gameState.puckPosition - totalWidth / 2; - ctx.globalAlpha = gameState.combo > (0, _gameStateMutators.baseCombo)(gameState) ? 1 : 0.3; - if (totalWidth < gameState.puckWidth) { - drawText(ctx, comboText, "#000", gameState.puckHeight, left + gameState.coinSize * 1.5, gameState.gameZoneHeight - gameState.puckHeight / 2, true); - ctx.globalAlpha = 1; - drawCoin(ctx, "#ffd300", gameState.coinSize, left + gameState.coinSize / 2, gameState.gameZoneHeight - gameState.puckHeight / 2, "#ffd300", 0); - } else drawText(ctx, comboTextWidth > gameState.puckWidth ? gameState.combo.toString() : comboText, "#000", comboTextWidth > gameState.puckWidth ? 12 : 20, gameState.puckPosition, gameState.gameZoneHeight - gameState.puckHeight / 2, false); - } - (0, _game.startWork)("render:borders"); - // Borders - ctx.globalCompositeOperation = "source-over"; - ctx.globalAlpha = 1; - let redLeftSide = hasCombo && (gameState.perks.left_is_lava || gameState.perks.trampoline); - let redRightSide = hasCombo && (gameState.perks.right_is_lava || gameState.perks.trampoline); - let redTop = hasCombo && (gameState.perks.top_is_lava || gameState.perks.trampoline); - if (gameState.offsetXRoundedDown) { - // draw outside of gaming area to avoid capturing borders in recordings - if (gameState.perks.left_is_lava < 2) drawStraightLine(ctx, gameState, redLeftSide && "#FF0000" || "#FFFFFF", gameState.offsetXRoundedDown - 1, 0, gameState.offsetXRoundedDown - 1, height, 1); - if (gameState.perks.right_is_lava < 2) drawStraightLine(ctx, gameState, redRightSide && "#FF0000" || "#FFFFFF", width - gameState.offsetXRoundedDown + 1, 0, width - gameState.offsetXRoundedDown + 1, height, 1); - } else { - if (gameState.perks.left_is_lava < 2) drawStraightLine(ctx, gameState, redLeftSide && "#FF0000" || "", 0, 0, 0, height, 1); - if (gameState.perks.right_is_lava < 2) drawStraightLine(ctx, gameState, redRightSide && "#FF0000" || "", width - 1, 0, width - 1, height, 1); - } - if (redTop && gameState.perks.top_is_lava < 2) drawStraightLine(ctx, gameState, "#FF0000", gameState.offsetXRoundedDown, 1, width - gameState.offsetXRoundedDown, 1, 1); - (0, _game.startWork)("render:bottom_line"); - ctx.globalAlpha = 1; - const corner = (0, _gameUtils.getCornerOffset)(gameState); - const bottomLineIsRed = hasCombo && gameState.perks.compound_interest; - drawStraightLine(ctx, gameState, bottomLineIsRed && "#FF0000" || (0, _options.isOptionOn)("mobile-mode") && "#FFFFFF" || corner && "#FFFFFF" || "", gameState.offsetXRoundedDown - corner, gameState.gameZoneHeight - 1, width - gameState.offsetXRoundedDown + corner, gameState.gameZoneHeight - 1, bottomLineIsRed ? 1 : 0.5); - (0, _game.startWork)("render:contrast"); - if (!(0, _options.isOptionOn)("basic") && (0, _options.isOptionOn)("contrast") && level.svg && level.color === "#000000") { - ctx.imageSmoothingEnabled = (0, _options.isOptionOn)("smooth_lighting") || false; - if ((0, _options.isOptionOn)("probabilistic_lighting")) { - ctx.globalAlpha = 1; - ctx.globalCompositeOperation = "soft-light"; - } else { - haloCanvasCtx.fillStyle = "#FFFFFF"; - haloCanvasCtx.globalAlpha = 0.25; - haloCanvasCtx.globalCompositeOperation = "screen"; - haloCanvasCtx.fillRect(0, 0, haloCanvas.width, haloCanvas.height); - ctx.globalAlpha = 1; - ctx.globalCompositeOperation = "overlay"; - } - ctx.drawImage(haloCanvas, 0, 0, width, height); - ctx.imageSmoothingEnabled = false; - } - (0, _game.startWork)("render:text_under_puck"); - ctx.globalCompositeOperation = "source-over"; - ctx.globalAlpha = 1; - if ((0, _options.isOptionOn)("mobile-mode") && gameState.startParams.computer_controlled) drawText(ctx, "breakout.lecaro.me?autoplay", gameState.puckColor, gameState.puckHeight, gameState.canvasWidth / 2, gameState.gameZoneHeight + (gameState.canvasHeight - gameState.gameZoneHeight) / 2); - if ((0, _options.isOptionOn)("mobile-mode") && !gameState.running) drawText(ctx, (0, _i18N.t)("play.mobile_press_to_play"), gameState.puckColor, gameState.puckHeight, gameState.canvasWidth / 2, gameState.gameZoneHeight + (gameState.canvasHeight - gameState.gameZoneHeight) / 2); - (0, _game.startWork)("render:askForWakeLock"); - askForWakeLock(gameState); - (0, _game.startWork)("render:resetTransform"); - if (shaked) ctx.resetTransform(); -} -function drawStraightLine(ctx, gameState, mode, x1, y1, x2, y2, alpha = 1) { - ctx.globalAlpha = alpha; - if (!mode) return; - x1 = Math.round(x1); - y1 = Math.round(y1); - x2 = Math.round(x2); - y2 = Math.round(y2); - if (mode == "#FF0000") { - ctx.strokeStyle = "red"; - ctx.lineDashOffset = getDashOffset(gameState); - ctx.lineWidth = 2; - ctx.setLineDash(redBorderDash); - ctx.beginPath(); - ctx.moveTo(x1, y1); - ctx.lineTo(x2, y2); - ctx.stroke(); - ctx.setLineDash(emptyArray); - ctx.lineWidth = 1; - } else { - ctx.fillStyle = mode; - ctx.fillRect(Math.min(x1, x2), Math.min(y1, y2), Math.max(1, Math.abs(x1 - x2)), Math.max(1, Math.abs(y1 - y2))); - } - mode; - ctx.globalAlpha = 1; -} -let cachedBricksRender = document.createElement("canvas"); -let cachedBricksRenderKey = ""; -function renderAllBricks() { - ctx.globalAlpha = 1; - const hasCombo = (0, _game.gameState).combo > (0, _gameStateMutators.baseCombo)((0, _game.gameState)); - const redBorderOnBricksWithWrongColor = hasCombo && (0, _game.gameState).perks.picky_eater && (0, _gameUtils.isPickyEatingPossible)((0, _game.gameState)); - const redRowReach = (0, _gameUtils.reachRedRowIndex)((0, _game.gameState)); - const { clairvoyant } = (0, _game.gameState).perks; - let offset = getDashOffset((0, _game.gameState)); - if (!(redBorderOnBricksWithWrongColor || redRowReach !== -1 || (0, _game.gameState).perks.zen)) offset = 0; - const clairVoyance = clairvoyant && (0, _game.gameState).brickHP.reduce((a, b)=>a + b, 0); - const newKey = (0, _game.gameState).gameZoneWidth + "_" + (0, _game.gameState).bricks.join("_") + bombSVG.complete + "_" + redRowReach + "_" + redBorderOnBricksWithWrongColor + "_" + (0, _game.gameState).ballsColor + "_" + (0, _game.gameState).perks.pierce_color + "_" + clairVoyance + "_" + offset; - if (newKey !== cachedBricksRenderKey) { - cachedBricksRenderKey = newKey; - cachedBricksRender.width = (0, _game.gameState).gameZoneWidth; - cachedBricksRender.height = (0, _game.gameState).gameZoneWidth + 1; - const canctx = cachedBricksRender.getContext("2d"); - canctx.clearRect(0, 0, (0, _game.gameState).gameZoneWidth, (0, _game.gameState).gameZoneWidth); - canctx.resetTransform(); - canctx.translate(-(0, _game.gameState).offsetX, 0); - // Bricks - (0, _game.gameState).bricks.forEach((color, index)=>{ - const x = (0, _gameUtils.brickCenterX)((0, _game.gameState), index), y = (0, _gameUtils.brickCenterY)((0, _game.gameState), index); - if (!color) return; - let redBecauseOfReach = redRowReach === Math.floor(index / (0, _game.gameState).level.size); - let redBorder = (0, _game.gameState).ballsColor !== color && color !== "black" && redBorderOnBricksWithWrongColor || hasCombo && (0, _game.gameState).perks.zen && color === "black" || redBecauseOfReach; - canctx.globalCompositeOperation = "source-over"; - drawBrick((0, _game.gameState), canctx, color, x, y, redBorder ? offset : -1, clairvoyant >= 2); - if ((0, _game.gameState).brickHP[index] > 1 && clairvoyant) { - canctx.globalCompositeOperation = "source-over"; - drawText(canctx, (0, _game.gameState).brickHP[index].toString(), clairvoyant >= 2 ? color : (0, _game.gameState).level.color, (0, _game.gameState).puckHeight, x, y); - } - if (color === "black") { - canctx.globalCompositeOperation = "source-over"; - drawIMG(canctx, bombSVG, (0, _game.gameState).brickWidth, x, y); - } - }); - } - ctx.drawImage(cachedBricksRender, (0, _game.gameState).offsetX, 0); -} -let cachedGraphics = {}; -function drawPuck(ctx, color, puckWidth, puckHeight, yOffset = 0, 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"); - canctx.fillStyle = color; - 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 = "#FF0000"; - canctx.lineWidth = 4; - canctx.setLineDash(redBorderDash); - canctx.lineDashOffset = redBorderOffset; - canctx.stroke(); - } - cachedGraphics[key] = can; - } - ctx.drawImage(cachedGraphics[key], Math.round((0, _game.gameState).puckPosition - puckWidth / 2), (0, _game.gameState).gameZoneHeight - puckHeight * 2 + yOffset); -} -function drawBall(ctx, color, width, x, y, borderColor = "") { - const key = "ball" + color + "_" + width + "_" + borderColor; - const size = Math.round(width); - if (!cachedGraphics[key]) { - const can = document.createElement("canvas"); - can.width = size; - can.height = size; - const canctx = can.getContext("2d"); - canctx.beginPath(); - canctx.arc(size / 2, size / 2, Math.round(size / 2) - 1, 0, 2 * Math.PI); - canctx.fillStyle = color; - canctx.fill(); - if (borderColor) { - canctx.lineWidth = 2; - canctx.strokeStyle = borderColor; - canctx.stroke(); - } - cachedGraphics[key] = can; - } - ctx.drawImage(cachedGraphics[key], Math.round(x - size / 2), Math.round(y - size / 2)); -} -const angles = 32; -function drawCoin(ctx, color, size, x, y, borderColor, rawAngle) { - const angle = (Math.round(rawAngle / Math.PI * 2 * angles) % angles + angles) % angles; - const key = "coin with halo_" + color + "_" + size + "_" + borderColor + "_" + (color === "#ffd300" ? angle : "whatever"); - if (!cachedGraphics[key]) { - const can = document.createElement("canvas"); - can.width = size; - can.height = size; - const canctx = can.getContext("2d"); - // coin - canctx.beginPath(); - canctx.arc(size / 2, size / 2, size / 2, 0, 2 * Math.PI); - canctx.fillStyle = color; - canctx.fill(); - canctx.strokeStyle = borderColor; - if (borderColor == "#FF0000") { - canctx.lineWidth = 2; - canctx.setLineDash(redBorderDash); - } - if (color === "transparent") canctx.lineWidth = 2; - canctx.stroke(); - if (color === "#ffd300") { - // 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)); -} -function drawFuzzyBall(ctx, color, width, x, y) { - width = Math.max(width, 2); - const key = "fuzzy-circle" + color + "_" + width; - if (!color?.startsWith("#")) debugger; - const size = Math.round(width * 3); - if (!size || isNaN(size)) { - debugger; - return; - } - if (!cachedGraphics[key]) { - const can = document.createElement("canvas"); - can.width = size; - can.height = size; - const canctx = can.getContext("2d"); - const gradient = canctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2); - gradient.addColorStop(0, color); - gradient.addColorStop(0.3, color + "88"); - gradient.addColorStop(0.6, color + "22"); - gradient.addColorStop(1, "transparent"); - canctx.fillStyle = gradient; - canctx.fillRect(0, 0, size, size); - cachedGraphics[key] = can; - } - ctx.drawImage(cachedGraphics[key], Math.round(x - size / 2), Math.round(y - size / 2)); -} -function drawBrick(gameState, ctx, color, x, y, offset = 0, borderOnly) { - const tlx = Math.ceil(x - gameState.brickWidth / 2); - const tly = Math.ceil(y - gameState.brickWidth / 2); - const brx = Math.ceil(x + gameState.brickWidth / 2) - 1; - const bry = Math.ceil(y + gameState.brickWidth / 2) - 1; - 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"); - canctx.fillStyle = color; - canctx.setLineDash(offset !== -1 ? redBorderDash : emptyArray); - canctx.lineDashOffset = offset; - canctx.strokeStyle = offset !== -1 && "#FF000033" || 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; - } - ctx.drawImage(cachedGraphics[key], tlx, tly, width, height); -// It's not easy to have a 1px gap between bricks without antialiasing -} -function roundRect(ctx, x, y, width, height, radius) { - ctx.beginPath(); - ctx.moveTo(x + radius, y); - ctx.lineTo(x + width - radius, y); - ctx.quadraticCurveTo(x + width, y, x + width, y + radius); - ctx.lineTo(x + width, y + height - radius); - ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); - ctx.lineTo(x + radius, y + height); - ctx.quadraticCurveTo(x, y + height, x, y + height - radius); - ctx.lineTo(x, y + radius); - ctx.quadraticCurveTo(x, y, x + radius, y); - ctx.closePath(); -} -function drawIMG(ctx, img, size, x, y) { - const key = "svg" + img + "_" + size + "_" + img.complete; - if (!cachedGraphics[key]) { - const can = document.createElement("canvas"); - can.width = size; - can.height = size; - const canctx = can.getContext("2d"); - const ratio = size / Math.max(img.width, img.height); - const w = img.width * ratio; - const h = img.height * ratio; - canctx.drawImage(img, (size - w) / 2, (size - h) / 2, w, h); - cachedGraphics[key] = can; - } - ctx.drawImage(cachedGraphics[key], Math.round(x - size / 2), Math.round(y - size / 2)); -} -function drawText(ctx, text, color, fontSize, x, y, left = false) { - const key = "text" + text + "_" + color + "_" + fontSize + "_" + left; - if (!cachedGraphics[key]) { - const can = document.createElement("canvas"); - can.width = fontSize * text.length; - can.height = fontSize; - const canctx = can.getContext("2d"); - canctx.fillStyle = color; - canctx.textAlign = left ? "left" : "center"; - canctx.textBaseline = "middle"; - canctx.font = fontSize + "px monospace"; - canctx.fillText(text, left ? 0 : can.width / 2, can.height / 2, can.width); - cachedGraphics[key] = can; - } - ctx.drawImage(cachedGraphics[key], left ? x : Math.round(x - cachedGraphics[key].width / 2), Math.round(y - cachedGraphics[key].height / 2)); -} -const scoreDisplay = document.getElementById("score"); -const menuLabel = document.getElementById("menuLabel"); -const emptyArray = []; -const redBorderDash = [ - 5, - 5 -]; -function getDashOffset(gameState) { - if ((0, _options.isOptionOn)("basic")) return 0; - return Math.floor(gameState.levelTime % 500 / 500 * 10) % 10; -} -let wakeLock = null, wakeLockPending = false; -function askForWakeLock(gameState) { - if (gameState.startParams.computer_controlled && !wakeLock && !wakeLockPending) { - wakeLockPending = true; - try { - navigator.wakeLock.request("screen").then((lock)=>{ - wakeLock = lock; - wakeLockPending = false; - lock.addEventListener("release", ()=>{ - // the wake lock has been released - wakeLock = null; - }); - }); - } catch (e) { - console.warn("askForWakeLock error", e); - } - } -} - -},{"./gameStateMutators":"9ZeQl","./game_utils":"cEeac","./i18n/i18n":"eNPRm","./game":"edeGs","./options":"d5NoS","./pure_functions":"6pQh7","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"caCAf":[function(require,module,exports,__globalThis) { +},{"./game_utils":"cEeac","./i18n/i18n":"eNPRm","./settings":"5blfu","./render":"9AS2t","./gameOver":"caCAf","./game":"edeGs","./recording":"godmD","./options":"d5NoS","./pure_functions":"6pQh7","./addToTotalScore":"ka4dG","./getLevelBackground":"7OIPf","./openUpgradesPicker":"2fQt0","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"caCAf":[function(require,module,exports,__globalThis) { var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js"); parcelHelpers.defineInteropFlag(exports); parcelHelpers.export(exports, "addToTotalPlayTime", ()=>addToTotalPlayTime); @@ -6722,7 +6697,54 @@ function getNearestUnlockHTML(gameState) { `; } -},{"./asyncAlert":"rSqLY","./i18n/i18n":"eNPRm","./game_utils":"cEeac","./gameOver":"caCAf","./game":"edeGs","./loadGameData":"l1B4x","./pure_functions":"6pQh7","./settings":"5blfu","./get_level_unlock_condition":"a0fq0","./options":"d5NoS","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"aQN6X":[function(require,module,exports,__globalThis) { +},{"./asyncAlert":"rSqLY","./i18n/i18n":"eNPRm","./game_utils":"cEeac","./gameOver":"caCAf","./game":"edeGs","./loadGameData":"l1B4x","./pure_functions":"6pQh7","./settings":"5blfu","./get_level_unlock_condition":"a0fq0","./options":"d5NoS","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"2n0gK":[function(require,module,exports,__globalThis) { +var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js"); +parcelHelpers.defineInteropFlag(exports); +if ("serviceWorker" in navigator && window.location.href.endsWith("/index.html?isPWA=true")) { + // @ts-ignore + const url = new URL(require("b04459cc43e56e8c")); + navigator.serviceWorker.register(url); +} + +},{"b04459cc43e56e8c":"17ciJ","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"17ciJ":[function(require,module,exports,__globalThis) { +module.exports = require("9c7c7951fd7c4db6").getBundleURL('arAGi') + "sw-b71.41cdff1b.js"; + +},{"9c7c7951fd7c4db6":"lgJ39"}],"lgJ39":[function(require,module,exports,__globalThis) { +"use strict"; +var bundleURL = {}; +function getBundleURLCached(id) { + var value = bundleURL[id]; + if (!value) { + value = getBundleURL(); + bundleURL[id] = value; + } + return value; +} +function getBundleURL() { + try { + throw new Error(); + } catch (err) { + var matches = ('' + err.stack).match(/(https?|file|ftp|(chrome|moz|safari-web)-extension):\/\/[^)\n]+/g); + if (matches) // The first two stack frames will be this function and getBundleURLCached. + // Use the 3rd one, which will be a runtime in the original bundle. + return getBaseURL(matches[2]); + } + return '/'; +} +function getBaseURL(url) { + return ('' + url).replace(/^((?:https?|file|ftp|(chrome|moz|safari-web)-extension):\/\/.+)\/[^/]+$/, '$1') + '/'; +} +// TODO: Replace uses with `new URL(url).origin` when ie11 is no longer supported. +function getOrigin(url) { + var matches = ('' + url).match(/(https?|file|ftp|(chrome|moz|safari-web)-extension):\/\/[^/]+/); + if (!matches) throw new Error('Origin not found'); + return matches[0]; +} +exports.getBundleURL = getBundleURLCached; +exports.getBaseURL = getBaseURL; +exports.getOrigin = getOrigin; + +},{}],"aQN6X":[function(require,module,exports,__globalThis) { var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js"); parcelHelpers.defineInteropFlag(exports); parcelHelpers.export(exports, "getRunLevels", ()=>getRunLevels); diff --git a/src/game.less b/src/game.less index 7e90213..5b855f7 100644 --- a/src/game.less +++ b/src/game.less @@ -8,7 +8,6 @@ box-sizing: border-box; } -@purple: #6262ea; body { margin: 0; @@ -648,8 +647,12 @@ h2.histogram-title strong { left: 50vw; top: 50vh; font-size: 60px; - opacity: 0.9; + font-weight: bold; + opacity: 0.8; background: none; + text-shadow: 2px 0 #000, -2px 0 #000, 0 2px #000, 0 -2px #000, + 1px 1px #000, -1px -1px #000, 1px -1px #000, -1px 1px #000; + transform: translate(-50%,-50%); } } @@ -709,7 +712,7 @@ h2.histogram-title strong { position: relative; > div { - background: @purple; + background: @palette_b; position: absolute; inset: 0; transform-origin: top left; @@ -729,7 +732,7 @@ h2.histogram-title strong { content: ""; position: absolute; inset: 0; - background: linear-gradient(-45deg, @purple, transparent); + background: linear-gradient(-45deg, @palette_b, transparent); mix-blend-mode: screen; opacity: 0.3; } @@ -739,3 +742,25 @@ h2.histogram-title strong { opacity: 0.8; color: #8a8a8a; } + +@palette_B:black; +@palette_W:#FFFFFF; +@palette_g:#231f20; +@palette_y:#FFD300; +@palette_b:#6262EA; +@palette_t:#5DA3EA; +@palette_s:#E67070; +@palette_r:#e32119; +@palette_R:#ab0c0c; +@palette_c:#59EEA3; +@palette_G:#A1F051; +@palette_v:#A664E8; +@palette_p:#E869E8; +@palette_a:#5BECEC; +@palette_C:#53EE53; +@palette_S:#F44848; +@palette_P:#E66BA8; +@palette_O:#F29E4A; +@palette_k:#618227; +@palette_e:#e1c8b4; +@palette_l:#9b9fa; \ No newline at end of file diff --git a/src/game.ts b/src/game.ts index ef81827..178b31f 100644 --- a/src/game.ts +++ b/src/game.ts @@ -82,7 +82,7 @@ import { monitorLevelsUnlocks } from "./monitorLevelsUnlocks"; import { levelEditorMenuEntry } from "./levelEditor"; import { categories } from "./upgrades"; import { reasonLevelIsLocked } from "./get_level_unlock_condition"; -import { toast } from "./toast"; +import {clearToasts, toast} from "./toast"; export async function play() { if (await applyFullScreenChoice()) return; @@ -263,8 +263,12 @@ function startPlayCountDown() { play(); }, 3000), ); + timers.push(setTimeout(() => clearToasts(), 3500)); + } function stopPlayCountDown() { + if(!timers.length) return + clearToasts() timers.forEach((id) => clearTimeout(id)); timers.length = 0; } diff --git a/src/gameStateMutators.ts b/src/gameStateMutators.ts index 5bfef54..a5498df 100644 --- a/src/gameStateMutators.ts +++ b/src/gameStateMutators.ts @@ -12,7 +12,7 @@ import { import { brickCenterX, - brickCenterY, + brickCenterY, canvasCenterX, currentLevelInfo, distance2, distanceBetween, @@ -27,7 +27,7 @@ import { reachRedRowIndex, shouldPierceByColor, telekinesisEffectRate, - yoyoEffectRate, + yoyoEffectRate, zoneLeftBorderX, zoneRightBorderX, } from "./game_utils"; import { t } from "./i18n/i18n"; @@ -1184,7 +1184,7 @@ export function gameStateTick( if ( gameState.perks.wrap_right > 1 && hitBorder % 2 && - coin.previousX > gameState.offsetX + gameState.gameZoneWidth / 2 + coin.previousX > canvasCenterX(gameState) ) { schedulGameSound(gameState, "plouf", coin.x, 1); coin.x = gameState.offsetX + gameState.coinSize; @@ -1395,7 +1395,7 @@ export function gameStateTick( if (gameState.perks.wind) { const windD = ((gameState.puckPosition - - (gameState.offsetX + gameState.gameZoneWidth / 2)) / + canvasCenterX(gameState)) / gameState.gameZoneWidth) * 2 * gameState.perks.wind; @@ -1529,7 +1529,7 @@ export function gameStateTick( ) { makeParticle( gameState, - gameState.offsetXRoundedDown, + zoneLeftBorderX(gameState), Math.random() * gameState.gameZoneHeight, 5, (Math.random() - 0.5) * 10, @@ -1546,7 +1546,7 @@ export function gameStateTick( ) { makeParticle( gameState, - gameState.offsetXRoundedDown + gameState.gameZoneWidth, + zoneRightBorderX(gameState), Math.random() * gameState.gameZoneHeight, -5, (Math.random() - 0.5) * 10, @@ -1694,20 +1694,21 @@ export function ballTick(gameState: GameState, ball: Ball, frames: number) { if (gameState.perks.steering) { const delta = gameState.puckPosition - gameState.lastPuckPosition; - const angle = - Math.atan2(ball.vy, ball.vx) + - ((((delta / gameState.gameZoneWidth) * Math.PI) / 2) * - gameState.perks.steering * - frames) / - 2; - const d = Math.sqrt(ball.vy * ball.vy + ball.vx * ball.vx); - ball.vy = Math.sin(angle) * d; - ball.vx = Math.cos(angle) * d; - console.log({ - delta, - angle, - d, - }); + if(Math.abs(delta)>1) { + const angle = + Math.atan2(ball.vy, ball.vx) + + ((((delta / gameState.gameZoneWidth) * Math.PI) / 2) * + gameState.perks.steering * + frames) / + 2; + const d = Math.sqrt(ball.vy * ball.vy + ball.vx * ball.vx); + ball.vy = Math.sin(angle) * d; + ball.vx = Math.cos(angle) * d; + if (Math.random() < frames && !isOptionOn('basic')) { + makeParticle(gameState, ball.x, ball.y, -ball.vx / 10, -ball.vy / 10, + '#6262EA', true, 8, 500) + } + } } // Bounces diff --git a/src/game_utils.ts b/src/game_utils.ts index 34d606b..21c7178 100644 --- a/src/game_utils.ts +++ b/src/game_utils.ts @@ -12,6 +12,7 @@ import { t } from "./i18n/i18n"; import { clamp } from "./pure_functions"; import { getSettingValue, getTotalScore } from "./settings"; import { isOptionOn } from "./options"; +import {gameCanvas} from "./render"; export function describeLevel(level: Level) { let bricks = 0, @@ -345,3 +346,14 @@ export function escapeAttribute(str: String) { .replace(/"/gi, """) .replace(/'/gi, "'"); } + +export function canvasCenterX(gameState:GameState){ + return gameState.canvasWidth/2 +} +export function zoneLeftBorderX(gameState:GameState){ + + return gameState.offsetXRoundedDown - 1 +} +export function zoneRightBorderX(gameState:GameState){ + return gameCanvas.width - gameState.offsetXRoundedDown + 1 +} \ No newline at end of file diff --git a/src/render.ts b/src/render.ts index ac3f091..b784d4b 100644 --- a/src/render.ts +++ b/src/render.ts @@ -10,7 +10,7 @@ import { max_levels, reachRedRowIndex, telekinesisEffectRate, - yoyoEffectRate, + yoyoEffectRate, zoneLeftBorderX, zoneRightBorderX, } from "./game_utils"; import { colorString, GameState } from "./types"; import { t } from "./i18n/i18n"; @@ -517,9 +517,9 @@ export function render(gameState: GameState) { ctx, gameState, (redLeftSide && "#FF0000") || "#FFFFFF", - gameState.offsetXRoundedDown - 1, + zoneLeftBorderX(gameState), 0, - gameState.offsetXRoundedDown - 1, + zoneLeftBorderX(gameState), height, 1, ); @@ -528,9 +528,9 @@ export function render(gameState: GameState) { ctx, gameState, (redRightSide && "#FF0000") || "#FFFFFF", - width - gameState.offsetXRoundedDown + 1, + zoneRightBorderX(gameState), 0, - width - gameState.offsetXRoundedDown + 1, + zoneRightBorderX(gameState), height, 1, ); @@ -565,9 +565,9 @@ export function render(gameState: GameState) { ctx, gameState, "#FF0000", - gameState.offsetXRoundedDown, + zoneLeftBorderX(gameState), 1, - width - gameState.offsetXRoundedDown, + zoneRightBorderX(gameState), 1, 1, ); diff --git a/src/toast.ts b/src/toast.ts index 06ab73b..05866bb 100644 --- a/src/toast.ts +++ b/src/toast.ts @@ -3,13 +3,16 @@ div.classList = "hidden toast"; document.body.appendChild(div); let timeout: NodeJS.Timeout | undefined; export function toast(html: string, className = "") { + clearToasts() div.classList = "toast visible " + className; div.innerHTML = html; + timeout = setTimeout(clearToasts, 1500); +} + +export function clearToasts(){ if (timeout) { clearTimeout(timeout); + timeout = undefined } - timeout = setTimeout(() => { - timeout = undefined; - div.classList = "hidden toast"; - }, 1500); -} + div.classList = "hidden toast"; +} \ No newline at end of file