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

@ -10,20 +10,14 @@ Break colourful bricks, catch bouncing coins and select powerful upgrades !
- [F-Droid](https://f-droid.org/en/packages/me.lecaro.breakout/)
- [Google Play](https://play.google.com/store/apps/details?id=me.lecaro.breakout)
- [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
## To do
## 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)
- 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)

View file

@ -29,8 +29,8 @@ android {
applicationId = "me.lecaro.breakout"
minSdk = 21
targetSdk = 34
versionCode = 29085904
versionName = "29085904"
versionCode = 29087244
versionName = "29087244"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
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.
const VERSION = "29085904";
const VERSION = "29087244";
// The name of the cache
const CACHE_NAME = `breakout-71-${VERSION}`;

View file

@ -36,54 +36,54 @@ export function creativeMode(gameState: GameState) {
export async function openCreativeModePerksPicker() {
let creativeModePerks: Partial<{ [id in PerkId]: number }> = getSettingValue(
"creativeModePerks",
{},
);
"creativeModePerks",
{},
);
const customLevels = (getSettingValue("custom_levels", []) as RawLevel[]).map(
transformRawLevel,
);
while (true ) {
while (true) {
const levelOptions = [
...allLevels.map((l, li) => {
const problem = reasonLevelIsLocked(li, getHistory(), true)?.text || "";
return {
icon: icons[l.name],
text: l.name,
value: l,
disabled: !!problem,
tooltip: problem || describeLevel(l),
className: "",
};
}),
...customLevels.map((l) => ({
icon: levelIconHTML(l.bricks, l.size, l.color),
text: l.name,
value: l,
disabled: !l.bricks.filter((b) => b !== "_").length,
tooltip: describeLevel(l),
className: "",
})),
];
const levelOptions= [
...allLevels.map((l, li) => {
const problem =
reasonLevelIsLocked(li, getHistory(), true)?.text || "";
return {
icon: icons[l.name],
text: l.name,
value: l,
disabled: !!problem,
tooltip: problem || describeLevel(l),
className:''
};
}),
...customLevels.map((l) => ({
icon: levelIconHTML(l.bricks, l.size, l.color),
text: l.name,
value: l,
disabled: !l.bricks.filter((b) => b !== "_").length,
tooltip: describeLevel(l),
className:''
}))
]
const selectedLeveOption =
levelOptions.find(
(l) => l.text === getSettingValue("creativeModeLevel", ""),
) || levelOptions[0];
selectedLeveOption.className = "highlight";
const selectedLeveOption= 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"),
className: "actionsAsGrid",
content: [
{
icon: icons['icon:reset'],
icon: icons["icon:reset"],
value: "reset",
text: t("lab.reset"),
disabled: !sumOfValues(creativeModePerks),
},
{
icon: icons['icon:new_run'],
{
icon: icons["icon:new_run"],
value: "play",
text: t("lab.play"),
disabled: !sumOfValues(creativeModePerks),
@ -106,24 +106,28 @@ export async function openCreativeModePerksPicker() {
tooltip: u.help(creativeModePerks[u.id] || 1),
})),
t("lab.select_level"),
...levelOptions
...levelOptions,
],
})
if(!choice)return
});
if (!choice) return;
if (choice === "reset") {
upgrades.forEach((u) => {
creativeModePerks[u.id] = 0;
});
setSettingValue("creativeModePerks", creativeModePerks);
setSettingValue("creativeModeLevel", '')
} else if (choice === "play" || ("bricks" in choice && choice.name==getSettingValue("creativeModeLevel", ''))) {
setSettingValue("creativeModeLevel", "");
} else if (
choice === "play" ||
("bricks" in choice &&
choice.name == getSettingValue("creativeModeLevel", ""))
) {
if (await confirmRestart(gameState)) {
restart({
perks: creativeModePerks,
level: selectedLeveOption.value,
isCreativeRun: true,
});
return
return;
}
} else if ("bricks" in choice) {
setSettingValue("creativeModeLevel", choice.name);

View file

@ -1373,4 +1373,4 @@
"svg": null,
"color": ""
}
]
]

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -13,7 +13,7 @@ import { t } from "./i18n/i18n";
import { clamp } from "./pure_functions";
import { rawUpgrades } from "./upgrades";
import { hashCode } from "./getLevelBackground";
import { getTotalScore } from "./settings";
import { getSettingValue, getTotalScore } from "./settings";
import { isOptionOn } from "./options";
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) {
if (
gameState.perks.metamorphosis ||
@ -423,3 +413,12 @@ export function getCornerOffset(gameState: GameState) {
}
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 { t } from "./i18n/i18n";
import {
getSettingValue,
getTotalScore,
setSettingValue,
} from "./settings";
import { getSettingValue, getTotalScore, setSettingValue } from "./settings";
import { asyncAlert } from "./asyncAlert";
import { Palette, RawLevel } from "./types";
import { levelIconHTML } from "./levelIcon";
@ -165,7 +161,6 @@ export async function editRawLevelList(nth: number, color = "W") {
text: t("editor.editing.copy"),
value: "copy",
help: t("editor.editing.copy_help"),
},
{
text: t("editor.editing.bigger"),
@ -250,7 +245,10 @@ export async function editRawLevelList(nth: number, color = "W") {
return;
}
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) => {
if (!(bi % level.size)) text += "\n";
text += b;

View file

@ -1,7 +1,6 @@
import _palette from "./data/palette.json";
import _rawLevelsList from "./data/levels.json";
import _appVersion from "./data/version.json";
import { rawUpgrades } from "./upgrades";
describe("json data checks", () => {
it("_rawLevelsList has icon levels", () => {
@ -10,13 +9,6 @@ describe("json data checks", () => {
).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", () => {
expect(
_rawLevelsList.filter((l) => !l.name.startsWith("icon:")).length,

View file

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

View file

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

View file

@ -1,6 +1,4 @@
import { getSettingValue } from "./settings";
import {GameState} from "./types";
import {ballTransparency} from "./game_utils";
import { Ball, GameState } from "./types";
export function clamp(value: number, min: number, max: number) {
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);
}
export function hoursSpentPlaying() {
try {
const timePlayed = getSettingValue("breakout_71_total_play_time", 0);
return Math.floor(timePlayed / 1000 / 60 / 60);
} catch (e) {
return 0;
}
export function shouldCoinsStick(gameState: GameState) {
return (
gameState.perks.sticky_coins &&
(!gameState.lastExplosion ||
gameState.lastExplosion <
gameState.levelTime - 300 * gameState.perks.sticky_coins)
);
}
export function shouldCoinsStick(gameState:GameState){
return gameState.perks.sticky_coins && (!gameState.lastExplosion || gameState.lastExplosion < gameState.levelTime - 300 * gameState.perks.sticky_coins)
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 coinsBoostedCombo(gameState:GameState){
let boost = 1+gameState.perks.sturdy_bricks / 2 + gameState.perks.smaller_puck/2
if(gameState.perks.transparency){
let min=1;
gameState.balls.forEach(ball=>{
const bt=ballTransparency(ball, gameState)
if(bt<min){
min=bt
export function coinsBoostedCombo(gameState: GameState) {
let boost =
1 + gameState.perks.sturdy_bricks / 2 + gameState.perks.smaller_puck / 2;
if (gameState.perks.transparency) {
let min = 1;
gameState.balls.forEach((ball) => {
const bt = ballTransparency(ball, gameState);
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) {

View file

@ -1,6 +1,5 @@
import { baseCombo, forEachLiveOne, liveCount } from "./gameStateMutators";
import {
ballTransparency,
brickCenterX,
brickCenterY,
currentLevelInfo,
@ -18,8 +17,10 @@ import { t } from "./i18n/i18n";
import { gameState, lastMeasuredFPS, startWork } from "./game";
import { isOptionOn } from "./options";
import {
ballTransparency,
catchRateBest,
catchRateGood, coinsBoostedCombo,
catchRateGood,
coinsBoostedCombo,
levelTimeBest,
levelTimeGood,
missesBest,
@ -75,12 +76,11 @@ export function render(gameState: GameState) {
}
const catchRate = gameState.levelSpawnedCoins
?
(gameState.score - gameState.levelStartScore) /
(gameState.levelSpawnedCoins || 1)
// (gameState.levelSpawnedCoins - gameState.levelLostCoins) /
? (gameState.score - gameState.levelStartScore) /
(gameState.levelSpawnedCoins || 1)
: // (gameState.levelSpawnedCoins - gameState.levelLostCoins) /
// gameState.levelSpawnedCoins
: 1;
1;
startWork("render:scoreDisplay");
scoreDisplay.innerHTML =
(isOptionOn("show_fps") || gameState.startParams.computer_controlled
@ -440,12 +440,12 @@ export function render(gameState: GameState) {
);
startWork("render:combotext");
const spawns=coinsBoostedCombo(gameState)
if (spawns > 1) {
const spawns = coinsBoostedCombo(gameState);
if (spawns > 1) {
ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = 1;
const comboText = spawns.toString();
const comboText = spawns.toString();
const comboTextWidth = (comboText.length * gameState.puckHeight) / 1.8;
const totalWidth = comboTextWidth + gameState.coinSize * 2;
const left = gameState.puckPosition - totalWidth / 2;
@ -500,56 +500,55 @@ export function render(gameState: GameState) {
if (gameState.offsetXRoundedDown) {
// draw outside of gaming area to avoid capturing borders in recordings
if(gameState.perks.left_is_lava<2)
drawStraightLine(
ctx,
gameState,
(redLeftSide && "#FF0000") || "#FFFFFF",
gameState.offsetXRoundedDown - 1,
0,
gameState.offsetXRoundedDown - 1,
height,
1,
);
if(gameState.perks.right_is_lava<2)
drawStraightLine(
ctx,
gameState,
(redRightSide && "#FF0000") || "#FFFFFF",
width - gameState.offsetXRoundedDown + 1,
0,
width - gameState.offsetXRoundedDown + 1,
height,
1,
);
if (gameState.perks.left_is_lava < 2)
drawStraightLine(
ctx,
gameState,
(redLeftSide && "#FF0000") || "#FFFFFF",
gameState.offsetXRoundedDown - 1,
0,
gameState.offsetXRoundedDown - 1,
height,
1,
);
if (gameState.perks.right_is_lava < 2)
drawStraightLine(
ctx,
gameState,
(redRightSide && "#FF0000") || "#FFFFFF",
width - gameState.offsetXRoundedDown + 1,
0,
width - gameState.offsetXRoundedDown + 1,
height,
1,
);
} else {
if (gameState.perks.left_is_lava < 2)
drawStraightLine(
ctx,
gameState,
(redLeftSide && "#FF0000") || "",
0,
0,
0,
height,
1,
);
if(gameState.perks.left_is_lava<2)
drawStraightLine(
ctx,
gameState,
(redLeftSide && "#FF0000") || "",
0,
0,
0,
height,
1,
);
if(gameState.perks.right_is_lava<2)
drawStraightLine(
ctx,
gameState,
(redRightSide && "#FF0000") || "",
width - 1,
0,
width - 1,
height,
1,
);
if (gameState.perks.right_is_lava < 2)
drawStraightLine(
ctx,
gameState,
(redRightSide && "#FF0000") || "",
width - 1,
0,
width - 1,
height,
1,
);
}
if (redTop && gameState.perks.top_is_lava<2)
if (redTop && gameState.perks.top_is_lava < 2)
drawStraightLine(
ctx,
gameState,

View file

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

2
src/types.d.ts vendored
View file

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

View file

@ -189,7 +189,8 @@ export const rawUpgrades = [
id: "smaller_puck",
max: 2,
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"),
},
{
@ -718,14 +719,13 @@ export const rawUpgrades = [
id: "fountain_toss",
max: 7,
name: t("upgrades.fountain_toss.name"),
help: () => t("upgrades.fountain_toss.tooltip"),
help: () => t("upgrades.fountain_toss.tooltip"),
fullHelp: t("upgrades.fountain_toss.verbose_description"),
},
{
requires: "",
threshold: 175000,
gift: false,
id: "limitless",
max: 1,
name: t("upgrades.limitless.name"),
@ -828,8 +828,7 @@ export const rawUpgrades = [
id: "buoy",
max: 3,
name: t("upgrades.buoy.name"),
help: (lvl: number) =>
t("upgrades.buoy.tooltip", { duration: lvl * 0.5 }),
help: (lvl: number) => t("upgrades.buoy.tooltip", { duration: lvl * 0.5 }),
fullHelp: t("upgrades.buoy.verbose_description"),
},
{
@ -839,7 +838,7 @@ export const rawUpgrades = [
id: "ottawa_treaty",
max: 1,
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"),
},
{
@ -849,18 +848,18 @@ export const rawUpgrades = [
id: "three_cushion",
max: 1,
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"),
},
{
{
requires: "",
threshold: 235000,
gift: false,
id: "sticky_coins",
max: 1,
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"),
},
] as const;