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)
- [HackerNews thread](https://news.ycombinator.com/item?id=43183131)
# Todo
- bring back detailed help of perks as "intel"
- people assume unbounded allows for wrap around
- coin magnet and viscosity : only one level ~2.5
- 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
- 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
- [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.
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.
The score you make in each level is instead multiplied by the number of "upgrades" and "choices" you would have had.
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.
Each downgrade acts as a score multiplier.
Your goal is no longer to score higher, but to go farther
Allow players to loop the game :
- [x] keep your score
- [x] keep 1 perk
- [x] add one hasard
- [ ] add one HP to all bricks
- [ ] advertise looping in normal game over screen
- [ ] save score at the end of first loop, in addition to the final one ?
- [ ] check that stats like max level are correct
# System requirements
@ -164,7 +172,18 @@ There's also an easy mode for kids (slower ball).
# 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
- famous characters and movies
- 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
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",
"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,
"color": ""
},
@ -1032,13 +1032,6 @@
"svg": null,
"color": ""
},
{
"name": "icon:adventure_mode",
"size": 11,
"bricks": "__________________________________ttt___bbb_bttttbbbbbbbb__tbt__bbbbbbbbttttb_bbb___ttt__________________________________",
"svg": null,
"color": ""
},
{
"name": "icon:7_levels_run",
"size": 7,

View file

@ -1,56 +1,47 @@
import { t } from "./i18n/i18n";
import {Debuff} from "./types";
export const debuffs = [
{
id: "negative_coins",
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 }),
},
{
id: "negative_bricks",
id: "more_bombs",
max: 20,
name: t("debuffs.negative_bricks.name"),
help: (lvl: number) => t("debuffs.negative_bricks.help", { lvl }),
name: (lvl: number) => t("debuffs.more_bombs.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 }),
},
{
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;
] as const as Debuff[];
/*
Possible challenges :
- add a force field for 10s that negates hots start
- other perks can be randomly turned off
- ball keeps accelerating until unplayable
- interference : telekinesis works backward for lvl/2 seconds every 5 seconds (show timer ?)
- exclusion : one of your current perks (except the kept one) is banned
- 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
- ball creates a draft behind itself that blows coins in odd patterns
- bricks are invisible
- downward wind
- side wind
- add red anti-coins that apply downgrades
- destroy your combo
- hurt your score

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,7 @@
import { Ball, GameState, PerkId, PerksMap } from "./types";
import { icons, upgrades } from "./loadGameData";
import { t } from "./i18n/i18n";
import {debuffs} from "./debuffs";
export function getMajorityValue(arr: string[]): string {
const count: { [k: string]: number } = {};
@ -54,6 +55,8 @@ export function getPossibleUpgrades(gameState: GameState) {
}
export function max_levels(gameState: GameState) {
// TODO
return 2
return 7 + gameState.perks.extra_levels;
}
@ -61,14 +64,25 @@ export function pickedUpgradesHTMl(gameState: GameState) {
let list = "";
for (let u of upgrades) {
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 "";
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) {
if (gameState.isAdventureMode) return "";
if (!gameState.perks.clairvoyant) return "";
let list = "";
for (let i = 0; i < max_levels(gameState); i++) {

View file

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

View file

@ -3,18 +3,11 @@
"confirmRestart.text": "You're about to start a new run, is that really what you wanted ?",
"confirmRestart.title": "Start a new run ?",
"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.downward_wind.name": "Downward wind",
"debuffs.negative_bricks.help": "{{lvl}} bricks on each level are replaced by negative bricks that break the combo",
"debuffs.negative_bricks.name": "Void bricks",
"debuffs.negative_coins.help": "{{lvl}}% of coins spawn empty 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",
"debuffs.banned.description": "{{lvl}} banned perk(s) : {{banned}}",
"debuffs.banned.help": "{{perk}} is banned for this run",
"debuffs.interference.help": "Telekinesis and yo-yo stop working for {{lvl}}s every 7s",
"debuffs.more_bombs.help": "{{lvl}} bricks replaced by bombs",
"debuffs.negative_coins.help": "{{lvl}}% of coins spawn void and break the combo if caught",
"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.title": "Game Over",
@ -50,8 +43,12 @@
"level_up.unlocked_level": " (Level)",
"level_up.unlocked_perk": " (Perk)",
"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_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_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",
@ -93,49 +90,42 @@
"main_menu.settings_title": "Settings",
"main_menu.show_fps": "FPS counter",
"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_help": "Can slow down some phones.",
"main_menu.title": "Breakout 71",
"main_menu.unlocks": "Unlocked content",
"main_menu.unlocks_help": "Try perks and levels you unlocked",
"play.close_modale_window_tooltip": "close ",
"play.current_lvl": "L{{level}}/{{max}}",
"play.current_lvl_adventure": "L {{level}}",
"play.current_lvl": "Level {{level}}/{{max}}",
"play.current_lvl_loop": "Level {{level}}/{{max}} loop {{loop}}",
"play.menu_label": "menu",
"play.missed_ball": "miss",
"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_help": "Return to main menu",
"premium.buy": "Buy a license key",
"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.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_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.next_step_title": "Buy upgrades and continue to next level",
"premium.pick_level": "Go to level \"{{name}}\" for ${{cost}}",
"premium.pick_perk": "Get {{name}} for ${{cost}}",
"premium.short_help": "Play as long as possible",
"premium.upgrade_perk_to_level": "Upgrade {{name}} to {{lvl}} for ${{cost}}",
"premium.your_upgrades": "Your upgrades so far : ",
"premium.per_hours": "You've played for {{hours}} hours",
"premium.per_hours_help": "Donate 4.99€ to get premium",
"premium.thanks": "You are premium, thanks ! ",
"premium.thanks_help": "Copy your license key",
"premium.title": "Unlock looping with premium ",
"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.start": "Start test run",
"sandbox.title": "Sandbox mode",
"sandbox.unlocks_at": "Unlocks at total score {{score}}",
"score_panel.restart": "Restart",
"score_panel.restart_help": "Start a brand new run",
"score_panel.resume": "Resume",
"score_panel.resume_help": "Return to your run",
"score_panel.bebuffs_list": "Debuffs :",
"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_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.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.",

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.title": "Démarrer 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.downward_wind.name": "Vent descendant",
"debuffs.negative_bricks.help": "{{lvl}} briques à chaque niveau sont remplacées par des briques négatives qui brisent le combo",
"debuffs.negative_bricks.name": "Briques vides",
"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",
"debuffs.banned.description": "{{lvl}} amélioration(s) bannie(s) : {{banned}}",
"debuffs.banned.help": "",
"debuffs.interference.help": "",
"debuffs.more_bombs.help": "",
"debuffs.negative_coins.help": "",
"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.title": "Balle perdue",
@ -50,8 +43,12 @@
"level_up.unlocked_level": " (Niveau)",
"level_up.unlocked_perk": " (Amélioration)",
"level_up.upgrade_perk_to_level": " niveau {{level}}",
"loop.instructions": "",
"loop.title": "",
"main_menu.basic": "Graphismes simplifiés",
"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_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>",
@ -93,6 +90,8 @@
"main_menu.settings_title": "Paramètre",
"main_menu.show_fps": "Compteur de FPS",
"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_help": "Ralentis certains téléphones.",
"main_menu.title": "Breakout 71",
@ -100,42 +99,33 @@
"main_menu.unlocks_help": "Essayez les éléments débloqués",
"play.close_modale_window_tooltip": "Fermer",
"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.missed_ball": "raté",
"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_help": "Retour au menu principal",
"premium.buy": "Acheter une clé de licence",
"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.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_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.next_step_title": "Achetez des améliorations et passez au niveau suivant",
"premium.pick_level": "Accédez au niveau « {{name}} » pour $ {{cost}}",
"premium.pick_perk": "Obtenez {{name}}pour{{cost}}$",
"premium.short_help": "Jouez le plus longtemps possible",
"premium.upgrade_perk_to_level": "Passez de {{name}} à {{lvl}} pour{{cost}}$",
"premium.your_upgrades": "Vos mises à jour jusqu'à présent :",
"premium.per_hours": "Vous avez passé {{hours}} heures à jouer",
"premium.per_hours_help": "Donnez 4.99€ pour être premium",
"premium.thanks": "",
"premium.thanks_help": "",
"premium.title": "",
"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.start": "Démarrer la partie de test",
"sandbox.title": "Mode bac à sable",
"sandbox.unlocks_at": "Déverrouillé à partir d'un score total de {{score}}",
"score_panel.restart": "Redémarrer",
"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.bebuffs_list": "Handicapes : ",
"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_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.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.",

View file

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

View file

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

View file

@ -19,11 +19,21 @@ export const options = {
name: t("main_menu.basic"),
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: {
default: false,
name: t("main_menu.show_fps"),
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: {
default: false,
name: t("main_menu.pointer_lock"),

View file

@ -1,9 +1,9 @@
import { GameState } from "./types";
import { icons } from "./loadGameData";
import { t } from "./i18n/i18n";
import { getSettingValue, setSettingValue } from "./settings";
import { asyncAlert } from "./asyncAlert";
import { confirmRestart, openMainMenu, restart } from "./game";
import {GameState} from "./types";
import {icons} from "./loadGameData";
import {t} from "./i18n/i18n";
import {getSettingValue, setSettingValue} from "./settings";
import {asyncAlert} from "./asyncAlert";
import {openMainMenu} from "./game";
const publicKeyString = `-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyGQJgs6gxa0Fd86TuZ2q
@ -21,49 +21,49 @@ dZpWPx3O+rZbRQb0gHcvN4+n2Y7fWAeC9mxVZtADqvVr/GTumMbLj7DdhWtt1Ogu
-----END PUBLIC KEY-----`;
function pemToArrayBuffer(pem: string) {
const b64 = pem
.replace(/-----BEGIN PUBLIC KEY-----/, "")
.replace(/-----END PUBLIC KEY-----/, "")
.replace(/\s+/g, "");
const binaryDerString = atob(b64);
const binaryDer = new Uint8Array(binaryDerString.length);
for (let i = 0; i < binaryDerString.length; i++) {
binaryDer[i] = binaryDerString.charCodeAt(i);
}
return binaryDer.buffer;
const b64 = pem
.replace(/-----BEGIN PUBLIC KEY-----/, "")
.replace(/-----END PUBLIC KEY-----/, "")
.replace(/\s+/g, "");
const binaryDerString = atob(b64);
const binaryDer = new Uint8Array(binaryDerString.length);
for (let i = 0; i < binaryDerString.length; i++) {
binaryDer[i] = binaryDerString.charCodeAt(i);
}
return binaryDer.buffer;
}
async function getPriceId(key: string, pem: string) {
// Split the key into its components
const [priceId, timestamp, signature] = key.split(":");
const data = `${priceId}:${timestamp}`;
// Split the key into its components
const [priceId, timestamp, signature] = key.split(":");
const data = `${priceId}:${timestamp}`;
const publicKeyBuffer = pemToArrayBuffer(pem);
const publicKeyBuffer = pemToArrayBuffer(pem);
const publicKey = await crypto.subtle.importKey(
"spki",
publicKeyBuffer,
{
name: "RSA-PSS",
hash: "SHA-256",
},
true,
["verify"],
);
const publicKey = await crypto.subtle.importKey(
"spki",
publicKeyBuffer,
{
name: "RSA-PSS",
hash: "SHA-256",
},
true,
["verify"],
);
// Verify the signature using ECDSA
const isValid = await crypto.subtle.verify(
{
name: "RSA-PSS",
saltLength: 32,
},
publicKey,
new Uint8Array(Array.from(atob(signature), (c) => c.charCodeAt(0))),
new TextEncoder().encode(data),
);
if (!isValid) throw new Error("Invalid key signature");
// Verify the signature using ECDSA
const isValid = await crypto.subtle.verify(
{
name: "RSA-PSS",
saltLength: 32,
},
publicKey,
new Uint8Array(Array.from(atob(signature), (c) => c.charCodeAt(0))),
new TextEncoder().encode(data),
);
if (!isValid) throw new Error("Invalid key signature");
return priceId;
return priceId;
}
let premium = false;
@ -71,94 +71,116 @@ const gamePriceId = "price_1R6YaEGRf74lr2EkSo2GPvuO";
checkKey(getSettingValue("license", "")).then();
async function checkKey(key: string) {
if (!key) return "No key";
try {
if (gamePriceId !== (await getPriceId(key, publicKeyString))) {
return "Wrong product";
if (!key) return "No key";
try {
if (gamePriceId !== (await getPriceId(key, publicKeyString))) {
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() {
return premium;
return premium;
}
export function premiumMenuEntry(gameState: GameState) {
if (isPremium()) {
return {
icon: icons["icon:adventure_mode"],
text: t("premium.adventure_mode"),
help: t("premium.adventure_mode_help"),
value: async () => {
if (await confirmRestart(gameState)) {
restart({
adventure: true,
});
if (isPremium()) {
return {
icon: icons["icon:premium_active"],
text: t("premium.thanks"),
help: t("premium.thanks_help"),
value: async () => {
navigator.clipboard.writeText(getSettingValue('license', ''))
openMainMenu()
},
};
}
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") ===
"com.android.vending";
const cb = await asyncAlert({
title: t("premium.adventure_mode"),
content: [
text ||
(isGooglePlayInstall && t("premium.help_google")) ||
t("premium.help"),
{
text: t("premium.buy"),
disabled: isGooglePlayInstall,
help: isGooglePlayInstall
? t("premium.buy_disabled_help")
: t("premium.buy_help"),
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(
/\s+/g,
"",
);
const problem = await checkKey(value);
if (problem) {
openPremiumMenu(problem).then();
} else {
setSettingValue("license", value);
openMainMenu().then();
}
},
},
{
text: t("premium.back"),
help: t("premium.back_help"),
value() {
openMainMenu().then();
},
},
],
});
if (cb) cb();
async function openPremiumMenu(text) {
const cb = await asyncAlert({
title: t("premium.title"),
content: [
text ||
(isGooglePlayInstall && t("premium.help_google")) ||
t("premium.help"),
{
text: t("premium.buy"),
disabled: isGooglePlayInstall,
help: isGooglePlayInstall
? t("premium.buy_disabled_help")
: t("premium.buy_help"),
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(
/\s+/g,
"",
);
const problem = await checkKey(value);
if (problem) {
openPremiumMenu(problem).then();
} else {
setSettingValue("license", value);
openMainMenu().then();
}
},
},
{
text: t("premium.back"),
help: t("premium.back_help"),
value() {
openMainMenu().then();
},
},
],
});
if (cb) cb();
}

View file

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

View file

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

14
src/types.d.ts vendored
View file

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

View file

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