This commit is contained in:
Renan LE CARO 2025-02-17 00:49:03 +01:00
parent 0ff6ae1fdf
commit d3296c4f0f
2 changed files with 262 additions and 152 deletions

View file

@ -14,21 +14,21 @@ At the end of each level, you get to select an upgrade.
## TODO ## TODO
- Fdroid - Fdroid
- pause/resume audio context - perk : elastic between balls
- easily start a test game with specific upgrades or levels (with query string or through menu)
- 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
- handle back bouton in menu - handle back bouton in menu
- more levels : famous simple games, letters, fruits, animals - more levels : famous simple games, letters, fruits, animals
- perk : elastic between balls
- perk : wrap left / right - perk : wrap left / right
- perk : twice as many coins after a wall bounce, twice as little otherwise - perk : twice as many coins after a wall bounce, twice as little otherwise
- perk : fusion reactor (gather coins in one spot to triple their value) - perk : fusion reactor (gather coins in one spot to triple their value)
- perk : missing makes you loose all score of level, but otherwise multiplier goes up after each breaking - perk : missing makes you loose all score of level, but otherwise multiplier goes up after each breaking
- perk : n/10 of the broken bricks respawn when the ball comes back - perk : n/10 of the broken bricks respawn when the ball comes back
- perk : bricks take twice as many hits but drop 50% more coins - perk : bricks take twice as many hits but drop 50% more coins
- perk : wind (puck positions adds force to coins and balls) - perk : wind (puck positions adds force to coins and balls)
- perk : balls repulse each other
- perk : balls repulse coins - perk : balls repulse coins
- missing triggers and explosive lighting strike around ball path
## maybe ## maybe

View file

