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).
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).
# Next
- stop scrolling back to top in menu
- render next level behind upgrade picker again
- sturdy bricks: map of remaining hits
# 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;
schedulGameSound(gameState, "explode", ball.x, 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 row = Math.floor(index / gameState.gridSize);
// Break bricks around
@ -2612,8 +2612,8 @@ function explosionAt(gameState, index, x, y, ball) {
const i = (0, _gameUtils.getRowColIndex)(gameState, row + dy, col + dx);
if (gameState.bricks[i] && i !== -1) {
// Study bricks resist explosions too
if (gameState.bricks[i] !== "black" && gameState.perks.sturdy_bricks > Math.random() * 5) continue;
explodeBrick(gameState, i, ball, true);
gameState.brickHP[i]--;
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
// Flashing is take care of by the tick loop
const x = (0, _gameUtils.brickCenterX)(gameState, index), y = (0, _gameUtils.brickCenterY)(gameState, index);
gameState.bricks[index] = "";
setBrick(gameState, index, "");
let coinsToSpawn = gameState.combo;
if (gameState.perks.sturdy_bricks) // +10% per level
coinsToSpawn += Math.ceil((10 + gameState.perks.sturdy_bricks) / 10 * coinsToSpawn);
@ -2765,9 +2765,8 @@ async function setLevel(gameState, l) {
empty(gameState.particles);
empty(gameState.lights);
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
resetBalls(gameState);
gameState.needsRender = true;
@ -2775,6 +2774,10 @@ async function setLevel(gameState, l) {
// background.src = 'data:image/svg+xml;base64,' + btoa(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() {
return `hsl(${Math.round((0, _game.gameState).levelTime / 4) * 2 % 360},100%,70%)`;
}
@ -2946,6 +2949,7 @@ frames = 1) {
const hitBrick = coinBrickHitCheck(gameState, coin);
if (gameState.perks.metamorphosis && typeof hitBrick !== "undefined") {
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;
coin.coloredABrick = true;
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.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.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 = [];
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 hitBrick = vhit ?? hhit ?? chit;
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++;
let pierce = false;
if (sturdyBounce) schedulGameSound(gameState, "wallBeep", x, 1);
@ -3184,7 +3192,7 @@ function ballTick(gameState, ball, delta) {
explodeBrick(gameState, hitBrick, ball, false);
if (ball.sapperUses < gameState.perks.sapper && initialBrickColor !== "black" && // don't replace a brick that bounced with sturdy_bricks
!gameState.bricks[hitBrick]) {
gameState.bricks[hitBrick] = "black";
setBrick(gameState, hitBrick, "black");
ball.sapperUses++;
}
}
@ -3514,7 +3522,7 @@ let cachedBricksRenderKey = "";
function renderAllBricks() {
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 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) {
cachedBricksRenderKey = newKey;
cachedBricksRender.width = (0, _game.gameState).gameZoneWidth;
@ -3529,7 +3537,12 @@ function renderAllBricks() {
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 redBorder = (0, _game.gameState).ballsColor !== color && color !== "black" && redBorderOnBricksWithWrongColor || redBecauseOfReach;
canctx.globalCompositeOperation = "source-over";
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") {
canctx.globalCompositeOperation = "source-over";
drawIMG(canctx, bombSVG, (0, _game.gameState).brickWidth, x, y);
@ -4165,6 +4178,7 @@ function newGameState(params) {
balls: [],
ballsColor: "white",
bricks: [],
brickHP: [],
lights: {
indexMin: 0,
total: 0,

View file

@ -258,7 +258,9 @@ export function explosionAt(
const size = 1 + gameState.perks.bigger_explosions;
schedulGameSound(gameState, "explode", ball.x, 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 row = Math.floor(index / gameState.gridSize);
@ -268,11 +270,8 @@ export function explosionAt(
const i = getRowColIndex(gameState, row + dy, col + dx);
if (gameState.bricks[i] && i !== -1) {
// Study bricks resist explosions too
if (
gameState.bricks[i] !== "black" &&
gameState.perks.sturdy_bricks > Math.random() * 5
)
continue;
gameState.brickHP[i]--
if (gameState.brickHP<=0)
explodeBrick(gameState, i, ball, true);
}
}
@ -308,7 +307,7 @@ export function explodeBrick(
gameState: GameState,
index: number,
ball: Ball,
isExplosion: boolean,
isExplosion: boolean
) {
const color = gameState.bricks[index];
if (!color) return;
@ -324,7 +323,7 @@ export function explodeBrick(
const x = brickCenterX(gameState, index),
y = brickCenterY(gameState, index);
gameState.bricks[index] = "";
setBrick( gameState,index,"");
let coinsToSpawn = gameState.combo;
if (gameState.perks.sturdy_bricks) {
@ -556,7 +555,11 @@ export async function setLevel(gameState: GameState, l: number) {
empty(gameState.particles);
empty(gameState.lights);
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
resetBalls(gameState);
gameState.needsRender = true;
@ -565,6 +568,12 @@ export async function setLevel(gameState: GameState, l: number) {
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 {
return `hsl(${(Math.round(gameState.levelTime / 4) * 2) % 360},100%,70%)`;
}
@ -947,6 +956,7 @@ export function gameStateTick(
gameState.bricks[hitBrick] !== "black" &&
!coin.coloredABrick
) {
// Not using setbrick because we don't want to reset HP
gameState.bricks[hitBrick] = coin.color;
coin.coloredABrick = true;
@ -1390,8 +1400,11 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
.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 = [];
@ -1455,10 +1468,9 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
const hitBrick = vhit ?? hhit ?? chit;
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++;
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
!gameState.bricks[hitBrick]
) {
gameState.bricks[hitBrick] = "black";
setBrick(gameState, hitBrick, "black")
ball.sapperUses++;
}
}

View file

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

View file

@ -398,7 +398,8 @@ export function renderAllBricks() {
"_" +
gameState.ballsColor +
"_" +
gameState.perks.pierce_color;
gameState.perks.pierce_color +
"_"+ gameState.brickHP.reduce((a,b)=>a+b,0);
if (newKey !== cachedBricksRenderKey) {
cachedBricksRenderKey = newKey;
@ -427,7 +428,12 @@ export function renderAllBricks() {
redBorderOnBricksWithWrongColor) ||
redBecauseOfReach;
canctx.globalCompositeOperation = "source-over";
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") {
canctx.globalCompositeOperation = "source-over";

2
src/types.d.ts vendored
View file

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