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)
- [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

@ -44,18 +44,16 @@ export async function openCreativeModePerksPicker() {
);
while (true) {
const levelOptions = [
...allLevels.map((l, li) => {
const problem =
reasonLevelIsLocked(li, getHistory(), true)?.text || "";
const problem = reasonLevelIsLocked(li, getHistory(), true)?.text || "";
return {
icon: icons[l.name],
text: l.name,
value: l,
disabled: !!problem,
tooltip: problem || describeLevel(l),
className:''
className: "",
};
}),
...customLevels.map((l) => ({
@ -64,26 +62,28 @@ export async function openCreativeModePerksPicker() {
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'
className: "",
})),
];
const selectedLeveOption =
levelOptions.find(
(l) => l.text === getSettingValue("creativeModeLevel", ""),
) || levelOptions[0];
selectedLeveOption.className = "highlight";
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

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

View file

@ -586,7 +586,6 @@ h2.histogram-title strong {
}
}
.not-highlighed {
opacity: 0.8; color: #8a8a8a;
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;

View file

@ -12,7 +12,6 @@ import {
} from "./types";
import {
ballTransparency,
brickCenterX,
brickCenterY,
currentLevelInfo,
@ -50,7 +49,13 @@ import {
} from "./game";
import { stopRecording } from "./recording";
import { isOptionOn } from "./options";
import {clamp, coinsBoostedCombo, comboKeepingRate, shouldCoinsStick} from "./pure_functions";
import {
ballTransparency,
clamp,
coinsBoostedCombo,
comboKeepingRate,
shouldCoinsStick,
} from "./pure_functions";
import { addToTotalScore } from "./addToTotalScore";
import { hashCode } from "./getLevelBackground";
@ -216,11 +221,7 @@ export function baseCombo(gameState: GameState) {
gameState.perks.minefield &&
gameState.bricks.filter((b) => b === "black").length *
gameState.perks.minefield;
return (
1 +
gameState.perks.base_combo * 3 +
mineFieldBonus
);
return 1 + gameState.perks.base_combo * 3 + mineFieldBonus;
}
export function resetCombo(
@ -432,7 +433,7 @@ export function explodeBrick(
setBrick(gameState, index, "");
let coinsToSpawn = coinsBoostedCombo(gameState)
let coinsToSpawn = coinsBoostedCombo(gameState);
gameState.levelSpawnedCoins += coinsToSpawn;
gameState.runStatistics.coins_spawned += coinsToSpawn;
@ -630,9 +631,7 @@ export function addToScore(gameState: GameState, coin: Coin) {
gameState.highScore = gameState.score;
try {
localStorage.setItem("breakout-3-hs-short", gameState.score.toString());
} catch (e) {
}
} catch (e) {}
}
if (!isOptionOn("basic")) {
makeParticle(
@ -885,11 +884,11 @@ export function coinBrickHitCheck(gameState: GameState, coin: Coin) {
if (typeof (vhit ?? hhit ?? chit) !== "undefined") {
if (shouldCoinsStick(gameState)) {
if (coin.collidedLastFrame) {
coin.x = previousX
coin.y = previousY
coin.x = previousX;
coin.y = previousY;
}
coin.vx = 0
coin.vy = 0
coin.vx = 0;
coin.vy = 0;
} else if (gameState.perks.ghost_coins) {
// slow down
coin.vy *= 1 - 0.2 / gameState.perks.ghost_coins;
@ -945,7 +944,10 @@ export function bordersHitCheck(
let vhit = 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 =
gameState.offsetXRoundedDown +
radius +
@ -958,7 +960,10 @@ export function bordersHitCheck(
coin.vy *= -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 =
gameState.canvasWidth -
gameState.offsetXRoundedDown -
@ -969,7 +974,6 @@ export function bordersHitCheck(
hhit = 1;
}
return hhit + vhit * 2;
}
@ -1014,10 +1018,8 @@ export function gameStateTick(
if (gameState.perks.hot_start) {
if (gameState.combo === baseCombo(gameState)) {
// Give 1s of time between catching a coin and tick down
gameState.lastTickDown = gameState.levelTime
} else if (
gameState.levelTime > gameState.lastTickDown + 1000
) {
gameState.lastTickDown = gameState.levelTime;
} else if (gameState.levelTime > gameState.lastTickDown + 1000) {
gameState.lastTickDown = gameState.levelTime;
decreaseCombo(
gameState,
@ -1028,7 +1030,6 @@ export function gameStateTick(
}
}
if (
remainingBricks <= gameState.perks.skip_last &&
!gameState.autoCleanUses
@ -1161,10 +1162,12 @@ export function gameStateTick(
coin.vy *= ratio;
coin.vx *= ratio;
}
if (coin.y > gameState.gameZoneHeight && coin.floatingTime < gameState.perks.buoy * 30) {
coin.floatingTime += frames
coin.vy -= 1.5
if (
coin.y > gameState.gameZoneHeight &&
coin.floatingTime < gameState.perks.buoy * 30
) {
coin.floatingTime += frames;
coin.vy -= 1.5;
}
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);
if (
coin.previousY < gameState.gameZoneHeight &&
coin.y > gameState.gameZoneHeight &&
@ -1267,11 +1269,14 @@ export function gameStateTick(
if (
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.y, 20, gameState.gameZoneHeight - 20)
clamp(coin.y, 20, gameState.gameZoneHeight - 20),
);
}
}
@ -1319,10 +1324,7 @@ export function gameStateTick(
}
}
// remember collision
coin.collidedLastFrame = !!(
typeof hitBrick !== "undefined" ||
hitBorder
)
coin.collidedLastFrame = !!(typeof hitBrick !== "undefined" || hitBorder);
});
gameState.balls.forEach((ball) => ballTick(gameState, ball, frames));
@ -1330,7 +1332,6 @@ export function gameStateTick(
if (gameState.perks.shocks) {
gameState.balls.forEach((a, ai) =>
gameState.balls.forEach((b, bi) => {
if (
ai < bi &&
!a.destroyed &&
@ -1349,7 +1350,7 @@ export function gameStateTick(
let y = (a.y + b.y) / 2;
// space out the balls with extra speed
if (gameState.perks.shocks > 1) {
const limit = gameState.baseSpeed * gameState.perks.shocks / 2;
const limit = (gameState.baseSpeed * gameState.perks.shocks) / 2;
a.vx +=
clamp(a.x - x, -limit, limit) +
((Math.random() - 0.5) * limit) / 3;
@ -1641,8 +1642,7 @@ export function ballTick(gameState: GameState, ball: Ball, frames: number) {
frames,
);
if (borderHitCode) {
ball.sidesHitsSinceBounce++
ball.sidesHitsSinceBounce++;
if (ball.sidesHitsSinceBounce <= gameState.perks.three_cushion * 3) {
increaseCombo(gameState, 1, ball.x, ball.y);
}
@ -1755,13 +1755,10 @@ export function ballTick(gameState: GameState, ball: Ball, frames: number) {
if (
gameState.running &&
(
ball.y > gameState.gameZoneHeight + gameState.ballSize / 2 ||
(ball.y > gameState.gameZoneHeight + gameState.ballSize / 2 ||
ball.y < -gameState.gameZoneHeight ||
ball.x < -gameState.gameZoneHeight ||
ball.x > gameState.canvasWidth + gameState.gameZoneHeight
)
ball.x > gameState.canvasWidth + gameState.gameZoneHeight)
) {
ball.destroyed = true;
gameState.runStatistics.balls_lost++;
@ -1841,7 +1838,7 @@ export function ballTick(gameState: GameState, ball: Ball, frames: number) {
if (!gameState.brickHP[hitBrick]) {
ball.brokenSinceBounce++;
applyOttawaTreatyPerk(gameState, hitBrick, ball)
applyOttawaTreatyPerk(gameState, hitBrick, ball);
explodeBrick(gameState, hitBrick, ball, false);
if (
ball.sapperUses < gameState.perks.sapper &&
@ -1851,8 +1848,6 @@ export function ballTick(gameState: GameState, ball: Ball, frames: number) {
setBrick(gameState, hitBrick, "black");
ball.sapperUses++;
}
} else {
schedulGameSound(gameState, "wallBeep", x, 1);
makeLight(
@ -1971,7 +1966,7 @@ function makeCoin(
p.points = points;
p.weight = weight;
p.metamorphosisPoints = gameState.perks.metamorphosis;
p.floatingTime = 0
p.floatingTime = 0;
});
}
@ -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) {
if (!gameState.perks.ottawa_treaty) return
if (ball.sapperUses) return
const originalColor = gameState.bricks[index]
if (originalColor == 'black') return
const x = index % gameState.gridSize
const y = Math.floor(index / gameState.gridSize)
let converted = 0
const originalColor = gameState.bricks[index];
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 dy = -1; dy <= 1; dy++)
if (dx || dy) {
const nIndex = getRowColIndex(gameState, y + dy, x + dx)
if (gameState.bricks[nIndex] && gameState.bricks[nIndex] === 'black') {
setBrick(gameState, nIndex, originalColor)
schedulGameSound(gameState, "colorChange", brickCenterX(gameState, index), 1)
const nIndex = getRowColIndex(gameState, y + dy, x + dx);
if (gameState.bricks[nIndex] && gameState.bricks[nIndex] === "black") {
setBrick(gameState, nIndex, originalColor);
schedulGameSound(
gameState,
"colorChange",
brickCenterX(gameState, index),
1,
);
// Avoid infinite bricks generation hack
ball.sapperUses = Infinity
converted++
ball.sapperUses = Infinity;
converted++;
// 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 { 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', ()=>{
migrate("clean_ls", () => {
for (let key in localStorage) {
try {
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
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)
gameState.balls.forEach((ball) => {
const bt = ballTransparency(ball, gameState);
if (bt < min) {
min=bt
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.score - gameState.levelStartScore) /
(gameState.levelSpawnedCoins || 1)
// (gameState.levelSpawnedCoins - gameState.levelLostCoins) /
: // (gameState.levelSpawnedCoins - gameState.levelLostCoins) /
// gameState.levelSpawnedCoins
: 1;
1;
startWork("render:scoreDisplay");
scoreDisplay.innerHTML =
(isOptionOn("show_fps") || gameState.startParams.computer_controlled
@ -440,7 +440,7 @@ export function render(gameState: GameState) {
);
startWork("render:combotext");
const spawns=coinsBoostedCombo(gameState)
const spawns = coinsBoostedCombo(gameState);
if (spawns > 1) {
ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = 1;
@ -523,7 +523,6 @@ export function render(gameState: GameState) {
1,
);
} else {
if (gameState.perks.left_is_lava < 2)
drawStraightLine(
ctx,

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) {

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"),
},
{
@ -725,7 +726,6 @@ export const rawUpgrades = [
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"),
},
{
@ -849,7 +848,8 @@ 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"),
},
{
@ -862,5 +862,4 @@ export const rawUpgrades = [
help: (lvl: number) => t("upgrades.sticky_coins.tooltip"),
fullHelp: t("upgrades.sticky_coins.verbose_description"),
},
] as const;