From 1cec9a7a0c37b0aed8304d82b91fb4fc453970d9 Mon Sep 17 00:00:00 2001 From: Renan LE CARO Date: Mon, 24 Feb 2025 21:04:31 +0100 Subject: [PATCH] Automatic deploy 29007124 --- Readme.md | 5 + app/build.gradle.kts | 4 +- app/src/main/assets/game.js | 259 +++++++++++++++------------------ app/src/main/assets/index.html | 8 +- 4 files changed, 132 insertions(+), 144 deletions(-) diff --git a/Readme.md b/Readme.md index fdfd3da..24d85bb 100644 --- a/Readme.md +++ b/Readme.md @@ -5,6 +5,7 @@ Break colourful bricks, catch bouncing coins and select powerful upgrades ! [Play now](https://breakout.lecaro.me/) - [Google Play](https://play.google.com/store/apps/details?id=me.lecaro.breakout) - [itch.io](https://renanlecaro.itch.io/breakout71) - +[SubReddit](https://www.reddit.com/r/Breakout71/) [GitLab](https://gitlab.com/lecarore/breakout71) - [Donate](https://github.com/sponsors/renanlecaro) @@ -127,6 +128,7 @@ There's also an easy mode for kids (slower ball) and a color-blind mode (no colo There are many possible perks left to implement : - wrap left / right +- +1 upgrade per level but -2 choices - n% of the broken bricks respawn when the ball touches the puck - bricks break 50% of the time but drop 50% more coins - wind (puck positions adds force to coins and balls) @@ -161,6 +163,7 @@ There are many possible perks left to implement : The "engine" could be better +- convert captures to mp4 unsing ffmpeg wasm because reddit refuses webm files - few puck bounces = more choices / upgrades - disable zooming (for ios double tap) - particles when bouncing on sides / top @@ -178,6 +181,8 @@ The "engine" could be better - controller support on web/mobile - webgl rendering - enable export of gameplay capture in webview +- endgame histograms could work as filters, when you hover a bar, all other histograms would show the stats of those runs only, without changing reference of categories + Some extra levels wouldn't hurt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4069a2c..34ab867 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -11,8 +11,8 @@ android { applicationId = "me.lecaro.breakout" minSdk = 21 targetSdk = 34 - versionCode = 29005750 - versionName = "29005750" + versionCode = 29007124 + versionName = "29007124" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary = true diff --git a/app/src/main/assets/game.js b/app/src/main/assets/game.js index 928f9fa..9e2c51c 100644 --- a/app/src/main/assets/game.js +++ b/app/src/main/assets/game.js @@ -209,7 +209,6 @@ function spawnExplosion(count, x, y, color, duration = 150, size = coinSize) { let score = 0; -let scoreStory = []; let lastexplosion = 0; let highScore = parseFloat(localStorage.getItem("breakout-3-hs") || "0"); @@ -307,8 +306,20 @@ let levelStartScore = 0; let levelMisses = 0; let levelSpawnedCoins = 0; -function getLevelStats() { - const catchRate = (score - levelStartScore) / (levelSpawnedCoins || 1); + +function pickedUpgradesHTMl() { + let list = '' + for (let u of upgrades) { + for (let i = 0; i < perks[u.id]; i++) + list += u.icon + ' ' + } + + return list +} + +async function openUpgradesPicker() { + + const catchRate = (score - levelStartScore) / (levelSpawnedCoins || 1); let repeats = 1; let choices = 3; @@ -339,42 +350,23 @@ function getLevelStats() { missesGain = " (+1 choice)" } - let stats = ` - You caught ${score - levelStartScore} coins ${catchGain} out of ${levelSpawnedCoins} in ${Math.round(levelTime / 1000)} seconds${timeGain}. - You missed ${levelMisses} times ${missesGain}. - `; - let text = [stats]; - - return { - stats, text: text.map(t => '

' + t + '

').join('\n'), repeats, choices, - }; -} - -function pickedUpgradesHTMl() { - let list = '' - for (let u of upgrades) { - for (let i = 0; i < perks[u.id]; i++) - list += u.icon + ' ' - } - - return list -} - -async function openUpgradesPicker() { - let {text, repeats, choices} = getLevelStats(); - scoreStory.push(`Finished level ${currentLevel + 1} (${currentLevelInfo().name}): ${text}`,); - while (repeats--) { const actions = pickRandomUpgrades(choices); if (!actions.length) break - let textAfterButtons = `

Upgrades picked so far :

${pickedUpgradesHTMl()}

-
`; + let textAfterButtons = ` +

Upgrades picked so far :

${pickedUpgradesHTMl()}

+
+`; const cb = await asyncAlert({ - title: "Pick an upgrade " + (repeats ? "(" + (repeats + 1) + ")" : ""), actions, text, allowClose: false, + title: "Pick an upgrade " + (repeats ? "(" + (repeats + 1) + ")" : ""), actions, + text: `

+You caught ${score - levelStartScore} coins ${catchGain} out of ${levelSpawnedCoins} in ${Math.round(levelTime / 1000)} seconds${timeGain}. + You missed ${levelMisses} times ${missesGain}.

`, + allowClose: false, textAfterButtons }); cb(); @@ -785,7 +777,6 @@ function pickRandomUpgrades(count) { icon: u.icon, value: () => { perks[u.id]++; - scoreStory.push("Picked upgrade : " + u.name); }, help: (perks[u.id] && u.extraLevelsHelp) || u.help, // max: u.max, @@ -807,17 +798,12 @@ function restart() { shuffleLevels(levelTime || score ? currentLevelInfo().name : null); resetRunStatistics() score = 0; - scoreStory = []; - if (hadOverrides) { - scoreStory.push(`This is a test run, started from the unlocks menu. It stops after one level and is not recorded in the stats. `) - } + const randomGift = reset_perks(); dontOfferTooSoon(randomGift) setLevel(0); - scoreStory.push(`You started playing with the upgrade "${upgrades.find(u => u.id === randomGift)?.name}" on the level "${runLevels[0].name}". `,); - pauseRecording() } @@ -1336,14 +1322,6 @@ function gameOver(title, intro) { runStatistics.max_level = currentLevel+1 - const {stats} = getLevelStats(); - - scoreStory.push(`During level ${currentLevel + 1} ${stats}`); - if (balls.find((b) => !b.destroyed)) { - scoreStory.push(`You cleared the last level and won. `); - } else { - scoreStory.push(`You dropped the ball and finished your run early. `); - } let animationDelay = -300 const getDelay = () => { animationDelay += 800 @@ -1388,78 +1366,6 @@ function gameOver(title, intro) { }) } - let runStats = '' - if (!hadOverrides) { - - try { - // Stores only top 100 runs - let runsHistory = JSON.parse(localStorage.getItem('breakout_71_runs_history') || '[]'); - runsHistory.sort((a,b)=>a.score-b.score).reverse() - runsHistory=runsHistory.slice(0, 100) - console.log(runsHistory.map(r=>r.score)) - runsHistory.push(runStatistics) - - // Generate some histogram - localStorage.setItem('breakout_71_runs_history', JSON.stringify(runsHistory, null, 2)) - - const makeHistogram = (title, getter, unit) => { - let values = runsHistory.map(h => getter(h) || 0) - const min = Math.min(...values) - const max = Math.max(...values) - // No point - if(min===max) return '' - // One bin per unique value, max 10 - const binsCount = Math.min(values.length,10) - if(binsCount<3) return '' - const bins = [] - const binsTotal = [] - for(let i=0;i Math.min(bins.length - 1, Math.floor((v - min) / binSize)) - values.forEach(v => { - if(isNaN(v)) return - const index=binIndexOf(v) - bins[index]++ - binsTotal[index]+=v - }) - if(bins.filter(b=>b).length<3) return '' - const maxBin = Math.max(...bins) - const lastValue = values[values.length - 1] - const activeBin = binIndexOf(lastValue) - return `

${title} : ${lastValue}${unit}

- ${bins.map((v, vi) => `${ - (!v && ' ') || (vi==activeBin && lastValue+unit) || (Math.round(binsTotal[vi]/v)+unit) - }`).join('')} -
- ` - } - - - runStats += makeHistogram('Total score', r => r.score, '') - runStats += makeHistogram('Catch rate', r => Math.round(r.score / r.coins_spawned * 100), '%') - runStats += makeHistogram('Bricks broken', r => r.bricks_broken, '') - runStats += makeHistogram('Bricks broken per minute', r =>Math.round(r.bricks_broken/r.runTime*1000*60), ' bpm') - runStats += makeHistogram('Hit rate', r => Math.round((1-r.misses / r.puck_bounces) * 100), '%') - runStats += makeHistogram('Duration per level', r => Math.round(r.runTime/1000/r.levelsPlayed), 's') - runStats += makeHistogram('Level reached', r => r.levelsPlayed, '') - runStats += makeHistogram('Upgrades applied', r => r.upgrades_picked, '') - runStats += makeHistogram('Balls lost', r => r.balls_lost, '') - runStats += makeHistogram('Average combo', r => Math.round(r.coins_spawned /r.bricks_broken) , '') - runStats += makeHistogram('Max combo', r => r.max_combo , '') - - if(runStats){ - runStats= `

Find below your run statistics compared to past runs.

`+ runStats - } - } catch (e) { - console.warn(e) - } - - } // Avoid the sad sound right as we restart a new games @@ -1469,13 +1375,88 @@ function gameOver(title, intro) {

${intro}

${unlocksInfo} `, textAfterButtons: ` - ${runStats}
- ${scoreStory.map((t) => "

" + t + "

").join("")} + ${getHistograms(true)} ` }).then(() => restart()); } +function getHistograms(saveStats){ + + if (hadOverrides) {return''} + + let runStats='' + try { + // Stores only top 100 runs + let runsHistory = JSON.parse(localStorage.getItem('breakout_71_runs_history') || '[]'); + runsHistory.sort((a,b)=>a.score-b.score).reverse() + runsHistory=runsHistory.slice(0, 100) + runsHistory.push(runStatistics) + + // Generate some histogram + if(saveStats) { + localStorage.setItem('breakout_71_runs_history', JSON.stringify(runsHistory, null, 2)) + } + const makeHistogram = (title, getter, unit) => { + let values = runsHistory.map(h => getter(h) || 0) + const min = Math.min(...values) + const max = Math.max(...values) + // No point + if(min===max) return '' + // One bin per unique value, max 10 + const binsCount = Math.min(values.length,10) + if(binsCount<3) return '' + const bins = [] + const binsTotal = [] + for(let i=0;i Math.min(bins.length - 1, Math.floor((v - min) / binSize)) + values.forEach(v => { + if(isNaN(v)) return + const index=binIndexOf(v) + bins[index]++ + binsTotal[index]+=v + }) + if(bins.filter(b=>b).length<3) return '' + const maxBin = Math.max(...bins) + const lastValue = values[values.length - 1] + const activeBin = binIndexOf(lastValue) + return `

${title} : ${lastValue}${unit}

+ ${bins.map((v, vi) => `${ + (!v && ' ') || (vi==activeBin && lastValue+unit) || (Math.round(binsTotal[vi]/v)+unit) + }`).join('')} +
+ ` + } + + + runStats += makeHistogram('Total score', r => r.score, '') + runStats += makeHistogram('Catch rate', r => Math.round(r.score / r.coins_spawned * 100), '%') + runStats += makeHistogram('Bricks broken', r => r.bricks_broken, '') + runStats += makeHistogram('Bricks broken per minute', r =>Math.round(r.bricks_broken/r.runTime*1000*60), ' bpm') + runStats += makeHistogram('Hit rate', r => Math.round((1-r.misses / r.puck_bounces) * 100), '%') + runStats += makeHistogram('Duration per level', r => Math.round(r.runTime/1000/r.levelsPlayed), 's') + runStats += makeHistogram('Level reached', r => r.levelsPlayed, '') + runStats += makeHistogram('Upgrades applied', r => r.upgrades_picked, '') + runStats += makeHistogram('Balls lost', r => r.balls_lost, '') + runStats += makeHistogram('Average combo', r => Math.round(r.coins_spawned /r.bricks_broken) , '') + runStats += makeHistogram('Max combo', r => r.max_combo , '') + + if(runStats){ + runStats= `

Find below your run statistics compared to past runs.

`+ runStats + } + } catch (e) { + console.warn(e) + } + return runStats +} + + function resetRunStatistics() { runStatistics = { started: Date.now(), @@ -2435,9 +2416,10 @@ scoreDisplay.addEventListener("click", async (e) => { const cb = await asyncAlert({ title: ` ${score} points at level ${currentLevel + 1} / ${max_levels()}`, text: ` -

${pickedUpgradesHTMl()}

- ${scoreStory.map((t) => "

" + t + "

").join("")} - + +

Upgrades picked so far :

+

${pickedUpgradesHTMl()}

+ `, allowClose: true, actions: [{ text: "New run", help: "Start a brand new run.", value: () => { restart(); @@ -2470,11 +2452,11 @@ const options = { disabled: () => false }, basic: { - default: false, name: `Fast mode`, help: `Simpler graphics for older devices.`, + default: false, name: `Basic graphics`, help: `Better performance on older devices.`, disabled: () => false }, "easy": { - default: false, name: `Easy mode`, help: `Slower ball as starting perk.`, restart: true, + default: false, name: `Kids mode`, help: `Starting perk always "slower ball".`, restart: true, disabled: () => false }, "color_blind": { default: false, name: `Color blind mode`, help: `Removes mechanics about colors.`, restart: true, @@ -2634,6 +2616,7 @@ Click an item above to start a test run with it. Privacy Policy Google Play itch.io + Subreddit Gitlab Web version v.${window.appVersion} @@ -2803,7 +2786,8 @@ function recordOneFrame() { function drawMainCanvasOnSmallCanvas() { - recordCanvasCtx?.drawImage(canvas, offsetXRoundedDown, 0, gameZoneWidthRoundedUp, gameZoneHeight, 0, 0, recordCanvas.width, recordCanvas.height) + if(!recordCanvasCtx) return + recordCanvasCtx.drawImage(canvas, offsetXRoundedDown, 0, gameZoneWidthRoundedUp, gameZoneHeight, 0, 0, recordCanvas.width, recordCanvas.height) recordCanvasCtx.fillStyle = currentLevelInfo()?.black_puck ? '#000' : '#FFF' recordCanvasCtx.textBaseline = "top"; recordCanvasCtx.font = "12px monospace"; @@ -2843,15 +2827,10 @@ function startRecordingGame() { gifCtx = gifCanvas.getContext("2d", {antialias: false, alpha: false}) } - let scale = 1 - while (Math.max(gameZoneWidthRoundedUp, gameZoneHeight) * scale > 400 * 2) { - scale = scale / 2 - } - console.log('Recording at scale ' + scale) - recordCanvas.width = gameZoneWidthRoundedUp * scale - recordCanvas.height = gameZoneHeight * scale - gifCanvas.width = Math.floor(gameZoneWidthRoundedUp * scale / 2) - gifCanvas.height = Math.floor(gameZoneHeight * scale / 2) + recordCanvas.width = gameZoneWidthRoundedUp + recordCanvas.height = gameZoneHeight + gifCanvas.width = 400 + gifCanvas.height = Math.floor( gameZoneHeight *(400/gameZoneWidthRoundedUp) ) // Gif worker won't work there @@ -2882,8 +2861,13 @@ function startRecordingGame() { } instance.onstop = async function () { - let targetDiv = document.getElementById("level-recording-container") - if (!targetDiv) return + let targetDiv ; + let blob = new Blob(recordedChunks, {type: "video/webm"}); + if(blob.size< 200000) return // under 0.2MB, probably bugged out or pointlessly short + + while(!(targetDiv = document.getElementById("level-recording-container"))){ + await new Promise(r=>setTimeout(r, 200)) + } const video = document.createElement("video") video.autoplay = true video.controls = false @@ -2896,7 +2880,6 @@ function startRecordingGame() { video.loop = true video.muted = true video.playsinline = true - let blob = new Blob(recordedChunks, {type: "video/webm"}); video.src = URL.createObjectURL(blob); const a = document.createElement("a") diff --git a/app/src/main/assets/index.html b/app/src/main/assets/index.html index 09d41af..cec9e1a 100644 --- a/app/src/main/assets/index.html +++ b/app/src/main/assets/index.html @@ -8,16 +8,16 @@ /> Breakout 71 - + - + - - + +