@ -9,7 +9,7 @@ const puckHeight = ballSize;
if (allLevels.find(l => l.focus)) { if (allLevels.find(l => l.focus)) {
allLevels = allLevels.filter(l => l.focus) allLevels = allLevels.filter(l => l.focus)
} }
allLevels=allLevels.filter(l=>!l.draft) allLevels = allLevels.filter(l => !l.draft)
let runLevels = [] let runLevels = []
@ -33,7 +33,7 @@ function baseCombo() {
return 1 + perks.base_combo * 3; return 1 + perks.base_combo * 3;
} }
function resetCombo(x, y ) { function resetCombo(x, y) {
const prev = combo; const prev = combo;
combo = baseCombo(); combo = baseCombo();
if (!levelTime) { if (!levelTime) {
@ -88,18 +88,20 @@ function decreaseCombo(by, x, y) {
let gridSize = 12; let gridSize = 12;
let running = false, puck = 400; let running = false, puck = 400;
function play(){
if(running) return function play() {
if (running) return
running = true running = true
if(audioContext){ if (audioContext) {
audioContext.resume() audioContext.resume()
} }
} }
function pause(){
if(!running) return function pause() {
if (!running) return
running = false running = false
needsRender=true needsRender = true
if(audioContext){ if (audioContext) {
audioContext.suspend() audioContext.suspend()
} }
} }
@ -191,7 +193,7 @@ function addToScore(coin) {
coin.destroyed = true coin.destroyed = true
score += coin.points; score += coin.points;
addToTotalScore(coin.points) addToTotalScore(coin.points)
if (score > highScore) { if (score > highScore && !hadOverrides) {
highScore = score; highScore = score;
localStorage.setItem("breakout-3-hs", score); localStorage.setItem("breakout-3-hs", score);
} }
@ -373,46 +375,49 @@ function reset_perks() {
perks[u.id] = 0; perks[u.id] = 0;
} }
const giftable = getPossibleUpgrades().filter(u => u.giftable) if (nextRunOverrides.perks) {
if (!giftable.length) { const first = Object.keys(nextRunOverrides.perks)[0]
debugger Object.assign(perks, nextRunOverrides.perks)
nextRunOverrides.perks = null
return first
} }
const giftable = getPossibleUpgrades().filter(u => u.giftable && u.max > 0)
const randomGift = isSettingOn('easy') ? 'slow_down' : giftable[Math.floor(Math.random() * giftable.length)].id; const randomGift = isSettingOn('easy') ? 'slow_down' : giftable[Math.floor(Math.random() * giftable.length)].id;
perks[randomGift] = 1; perks[randomGift] = 1;
// TODO // TODO
// perks.puck_repulse_ball = 3 // perks.puck_repulse_ball=3
// perks.multiball = 1 // perks.ball_repulse_ball=3
// perks.ball_repulse_ball = 1 // perks.ball_attract_ball=3
// perks.multiball=3
return randomGift return randomGift
} }
const upgrades = [{ const upgrades = [
{
minimumTotalScore: 3000, minimumTotalScore: 3000,
id: 'multiball', id: 'multiball',
giftableAfterTotalScore: 20000, giftable: true,
name: "+1 ball", name: "+1 ball",
max: 3, max: 3,
help: `Start each level with one more balls.`, help: `Start each level with one more balls.`,
}, { }, {
minimumTotalScore: 5000, minimumTotalScore: 5000,
id: 'pierce', id: 'pierce',
giftableAfterTotalScore: 15000, giftable: true,
name: "Ball pierces bricks", name: "Ball pierces bricks",
max: 3, max: 3,
help: `Pierce through 3 blocks after bouncing on the puck.`, help: `Pierce through 3 blocks after bouncing on the puck.`,
}, { }, {
minimumTotalScore: 500, minimumTotalScore: 500,
id: 'telekinesis', id: 'telekinesis',
giftableAfterTotalScore: 900, giftable: true,
name: "Puck controls ball", name: "Puck controls ball",
max: 2, max: 2,
help: `Control the ball's trajectory with the puck.`, help: `Control the ball's trajectory with the puck.`,
}, { }, {
minimumTotalScore: 0, minimumTotalScore: 0,
extra_levels_minimum_total_score: 250,
id: 'extra_life', id: 'extra_life',
name: "+1 life", name: "+1 life",
max: 3, max: 3,
@ -420,7 +425,7 @@ const upgrades = [{
}, { }, {
minimumTotalScore: 20000, minimumTotalScore: 20000,
id: 'sapper', id: 'sapper',
giftableAfterTotalScore: 32000, giftable: true,
name: "Bricks become bombs", name: "Bricks become bombs",
max: 1, max: 1,
help: `Broken blocks are replaced by bombs.`, help: `Broken blocks are replaced by bombs.`,
@ -460,7 +465,7 @@ const upgrades = [{
{ {
minimumTotalScore: 6000, minimumTotalScore: 6000,
id: 'picky_eater', id: 'picky_eater',
giftableAfterTotalScore: 9000, giftable: true,
name: "Single color streak", name: "Single color streak",
color_blind_exclude: true, color_blind_exclude: true,
max: 1, max: 1,
@ -479,7 +484,7 @@ const upgrades = [{
{ {
minimumTotalScore: 0, minimumTotalScore: 0,
id: 'streak_shots', id: 'streak_shots',
giftableAfterTotalScore: 1500, giftable: true,
name: "Single puck hit streak", name: "Single puck hit streak",
max: 1, max: 1,
help: `Break many bricks at once for more coins.`, help: `Break many bricks at once for more coins.`,
@ -488,7 +493,7 @@ const upgrades = [{
{ {
minimumTotalScore: 10000, minimumTotalScore: 10000,
id: 'hot_start', id: 'hot_start',
giftableAfterTotalScore: 24000, giftable: true,
name: "Hot start", name: "Hot start",
max: 3, max: 3,
help: `Clear the level quickly for more coins.`, help: `Clear the level quickly for more coins.`,
@ -497,14 +502,14 @@ const upgrades = [{
{ {
minimumTotalScore: 200, minimumTotalScore: 200,
id: 'sides_are_lava', id: 'sides_are_lava',
giftableAfterTotalScore: 500, giftable: true,
name: "Shoot straight", name: "Shoot straight",
max: 1, max: 1,
help: `Avoid the sides for more coins.`, help: `Avoid the sides for more coins.`,
}, { }, {
minimumTotalScore: 600, minimumTotalScore: 600,
id: 'top_is_lava', id: 'top_is_lava',
giftableAfterTotalScore: 1200, giftable: true,
name: "Sky is the limit", name: "Sky is the limit",
max: 1, max: 1,
help: `Avoid the top for more coins.`, help: `Avoid the top for more coins.`,
@ -513,13 +518,12 @@ const upgrades = [{
{ {
minimumTotalScore: 8000, minimumTotalScore: 8000,
id: 'catch_all_coins', id: 'catch_all_coins',
giftableAfterTotalScore: 16000, giftable: true,
name: "Compound interest", name: "Compound interest",
max: 3, max: 3,
help: `Catch all coins with your puck for even more coins.`, help: `Catch all coins with your puck for even more coins.`,
}, { }, {
minimumTotalScore: 0, minimumTotalScore: 0,
extra_levels_minimum_total_score: 6250,
id: 'viscosity', id: 'viscosity',
name: "Slower coins fall", name: "Slower coins fall",
max: 3, max: 3,
@ -528,9 +532,8 @@ const upgrades = [{
{ {
minimumTotalScore: 0, minimumTotalScore: 0,
extra_levels_minimum_total_score: 750,
id: 'base_combo', id: 'base_combo',
giftableAfterTotalScore: 0, giftable: true,
name: "+3 base combo", name: "+3 base combo",
max: 3, max: 3,
help: `Your combo starts 3 points higher.`, help: `Your combo starts 3 points higher.`,
@ -538,7 +541,6 @@ const upgrades = [{
{ {
minimumTotalScore: 0, minimumTotalScore: 0,
extra_levels_minimum_total_score: 25,
id: 'slow_down', id: 'slow_down',
name: "Slower ball", name: "Slower ball",
max: 2, max: 2,
@ -559,7 +561,6 @@ const upgrades = [{
minimumTotalScore: 3600, id: 'smaller_puck', name: "Smaller puck", max: 2, help: `Gives you more control.`, minimumTotalScore: 3600, id: 'smaller_puck', name: "Smaller puck", max: 2, help: `Gives you more control.`,
}, { }, {
minimumTotalScore: 0, minimumTotalScore: 0,
extra_levels_minimum_total_score: 0,
id: 'bigger_puck', id: 'bigger_puck',
name: "Bigger puck", name: "Bigger puck",
max: 2, max: 2,
@ -570,6 +571,12 @@ const upgrades = [{
name: "Balls repulse balls", name: "Balls repulse balls",
max: 3, max: 3,
help: `Only has an effect when 2+ balls.`, help: `Only has an effect when 2+ balls.`,
}, {
minimumTotalScore: 2000,
id: 'ball_attract_ball',
name: "Balls attract balls",
max: 3,
help: `Only has an effect when 2+ balls.`,
}, { }, {
minimumTotalScore: 4000, minimumTotalScore: 4000,
id: 'puck_repulse_ball', id: 'puck_repulse_ball',
@ -577,32 +584,17 @@ const upgrades = [{
max: 3, max: 3,
help: `Prevents the puck from touching the balls.`, help: `Prevents the puck from touching the balls.`,
} }
] ]
function computeUpgradeCurrentMaxLevel(u, ts) {
let max = 0
const setMax = (v) => max = Math.max(max, v)
if (u.max && ts >= u.minimumTotalScore) {
setMax(1)
}
if (u.max > 1) {
if (u.minimumTotalScore) {
setMax(Math.min(u.max, Math.floor(ts / u.minimumTotalScore)))
} else if (u.extra_levels_minimum_total_score) {
setMax(Math.min(u.max, Math.floor(ts / u.extra_levels_minimum_total_score) + 1))
}
}
return max
}
function getPossibleUpgrades() { function getPossibleUpgrades() {
const ts = getTotalScore() const ts = getTotalScore()
return upgrades return upgrades
.filter(u => !(isSettingOn('color_blind') && u.color_blind_exclude)) .filter(u => !(isSettingOn('color_blind') && u.color_blind_exclude))
.map(u => ({ .map(u => ({
...u, max: computeUpgradeCurrentMaxLevel(u, ts), giftable: ts >= (u.giftableAfterTotalScore ?? Infinity) ...u, max: ts > u.minimumTotalScore ? u.max:0, originalMax: u.max
})).filter(u => u.max > 0) }))
} }
function levelTotalScoreCondition(l, li) { function levelTotalScoreCondition(l, li) {
@ -610,13 +602,16 @@ function levelTotalScoreCondition(l, li) {
} }
function shuffleLevels(nameToAvoid = null) { function shuffleLevels(nameToAvoid = null) {
const ts = getTotalScore() const ts = getTotalScore();
runLevels = allLevels runLevels = allLevels
.filter(l => nextRunOverrides.level ? l.name === nextRunOverrides.level : true)
.filter((l, li) => ts >= levelTotalScoreCondition(l, li)) .filter((l, li) => ts >= levelTotalScoreCondition(l, li))
.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.bricks.filter(i => i).length - b.bricks.filter(i => i).length) .sort((a, b) => a.bricks.filter(i => i).length - b.bricks.filter(i => i).length);
nextRunOverrides.level = null
} }
function getUpgraderUnlockPoints() { function getUpgraderUnlockPoints() {
@ -628,32 +623,18 @@ function getUpgraderUnlockPoints() {
if (u.minimumTotalScore) { if (u.minimumTotalScore) {
list.push({ list.push({
threshold: u.minimumTotalScore, threshold: u.minimumTotalScore,
title: 'Unlock: ' + u.name, title: u.name + ' (Perk)',
help: 'This new perks will be added to the choices offered to you.' help: u.help,
}) })
} }
if (u.max > 1) {
for (var l = 1; l < u.max; l++) list.push({
threshold: l * (u.minimumTotalScore || u.extra_levels_minimum_total_score || 0),
title: 'Upgrade: ' + u.name,
help: 'You will be able to take this perk ' + (l + 1) + ' times for greater effect.'
})
}
if (u.giftableAfterTotalScore) {
list.push({
threshold: u.giftableAfterTotalScore,
title: 'Start: ' + u.name,
help: u.name + ' will be added to the list of possible starting perks.'
})
}
}) })
allLevels.forEach((l, li) => { allLevels.forEach((l, li) => {
list.push({ list.push({
threshold: levelTotalScoreCondition(l, li), threshold: levelTotalScoreCondition(l, li),
title: 'Level: ' + l.name, title: l.name + ' (Level)',
help: l.name + ' will be added to the list of possible levels.' // help: 'Adds level "'+l.name + '" to the list of possible levels.',
}) })
}) })
@ -663,7 +644,6 @@ function getUpgraderUnlockPoints() {
function pickRandomUpgrades(count) { function pickRandomUpgrades(count) {
let list = getPossibleUpgrades() let list = getPossibleUpgrades()
.sort(() => Math.random() - 0.5) .sort(() => Math.random() - 0.5)
.filter(u => perks[u.id] < u.max) .filter(u => perks[u.id] < u.max)
@ -688,15 +668,21 @@ function pickRandomUpgrades(count) {
return list; return list;
} }
let nextRunOverrides = {level: null, perks: null}
let hadOverrides = false
function restart() { function restart() {
console.log("restart") console.log("restart")
hadOverrides = !!(nextRunOverrides.level || nextRunOverrides.perks)
// When restarting, we want to avoid restarting with the same level we're on, so we exclude from the next // When restarting, we want to avoid restarting with the same level we're on, so we exclude from the next
// run's level list // run's level list
shuffleLevels(levelTime || score ? currentLevelInfo().name : null); shuffleLevels(levelTime || score ? currentLevelInfo().name : null);
resetRunStatistics() resetRunStatistics()
score = 0; score = 0;
scoreStory = []; 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();
incrementRunStatistics('starting_upgrade.' + randomGift, 1) incrementRunStatistics('starting_upgrade.' + randomGift, 1)
@ -734,9 +720,9 @@ function setMousePos(x) {
canvas.addEventListener("mouseup", (e) => { canvas.addEventListener("mouseup", (e) => {
if (e.button !== 0) return; if (e.button !== 0) return;
if(running) { if (running) {
pause() pause()
}else { } else {
play() play()
} }
}); });
@ -922,6 +908,7 @@ function tick() {
let playedCoinBounce = false; let playedCoinBounce = false;
const coinRadius = Math.round(coinSize / 2); const coinRadius = Math.round(coinSize / 2);
coins.forEach((coin) => { coins.forEach((coin) => {
if (coin.destroyed) return; if (coin.destroyed) return;
if (perks.coin_magnet) { if (perks.coin_magnet) {
@ -1008,33 +995,39 @@ function ballTick(ball, delta) {
ball.vx += ((puck - ball.x) / 1000) * delta * perks.telekinesis; ball.vx += ((puck - ball.x) / 1000) * delta * perks.telekinesis;
} }
const speedLimitDampener =1+ perks.telekinesis+perks.ball_repulse_ball +perks.puck_repulse_ball const speedLimitDampener = 1 + perks.telekinesis + perks.ball_repulse_ball + perks.puck_repulse_ball + perks.ball_attract_ball
if (ball.vx * ball.vx + ball.vy * ball.vy < baseSpeed * baseSpeed * 2) { if (ball.vx * ball.vx + ball.vy * ball.vy < baseSpeed * baseSpeed * 2) {
ball.vx *= (1 + .02/speedLimitDampener); ball.vx *= (1 + .02 / speedLimitDampener);
ball.vy *= (1 + .02/speedLimitDampener); ball.vy *= (1 + .02 / speedLimitDampener);
} else { } else {
ball.vx *= (1 - .02/speedLimitDampener);; ball.vx *= (1 - .02 / speedLimitDampener);
;
if (Math.abs(ball.vy) > 0.5 * baseSpeed) { if (Math.abs(ball.vy) > 0.5 * baseSpeed) {
ball.vy *= (1 - .02/speedLimitDampener);; ball.vy *= (1 - .02 / speedLimitDampener);
;
} }
} }
if(perks.ball_repulse_ball){ if (perks.ball_repulse_ball) {
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
repulse(ball,b2,15* perks.ball_repulse_ball ) repulse(ball, b2, perks.ball_repulse_ball, true)
} }
} }
if(perks.puck_repulse_ball){ if (perks.ball_attract_ball) {
repulse(ball,{ for (b2 of balls) {
x:puck, // avoid computing this twice, and repulsing itself
y:gameZoneHeight, if (b2.x >= ball.x) continue
vx:0, attract(ball, b2, 2 * perks.ball_attract_ball)
vy:0, }
color:currentLevelInfo().black_puck ? '#000' : '#FFF' , }
},15* perks.puck_repulse_ball ) if (perks.puck_repulse_ball) {
repulse(ball, {
x: puck,
y: gameZoneHeight,
color: currentLevelInfo().black_puck ? '#000' : '#FFF',
}, perks.puck_repulse_ball, false)
} }
@ -1080,14 +1073,14 @@ function ballTick(ball, delta) {
x: ball.previousx, x: ball.previousx,
y: ball.previousy y: ball.previousy
}) })
for(si=0; si< ball.bouncesList.length-1;si++){ for (si = 0; si < ball.bouncesList.length - 1; si++) {
// segement // segement
const start= ball.bouncesList[si] const start = ball.bouncesList[si]
const end= ball.bouncesList[si+1] const end = ball.bouncesList[si + 1]
const distance= distanceBetween(start,end) const distance = distanceBetween(start, end)
const parts = distance/30 const parts = distance / 30
for(var i = 0; i <parts;i++ ){ for (var i = 0; i < parts; i++) {
flashes.push({ flashes.push({
type: "particle", type: "particle",
duration: 200, duration: 200,
@ -1095,8 +1088,8 @@ function ballTick(ball, delta) {
time: levelTime, time: levelTime,
size: coinSize / 2, size: coinSize / 2,
color: ball.color, color: 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,
vy: (Math.random() - 0.5) * baseSpeed, vy: (Math.random() - 0.5) * baseSpeed,
}); });
@ -1109,8 +1102,8 @@ function ballTick(ball, delta) {
ball.hitSinceBounce = 0; ball.hitSinceBounce = 0;
ball.piercedSinceBounce = 0; ball.piercedSinceBounce = 0;
ball.bouncesList = [{ ball.bouncesList = [{
x: ball.previousx, x: ball.previousx,
y: ball.previousy y: ball.previousy
}] }]
} }
@ -1173,6 +1166,7 @@ function resetRunStatistics() {
runStatistics = { runStatistics = {
started: Date.now(), started: Date.now(),
ended: null, ended: null,
hadOverrides,
width: window.innerWidth, width: window.innerWidth,
height: window.innerHeight, height: window.innerHeight,
easy: isSettingOn('easy'), easy: isSettingOn('easy'),
@ -1195,6 +1189,7 @@ function getTotalScore() {
} }
function addToTotalScore(points) { function addToTotalScore(points) {
if (hadOverrides) return
try { try {
localStorage.setItem('breakout_71_total_score', JSON.stringify(getTotalScore() + points)) localStorage.setItem('breakout_71_total_score', JSON.stringify(getTotalScore() + points))
} catch (e) { } catch (e) {
@ -1238,7 +1233,7 @@ function gameOver(title, intro) {
const list = getUpgraderUnlockPoints() const list = getUpgraderUnlockPoints()
list.filter(u => u.threshold > startTs && u.threshold < endTs).forEach(u => { list.filter(u => u.threshold > startTs && u.threshold < endTs).forEach(u => {
unlocksInfo += ` unlocksInfo += `
<p class="progress" title=${JSON.stringify(u.help)}> <p class="progress" title=${JSON.stringify(u.help || '')}>
<span>${u.title}</span> <span>${u.title}</span>
<span class="progress_bar_part" style="${getDelay()}"></span> <span class="progress_bar_part" style="${getDelay()}"></span>
</p> </p>
@ -1253,10 +1248,11 @@ function gameOver(title, intro) {
const done = endTs - previousUnlockAt const done = endTs - previousUnlockAt
intro += `Score ${nextUnlock.threshold - endTs} more points to reach the next unlock.` intro += `Score ${nextUnlock.threshold - endTs} more points to reach the next unlock.`
const scaleX=(done / total).toFixed(2)
unlocksInfo += ` unlocksInfo += `
<p class="progress" title=${JSON.stringify(unlocksInfo.help)}> <p class="progress" title=${JSON.stringify(unlocksInfo.help)}>
<span>${nextUnlock.title}</span> <span>${nextUnlock.title}</span>
<span style="transform: scale(${(done / total).toFixed(2)},1);${getDelay()}" class="progress_bar_part"></span> <span style="transform: scale(${scaleX},1);${getDelay()}" class="progress_bar_part"></span>
</p> </p>
` `
@ -1270,7 +1266,7 @@ function gameOver(title, intro) {
} }
// 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({
allowClose: true, title, text: ` allowClose: true, title, text: `
<p>${intro}</p> <p>${intro}</p>
@ -1375,6 +1371,7 @@ function explodeBrick(index, ball, isExplosion) {
} }
function max_levels() { function max_levels() {
if (hadOverrides) return 1
return 7 + perks.extra_levels; return 7 + perks.extra_levels;
} }
@ -1432,9 +1429,13 @@ function render() {
const {x, y, time, color, size, type, duration} = flash; const {x, y, time, color, size, type, duration} = flash;
const elapsed = levelTime - time; const elapsed = levelTime - time;
ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2); ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2);
if (type === "ball" || type === "particle") { if (type === "ball") {
drawFuzzyBall(ctx, color, size, x, y); drawFuzzyBall(ctx, color, size, x, y);
} }
if (type === "particle") {
drawFuzzyBall(ctx, color, size * 3, x, y);
}
}); });
ctx.globalAlpha = 0.9; ctx.globalAlpha = 0.9;
@ -1565,10 +1566,10 @@ function render() {
ctx.globalCompositeOperation = "source-over"; ctx.globalCompositeOperation = "source-over";
drawPuck(ctx, puckColor, puckWidth, puckHeight) drawPuck(ctx, puckColor, puckWidth, puckHeight)
if (combo > 1) { if (combo > 1) {
ctx.globalCompositeOperation = "destination-out";
drawText(ctx, "x " + combo, "white", puckHeight, { ctx.globalCompositeOperation = "source-over";
drawText(ctx, "x " + combo, !level.black_puck ? '#000' : '#FFF', puckHeight, {
x: puck, y: gameZoneHeight - puckHeight / 2, x: puck, y: gameZoneHeight - puckHeight / 2,
}); });
} }
@ -2136,12 +2137,11 @@ function toggleSetting(key) {
scoreDisplay.addEventListener("click", async (e) => { scoreDisplay.addEventListener("click", async (e) => {
e.preventDefault(); e.preventDefault();
running=false running = false
const cb = await asyncAlert({ const cb = await asyncAlert({
title: `You scored ${score} points so far`, text: ` title: `You scored ${score} points so far`, text: `
<p>You are playing level ${currentLevel + 1} out of ${max_levels()}. </p> <p>You are playing level ${currentLevel + 1} out of ${max_levels()}. </p>
${scoreStory.map((t) => "<p>" + t + "</p>").join("")} ${scoreStory.map((t) => "<p>" + t + "</p>").join("")}
<p>You high score is ${highScore}.</p>
`, 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: () => {
restart(); restart();
@ -2200,6 +2200,57 @@ async function openSettingsPanel() {
const cb = await asyncAlert({ const cb = await asyncAlert({
title: "Breakout 71", text: ` title: "Breakout 71", text: `
`, allowClose: true, actions: [ `, allowClose: true, actions: [
{
text: 'Unlocks',
help: "See and try what you've unlocked",
async value() {
const ts = getTotalScore()
const tryOn = await asyncAlert({
title: 'Your unlocks',
text: `
<p>Your high score is ${highScore}. In total, you've cought ${ts} coins. Click an upgrade below to start a test run with it (stops after 1 level).</p>
`,
actions: [...getPossibleUpgrades()
.sort((a,b)=>a.minimumTotalScore-b.minimumTotalScore)
.map(({
originalMax,
name,
max,
help,id,
minimumTotalScore
}) =>
({
text: name,
help:`${help} (${minimumTotalScore} coins)`,
disabled: !max,
value: {perks: {[id]: 1}}
}))
,
...allLevels.map((l, li) => {
const threshold=levelTotalScoreCondition(l, li)
const avaliable= ts >= threshold
return ({
text: l.name,
help:`A ${l.size}x${l.size} level (${threshold} coins)`,
disabled: !avaliable,
value: {level: l.name}
})
})
]
,
allowClose: true,
})
if (tryOn) {
nextRunOverrides = tryOn
restart()
}
}
},
...optionsList, ...optionsList,
(window.screenTop || window.screenY) && { (window.screenTop || window.screenY) && {
@ -2237,7 +2288,6 @@ async function openSettingsPanel() {
} }
} }
} }
], ],
textAfterButtons: ` textAfterButtons: `
@ -2254,58 +2304,118 @@ async function openSettingsPanel() {
} }
} }
function distance2(a,b){ function distance2(a, b) {
return Math.pow(a.x-b.x,2)+ Math.pow(a.y-b.y,2) return Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2)
}
function distanceBetween(a,b){
return Math.sqrt(distance2(a,b))
} }
function repulse(a,b,power){ function distanceBetween(a, b) {
return Math.sqrt(distance2(a, b))
}
const distance = distanceBetween(a,b) function rainbowColor() {
return `hsl(${(levelTime / 2) % 360},100%,70%)`
}
function repulse(a, b, power, impactsBToo) {
const distance = distanceBetween(a, b)
// Ensure we don't get soft locked // Ensure we don't get soft locked
if(distance>gameZoneWidth/2) return const max = gameZoneWidth / 2
if (distance > max) return
// 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 / (1+Math.max(1, distance)) const fact = -power * (max - distance) / (max * 1.2) / 3 * Math.min(500, levelTime) / 500
b.vx+=dx*fact if (impactsBToo) {
b.vy+=dy*fact b.vx += dx * fact
a.vx-=dx*fact b.vy += dy * fact
a.vy-=dy*fact }
a.vx -= dx * fact
a.vy -= dy * fact
if(!isSettingOn('basic')){ if (!isSettingOn('basic')) {
const speed= 10 const speed = 10
const rand= 2 const rand = 2
flashes.push({ flashes.push({
type: "particle", type: "particle",
duration:150, duration: 100,
time: levelTime, time: levelTime,
size:coinSize/2, size: coinSize / 2,
color:b.color, 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,
})
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,
})
}
}
}
function attract(a, b, power) {
const distance = distanceBetween(a, b)
// Ensure we don't get soft locked
const min = gameZoneWidth * .5
if (distance < min) return
// Unit vector
const dx = (a.x - b.x) / distance
const dy = (a.y - b.y) / distance
const fact = power * (distance - min) / min * Math.min(500, levelTime) / 500
b.vx += dx * fact
b.vy += dy * fact
a.vx -= dx * fact
a.vy -= dy * fact
if (!isSettingOn('basic')) {
const speed = 10
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,
}) })
flashes.push({ flashes.push({
type: "particle", type: "particle",
duration:150, duration: 100,
time: levelTime, time: levelTime,
size:coinSize/2, size: coinSize / 2,
color:a.color, 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,
}) })
} }
} }
fitSize() fitSize()
restart() restart()
tick(); tick();