diff --git a/Readme.md b/Readme.md index 0aa2a63..3322d53 100644 --- a/Readme.md +++ b/Readme.md @@ -26,19 +26,12 @@ languages, I may add features again. ## To do - redo video presentation -- chill game mode, to just relax your mind : - - no 7 levels limit - - no upgrades offered at the end of the level - - get a random perk - - every 7 level it's replaced by another random perk - - every 7 levels, +10 base combo and +1 piece - - ## Done +- measured and improve the performance (test here https://breakout.lecaro.me/?stresstest) - added a few levels -- autoplay mode (with wake lock and computer play) +- autoplay mode (with wake lock and computer play https://breakout.lecaro.me/?autoplay ) - slower coins fall once they are past the paddle - in game level editor - allow loading newer save in outdated app (for rollback) @@ -373,6 +366,12 @@ languages, I may add features again. ## UX / gameplay +- chill game mode, to just relax your mind : + - no 7 levels limit + - no upgrades offered at the end of the level + - get a random perk + - every 7 level it's replaced by another random perk + - every 7 levels, +10 base combo and +1 piece - avoid showing a +1 and -1 at the same time when a combo increase is reset - explain to iOS users how to add the app to home screen to get fullscreen - delayed start on mobile to let users place the puck where they want diff --git a/dist/index.html b/dist/index.html index 051b428..f52e942 100644 --- a/dist/index.html +++ b/dist/index.html @@ -723,6 +723,7 @@ parcelHelpers.export(exports, "hasBrick", ()=>hasBrick); parcelHelpers.export(exports, "hitsSomething", ()=>hitsSomething); parcelHelpers.export(exports, "tick", ()=>tick); parcelHelpers.export(exports, "lastMeasuredFPS", ()=>lastMeasuredFPS); +parcelHelpers.export(exports, "startWork", ()=>startWork); parcelHelpers.export(exports, "creativeModeThreshold", ()=>creativeModeThreshold); parcelHelpers.export(exports, "openMainMenu", ()=>openMainMenu); parcelHelpers.export(exports, "confirmRestart", ()=>confirmRestart); @@ -979,7 +980,6 @@ function tick() { gameState.runStatistics.runTime += timeDeltaMs * frames; (0, _gameStateMutators.gameStateTick)(gameState, frames); } - startWork('render'); if (gameState.running || gameState.needsRender) { gameState.needsRender = false; (0, _render.render)(gameState); @@ -998,18 +998,20 @@ setInterval(()=>{ lastMeasuredFPS = FPSCounter; FPSCounter = 0; }, 1000); +const showStats = window.location.search.includes("stress"); let total = {}; let lastTick = performance.now(); let doing = ''; function startWork(what) { + if (!showStats) return; const newNow = performance.now(); if (doing) total[doing] = (total[doing] || 0) + (newNow - lastTick); lastTick = newNow; doing = what; } -setInterval(()=>{ +if (showStats) setInterval(()=>{ const totalTime = (0, _gameUtils.sumOfValues)(total); - console.log((0, _gameStateMutators.liveCount)(gameState.coins) + ' coins\n' + Object.entries(total).sort((a, b)=>b[1] - a[1]).filter((a)=>a[1] > 1).map((t)=>t[0] + ':' + (t[1] / totalTime * 100).toFixed(2) + '% (' + t[1] + 'ms)').join('\n')); + console.debug((0, _gameStateMutators.liveCount)(gameState.coins) + ' coins\n' + Object.entries(total).sort((a, b)=>b[1] - a[1]).filter((a)=>a[1] > 1).map((t)=>t[0] + ':' + (t[1] / totalTime * 100).toFixed(2) + '% (' + t[1] + 'ms)').join('\n')); total = {}; }, 2000); setInterval(()=>{ @@ -1435,30 +1437,31 @@ function restart(params) { (0, _gameStateMutators.setLevel)(gameState, 0); if (params?.computer_controlled) play(); } -if (window.location.search.includes("autoplay")) startComputerControlledGame(); -else if (window.location.search.includes("stress")) { +if (window.location.search.match(/autoplay|stress/)) { + startComputerControlledGame(); if (!(0, _options.isOptionOn)('show_fps')) (0, _options.toggleOption)('show_fps'); - restart({ - level: (0, _loadGameData.allLevels).find((l)=>l.name == 'Worms'), - perks: { - base_combo: 5000, - pierce: 20, - rainbow: 3, - sapper: 2, - etherealcoins: 1 - } - }); } else restart({}); function startComputerControlledGame() { const perks = { base_combo: 20, pierce: 3 }; - for(let i = 0; i < 10; i++){ - const u = (0, _gameUtils.sample)((0, _loadGameData.upgrades)); - perks[u.id] ||= Math.floor(Math.random() * u.max) + 1; + if (window.location.search.includes("stress")) Object.assign(perks, { + base_combo: 5000, + pierce: 20, + rainbow: 3, + sapper: 2, + etherealcoins: 1, + bricks_attract_ball: 1, + respawn: 3 + }); + else { + for(let i = 0; i < 10; i++){ + const u = (0, _gameUtils.sample)((0, _loadGameData.upgrades)); + perks[u.id] ||= Math.floor(Math.random() * u.max) + 1; + } + perks.superhot = 0; } - perks.superhot = 0; restart({ level: (0, _gameUtils.sample)((0, _loadGameData.allLevels).filter((l)=>l.color === "#000000")), computer_controlled: true, @@ -2287,7 +2290,7 @@ const rawUpgrades = [ threshold: 215000, gift: false, id: "bricks_attract_ball", - max: 3, + max: 1, name: (0, _i18N.t)("upgrades.bricks_attract_ball.name"), help: (lvl)=>(0, _i18N.t)("upgrades.bricks_attract_ball.tooltip", { count: lvl * 3 @@ -2421,14 +2424,22 @@ try { function getSettingValue(key, defaultValue) { return cachedSettings[key] ?? defaultValue; } +// We avoid using localstorage synchronously for perf reasons +let needsSaving = new Set(); function setSettingValue(key, value) { - cachedSettings[key] = value; + if (cachedSettings[key] !== value) { + needsSaving.add(key); + cachedSettings[key] = value; + } +} +setInterval(()=>{ try { - localStorage.setItem(key, JSON.stringify(value)); + for (let key of needsSaving)localStorage.setItem(key, JSON.stringify(cachedSettings[key])); + needsSaving.clear(); } catch (e) { console.warn(e); } -} +}, 500); function getTotalScore() { return getSettingValue("breakout_71_total_score", 0); } @@ -2439,7 +2450,7 @@ function getCurrentMaxParticles() { return getCurrentMaxCoins(); } function cycleMaxCoins() { - setSettingValue("max_coins", (getSettingValue("max_coins", 2) + 1) % 10); + setSettingValue("max_coins", (getSettingValue("max_coins", 2) + 1) % 7); } },{"@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"gkKU3":[function(require,module,exports,__globalThis) { @@ -2628,7 +2639,7 @@ const sounds = { }, plouf: (volume, pan)=>{ if (!(0, _options.isOptionOn)("sound")) return; - createSingleBounceSound(240, pan, volume * 0.5); + createSingleBounceSound(500, pan, volume * 0.5); // createWaterDropSound(800, pan, volume*0.2, 0.2,'triangle') }, comboIncreaseMaybe: (volume, pan, combo)=>{ @@ -3524,8 +3535,8 @@ function explodeBrick(gameState, index, ball, isExplosion) { gameState.levelSpawnedCoins += coinsToSpawn; gameState.runStatistics.coins_spawned += coinsToSpawn; gameState.runStatistics.bricks_broken++; - const maxCoins = (0, _settings.getCurrentMaxCoins)() * ((0, _options.isOptionOn)("basic") ? 0.5 : 1); - const spawnableCoins = liveCount(gameState.coins) > (0, _settings.getCurrentMaxCoins)() ? 1 : Math.floor(maxCoins - liveCount(gameState.coins)) / 3; + const maxCoins = (0, _settings.getCurrentMaxCoins)(); + const spawnableCoins = liveCount(gameState.coins) > (0, _settings.getCurrentMaxCoins)() ? 1 : Math.floor((maxCoins - liveCount(gameState.coins)) / 2); const pointsPerCoin = Math.max(1, Math.ceil(coinsToSpawn / spawnableCoins)); while(coinsToSpawn > 0){ const points = Math.min(pointsPerCoin, coinsToSpawn); @@ -3874,7 +3885,10 @@ frames = 1) { if (gameState.perks.helium && !(0, _options.isOptionOn)("basic") && Math.random() < 0.1 * frames) makeParticle(gameState, coin.x, coin.y, 0, dvy * 10, gameState.perks.metamorphosis || (0, _options.isOptionOn)("colorful_coins") ? coin.color : "#ffd300", 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.previousY < gameState.gameZoneHeight && coin.y > gameState.gameZoneHeight && coin.vy > 0 && speed > 20) schedulGameSound(gameState, "plouf", coin.x, (0, _pureFunctions.clamp)(speed, 20, 100) / 100 * 0.2); + if (coin.previousY < gameState.gameZoneHeight && coin.y > gameState.gameZoneHeight && coin.vy > 0 && speed > 20) { + schedulGameSound(gameState, "plouf", coin.x, (0, _pureFunctions.clamp)(speed, 20, 100) / 100 * 0.2); + if (!(0, _options.isOptionOn)('basic')) makeParticle(gameState, coin.x, gameState.gameZoneHeight, -coin.vx / 5, -coin.vy / 5, coin.color, false); + } 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); @@ -4298,6 +4312,7 @@ var _i18N = require("./i18n/i18n"); var _game = require("./game"); var _options = require("./options"); var _pureFunctions = require("./pure_functions"); +var _settings = require("./settings"); const gameCanvas = document.getElementById("game"); const ctx = gameCanvas.getContext("2d", { alpha: false @@ -4316,6 +4331,7 @@ const haloCanvasCtx = haloCanvas.getContext("2d", { }); const haloScale = 16; function render(gameState) { + (0, _game.startWork)('render:init'); const level = (0, _gameUtils.currentLevelInfo)(gameState); const hasCombo = gameState.combo > (0, _gameStateMutators.baseCombo)(gameState); const { width, height } = gameCanvas; @@ -4326,8 +4342,12 @@ function render(gameState) { }); else menuLabel.innerText = (0, _i18N.t)("play.menu_label"); const catchRate = gameState.levelSpawnedCoins ? (gameState.levelSpawnedCoins - gameState.levelLostCoins) / gameState.levelSpawnedCoins : 1; + (0, _game.startWork)('render:scoreDisplay'); scoreDisplay.innerHTML = ((0, _options.isOptionOn)("show_fps") || gameState.computer_controlled ? ` - + + ${Math.floor((0, _gameStateMutators.liveCount)(gameState.coins) / (0, _settings.getCurrentMaxCoins)() * 100)} % + / + ${0, _game.lastMeasuredFPS} FPS / @@ -4348,6 +4368,7 @@ function render(gameState) { scoreDisplay.className = gameState.computer_controlled && "computer_controlled" || gameState.lastScoreIncrease > gameState.levelTime - 500 && "active" || ""; // Clear if (!(0, _options.isOptionOn)("basic") && level.svg && level.color === "#000000") { + (0, _game.startWork)('render:halo:clear'); haloCanvasCtx.globalCompositeOperation = "source-over"; haloCanvasCtx.globalAlpha = 0.99; haloCanvasCtx.fillStyle = level.color; @@ -4355,14 +4376,17 @@ function render(gameState) { 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)=>{ const color = 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)=>{ haloCanvasCtx.globalAlpha = 0.3 * (1 - (0, _gameUtils.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; @@ -4370,6 +4394,7 @@ function render(gameState) { 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)=>{ const { x, y, time, color, size, duration } = flash; @@ -4383,6 +4408,7 @@ function render(gameState) { ctx.imageSmoothingQuality = "high"; 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) { @@ -4422,6 +4448,7 @@ function render(gameState) { 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"; @@ -4433,6 +4460,7 @@ function render(gameState) { drawBall(ctx, color, size, x, y); }); } + (0, _game.startWork)('render:explosionshake'); ctx.globalAlpha = 1; ctx.globalCompositeOperation = "source-over"; const lastExplosionDelay = Date.now() - gameState.lastExplosion + 5; @@ -4441,6 +4469,7 @@ function render(gameState) { 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)=>{ @@ -4452,6 +4481,7 @@ function render(gameState) { // (color === "#ffd300" && "#ffd300") || hollow && color || gameState.level.color, coin.a); }); + (0, _game.startWork)('render:ball shade'); // Black shadow around balls if (!(0, _options.isOptionOn)("basic")) { ctx.globalCompositeOperation = "source-over"; @@ -4460,8 +4490,10 @@ function render(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; @@ -4469,6 +4501,7 @@ function render(gameState) { 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; @@ -4477,6 +4510,7 @@ function render(gameState) { 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; @@ -4484,12 +4518,14 @@ function render(gameState) { ctx.globalCompositeOperation = "screen"; drawBall(ctx, color, size, x, y); }); + (0, _game.startWork)('render:extra_life'); if (gameState.perks.extra_life) { ctx.globalAlpha = 1; ctx.globalCompositeOperation = "source-over"; ctx.fillStyle = gameState.puckColor; for(let i = 0; i < gameState.perks.extra_life; i++)ctx.fillRect(gameState.offsetXRoundedDown, gameState.gameZoneHeight - gameState.puckHeight / 2 + 2 * i, gameState.gameZoneWidthRoundedUp, 1); } + (0, _game.startWork)('render:balls'); ctx.globalAlpha = 1; ctx.globalCompositeOperation = "source-over"; gameState.balls.forEach((ball)=>{ @@ -4517,10 +4553,11 @@ function render(gameState) { ctx.stroke(); } }); - // The puck + (0, _game.startWork)('render: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); + (0, _game.startWork)('render:combotext'); if (gameState.combo > 1) { ctx.globalCompositeOperation = "source-over"; const comboText = "x " + gameState.combo; @@ -4532,6 +4569,7 @@ function render(gameState) { 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); } + (0, _game.startWork)('render:Borders'); // Borders ctx.globalCompositeOperation = "source-over"; ctx.globalAlpha = 1; @@ -4549,6 +4587,7 @@ function render(gameState) { if (redTop) drawStraightLine(ctx, gameState, "#FF0000", gameState.offsetXRoundedDown, 1, width - gameState.offsetXRoundedDown, 1, 1); ctx.globalAlpha = 1; drawStraightLine(ctx, gameState, hasCombo && gameState.perks.compound_interest && "#FF0000" || (0, _options.isOptionOn)("mobile-mode") && "#FFFFFF" || "", gameState.offsetXRoundedDown, gameState.gameZoneHeight, width - gameState.offsetXRoundedDown, gameState.gameZoneHeight, 1); + (0, _game.startWork)('render:contrast'); if (!(0, _options.isOptionOn)("basic") && (0, _options.isOptionOn)("contrast") && level.svg && level.color === "#000000") { ctx.imageSmoothingEnabled = true; // haloCanvasCtx.globalCompositeOperation = 'multiply'; @@ -4562,9 +4601,11 @@ function render(gameState) { ctx.drawImage(haloCanvas, 0, 0, width, height); ctx.imageSmoothingEnabled = false; } + (0, _game.startWork)('render:breakout.lecaro.me?autoplay'); ctx.globalCompositeOperation = "source-over"; ctx.globalAlpha = 1; if ((0, _options.isOptionOn)("mobile-mode") && gameState.computer_controlled) drawText(ctx, "breakout.lecaro.me?autoplay", gameState.puckColor, gameState.puckHeight, gameState.canvasWidth / 2, gameState.gameZoneHeight + (gameState.canvasHeight - gameState.gameZoneHeight) / 2); + (0, _game.startWork)('render:mobile_press_to_play'); 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); // if(isOptionOn('mobile-mode')) { // ctx.globalCompositeOperation = "source-over"; @@ -4573,7 +4614,9 @@ function render(gameState) { // ctx.fillRect(0,gameState.gameZoneHeight, gameState.canvasWidth, gameState.canvasHeight-gameState.gameZoneHeight) // } // ctx.globalAlpha=1 + (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) { @@ -4860,7 +4903,7 @@ function askForWakeLock(gameState) { } } -},{"./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) { +},{"./gameStateMutators":"9ZeQl","./game_utils":"cEeac","./i18n/i18n":"eNPRm","./game":"edeGs","./options":"d5NoS","./pure_functions":"6pQh7","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3","./settings":"5blfu"}],"caCAf":[function(require,module,exports,__globalThis) { var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js"); parcelHelpers.defineInteropFlag(exports); parcelHelpers.export(exports, "addToTotalPlayTime", ()=>addToTotalPlayTime); @@ -4878,9 +4921,7 @@ var _asyncAlert = require("./asyncAlert"); var _upgrades = require("./upgrades"); var _levelEditor = require("./levelEditor"); function addToTotalPlayTime(ms) { - try { - (0, _settings.setSettingValue)('breakout_71_total_play_time', (0, _settings.getSettingValue)('breakout_71_total_play_time', 0) + ms); - } catch (e) {} + (0, _settings.setSettingValue)('breakout_71_total_play_time', (0, _settings.getSettingValue)('breakout_71_total_play_time', 0) + ms); } function gameOver(title, intro) { if (!(0, _game.gameState).running) return; diff --git a/src/game.ts b/src/game.ts index c004437..4cd9df3 100644 --- a/src/game.ts +++ b/src/game.ts @@ -452,7 +452,6 @@ startWork('gameStateTick') gameStateTick(gameState, frames); } -startWork('render') if (gameState.running || gameState.needsRender) { gameState.needsRender = false; render(gameState); @@ -478,10 +477,12 @@ setInterval(() => { FPSCounter = 0; }, 1000); +const showStats= window.location.search.includes("stress") let total={} let lastTick=performance.now(); let doing= '' -function startWork(what){ +export function startWork(what){ + if(!showStats) return const newNow=performance.now(); if(doing) { total[doing] = (total[doing]||0) + ( newNow-lastTick ) @@ -489,12 +490,12 @@ function startWork(what){ lastTick=newNow doing=what } +if(showStats) setInterval(()=>{ const totalTime = sumOfValues(total) - console.log( + console.debug( liveCount(gameState.coins) +' coins\n'+ Object.entries(total).sort((a,b)=>b[1]-a[1]).filter(a=>a[1]>1).map(t=>t[0]+':'+(t[1]/totalTime*100).toFixed(2)+'% ('+t[1]+'ms)').join('\n')) - total={} },2000) @@ -1040,27 +1041,29 @@ export function restart(params: RunParams) { play(); } } - if (window.location.search.includes("autoplay")) { + if (window.location.search.match(/autoplay|stress/) ) { startComputerControlledGame(); -} else if (window.location.search.includes("stress")) { - if(!isOptionOn('show_fps')) + if(!isOptionOn('show_fps')) toggleOption('show_fps') - restart({ - level:allLevels.find(l=>l.name=='Worms'), - perks:{base_combo:5000, pierce:20, rainbow:3, sapper:2, etherealcoins:1} - }); -}else { +} else { restart({}); } export function startComputerControlledGame() { - const perks: Partial = { base_combo: 20, pierce: 3 }; - for (let i = 0; i < 10; i++) { - const u = sample(upgrades); - perks[u.id] ||= Math.floor(Math.random() * u.max) + 1; + const perks: Partial = { base_combo: 20, pierce: 3 }; + if(window.location.search.includes("stress")){ + + Object.assign(perks,{base_combo:5000, pierce:20, rainbow:3, sapper:2, etherealcoins:1, bricks_attract_ball:1, respawn:3}) + + }else{ + for (let i = 0; i < 10; i++) { + const u = sample(upgrades); + + perks[u.id] ||= Math.floor(Math.random() * u.max) + 1; + } + perks.superhot = 0; } - perks.superhot = 0; restart({ level: sample(allLevels.filter((l) => l.color === "#000000")), computer_controlled: true, diff --git a/src/gameOver.ts b/src/gameOver.ts index 05215d2..366faa5 100644 --- a/src/gameOver.ts +++ b/src/gameOver.ts @@ -17,10 +17,7 @@ import { run } from "jest"; import { editRawLevelList } from "./levelEditor"; export function addToTotalPlayTime(ms: number) { - try { setSettingValue('breakout_71_total_play_time', getSettingValue('breakout_71_total_play_time',0)+ms) - - } catch (e) {} } export function gameOver(title: string, intro: string) { diff --git a/src/gameStateMutators.ts b/src/gameStateMutators.ts index 0f351aa..2942828 100644 --- a/src/gameStateMutators.ts +++ b/src/gameStateMutators.ts @@ -461,11 +461,11 @@ export function explodeBrick( gameState.runStatistics.coins_spawned += coinsToSpawn; gameState.runStatistics.bricks_broken++; - const maxCoins = getCurrentMaxCoins() * (isOptionOn("basic") ? 0.5 : 1); + const maxCoins = getCurrentMaxCoins() const spawnableCoins = liveCount(gameState.coins) > getCurrentMaxCoins() ? 1 - : Math.floor(maxCoins - liveCount(gameState.coins)) / 3; + : Math.floor((maxCoins - liveCount(gameState.coins)) /2) ; const pointsPerCoin = Math.max(1, Math.ceil(coinsToSpawn / spawnableCoins)); @@ -1220,6 +1220,9 @@ export function gameStateTick( const hitBorder = bordersHitCheck(gameState, coin, coin.size / 2, frames); if(coin.previousYgameState.gameZoneHeight && coin.vy>0 && speed > 20) { schedulGameSound(gameState, "plouf", coin.x, clamp(speed, 20,100)/100*0.2); + if(!isOptionOn('basic')){ + makeParticle(gameState, coin.x,gameState.gameZoneHeight, -coin.vx/5, -coin.vy/5, coin.color, false ) + } } if ( diff --git a/src/render.ts b/src/render.ts index ecd853f..dd68bd2 100644 --- a/src/render.ts +++ b/src/render.ts @@ -13,7 +13,7 @@ import { } from "./game_utils"; import { Coin, colorString, GameState } from "./types"; import { t } from "./i18n/i18n"; -import { gameState, lastMeasuredFPS } from "./game"; +import {gameState, lastMeasuredFPS, startWork} from "./game"; import { isOptionOn } from "./options"; import { catchRateBest, @@ -25,6 +25,7 @@ import { wallBouncedBest, wallBouncedGood, } from "./pure_functions"; +import {getCurrentMaxCoins} from "./settings"; export const gameCanvas = document.getElementById("game") as HTMLCanvasElement; export const ctx = gameCanvas.getContext("2d", { @@ -51,6 +52,7 @@ const haloCanvasCtx = haloCanvas.getContext("2d", { export const haloScale = 16; export function render(gameState: GameState) { +startWork('render:init') const level = currentLevelInfo(gameState); const hasCombo = gameState.combo > baseCombo(gameState); @@ -70,11 +72,14 @@ export function render(gameState: GameState) { ? (gameState.levelSpawnedCoins - gameState.levelLostCoins) / gameState.levelSpawnedCoins : 1; - +startWork('render:scoreDisplay') scoreDisplay.innerHTML = (isOptionOn("show_fps") || gameState.computer_controlled ? ` - + + ${Math.floor(liveCount(gameState.coins) / getCurrentMaxCoins() * 100)} % + / + ${lastMeasuredFPS} FPS / @@ -102,19 +107,20 @@ export function render(gameState: GameState) { (gameState.computer_controlled && "computer_controlled") || (gameState.lastScoreIncrease > gameState.levelTime - 500 && "active") || ""; - // Clear if (!isOptionOn("basic") && level.svg && level.color === "#000000") { + + startWork('render:halo:clear') haloCanvasCtx.globalCompositeOperation = "source-over"; haloCanvasCtx.globalAlpha = 0.99; haloCanvasCtx.fillStyle = level.color; haloCanvasCtx.fillRect(0, 0, width / haloScale, height / haloScale); const brightness = isOptionOn("extra_bright") ? 3 : 1; - haloCanvasCtx.globalCompositeOperation = "lighten"; haloCanvasCtx.globalAlpha = 0.1 + (0.5 * 10) / (liveCount(gameState.coins) + 10); + startWork('render:halo:coins') forEachLiveOne(gameState.coins, (coin) => { const color = getCoinRenderColor(gameState, coin); drawFuzzyBall( @@ -126,6 +132,7 @@ export function render(gameState: GameState) { ); }); + startWork('render:halo:balls') gameState.balls.forEach((ball) => { haloCanvasCtx.globalAlpha = 0.3 * (1 - ballTransparency(ball, gameState)); drawFuzzyBall( @@ -136,6 +143,8 @@ export function render(gameState: GameState) { ball.y / haloScale, ); }); + + startWork('render:halo:bricks') haloCanvasCtx.globalAlpha = 0.05; gameState.bricks.forEach((color, index) => { if (!color) return; @@ -151,6 +160,7 @@ export function render(gameState: GameState) { ); }); + startWork('render:halo:particles') haloCanvasCtx.globalCompositeOperation = "screen"; forEachLiveOne(gameState.particles, (flash) => { const { x, y, time, color, size, duration } = flash; @@ -174,6 +184,7 @@ export function render(gameState: GameState) { ctx.drawImage(haloCanvas, 0, 0, width, height); ctx.imageSmoothingEnabled = false; + startWork('render:halo:pattern') ctx.globalAlpha = 1; ctx.globalCompositeOperation = "multiply"; if (level.svg && background.width && background.complete) { @@ -225,6 +236,8 @@ export function render(gameState: GameState) { ctx.fillRect(0, 0, width, height); } } else { + + startWork('render:halo-basic') ctx.globalAlpha = 1; ctx.globalCompositeOperation = "source-over"; ctx.fillStyle = level.color || "#000"; @@ -237,6 +250,7 @@ export function render(gameState: GameState) { }); } +startWork('render:explosionshake') ctx.globalAlpha = 1; ctx.globalCompositeOperation = "source-over"; const lastExplosionDelay = Date.now() - gameState.lastExplosion + 5; @@ -249,7 +263,7 @@ export function render(gameState: GameState) { Math.sin(Date.now() + 36) * amplitude, ); } - +startWork('render:coins') // Coins ctx.globalAlpha = 1; forEachLiveOne(gameState.coins, (coin) => { @@ -272,6 +286,7 @@ export function render(gameState: GameState) { coin.a, ); }); + startWork('render:ball shade') // Black shadow around balls if (!isOptionOn("basic")) { ctx.globalCompositeOperation = "source-over"; @@ -289,10 +304,11 @@ export function render(gameState: GameState) { ); }); } - +startWork('render:bricks') ctx.globalCompositeOperation = "source-over"; renderAllBricks(); +startWork('render:lights') ctx.globalCompositeOperation = "screen"; forEachLiveOne(gameState.lights, (flash) => { const { x, y, time, color, size, duration } = flash; @@ -309,6 +325,7 @@ export function render(gameState: GameState) { ); }); +startWork('render:texts') ctx.globalCompositeOperation = "screen"; forEachLiveOne(gameState.texts, (flash) => { const { x, y, time, color, size, duration } = flash; @@ -318,6 +335,7 @@ export function render(gameState: GameState) { drawText(ctx, flash.text, color, size, x, y - elapsed / 10); }); +startWork('render:particles') forEachLiveOne(gameState.particles, (particle) => { const { x, y, time, color, size, duration } = particle; const elapsed = gameState.levelTime - time; @@ -325,6 +343,8 @@ export function render(gameState: GameState) { ctx.globalCompositeOperation = "screen"; drawBall(ctx, color, size, x, y); }); + +startWork('render:extra_life') if (gameState.perks.extra_life) { ctx.globalAlpha = 1; ctx.globalCompositeOperation = "source-over"; @@ -339,9 +359,9 @@ export function render(gameState: GameState) { } } +startWork('render:balls') ctx.globalAlpha = 1; ctx.globalCompositeOperation = "source-over"; - gameState.balls.forEach((ball) => { const drawingColor = gameState.ballsColor; const ballAlpha = 1 - ballTransparency(ball, gameState); @@ -390,10 +410,10 @@ export function render(gameState: GameState) { ctx.stroke(); } }); - // The puck + + startWork('render:puck') ctx.globalAlpha = 1; ctx.globalCompositeOperation = "source-over"; - drawPuck( ctx, gameState.puckColor, @@ -404,6 +424,7 @@ export function render(gameState: GameState) { gameState.perks.streak_shots && hasCombo ? getDashOffset(gameState) : -1, ); + startWork('render:combotext') if (gameState.combo > 1) { ctx.globalCompositeOperation = "source-over"; const comboText = "x " + gameState.combo; @@ -443,8 +464,8 @@ export function render(gameState: GameState) { ); } } + startWork('render:Borders') // Borders - ctx.globalCompositeOperation = "source-over"; ctx.globalAlpha = 1; @@ -527,6 +548,7 @@ export function render(gameState: GameState) { 1, ); + startWork('render:contrast') if ( !isOptionOn("basic") && isOptionOn("contrast") && @@ -547,6 +569,7 @@ export function render(gameState: GameState) { ctx.imageSmoothingEnabled = false; } + startWork('render:breakout.lecaro.me?autoplay') ctx.globalCompositeOperation = "source-over"; ctx.globalAlpha = 1; @@ -561,6 +584,7 @@ export function render(gameState: GameState) { (gameState.canvasHeight - gameState.gameZoneHeight) / 2, ); } + startWork('render:mobile_press_to_play') if (isOptionOn("mobile-mode") && !gameState.running) { drawText( ctx, @@ -580,8 +604,10 @@ export function render(gameState: GameState) { // ctx.fillRect(0,gameState.gameZoneHeight, gameState.canvasWidth, gameState.canvasHeight-gameState.gameZoneHeight) // } // ctx.globalAlpha=1 + startWork('render:askForWakeLock') askForWakeLock(gameState); + startWork('render:resetTransform') if (shaked) { ctx.resetTransform(); } diff --git a/src/settings.ts b/src/settings.ts index 3cb9d0f..81ab5ec 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -19,14 +19,25 @@ export function getSettingValue(key: string, defaultValue: T) { return (cachedSettings[key] as T) ?? defaultValue; } +// We avoid using localstorage synchronously for perf reasons +let needsSaving= new Set() export function setSettingValue(key: string, value: T) { - cachedSettings[key] = value; + if(cachedSettings[key] !==value){ + needsSaving.add(key) + cachedSettings[key] = value; + } +} +setInterval(()=>{ try { - localStorage.setItem(key, JSON.stringify(value)); + for(let key of needsSaving){ + localStorage.setItem(key, JSON.stringify(cachedSettings[key])); + } + needsSaving.clear() } catch (e) { console.warn(e); } -} +}, 500) + export function getTotalScore() { return getSettingValue("breakout_71_total_score", 0); @@ -39,5 +50,5 @@ export function getCurrentMaxParticles() { return getCurrentMaxCoins() } export function cycleMaxCoins() { - setSettingValue("max_coins", (getSettingValue("max_coins", 2) + 1) % 10); + setSettingValue("max_coins", (getSettingValue("max_coins", 2) + 1) % 7); } diff --git a/src/sounds.ts b/src/sounds.ts index e57c16f..a4d9f2e 100644 --- a/src/sounds.ts +++ b/src/sounds.ts @@ -33,7 +33,7 @@ export const sounds = { plouf: (volume: number, pan: number) => { if (!isOptionOn("sound")) return; - createSingleBounceSound(240, pan, volume*0.5); + createSingleBounceSound(500, pan, volume*0.5); // createWaterDropSound(800, pan, volume*0.2, 0.2,'triangle') }, diff --git a/src/upgrades.ts b/src/upgrades.ts index bb4e508..bc410f6 100644 --- a/src/upgrades.ts +++ b/src/upgrades.ts @@ -818,7 +818,7 @@ export const rawUpgrades = [ threshold: 215000, gift: false, id: "bricks_attract_ball", - max: 3, + max: 1, name: t("upgrades.bricks_attract_ball.name"), help: (lvl: number) => t("upgrades.bricks_attract_ball.tooltip", { count: lvl * 3 }),