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/) - [F-Droid](https://f-droid.org/en/packages/me.lecaro.breakout/)
- [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

@ -36,54 +36,54 @@ export function creativeMode(gameState: GameState) {
export async function openCreativeModePerksPicker() { export async function openCreativeModePerksPicker() {
let creativeModePerks: Partial<{ [id in PerkId]: number }> = getSettingValue( let creativeModePerks: Partial<{ [id in PerkId]: number }> = getSettingValue(
"creativeModePerks", "creativeModePerks",
{}, {},
); );
const customLevels = (getSettingValue("custom_levels", []) as RawLevel[]).map( const customLevels = (getSettingValue("custom_levels", []) as RawLevel[]).map(
transformRawLevel, 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= [ const selectedLeveOption =
...allLevels.map((l, li) => { levelOptions.find(
const problem = (l) => l.text === getSettingValue("creativeModeLevel", ""),
reasonLevelIsLocked(li, getHistory(), true)?.text || ""; ) || levelOptions[0];
return { selectedLeveOption.className = "highlight";
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] const choice = await asyncAlert<Upgrade | Level | "reset" | "play">({
selectedLeveOption.className= 'highlight'
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

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

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;

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 { 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,12 +440,12 @@ 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;
const comboText = spawns.toString(); const comboText = spawns.toString();
const comboTextWidth = (comboText.length * gameState.puckHeight) / 1.8; const comboTextWidth = (comboText.length * gameState.puckHeight) / 1.8;
const totalWidth = comboTextWidth + gameState.coinSize * 2; const totalWidth = comboTextWidth + gameState.coinSize * 2;
const left = gameState.puckPosition - totalWidth / 2; const left = gameState.puckPosition - totalWidth / 2;
@ -500,56 +500,55 @@ 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,
(redLeftSide && "#FF0000") || "#FFFFFF", (redLeftSide && "#FF0000") || "#FFFFFF",
gameState.offsetXRoundedDown - 1, gameState.offsetXRoundedDown - 1,
0, 0,
gameState.offsetXRoundedDown - 1, gameState.offsetXRoundedDown - 1,
height, height,
1, 1,
); );
if(gameState.perks.right_is_lava<2) if (gameState.perks.right_is_lava < 2)
drawStraightLine( drawStraightLine(
ctx, ctx,
gameState, gameState,
(redRightSide && "#FF0000") || "#FFFFFF", (redRightSide && "#FF0000") || "#FFFFFF",
width - gameState.offsetXRoundedDown + 1, width - gameState.offsetXRoundedDown + 1,
0, 0,
width - gameState.offsetXRoundedDown + 1, width - gameState.offsetXRoundedDown + 1,
height, height,
1, 1,
); );
} else { } 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) if (gameState.perks.right_is_lava < 2)
drawStraightLine( drawStraightLine(
ctx, ctx,
gameState, gameState,
(redLeftSide && "#FF0000") || "", (redRightSide && "#FF0000") || "",
0, width - 1,
0, 0,
0, width - 1,
height, height,
1, 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( 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"),
}, },
{ {
@ -718,14 +719,13 @@ export const rawUpgrades = [
id: "fountain_toss", id: "fountain_toss",
max: 7, max: 7,
name: t("upgrades.fountain_toss.name"), 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"), fullHelp: t("upgrades.fountain_toss.verbose_description"),
}, },
{ {
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,18 +848,18 @@ 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"),
}, },
{ {
requires: "", requires: "",
threshold: 235000, threshold: 235000,
gift: false, gift: false,
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;