Added statistics (the last ones weren't actually recording anything)

This commit is contained in:
Renan LE CARO 2025-02-23 21:17:22 +01:00
parent d952139eeb
commit c2e1924e52
3 changed files with 325 additions and 179 deletions

View file

@ -63,7 +63,8 @@ There's also an easy mode for kids (slower ball) and a color-blind mode (no colo
- puck bounce predictions rendered with particles or lines (requires big refactor) - puck bounce predictions rendered with particles or lines (requires big refactor)
## Engine ideas ## Engine ideas
- few puck bounces = more choices / upgrades
- disable zooming (for ios double tap)
- particles when bouncing on sides / top - particles when bouncing on sides / top
- show total score on end screen (score added to total) - show total score on end screen (score added to total)
- show stats on end screen compared to other runs - show stats on end screen compared to other runs

View file

@ -55,7 +55,7 @@ function resetCombo(x, y) {
} }
const lost = Math.max(0, prev - combo); const lost = Math.max(0, prev - combo);
if (lost) { if (lost) {
incrementRunStatistics('combo_resets', 1)
for (let i = 0; i < lost && i < 8; i++) { for (let i = 0; i < lost && i < 8; i++) {
setTimeout(() => sounds.comboDecrease(), i * 100); setTimeout(() => sounds.comboDecrease(), i * 100);
} }
@ -132,14 +132,13 @@ background.addEventListener("load", () => {
}) })
const fitSize = () => { const fitSize = () => {
const {width, height} = canvas.getBoundingClientRect(); const {width, height} = canvas.getBoundingClientRect();
canvas.width = width; canvas.width = width;
canvas.height = height; canvas.height = height;
ctx.fillStyle=currentLevelInfo()?.color||'black' ctx.fillStyle = currentLevelInfo()?.color || 'black'
ctx.globalAlpha=1 ctx.globalAlpha = 1
ctx.fillRect(0,0,width,height) ctx.fillRect(0, 0, width, height)
backgroundCanvas.width = width; backgroundCanvas.width = width;
backgroundCanvas.height = height; backgroundCanvas.height = height;
@ -244,7 +243,8 @@ function addToScore(coin) {
lastPlayedCoinGrab = Date.now() lastPlayedCoinGrab = Date.now()
sounds.coinCatch(coin.x) sounds.coinCatch(coin.x)
} }
incrementRunStatistics('caught_coins', coin.points) runStatistics.score+=coin.points
} }
@ -269,7 +269,8 @@ function resetBalls() {
sparks: 0, sparks: 0,
piercedSinceBounce: 0, piercedSinceBounce: 0,
hitSinceBounce: 0, hitSinceBounce: 0,
hitItem:[], hitItem: [],
sapperUses: 0,
}); });
} }
} }
@ -289,7 +290,7 @@ function putBallsAtPuck() {
vy: -baseSpeed, vy: -baseSpeed,
sx: 0, sx: 0,
sy: 0, sy: 0,
hitItem:[], hitItem: [],
hitSinceBounce: 0, hitSinceBounce: 0,
piercedSinceBounce: 0, piercedSinceBounce: 0,
// piercedSinceBounce: 0, // piercedSinceBounce: 0,
@ -377,6 +378,7 @@ async function openUpgradesPicker() {
textAfterButtons textAfterButtons
}); });
cb(); cb();
runStatistics.upgrades_picked++
} }
resetCombo(); resetCombo();
resetBalls(); resetBalls();
@ -394,6 +396,7 @@ function setLevel(l) {
levelStartScore = score; levelStartScore = score;
levelSpawnedCoins = 0; levelSpawnedCoins = 0;
levelMisses = 0; levelMisses = 0;
runStatistics.levelsPlayed++
resetCombo(); resetCombo();
recomputeTargetBaseSpeed(); recomputeTargetBaseSpeed();
@ -404,8 +407,6 @@ function setLevel(l) {
gridSize = lvl.size; gridSize = lvl.size;
fitSize(); fitSize();
} }
incrementRunStatistics('lvl_size_' + lvl.size, 1)
incrementRunStatistics('lvl_name_' + lvl.name, 1)
coins = []; coins = [];
bricks = [...lvl.bricks]; bricks = [...lvl.bricks];
flashes = []; flashes = [];
@ -445,7 +446,8 @@ const upgrades = [
"id": "extra_life", "id": "extra_life",
"name": "+1 life", "name": "+1 life",
"max": 7, "max": 7,
"help": "Survive dropping the ball once." "help": "Survive dropping the ball",
extraLevelsHelp: `One more life just in case`
}, },
{ {
"threshold": 0, "threshold": 0,
@ -462,28 +464,32 @@ const upgrades = [
"giftable": true, "giftable": true,
"name": "+3 base combo", "name": "+3 base combo",
"max": 7, "max": 7,
"help": "Your combo starts 3 points higher." "help": "Your combo starts at 4",
extraLevelsHelp: `Combo starts 3 points higher`
}, },
{ {
"threshold": 0, "threshold": 0,
"id": "slow_down", "id": "slow_down",
"name": "Slower ball", "name": "Slower ball",
"max": 2, "max": 2,
"help": "Slows down the ball." "help": "Slows down the ball",
extraLevelsHelp: `Make it even slower`
}, },
{ {
"threshold": 0, "threshold": 0,
"id": "bigger_puck", "id": "bigger_puck",
"name": "Bigger puck", "name": "Bigger puck",
"max": 2, "max": 2,
"help": "Catches more coins." "help": "Catches more coins",
extraLevelsHelp: `Even bigger puck`
}, },
{ {
"threshold": 0, "threshold": 0,
"id": "viscosity", "id": "viscosity",
"name": "Viscosity", "name": "Viscosity",
"max": 3, "max": 3,
"help": "Slower coins fall.", "help": "Slower coins fall",
extraLevelsHelp: `Even slower fall`,
tryout: { tryout: {
perks: {viscosity: 3, base_combo: 3}, perks: {viscosity: 3, base_combo: 3},
level: 'Waves' level: 'Waves'
@ -510,7 +516,8 @@ const upgrades = [
"id": "skip_last", "id": "skip_last",
"name": "Easy Cleanup", "name": "Easy Cleanup",
"max": 7, "max": 7,
"help": "The last brick will self-destruct." "help": "The last brick will self-destruct",
extraLevelsHelp: `Level clears one brick earlier`,
}, },
{ {
"threshold": 500, "threshold": 500,
@ -518,40 +525,44 @@ const upgrades = [
"giftable": true, "giftable": true,
"name": "Puck controls ball", "name": "Puck controls ball",
"max": 2, "max": 2,
"help": "Control the ball's trajectory." "help": "Control the ball's trajectory",
extraLevelsHelp: `Stronger effect on the ball`,
}, },
{ {
"threshold": 1000, "threshold": 1000,
"id": "coin_magnet", "id": "coin_magnet",
"name": "Coins magnet", "name": "Coins magnet",
"max": 3, "max": 3,
"help": "Puck attracts coins.", "help": "Puck attracts coins",
tryout: { tryout: {
perks: {coin_magnet: 3, base_combo: 3} perks: {coin_magnet: 3, base_combo: 3}
} }, extraLevelsHelp: `Stronger effect on the coins`,
}, },
{ {
"threshold": 1500, "threshold": 1500,
"id": "multiball", "id": "multiball",
"giftable": true, "giftable": true,
"name": "+1 ball", "name": "+1 ball",
"max": 3, "max": 6,
"help": "Start with one more balls.", "help": "Start with two balls",
extraLevelsHelp: `One more ball`,
}, },
{ {
"threshold": 2000, "threshold": 2000,
"id": "smaller_puck", "id": "smaller_puck",
"name": "Smaller puck", "name": "Smaller puck",
"max": 2, "max": 2,
"help": "Gives you more control." "help": "Gives you more control",
extraLevelsHelp: `Even smaller puck`,
}, },
{ {
"threshold": 3000, "threshold": 3000,
"id": "pierce", "id": "pierce",
"giftable": true, "giftable": true,
"name": "Heavy ball", "name": "Piercing",
"max": 3, "max": 3,
"help": "Ball pierces bricks." "help": "Ball pierces 3 bricks",
extraLevelsHelp: `Pierce 3 more bricks`,
}, },
{ {
"threshold": 4000, "threshold": 4000,
@ -560,7 +571,7 @@ const upgrades = [
"name": "Picky eater", "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: { tryout: {
perks: {picky_eater: 1}, perks: {picky_eater: 1},
@ -573,7 +584,7 @@ const upgrades = [
"name": "Stain", "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: { tryout: {
perks: {metamorphosis: 3}, perks: {metamorphosis: 3},
level: 'Lines' level: 'Lines'
@ -585,7 +596,8 @@ const upgrades = [
"giftable": true, "giftable": true,
"name": "Compound interest", "name": "Compound interest",
"max": 3, "max": 3,
"help": "Avoid missing coins with your puck." "help": "Avoid missing coins with your puck",
extraLevelsHelp: `Combo grows faster but missed coins hurt it more`,
}, },
{ {
"threshold": 7000, "threshold": 7000,
@ -593,23 +605,24 @@ const upgrades = [
"giftable": true, "giftable": true,
"name": "Hot start", "name": "Hot start",
"max": 3, "max": 3,
"help": "Clear the level quickly." "help": "Clear the level quickly",
extraLevelsHelp: `Combo starts higher but shrinks faster`,
}, },
{ {
"threshold": 9000, "threshold": 9000,
"id": "sapper", "id": "sapper",
"giftable": true, "giftable": true,
"name": "Sapper", "name": "Sapper",
"max": 1, "max": 7,
"help": "Bricks become bombs." "help": "1st brick hit becomes bomb",
extraLevelsHelp: `1 more brick replaced by a bomb`,
}, },
{ {
"threshold": 11000, "threshold": 11000,
"id": "bigger_explosions", "id": "bigger_explosions",
"name": "Kaboom", "name": "Kaboom",
"max": 1, "max": 1,
"help": "Bigger explosions.", "help": "Bigger explosions",
tryout: { tryout: {
perks: {bigger_explosions: 1}, perks: {bigger_explosions: 1},
level: 'Ship' level: 'Ship'
@ -620,7 +633,8 @@ const upgrades = [
"id": "extra_levels", "id": "extra_levels",
"name": "+1 level", "name": "+1 level",
"max": 3, "max": 3,
"help": "Play one more level before winning." "help": "Play 8 levels instead of 7",
extraLevelsHelp: `1 more brick replaced by a bomb`,
}, },
{ {
"threshold": 15000, "threshold": 15000,
@ -628,14 +642,15 @@ const upgrades = [
"name": "Color pierce", "name": "Color pierce",
"color_blind_exclude": true, "color_blind_exclude": true,
"max": 1, "max": 1,
"help": "Ball breaks same color bricks." "help": "Ball breaks same color bricks"
}, },
{ {
"threshold": 18000, "threshold": 18000,
"id": "soft_reset", "id": "soft_reset",
"name": "Soft reset", "name": "Soft reset",
"max": 2, "max": 2,
"help": "Combo grows slower but resets less" "help": "Combo grows slower but resets less",
extraLevelsHelp: `Even slower combo growth but softer reset`,
}, },
{ {
"threshold": 21000, "threshold": 21000,
@ -644,6 +659,7 @@ const upgrades = [
requires: 'multiball', requires: 'multiball',
"max": 3, "max": 3,
"help": "Balls repulse balls.", "help": "Balls repulse balls.",
extraLevelsHelp: 'Stronger repulsion force ',
tryout: { tryout: {
perks: {ball_repulse_ball: 1, multiball: 2}, perks: {ball_repulse_ball: 1, multiball: 2},
} }
@ -654,7 +670,7 @@ const upgrades = [
requires: 'multiball', requires: 'multiball',
"name": "Gravity", "name": "Gravity",
"max": 3, "max": 3,
"help": "Balls attract balls.", "help": "Balls attract balls.", extraLevelsHelp: 'Stronger attraction force ',
tryout: { tryout: {
perks: {ball_attract_ball: 1, multiball: 2}, perks: {ball_attract_ball: 1, multiball: 2},
} }
@ -663,6 +679,7 @@ const upgrades = [
"threshold": 30000, "threshold": 30000,
"id": "puck_repulse_ball", "id": "puck_repulse_ball",
"name": "Soft landing", "name": "Soft landing",
extraLevelsHelp: 'Stronger repulsion force ',
"max": 3, "max": 3,
"help": "Puck repulses balls.", "help": "Puck repulses balls.",
}, },
@ -671,7 +688,7 @@ const upgrades = [
"id": "wind", "id": "wind",
"name": "Wind", "name": "Wind",
"max": 3, "max": 3,
"help": "Puck position creates wind.", "help": "Puck position creates wind.", extraLevelsHelp: 'Stronger wind force ',
}, },
{ {
"threshold": 40000, "threshold": 40000,
@ -679,13 +696,14 @@ const upgrades = [
"name": "Sturdy bricks", "name": "Sturdy bricks",
"max": 4, "max": 4,
"help": "Bricks sometimes resist hits but drop more coins.", "help": "Bricks sometimes resist hits but drop more coins.",
extraLevelsHelp: 'Bricks resist more and drop more coins ',
}, },
{ {
"threshold": 45000, "threshold": 45000,
"id": "respawn", "id": "respawn",
"name": "Respawn", "name": "Respawn",
"max": 4, "max": 4,
"help": "The first brick hit will respawn.", "help": "The first brick hit will respawn.", extraLevelsHelp: 'More bricks can respawn ',
}, },
] ]
@ -759,7 +777,6 @@ function pickRandomUpgrades(count) {
.sort((a, b) => a.id > b.id ? 1 : -1) .sort((a, b) => a.id > b.id ? 1 : -1)
list.forEach(u => { list.forEach(u => {
incrementRunStatistics('offered_upgrade.' + u.id, 1)
dontOfferTooSoon(u.id) dontOfferTooSoon(u.id)
}) })
@ -768,10 +785,9 @@ function pickRandomUpgrades(count) {
icon: u.icon, icon: u.icon,
value: () => { value: () => {
perks[u.id]++; perks[u.id]++;
incrementRunStatistics('picked_upgrade.' + u.id, 1)
scoreStory.push("Picked upgrade : " + u.name); scoreStory.push("Picked upgrade : " + u.name);
}, },
help: u.help, help: (perks[u.id] && u.extraLevelsHelp) || u.help,
// max: u.max, // max: u.max,
// checked: perks[u.id] // checked: perks[u.id]
})) }))
@ -797,7 +813,6 @@ function restart() {
} }
const randomGift = reset_perks(); const randomGift = reset_perks();
incrementRunStatistics('starting_upgrade.' + randomGift, 1)
dontOfferTooSoon(randomGift) dontOfferTooSoon(randomGift)
setLevel(0); setLevel(0);
@ -983,6 +998,9 @@ function tick() {
if (running) { if (running) {
levelTime += currentTick - lastTick; levelTime += currentTick - lastTick;
runStatistics.runTime += currentTick - lastTick
runStatistics.max_combo = Math.max(runStatistics.max_combo, combo)
// How many time to compute // How many time to compute
let delta = Math.min(4, (currentTick - lastTick) / (1000 / 60)); let delta = Math.min(4, (currentTick - lastTick) / (1000 / 60));
delta *= running ? 1 : 0 delta *= running ? 1 : 0
@ -1006,7 +1024,6 @@ function tick() {
}); });
} }
if (!remainingBricks && !coins.length) { if (!remainingBricks && !coins.length) {
incrementRunStatistics('level_time', levelTime)
if (currentLevel + 1 < max_levels()) { if (currentLevel + 1 < max_levels()) {
setLevel(currentLevel + 1); setLevel(currentLevel + 1);
@ -1185,13 +1202,13 @@ function ballTick(ball, delta) {
resetCombo(ball.x, ball.y); resetCombo(ball.x, ball.y);
} }
if(perks.respawn){ if (perks.respawn) {
ball.hitItem.slice(0,-1).slice(0,perks.respawn) ball.hitItem.slice(0, -1).slice(0, perks.respawn)
.forEach(({index,color})=>bricks[index]=bricks[index]||color) .forEach(({index, color}) => bricks[index] = bricks[index] || color)
} }
ball.hitItem=[] ball.hitItem = []
if (!ball.hitSinceBounce) { if (!ball.hitSinceBounce) {
incrementRunStatistics('miss') runStatistics.misses++
levelMisses++; levelMisses++;
const loss = resetCombo(ball.x, ball.y) const loss = resetCombo(ball.x, ball.y)
if (ball.bouncesList?.length) { if (ball.bouncesList?.length) {
@ -1224,8 +1241,9 @@ function ballTick(ball, delta) {
} }
} }
incrementRunStatistics('puck_bounces') runStatistics.puck_bounces++
ball.hitSinceBounce = 0; ball.hitSinceBounce = 0;
ball.sapperUses = 0;
ball.piercedSinceBounce = 0; ball.piercedSinceBounce = 0;
ball.bouncesList = [{ ball.bouncesList = [{
x: ball.previousx, x: ball.previousx,
@ -1235,6 +1253,7 @@ function ballTick(ball, delta) {
if (ball.y > gameZoneHeight + ballSize / 2 && running) { if (ball.y > gameZoneHeight + ballSize / 2 && running) {
ball.destroyed = true; ball.destroyed = true;
runStatistics.balls_lost++
if (!balls.find((b) => !b.destroyed)) { if (!balls.find((b) => !b.destroyed)) {
if (perks.extra_life) { if (perks.extra_life) {
perks.extra_life--; perks.extra_life--;
@ -1262,10 +1281,11 @@ function ballTick(ball, delta) {
explodeBrick(hitBrick, ball, false); explodeBrick(hitBrick, ball, false);
if (perks.sapper && initialBrickColor !== "black" && if (ball.sapperUses < perks.sapper && initialBrickColor !== "black" &&
// don't replace a brick that bounced with sturdy_bricks // don't replace a brick that bounced with sturdy_bricks
!bricks[hitBrick]) { !bricks[hitBrick]) {
bricks[hitBrick] = "black"; bricks[hitBrick] = "black";
ball.sapperUses++
} }
} }
@ -1289,26 +1309,8 @@ function ballTick(ball, delta) {
} }
} }
let runStatistics = {}; let runStatistics = {};
function resetRunStatistics() {
runStatistics = {
started: Date.now(),
ended: null,
hadOverrides,
width: window.innerWidth,
height: window.innerHeight,
easy: isSettingOn('easy'),
color_blind: isSettingOn('color_blind'),
}
}
function incrementRunStatistics(key, amount = 1) {
runStatistics[key + '_total'] = (runStatistics[key + '_total'] || 0) + amount
runStatistics[key + '_lvl_' + currentLevel] = (runStatistics[key + '_lvl_' + currentLevel] || 0) + amount
}
function getTotalScore() { function getTotalScore() {
try { try {
@ -1326,12 +1328,13 @@ function addToTotalScore(points) {
} }
} }
function gameOver(title, intro) { function gameOver(title, intro) {
if (!running) return; if (!running) return;
pause() pause()
stopRecording() stopRecording()
runStatistics.ended = Date.now() runStatistics.max_level = currentLevel+1
const {stats} = getLevelStats(); const {stats} = getLevelStats();
@ -1341,17 +1344,6 @@ function gameOver(title, intro) {
} else { } else {
scoreStory.push(`You dropped the ball and finished your run early. `); scoreStory.push(`You dropped the ball and finished your run early. `);
} }
try {
// Stores only last 100 runs
const runsHistory = JSON.parse(localStorage.getItem('breakout_71_history') || '[]').slice(0, 99).concat([runStatistics])
// Generate some histogram
localStorage.setItem('breakout_71_history', '<pre>' + JSON.stringify(runsHistory, null, 2) + '</pre>')
} catch {
}
let animationDelay = -300 let animationDelay = -300
const getDelay = () => { const getDelay = () => {
animationDelay += 800 animationDelay += 800
@ -1396,6 +1388,80 @@ 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<binsCount;i++){
bins.push(0)
binsTotal.push(0)
}
const binSize = (max - min) / bins.length
const binIndexOf = v => 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 `<h2 class="histogram-title">${title} : <strong>${lastValue}${unit}</strong></h2><div class="histogram">
${bins.map((v, vi) => `<span class="${vi === activeBin ? 'active' : ''}"><span style="height:${v / maxBin * 80}px" title="${v} run${v>1 ? 's':''} between ${
Math.floor(min + vi * binSize)} and ${Math.floor(min + (vi + 1) * binSize)}${unit}"
><span>${
(!v && ' ') || (vi==activeBin && lastValue+unit) || (Math.round(binsTotal[vi]/v)+unit)
}</span></span></span>`).join('')}
</div>
`
}
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= `<p>Find below your run statistics compared to past runs.</p>`+ runStats
}
} catch (e) {
console.warn(e)
}
}
// Avoid the sad sound right as we restart a new games // Avoid the sad sound right as we restart a new games
combo = 1 combo = 1
asyncAlert({ asyncAlert({
@ -1403,19 +1469,35 @@ function gameOver(title, intro) {
<p>${intro}</p> <p>${intro}</p>
${unlocksInfo} ${unlocksInfo}
`, textAfterButtons: ` `, textAfterButtons: `
${runStats}
<div id="level-recording-container"></div> <div id="level-recording-container"></div>
${scoreStory.map((t) => "<p>" + t + "</p>").join("")} ${scoreStory.map((t) => "<p>" + t + "</p>").join("")}
` `
}).then(() => restart()); }).then(() => restart());
} }
function resetRunStatistics() {
runStatistics = {
started: Date.now(),
levelsPlayed: 0,
runTime: 0,
coins_spawned: 0,
score: 0,
bricks_broken:0,
misses:0,
balls_lost:0,
puck_bounces:0,
upgrades_picked:1,
max_combo:1
}
}
function explodeBrick(index, ball, isExplosion) { function explodeBrick(index, ball, isExplosion) {
const color = bricks[index]; const color = bricks[index];
if (color === 'black') { if (color === 'black') {
delete bricks[index]; delete bricks[index];
const x = brickCenterX(index), y = brickCenterY(index); const x = brickCenterX(index), y = brickCenterY(index);
incrementRunStatistics('explosion', 1)
sounds.explode(ball.x); sounds.explode(ball.x);
const {col, row} = getRowCol(index); const {col, row} = getRowCol(index);
const size = 1 + perks.bigger_explosions; const size = 1 + perks.bigger_explosions;
@ -1443,16 +1525,16 @@ function explodeBrick(index, ball, isExplosion) {
}); });
spawnExplosion(7 * (1 + perks.bigger_explosions), x, y, 'white', 150, coinSize,); spawnExplosion(7 * (1 + perks.bigger_explosions), x, y, 'white', 150, coinSize,);
ball.hitSinceBounce++; ball.hitSinceBounce++;
runStatistics.bricks_broken++
} else if (color) { } else if (color) {
// Even if it bounces we don't want to count that as a miss // Even if it bounces we don't want to count that as a miss
ball.hitSinceBounce++; ball.hitSinceBounce++;
if(perks.sturdy_bricks && perks.sturdy_bricks*2>Math.random()*10){ if (perks.sturdy_bricks && perks.sturdy_bricks * 2 > Math.random() * 10) {
// Resist // Resist
sounds.coinBounce(ball.x, 1) sounds.coinBounce(ball.x, 1)
return return
} }
// Flashing is take care of by the tick loop // Flashing is take care of by the tick loop
const x = brickCenterX(index), y = brickCenterY(index); const x = brickCenterX(index), y = brickCenterY(index);
@ -1460,16 +1542,18 @@ function explodeBrick(index, ball, isExplosion) {
levelSpawnedCoins += combo; levelSpawnedCoins += combo;
incrementRunStatistics('spawned_coins', combo) runStatistics.coins_spawned+=combo
runStatistics.bricks_broken++
coins = coins.filter((c) => !c.destroyed); coins = coins.filter((c) => !c.destroyed);
let coinsToSpawn=combo let coinsToSpawn = combo
if(perks.sturdy_bricks){ if (perks.sturdy_bricks) {
// +10% per level // +10% per level
coinsToSpawn+=Math.ceil((10+perks.sturdy_bricks) / 10 * coinsToSpawn) coinsToSpawn += Math.ceil((10 + perks.sturdy_bricks) / 10 * coinsToSpawn)
} }
while (coinsToSpawn-- ) { while (coinsToSpawn--) {
// Avoids saturating the canvas with coins // Avoids saturating the canvas with coins
if (coins.length > MAX_COINS * (isSettingOn("basic") ? 0.5 : 1)) { if (coins.length > MAX_COINS * (isSettingOn("basic") ? 0.5 : 1)) {
// Just pick a random one // Just pick a random one
@ -1518,7 +1602,7 @@ function explodeBrick(index, ball, isExplosion) {
spawnExplosion(5 + combo, x, y, color, 100, coinSize / 2); spawnExplosion(5 + combo, x, y, color, 100, coinSize / 2);
} }
if(!bricks[index] ){ if (!bricks[index]) {
ball.hitItem?.push({ ball.hitItem?.push({
index, index,
color color
@ -2375,7 +2459,7 @@ document.getElementById("menu").addEventListener("click", (e) => {
const options = { const options = {
sound: { sound: {
default: true, name: `Game sounds`, help: `Can slow down some phones.`, default: true, name: `Game sounds`, help: `Can slow down some phones.`,
disabled:()=>false disabled: () => false
}, "mobile-mode": { }, "mobile-mode": {
default: window.innerHeight > window.innerWidth, default: window.innerHeight > window.innerWidth,
name: `Mobile mode`, name: `Mobile mode`,
@ -2383,30 +2467,30 @@ const options = {
afterChange() { afterChange() {
fitSize(); fitSize();
}, },
disabled:()=>false disabled: () => false
}, },
basic: { basic: {
default: false, name: `Fast mode`, help: `Simpler graphics for older devices.`, default: false, name: `Fast mode`, help: `Simpler graphics for older devices.`,
disabled:()=>false disabled: () => false
}, },
"easy": { "easy": {
default: false, name: `Easy mode`, help: `Slower ball as starting perk.`, restart: true, default: false, name: `Easy mode`, help: `Slower ball as starting perk.`, 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,
disabled:()=>false disabled: () => false
}, },
// Could not get the sharing to work without loading androidx and all the modern android things so for now i'll just disable sharing in the android app // Could not get the sharing to work without loading androidx and all the modern android things so for now i'll just disable sharing in the android app
"record": { "record": {
default: false, name: `Record gameplay videos`, help: `Get a video of each level.`, default: false, name: `Record gameplay videos`, help: `Get a video of each level.`,
disabled(){ disabled() {
return window.location.search.includes('isInWebView=true') return window.location.search.includes('isInWebView=true')
} }
}, },
gif: { gif: {
default: false, name: `Make a gif too`, help: `3x heavier, 2x smaller, 7s max`, default: false, name: `Make a gif too`, help: `3x heavier, 2x smaller, 7s max`,
disabled(){ disabled() {
return window.location.protocol === "file:" ||! isSettingOn('record') return window.location.protocol === "file:" || !isSettingOn('record')
} }
} }
}; };
@ -2417,9 +2501,9 @@ async function openSettingsPanel() {
const optionsList = []; const optionsList = [];
for (const key in options) { for (const key in options) {
if (options[key] ) if (options[key])
optionsList.push({ optionsList.push({
disabled:options[key].disabled(), disabled: options[key].disabled(),
checked: isSettingOn(key) ? 1 : 0, checked: isSettingOn(key) ? 1 : 0,
max: 1, text: options[key].name, help: options[key].help, value: () => { max: 1, text: options[key].name, help: options[key].help, value: () => {
toggleSetting(key) toggleSetting(key)
@ -2496,29 +2580,29 @@ Click an item above to start a test run with it.
...optionsList, ...optionsList,
(document.fullscreenEnabled || document.webkitFullscreenEnabled) &&(document.fullscreenElement!==null ?{ (document.fullscreenEnabled || document.webkitFullscreenEnabled) && (document.fullscreenElement !== null ? {
text: "Exit Fullscreen", text: "Exit Fullscreen",
help: "Might not work on some machines", help: "Might not work on some machines",
value() { value() {
if (document.exitFullscreen) { if (document.exitFullscreen) {
document.exitFullscreen(); document.exitFullscreen();
} else if (document.webkitCancelFullScreen) { } else if (document.webkitCancelFullScreen) {
document.webkitCancelFullScreen(); document.webkitCancelFullScreen();
}
} }
} } :
}: {
{ text: "Fullscreen",
text: "Fullscreen", help: "Might not work on some machines",
help: "Might not work on some machines", value() {
value() { const docel = document.documentElement
const docel = document.documentElement if (docel.requestFullscreen) {
if (docel.requestFullscreen) { docel.requestFullscreen();
docel.requestFullscreen(); } else if (docel.webkitRequestFullscreen) {
} else if (docel.webkitRequestFullscreen) { docel.webkitRequestFullscreen();
docel.webkitRequestFullscreen(); }
} }
} }),
}),
{ {
text: 'Reset Game', text: 'Reset Game',
help: "Erase high score and statistics", help: "Erase high score and statistics",
@ -2590,8 +2674,22 @@ function repulse(a, b, power, impactsBToo) {
a.vx -= dx * fact a.vx -= dx * fact
a.vy -= dy * fact a.vy -= dy * fact
const speed = 10 const speed = 10
const rand = 2 const rand = 2
flashes.push({
type: "particle",
duration: 100,
time: levelTime,
size: coinSize / 2,
color: rainbowColor(),
ethereal: true,
x: a.x,
y: a.y,
vx: -dx * speed + a.vx + (Math.random() - 0.5) * rand,
vy: -dy * speed + a.vy + (Math.random() - 0.5) * rand,
})
if (impactsBToo) {
flashes.push({ flashes.push({
type: "particle", type: "particle",
duration: 100, duration: 100,
@ -2599,26 +2697,12 @@ function repulse(a, b, power, impactsBToo) {
size: coinSize / 2, size: coinSize / 2,
color: rainbowColor(), color: rainbowColor(),
ethereal: true, ethereal: true,
x: a.x, x: b.x,
y: a.y, y: b.y,
vx: -dx * speed + a.vx + (Math.random() - 0.5) * rand, vx: dx * speed + b.vx + (Math.random() - 0.5) * rand,
vy: -dy * speed + a.vy + (Math.random() - 0.5) * rand, vy: dy * speed + b.vy + (Math.random() - 0.5) * rand,
}) })
if (impactsBToo) { }
flashes.push({
type: "particle",
duration: 100,
time: levelTime,
size: coinSize / 2,
color: rainbowColor(),
ethereal: true,
x: b.x,
y: b.y,
vx: dx * speed + b.vx + (Math.random() - 0.5) * rand,
vy: dy * speed + b.vy + (Math.random() - 0.5) * rand,
})
}
} }
@ -2638,32 +2722,32 @@ function attract(a, b, power) {
a.vx -= dx * fact a.vx -= dx * fact
a.vy -= dy * fact a.vy -= dy * fact
const speed = 10 const speed = 10
const rand = 2 const rand = 2
flashes.push({ flashes.push({
type: "particle", type: "particle",
duration: 100, duration: 100,
time: levelTime, time: levelTime,
size: coinSize / 2, size: coinSize / 2,
color: rainbowColor(), color: rainbowColor(),
ethereal: true, ethereal: true,
x: a.x, x: a.x,
y: a.y, y: a.y,
vx: dx * speed + a.vx + (Math.random() - 0.5) * rand, vx: dx * speed + a.vx + (Math.random() - 0.5) * rand,
vy: dy * speed + a.vy + (Math.random() - 0.5) * rand, vy: dy * speed + a.vy + (Math.random() - 0.5) * rand,
}) })
flashes.push({ flashes.push({
type: "particle", type: "particle",
duration: 100, duration: 100,
time: levelTime, time: levelTime,
size: coinSize / 2, size: coinSize / 2,
color: rainbowColor(), color: rainbowColor(),
ethereal: true, ethereal: true,
x: b.x, x: b.x,
y: b.y, y: b.y,
vx: -dx * speed + b.vx + (Math.random() - 0.5) * rand, vx: -dx * speed + b.vx + (Math.random() - 0.5) * rand,
vy: -dy * speed + b.vy + (Math.random() - 0.5) * rand, vy: -dy * speed + b.vy + (Math.random() - 0.5) * rand,
}) })
} }
let levelIconHTMLCanvas = document.createElement('canvas') let levelIconHTMLCanvas = document.createElement('canvas')
@ -2732,7 +2816,7 @@ function drawMainCanvasOnSmallCanvas() {
let nthGifFrame = 0, gifFrameReduction = 2 let nthGifFrame = 0, gifFrameReduction = 2
function recordGifFrame() { function recordGifFrame() {
if(nthGifFrame/60>7) return if (nthGifFrame / 60 > 7) return
gifCtx.globalCompositeOperation = 'screen' gifCtx.globalCompositeOperation = 'screen'
gifCtx.globalAlpha = 1 / gifFrameReduction gifCtx.globalAlpha = 1 / gifFrameReduction
gifCtx?.drawImage(canvas, offsetXRoundedDown, 0, gameZoneWidthRoundedUp, gameZoneHeight, 0, 0, gifCanvas.width, gifCanvas.height) gifCtx?.drawImage(canvas, offsetXRoundedDown, 0, gameZoneWidthRoundedUp, gameZoneHeight, 0, 0, gifCanvas.width, gifCanvas.height)
@ -2782,8 +2866,8 @@ function startRecordingGame() {
height: gifCanvas.height, height: gifCanvas.height,
dither: false, dither: false,
}); });
}else{ } else {
levelGif=null levelGif = null
} }
// drawMainCanvasOnSmallCanvas() // drawMainCanvasOnSmallCanvas()
@ -2862,12 +2946,10 @@ function resumeRecording() {
} }
function stopRecording() { function stopRecording() {
if (!isSettingOn('record')) { if (!isSettingOn('record')) {
return return
} }
if (!mediaRecorder) return; if (!mediaRecorder) return;
mediaRecorder?.stop() mediaRecorder?.stop()
levelGif?.render() levelGif?.render()
mediaRecorder = null mediaRecorder = null
@ -2879,6 +2961,11 @@ function captureFileName(ext) {
} }
fitSize() fitSize()
restart() restart()
tick(); tick();

View file

@ -84,13 +84,13 @@ body.black_puck #menu {
.popup > div { .popup > div {
margin: auto; margin: auto;
padding: 20px; padding: 20px 10px;
/*border: 1px solid white;*/ /*border: 1px solid white;*/
transform-origin: center; transform-origin: center;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
width: 90%; width: 100%;
max-width: 450px; max-width: 450px;
} }
@ -249,10 +249,11 @@ body.black_puck #menu {
margin: 40px; margin: 40px;
} }
#level-recording-container img,#level-recording-container video{ #level-recording-container img, #level-recording-container video {
max-width: 100%; max-width: 100%;
height: auto; height: auto;
} }
#level-recording-container a { #level-recording-container a {
display: block; display: block;
} }
@ -274,4 +275,61 @@ body.black_puck #menu {
} }
}
.histogram {
display: flex;
gap: 10px;
align-items: stretch;
margin: 10px 0 40px 0;
}
.histogram > span {
/* Hover zone */
flex-grow: 1;
width: 10px;
position: relative;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
.histogram > span.active > span{
background: #4049ca;
}
.histogram > span > span{
/*Visible bar*/
background: #1c1c2f;
width: 100%;
display: block;
border-radius: 5px;
min-height: 1px;
}
.histogram > span > span> span {
/*label */
position: absolute;
bottom: -20px;
pointer-events: none;
white-space: nowrap;
transform-origin: bottom left;
font-size: 13px;
text-align: center;
display: block;
left: 50%;
transform: translate(-50%,0);
}
.histogram > span:not(:hover):not(.active) > span> span {
opacity: 0;
}
h2.histogram-title {
color: #3b3f75;
font-size: 18px;
}
h2.histogram-title strong {
color: #4049ca;
} }