diff --git a/Readme.md b/Readme.md index 7959d45..681219b 100644 --- a/Readme.md +++ b/Readme.md @@ -25,72 +25,61 @@ The app should work offline and perform well even on low-end devices. It's very There's also an easy mode for kids (slower ball) and a color-blind mode (no color related game mechanics). - - ## Doing +- publish on Fdroid -- publish on Fdroid -- Delay the sound context stop by a few ms +## Perk ideas +- wrap left / right +- n% of the broken bricks respawn when the ball touches the puck +- bricks take twice as many hits but drop 50% more coins +- wind (puck positions adds force to coins and balls) +- balls repulse coins +- n% of coins missed respawn at the top +- lightning : missing triggers and explosive lighting strike around ball path +- coins repulse coins (could get really laggy) +- balls repulse coins +- balls attract coins +- twice as many coins after a wall bounce, twice as little otherwise ? +- fusion reactor (gather coins in one spot to triple their value) +- missing makes you loose all score of level, but otherwise multiplier goes up after each breaking +- soft reset, cut combo in half instead of zero +- missile goes when you catch coin +- missile goes when you break a brick +- puck bounce +1 combo, hit nothing resets +- multiple hits on the same brick (show remaining resistance as number) +- bricks attract ball +- replay last level (remove score, restores lives if any, and rebuild ) +- accelerometer controls coins and balls +- bricks attract coins +- breaking bricks stains neighbours +- extra kick after bouncing on puck +- transparent coins +- coins of different colors repulse +- bricks follow game of life pattern with one update every second +- 2x coins when ball goes downward / upward, half that amount otherwise ? -## Todo +## Engine ideas -- 2 x "slower balls" is too slow on the first levels - show total score on end screen (score added to total) - show stats on end screen compared to other runs -- handle back bouton in menu -- more levels : famous simple games, letters, fruits, animals -- icons for upgrades and level -- remind users of what they have already picked. Maybe show all upgrades, disabled except the offered ones ? - -## Other ideas - -- perk : wrap left / right -- perk : n% of the broken bricks respawn when the ball touches the puck -- perk : bricks take twice as many hits but drop 50% more coins -- perk : wind (puck positions adds force to coins and balls) -- perk : balls repulse coins -- perk : n% of coins missed respawn at the top -- perk : lightning : missing triggers and explosive lighting strike around ball path -- perk : coins repulse coins (could get really laggy) -- perk : balls repulse coins -- perk : balls attract coins -- 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 : missing makes you loose all score of level, but otherwise multiplier goes up after each breaking -- Make a small mp4 of game which can be shown on gameover and shared. https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder -- perk : soft reset, cut combo in half instead of zero -- perk : missile goes when you catch coin -- perk : missile goes when you break a brick -- when game resumes near bottom, be unvulnerable for .5s ? , once per level -- accelerometer controls coins and balls +- handle back bouton in menu +- Make a small mp4/gif of game which can be shown on gameover and shared. https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder - mouvement relatif du puck - balls should collide with each other -- randomize coins gravity a bit, to make fall more appealing +- when game resumes near bottom, be unvulnerable for .5s ? , once per level - apply global curve / brightness to canvas when things blow, or just always to make neon effect better -- perk: bricks attract coins -- perk : puck bounce +1 combo, hit nothing resets -- manifest for PWA (android and apple) -- publish on fdroid -- nerf the hot start a bit -- brick parts fly around with trailing effect ? -- trailing white lines behind ball -- some 3d ish effect ? -- shrink brick at beaking time ? -- perk : multiple hits on the same brick (show remaining resistance as number) -- particle effect around ball when loosing some combo (looks bad) -- Make bricks shadow the light ? using a "fill path" in screen mode, with a gradient background...would get very laggy, maybe just for the ball -- keyboard support -- perk : bricks attract ball -- perk : replay last level (remove score, restores lives if any, and rebuild ) -- perk: breaking bricks stains neighbours -- perk: extra kick after bouncing on puck -- perk: transparent coins -- perk: coins of different colors repulse -- perk: bricks follow game of life pattern with one opdate every second ? -- 2x coins when ball goes downward / upward, half that amount otherwise ? -- engine: Offline mode web for iphone -- engine: webgl rendering (not with sdf though, that's super slow) +- manifest for PWA (android and apple) +- lights shadows +- keyboard support +- Offline mode web for iphone +- webgl rendering +## Level ides + +- famous games +- letters +- fruits +- animals ## Credits diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a4f1170..e5d1b10 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -11,8 +11,8 @@ android { applicationId = "me.lecaro.breakout" minSdk = 21 targetSdk = 34 - versionCode = 28998184 - versionName = "28998184" + versionCode = 28999417 + versionName = "28999417" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary = true diff --git a/app/src/main/assets/game.js b/app/src/main/assets/game.js index e523473..5dcb2b9 100644 --- a/app/src/main/assets/game.js +++ b/app/src/main/assets/game.js @@ -9,8 +9,18 @@ const puckHeight = ballSize; if (allLevels.find(l => l.focus)) { allLevels = allLevels.filter(l => l.focus) } +// Used to render perk icons +const perkIconsLevels = {} +allLevels = allLevels.filter(l => { + if (l.name.startsWith('perk:')) { + perkIconsLevels[l.name.split(':')[1]] = l + return false + } + return true +}) allLevels.forEach((l, li) => { - l.threshold = li < 8 ? 0 : Math.round(Math.pow(10, 1 + (li + l.size) / 30) * (li)) * 10 + 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 }) let runLevels = [] @@ -145,7 +155,8 @@ const fitSize = () => { window.addEventListener("resize", fitSize); function recomputeTargetBaseSpeed() { - baseSpeed = gameZoneWidth / 12 / 10 + currentLevel / 3 + levelTime / (30 * 1000) - perks.slow_down * 2; + // 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); } @@ -172,21 +183,23 @@ function getRowColIndex(row, col) { function spawnExplosion(count, x, y, color, duration = 150, size = coinSize) { if (!!isSettingOn("basic")) return; for (let i = 0; i < count; i++) { + flashes.push({ type: "particle", - duration, time: levelTime, size, - color, x: x + ((Math.random() - 0.5) * brickWidth) / 2, y: y + ((Math.random() - 0.5) * brickWidth) / 2, vx: (Math.random() - 0.5) * 30, vy: (Math.random() - 0.5) * 30, + color, + duration:150 }); } } + let score = 0; let scoreStory = []; @@ -280,44 +293,53 @@ let levelSpawnedCoins = 0; function getLevelStats() { const catchRate = (score - levelStartScore) / (levelSpawnedCoins || 1); - let stats = ` - you caught ${score - levelStartScore} coins out of ${levelSpawnedCoins} in ${Math.round(levelTime / 1000)} seconds. - `; - stats += levelMisses ? `You missed ${levelMisses} times. ` : ""; - let text = [stats]; + let repeats = 1; let choices = 3; + let timeGain = '', catchGain = '', missesGain = '' if (levelTime < 30 * 1000) { repeats++; choices++; - text.push("speed bonus: +1 upgrade and choice"); + timeGain = " (+1 upgrade)" } else if (levelTime < 60 * 1000) { choices++; - text.push("speed bonus: +1 choice"); + timeGain = " (+1 choice)" } if (catchRate === 1) { repeats++; choices++; - text.push("coins bonus: +1 upgrade and choice"); + catchGain = " (+1 upgrade)" } else if (catchRate > 0.9) { choices++; - text.push("coins bonus: +1 choice."); + catchGain = " (+1 choice)" } if (levelMisses === 0) { repeats++; choices++; - text.push("accuracy bonus: +1 upgrade and choice"); + missesGain = " (+1 upgrade)" } else if (levelMisses <= 3) { choices++; - text.push("accuracy bonus:+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 => '
' + t + '
').join('\n'), repeats, choices, }; } +function pickedUpgradesHTMl() { + return upgrades.filter(u => perks[u.id]).map(u => u.icon).join(' ') +} + async function openUpgradesPicker() { let {text, repeats, choices} = getLevelStats(); scoreStory.push(`Finished level ${currentLevel + 1} (${currentLevelInfo().name}): ${text}`,); @@ -325,10 +347,8 @@ async function openUpgradesPicker() { while (repeats--) { const actions = pickRandomUpgrades(choices); if (!actions.length) break - let textAfterButtons; - if (actions.length < choices) { - textAfterButtons = `You are running out of upgrades, more will be unlocked when you catch lots of coins.
` - } + let textAfterButtons=`Upgrades picked so far :
${pickedUpgradesHTMl()}
`; + const cb = await asyncAlert({ title: "Pick an upgrade " + (repeats ? "(" + (repeats + 1) + ")" : ""), actions, text, allowClose: false, textAfterButtons @@ -391,11 +411,6 @@ function reset_perks() { const giftable = getPossibleUpgrades().filter(u => u.giftable) const randomGift = isSettingOn('easy') ? 'slow_down' : giftable[Math.floor(Math.random() * giftable.length)].id; perks[randomGift] = 1; - // TODO - // perks.puck_repulse_ball=3 - // perks.ball_repulse_ball=3 - // perks.ball_attract_ball=3 - // perks.multiball=3 return randomGift } @@ -405,7 +420,7 @@ const upgrades = [ "threshold": 0, "id": "extra_life", "name": "+1 life", - "max": 3, + "max": 7, "help": "Survive dropping the ball once." }, { @@ -422,7 +437,7 @@ const upgrades = [ "id": "base_combo", "giftable": true, "name": "+3 base combo", - "max": 3, + "max": 7, "help": "Your combo starts 3 points higher." }, { @@ -440,14 +455,14 @@ const upgrades = [ "help": "Catches more coins." }, { - "threshold": 50, + "threshold": 0, "id": "viscosity", "name": "Slower coins fall", "max": 3, "help": "Coins quickly decelerate." }, { - "threshold": 100, + "threshold": 0, "id": "sides_are_lava", "giftable": true, "name": "Shoot straight", @@ -455,15 +470,7 @@ const upgrades = [ "help": "Avoid the sides for more coins." }, { - "threshold": 200, - "id": "telekinesis", - "giftable": true, - "name": "Puck controls ball", - "max": 2, - "help": "Control the ball's trajectory." - }, - { - "threshold": 400, + "threshold": 0, "id": "top_is_lava", "giftable": true, "name": "Sky is the limit", @@ -471,53 +478,61 @@ const upgrades = [ "help": "Avoid the top for more coins." }, { - "threshold": 800, - "id": "coin_magnet", - "name": "Puck attracts coins", - "max": 3, - "help": "Coins falling are drawn toward the puck." - }, - { - "threshold": 1600, + "threshold": 0, "id": "skip_last", "name": "Last brick breaks", - "max": 3, + "max": 7, "help": "The last brick will self-destruct." }, { - "threshold": 3200, + "threshold": 500, + "id": "telekinesis", + "giftable": true, + "name": "Puck controls ball", + "max": 2, + "help": "Control the ball's trajectory." + }, + { + "threshold": 1000, + "id": "coin_magnet", + "name": "Puck attracts coins", + "max": 3, + "help": "Coins are drawn toward the puck." + }, + { + "threshold": 1500, "id": "multiball", "giftable": true, "name": "+1 ball", "max": 3, - "help": "Start each level with one more balls." + "help": "Start with one more balls." }, { - "threshold": 5600, + "threshold": 2000, "id": "smaller_puck", "name": "Smaller puck", "max": 2, "help": "Gives you more control." }, { - "threshold": 7000, + "threshold": 3000, "id": "pierce", "giftable": true, "name": "Ball pierces bricks", "max": 3, - "help": "Go through 3 blocks before bouncing." + "help": "Destroy 3 blocks before bouncing." }, { - "threshold": 12000, + "threshold": 4000, "id": "picky_eater", "giftable": true, "name": "Single color streak", "color_blind_exclude": true, "max": 1, - "help": "Hit groups of bricks of the same color." + "help": "Break bricks color by color." }, { - "threshold": 16000, + "threshold": 5000, "id": "metamorphosis", "name": "Coins stain bricks", "color_blind_exclude": true, @@ -525,60 +540,60 @@ const upgrades = [ "help": "Coins color the bricks they touch." }, { - "threshold": 22000, + "threshold": 6000, "id": "catch_all_coins", "giftable": true, "name": "Compound interest", "max": 3, - "help": "Catch all coins with your puck for even more coins." + "help": "Avoid missing coins with your puck." }, { - "threshold": 26000, + "threshold": 7000, "id": "hot_start", "giftable": true, "name": "Hot start", "max": 3, - "help": "Clear the level quickly for more coins." + "help": "Clear the level quickly." }, { - "threshold": 33000, + "threshold": 9000, "id": "sapper", "giftable": true, "name": "Bricks become bombs", "max": 1, - "help": "Broken blocks are replaced by bombs." + "help": "Broken blocks become bombs." }, { - "threshold": 42000, + "threshold": 11000, "id": "bigger_explosions", "name": "Bigger explosions", "max": 1, - "help": "All bombs have larger area of effect." + "help": "Larger bomb area of effect." }, { - "threshold": 54000, + "threshold": 13000, "id": "extra_levels", "name": "+1 level", "max": 3, - "help": "Play one more level before game over." + "help": "Play one more level before winning." }, { - "threshold": 65000, + "threshold": 15000, "id": "pierce_color", "name": "Color pierce", "color_blind_exclude": true, "max": 1, - "help": "Colored ball pierces bricks of the same color." + "help": "Ball breaks same color bricks." }, { - "threshold": 760000, + "threshold": 18000, "id": "soft_reset", "name": "Soft reset", "max": 2, - "help": "Only loose half your combo when it resets." + "help": "Combo grows slower but resets less" }, { - "threshold": 87000, + "threshold": 21000, "id": "ball_repulse_ball", "name": "Balls repulse balls", requires: 'multiball', @@ -586,7 +601,7 @@ const upgrades = [ "help": "Only has an effect with 2+ balls." }, { - "threshold": 98000, + "threshold": 25000, "id": "ball_attract_ball", requires: 'multiball', "name": "Balls attract balls", @@ -594,16 +609,17 @@ const upgrades = [ "help": "Only has an effect with 2+ balls." }, { - "threshold": 120000, + "threshold": 30000, "id": "puck_repulse_ball", "name": "Puck repulse balls", "max": 3, - "help": "Prevents the puck from touching the balls." + "help": "Prevents the puck from touching the balls.", }, ] -let totalScoreAtRunStart= getTotalScore() +let totalScoreAtRunStart = getTotalScore() + function getPossibleUpgrades() { return upgrades .filter(u => !(isSettingOn('color_blind') && u.color_blind_exclude)) @@ -612,14 +628,15 @@ function getPossibleUpgrades() { } -function shuffleLevels(nameToAvoid = null) { +function shuffleLevels(nameToAvoid = null) { + runLevels = allLevels .filter(l => nextRunOverrides.level ? l.name === nextRunOverrides.level : true) .filter((l, li) => totalScoreAtRunStart >= l.threshold) .filter(l => l.name !== nameToAvoid || allLevels.length === 1) .sort(() => Math.random() - 0.5) .slice(0, 7 + 3) - .sort((a, b) => a.bricks.filter(i => i).length - b.bricks.filter(i => i).length); + .sort((a, b) => a.sortKey - b.sortKey); nextRunOverrides.level = null } @@ -633,8 +650,7 @@ function getUpgraderUnlockPoints() { if (u.threshold) { list.push({ threshold: u.threshold, - title: u.name + ' (Perk)', - help: u.help, + title: u.name + ' (Perk)' }) } }) @@ -660,24 +676,26 @@ function pickRandomUpgrades(count) { .filter(u => perks[u.id] < u.max) .slice(0, count) .sort((a, b) => a.id > b.id ? 1 : -1) - .map(u => { - incrementRunStatistics('offered_upgrade.' + u.id, 1) - return { - key: u.id, text: u.name, value: () => { - perks[u.id]++; - incrementRunStatistics('picked_upgrade.' + u.id, 1) - scoreStory.push("Picked upgrade : " + u.name); - }, help: u.help, max: u.max, - checked: perks[u.id], - } - }) - list.forEach(u => { - lastOffered[u.key] = Math.round(Date.now() / 1000) + incrementRunStatistics('offered_upgrade.' + u.id, 1) + lastOffered[u.id] = Math.round(Date.now() / 1000) }) - return list; + return list.map(u => ({ + text: u.name + (perks[u.id]?' lvl '+(perks[u.id]+1):''), + icon: u.icon, + value: () => { + perks[u.id]++; + incrementRunStatistics('picked_upgrade.' + u.id, 1) + scoreStory.push("Picked upgrade : " + u.name); + }, + help: u.help, + // max: u.max, + // checked: perks[u.id] + })) + + } let nextRunOverrides = {level: null, perks: null} @@ -688,7 +706,7 @@ function 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 // run's level list - totalScoreAtRunStart= getTotalScore() + totalScoreAtRunStart = getTotalScore() shuffleLevels(levelTime || score ? currentLevelInfo().name : null); resetRunStatistics() score = 0; @@ -1022,7 +1040,7 @@ function ballTick(ball, delta) { for (b2 of balls) { // avoid computing this twice, and repulsing itself if (b2.x >= ball.x) continue - attract(ball, b2, 2 * perks.ball_attract_ball) + attract(ball, b2, perks.ball_attract_ball) } } if (perks.puck_repulse_ball) { @@ -1236,7 +1254,7 @@ function gameOver(title, intro) { const list = getUpgraderUnlockPoints() list.filter(u => u.threshold > startTs && u.threshold < endTs).forEach(u => { unlocksInfo += ` -+
${u.title}
@@ -1253,7 +1271,7 @@ function gameOver(title, intro) { const scaleX = (done / total).toFixed(2) unlocksInfo += ` -+
${nextUnlock.title}
@@ -1261,7 +1279,7 @@ function gameOver(title, intro) { ` list.slice(list.indexOf(nextUnlock) + 1).slice(0, 3).forEach(u => { unlocksInfo += ` -+
${u.title}
` @@ -1313,7 +1331,7 @@ function explodeBrick(index, ball, isExplosion) { flashes.push({ type: "ball", duration: 150, time: levelTime, size: brickWidth * 2, color: "white", x, y, }); - spawnExplosion(7 * (1 + perks.bigger_explosions), x, y, "white", 150, coinSize,); + spawnExplosion(7 * (1 + perks.bigger_explosions), x, y, 'white', 150, coinSize,); ball.hitSinceBounce++; } else if (color) { // Flashing is take care of by the tick loop @@ -1354,7 +1372,9 @@ function explodeBrick(index, ball, isExplosion) { }); } - combo += 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)); if (!isExplosion) { // color change @@ -1371,7 +1391,7 @@ function explodeBrick(index, ball, isExplosion) { flashes.push({ 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); } } @@ -1485,60 +1505,60 @@ function render() { // The red should still be visible on a white bg ctx.globalCompositeOperation = !level.color && level.svg ? "screen" : 'source-over'; ctx.globalAlpha = (2 + combo - baseCombo()) / 50; - const baseParticle= !isSettingOn('basic') && ( combo - baseCombo() )*Math.random() >5&& running &&{ - type: "particle", - duration: 100*(Math.random()+1), - time: levelTime, - size: coinSize / 2, - color: 'red', - ethereal: true, + const baseParticle = !isSettingOn('basic') && (combo - baseCombo()) * Math.random() > 5 && running && { + type: "particle", + duration: 100 * (Math.random() + 1), + time: levelTime, + size: coinSize / 2, + color: 'red', + ethereal: true, } if (perks.top_is_lava) { - drawRedGradientSquare( ctx, offsetXRoundedDown, 0, gameZoneWidthRoundedUp, ballSize, 0, 0, 0, ballSize); + drawRedGradientSquare(ctx, offsetXRoundedDown, 0, gameZoneWidthRoundedUp, ballSize, 0, 0, 0, ballSize); baseParticle && flashes.push({ - ...baseParticle, - x: offsetXRoundedDown+Math.random()*gameZoneWidthRoundedUp, + ...baseParticle, + x: offsetXRoundedDown + Math.random() * gameZoneWidthRoundedUp, y: 0, vx: (Math.random() - 0.5) * 10, vy: 5, }) } if (perks.sides_are_lava) { - drawRedGradientSquare( ctx, offsetXRoundedDown, 0, ballSize, gameZoneHeight, 0, 0, ballSize, 0,); - drawRedGradientSquare( ctx, offsetXRoundedDown + gameZoneWidthRoundedUp - ballSize, 0, ballSize, gameZoneHeight, ballSize, 0, 0, 0,); - const fromLeft =Math.random()>0.5 + drawRedGradientSquare(ctx, offsetXRoundedDown, 0, ballSize, gameZoneHeight, 0, 0, ballSize, 0,); + drawRedGradientSquare(ctx, offsetXRoundedDown + gameZoneWidthRoundedUp - ballSize, 0, ballSize, gameZoneHeight, ballSize, 0, 0, 0,); + const fromLeft = Math.random() > 0.5 baseParticle && flashes.push({ - ...baseParticle, - x: offsetXRoundedDown+(fromLeft?0:gameZoneWidthRoundedUp), - y: Math.random()*gameZoneHeight, - vx: fromLeft?5:-5, - vy: (Math.random() - 0.5) * 10, + ...baseParticle, + x: offsetXRoundedDown + (fromLeft ? 0 : gameZoneWidthRoundedUp), + y: Math.random() * gameZoneHeight, + vx: fromLeft ? 5 : -5, + vy: (Math.random() - 0.5) * 10, }) } if (perks.catch_all_coins) { - drawRedGradientSquare( ctx, offsetXRoundedDown, gameZoneHeight - ballSize, gameZoneWidthRoundedUp, ballSize, 0, ballSize, 0, 0,); + drawRedGradientSquare(ctx, offsetXRoundedDown, gameZoneHeight - ballSize, gameZoneWidthRoundedUp, ballSize, 0, ballSize, 0, 0,); let x = puck - do{ - x= offsetXRoundedDown + gameZoneWidthRoundedUp*Math.random() - }while(Math.abs(x-puck)You are playing level ${currentLevel + 1} out of ${max_levels()}.
- ${scoreStory.map((t) => "" + t + "
").join("")} + title: ` ${score} points at level ${currentLevel + 1} / ${max_levels()}`, + text: ` +${pickedUpgradesHTMl()}
+ ${scoreStory.map((t) => "" + t + "
").join("")} + `, allowClose: true, actions: [{ text: "New run", help: "Start a brand new run.", value: () => { restart(); @@ -2268,23 +2292,19 @@ async function openSettingsPanel() { help: "See and try what you've unlocked", async value() { const ts = getTotalScore() - const tryOn = await asyncAlert({ - title: 'Your unlocks', - text: ` -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).
- `, - actions: [...upgrades + const actions=[...upgrades .sort((a, b) => a.threshold - b.threshold) .map(({ name, max, help, id, - threshold + threshold, icon }) => ({ text: name, - help: help + (ts >= threshold ? '' : `(${threshold} coins)`), + help: ts >= threshold ? help :`Unlocks at total score ${threshold}.`, disabled: ts < threshold, - value: {perks: {[id]: 1}} + value: {perks: {[id]: 1}}, + icon }) ) @@ -2295,14 +2315,26 @@ async function openSettingsPanel() { const avaliable = ts >= l.threshold return ({ text: l.name, - help: `A ${l.size}x${l.size} level with ${l.bricks.filter(i => i).length} bricks` + (avaliable ? '' : `(${l.threshold} coins)`), + 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, - value: {level: l.name} - + value: {level: l.name}, + icon: levelIconHTML(l) }) }) ] + const tryOn = await asyncAlert({ + title: `You unlocked ${Math.round(actions.filter(a=>!a.disabled).length / actions.length * 100)}% of the game.`, + text: ` +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.
+ `, + textAfterButtons:`+The total score increases every time you score in game. +Your high score is ${highScore}. +Click an item above to start a test run with it. +
`, + actions + , allowClose: true, @@ -2476,9 +2508,39 @@ function attract(a, b, power) { vy: -dy * speed + b.vy + (Math.random() - 0.5) * rand, }) } - } +let levelIconHTMLCanvas = document.createElement('canvas') +const levelIconHTMLCanvasCtx = levelIconHTMLCanvas.getContext("2d", {antialias: false, alpha: true}) + +function levelIconHTML(level, title) { + const size=40 + const c = levelIconHTMLCanvas + const ctx = levelIconHTMLCanvasCtx + c.width = size + c.height = size + if (level.color) { + ctx.fillStyle = level.color + ctx.fillRect(0, 0, size, size) + } else { + ctx.clearRect(0, 0, size, size) + } + const pxSize = size / level.size + for (let x = 0; x < level.size; x++) { + for (let y = 0; y < level.size; y++) { + const c = level.bricks[y * level.size + x] + if (c) { + ctx.fillStyle = c + ctx.fillRect(Math.floor(pxSize * x), Math.floor(pxSize * y), Math.ceil(pxSize), Math.ceil(pxSize)) + } + } + } + // I don't think many blind people will benefit for this but it's nice to have something to put in "alt" + return `