Build 29087244

This commit is contained in:
Renan LE CARO 2025-04-21 13:25:06 +02:00
parent 5ba93500b4
commit 49f3769b54
21 changed files with 2505 additions and 2517 deletions

View file

@ -11,19 +11,13 @@ Break colourful bricks, catch bouncing coins and select powerful upgrades !
- [Google Play](https://play.google.com/store/apps/details?id=me.lecaro.breakout) - [Google Play](https://play.google.com/store/apps/details?id=me.lecaro.breakout)
- [GitLab](https://gitlab.com/lecarore/breakout71) - [GitLab](https://gitlab.com/lecarore/breakout71)
# Current priorities
The goal of this project is to make a game used by many people.
The game is already pretty fun.
I'm now trying to translate it to (Lebanese) Arabic, Russian and (Chilean) Spanish.
Other translation are very welcome, contact me if you'd like to submit one.
# Changelog # Changelog
## To do ## To do
## Done ## Done
- apply percentage boost to combo shown on brick
- smaller puck now gives +50% coins per level
- transparency now gives +50% coins if ALL balls are fully transparent, less otherwise
- new perk : sticky coins (coins stick to bricks) - new perk : sticky coins (coins stick to bricks)
- left/top/right is laval perks : at level 2+, the corresponding borders completely disappears (reachable with limitless) - left/top/right is laval perks : at level 2+, the corresponding borders completely disappears (reachable with limitless)
- new perk : three cushion (gain point for indirect hits) - new perk : three cushion (gain point for indirect hits)

View file

@ -29,8 +29,8 @@ android {
applicationId = "me.lecaro.breakout" applicationId = "me.lecaro.breakout"
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 34
versionCode = 29085904 versionCode = 29087244
versionName = "29085904" versionName = "29087244"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
useSupportLibrary = true useSupportLibrary = true

File diff suppressed because one or more lines are too long

819
dist/index.html vendored

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,5 @@
// The version of the cache. // The version of the cache.
const VERSION = "29085904"; const VERSION = "29087244";
// The name of the cache // The name of the cache
const CACHE_NAME = `breakout-71-${VERSION}`; const CACHE_NAME = `breakout-71-${VERSION}`;

View file

@ -43,19 +43,17 @@ export async function openCreativeModePerksPicker() {
transformRawLevel, transformRawLevel,
); );
while (true ) { while (true) {
const levelOptions = [
const levelOptions= [
...allLevels.map((l, li) => { ...allLevels.map((l, li) => {
const problem = const problem = reasonLevelIsLocked(li, getHistory(), true)?.text || "";
reasonLevelIsLocked(li, getHistory(), true)?.text || "";
return { return {
icon: icons[l.name], icon: icons[l.name],
text: l.name, text: l.name,
value: l, value: l,
disabled: !!problem, disabled: !!problem,
tooltip: problem || describeLevel(l), tooltip: problem || describeLevel(l),
className:'' className: "",
}; };
}), }),
...customLevels.map((l) => ({ ...customLevels.map((l) => ({
@ -64,26 +62,28 @@ export async function openCreativeModePerksPicker() {
value: l, value: l,
disabled: !l.bricks.filter((b) => b !== "_").length, disabled: !l.bricks.filter((b) => b !== "_").length,
tooltip: describeLevel(l), tooltip: describeLevel(l),
className:'' className: "",
})) })),
] ];
const selectedLeveOption= levelOptions.find(l=>l.text===getSettingValue("creativeModeLevel", '')) || levelOptions[0] const selectedLeveOption =
selectedLeveOption.className= 'highlight' levelOptions.find(
(l) => l.text === getSettingValue("creativeModeLevel", ""),
) || levelOptions[0];
selectedLeveOption.className = "highlight";
const choice = await asyncAlert<Upgrade | Level | "reset" | "play">({
const choice=await asyncAlert<Upgrade | Level | "reset" | "play">({
title: t("lab.menu_entry"), title: t("lab.menu_entry"),
className: "actionsAsGrid", className: "actionsAsGrid",
content: [ content: [
{ {
icon: icons['icon:reset'], icon: icons["icon:reset"],
value: "reset", value: "reset",
text: t("lab.reset"), text: t("lab.reset"),
disabled: !sumOfValues(creativeModePerks), disabled: !sumOfValues(creativeModePerks),
}, },
{ {
icon: icons['icon:new_run'], icon: icons["icon:new_run"],
value: "play", value: "play",
text: t("lab.play"), text: t("lab.play"),
disabled: !sumOfValues(creativeModePerks), disabled: !sumOfValues(creativeModePerks),
@ -106,24 +106,28 @@ export async function openCreativeModePerksPicker() {
tooltip: u.help(creativeModePerks[u.id] || 1), tooltip: u.help(creativeModePerks[u.id] || 1),
})), })),
t("lab.select_level"), t("lab.select_level"),
...levelOptions ...levelOptions,
], ],
}) });
if(!choice)return if (!choice) return;
if (choice === "reset") { if (choice === "reset") {
upgrades.forEach((u) => { upgrades.forEach((u) => {
creativeModePerks[u.id] = 0; creativeModePerks[u.id] = 0;
}); });
setSettingValue("creativeModePerks", creativeModePerks); setSettingValue("creativeModePerks", creativeModePerks);
setSettingValue("creativeModeLevel", '') setSettingValue("creativeModeLevel", "");
} else if (choice === "play" || ("bricks" in choice && choice.name==getSettingValue("creativeModeLevel", ''))) { } else if (
choice === "play" ||
("bricks" in choice &&
choice.name == getSettingValue("creativeModeLevel", ""))
) {
if (await confirmRestart(gameState)) { if (await confirmRestart(gameState)) {
restart({ restart({
perks: creativeModePerks, perks: creativeModePerks,
level: selectedLeveOption.value, level: selectedLeveOption.value,
isCreativeRun: true, isCreativeRun: true,
}); });
return return;
} }
} else if ("bricks" in choice) { } else if ("bricks" in choice) {
setSettingValue("creativeModeLevel", choice.name); setSettingValue("creativeModeLevel", choice.name);

View file

@ -1 +1 @@
"29085904" "29087244"

View file

@ -585,8 +585,7 @@ h2.histogram-title strong {
opacity: 0.3; opacity: 0.3;
} }
} }
.not-highlighed{ .not-highlighed {
opacity: 0.8; color: #8a8a8a; opacity: 0.8;
color: #8a8a8a;
} }

View file

@ -23,6 +23,7 @@ import {
describeLevel, describeLevel,
getRowColIndex, getRowColIndex,
highScoreText, highScoreText,
hoursSpentPlaying,
isInWebView, isInWebView,
levelsListHTMl, levelsListHTMl,
max_levels, max_levels,
@ -79,7 +80,6 @@ import {
catchRateBest, catchRateBest,
catchRateGood, catchRateGood,
clamp, clamp,
hoursSpentPlaying,
levelTimeBest, levelTimeBest,
levelTimeGood, levelTimeGood,
missesBest, missesBest,
@ -248,7 +248,8 @@ setInterval(() => {
}, 1000); }, 1000);
export async function openUpgradesPicker(gameState: GameState) { export async function openUpgradesPicker(gameState: GameState) {
const catchRate = (gameState.score - gameState.levelStartScore) / const catchRate =
(gameState.score - gameState.levelStartScore) /
(gameState.levelSpawnedCoins || 1); (gameState.levelSpawnedCoins || 1);
let repeats = 1; let repeats = 1;

View file

@ -12,7 +12,6 @@ import {
} from "./types"; } from "./types";
import { import {
ballTransparency,
brickCenterX, brickCenterX,
brickCenterY, brickCenterY,
currentLevelInfo, currentLevelInfo,
@ -32,12 +31,12 @@ import {
telekinesisEffectRate, telekinesisEffectRate,
yoyoEffectRate, yoyoEffectRate,
} from "./game_utils"; } from "./game_utils";
import {t} from "./i18n/i18n"; import { t } from "./i18n/i18n";
import {icons} from "./loadGameData"; import { icons } from "./loadGameData";
import {getCurrentMaxCoins, getCurrentMaxParticles} from "./settings"; import { getCurrentMaxCoins, getCurrentMaxParticles } from "./settings";
import {background} from "./render"; import { background } from "./render";
import {gameOver} from "./gameOver"; import { gameOver } from "./gameOver";
import { import {
brickIndex, brickIndex,
fitSize, fitSize,
@ -48,11 +47,17 @@ import {
pause, pause,
startComputerControlledGame, startComputerControlledGame,
} from "./game"; } from "./game";
import {stopRecording} from "./recording"; import { stopRecording } from "./recording";
import {isOptionOn} from "./options"; import { isOptionOn } from "./options";
import {clamp, coinsBoostedCombo, comboKeepingRate, shouldCoinsStick} from "./pure_functions"; import {
import {addToTotalScore} from "./addToTotalScore"; ballTransparency,
import {hashCode} from "./getLevelBackground"; clamp,
coinsBoostedCombo,
comboKeepingRate,
shouldCoinsStick,
} from "./pure_functions";
import { addToTotalScore } from "./addToTotalScore";
import { hashCode } from "./getLevelBackground";
export function setMousePos(gameState: GameState, x: number) { export function setMousePos(gameState: GameState, x: number) {
if (gameState.startParams.computer_controlled) return; if (gameState.startParams.computer_controlled) return;
@ -216,11 +221,7 @@ export function baseCombo(gameState: GameState) {
gameState.perks.minefield && gameState.perks.minefield &&
gameState.bricks.filter((b) => b === "black").length * gameState.bricks.filter((b) => b === "black").length *
gameState.perks.minefield; gameState.perks.minefield;
return ( return 1 + gameState.perks.base_combo * 3 + mineFieldBonus;
1 +
gameState.perks.base_combo * 3 +
mineFieldBonus
);
} }
export function resetCombo( export function resetCombo(
@ -432,7 +433,7 @@ export function explodeBrick(
setBrick(gameState, index, ""); setBrick(gameState, index, "");
let coinsToSpawn = coinsBoostedCombo(gameState) let coinsToSpawn = coinsBoostedCombo(gameState);
gameState.levelSpawnedCoins += coinsToSpawn; gameState.levelSpawnedCoins += coinsToSpawn;
gameState.runStatistics.coins_spawned += coinsToSpawn; gameState.runStatistics.coins_spawned += coinsToSpawn;
@ -449,7 +450,7 @@ export function explodeBrick(
while (coinsToSpawn > 0) { while (coinsToSpawn > 0) {
const points = Math.min(pointsPerCoin, coinsToSpawn); const points = Math.min(pointsPerCoin, coinsToSpawn);
if (points < 0 || isNaN(points)) { if (points < 0 || isNaN(points)) {
console.error({points}); console.error({ points });
debugger; debugger;
} }
@ -630,9 +631,7 @@ export function addToScore(gameState: GameState, coin: Coin) {
gameState.highScore = gameState.score; gameState.highScore = gameState.score;
try { try {
localStorage.setItem("breakout-3-hs-short", gameState.score.toString()); localStorage.setItem("breakout-3-hs-short", gameState.score.toString());
} catch (e) { } catch (e) {}
}
} }
if (!isOptionOn("basic")) { if (!isOptionOn("basic")) {
makeParticle( makeParticle(
@ -872,7 +871,7 @@ export function attract(gameState: GameState, a: Ball, b: Ball, power: number) {
export function coinBrickHitCheck(gameState: GameState, coin: Coin) { export function coinBrickHitCheck(gameState: GameState, coin: Coin) {
// Make ball/coin bonce, and return bricks that were hit // Make ball/coin bonce, and return bricks that were hit
const radius = coin.size / 2; const radius = coin.size / 2;
const {x, y, previousX, previousY} = coin; const { x, y, previousX, previousY } = coin;
const vhit = hitsSomething(previousX, y, radius); const vhit = hitsSomething(previousX, y, radius);
const hhit = hitsSomething(x, previousY, radius); const hhit = hitsSomething(x, previousY, radius);
@ -884,12 +883,12 @@ export function coinBrickHitCheck(gameState: GameState, coin: Coin) {
if (typeof (vhit ?? hhit ?? chit) !== "undefined") { if (typeof (vhit ?? hhit ?? chit) !== "undefined") {
if (shouldCoinsStick(gameState)) { if (shouldCoinsStick(gameState)) {
if(coin.collidedLastFrame) { if (coin.collidedLastFrame) {
coin.x = previousX coin.x = previousX;
coin.y = previousY coin.y = previousY;
} }
coin.vx = 0 coin.vx = 0;
coin.vy = 0 coin.vy = 0;
} else if (gameState.perks.ghost_coins) { } else if (gameState.perks.ghost_coins) {
// slow down // slow down
coin.vy *= 1 - 0.2 / gameState.perks.ghost_coins; coin.vy *= 1 - 0.2 / gameState.perks.ghost_coins;
@ -945,7 +944,10 @@ export function bordersHitCheck(
let vhit = 0, let vhit = 0,
hhit = 0; hhit = 0;
if (coin.x < gameState.offsetXRoundedDown + radius && gameState.perks.left_is_lava < 2) { if (
coin.x < gameState.offsetXRoundedDown + radius &&
gameState.perks.left_is_lava < 2
) {
coin.x = coin.x =
gameState.offsetXRoundedDown + gameState.offsetXRoundedDown +
radius + radius +
@ -958,7 +960,10 @@ export function bordersHitCheck(
coin.vy *= -1; coin.vy *= -1;
vhit = 1; vhit = 1;
} }
if (coin.x > gameState.canvasWidth - gameState.offsetXRoundedDown - radius && gameState.perks.right_is_lava < 2) { if (
coin.x > gameState.canvasWidth - gameState.offsetXRoundedDown - radius &&
gameState.perks.right_is_lava < 2
) {
coin.x = coin.x =
gameState.canvasWidth - gameState.canvasWidth -
gameState.offsetXRoundedDown - gameState.offsetXRoundedDown -
@ -969,7 +974,6 @@ export function bordersHitCheck(
hhit = 1; hhit = 1;
} }
return hhit + vhit * 2; return hhit + vhit * 2;
} }
@ -1014,10 +1018,8 @@ export function gameStateTick(
if (gameState.perks.hot_start) { if (gameState.perks.hot_start) {
if (gameState.combo === baseCombo(gameState)) { if (gameState.combo === baseCombo(gameState)) {
// Give 1s of time between catching a coin and tick down // Give 1s of time between catching a coin and tick down
gameState.lastTickDown = gameState.levelTime gameState.lastTickDown = gameState.levelTime;
} else if ( } else if (gameState.levelTime > gameState.lastTickDown + 1000) {
gameState.levelTime > gameState.lastTickDown + 1000
) {
gameState.lastTickDown = gameState.levelTime; gameState.lastTickDown = gameState.levelTime;
decreaseCombo( decreaseCombo(
gameState, gameState,
@ -1028,7 +1030,6 @@ export function gameStateTick(
} }
} }
if ( if (
remainingBricks <= gameState.perks.skip_last && remainingBricks <= gameState.perks.skip_last &&
!gameState.autoCleanUses !gameState.autoCleanUses
@ -1066,7 +1067,7 @@ export function gameStateTick(
} else { } else {
gameOver( gameOver(
t("gameOver.win.title"), t("gameOver.win.title"),
t("gameOver.win.summary", {score: gameState.score}), t("gameOver.win.summary", { score: gameState.score }),
); );
} }
} else if (gameState.running || gameState.levelTime) { } else if (gameState.running || gameState.levelTime) {
@ -1161,10 +1162,12 @@ export function gameStateTick(
coin.vy *= ratio; coin.vy *= ratio;
coin.vx *= ratio; coin.vx *= ratio;
} }
if (coin.y > gameState.gameZoneHeight && coin.floatingTime < gameState.perks.buoy * 30) { if (
coin.y > gameState.gameZoneHeight &&
coin.floatingTime += frames coin.floatingTime < gameState.perks.buoy * 30
coin.vy -= 1.5 ) {
coin.floatingTime += frames;
coin.vy -= 1.5;
} }
if (coin.vx > 7 * gameState.baseSpeed) coin.vx = 7 * gameState.baseSpeed; if (coin.vx > 7 * gameState.baseSpeed) coin.vx = 7 * gameState.baseSpeed;
@ -1215,7 +1218,6 @@ export function gameStateTick(
const hitBorder = bordersHitCheck(gameState, coin, coin.size / 2, frames); const hitBorder = bordersHitCheck(gameState, coin, coin.size / 2, frames);
if ( if (
coin.previousY < gameState.gameZoneHeight && coin.previousY < gameState.gameZoneHeight &&
coin.y > gameState.gameZoneHeight && coin.y > gameState.gameZoneHeight &&
@ -1267,11 +1269,14 @@ export function gameStateTick(
if ( if (
gameState.combo < gameState.perks.fountain_toss * 30 && gameState.combo < gameState.perks.fountain_toss * 30 &&
Math.random() / coin.points < (1 / gameState.combo) * gameState.perks.fountain_toss Math.random() / coin.points <
(1 / gameState.combo) * gameState.perks.fountain_toss
) { ) {
increaseCombo(gameState, 1, increaseCombo(
gameState,
1,
clamp(coin.x, 20, gameState.canvasWidth - 20), clamp(coin.x, 20, gameState.canvasWidth - 20),
clamp(coin.y, 20, gameState.gameZoneHeight - 20) clamp(coin.y, 20, gameState.gameZoneHeight - 20),
); );
} }
} }
@ -1319,10 +1324,7 @@ export function gameStateTick(
} }
} }
// remember collision // remember collision
coin.collidedLastFrame = !!( coin.collidedLastFrame = !!(typeof hitBrick !== "undefined" || hitBorder);
typeof hitBrick !== "undefined" ||
hitBorder
)
}); });
gameState.balls.forEach((ball) => ballTick(gameState, ball, frames)); gameState.balls.forEach((ball) => ballTick(gameState, ball, frames));
@ -1330,7 +1332,6 @@ export function gameStateTick(
if (gameState.perks.shocks) { if (gameState.perks.shocks) {
gameState.balls.forEach((a, ai) => gameState.balls.forEach((a, ai) =>
gameState.balls.forEach((b, bi) => { gameState.balls.forEach((b, bi) => {
if ( if (
ai < bi && ai < bi &&
!a.destroyed && !a.destroyed &&
@ -1349,7 +1350,7 @@ export function gameStateTick(
let y = (a.y + b.y) / 2; let y = (a.y + b.y) / 2;
// space out the balls with extra speed // space out the balls with extra speed
if (gameState.perks.shocks > 1) { if (gameState.perks.shocks > 1) {
const limit = gameState.baseSpeed * gameState.perks.shocks / 2; const limit = (gameState.baseSpeed * gameState.perks.shocks) / 2;
a.vx += a.vx +=
clamp(a.x - x, -limit, limit) + clamp(a.x - x, -limit, limit) +
((Math.random() - 0.5) * limit) / 3; ((Math.random() - 0.5) * limit) / 3;
@ -1512,7 +1513,7 @@ export function gameStateTick(
setBrick(gameState, r.index, r.color); setBrick(gameState, r.index, r.color);
destroy(gameState.respawns, ri); destroy(gameState.respawns, ri);
} else { } else {
const {index, color} = r; const { index, color } = r;
const vertical = Math.random() > 0.5; const vertical = Math.random() > 0.5;
const dx = Math.random() > 0.5 ? 1 : -1; const dx = Math.random() > 0.5 ? 1 : -1;
const dy = Math.random() > 0.5 ? 1 : -1; const dy = Math.random() > 0.5 ? 1 : -1;
@ -1641,8 +1642,7 @@ export function ballTick(gameState: GameState, ball: Ball, frames: number) {
frames, frames,
); );
if (borderHitCode) { if (borderHitCode) {
ball.sidesHitsSinceBounce++;
ball.sidesHitsSinceBounce++
if (ball.sidesHitsSinceBounce <= gameState.perks.three_cushion * 3) { if (ball.sidesHitsSinceBounce <= gameState.perks.three_cushion * 3) {
increaseCombo(gameState, 1, ball.x, ball.y); increaseCombo(gameState, 1, ball.x, ball.y);
} }
@ -1755,13 +1755,10 @@ export function ballTick(gameState: GameState, ball: Ball, frames: number) {
if ( if (
gameState.running && gameState.running &&
( (ball.y > gameState.gameZoneHeight + gameState.ballSize / 2 ||
ball.y > gameState.gameZoneHeight + gameState.ballSize / 2 ||
ball.y < -gameState.gameZoneHeight || ball.y < -gameState.gameZoneHeight ||
ball.x < -gameState.gameZoneHeight || ball.x < -gameState.gameZoneHeight ||
ball.x > gameState.canvasWidth + gameState.gameZoneHeight ball.x > gameState.canvasWidth + gameState.gameZoneHeight)
)
) { ) {
ball.destroyed = true; ball.destroyed = true;
gameState.runStatistics.balls_lost++; gameState.runStatistics.balls_lost++;
@ -1771,14 +1768,14 @@ export function ballTick(gameState: GameState, ball: Ball, frames: number) {
} else { } else {
gameOver( gameOver(
t("gameOver.lost.title"), t("gameOver.lost.title"),
t("gameOver.lost.summary", {score: gameState.score}), t("gameOver.lost.summary", { score: gameState.score }),
); );
} }
} }
} }
const radius = gameState.ballSize / 2; const radius = gameState.ballSize / 2;
// Make ball/coin bonce, and return bricks that were hit // Make ball/coin bonce, and return bricks that were hit
const {x, y, previousX, previousY} = ball; const { x, y, previousX, previousY } = ball;
const vhit = hitsSomething(previousX, y, radius); const vhit = hitsSomething(previousX, y, radius);
const hhit = hitsSomething(x, previousY, radius); const hhit = hitsSomething(x, previousY, radius);
@ -1841,7 +1838,7 @@ export function ballTick(gameState: GameState, ball: Ball, frames: number) {
if (!gameState.brickHP[hitBrick]) { if (!gameState.brickHP[hitBrick]) {
ball.brokenSinceBounce++; ball.brokenSinceBounce++;
applyOttawaTreatyPerk(gameState, hitBrick, ball) applyOttawaTreatyPerk(gameState, hitBrick, ball);
explodeBrick(gameState, hitBrick, ball, false); explodeBrick(gameState, hitBrick, ball, false);
if ( if (
ball.sapperUses < gameState.perks.sapper && ball.sapperUses < gameState.perks.sapper &&
@ -1851,8 +1848,6 @@ export function ballTick(gameState: GameState, ball: Ball, frames: number) {
setBrick(gameState, hitBrick, "black"); setBrick(gameState, hitBrick, "black");
ball.sapperUses++; ball.sapperUses++;
} }
} else { } else {
schedulGameSound(gameState, "wallBeep", x, 1); schedulGameSound(gameState, "wallBeep", x, 1);
makeLight( makeLight(
@ -1971,7 +1966,7 @@ function makeCoin(
p.points = points; p.points = points;
p.weight = weight; p.weight = weight;
p.metamorphosisPoints = gameState.perks.metamorphosis; p.metamorphosisPoints = gameState.perks.metamorphosis;
p.floatingTime = 0 p.floatingTime = 0;
}); });
} }
@ -2053,7 +2048,7 @@ export function append<T>(
makeItem(where.list[where.indexMin]); makeItem(where.list[where.indexMin]);
where.indexMin++; where.indexMin++;
} else { } else {
const p = {destroyed: false}; const p = { destroyed: false };
makeItem(p); makeItem(p);
where.list.push(p); where.list.push(p);
} }
@ -2146,32 +2141,37 @@ function goToNearestBrick(
} }
} }
function applyOttawaTreatyPerk(
gameState: GameState,
index: number,
ball: Ball,
) {
if (!gameState.perks.ottawa_treaty) return;
if (ball.sapperUses) return;
function applyOttawaTreatyPerk(gameState: GameState, index: number, ball: Ball) { const originalColor = gameState.bricks[index];
if (!gameState.perks.ottawa_treaty) return if (originalColor == "black") return;
if (ball.sapperUses) return const x = index % gameState.gridSize;
const y = Math.floor(index / gameState.gridSize);
const originalColor = gameState.bricks[index] let converted = 0;
if (originalColor == 'black') return
const x = index % gameState.gridSize
const y = Math.floor(index / gameState.gridSize)
let converted = 0
for (let dx = -1; dx <= 1; dx++) for (let dx = -1; dx <= 1; dx++)
for (let dy = -1; dy <= 1; dy++) for (let dy = -1; dy <= 1; dy++)
if (dx || dy) { if (dx || dy) {
const nIndex = getRowColIndex(gameState, y + dy, x + dx) const nIndex = getRowColIndex(gameState, y + dy, x + dx);
if (gameState.bricks[nIndex] && gameState.bricks[nIndex] === 'black') { if (gameState.bricks[nIndex] && gameState.bricks[nIndex] === "black") {
setBrick(gameState, nIndex, originalColor);
setBrick(gameState, nIndex, originalColor) schedulGameSound(
schedulGameSound(gameState, "colorChange", brickCenterX(gameState, index), 1) gameState,
"colorChange",
brickCenterX(gameState, index),
1,
);
// Avoid infinite bricks generation hack // Avoid infinite bricks generation hack
ball.sapperUses = Infinity ball.sapperUses = Infinity;
converted++ converted++;
// Don't convert more than one brick per hit normally // Don't convert more than one brick per hit normally
if (converted >= gameState.perks.ottawa_treaty) return if (converted >= gameState.perks.ottawa_treaty) return;
} }
} }
return return;
} }

View file

@ -13,7 +13,7 @@ import { t } from "./i18n/i18n";
import { clamp } from "./pure_functions"; import { clamp } from "./pure_functions";
import { rawUpgrades } from "./upgrades"; import { rawUpgrades } from "./upgrades";
import { hashCode } from "./getLevelBackground"; import { hashCode } from "./getLevelBackground";
import { getTotalScore } from "./settings"; import { getSettingValue, getTotalScore } from "./settings";
import { isOptionOn } from "./options"; import { isOptionOn } from "./options";
export function describeLevel(level: Level) { export function describeLevel(level: Level) {
@ -392,16 +392,6 @@ export function reasonLevelIsLocked(
} }
} }
export function ballTransparency(ball: Ball, gameState: GameState) {
if (!gameState.perks.transparency) return 0;
return clamp(
gameState.perks.transparency *
(1 - (ball.y / gameState.gameZoneHeight) * 1.2),
0,
1,
);
}
export function getCoinRenderColor(gameState: GameState, coin: Coin) { export function getCoinRenderColor(gameState: GameState, coin: Coin) {
if ( if (
gameState.perks.metamorphosis || gameState.perks.metamorphosis ||
@ -423,3 +413,12 @@ export function getCornerOffset(gameState: GameState) {
} }
export const isInWebView = !!window.location.href.includes("isInWebView=true"); export const isInWebView = !!window.location.href.includes("isInWebView=true");
export function hoursSpentPlaying() {
try {
const timePlayed = getSettingValue("breakout_71_total_play_time", 0);
return Math.floor(timePlayed / 1000 / 60 / 60);
} catch (e) {
return 0;
}
}

View file

@ -1,10 +1,6 @@
import { icons, transformRawLevel } from "./loadGameData"; import { icons, transformRawLevel } from "./loadGameData";
import { t } from "./i18n/i18n"; import { t } from "./i18n/i18n";
import { import { getSettingValue, getTotalScore, setSettingValue } from "./settings";
getSettingValue,
getTotalScore,
setSettingValue,
} from "./settings";
import { asyncAlert } from "./asyncAlert"; import { asyncAlert } from "./asyncAlert";
import { Palette, RawLevel } from "./types"; import { Palette, RawLevel } from "./types";
import { levelIconHTML } from "./levelIcon"; import { levelIconHTML } from "./levelIcon";
@ -165,7 +161,6 @@ export async function editRawLevelList(nth: number, color = "W") {
text: t("editor.editing.copy"), text: t("editor.editing.copy"),
value: "copy", value: "copy",
help: t("editor.editing.copy_help"), help: t("editor.editing.copy_help"),
}, },
{ {
text: t("editor.editing.bigger"), text: t("editor.editing.bigger"),
@ -250,7 +245,10 @@ export async function editRawLevelList(nth: number, color = "W") {
return; return;
} }
if (action === "copy") { if (action === "copy") {
let text = "```\n[" + (level.name||'unnamed level')?.replace(/\[|\]/gi, " ") + "]"; let text =
"```\n[" +
(level.name || "unnamed level")?.replace(/\[|\]/gi, " ") +
"]";
bricks.forEach((b, bi) => { bricks.forEach((b, bi) => {
if (!(bi % level.size)) text += "\n"; if (!(bi % level.size)) text += "\n";
text += b; text += b;

View file

@ -1,7 +1,6 @@
import _palette from "./data/palette.json"; import _palette from "./data/palette.json";
import _rawLevelsList from "./data/levels.json"; import _rawLevelsList from "./data/levels.json";
import _appVersion from "./data/version.json"; import _appVersion from "./data/version.json";
import { rawUpgrades } from "./upgrades";
describe("json data checks", () => { describe("json data checks", () => {
it("_rawLevelsList has icon levels", () => { it("_rawLevelsList has icon levels", () => {
@ -10,13 +9,6 @@ describe("json data checks", () => {
).toBeGreaterThan(10); ).toBeGreaterThan(10);
}); });
it("all upgrades have icons", () => {
const missingIcon = rawUpgrades.filter(
(u) => !_rawLevelsList.find((l) => l.name == "icon:" + u.id),
);
expect(missingIcon).toEqual([]);
});
it("_rawLevelsList has non-icon few levels", () => { it("_rawLevelsList has non-icon few levels", () => {
expect( expect(
_rawLevelsList.filter((l) => !l.name.startsWith("icon:")).length, _rawLevelsList.filter((l) => !l.name.startsWith("icon:")).length,

View file

@ -140,16 +140,15 @@ migrate("set_breakout_71_unlocked_levels" + _appVersion, () => {
); );
}); });
migrate('clean_ls', ()=>{ migrate("clean_ls", () => {
for (let key in localStorage) { for (let key in localStorage) {
try { try {
JSON.parse(localStorage.getItem(key) || "null"); JSON.parse(localStorage.getItem(key) || "null");
} catch (e) { } catch (e) {
localStorage.removeItem(key) localStorage.removeItem(key);
console.warn('Removed invalid key '+key,e); console.warn("Removed invalid key " + key, e);
} }
} }
});
})
afterMigration(); afterMigration();

View file

@ -2,7 +2,8 @@ import { t } from "./i18n/i18n";
import { OptionDef, OptionId } from "./types"; import { OptionDef, OptionId } from "./types";
import { getSettingValue, setSettingValue } from "./settings"; import { getSettingValue, setSettingValue } from "./settings";
import { hoursSpentPlaying } from "./pure_functions";
import { hoursSpentPlaying } from "./game_utils";
export const options = { export const options = {
sound: { sound: {

View file

@ -1,6 +1,4 @@
import { getSettingValue } from "./settings"; import { Ball, GameState } from "./types";
import {GameState} from "./types";
import {ballTransparency} from "./game_utils";
export function clamp(value: number, min: number, max: number) { export function clamp(value: number, min: number, max: number) {
return Math.max(min, Math.min(value, max)); return Math.max(min, Math.min(value, max));
@ -10,33 +8,39 @@ export function comboKeepingRate(level: number) {
return clamp(1 - (1 / (1 + level)) * 1.5, 0, 1); return clamp(1 - (1 / (1 + level)) * 1.5, 0, 1);
} }
export function hoursSpentPlaying() { export function shouldCoinsStick(gameState: GameState) {
try { return (
const timePlayed = getSettingValue("breakout_71_total_play_time", 0); gameState.perks.sticky_coins &&
return Math.floor(timePlayed / 1000 / 60 / 60); (!gameState.lastExplosion ||
} catch (e) { gameState.lastExplosion <
return 0; gameState.levelTime - 300 * gameState.perks.sticky_coins)
} );
} }
export function shouldCoinsStick(gameState:GameState){ export function ballTransparency(ball: Ball, gameState: GameState) {
return gameState.perks.sticky_coins && (!gameState.lastExplosion || gameState.lastExplosion < gameState.levelTime - 300 * gameState.perks.sticky_coins) if (!gameState.perks.transparency) return 0;
return clamp(
gameState.perks.transparency *
(1 - (ball.y / gameState.gameZoneHeight) * 1.2),
0,
1,
);
} }
export function coinsBoostedCombo(gameState:GameState){ export function coinsBoostedCombo(gameState: GameState) {
let boost = 1+gameState.perks.sturdy_bricks / 2 + gameState.perks.smaller_puck/2 let boost =
if(gameState.perks.transparency){ 1 + gameState.perks.sturdy_bricks / 2 + gameState.perks.smaller_puck / 2;
let min=1; if (gameState.perks.transparency) {
gameState.balls.forEach(ball=>{ let min = 1;
const bt=ballTransparency(ball, gameState) gameState.balls.forEach((ball) => {
if(bt<min){ const bt = ballTransparency(ball, gameState);
min=bt if (bt < min) {
min = bt;
} }
}) });
boost+=min*gameState.perks.transparency / 2 boost += (min * gameState.perks.transparency) / 2;
} }
return Math.ceil(Math.max(gameState.combo,gameState.lastCombo) * boost) return Math.ceil(Math.max(gameState.combo, gameState.lastCombo) * boost);
} }
export function miniMarkDown(md: string) { export function miniMarkDown(md: string) {

View file

@ -1,6 +1,5 @@
import { baseCombo, forEachLiveOne, liveCount } from "./gameStateMutators"; import { baseCombo, forEachLiveOne, liveCount } from "./gameStateMutators";
import { import {
ballTransparency,
brickCenterX, brickCenterX,
brickCenterY, brickCenterY,
currentLevelInfo, currentLevelInfo,
@ -18,8 +17,10 @@ import { t } from "./i18n/i18n";
import { gameState, lastMeasuredFPS, startWork } from "./game"; import { gameState, lastMeasuredFPS, startWork } from "./game";
import { isOptionOn } from "./options"; import { isOptionOn } from "./options";
import { import {
ballTransparency,
catchRateBest, catchRateBest,
catchRateGood, coinsBoostedCombo, catchRateGood,
coinsBoostedCombo,
levelTimeBest, levelTimeBest,
levelTimeGood, levelTimeGood,
missesBest, missesBest,
@ -75,12 +76,11 @@ export function render(gameState: GameState) {
} }
const catchRate = gameState.levelSpawnedCoins const catchRate = gameState.levelSpawnedCoins
? ? (gameState.score - gameState.levelStartScore) /
(gameState.score - gameState.levelStartScore) /
(gameState.levelSpawnedCoins || 1) (gameState.levelSpawnedCoins || 1)
// (gameState.levelSpawnedCoins - gameState.levelLostCoins) / : // (gameState.levelSpawnedCoins - gameState.levelLostCoins) /
// gameState.levelSpawnedCoins // gameState.levelSpawnedCoins
: 1; 1;
startWork("render:scoreDisplay"); startWork("render:scoreDisplay");
scoreDisplay.innerHTML = scoreDisplay.innerHTML =
(isOptionOn("show_fps") || gameState.startParams.computer_controlled (isOptionOn("show_fps") || gameState.startParams.computer_controlled
@ -440,7 +440,7 @@ export function render(gameState: GameState) {
); );
startWork("render:combotext"); startWork("render:combotext");
const spawns=coinsBoostedCombo(gameState) const spawns = coinsBoostedCombo(gameState);
if (spawns > 1) { if (spawns > 1) {
ctx.globalCompositeOperation = "source-over"; ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = 1; ctx.globalAlpha = 1;
@ -500,7 +500,7 @@ export function render(gameState: GameState) {
if (gameState.offsetXRoundedDown) { if (gameState.offsetXRoundedDown) {
// draw outside of gaming area to avoid capturing borders in recordings // draw outside of gaming area to avoid capturing borders in recordings
if(gameState.perks.left_is_lava<2) if (gameState.perks.left_is_lava < 2)
drawStraightLine( drawStraightLine(
ctx, ctx,
gameState, gameState,
@ -511,7 +511,7 @@ export function render(gameState: GameState) {
height, height,
1, 1,
); );
if(gameState.perks.right_is_lava<2) if (gameState.perks.right_is_lava < 2)
drawStraightLine( drawStraightLine(
ctx, ctx,
gameState, gameState,
@ -523,8 +523,7 @@ export function render(gameState: GameState) {
1, 1,
); );
} else { } else {
if (gameState.perks.left_is_lava < 2)
if(gameState.perks.left_is_lava<2)
drawStraightLine( drawStraightLine(
ctx, ctx,
gameState, gameState,
@ -536,7 +535,7 @@ export function render(gameState: GameState) {
1, 1,
); );
if(gameState.perks.right_is_lava<2) if (gameState.perks.right_is_lava < 2)
drawStraightLine( drawStraightLine(
ctx, ctx,
gameState, gameState,
@ -549,7 +548,7 @@ export function render(gameState: GameState) {
); );
} }
if (redTop && gameState.perks.top_is_lava<2) if (redTop && gameState.perks.top_is_lava < 2)
drawStraightLine( drawStraightLine(
ctx, ctx,
gameState, gameState,

View file

@ -14,7 +14,7 @@ try {
warnedUserAboutLSIssue = true; warnedUserAboutLSIssue = true;
toast(`Storage issue : ${(e as Error)?.message}`); toast(`Storage issue : ${(e as Error)?.message}`);
} }
console.warn('Reading '+key,e); console.warn("Reading " + key, e);
} }
} }
} catch (e) { } catch (e) {

2
src/types.d.ts vendored
View file

@ -84,7 +84,7 @@ export type Coin = {
destroyed?: boolean; destroyed?: boolean;
collidedLastFrame?: boolean; collidedLastFrame?: boolean;
metamorphosisPoints: number; metamorphosisPoints: number;
floatingTime:number; floatingTime: number;
}; };
export type Ball = { export type Ball = {
x: number; x: number;

View file

@ -189,7 +189,8 @@ export const rawUpgrades = [
id: "smaller_puck", id: "smaller_puck",
max: 2, max: 2,
name: t("upgrades.smaller_puck.name"), name: t("upgrades.smaller_puck.name"),
help: (lvl: number) => t("upgrades.smaller_puck.tooltip", {percent:50*lvl}), help: (lvl: number) =>
t("upgrades.smaller_puck.tooltip", { percent: 50 * lvl }),
fullHelp: t("upgrades.smaller_puck.verbose_description"), fullHelp: t("upgrades.smaller_puck.verbose_description"),
}, },
{ {
@ -725,7 +726,6 @@ export const rawUpgrades = [
requires: "", requires: "",
threshold: 175000, threshold: 175000,
gift: false, gift: false,
id: "limitless", id: "limitless",
max: 1, max: 1,
name: t("upgrades.limitless.name"), name: t("upgrades.limitless.name"),
@ -828,8 +828,7 @@ export const rawUpgrades = [
id: "buoy", id: "buoy",
max: 3, max: 3,
name: t("upgrades.buoy.name"), name: t("upgrades.buoy.name"),
help: (lvl: number) => help: (lvl: number) => t("upgrades.buoy.tooltip", { duration: lvl * 0.5 }),
t("upgrades.buoy.tooltip", { duration: lvl * 0.5 }),
fullHelp: t("upgrades.buoy.verbose_description"), fullHelp: t("upgrades.buoy.verbose_description"),
}, },
{ {
@ -839,7 +838,7 @@ export const rawUpgrades = [
id: "ottawa_treaty", id: "ottawa_treaty",
max: 1, max: 1,
name: t("upgrades.ottawa_treaty.name"), name: t("upgrades.ottawa_treaty.name"),
help: () =>t("upgrades.ottawa_treaty.tooltip"), help: () => t("upgrades.ottawa_treaty.tooltip"),
fullHelp: t("upgrades.ottawa_treaty.verbose_description"), fullHelp: t("upgrades.ottawa_treaty.verbose_description"),
}, },
{ {
@ -849,7 +848,8 @@ export const rawUpgrades = [
id: "three_cushion", id: "three_cushion",
max: 1, max: 1,
name: t("upgrades.three_cushion.name"), name: t("upgrades.three_cushion.name"),
help: (lvl:number) =>t("upgrades.three_cushion.tooltip",{max:lvl*3}), help: (lvl: number) =>
t("upgrades.three_cushion.tooltip", { max: lvl * 3 }),
fullHelp: t("upgrades.three_cushion.verbose_description"), fullHelp: t("upgrades.three_cushion.verbose_description"),
}, },
{ {
@ -859,8 +859,7 @@ export const rawUpgrades = [
id: "sticky_coins", id: "sticky_coins",
max: 1, max: 1,
name: t("upgrades.sticky_coins.name"), name: t("upgrades.sticky_coins.name"),
help: (lvl:number) =>t("upgrades.sticky_coins.tooltip"), help: (lvl: number) => t("upgrades.sticky_coins.tooltip"),
fullHelp: t("upgrades.sticky_coins.verbose_description"), fullHelp: t("upgrades.sticky_coins.verbose_description"),
}, },
] as const; ] as const;