mirror of
https://gitlab.com/lecarore/breakout71.git
synced 2025-04-20 12:15:06 -04:00
Automatic deploy 29007124
This commit is contained in:
parent
c5351bfd35
commit
1cec9a7a0c
4 changed files with 132 additions and 144 deletions
|
@ -5,6 +5,7 @@ Break colourful bricks, catch bouncing coins and select powerful upgrades !
|
||||||
[Play now](https://breakout.lecaro.me/) -
|
[Play now](https://breakout.lecaro.me/) -
|
||||||
[Google Play](https://play.google.com/store/apps/details?id=me.lecaro.breakout) -
|
[Google Play](https://play.google.com/store/apps/details?id=me.lecaro.breakout) -
|
||||||
[itch.io](https://renanlecaro.itch.io/breakout71) -
|
[itch.io](https://renanlecaro.itch.io/breakout71) -
|
||||||
|
[SubReddit](https://www.reddit.com/r/Breakout71/)
|
||||||
[GitLab](https://gitlab.com/lecarore/breakout71) -
|
[GitLab](https://gitlab.com/lecarore/breakout71) -
|
||||||
[Donate](https://github.com/sponsors/renanlecaro)
|
[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 :
|
There are many possible perks left to implement :
|
||||||
|
|
||||||
- wrap left / right
|
- wrap left / right
|
||||||
|
- +1 upgrade per level but -2 choices
|
||||||
- n% of the broken bricks respawn when the ball touches the puck
|
- n% of the broken bricks respawn when the ball touches the puck
|
||||||
- bricks break 50% of the time but drop 50% more coins
|
- bricks break 50% of the time but drop 50% more coins
|
||||||
- wind (puck positions adds force to coins and balls)
|
- 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
|
The "engine" could be better
|
||||||
|
|
||||||
|
- convert captures to mp4 unsing ffmpeg wasm because reddit refuses webm files
|
||||||
- few puck bounces = more choices / upgrades
|
- few puck bounces = more choices / upgrades
|
||||||
- disable zooming (for ios double tap)
|
- disable zooming (for ios double tap)
|
||||||
- particles when bouncing on sides / top
|
- particles when bouncing on sides / top
|
||||||
|
@ -178,6 +181,8 @@ The "engine" could be better
|
||||||
- controller support on web/mobile
|
- controller support on web/mobile
|
||||||
- webgl rendering
|
- webgl rendering
|
||||||
- enable export of gameplay capture in webview
|
- 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
|
Some extra levels wouldn't hurt
|
||||||
|
|
||||||
|
|
|
@ -11,8 +11,8 @@ android {
|
||||||
applicationId = "me.lecaro.breakout"
|
applicationId = "me.lecaro.breakout"
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 29005750
|
versionCode = 29007124
|
||||||
versionName = "29005750"
|
versionName = "29007124"
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
useSupportLibrary = true
|
useSupportLibrary = true
|
||||||
|
|
|
@ -209,7 +209,6 @@ function spawnExplosion(count, x, y, color, duration = 150, size = coinSize) {
|
||||||
|
|
||||||
|
|
||||||
let score = 0;
|
let score = 0;
|
||||||
let scoreStory = [];
|
|
||||||
|
|
||||||
let lastexplosion = 0;
|
let lastexplosion = 0;
|
||||||
let highScore = parseFloat(localStorage.getItem("breakout-3-hs") || "0");
|
let highScore = parseFloat(localStorage.getItem("breakout-3-hs") || "0");
|
||||||
|
@ -307,7 +306,19 @@ let levelStartScore = 0;
|
||||||
let levelMisses = 0;
|
let levelMisses = 0;
|
||||||
let levelSpawnedCoins = 0;
|
let levelSpawnedCoins = 0;
|
||||||
|
|
||||||
function getLevelStats() {
|
|
||||||
|
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);
|
const catchRate = (score - levelStartScore) / (levelSpawnedCoins || 1);
|
||||||
|
|
||||||
let repeats = 1;
|
let repeats = 1;
|
||||||
|
@ -339,42 +350,23 @@ function getLevelStats() {
|
||||||
missesGain = " (+1 choice)"
|
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 => '<p>' + t + '</p>').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--) {
|
while (repeats--) {
|
||||||
const actions = pickRandomUpgrades(choices);
|
const actions = pickRandomUpgrades(choices);
|
||||||
if (!actions.length) break
|
if (!actions.length) break
|
||||||
let textAfterButtons = `<p>Upgrades picked so far : </p><p>${pickedUpgradesHTMl()}</p>
|
let textAfterButtons = `
|
||||||
<div id="level-recording-container"></div>`;
|
<p>Upgrades picked so far : </p><p>${pickedUpgradesHTMl()}</p>
|
||||||
|
<div id="level-recording-container"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
const cb = await asyncAlert({
|
const cb = await asyncAlert({
|
||||||
title: "Pick an upgrade " + (repeats ? "(" + (repeats + 1) + ")" : ""), actions, text, allowClose: false,
|
title: "Pick an upgrade " + (repeats ? "(" + (repeats + 1) + ")" : ""), actions,
|
||||||
|
text: `<p>
|
||||||
|
You caught ${score - levelStartScore} coins ${catchGain} out of ${levelSpawnedCoins} in ${Math.round(levelTime / 1000)} seconds${timeGain}.
|
||||||
|
You missed ${levelMisses} times ${missesGain}.</p>`,
|
||||||
|
allowClose: false,
|
||||||
textAfterButtons
|
textAfterButtons
|
||||||
});
|
});
|
||||||
cb();
|
cb();
|
||||||
|
@ -785,7 +777,6 @@ function pickRandomUpgrades(count) {
|
||||||
icon: u.icon,
|
icon: u.icon,
|
||||||
value: () => {
|
value: () => {
|
||||||
perks[u.id]++;
|
perks[u.id]++;
|
||||||
scoreStory.push("Picked upgrade : " + u.name);
|
|
||||||
},
|
},
|
||||||
help: (perks[u.id] && u.extraLevelsHelp) || u.help,
|
help: (perks[u.id] && u.extraLevelsHelp) || u.help,
|
||||||
// max: u.max,
|
// max: u.max,
|
||||||
|
@ -807,17 +798,12 @@ function restart() {
|
||||||
shuffleLevels(levelTime || score ? currentLevelInfo().name : null);
|
shuffleLevels(levelTime || score ? currentLevelInfo().name : null);
|
||||||
resetRunStatistics()
|
resetRunStatistics()
|
||||||
score = 0;
|
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();
|
const randomGift = reset_perks();
|
||||||
|
|
||||||
dontOfferTooSoon(randomGift)
|
dontOfferTooSoon(randomGift)
|
||||||
|
|
||||||
setLevel(0);
|
setLevel(0);
|
||||||
scoreStory.push(`You started playing with the upgrade "${upgrades.find(u => u.id === randomGift)?.name}" on the level "${runLevels[0].name}". `,);
|
|
||||||
|
|
||||||
pauseRecording()
|
pauseRecording()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1336,14 +1322,6 @@ function gameOver(title, intro) {
|
||||||
|
|
||||||
runStatistics.max_level = currentLevel+1
|
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
|
let animationDelay = -300
|
||||||
const getDelay = () => {
|
const getDelay = () => {
|
||||||
animationDelay += 800
|
animationDelay += 800
|
||||||
|
@ -1388,20 +1366,37 @@ function gameOver(title, intro) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let runStats = ''
|
|
||||||
if (!hadOverrides) {
|
|
||||||
|
|
||||||
|
|
||||||
|
// Avoid the sad sound right as we restart a new games
|
||||||
|
combo = 1
|
||||||
|
asyncAlert({
|
||||||
|
allowClose: true, title, text: `
|
||||||
|
<p>${intro}</p>
|
||||||
|
${unlocksInfo}
|
||||||
|
`, textAfterButtons: `
|
||||||
|
<div id="level-recording-container"></div>
|
||||||
|
${getHistograms(true)}
|
||||||
|
`
|
||||||
|
}).then(() => restart());
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHistograms(saveStats){
|
||||||
|
|
||||||
|
if (hadOverrides) {return''}
|
||||||
|
|
||||||
|
let runStats=''
|
||||||
try {
|
try {
|
||||||
// Stores only top 100 runs
|
// Stores only top 100 runs
|
||||||
let runsHistory = JSON.parse(localStorage.getItem('breakout_71_runs_history') || '[]');
|
let runsHistory = JSON.parse(localStorage.getItem('breakout_71_runs_history') || '[]');
|
||||||
runsHistory.sort((a,b)=>a.score-b.score).reverse()
|
runsHistory.sort((a,b)=>a.score-b.score).reverse()
|
||||||
runsHistory=runsHistory.slice(0, 100)
|
runsHistory=runsHistory.slice(0, 100)
|
||||||
console.log(runsHistory.map(r=>r.score))
|
|
||||||
runsHistory.push(runStatistics)
|
runsHistory.push(runStatistics)
|
||||||
|
|
||||||
// Generate some histogram
|
// Generate some histogram
|
||||||
|
if(saveStats) {
|
||||||
localStorage.setItem('breakout_71_runs_history', JSON.stringify(runsHistory, null, 2))
|
localStorage.setItem('breakout_71_runs_history', JSON.stringify(runsHistory, null, 2))
|
||||||
|
}
|
||||||
const makeHistogram = (title, getter, unit) => {
|
const makeHistogram = (title, getter, unit) => {
|
||||||
let values = runsHistory.map(h => getter(h) || 0)
|
let values = runsHistory.map(h => getter(h) || 0)
|
||||||
const min = Math.min(...values)
|
const min = Math.min(...values)
|
||||||
|
@ -1458,24 +1453,10 @@ function gameOver(title, intro) {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e)
|
console.warn(e)
|
||||||
}
|
}
|
||||||
|
return runStats
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Avoid the sad sound right as we restart a new games
|
|
||||||
combo = 1
|
|
||||||
asyncAlert({
|
|
||||||
allowClose: true, title, text: `
|
|
||||||
<p>${intro}</p>
|
|
||||||
${unlocksInfo}
|
|
||||||
`, textAfterButtons: `
|
|
||||||
${runStats}
|
|
||||||
<div id="level-recording-container"></div>
|
|
||||||
${scoreStory.map((t) => "<p>" + t + "</p>").join("")}
|
|
||||||
`
|
|
||||||
}).then(() => restart());
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetRunStatistics() {
|
function resetRunStatistics() {
|
||||||
runStatistics = {
|
runStatistics = {
|
||||||
started: Date.now(),
|
started: Date.now(),
|
||||||
|
@ -2435,8 +2416,9 @@ scoreDisplay.addEventListener("click", async (e) => {
|
||||||
const cb = await asyncAlert({
|
const cb = await asyncAlert({
|
||||||
title: ` ${score} points at level ${currentLevel + 1} / ${max_levels()}`,
|
title: ` ${score} points at level ${currentLevel + 1} / ${max_levels()}`,
|
||||||
text: `
|
text: `
|
||||||
|
|
||||||
|
<p>Upgrades picked so far : </p>
|
||||||
<p>${pickedUpgradesHTMl()}</p>
|
<p>${pickedUpgradesHTMl()}</p>
|
||||||
${scoreStory.map((t) => "<p>" + t + "</p>").join("")}
|
|
||||||
|
|
||||||
`, allowClose: true, actions: [{
|
`, allowClose: true, actions: [{
|
||||||
text: "New run", help: "Start a brand new run.", value: () => {
|
text: "New run", help: "Start a brand new run.", value: () => {
|
||||||
|
@ -2470,11 +2452,11 @@ const options = {
|
||||||
disabled: () => false
|
disabled: () => false
|
||||||
},
|
},
|
||||||
basic: {
|
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
|
disabled: () => false
|
||||||
},
|
},
|
||||||
"easy": {
|
"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
|
disabled: () => false
|
||||||
}, "color_blind": {
|
}, "color_blind": {
|
||||||
default: false, name: `Color blind mode`, help: `Removes mechanics about colors.`, restart: true,
|
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.
|
||||||
<a href="./privacy.html" target="_blank">Privacy Policy</a>
|
<a href="./privacy.html" target="_blank">Privacy Policy</a>
|
||||||
<a href="https://play.google.com/store/apps/details?id=me.lecaro.breakout" target="_blank">Google Play</a>
|
<a href="https://play.google.com/store/apps/details?id=me.lecaro.breakout" target="_blank">Google Play</a>
|
||||||
<a href="https://renanlecaro.itch.io/breakout71" target="_blank">itch.io</a>
|
<a href="https://renanlecaro.itch.io/breakout71" target="_blank">itch.io</a>
|
||||||
|
<a href="https://www.reddit.com/r/Breakout71/" target="_blank">Subreddit</a>
|
||||||
<a href="https://gitlab.com/lecarore/breakout71" target="_blank">Gitlab</a>
|
<a href="https://gitlab.com/lecarore/breakout71" target="_blank">Gitlab</a>
|
||||||
<a href="https://breakout.lecaro.me/" target="_blank">Web version</a>
|
<a href="https://breakout.lecaro.me/" target="_blank">Web version</a>
|
||||||
<span>v.${window.appVersion}</span>
|
<span>v.${window.appVersion}</span>
|
||||||
|
@ -2803,7 +2786,8 @@ function recordOneFrame() {
|
||||||
|
|
||||||
|
|
||||||
function drawMainCanvasOnSmallCanvas() {
|
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.fillStyle = currentLevelInfo()?.black_puck ? '#000' : '#FFF'
|
||||||
recordCanvasCtx.textBaseline = "top";
|
recordCanvasCtx.textBaseline = "top";
|
||||||
recordCanvasCtx.font = "12px monospace";
|
recordCanvasCtx.font = "12px monospace";
|
||||||
|
@ -2843,15 +2827,10 @@ function startRecordingGame() {
|
||||||
gifCtx = gifCanvas.getContext("2d", {antialias: false, alpha: false})
|
gifCtx = gifCanvas.getContext("2d", {antialias: false, alpha: false})
|
||||||
}
|
}
|
||||||
|
|
||||||
let scale = 1
|
recordCanvas.width = gameZoneWidthRoundedUp
|
||||||
while (Math.max(gameZoneWidthRoundedUp, gameZoneHeight) * scale > 400 * 2) {
|
recordCanvas.height = gameZoneHeight
|
||||||
scale = scale / 2
|
gifCanvas.width = 400
|
||||||
}
|
gifCanvas.height = Math.floor( gameZoneHeight *(400/gameZoneWidthRoundedUp) )
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
// Gif worker won't work there
|
// Gif worker won't work there
|
||||||
|
@ -2882,8 +2861,13 @@ function startRecordingGame() {
|
||||||
}
|
}
|
||||||
|
|
||||||
instance.onstop = async function () {
|
instance.onstop = async function () {
|
||||||
let targetDiv = document.getElementById("level-recording-container")
|
let targetDiv ;
|
||||||
if (!targetDiv) return
|
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")
|
const video = document.createElement("video")
|
||||||
video.autoplay = true
|
video.autoplay = true
|
||||||
video.controls = false
|
video.controls = false
|
||||||
|
@ -2896,7 +2880,6 @@ function startRecordingGame() {
|
||||||
video.loop = true
|
video.loop = true
|
||||||
video.muted = true
|
video.muted = true
|
||||||
video.playsinline = true
|
video.playsinline = true
|
||||||
let blob = new Blob(recordedChunks, {type: "video/webm"});
|
|
||||||
video.src = URL.createObjectURL(blob);
|
video.src = URL.createObjectURL(blob);
|
||||||
|
|
||||||
const a = document.createElement("a")
|
const a = document.createElement("a")
|
||||||
|
|
|
@ -8,16 +8,16 @@
|
||||||
/>
|
/>
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||||
<title>Breakout 71</title>
|
<title>Breakout 71</title>
|
||||||
<link rel="stylesheet" href="style.css?v=29005750" />
|
<link rel="stylesheet" href="style.css?v=29007124" />
|
||||||
<link rel="icon" href="./icon.svg" />
|
<link rel="icon" href="./icon.svg" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<button id="menu">☰<span> menu</span></button>
|
<button id="menu">☰<span> menu</span></button>
|
||||||
<button id="score"></button>
|
<button id="score"></button>
|
||||||
<canvas id="game"></canvas>
|
<canvas id="game"></canvas>
|
||||||
<script>window.appVersion="?v=29005750".slice(3)</script>
|
<script>window.appVersion="?v=29007124".slice(3)</script>
|
||||||
<script src="gif.js"></script>
|
<script src="gif.js"></script>
|
||||||
<script src="levels.js?v=29005750"></script>
|
<script src="levels.js?v=29007124"></script>
|
||||||
<script src="game.js?v=29005750"></script>
|
<script src="game.js?v=29007124"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue