Sturdy bricks now have a proper hit counter

This commit is contained in:
Renan LE CARO 2025-03-23 17:52:25 +01:00
parent 6899b5cf36
commit 3745b527f5
6 changed files with 64 additions and 32 deletions

View file

@ -20,12 +20,9 @@ Breakout 71 can work offline (add it to home screen) and perform well even on lo
It's very lean and does not take much storage space (Roughly 0.1MB). It's very lean and does not take much storage space (Roughly 0.1MB).
If the app stutters, turn on "fast mode" in the settings to render a simplified view that should be faster. If the app stutters, turn on "fast mode" in the settings to render a simplified view that should be faster.
There's also an easy mode for kids (slower ball). There's also an easy mode for kids (slower ball).
# Next # Next
- stop scrolling back to top in menu
- render next level behind upgrade picker again
- sturdy bricks: map of remaining hits - sturdy bricks: map of remaining hits
# bugs # bugs

36
dist/index.html vendored
View file

@ -2604,7 +2604,7 @@ function explosionAt(gameState, index, x, y, ball) {
const size = 1 + gameState.perks.bigger_explosions; const size = 1 + gameState.perks.bigger_explosions;
schedulGameSound(gameState, "explode", ball.x, 1); schedulGameSound(gameState, "explode", ball.x, 1);
if (index !== -1) { if (index !== -1) {
if (gameState.bricks[index] == "black") delete gameState.bricks[index]; if (gameState.bricks[index] == "black") setBrick(gameState, index, "");
const col = index % gameState.gridSize; const col = index % gameState.gridSize;
const row = Math.floor(index / gameState.gridSize); const row = Math.floor(index / gameState.gridSize);
// Break bricks around // Break bricks around
@ -2612,8 +2612,8 @@ function explosionAt(gameState, index, x, y, ball) {
const i = (0, _gameUtils.getRowColIndex)(gameState, row + dy, col + dx); const i = (0, _gameUtils.getRowColIndex)(gameState, row + dy, col + dx);
if (gameState.bricks[i] && i !== -1) { if (gameState.bricks[i] && i !== -1) {
// Study bricks resist explosions too // Study bricks resist explosions too
if (gameState.bricks[i] !== "black" && gameState.perks.sturdy_bricks > Math.random() * 5) continue; gameState.brickHP[i]--;
explodeBrick(gameState, i, ball, true); if (gameState.brickHP <= 0) explodeBrick(gameState, i, ball, true);
} }
} }
} }
@ -2641,7 +2641,7 @@ function explodeBrick(gameState, index, ball, isExplosion) {
// 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
// Flashing is take care of by the tick loop // Flashing is take care of by the tick loop
const x = (0, _gameUtils.brickCenterX)(gameState, index), y = (0, _gameUtils.brickCenterY)(gameState, index); const x = (0, _gameUtils.brickCenterX)(gameState, index), y = (0, _gameUtils.brickCenterY)(gameState, index);
gameState.bricks[index] = ""; setBrick(gameState, index, "");
let coinsToSpawn = gameState.combo; let coinsToSpawn = gameState.combo;
if (gameState.perks.sturdy_bricks) // +10% per level if (gameState.perks.sturdy_bricks) // +10% per level
coinsToSpawn += Math.ceil((10 + gameState.perks.sturdy_bricks) / 10 * coinsToSpawn); coinsToSpawn += Math.ceil((10 + gameState.perks.sturdy_bricks) / 10 * coinsToSpawn);
@ -2765,9 +2765,8 @@ async function setLevel(gameState, l) {
empty(gameState.particles); empty(gameState.particles);
empty(gameState.lights); empty(gameState.lights);
empty(gameState.texts); empty(gameState.texts);
gameState.bricks = [ gameState.bricks = [];
...lvl.bricks for(let i = 0; i < lvl.size * lvl.size; i++)setBrick(gameState, i, lvl.bricks[i]);
];
// Balls color will depend on most common brick color sometimes // Balls color will depend on most common brick color sometimes
resetBalls(gameState); resetBalls(gameState);
gameState.needsRender = true; gameState.needsRender = true;
@ -2775,6 +2774,10 @@ async function setLevel(gameState, l) {
// background.src = 'data:image/svg+xml;base64,' + btoa(lvl.svg) // background.src = 'data:image/svg+xml;base64,' + btoa(lvl.svg)
(0, _render.background).src = "data:image/svg+xml;UTF8," + lvl.svg; (0, _render.background).src = "data:image/svg+xml;UTF8," + lvl.svg;
} }
function setBrick(gameState, index, color) {
gameState.bricks[index] = color || '';
gameState.brickHP[index] = color === 'black' && 1 || color && 1 + gameState.perks.sturdy_bricks || 0;
}
function rainbowColor() { function rainbowColor() {
return `hsl(${Math.round((0, _game.gameState).levelTime / 4) * 2 % 360},100%,70%)`; return `hsl(${Math.round((0, _game.gameState).levelTime / 4) * 2 % 360},100%,70%)`;
} }
@ -2946,6 +2949,7 @@ frames = 1) {
const hitBrick = coinBrickHitCheck(gameState, coin); const hitBrick = coinBrickHitCheck(gameState, coin);
if (gameState.perks.metamorphosis && typeof hitBrick !== "undefined") { if (gameState.perks.metamorphosis && typeof hitBrick !== "undefined") {
if (gameState.bricks[hitBrick] && coin.color !== gameState.bricks[hitBrick] && gameState.bricks[hitBrick] !== "black" && !coin.coloredABrick) { if (gameState.bricks[hitBrick] && coin.color !== gameState.bricks[hitBrick] && gameState.bricks[hitBrick] !== "black" && !coin.coloredABrick) {
// Not using setbrick because we don't want to reset HP
gameState.bricks[hitBrick] = coin.color; gameState.bricks[hitBrick] = coin.color;
coin.coloredABrick = true; coin.coloredABrick = true;
schedulGameSound(gameState, "colorChange", coin.x, 0.3); schedulGameSound(gameState, "colorChange", coin.x, 0.3);
@ -3123,7 +3127,9 @@ function ballTick(gameState, ball, delta) {
if (gameState.perks.trampoline) gameState.combo += gameState.perks.trampoline; if (gameState.perks.trampoline) gameState.combo += gameState.perks.trampoline;
if (gameState.perks.nbricks && gameState.perks.nbricks !== ball.brokenSinceBounce) resetCombo(gameState, ball.x, ball.y); if (gameState.perks.nbricks && gameState.perks.nbricks !== ball.brokenSinceBounce) resetCombo(gameState, ball.x, ball.y);
if (gameState.perks.respawn) ball.hitItem.slice(0, -1).slice(0, gameState.perks.respawn).forEach(({ index, color })=>{ if (gameState.perks.respawn) ball.hitItem.slice(0, -1).slice(0, gameState.perks.respawn).forEach(({ index, color })=>{
if (!gameState.bricks[index] && color !== "black") gameState.bricks[index] = color; if (!gameState.bricks[index] && color !== "black") // respawns with full hp
setBrick(gameState, index, color);
// gameState.bricks[index] = color;
}); });
ball.hitItem = []; ball.hitItem = [];
if (!ball.hitSinceBounce) { if (!ball.hitSinceBounce) {
@ -3157,7 +3163,9 @@ function ballTick(gameState, ball, delta) {
const chit = typeof vhit == "undefined" && typeof hhit == "undefined" && (0, _game.hitsSomething)(x, y, radius) || undefined; const chit = typeof vhit == "undefined" && typeof hhit == "undefined" && (0, _game.hitsSomething)(x, y, radius) || undefined;
const hitBrick = vhit ?? hhit ?? chit; const hitBrick = vhit ?? hhit ?? chit;
if (typeof hitBrick !== "undefined") { if (typeof hitBrick !== "undefined") {
let sturdyBounce = gameState.bricks[hitBrick] !== "black" && gameState.perks.sturdy_bricks && gameState.perks.sturdy_bricks > Math.random() * 5; // TODO higher damage balls
gameState.brickHP[hitBrick]--;
let sturdyBounce = gameState.brickHP[hitBrick];
ball.hitSinceBounce++; ball.hitSinceBounce++;
let pierce = false; let pierce = false;
if (sturdyBounce) schedulGameSound(gameState, "wallBeep", x, 1); if (sturdyBounce) schedulGameSound(gameState, "wallBeep", x, 1);
@ -3184,7 +3192,7 @@ function ballTick(gameState, ball, delta) {
explodeBrick(gameState, hitBrick, ball, false); explodeBrick(gameState, hitBrick, ball, false);
if (ball.sapperUses < gameState.perks.sapper && initialBrickColor !== "black" && // don't replace a brick that bounced with sturdy_bricks if (ball.sapperUses < gameState.perks.sapper && initialBrickColor !== "black" && // don't replace a brick that bounced with sturdy_bricks
!gameState.bricks[hitBrick]) { !gameState.bricks[hitBrick]) {
gameState.bricks[hitBrick] = "black"; setBrick(gameState, hitBrick, "black");
ball.sapperUses++; ball.sapperUses++;
} }
} }
@ -3514,7 +3522,7 @@ let cachedBricksRenderKey = "";
function renderAllBricks() { function renderAllBricks() {
ctx.globalAlpha = 1; ctx.globalAlpha = 1;
const redBorderOnBricksWithWrongColor = (0, _game.gameState).combo > (0, _gameStateMutators.baseCombo)((0, _game.gameState)) && (0, _game.gameState).perks.picky_eater && !(0, _options.isOptionOn)("basic"); const redBorderOnBricksWithWrongColor = (0, _game.gameState).combo > (0, _gameStateMutators.baseCombo)((0, _game.gameState)) && (0, _game.gameState).perks.picky_eater && !(0, _options.isOptionOn)("basic");
const newKey = (0, _game.gameState).gameZoneWidth + "_" + (0, _game.gameState).bricks.join("_") + bombSVG.complete + "_" + redBorderOnBricksWithWrongColor + "_" + (0, _game.gameState).ballsColor + "_" + (0, _game.gameState).perks.pierce_color; const newKey = (0, _game.gameState).gameZoneWidth + "_" + (0, _game.gameState).bricks.join("_") + bombSVG.complete + "_" + redBorderOnBricksWithWrongColor + "_" + (0, _game.gameState).ballsColor + "_" + (0, _game.gameState).perks.pierce_color + "_" + (0, _game.gameState).brickHP.reduce((a, b)=>a + b, 0);
if (newKey !== cachedBricksRenderKey) { if (newKey !== cachedBricksRenderKey) {
cachedBricksRenderKey = newKey; cachedBricksRenderKey = newKey;
cachedBricksRender.width = (0, _game.gameState).gameZoneWidth; cachedBricksRender.width = (0, _game.gameState).gameZoneWidth;
@ -3529,7 +3537,12 @@ function renderAllBricks() {
if (!color) return; if (!color) return;
let redBecauseOfReach = (0, _game.gameState).perks.reach && (0, _gameUtils.countBricksAbove)((0, _game.gameState), index) && !(0, _gameUtils.countBricksBelow)((0, _game.gameState), index); let redBecauseOfReach = (0, _game.gameState).perks.reach && (0, _gameUtils.countBricksAbove)((0, _game.gameState), index) && !(0, _gameUtils.countBricksBelow)((0, _game.gameState), index);
let redBorder = (0, _game.gameState).ballsColor !== color && color !== "black" && redBorderOnBricksWithWrongColor || redBecauseOfReach; let redBorder = (0, _game.gameState).ballsColor !== color && color !== "black" && redBorderOnBricksWithWrongColor || redBecauseOfReach;
canctx.globalCompositeOperation = "source-over";
drawBrick(canctx, color, redBorder && "red" || color, x, y); drawBrick(canctx, color, redBorder && "red" || color, x, y);
if ((0, _game.gameState).brickHP[index] > 1) {
canctx.globalCompositeOperation = "destination-out";
drawText(canctx, (0, _game.gameState).brickHP[index].toString(), "white", (0, _game.gameState).puckHeight, x, y);
}
if (color === "black") { if (color === "black") {
canctx.globalCompositeOperation = "source-over"; canctx.globalCompositeOperation = "source-over";
drawIMG(canctx, bombSVG, (0, _game.gameState).brickWidth, x, y); drawIMG(canctx, bombSVG, (0, _game.gameState).brickWidth, x, y);
@ -4165,6 +4178,7 @@ function newGameState(params) {
balls: [], balls: [],
ballsColor: "white", ballsColor: "white",
bricks: [], bricks: [],
brickHP: [],
lights: { lights: {
indexMin: 0, indexMin: 0,
total: 0, total: 0,

View file

@ -258,7 +258,9 @@ export function explosionAt(
const size = 1 + gameState.perks.bigger_explosions; const size = 1 + gameState.perks.bigger_explosions;
schedulGameSound(gameState, "explode", ball.x, 1); schedulGameSound(gameState, "explode", ball.x, 1);
if (index !== -1) { if (index !== -1) {
if (gameState.bricks[index] == "black") delete gameState.bricks[index]; if (gameState.bricks[index] == "black") {
setBrick(gameState, index, "")
}
const col = index % gameState.gridSize; const col = index % gameState.gridSize;
const row = Math.floor(index / gameState.gridSize); const row = Math.floor(index / gameState.gridSize);
@ -268,11 +270,8 @@ export function explosionAt(
const i = getRowColIndex(gameState, row + dy, col + dx); const i = getRowColIndex(gameState, row + dy, col + dx);
if (gameState.bricks[i] && i !== -1) { if (gameState.bricks[i] && i !== -1) {
// Study bricks resist explosions too // Study bricks resist explosions too
if ( gameState.brickHP[i]--
gameState.bricks[i] !== "black" && if (gameState.brickHP<=0)
gameState.perks.sturdy_bricks > Math.random() * 5
)
continue;
explodeBrick(gameState, i, ball, true); explodeBrick(gameState, i, ball, true);
} }
} }
@ -308,7 +307,7 @@ export function explodeBrick(
gameState: GameState, gameState: GameState,
index: number, index: number,
ball: Ball, ball: Ball,
isExplosion: boolean, isExplosion: boolean
) { ) {
const color = gameState.bricks[index]; const color = gameState.bricks[index];
if (!color) return; if (!color) return;
@ -324,7 +323,7 @@ export function explodeBrick(
const x = brickCenterX(gameState, index), const x = brickCenterX(gameState, index),
y = brickCenterY(gameState, index); y = brickCenterY(gameState, index);
gameState.bricks[index] = ""; setBrick( gameState,index,"");
let coinsToSpawn = gameState.combo; let coinsToSpawn = gameState.combo;
if (gameState.perks.sturdy_bricks) { if (gameState.perks.sturdy_bricks) {
@ -556,7 +555,11 @@ export async function setLevel(gameState: GameState, l: number) {
empty(gameState.particles); empty(gameState.particles);
empty(gameState.lights); empty(gameState.lights);
empty(gameState.texts); empty(gameState.texts);
gameState.bricks = [...lvl.bricks]; gameState.bricks = []
for(let i=0;i<lvl.size*lvl.size;i++){
setBrick(gameState, i,lvl.bricks[i])
}
// Balls color will depend on most common brick color sometimes // Balls color will depend on most common brick color sometimes
resetBalls(gameState); resetBalls(gameState);
gameState.needsRender = true; gameState.needsRender = true;
@ -565,6 +568,12 @@ export async function setLevel(gameState: GameState, l: number) {
background.src = "data:image/svg+xml;UTF8," + lvl.svg; background.src = "data:image/svg+xml;UTF8," + lvl.svg;
} }
function setBrick(gameState:GameState, index:number, color:string){
gameState.bricks[index] = color||'';
gameState.brickHP[index] = (color === 'black' && 1) ||(color && 1+gameState.perks.sturdy_bricks)||0
}
export function rainbowColor(): colorString { export function rainbowColor(): colorString {
return `hsl(${(Math.round(gameState.levelTime / 4) * 2) % 360},100%,70%)`; return `hsl(${(Math.round(gameState.levelTime / 4) * 2) % 360},100%,70%)`;
} }
@ -947,6 +956,7 @@ export function gameStateTick(
gameState.bricks[hitBrick] !== "black" && gameState.bricks[hitBrick] !== "black" &&
!coin.coloredABrick !coin.coloredABrick
) { ) {
// Not using setbrick because we don't want to reset HP
gameState.bricks[hitBrick] = coin.color; gameState.bricks[hitBrick] = coin.color;
coin.coloredABrick = true; coin.coloredABrick = true;
@ -1390,8 +1400,11 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
.slice(0, -1) .slice(0, -1)
.slice(0, gameState.perks.respawn) .slice(0, gameState.perks.respawn)
.forEach(({ index, color }) => { .forEach(({ index, color }) => {
if (!gameState.bricks[index] && color !== "black") if (!gameState.bricks[index] && color !== "black") {
gameState.bricks[index] = color; // respawns with full hp
setBrick(gameState, index, color);
}
// gameState.bricks[index] = color;
}); });
} }
ball.hitItem = []; ball.hitItem = [];
@ -1455,10 +1468,9 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
const hitBrick = vhit ?? hhit ?? chit; const hitBrick = vhit ?? hhit ?? chit;
if (typeof hitBrick !== "undefined") { if (typeof hitBrick !== "undefined") {
let sturdyBounce = // TODO higher damage balls
gameState.bricks[hitBrick] !== "black" && gameState.brickHP[hitBrick]--
gameState.perks.sturdy_bricks && let sturdyBounce = gameState.brickHP[hitBrick]
gameState.perks.sturdy_bricks > Math.random() * 5;
ball.hitSinceBounce++; ball.hitSinceBounce++;
let pierce = false; let pierce = false;
@ -1494,7 +1506,7 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
initialBrickColor !== "black" && // don't replace a brick that bounced with sturdy_bricks initialBrickColor !== "black" && // don't replace a brick that bounced with sturdy_bricks
!gameState.bricks[hitBrick] !gameState.bricks[hitBrick]
) { ) {
gameState.bricks[hitBrick] = "black"; setBrick(gameState, hitBrick, "black")
ball.sapperUses++; ball.sapperUses++;
} }
} }

View file

@ -57,6 +57,7 @@ export function newGameState(params: RunParams): GameState {
balls: [], balls: [],
ballsColor: "white", ballsColor: "white",
bricks: [], bricks: [],
brickHP: [],
lights: { indexMin: 0, total: 0, list: [] }, lights: { indexMin: 0, total: 0, list: [] },
particles: { indexMin: 0, total: 0, list: [] }, particles: { indexMin: 0, total: 0, list: [] },
texts: { indexMin: 0, total: 0, list: [] }, texts: { indexMin: 0, total: 0, list: [] },

View file

@ -398,7 +398,8 @@ export function renderAllBricks() {
"_" + "_" +
gameState.ballsColor + gameState.ballsColor +
"_" + "_" +
gameState.perks.pierce_color; gameState.perks.pierce_color +
"_"+ gameState.brickHP.reduce((a,b)=>a+b,0);
if (newKey !== cachedBricksRenderKey) { if (newKey !== cachedBricksRenderKey) {
cachedBricksRenderKey = newKey; cachedBricksRenderKey = newKey;
@ -427,7 +428,12 @@ export function renderAllBricks() {
redBorderOnBricksWithWrongColor) || redBorderOnBricksWithWrongColor) ||
redBecauseOfReach; redBecauseOfReach;
canctx.globalCompositeOperation = "source-over";
drawBrick(canctx, color, (redBorder && "red") || color, x, y); drawBrick(canctx, color, (redBorder && "red") || color, x, y);
if(gameState.brickHP[index]>1){
canctx.globalCompositeOperation="destination-out"
drawText(canctx, gameState.brickHP[index].toString(), "white", gameState.puckHeight, x,y )
}
if (color === "black") { if (color === "black") {
canctx.globalCompositeOperation = "source-over"; canctx.globalCompositeOperation = "source-over";

2
src/types.d.ts vendored
View file

@ -226,6 +226,8 @@ export type GameState = {
ballsColor: colorString; ballsColor: colorString;
// Array of bricks to display. 'black' means bomb. '' means no brick. // Array of bricks to display. 'black' means bomb. '' means no brick.
bricks: colorString[]; bricks: colorString[];
// Number of times a brick has been hit already
brickHP: number[];
particles: ReusableArray<ParticleFlash>; particles: ReusableArray<ParticleFlash>;
texts: ReusableArray<TextFlash>; texts: ReusableArray<TextFlash>;