Automatic deploy 28999931

This commit is contained in:
Renan LE CARO 2025-02-19 21:11:22 +01:00
parent 0e95dbcbc8
commit 1141ffaf39
5 changed files with 143 additions and 96 deletions

View file

@ -57,6 +57,9 @@ There's also an easy mode for kids (slower ball) and a color-blind mode (no colo
- coins of different colors repulse - coins of different colors repulse
- bricks follow game of life pattern with one update every second - bricks follow game of life pattern with one update every second
- 2x coins when ball goes downward / upward, half that amount otherwise ? - 2x coins when ball goes downward / upward, half that amount otherwise ?
- new ball spawns when reaching combo X
- missing with combo triggers explosive lightning strike
- correction : pick one past upgrade to remove and replace by something else
## Engine ideas ## Engine ideas

View file

@ -11,8 +11,8 @@ android {
applicationId = "me.lecaro.breakout" applicationId = "me.lecaro.breakout"
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 34
versionCode = 28999417 versionCode = 28999931
versionName = "28999417" versionName = "28999931"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
useSupportLibrary = true useSupportLibrary = true

View file

@ -19,8 +19,8 @@ allLevels = allLevels.filter(l => {
return true return true
}) })
allLevels.forEach((l, li) => { allLevels.forEach((l, li) => {
l.threshold = li < 8 ? 0 : Math.round(Math.min(Math.pow(10, 1 + (li + l.size) / 30)* 10, 10000) * (li)) l.threshold = li < 8 ? 0 : Math.round(Math.min(Math.pow(10, 1 + (li + l.size) / 30) * 10, 10000) * (li))
l.sortKey = (Math.random()+3)/3.5 * l.bricks.filter(i=>i).length l.sortKey = (Math.random() + 3) / 3.5 * l.bricks.filter(i => i).length
}) })
let runLevels = [] let runLevels = []
@ -72,6 +72,7 @@ function resetCombo(x, y) {
}); });
} }
} }
return lost
} }
function decreaseCombo(by, x, y) { function decreaseCombo(by, x, y) {
@ -156,7 +157,7 @@ window.addEventListener("resize", fitSize);
function recomputeTargetBaseSpeed() { function recomputeTargetBaseSpeed() {
// We never want the ball to completely stop, it will move at least 3px per frame // We never want the ball to completely stop, it will move at least 3px per frame
baseSpeed = Math.max(3,gameZoneWidth / 12 / 10 + currentLevel / 3 + levelTime / (30 * 1000) - perks.slow_down * 2); baseSpeed = Math.max(3, gameZoneWidth / 12 / 10 + currentLevel / 3 + levelTime / (30 * 1000) - perks.slow_down * 2);
} }
@ -193,13 +194,12 @@ function spawnExplosion(count, x, y, color, duration = 150, size = coinSize) {
vx: (Math.random() - 0.5) * 30, vx: (Math.random() - 0.5) * 30,
vy: (Math.random() - 0.5) * 30, vy: (Math.random() - 0.5) * 30,
color, color,
duration:150 duration: 150
}); });
} }
} }
let score = 0; let score = 0;
let scoreStory = []; let scoreStory = [];
@ -337,7 +337,13 @@ function getLevelStats() {
} }
function pickedUpgradesHTMl() { function pickedUpgradesHTMl() {
return upgrades.filter(u => perks[u.id]).map(u => u.icon).join(' ') let list=''
for(let u of upgrades){
for(let i=0;i< perks[u.id];i++)
list+=u.icon+' '
}
return list
} }
async function openUpgradesPicker() { async function openUpgradesPicker() {
@ -347,7 +353,7 @@ async function openUpgradesPicker() {
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 = `<p>Upgrades picked so far : </p><p>${pickedUpgradesHTMl()}</p>`;
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, allowClose: false,
@ -457,9 +463,13 @@ const upgrades = [
{ {
"threshold": 0, "threshold": 0,
"id": "viscosity", "id": "viscosity",
"name": "Slower coins fall", "name": "Viscosity",
"max": 3, "max": 3,
"help": "Coins quickly decelerate." "help": "Slower coins fall.",
tryout: {
perks: {viscosity: 3, base_combo: 3},
level: 'Waves'
}
}, },
{ {
"threshold": 0, "threshold": 0,
@ -480,7 +490,7 @@ const upgrades = [
{ {
"threshold": 0, "threshold": 0,
"id": "skip_last", "id": "skip_last",
"name": "Last brick breaks", "name": "Easy Cleanup",
"max": 7, "max": 7,
"help": "The last brick will self-destruct." "help": "The last brick will self-destruct."
}, },
@ -495,9 +505,12 @@ const upgrades = [
{ {
"threshold": 1000, "threshold": 1000,
"id": "coin_magnet", "id": "coin_magnet",
"name": "Puck attracts coins", "name": "Coins magnet",
"max": 3, "max": 3,
"help": "Coins are drawn toward the puck." "help": "Puck attracts coins.",
tryout: {
perks: {coin_magnet: 3, base_combo: 3}
}
}, },
{ {
"threshold": 1500, "threshold": 1500,
@ -505,7 +518,7 @@ const upgrades = [
"giftable": true, "giftable": true,
"name": "+1 ball", "name": "+1 ball",
"max": 3, "max": 3,
"help": "Start with one more balls." "help": "Start with one more balls.",
}, },
{ {
"threshold": 2000, "threshold": 2000,
@ -518,26 +531,35 @@ const upgrades = [
"threshold": 3000, "threshold": 3000,
"id": "pierce", "id": "pierce",
"giftable": true, "giftable": true,
"name": "Ball pierces bricks", "name": "Heavy ball",
"max": 3, "max": 3,
"help": "Destroy 3 blocks before bouncing." "help": "Ball pierces bricks."
}, },
{ {
"threshold": 4000, "threshold": 4000,
"id": "picky_eater", "id": "picky_eater",
"giftable": true, "giftable": true,
"name": "Single color streak", "name": "Picky eater",
"color_blind_exclude": true, "color_blind_exclude": true,
"max": 1, "max": 1,
"help": "Break bricks color by color." "help": "Break bricks color by color.",
tryout: {
perks: {picky_eater: 1},
level: 'Mountain'
}
}, },
{ {
"threshold": 5000, "threshold": 5000,
"id": "metamorphosis", "id": "metamorphosis",
"name": "Coins stain bricks", "name": "Stain",
"color_blind_exclude": true, "color_blind_exclude": true,
"max": 1, "max": 1,
"help": "Coins color the bricks they touch." "help": "Coins color the bricks they touch.",
tryout: {
perks: {metamorphosis: 3},
level: 'Lines'
}
}, },
{ {
"threshold": 6000, "threshold": 6000,
@ -559,16 +581,21 @@ const upgrades = [
"threshold": 9000, "threshold": 9000,
"id": "sapper", "id": "sapper",
"giftable": true, "giftable": true,
"name": "Bricks become bombs", "name": "Sapper",
"max": 1, "max": 1,
"help": "Broken blocks become bombs." "help": "Bricks become bombs."
}, },
{ {
"threshold": 11000, "threshold": 11000,
"id": "bigger_explosions", "id": "bigger_explosions",
"name": "Bigger explosions", "name": "Kaboom",
"max": 1, "max": 1,
"help": "Larger bomb area of effect." "help": "Bigger explosions.",
tryout: {
perks: {bigger_explosions: 1},
level: 'Ship'
}
}, },
{ {
"threshold": 13000, "threshold": 13000,
@ -595,25 +622,31 @@ const upgrades = [
{ {
"threshold": 21000, "threshold": 21000,
"id": "ball_repulse_ball", "id": "ball_repulse_ball",
"name": "Balls repulse balls", "name": "Personal space",
requires: 'multiball', requires: 'multiball',
"max": 3, "max": 3,
"help": "Only has an effect with 2+ balls." "help": "Balls repulse balls.",
tryout: {
perks: {ball_repulse_ball: 1, multiball: 2},
}
}, },
{ {
"threshold": 25000, "threshold": 25000,
"id": "ball_attract_ball", "id": "ball_attract_ball",
requires: 'multiball', requires: 'multiball',
"name": "Balls attract balls", "name": "Gravity",
"max": 3, "max": 3,
"help": "Only has an effect with 2+ balls." "help": "Balls attract balls.",
tryout: {
perks: {ball_attract_ball: 1, multiball: 2},
}
}, },
{ {
"threshold": 30000, "threshold": 30000,
"id": "puck_repulse_ball", "id": "puck_repulse_ball",
"name": "Puck repulse balls", "name": "Soft landing",
"max": 3, "max": 3,
"help": "Prevents the puck from touching the balls.", "help": "Puck repulses balls.",
}, },
] ]
@ -629,16 +662,21 @@ function getPossibleUpgrades() {
function shuffleLevels(nameToAvoid = null) { function shuffleLevels(nameToAvoid = null) {
const target = nextRunOverrides?.level;
if (target) {
runLevels = allLevels.filter(l => l.name === target)
nextRunOverrides.level = null
if (runLevels.length) return
console.log('target level not found, will take random one : ' + target)
}
runLevels = allLevels runLevels = allLevels
.filter(l => nextRunOverrides.level ? l.name === nextRunOverrides.level : true)
.filter((l, li) => totalScoreAtRunStart >= l.threshold) .filter((l, li) => totalScoreAtRunStart >= l.threshold)
.filter(l => l.name !== nameToAvoid || allLevels.length === 1) .filter(l => l.name !== nameToAvoid || allLevels.length === 1)
.sort(() => Math.random() - 0.5) .sort(() => Math.random() - 0.5)
.slice(0, 7 + 3) .slice(0, 7 + 3)
.sort((a, b) => a.sortKey - b.sortKey); .sort((a, b) => a.sortKey - b.sortKey);
nextRunOverrides.level = null
} }
function getUpgraderUnlockPoints() { function getUpgraderUnlockPoints() {
@ -668,6 +706,10 @@ function getUpgraderUnlockPoints() {
let lastOffered = {} let lastOffered = {}
function dontOfferTooSoon(id){
lastOffered[id] = Math.round(Date.now() / 1000)
}
function pickRandomUpgrades(count) { function pickRandomUpgrades(count) {
let list = getPossibleUpgrades() let list = getPossibleUpgrades()
@ -679,11 +721,11 @@ function pickRandomUpgrades(count) {
list.forEach(u => { list.forEach(u => {
incrementRunStatistics('offered_upgrade.' + u.id, 1) incrementRunStatistics('offered_upgrade.' + u.id, 1)
lastOffered[u.id] = Math.round(Date.now() / 1000) dontOfferTooSoon(u.id)
}) })
return list.map(u => ({ return list.map(u => ({
text: u.name + (perks[u.id]?' lvl '+(perks[u.id]+1):''), text: u.name + (perks[u.id] ? ' lvl ' + (perks[u.id] + 1) : ''),
icon: u.icon, icon: u.icon,
value: () => { value: () => {
perks[u.id]++; perks[u.id]++;
@ -717,6 +759,7 @@ function restart() {
const randomGift = reset_perks(); const randomGift = reset_perks();
incrementRunStatistics('starting_upgrade.' + randomGift, 1) incrementRunStatistics('starting_upgrade.' + randomGift, 1)
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}". `,); scoreStory.push(`You started playing with the upgrade "${upgrades.find(u => u.id === randomGift)?.name}" on the level "${runLevels[0].name}". `,);
@ -1040,14 +1083,14 @@ function ballTick(ball, delta) {
for (b2 of balls) { for (b2 of balls) {
// avoid computing this twice, and repulsing itself // avoid computing this twice, and repulsing itself
if (b2.x >= ball.x) continue if (b2.x >= ball.x) continue
attract(ball, b2, perks.ball_attract_ball) attract(ball, b2, perks.ball_attract_ball)
} }
} }
if (perks.puck_repulse_ball) { if (perks.puck_repulse_ball && Math.abs(ball.x-puck)<puckWidth/2+ballSize*(9+perks.puck_repulse_ball)/10) {
repulse(ball, { repulse(ball, {
x: puck, x: puck,
y: gameZoneHeight, y: gameZoneHeight,
color: currentLevelInfo().black_puck ? '#000' : '#FFF', color: currentLevelInfo()?.black_puck ? '#000' : '#FFF',
}, perks.puck_repulse_ball, false) }, perks.puck_repulse_ball, false)
} }
@ -1079,17 +1122,19 @@ function ballTick(ball, delta) {
if (!ball.hitSinceBounce) { if (!ball.hitSinceBounce) {
incrementRunStatistics('miss') incrementRunStatistics('miss')
levelMisses++; levelMisses++;
flashes.push({ const loss=resetCombo(ball.x,ball.y)
type: "text", //
text: 'miss', // flashes.push({
time: levelTime, // type: "text",
color: ball.color, // text: 'miss',
x: ball.x, // time: levelTime,
y: ball.y - ballSize, // color: ball.color,
duration: 450, // x: ball.x,
size: puckHeight, // y: ball.y - ballSize,
}) // duration: 450,
if (ball.bouncesList?.length) { // size: puckHeight,
// })
if (ball.bouncesList?.length ) {
ball.bouncesList.push({ ball.bouncesList.push({
x: ball.previousx, x: ball.previousx,
y: ball.previousy y: ball.previousy
@ -1108,7 +1153,7 @@ function ballTick(ball, delta) {
ethereal: true, ethereal: true,
time: levelTime, time: levelTime,
size: coinSize / 2, size: coinSize / 2,
color: ball.color, color: loss?'red':ball.color,
x: start.x + (i / (parts - 1)) * (end.x - start.x), x: start.x + (i / (parts - 1)) * (end.x - start.x),
y: start.y + (i / (parts - 1)) * (end.y - start.y), y: start.y + (i / (parts - 1)) * (end.y - start.y),
vx: (Math.random() - 0.5) * baseSpeed, vx: (Math.random() - 0.5) * baseSpeed,
@ -1373,8 +1418,8 @@ function explodeBrick(index, ball, isExplosion) {
} }
combo += Math.max(0,perks.streak_shots + perks.catch_all_coins + perks.sides_are_lava + perks.top_is_lava + perks.picky_eater combo += Math.max(0, perks.streak_shots + perks.catch_all_coins + perks.sides_are_lava + perks.top_is_lava + perks.picky_eater
- Math.round(Math.random()*perks.soft_reset)); - Math.round(Math.random() * perks.soft_reset));
if (!isExplosion) { if (!isExplosion) {
// color change // color change
@ -1391,7 +1436,7 @@ function explodeBrick(index, ball, isExplosion) {
flashes.push({ flashes.push({
type: "ball", duration: 40, time: levelTime, size: brickWidth, color: color, x, y, type: "ball", duration: 40, time: levelTime, size: brickWidth, color: color, x, y,
}); });
spawnExplosion(5 + combo, x, y,color, 100, coinSize / 2); spawnExplosion(5 + combo, x, y, color, 100, coinSize / 2);
} }
} }
@ -2096,7 +2141,7 @@ function createExplosionSound(pan = 0.5) {
let levelTime = 0; let levelTime = 0;
setInterval(() => { setInterval(() => {
document.body.className = (running ? " running " : " paused ") + (currentLevelInfo().black_puck ? ' black_puck ' : ' '); document.body.className = (running ? " running " : " paused ") + (currentLevelInfo()?.black_puck ? ' black_puck ' : ' ');
}, 100); }, 100);
window.addEventListener("visibilitychange", () => { window.addEventListener("visibilitychange", () => {
@ -2292,43 +2337,43 @@ async function openSettingsPanel() {
help: "See and try what you've unlocked", help: "See and try what you've unlocked",
async value() { async value() {
const ts = getTotalScore() const ts = getTotalScore()
const actions=[...upgrades const actions = [...upgrades
.sort((a, b) => a.threshold - b.threshold) .sort((a, b) => a.threshold - b.threshold)
.map(({ .map(({
name, name,
max, max,
help, id, help, id,
threshold, icon threshold, icon, tryout
}) => ({ }) => ({
text: name, text: name,
help: ts >= threshold ? help :`Unlocks at total score ${threshold}.`, help: ts >= threshold ? help : `Unlocks at total score ${threshold}.`,
disabled: ts < threshold, disabled: ts < threshold,
value: {perks: {[id]: 1}}, value: tryout || {perks: {[id]: max}},
icon icon
}) })
) )
, ,
...allLevels ...allLevels
.sort((a, b) => a.threshold - b.threshold) .sort((a, b) => a.threshold - b.threshold)
.map((l, li) => { .map((l, li) => {
const avaliable = ts >= l.threshold const avaliable = ts >= l.threshold
return ({ return ({
text: l.name, text: l.name,
help: avaliable ? `A ${l.size}x${l.size} level with ${l.bricks.filter(i => i).length} bricks` : `Unlocks at total score ${l.threshold}.`, help: avaliable ? `A ${l.size}x${l.size} level with ${l.bricks.filter(i => i).length} bricks` : `Unlocks at total score ${l.threshold}.`,
disabled: !avaliable, disabled: !avaliable,
value: {level: l.name}, value: {level: l.name},
icon: levelIconHTML(l) icon: levelIconHTML(l)
})
}) })
] })
]
const tryOn = await asyncAlert({ const tryOn = await asyncAlert({
title: `You unlocked ${Math.round(actions.filter(a=>!a.disabled).length / actions.length * 100)}% of the game.`, title: `You unlocked ${Math.round(actions.filter(a => !a.disabled).length / actions.length * 100)}% of the game.`,
text: ` text: `
<p> Your total score is ${ts}. Below are all the upgrades and levels the games has to offer. They greyed out ones can be unlocked by increasing your total score. </p> <p> Your total score is ${ts}. Below are all the upgrades and levels the games has to offer. They greyed out ones can be unlocked by increasing your total score. </p>
`, `,
textAfterButtons:`<p> textAfterButtons: `<p>
The total score increases every time you score in game. The total score increases every time you score in game.
Your high score is ${highScore}. Your high score is ${highScore}.
Click an item above to start a test run with it. Click an item above to start a test run with it.
@ -2386,11 +2431,11 @@ Click an item above to start a test run with it.
} }
], ],
textAfterButtons: ` textAfterButtons: `
<p>Made in France by <a href="https://lecaro.me">Renan LE CARO</a><br/> <p>Breakout 71 build ${window.appVersion}, made in France by <a href="https://lecaro.me">Renan LE CARO</a><br/>
<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>
</p> </p>
` `
}) })
@ -2408,7 +2453,7 @@ function distanceBetween(a, b) {
} }
function rainbowColor() { function rainbowColor() {
return `hsl(${(levelTime / 2) % 360},100%,70%)` return `hsl(${Math.round((levelTime / 4)) * 2 % 360},100%,70%)`
} }
function repulse(a, b, power, impactsBToo) { function repulse(a, b, power, impactsBToo) {
@ -2420,8 +2465,6 @@ function repulse(a, b, power, impactsBToo) {
// Unit vector // Unit vector
const dx = (a.x - b.x) / distance const dx = (a.x - b.x) / distance
const dy = (a.y - b.y) / distance const dy = (a.y - b.y) / distance
// TODO
const fact = -power * (max - distance) / (max * 1.2) / 3 * Math.min(500, levelTime) / 500 const fact = -power * (max - distance) / (max * 1.2) / 3 * Math.min(500, levelTime) / 500
if (impactsBToo) { if (impactsBToo) {
b.vx += dx * fact b.vx += dx * fact
@ -2513,8 +2556,8 @@ function attract(a, b, power) {
let levelIconHTMLCanvas = document.createElement('canvas') let levelIconHTMLCanvas = document.createElement('canvas')
const levelIconHTMLCanvasCtx = levelIconHTMLCanvas.getContext("2d", {antialias: false, alpha: true}) const levelIconHTMLCanvasCtx = levelIconHTMLCanvas.getContext("2d", {antialias: false, alpha: true})
function levelIconHTML(level, title) { function levelIconHTML(level, title) {
const size=40 const size = 40
const c = levelIconHTMLCanvas const c = levelIconHTMLCanvas
const ctx = levelIconHTMLCanvasCtx const ctx = levelIconHTMLCanvasCtx
c.width = size c.width = size
@ -2539,7 +2582,7 @@ function levelIconHTML(level, title) {
return `<img title="${title || level.name}" alt="Icon for ${level.name}" width="${size}" height="${size}" src="${c.toDataURL()}"/>` return `<img title="${title || level.name}" alt="Icon for ${level.name}" width="${size}" height="${size}" src="${c.toDataURL()}"/>`
} }
upgrades.forEach(u => u.icon = levelIconHTML(perkIconsLevels[u.id], u.name)) upgrades.forEach(u => u.icon = levelIconHTML(perkIconsLevels[u.id], u.name))
fitSize() fitSize()
restart() restart()

View file

@ -8,14 +8,15 @@
/> />
<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=28999417" /> <link rel="stylesheet" href="style.css?v=28999931" />
<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 src="levels.js?v=28999417"></script> <script>window.appVersion="?v=28999931".slice(3)</script>
<script src="game.js?v=28999417"></script> <script src="levels.js?v=28999931"></script>
<script src="game.js?v=28999931"></script>
</body> </body>
</html> </html>

View file

@ -12,7 +12,7 @@ sed -i -e "s/^[[:space:]]*versionCode = .*/ versionCode = $versionCode/"
# Invalidate web cache # Invalidate web cache and update version
sed -i "s/\?v=[0-9]*/\?v=$versionCode/g" ./app/src/main/assets/index.html sed -i "s/\?v=[0-9]*/\?v=$versionCode/g" ./app/src/main/assets/index.html
# remove all exif metadata from pictures, because i think fdroid doesn't like that. odd # remove all exif metadata from pictures, because i think fdroid doesn't like that. odd