Draft of looping mode, shine bricks when hit but not broken

This commit is contained in:
Renan LE CARO 2025-03-28 10:21:14 +01:00
parent 59ef24c865
commit 46f87556e1
20 changed files with 2639 additions and 3031 deletions

View file

@ -13,8 +13,9 @@ Break colourful bricks, catch bouncing coins and select powerful upgrades !
- [GitLab](https://gitlab.com/lecarore/breakout71) - [GitLab](https://gitlab.com/lecarore/breakout71)
- [HackerNews thread](https://news.ycombinator.com/item?id=43183131) - [HackerNews thread](https://news.ycombinator.com/item?id=43183131)
# Todo # Todo
- bring back detailed help of perks as "intel"
- people assume unbounded allows for wrap around - people assume unbounded allows for wrap around
- coin magnet and viscosity : only one level ~2.5 - coin magnet and viscosity : only one level ~2.5
- Boost Ascetism : give +2 or even +3 combo per brick destroyed - Boost Ascetism : give +2 or even +3 combo per brick destroyed
@ -22,17 +23,24 @@ Break colourful bricks, catch bouncing coins and select powerful upgrades !
- show -N points in red when combo resets - show -N points in red when combo resets
- reach : this is too punishing now, maybe only reset if you hit the lowest populate row of the level, if it's not a full width row - reach : this is too punishing now, maybe only reset if you hit the lowest populate row of the level, if it's not a full width row
- respawn: N% of bricks respawn after N seconds - respawn: N% of bricks respawn after N seconds
- [jaceys] Counters for coins lost, misses, and boundary bounces, as well as a timer.
- [jaceys] Move the restart button out of the menu, so that it is more easily accessible
- [jaceys] A visual indication of whether a ball has hit a brick this serve
- [obigre] Offer to level ups perks separately
- bring back detailed help of perks as "intel"
- https://weblate.org/fr/
# Premium: infinite mode # Premium: allow looping
Allow players to loop the game, adding one hasard per loop, making it harder and harder to exploit each strategy. Allow players to loop the game :
The high score are separated from the main mode. The scores are added for unlock. You no longer get upgrades after the first 7 levels. - [x] keep your score
The score you make in each level is instead multiplied by the number of "upgrades" and "choices" you would have had. - [x] keep 1 perk
- [x] add one hasard
The score is your "fuel", and lets you pick the next level from a list. Each level has a cost, preview, and one or two downgrades. - [ ] add one HP to all bricks
Each downgrade acts as a score multiplier. - [ ] advertise looping in normal game over screen
Your goal is no longer to score higher, but to go farther - [ ] save score at the end of first loop, in addition to the final one ?
- [ ] check that stats like max level are correct
# System requirements # System requirements
@ -164,7 +172,18 @@ There's also an easy mode for kids (slower ball).
# extra levels # extra levels
- Good games : FTL, Nova drift, Noita, Enter the gungeon, Zero Sivert, Factorio, Swarm - Good games :
- FTL
- Nova drift
- Noita
- Enter the gungeon
- Zero Sivert
- Factorio
- Swarm
- Nuclear throne
- Brigador
- letters and an associated word or name - letters and an associated word or name
- famous characters and movies - famous characters and movies
- fruits - fruits
@ -224,3 +243,13 @@ https://prohama.com/dog-21-pattern/
I wanted an APK to start in fullscreen and be able to list it on fdroid and the play store. I started with an empty view and went to work trimming it down, with the help of that tutorial I wanted an APK to start in fullscreen and be able to list it on fdroid and the play store. I started with an empty view and went to work trimming it down, with the help of that tutorial
https://github.com/fractalwrench/ApkGolf/blob/master/blog/BLOG_POST.md https://github.com/fractalwrench/ApkGolf/blob/master/blog/BLOG_POST.md
# Other noteworthy games in the breakout genre
LBreakoutHD : https://sourceforge.net/p/lgames/code/HEAD/tree/trunk/lbreakouthd/
Wizorb https://store.steampowered.com/app/207420/Wizorb/
Rollers of the realm : narratif, chaque balle est un aventurier
https://store.steampowered.com/app/262470/Rollers_of_the_Realm/

1015
dist/index.html vendored

File diff suppressed because one or more lines are too long

View file

@ -1,159 +0,0 @@
import { GameState, Level, PerkId } from "./types";
import { pickedUpgradesHTMl, sample, sumOfValues } from "./game_utils";
import { allLevels, icons, upgrades } from "./loadGameData";
import { t } from "./i18n/i18n";
import { requiredAsyncAlert } from "./asyncAlert";
import { debuffs } from "./debuffs";
type AdventureModeButton = {
text: string;
icon: string;
help?: string;
value: AdventureModeSelection;
};
type AdventureModeSelection = {
cost: number;
level?: Level;
perk?: PerkId;
discard?: PerkId;
};
const MAX_LVL = 3;
export async function openAdventureRunUpgradesPicker(gameState: GameState) {
// Just add random debuff for now
const debuffToApply = sample(
debuffs.filter((d) => gameState.debuffs[d.id] < d.max),
);
if (debuffToApply) {
gameState.debuffs[debuffToApply.id]++;
}
let levelChoiceCount = 1;
const catchRate =
(gameState.score - gameState.levelStartScore) /
(gameState.levelSpawnedCoins || 1);
if (gameState.levelWallBounces == 0) {
levelChoiceCount++;
}
if (gameState.levelTime < 30 * 1000) {
levelChoiceCount++;
}
if (catchRate === 1) {
levelChoiceCount++;
}
if (gameState.levelMisses === 0) {
levelChoiceCount++;
}
let perkChoices = 2 + gameState.perks.one_more_choice + levelChoiceCount;
const priceMultiplier =
1 +
Math.ceil(
gameState.currentLevel * Math.pow(1.05, gameState.currentLevel) * 10,
);
const levelChoices: AdventureModeButton[] = [...allLevels]
.sort(() => Math.random() - 0.5)
.slice(0, MAX_LVL)
.sort((a, b) => a.bricksCount - b.bricksCount)
.slice(0, Math.min(MAX_LVL, levelChoiceCount))
.map((level, levelIndex) => ({
text: t("premium.pick_level", {
name: level.name,
cost: priceMultiplier * levelIndex,
}),
icon: icons[level.name],
help:
level.size +
"x" +
level.size +
" with " +
level.bricksCount +
" bricks",
value: {
level,
cost: priceMultiplier * levelIndex,
},
}));
const perksChoices = upgrades
.filter((u) => u.adventure)
.filter((u) => !u?.requires || gameState.perks[u?.requires])
.filter((u) => gameState.perks[u.id] < u.max)
.sort(() => Math.random() - 0.5)
.slice(0, perkChoices);
const discardChoices: AdventureModeButton[] =
sumOfValues(gameState.perks) > 5
? upgrades
.filter((u) => u.adventure)
.filter((u) => gameState.perks[u.id])
.sort(() => Math.random() - 0.5)
.slice(0, 3)
.map((u, ui) => {
return {
icon: `<span class="red-icon">${u.icon}</span>`,
text: t("premium.discard", { name: u.name }),
help: t("premium.discard_help"),
value: { discard: u.id, cost: 0 },
};
})
: [];
let used = new Set();
let choice: AdventureModeSelection | null = null;
while (
(choice = await requiredAsyncAlert({
title: t("premium.next_step_title"),
content: [
`
<p>${t("premium.choose_next_step", { score: gameState.score })}</p>
${pickedUpgradesHTMl(gameState)}
`,
...perksChoices.map((u, ui) => {
const lvl = gameState.perks[u.id];
const cost =
(priceMultiplier + sumOfValues(gameState.perks) + lvl) * (ui + 1);
return {
icon: u.icon,
text:
lvl == 0
? t("premium.pick_perk", { name: u.name, cost })
: t("premium.upgrade_perk_to_level", {
name: u.name,
cost,
lvl: lvl + 1,
}),
help: u.help(lvl + 1),
value: { perk: u.id, cost },
disabled: gameState.score < cost || used.has(u.id),
};
}),
discardChoices.length ? "You can discard some perks" : "",
...discardChoices,
`Click a level below to continue`,
...levelChoices.map((p) => ({
...p,
disabled: gameState.score < p.value.cost,
})),
],
}))
) {
gameState.score -= choice.cost;
if (choice.perk) {
used.add(choice.perk);
gameState.perks[choice.perk]++;
}
if (choice.discard) {
used.add(choice.discard);
gameState.perks[choice.discard] = 0;
}
if (choice.level) {
gameState.runLevels[gameState.currentLevel + 1] = choice.level;
return;
}
}
}

View file

@ -860,7 +860,7 @@
{ {
"name": "icon:unbounded", "name": "icon:unbounded",
"size": 9, "size": 9,
"bricks": "y_WWWWW_y__________yttttt____ttttt__y____W__y______________y_y____________WWW___y", "bricks": "rrWWWWWrrrr_____rrrrtttttrrrrttt__rrrr____yrrrr_____rrrr___y_yWrr_____rrrrWWW__ry",
"svg": null, "svg": null,
"color": "" "color": ""
}, },
@ -1032,13 +1032,6 @@
"svg": null, "svg": null,
"color": "" "color": ""
}, },
{
"name": "icon:adventure_mode",
"size": 11,
"bricks": "__________________________________ttt___bbb_bttttbbbbbbbb__tbt__bbbbbbbbttttb_bbb___ttt__________________________________",
"svg": null,
"color": ""
},
{ {
"name": "icon:7_levels_run", "name": "icon:7_levels_run",
"size": 7, "size": 7,

View file

@ -1,56 +1,47 @@
import { t } from "./i18n/i18n"; import { t } from "./i18n/i18n";
import {Debuff} from "./types";
export const debuffs = [ export const debuffs = [
{ {
id: "negative_coins", id: "negative_coins",
max: 20, max: 20,
name: t("debuffs.negative_coins.name"), name: (lvl: number) => t("debuffs.negative_coins.help",{lvl}),
help: (lvl: number) => t("debuffs.negative_coins.help", { lvl }), help: (lvl: number) => t("debuffs.negative_coins.help", { lvl }),
}, },
{ {
id: "negative_bricks", id: "more_bombs",
max: 20, max: 20,
name: t("debuffs.negative_bricks.name"), name: (lvl: number) => t("debuffs.more_bombs.help", { lvl }),
help: (lvl: number) => t("debuffs.negative_bricks.help", { lvl }), help: (lvl: number) => t("debuffs.more_bombs.help", { lvl }),
},
{
id: "banned",
max: 50,
name: (lvl: number,banned:string) => t("debuffs.banned.description",{lvl,banned}),
help: (lvl: number,perk:string) => t("debuffs.banned.help", { lvl,perk }),
},
{
id: "interference",
max: 20,
name: (lvl: number) => t("debuffs.interference.help", { lvl }),
help: (lvl: number) => t("debuffs.interference.help", { lvl }),
}, },
{ ] as const as Debuff[];
id: "void_coins_on_touch",
max: 1,
name: t("debuffs.void_coins_on_touch.name"),
help: (lvl: number) => t("debuffs.void_coins_on_touch.help", { lvl }),
},
{
id: "void_brick_on_touch",
max: 1,
name: t("debuffs.void_brick_on_touch.name"),
help: (lvl: number) => t("debuffs.void_brick_on_touch.help", { lvl }),
},
{
id: "downward_wind",
max: 20,
name: t("debuffs.downward_wind.name"),
help: (lvl: number) => t("debuffs.downward_wind.help", { lvl }),
},
{
id: "side_wind",
max: 20,
name: t("debuffs.side_wind.name"),
help: (lvl: number) => t("debuffs.side_wind.help", { lvl }),
},
] as const;
/* /*
Possible challenges : Possible challenges :
- add a force field for 10s that negates hots start - interference : telekinesis works backward for lvl/2 seconds every 5 seconds (show timer ?)
- other perks can be randomly turned off - exclusion : one of your current perks (except the kept one) is banned
- ball keeps accelerating until unplayable - fireworks : some bricks are explosive, you're not told which ones
-
- graphical effects like trail, contrast, blur to make it harder to see what's going on - graphical effects like trail, contrast, blur to make it harder to see what's going on
- ball creates a draft behind itself that blows coins in odd patterns - ball creates a draft behind itself that blows coins in odd patterns
- bricks are invisible - bricks are invisible
- downward wind
- side wind
- add red anti-coins that apply downgrades - add red anti-coins that apply downgrades
- destroy your combo - destroy your combo
- hurt your score - hurt your score

View file

@ -13,7 +13,8 @@ import {
} from "./types"; } from "./types";
import { getAudioContext, playPendingSounds } from "./sounds"; import { getAudioContext, playPendingSounds } from "./sounds";
import { import {
currentLevelInfo, bannedUpgradesHTMl,
currentLevelInfo, debuffsHTMl,
getRowColIndex, getRowColIndex,
levelsListHTMl, levelsListHTMl,
max_levels, max_levels,
@ -446,25 +447,25 @@ document.addEventListener("visibilitychange", () => {
async function openScorePanel() { async function openScorePanel() {
pause(true); pause(true);
const cb = await asyncAlert({ const cb = await asyncAlert({
title: gameState.isAdventureMode title:
? t("score_panel.title_adventure", { gameState.loop ?
t("score_panel.title_looped", {
loop:gameState.loop,
score: gameState.score, score: gameState.score,
level: gameState.currentLevel + 1, level: gameState.currentLevel + 1,
max: max_levels(gameState), max: max_levels(gameState),
}) }):
: t("score_panel.title", { t("score_panel.title", {
score: gameState.score, score: gameState.score,
level: gameState.currentLevel + 1, level: gameState.currentLevel + 1,
max: max_levels(gameState), max: max_levels(gameState),
}), }),
content: [ content: [
` gameState.isCreativeModeRun ? `<p>${t("score_panel.test_run")}</p>` : "",
${gameState.isCreativeModeRun ? `<p>${t("score_panel.test_run")}</p>` : ""} pickedUpgradesHTMl(gameState),
${pickedUpgradesHTMl(gameState)} levelsListHTMl(gameState),
debuffsHTMl(gameState),
<p>${levelsListHTMl(gameState)}</p>
`,
], ],
allowClose: true, allowClose: true,
}); });
@ -1007,32 +1008,24 @@ restart(
(window.location.search.includes("stressTest") && { (window.location.search.includes("stressTest") && {
level: "Bird", level: "Bird",
perks: { perks: {
sapper: 10, // sapper: 1,
bigger_explosions: 1, // bigger_explosions: 20,
unbounded: 1, // // unbounded: 1,
pierce_color: 1, // // pierce_color: 1,
pierce: 20, // pierce: 1,
multiball: 6,
base_combo: 7, // multiball: 6,
// base_combo: 7,
telekinesis: 2, telekinesis: 2,
yoyo: 2, yoyo: 2,
metamorphosis: 1, // metamorphosis: 1,
implosions: 1, // implosions: 1,
// sturdy_bricks:5
}, },
debuffs:{
}
}) || }) ||
(window.location.search.includes("adventure") && {
adventure: true,
perks: {
// pierce:15
},
debuffs: {
// side_wind:20
// negative_bricks:3,
// negative_coins:5,
// void_coins_on_touch: 1,
// void_brick_on_touch: 1,
},
}) ||
{}, {},
); );

View file

@ -136,8 +136,7 @@ export function gameOver(title: string, intro: string) {
], ],
}).then(() => }).then(() =>
restart({ restart({
levelToAvoid: currentLevelInfo(gameState).name, levelToAvoid: currentLevelInfo(gameState).name
adventure: gameState.isAdventureMode,
}), }),
); );
} }

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,7 @@
import { Ball, GameState, PerkId, PerksMap } from "./types"; import { Ball, GameState, PerkId, PerksMap } from "./types";
import { icons, upgrades } from "./loadGameData"; import { icons, upgrades } from "./loadGameData";
import { t } from "./i18n/i18n"; import { t } from "./i18n/i18n";
import {debuffs} from "./debuffs";
export function getMajorityValue(arr: string[]): string { export function getMajorityValue(arr: string[]): string {
const count: { [k: string]: number } = {}; const count: { [k: string]: number } = {};
@ -54,6 +55,8 @@ export function getPossibleUpgrades(gameState: GameState) {
} }
export function max_levels(gameState: GameState) { export function max_levels(gameState: GameState) {
// TODO
return 2
return 7 + gameState.perks.extra_levels; return 7 + gameState.perks.extra_levels;
} }
@ -61,14 +64,25 @@ export function pickedUpgradesHTMl(gameState: GameState) {
let list = ""; let list = "";
for (let u of upgrades) { for (let u of upgrades) {
for (let i = 0; i < gameState.perks[u.id]; i++) for (let i = 0; i < gameState.perks[u.id]; i++)
list += `<span title="${u.name}">${icons["icon:" + u.id]}</span>`; list += `<span title="${u.name} : ${u.help(gameState.perks[u.id])}">${icons["icon:" + u.id]}</span>`;
} }
if (!list) return ""; if (!list) return "";
return ` <p>${t("score_panel.upgrades_picked")}</p> <p>${list}</p>`; return ` <p>${t("score_panel.upgrades_picked")}</p> <p>${list}</p>`;
} }
export function debuffsHTMl(gameState: GameState):string {
const banned = upgrades.filter(u=>gameState.bannedPerks[u.id]).map(u=>u.name).join(', ')
let list = debuffs.filter(d=>gameState.debuffs[d.id]).map(d=>d.name(gameState.debuffs[d.id], banned)).join(', ');
if (!list) return "";
return `<p>${t("score_panel.bebuffs_list")} : ${list}</p>`;
}
export function levelsListHTMl(gameState: GameState) { export function levelsListHTMl(gameState: GameState) {
if (gameState.isAdventureMode) return "";
if (!gameState.perks.clairvoyant) return ""; if (!gameState.perks.clairvoyant) return "";
let list = ""; let list = "";
for (let i = 0; i < max_levels(gameState); i++) { for (let i = 0; i < max_levels(gameState); i++) {

View file

@ -88,10 +88,10 @@
<name>debuffs</name> <name>debuffs</name>
<children> <children>
<folder_node> <folder_node>
<name>downward_wind</name> <name>banned</name>
<children> <children>
<concept_node> <concept_node>
<name>help</name> <name>description</name>
<description/> <description/>
<comment/> <comment/>
<translations> <translations>
@ -106,7 +106,7 @@
</translations> </translations>
</concept_node> </concept_node>
<concept_node> <concept_node>
<name>name</name> <name>help</name>
<description/> <description/>
<comment/> <comment/>
<translations> <translations>
@ -114,6 +114,26 @@
<language>en-US</language> <language>en-US</language>
<approved>true</approved> <approved>true</approved>
</translation> </translation>
<translation>
<language>fr-FR</language>
<approved>true</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
<name>interference</name>
<children>
<concept_node>
<name>help</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation> <translation>
<language>fr-FR</language> <language>fr-FR</language>
<approved>false</approved> <approved>false</approved>
@ -123,7 +143,7 @@
</children> </children>
</folder_node> </folder_node>
<folder_node> <folder_node>
<name>negative_bricks</name> <name>more_bombs</name>
<children> <children>
<concept_node> <concept_node>
<name>help</name> <name>help</name>
@ -140,21 +160,6 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>name</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>true</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children> </children>
</folder_node> </folder_node>
<folder_node> <folder_node>
@ -175,126 +180,6 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>name</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>true</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
<name>side_wind</name>
<children>
<concept_node>
<name>help</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>true</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>name</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>true</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
<name>void_brick_on_touch</name>
<children>
<concept_node>
<name>help</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>true</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>name</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>true</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
<name>void_coins_on_touch</name>
<children>
<concept_node>
<name>help</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>true</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>name</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>true</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children> </children>
</folder_node> </folder_node>
</children> </children>
@ -849,6 +734,41 @@
</concept_node> </concept_node>
</children> </children>
</folder_node> </folder_node>
<folder_node>
<name>loop</name>
<children>
<concept_node>
<name>instructions</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>title</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node> <folder_node>
<name>main_menu</name> <name>main_menu</name>
<children> <children>
@ -882,6 +802,36 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>colorful_coins</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>colorful_coins_help</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>download_save_file</name> <name>download_save_file</name>
<description/> <description/>
@ -1497,6 +1447,36 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>show_stats</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>show_stats_help</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>sounds</name> <name>sounds</name>
<description/> <description/>
@ -1608,7 +1588,7 @@
</translations> </translations>
</concept_node> </concept_node>
<concept_node> <concept_node>
<name>current_lvl_adventure</name> <name>current_lvl_loop</name>
<description/> <description/>
<comment/> <comment/>
<translations> <translations>
@ -1672,36 +1652,6 @@
<folder_node> <folder_node>
<name>premium</name> <name>premium</name>
<children> <children>
<concept_node>
<name>adventure_mode</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>true</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>true</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>adventure_mode_help</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>true</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>true</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>back</name> <name>back</name>
<description/> <description/>
@ -1777,51 +1727,6 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>choose_next_step</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>true</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>discard</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>true</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>discard_help</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>true</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>enter</name> <name>enter</name>
<description/> <description/>
@ -1883,13 +1788,13 @@
</translations> </translations>
</concept_node> </concept_node>
<concept_node> <concept_node>
<name>next_step_title</name> <name>per_hours</name>
<description/> <description/>
<comment/> <comment/>
<translations> <translations>
<translation> <translation>
<language>en-US</language> <language>en-US</language>
<approved>true</approved> <approved>false</approved>
</translation> </translation>
<translation> <translation>
<language>fr-FR</language> <language>fr-FR</language>
@ -1898,13 +1803,13 @@
</translations> </translations>
</concept_node> </concept_node>
<concept_node> <concept_node>
<name>pick_level</name> <name>per_hours_help</name>
<description/> <description/>
<comment/> <comment/>
<translations> <translations>
<translation> <translation>
<language>en-US</language> <language>en-US</language>
<approved>true</approved> <approved>false</approved>
</translation> </translation>
<translation> <translation>
<language>fr-FR</language> <language>fr-FR</language>
@ -1913,13 +1818,13 @@
</translations> </translations>
</concept_node> </concept_node>
<concept_node> <concept_node>
<name>pick_perk</name> <name>thanks</name>
<description/> <description/>
<comment/> <comment/>
<translations> <translations>
<translation> <translation>
<language>en-US</language> <language>en-US</language>
<approved>true</approved> <approved>false</approved>
</translation> </translation>
<translation> <translation>
<language>fr-FR</language> <language>fr-FR</language>
@ -1928,28 +1833,13 @@
</translations> </translations>
</concept_node> </concept_node>
<concept_node> <concept_node>
<name>short_help</name> <name>thanks_help</name>
<description/> <description/>
<comment/> <comment/>
<translations> <translations>
<translation> <translation>
<language>en-US</language> <language>en-US</language>
<approved>true</approved> <approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>true</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>upgrade_perk_to_level</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>true</approved>
</translation> </translation>
<translation> <translation>
<language>fr-FR</language> <language>fr-FR</language>
@ -1958,13 +1848,13 @@
</translations> </translations>
</concept_node> </concept_node>
<concept_node> <concept_node>
<name>your_upgrades</name> <name>title</name>
<description/> <description/>
<comment/> <comment/>
<translations> <translations>
<translation> <translation>
<language>en-US</language> <language>en-US</language>
<approved>true</approved> <approved>false</approved>
</translation> </translation>
<translation> <translation>
<language>fr-FR</language> <language>fr-FR</language>
@ -2058,62 +1948,17 @@
<name>score_panel</name> <name>score_panel</name>
<children> <children>
<concept_node> <concept_node>
<name>restart</name> <name>bebuffs_list</name>
<description/> <description/>
<comment/> <comment/>
<translations> <translations>
<translation> <translation>
<language>en-US</language> <language>en-US</language>
<approved>true</approved> <approved>false</approved>
</translation> </translation>
<translation> <translation>
<language>fr-FR</language> <language>fr-FR</language>
<approved>true</approved> <approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>restart_help</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>true</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>true</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>resume</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>true</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>true</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>resume_help</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>true</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>true</approved>
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
@ -2148,7 +1993,7 @@
</translations> </translations>
</concept_node> </concept_node>
<concept_node> <concept_node>
<name>title_adventure</name> <name>title_looped</name>
<description/> <description/>
<comment/> <comment/>
<translations> <translations>

View file

@ -3,18 +3,11 @@
"confirmRestart.text": "You're about to start a new run, is that really what you wanted ?", "confirmRestart.text": "You're about to start a new run, is that really what you wanted ?",
"confirmRestart.title": "Start a new run ?", "confirmRestart.title": "Start a new run ?",
"confirmRestart.yes": "Restart game", "confirmRestart.yes": "Restart game",
"debuffs.downward_wind.help": "A strong wind sends the ball back down for the first {{lvl}}s of each level.", "debuffs.banned.description": "{{lvl}} banned perk(s) : {{banned}}",
"debuffs.downward_wind.name": "Downward wind", "debuffs.banned.help": "{{perk}} is banned for this run",
"debuffs.negative_bricks.help": "{{lvl}} bricks on each level are replaced by negative bricks that break the combo", "debuffs.interference.help": "Telekinesis and yo-yo stop working for {{lvl}}s every 7s",
"debuffs.negative_bricks.name": "Void bricks", "debuffs.more_bombs.help": "{{lvl}} bricks replaced by bombs",
"debuffs.negative_coins.help": "{{lvl}}% of coins spawn empty and break the combo if caught", "debuffs.negative_coins.help": "{{lvl}}% of coins spawn void and break the combo if caught",
"debuffs.negative_coins.name": "Void coins",
"debuffs.side_wind.help": "A strong wind sends the ball and coins to one of the sides",
"debuffs.side_wind.name": "Side wind",
"debuffs.void_brick_on_touch.help": "Bricks touched by void coins become void",
"debuffs.void_brick_on_touch.name": "Bricks become void",
"debuffs.void_coins_on_touch.help": "Coins that touch void bricks become void",
"debuffs.void_coins_on_touch.name": "Coins become void",
"gameOver.cumulative_total": "Your total cumulative score went from {{startTs}} to {{endTs}}.", "gameOver.cumulative_total": "Your total cumulative score went from {{startTs}} to {{endTs}}.",
"gameOver.lost.summary": "You dropped the ball after catching {{score}} coins.", "gameOver.lost.summary": "You dropped the ball after catching {{score}} coins.",
"gameOver.lost.title": "Game Over", "gameOver.lost.title": "Game Over",
@ -50,8 +43,12 @@
"level_up.unlocked_level": " (Level)", "level_up.unlocked_level": " (Level)",
"level_up.unlocked_perk": " (Perk)", "level_up.unlocked_perk": " (Perk)",
"level_up.upgrade_perk_to_level": " lvl {{level}}", "level_up.upgrade_perk_to_level": " lvl {{level}}",
"loop.instructions": "All your perks will be erased except one that you can pick below. Each option comes with an additional hazard will appear on all levels going forward. ",
"loop.title": "Starting loop {{loop}}",
"main_menu.basic": "Basic graphics", "main_menu.basic": "Basic graphics",
"main_menu.basic_help": "Better performance.", "main_menu.basic_help": "Better performance.",
"main_menu.colorful_coins": "Colorful coins",
"main_menu.colorful_coins_help": "Coins always spawn of the color of the brick",
"main_menu.download_save_file": "Download score and stats", "main_menu.download_save_file": "Download score and stats",
"main_menu.download_save_file_help": "Get a save file", "main_menu.download_save_file_help": "Get a save file",
"main_menu.footer_html": "<p> \n<span>Made in France by <a href=\"https://lecaro.me\">Renan LE CARO</a>.</span> \n<a href=\"https://paypal.me/renanlecaro\" target=\"_blank\">Donate</a>\n<a href=\"https://discord.gg/DZSPqyJkwP\" target=\"_blank\">Discord</a>\n<a href=\"https://f-droid.org/en/packages/me.lecaro.breakout/\" target=\"_blank\">F-Droid</a>\n<a href=\"https://play.google.com/store/apps/details?id=me.lecaro.breakout\" target=\"_blank\">Google Play</a>\n<a href=\"https://renanlecaro.itch.io/breakout71\" target=\"_blank\">itch.io</a> \n<a href=\"https://gitlab.com/lecarore/breakout71\" target=\"_blank\">Gitlab</a>\n<a href=\"https://breakout.lecaro.me/\" target=\"_blank\">Web version</a>\n<a href=\"https://news.ycombinator.com/item?id=43183131\" target=\"_blank\">HackerNews</a>\n<a href=\"https://breakout.lecaro.me/privacy.html\" target=\"_blank\">Privacy Policy</a>\n<span>v.{{appVersion}}</span>\n</p>\n", "main_menu.footer_html": "<p> \n<span>Made in France by <a href=\"https://lecaro.me\">Renan LE CARO</a>.</span> \n<a href=\"https://paypal.me/renanlecaro\" target=\"_blank\">Donate</a>\n<a href=\"https://discord.gg/DZSPqyJkwP\" target=\"_blank\">Discord</a>\n<a href=\"https://f-droid.org/en/packages/me.lecaro.breakout/\" target=\"_blank\">F-Droid</a>\n<a href=\"https://play.google.com/store/apps/details?id=me.lecaro.breakout\" target=\"_blank\">Google Play</a>\n<a href=\"https://renanlecaro.itch.io/breakout71\" target=\"_blank\">itch.io</a> \n<a href=\"https://gitlab.com/lecarore/breakout71\" target=\"_blank\">Gitlab</a>\n<a href=\"https://breakout.lecaro.me/\" target=\"_blank\">Web version</a>\n<a href=\"https://news.ycombinator.com/item?id=43183131\" target=\"_blank\">HackerNews</a>\n<a href=\"https://breakout.lecaro.me/privacy.html\" target=\"_blank\">Privacy Policy</a>\n<span>v.{{appVersion}}</span>\n</p>\n",
@ -93,49 +90,42 @@
"main_menu.settings_title": "Settings", "main_menu.settings_title": "Settings",
"main_menu.show_fps": "FPS counter", "main_menu.show_fps": "FPS counter",
"main_menu.show_fps_help": "Monitor the app's performance", "main_menu.show_fps_help": "Monitor the app's performance",
"main_menu.show_stats": "Show real time stats",
"main_menu.show_stats_help": "Coins, time, bounces, misses",
"main_menu.sounds": "Game sounds", "main_menu.sounds": "Game sounds",
"main_menu.sounds_help": "Can slow down some phones.", "main_menu.sounds_help": "Can slow down some phones.",
"main_menu.title": "Breakout 71", "main_menu.title": "Breakout 71",
"main_menu.unlocks": "Unlocked content", "main_menu.unlocks": "Unlocked content",
"main_menu.unlocks_help": "Try perks and levels you unlocked", "main_menu.unlocks_help": "Try perks and levels you unlocked",
"play.close_modale_window_tooltip": "close ", "play.close_modale_window_tooltip": "close ",
"play.current_lvl": "L{{level}}/{{max}}", "play.current_lvl": "Level {{level}}/{{max}}",
"play.current_lvl_adventure": "L {{level}}", "play.current_lvl_loop": "Level {{level}}/{{max}} loop {{loop}}",
"play.menu_label": "menu", "play.menu_label": "menu",
"play.missed_ball": "miss", "play.missed_ball": "miss",
"play.mobile_press_to_play": "Press and hold here to play", "play.mobile_press_to_play": "Press and hold here to play",
"premium.adventure_mode": "Infinite mode",
"premium.adventure_mode_help": "Start a new game in infinite mode",
"premium.back": "Back", "premium.back": "Back",
"premium.back_help": "Return to main menu", "premium.back_help": "Return to main menu",
"premium.buy": "Buy a license key", "premium.buy": "Buy a license key",
"premium.buy_disabled_help": "Coming soon", "premium.buy_disabled_help": "Coming soon",
"premium.buy_help": "You'll be taken to a stripe form to pay and will receive the license by email. Come back to enter it here after.", "premium.buy_help": "You'll be taken to a stripe form to pay and will receive the license by email. Come back to enter it here after.",
"premium.choose_next_step": "You have ${{score}}. Click any upgrades you want to buy.",
"premium.discard": "Discard perk {{name}}",
"premium.discard_help": "Will make other perks cheaper",
"premium.enter": "Enter license key", "premium.enter": "Enter license key",
"premium.enter_help": "Paste the license in the window that opens", "premium.enter_help": "Paste the license in the window that opens",
"premium.help": "Buy a license for Breakout 71 to unlock infinite mode and support development. It costs 4.99€ and lasts forever. You can use it on multiple devices, but please don't share it online. ", "premium.help": "Buy a license for Breakout 71 to unlock looping and support development. It costs 4.99€ and lasts forever. You can use it on multiple devices, but please don't share it online. ",
"premium.help_google": "While I do plan to offer premium licenses through google play, I haven't gotten around it yet, so there's no buy link here. If you already have a license key, you can enter it below. ", "premium.help_google": "While I do plan to offer premium licenses through google play, I haven't gotten around it yet, so there's no buy link here. If you already have a license key, you can enter it below. ",
"premium.next_step_title": "Buy upgrades and continue to next level", "premium.per_hours": "You've played for {{hours}} hours",
"premium.pick_level": "Go to level \"{{name}}\" for ${{cost}}", "premium.per_hours_help": "Donate 4.99€ to get premium",
"premium.pick_perk": "Get {{name}} for ${{cost}}", "premium.thanks": "You are premium, thanks ! ",
"premium.short_help": "Play as long as possible", "premium.thanks_help": "Copy your license key",
"premium.upgrade_perk_to_level": "Upgrade {{name}} to {{lvl}} for ${{cost}}", "premium.title": "Unlock looping with premium ",
"premium.your_upgrades": "Your upgrades so far : ",
"sandbox.help": "Test any perk combination", "sandbox.help": "Test any perk combination",
"sandbox.instructions": "Select perks below and press \"start run\" to try them out in a test run. Scores and stats are not recorded.", "sandbox.instructions": "Select perks below and press \"start run\" to try them out in a test run. Scores and stats are not recorded.",
"sandbox.start": "Start test run", "sandbox.start": "Start test run",
"sandbox.title": "Sandbox mode", "sandbox.title": "Sandbox mode",
"sandbox.unlocks_at": "Unlocks at total score {{score}}", "sandbox.unlocks_at": "Unlocks at total score {{score}}",
"score_panel.restart": "Restart", "score_panel.bebuffs_list": "Debuffs :",
"score_panel.restart_help": "Start a brand new run",
"score_panel.resume": "Resume",
"score_panel.resume_help": "Return to your run",
"score_panel.test_run": "This is a test run, score is not recorded permanently", "score_panel.test_run": "This is a test run, score is not recorded permanently",
"score_panel.title": "{{score}} points at level {{level}}/{{max}} ", "score_panel.title": "{{score}} points at level {{level}}/{{max}} ",
"score_panel.title_adventure": "{{score}} points at level {{level}} of your adventure", "score_panel.title_looped": "{{score}} points at level {{level}}/{{max}} of loop {{loop}}",
"score_panel.upcoming_levels": "Upcoming levels :", "score_panel.upcoming_levels": "Upcoming levels :",
"score_panel.upgrades_picked": "Upgrades picked so far : ", "score_panel.upgrades_picked": "Upgrades picked so far : ",
"unlocks.greyed_out_help": "The greyed out ones can be unlocked by increasing your total score. The total score increases every time you score in game.", "unlocks.greyed_out_help": "The greyed out ones can be unlocked by increasing your total score. The total score increases every time you score in game.",

View file

@ -3,18 +3,11 @@
"confirmRestart.text": "Vous êtes sur le point de commencer une nouvelle partie, est-ce vraiment ce que vous vouliez ?", "confirmRestart.text": "Vous êtes sur le point de commencer une nouvelle partie, est-ce vraiment ce que vous vouliez ?",
"confirmRestart.title": "Démarrer une nouvelle partie ?", "confirmRestart.title": "Démarrer une nouvelle partie ?",
"confirmRestart.yes": "Commencer une nouvelle partie", "confirmRestart.yes": "Commencer une nouvelle partie",
"debuffs.downward_wind.help": "Un vent fort renvoie la balle vers le bas pendant les {{lvl}}premières secondes de chaque niveau.", "debuffs.banned.description": "{{lvl}} amélioration(s) bannie(s) : {{banned}}",
"debuffs.downward_wind.name": "Vent descendant", "debuffs.banned.help": "",
"debuffs.negative_bricks.help": "{{lvl}} briques à chaque niveau sont remplacées par des briques négatives qui brisent le combo", "debuffs.interference.help": "",
"debuffs.negative_bricks.name": "Briques vides", "debuffs.more_bombs.help": "",
"debuffs.negative_coins.help": " ", "debuffs.negative_coins.help": "",
"debuffs.negative_coins.name": "Pièces du vide",
"debuffs.side_wind.help": "Un vent fort envoie la balle et les pièces vers l'un des côtés",
"debuffs.side_wind.name": "Vent latéral",
"debuffs.void_brick_on_touch.help": "Les briques touchées par des pièces vides deviennent vides",
"debuffs.void_brick_on_touch.name": "Les briques deviennent vides",
"debuffs.void_coins_on_touch.help": "Les pièces qui touchent des briques vides deviennent nulles",
"debuffs.void_coins_on_touch.name": "Les pièces deviennent nulles",
"gameOver.cumulative_total": "Votre score total cumulé est passé de {{startTs}} à {{endTs}}.", "gameOver.cumulative_total": "Votre score total cumulé est passé de {{startTs}} à {{endTs}}.",
"gameOver.lost.summary": "Vous avez fait tomber la balle après avoir attrapé {{score}} pièces.", "gameOver.lost.summary": "Vous avez fait tomber la balle après avoir attrapé {{score}} pièces.",
"gameOver.lost.title": "Balle perdue", "gameOver.lost.title": "Balle perdue",
@ -50,8 +43,12 @@
"level_up.unlocked_level": " (Niveau)", "level_up.unlocked_level": " (Niveau)",
"level_up.unlocked_perk": " (Amélioration)", "level_up.unlocked_perk": " (Amélioration)",
"level_up.upgrade_perk_to_level": " niveau {{level}}", "level_up.upgrade_perk_to_level": " niveau {{level}}",
"loop.instructions": "",
"loop.title": "",
"main_menu.basic": "Graphismes simplifiés", "main_menu.basic": "Graphismes simplifiés",
"main_menu.basic_help": "Meilleures performances.", "main_menu.basic_help": "Meilleures performances.",
"main_menu.colorful_coins": "",
"main_menu.colorful_coins_help": "",
"main_menu.download_save_file": "Sauvegarder mes progrès", "main_menu.download_save_file": "Sauvegarder mes progrès",
"main_menu.download_save_file_help": "Obtenir un fichier de sauvegarde", "main_menu.download_save_file_help": "Obtenir un fichier de sauvegarde",
"main_menu.footer_html": " <p> \n<span>Programmé en France par <a href=\"https://lecaro.me\">Renan LE CARO</a>.</span>\n<a href=\"https://paypal.me/renanlecaro\" target=\"_blank\">Donner</a>\n<a href=\"https://discord.gg/DZSPqyJkwP\" target=\"_blank\">Discord</a>\n<a href=\"https://f-droid.org/en/packages/me.lecaro.breakout/\" target=\"_blank\">F-Droid</a>\n<a href=\"https://play.google.com/store/apps/details?id=me.lecaro.breakout\" target=\"_blank\">Google Play</a>\n<a href=\"https://renanlecaro.itch.io/breakout71\" target=\"_blank\">itch.io</a>\n<a href=\"https://gitlab.com/lecarore/breakout71\" target=\"_blank\">Gitlab</a>\n<a href=\"https://breakout.lecaro.me/\" target=\"_blank\">Version web</a>\n<a href=\"https://news.ycombinator.com/item?id=43183131\" target=\"_blank\">HackerNews</a>\n<a href=\"https://breakout.lecaro.me/privacy.html\" target=\"_blank\">Politique de confidentialité</a> \n<span>v.{{appVersion}}</span>\n</p>", "main_menu.footer_html": " <p> \n<span>Programmé en France par <a href=\"https://lecaro.me\">Renan LE CARO</a>.</span>\n<a href=\"https://paypal.me/renanlecaro\" target=\"_blank\">Donner</a>\n<a href=\"https://discord.gg/DZSPqyJkwP\" target=\"_blank\">Discord</a>\n<a href=\"https://f-droid.org/en/packages/me.lecaro.breakout/\" target=\"_blank\">F-Droid</a>\n<a href=\"https://play.google.com/store/apps/details?id=me.lecaro.breakout\" target=\"_blank\">Google Play</a>\n<a href=\"https://renanlecaro.itch.io/breakout71\" target=\"_blank\">itch.io</a>\n<a href=\"https://gitlab.com/lecarore/breakout71\" target=\"_blank\">Gitlab</a>\n<a href=\"https://breakout.lecaro.me/\" target=\"_blank\">Version web</a>\n<a href=\"https://news.ycombinator.com/item?id=43183131\" target=\"_blank\">HackerNews</a>\n<a href=\"https://breakout.lecaro.me/privacy.html\" target=\"_blank\">Politique de confidentialité</a> \n<span>v.{{appVersion}}</span>\n</p>",
@ -93,6 +90,8 @@
"main_menu.settings_title": "Paramètre", "main_menu.settings_title": "Paramètre",
"main_menu.show_fps": "Compteur de FPS", "main_menu.show_fps": "Compteur de FPS",
"main_menu.show_fps_help": "Surveiller la perf du jeu", "main_menu.show_fps_help": "Surveiller la perf du jeu",
"main_menu.show_stats": "",
"main_menu.show_stats_help": "",
"main_menu.sounds": "Sons du jeu", "main_menu.sounds": "Sons du jeu",
"main_menu.sounds_help": "Ralentis certains téléphones.", "main_menu.sounds_help": "Ralentis certains téléphones.",
"main_menu.title": "Breakout 71", "main_menu.title": "Breakout 71",
@ -100,42 +99,33 @@
"main_menu.unlocks_help": "Essayez les éléments débloqués", "main_menu.unlocks_help": "Essayez les éléments débloqués",
"play.close_modale_window_tooltip": "Fermer", "play.close_modale_window_tooltip": "Fermer",
"play.current_lvl": "Niveau {{level}}/{{max}}", "play.current_lvl": "Niveau {{level}}/{{max}}",
"play.current_lvl_adventure": "Niveau {{level}}", "play.current_lvl_loop": "Niveau {{level}}/{{max}} boucle {{loop}}",
"play.menu_label": "Menu", "play.menu_label": "Menu",
"play.missed_ball": "raté", "play.missed_ball": "raté",
"play.mobile_press_to_play": "Gardez le doigt ici pour jouer", "play.mobile_press_to_play": "Gardez le doigt ici pour jouer",
"premium.adventure_mode": "Mode sans fin",
"premium.adventure_mode_help": "Démarrer une nouvelle partie sans fin",
"premium.back": "Retour", "premium.back": "Retour",
"premium.back_help": "Retour au menu principal", "premium.back_help": "Retour au menu principal",
"premium.buy": "Acheter une clé de licence", "premium.buy": "Acheter une clé de licence",
"premium.buy_disabled_help": "À venir", "premium.buy_disabled_help": "À venir",
"premium.buy_help": "Vous serez redirigé vers un formulaire pour payer et recevrez la licence par e-mail. Revenez ensuite pour la saisir ici.", "premium.buy_help": "Vous serez redirigé vers un formulaire pour payer et recevrez la licence par e-mail. Revenez ensuite pour la saisir ici.",
"premium.choose_next_step": "Vous disposez de{{score}}$. Cliquez sur les améliorations que vous souhaitez acheter.",
"premium.discard": "Abandonner l'avantage {{name}}",
"premium.discard_help": "Cela rendra d'autres avantages moins chers",
"premium.enter": "Entrez la clé de licence", "premium.enter": "Entrez la clé de licence",
"premium.enter_help": "Collez la licence dans la fenêtre qui s'ouvre", "premium.enter_help": "Collez la licence dans la fenêtre qui s'ouvre",
"premium.help": "Achetez une licence pour Breakout 71 pour débloquer le mode infini et soutenir le développement. Elle coûte 4,99 € et est illimitée dans le temps. Vous pouvez l'utiliser sur plusieurs appareils, mais ne la partagez pas en ligne.", "premium.help": "Achetez une licence pour Breakout 71 pour débloquer le bouclage du jeu et soutenir le développement. Elle coûte 4,99 € et est illimitée dans le temps. Vous pouvez l'utiliser sur plusieurs appareils, mais ne la partagez pas en ligne.",
"premium.help_google": "Bien que je prévoie de proposer des licences premium via Google Play, je n'ai pas encore eu l'occasion de le faire ; il n'y a donc pas de lien d'achat ici. Si vous possédez déjà une clé de licence, vous pouvez la saisir ci-dessous.", "premium.help_google": "Bien que je prévoie de proposer des licences premium via Google Play, je n'ai pas encore eu l'occasion de le faire ; il n'y a donc pas de lien d'achat ici. Si vous possédez déjà une clé de licence, vous pouvez la saisir ci-dessous.",
"premium.next_step_title": "Achetez des améliorations et passez au niveau suivant", "premium.per_hours": "Vous avez passé {{hours}} heures à jouer",
"premium.pick_level": "Accédez au niveau « {{name}} » pour $ {{cost}}", "premium.per_hours_help": "Donnez 4.99€ pour être premium",
"premium.pick_perk": "Obtenez {{name}}pour{{cost}}$", "premium.thanks": "",
"premium.short_help": "Jouez le plus longtemps possible", "premium.thanks_help": "",
"premium.upgrade_perk_to_level": "Passez de {{name}} à {{lvl}} pour{{cost}}$", "premium.title": "",
"premium.your_upgrades": "Vos mises à jour jusqu'à présent :",
"sandbox.help": "Tester n'importe quelle combinaison d'améliorations", "sandbox.help": "Tester n'importe quelle combinaison d'améliorations",
"sandbox.instructions": "Sélectionnez les amélioration ci-dessous et appuyez sur \"Démarrer la partie de test\" pour les tester. Les scores et les statistiques ne seront pas enregistrés.", "sandbox.instructions": "Sélectionnez les amélioration ci-dessous et appuyez sur \"Démarrer la partie de test\" pour les tester. Les scores et les statistiques ne seront pas enregistrés.",
"sandbox.start": "Démarrer la partie de test", "sandbox.start": "Démarrer la partie de test",
"sandbox.title": "Mode bac à sable", "sandbox.title": "Mode bac à sable",
"sandbox.unlocks_at": "Déverrouillé à partir d'un score total de {{score}}", "sandbox.unlocks_at": "Déverrouillé à partir d'un score total de {{score}}",
"score_panel.restart": "Redémarrer", "score_panel.bebuffs_list": "Handicapes : ",
"score_panel.restart_help": "Commencer une nouvelle partie",
"score_panel.resume": "Continuer la partie",
"score_panel.resume_help": "Fermer cette fenêtre pour retourner au jeu",
"score_panel.test_run": "Il s'agit d'une partie d'essai, le score n'est pas enregistré.", "score_panel.test_run": "Il s'agit d'une partie d'essai, le score n'est pas enregistré.",
"score_panel.title": "{{score}} points au niveau {{level}}/{{max}} ", "score_panel.title": "{{score}} points au niveau {{level}}/{{max}} ",
"score_panel.title_adventure": "{{score}} points au niveau {{level}} de l'aventure", "score_panel.title_looped": "{{score}} points au niveau {{level}}/{{max}} ",
"score_panel.upcoming_levels": "Niveaux de la parties : ", "score_panel.upcoming_levels": "Niveaux de la parties : ",
"score_panel.upgrades_picked": "Améliorations choisies jusqu'à présent :", "score_panel.upgrades_picked": "Améliorations choisies jusqu'à présent :",
"unlocks.greyed_out_help": "Les éléments grisées peuvent être débloquées en augmentant votre score total. Le score total augmente à chaque fois que vous marquez des points dans le jeu.", "unlocks.greyed_out_help": "Les éléments grisées peuvent être débloquées en augmentant votre score total. Le score total augmente à chaque fois que vous marquez des points dans le jeu.",

View file

@ -45,7 +45,5 @@ export const allLevels = rawLevelsList
export const upgrades = rawUpgrades.map((u) => ({ export const upgrades = rawUpgrades.map((u) => ({
...u, ...u,
icon: icons["icon:" + u.id], icon: icons["icon:" + u.id]
adventure: "adventure" in u ? u.adventure : true,
normal: "normal" in u ? u.normal : true,
})) as Upgrade[]; })) as Upgrade[];

View file

@ -11,10 +11,10 @@ import { dontOfferTooSoon, resetBalls } from "./gameStateMutators";
import { isOptionOn } from "./options"; import { isOptionOn } from "./options";
import { debuffs } from "./debuffs"; import { debuffs } from "./debuffs";
export function newGameState(params: RunParams): GameState {
const totalScoreAtRunStart = getTotalScore(); export function getRunLevels(totalScoreAtRunStart:number, params: RunParams){
const firstLevel = params?.level const firstLevel =
? allLevels.filter((l) => l.name === params?.level) params?.level ? allLevels.filter((l) => l.name === params?.level)
: []; : [];
const restInRandomOrder = allLevels const restInRandomOrder = allLevels
@ -23,9 +23,15 @@ export function newGameState(params: RunParams): GameState {
.filter((l) => l.name !== params?.levelToAvoid) .filter((l) => l.name !== params?.levelToAvoid)
.sort(() => Math.random() - 0.5); .sort(() => Math.random() - 0.5);
const runLevels = firstLevel.concat( return firstLevel.concat(
restInRandomOrder.slice(0, 7 + 3).sort((a, b) => a.sortKey - b.sortKey), restInRandomOrder.slice(0, 7 + 3).sort((a, b) => a.sortKey - b.sortKey),
); );
}
export function newGameState(params: RunParams): GameState {
const totalScoreAtRunStart = getTotalScore();
const runLevels =getRunLevels(totalScoreAtRunStart, params)
const perks = { ...makeEmptyPerksMap(upgrades), ...(params?.perks || {}) }; const perks = { ...makeEmptyPerksMap(upgrades), ...(params?.perks || {}) };
@ -35,6 +41,7 @@ export function newGameState(params: RunParams): GameState {
currentLevel: 0, currentLevel: 0,
upgradesOfferedFor: -1, upgradesOfferedFor: -1,
perks, perks,
bannedPerks:makeEmptyPerksMap(upgrades),
debuffs: { ...emptyDebuffsMap(), ...(params?.debuffs || {}) }, debuffs: { ...emptyDebuffsMap(), ...(params?.debuffs || {}) },
puckWidth: 200, puckWidth: 200,
baseSpeed: 12, baseSpeed: 12,
@ -102,13 +109,12 @@ export function newGameState(params: RunParams): GameState {
needsRender: true, needsRender: true,
autoCleanUses: 0, autoCleanUses: 0,
...defaultSounds(), ...defaultSounds(),
isAdventureMode: !!params?.adventure,
rerolls: 0, rerolls: 0,
loop:0
}; };
resetBalls(gameState); resetBalls(gameState);
if (!sumOfValues(gameState.perks) && !params?.adventure) { if (!sumOfValues(gameState.perks)) {
const giftable = getPossibleUpgrades(gameState).filter((u) => u.giftable); const giftable = getPossibleUpgrades(gameState).filter((u) => u.giftable);
const randomGift = const randomGift =
(isOptionOn("easy") && "slow_down") || (isOptionOn("easy") && "slow_down") ||

View file

@ -19,11 +19,21 @@ export const options = {
name: t("main_menu.basic"), name: t("main_menu.basic"),
help: t("main_menu.basic_help"), help: t("main_menu.basic_help"),
}, },
colorful_coins: {
default: false,
name: t("main_menu.colorful_coins"),
help: t("main_menu.colorful_coins_help"),
},
show_fps: { show_fps: {
default: false, default: false,
name: t("main_menu.show_fps"), name: t("main_menu.show_fps"),
help: t("main_menu.show_fps_help"), help: t("main_menu.show_fps_help"),
}, },
show_stats: {
default: false,
name: t("main_menu.show_stats"),
help: t("main_menu.show_stats_help"),
},
pointerLock: { pointerLock: {
default: false, default: false,
name: t("main_menu.pointer_lock"), name: t("main_menu.pointer_lock"),

View file

@ -1,9 +1,9 @@
import { GameState } from "./types"; import {GameState} from "./types";
import { icons } from "./loadGameData"; import {icons} from "./loadGameData";
import { t } from "./i18n/i18n"; import {t} from "./i18n/i18n";
import { getSettingValue, setSettingValue } from "./settings"; import {getSettingValue, setSettingValue} from "./settings";
import { asyncAlert } from "./asyncAlert"; import {asyncAlert} from "./asyncAlert";
import { confirmRestart, openMainMenu, restart } from "./game"; import {openMainMenu} from "./game";
const publicKeyString = `-----BEGIN PUBLIC KEY----- const publicKeyString = `-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyGQJgs6gxa0Fd86TuZ2q MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyGQJgs6gxa0Fd86TuZ2q
@ -21,49 +21,49 @@ dZpWPx3O+rZbRQb0gHcvN4+n2Y7fWAeC9mxVZtADqvVr/GTumMbLj7DdhWtt1Ogu
-----END PUBLIC KEY-----`; -----END PUBLIC KEY-----`;
function pemToArrayBuffer(pem: string) { function pemToArrayBuffer(pem: string) {
const b64 = pem const b64 = pem
.replace(/-----BEGIN PUBLIC KEY-----/, "") .replace(/-----BEGIN PUBLIC KEY-----/, "")
.replace(/-----END PUBLIC KEY-----/, "") .replace(/-----END PUBLIC KEY-----/, "")
.replace(/\s+/g, ""); .replace(/\s+/g, "");
const binaryDerString = atob(b64); const binaryDerString = atob(b64);
const binaryDer = new Uint8Array(binaryDerString.length); const binaryDer = new Uint8Array(binaryDerString.length);
for (let i = 0; i < binaryDerString.length; i++) { for (let i = 0; i < binaryDerString.length; i++) {
binaryDer[i] = binaryDerString.charCodeAt(i); binaryDer[i] = binaryDerString.charCodeAt(i);
} }
return binaryDer.buffer; return binaryDer.buffer;
} }
async function getPriceId(key: string, pem: string) { async function getPriceId(key: string, pem: string) {
// Split the key into its components // Split the key into its components
const [priceId, timestamp, signature] = key.split(":"); const [priceId, timestamp, signature] = key.split(":");
const data = `${priceId}:${timestamp}`; const data = `${priceId}:${timestamp}`;
const publicKeyBuffer = pemToArrayBuffer(pem); const publicKeyBuffer = pemToArrayBuffer(pem);
const publicKey = await crypto.subtle.importKey( const publicKey = await crypto.subtle.importKey(
"spki", "spki",
publicKeyBuffer, publicKeyBuffer,
{ {
name: "RSA-PSS", name: "RSA-PSS",
hash: "SHA-256", hash: "SHA-256",
}, },
true, true,
["verify"], ["verify"],
); );
// Verify the signature using ECDSA // Verify the signature using ECDSA
const isValid = await crypto.subtle.verify( const isValid = await crypto.subtle.verify(
{ {
name: "RSA-PSS", name: "RSA-PSS",
saltLength: 32, saltLength: 32,
}, },
publicKey, publicKey,
new Uint8Array(Array.from(atob(signature), (c) => c.charCodeAt(0))), new Uint8Array(Array.from(atob(signature), (c) => c.charCodeAt(0))),
new TextEncoder().encode(data), new TextEncoder().encode(data),
); );
if (!isValid) throw new Error("Invalid key signature"); if (!isValid) throw new Error("Invalid key signature");
return priceId; return priceId;
} }
let premium = false; let premium = false;
@ -71,94 +71,116 @@ const gamePriceId = "price_1R6YaEGRf74lr2EkSo2GPvuO";
checkKey(getSettingValue("license", "")).then(); checkKey(getSettingValue("license", "")).then();
async function checkKey(key: string) { async function checkKey(key: string) {
if (!key) return "No key"; if (!key) return "No key";
try { try {
if (gamePriceId !== (await getPriceId(key, publicKeyString))) { if (gamePriceId !== (await getPriceId(key, publicKeyString))) {
return "Wrong product"; return "Wrong product";
}
premium = true;
return "";
} catch (e) {
return "Could not upgrade : " + e.message;
} }
premium = true;
return "";
} catch (e) {
return "Could not upgrade : " + e.message;
}
} }
export function isPremium() { export function isPremium() {
return premium; return premium;
} }
export function premiumMenuEntry(gameState: GameState) { export function premiumMenuEntry(gameState: GameState) {
if (isPremium()) { if (isPremium()) {
return { return {
icon: icons["icon:adventure_mode"], icon: icons["icon:premium_active"],
text: t("premium.adventure_mode"), text: t("premium.thanks"),
help: t("premium.adventure_mode_help"), help: t("premium.thanks_help"),
value: async () => { value: async () => {
if (await confirmRestart(gameState)) { navigator.clipboard.writeText(getSettingValue('license', ''))
restart({ openMainMenu()
adventure: true, },
}); };
}
let text = t("premium.title")
let help = t("premium.buy")
try {
const timePlayed = localStorage.getItem('breakout_71_total_play_time')
if (timePlayed && !isGooglePlayInstall) {
const hours = parseFloat(timePlayed) / 1000 / 60 / 60
const pricePerHours = 4.99 / hours
const args = {
hours: Math.floor(hours),
pricePerHours: pricePerHours.toFixed(2)
}
if (pricePerHours > 0 && pricePerHours < 0.5) {
text = t("premium.per_hours", args)
help = t("premium.per_hours_help", args)
}
console.log({args})
} }
}, } catch (e) {
console.warn(e)
}
return {
icon: icons["icon:premium"],
text,
help,
value: () => openPremiumMenu(""),
}; };
}
return {
icon: icons["icon:premium"],
text: t("premium.adventure_mode"),
help: t("premium.short_help"),
value: () => openPremiumMenu(""),
};
} }
async function openPremiumMenu(text) { const isGooglePlayInstall =
const isGooglePlayInstall =
new URLSearchParams(location.search).get("source") === new URLSearchParams(location.search).get("source") ===
"com.android.vending"; "com.android.vending";
const cb = await asyncAlert({ async function openPremiumMenu(text) {
title: t("premium.adventure_mode"),
content: [
text || const cb = await asyncAlert({
(isGooglePlayInstall && t("premium.help_google")) || title: t("premium.title"),
t("premium.help"), content: [
{ text ||
text: t("premium.buy"), (isGooglePlayInstall && t("premium.help_google")) ||
disabled: isGooglePlayInstall, t("premium.help"),
help: isGooglePlayInstall {
? t("premium.buy_disabled_help") text: t("premium.buy"),
: t("premium.buy_help"), disabled: isGooglePlayInstall,
value() { help: isGooglePlayInstall
window.open( ? t("premium.buy_disabled_help")
"https://licenses.lecaro.me/buy/price_1R6YaEGRf74lr2EkSo2GPvuO", : t("premium.buy_help"),
"_blank", value() {
); window.open(
}, "https://licenses.lecaro.me/buy/price_1R6YaEGRf74lr2EkSo2GPvuO",
}, "_blank",
{ );
text: t("premium.enter"), },
help: t("premium.enter_help"), },
async value() { {
const value = (prompt("Please paste your license key") || "").replace( text: t("premium.enter"),
/\s+/g, help: t("premium.enter_help"),
"", async value() {
); const value = (prompt("Please paste your license key") || "").replace(
const problem = await checkKey(value); /\s+/g,
if (problem) { "",
openPremiumMenu(problem).then(); );
} else { const problem = await checkKey(value);
setSettingValue("license", value); if (problem) {
openMainMenu().then(); openPremiumMenu(problem).then();
} } else {
}, setSettingValue("license", value);
}, openMainMenu().then();
{ }
text: t("premium.back"), },
help: t("premium.back_help"), },
value() { {
openMainMenu().then(); text: t("premium.back"),
}, help: t("premium.back_help"),
}, value() {
], openMainMenu().then();
}); },
if (cb) cb(); },
],
});
if (cb) cb();
} }

View file

@ -54,7 +54,7 @@ export function drawMainCanvasOnSmallCanvas(gameState: GameState) {
recordCanvasCtx.fillText( recordCanvasCtx.fillText(
"Level " + "Level " +
(gameState.currentLevel + 1) + (gameState.currentLevel + 1) +
(gameState.isAdventureMode ? "" : "/" + max_levels(gameState)), "/" + max_levels(gameState),
12, 12,
12, 12,
); );

View file

@ -36,11 +36,11 @@ export function render(gameState: GameState) {
if (!width || !height) return; if (!width || !height) return;
if (gameState.currentLevel || gameState.levelTime) { if (gameState.currentLevel || gameState.levelTime) {
menuLabel.innerText = gameState.isAdventureMode menuLabel.innerText = gameState.loop? t("play.current_lvl_loop", {
? t("play.current_lvl_adventure", {
level: gameState.currentLevel + 1, level: gameState.currentLevel + 1,
}) max: max_levels(gameState),
: t("play.current_lvl", { loop:gameState.loop
}) : t("play.current_lvl", {
level: gameState.currentLevel + 1, level: gameState.currentLevel + 1,
max: max_levels(gameState), max: max_levels(gameState),
}); });
@ -88,12 +88,7 @@ export function render(gameState: GameState) {
); );
}); });
ctx.globalAlpha = 1; ctx.globalAlpha = 1;
forEachLiveOne(gameState.lights, (flash) => {
const { x, y, time, color, size, duration } = flash;
const elapsed = gameState.levelTime - time;
ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2);
drawFuzzyBall(ctx, color, size, x, y);
});
forEachLiveOne(gameState.particles, (flash) => { forEachLiveOne(gameState.particles, (flash) => {
const { x, y, time, color, size, duration } = flash; const { x, y, time, color, size, duration } = flash;
const elapsed = gameState.levelTime - time; const elapsed = gameState.levelTime - time;
@ -166,8 +161,10 @@ export function render(gameState: GameState) {
// Coins // Coins
ctx.globalAlpha = 1; ctx.globalAlpha = 1;
forEachLiveOne(gameState.coins, (coin) => { forEachLiveOne(gameState.coins, (coin) => {
ctx.globalCompositeOperation =
coin.color === "gold" || level.color ? "source-over" : "screen"; ctx.globalCompositeOperation ='source-over'
// ctx.globalCompositeOperation =
// coin.color === "gold" || level.color ? "source-over" : "screen";
drawCoin( drawCoin(
ctx, ctx,
coin.color, coin.color,
@ -200,6 +197,15 @@ export function render(gameState: GameState) {
ctx.globalCompositeOperation = "source-over"; ctx.globalCompositeOperation = "source-over";
renderAllBricks(); renderAllBricks();
ctx.globalCompositeOperation = "screen";
forEachLiveOne(gameState.lights, (flash) => {
const { x, y, time, color, size, duration } = flash;
const elapsed = gameState.levelTime - time;
ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2) * 0.5;
drawBrick(ctx, color, x,y, -1)
});
ctx.globalCompositeOperation = "screen"; ctx.globalCompositeOperation = "screen";
forEachLiveOne(gameState.texts, (flash) => { forEachLiveOne(gameState.texts, (flash) => {
const { x, y, time, color, size, duration } = flash; const { x, y, time, color, size, duration } = flash;
@ -495,8 +501,7 @@ export function renderAllBricks() {
redBorderOnBricksWithWrongColor || redBorderOnBricksWithWrongColor ||
redColorOnAllBricks || redColorOnAllBricks ||
gameState.perks.reach || gameState.perks.reach ||
gameState.perks.zen || gameState.perks.zen
gameState.debuffs.negative_bricks
) )
) { ) {
offset = 0; offset = 0;

14
src/types.d.ts vendored
View file

@ -27,10 +27,6 @@ export type Palette = { [k: string]: string };
export type Upgrade = { export type Upgrade = {
threshold: number; threshold: number;
giftable: boolean; giftable: boolean;
// Offered in adventure mode
adventure: boolean;
// offered in normal mode
normal: boolean;
id: PerkId; id: PerkId;
name: string; name: string;
icon: string; icon: string;
@ -158,6 +154,12 @@ export type PerksMap = {
[k in PerkId]: number; [k in PerkId]: number;
}; };
type Debuff={
id: DebuffId;
max:number;
name:(lvl: number,banned:string)=>string;
help:(lvl: number,perk:string)=>string;
}
export type DebuffId = (typeof debuffs)[number]["id"]; export type DebuffId = (typeof debuffs)[number]["id"];
export type DebuffsMap = { export type DebuffsMap = {
@ -208,6 +210,7 @@ export type GameState = {
puckWidth: number; puckWidth: number;
// perks the user currently has // perks the user currently has
perks: PerksMap; perks: PerksMap;
bannedPerks: PerksMap;
debuffs: DebuffsMap; debuffs: DebuffsMap;
// Base speed of the ball in pixels/tick // Base speed of the ball in pixels/tick
baseSpeed: number; baseSpeed: number;
@ -281,15 +284,14 @@ export type GameState = {
colorChange: { vol: number; x: number }; colorChange: { vol: number; x: number };
void: { vol: number; x: number }; void: { vol: number; x: number };
}; };
isAdventureMode: boolean;
rerolls: number; rerolls: number;
loop: number;
}; };
export type RunParams = { export type RunParams = {
level?: string; level?: string;
levelToAvoid?: string; levelToAvoid?: string;
perks?: Partial<PerksMap>; perks?: Partial<PerksMap>;
adventure?: boolean;
debuffs?: boolean; debuffs?: boolean;
}; };
export type OptionDef = { export type OptionDef = {

View file

@ -3,7 +3,7 @@ import { t } from "./i18n/i18n";
export const rawUpgrades = [ export const rawUpgrades = [
{ {
requires: "", requires: "",
rejects: "",
threshold: 0, threshold: 0,
giftable: false, giftable: false,
id: "extra_life", id: "extra_life",
@ -17,7 +17,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 0, threshold: 0,
id: "streak_shots", id: "streak_shots",
giftable: true, giftable: true,
@ -29,7 +29,7 @@ export const rawUpgrades = [
{ {
requires: "", requires: "",
rejects: "",
threshold: 0, threshold: 0,
id: "base_combo", id: "base_combo",
giftable: true, giftable: true,
@ -41,7 +41,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 0, threshold: 0,
giftable: false, giftable: false,
id: "slow_down", id: "slow_down",
@ -52,7 +52,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 0, threshold: 0,
giftable: false, giftable: false,
id: "bigger_puck", id: "bigger_puck",
@ -63,7 +63,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 0, threshold: 0,
giftable: false, giftable: false,
id: "viscosity", id: "viscosity",
@ -75,7 +75,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 0, threshold: 0,
id: "left_is_lava", id: "left_is_lava",
giftable: true, giftable: true,
@ -87,7 +87,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 0, threshold: 0,
id: "right_is_lava", id: "right_is_lava",
giftable: true, giftable: true,
@ -98,7 +98,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 0, threshold: 0,
id: "top_is_lava", id: "top_is_lava",
giftable: true, giftable: true,
@ -109,7 +109,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 0, threshold: 0,
giftable: false, giftable: false,
id: "skip_last", id: "skip_last",
@ -123,10 +123,10 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 500, threshold: 500,
id: "telekinesis", id: "telekinesis",
giftable: true, giftable: false,
max: 2, max: 2,
name: t("upgrades.telekinesis.name"), name: t("upgrades.telekinesis.name"),
help: (lvl: number) => help: (lvl: number) =>
@ -137,7 +137,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 1000, threshold: 1000,
giftable: false, giftable: false,
id: "coin_magnet", id: "coin_magnet",
@ -151,10 +151,10 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 1500, threshold: 1500,
id: "multiball", id: "multiball",
giftable: true, giftable: false,
max: 6, max: 6,
name: t("upgrades.multiball.name"), name: t("upgrades.multiball.name"),
help: (lvl: number) => t("upgrades.multiball.help", { count: lvl + 1 }), help: (lvl: number) => t("upgrades.multiball.help", { count: lvl + 1 }),
@ -162,7 +162,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 2000, threshold: 2000,
giftable: false, giftable: false,
id: "smaller_puck", id: "smaller_puck",
@ -176,10 +176,10 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 3000, threshold: 3000,
id: "pierce", id: "pierce",
giftable: true, giftable: false,
max: 3, max: 3,
name: t("upgrades.pierce.name"), name: t("upgrades.pierce.name"),
help: (lvl: number) => t("upgrades.pierce.help", { count: 3 * lvl }), help: (lvl: number) => t("upgrades.pierce.help", { count: 3 * lvl }),
@ -187,7 +187,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 4000, threshold: 4000,
id: "picky_eater", id: "picky_eater",
giftable: true, giftable: true,
@ -198,7 +198,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 5000, threshold: 5000,
giftable: false, giftable: false,
id: "metamorphosis", id: "metamorphosis",
@ -209,7 +209,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 6000, threshold: 6000,
id: "compound_interest", id: "compound_interest",
giftable: true, giftable: true,
@ -220,7 +220,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 7000, threshold: 7000,
id: "hot_start", id: "hot_start",
giftable: true, giftable: true,
@ -235,10 +235,10 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 9000, threshold: 9000,
id: "sapper", id: "sapper",
giftable: true, giftable: false,
max: 7, max: 7,
name: t("upgrades.sapper.name"), name: t("upgrades.sapper.name"),
help: (lvl: number) => help: (lvl: number) =>
@ -249,7 +249,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 11000, threshold: 11000,
id: "bigger_explosions", id: "bigger_explosions",
giftable: false, giftable: false,
@ -260,7 +260,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 13000, threshold: 13000,
giftable: false, giftable: false,
adventure: false, adventure: false,
@ -272,7 +272,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 15000, threshold: 15000,
giftable: false, giftable: false,
id: "pierce_color", id: "pierce_color",
@ -283,7 +283,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 18000, threshold: 18000,
giftable: false, giftable: false,
id: "soft_reset", id: "soft_reset",
@ -320,7 +320,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 30000, threshold: 30000,
giftable: false, giftable: false,
id: "puck_repulse_ball", id: "puck_repulse_ball",
@ -334,7 +334,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 35000, threshold: 35000,
giftable: false, giftable: false,
id: "wind", id: "wind",
@ -346,7 +346,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 40000, threshold: 40000,
giftable: false, giftable: false,
id: "sturdy_bricks", id: "sturdy_bricks",
@ -360,7 +360,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 45000, threshold: 45000,
giftable: false, giftable: false,
id: "respawn", id: "respawn",
@ -372,7 +372,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 50000, threshold: 50000,
giftable: false, giftable: false,
id: "one_more_choice", id: "one_more_choice",
@ -383,7 +383,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 55000, threshold: 55000,
giftable: false, giftable: false,
id: "instant_upgrade", id: "instant_upgrade",
@ -395,7 +395,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 60000, threshold: 60000,
giftable: false, giftable: false,
id: "concave_puck", id: "concave_puck",
@ -406,7 +406,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 65000, threshold: 65000,
giftable: false, giftable: false,
id: "helium", id: "helium",
@ -417,9 +417,9 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 70000, threshold: 70000,
giftable: false, giftable: true,
id: "asceticism", id: "asceticism",
max: 1, max: 1,
name: t("upgrades.asceticism.name"), name: t("upgrades.asceticism.name"),
@ -428,7 +428,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 75000, threshold: 75000,
giftable: false, giftable: false,
id: "unbounded", id: "unbounded",
@ -439,7 +439,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 80000, threshold: 80000,
giftable: false, giftable: false,
id: "shunt", id: "shunt",
@ -450,7 +450,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 85000, threshold: 85000,
giftable: false, giftable: false,
id: "yoyo", id: "yoyo",
@ -461,9 +461,9 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 90000, threshold: 90000,
giftable: false, giftable: true,
id: "nbricks", id: "nbricks",
max: 3, max: 3,
name: t("upgrades.nbricks.name"), name: t("upgrades.nbricks.name"),
@ -472,7 +472,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 95000, threshold: 95000,
giftable: false, giftable: false,
id: "etherealcoins", id: "etherealcoins",
@ -493,9 +493,9 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 105000, threshold: 105000,
giftable: false, giftable: true,
id: "zen", id: "zen",
max: 1, max: 1,
name: t("upgrades.zen.name"), name: t("upgrades.zen.name"),
@ -516,7 +516,7 @@ export const rawUpgrades = [
{ {
requires: "", requires: "",
threshold: 115000, threshold: 115000,
giftable: false, giftable: true,
id: "trampoline", id: "trampoline",
max: 1, max: 1,
name: t("upgrades.trampoline.name"), name: t("upgrades.trampoline.name"),
@ -525,7 +525,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 120000, threshold: 120000,
giftable: false, giftable: false,
id: "ghost_coins", id: "ghost_coins",
@ -546,7 +546,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 130000, threshold: 130000,
giftable: false, giftable: false,
id: "ball_attracts_coins", id: "ball_attracts_coins",
@ -557,8 +557,9 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 135000, threshold: 135000,
// a bit too hard when starting up
giftable: false, giftable: false,
id: "reach", id: "reach",
max: 1, max: 1,
@ -568,9 +569,9 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 140000, threshold: 140000,
giftable: false, giftable: true,
id: "passive_income", id: "passive_income",
max: 4, max: 4,
name: t("upgrades.passive_income.name"), name: t("upgrades.passive_income.name"),
@ -580,7 +581,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 145000, threshold: 145000,
giftable: false, giftable: false,
id: "clairvoyant", id: "clairvoyant",
@ -592,7 +593,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 150000, threshold: 150000,
giftable: true, giftable: true,
id: "side_kick", id: "side_kick",
@ -603,7 +604,7 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 155000, threshold: 155000,
giftable: false, giftable: false,
id: "implosions", id: "implosions",
@ -614,7 +615,6 @@ export const rawUpgrades = [
}, },
{ {
requires: "", requires: "",
rejects: "",
threshold: 160000, threshold: 160000,
giftable: false, giftable: false,
id: "corner_shot", id: "corner_shot",