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

@ -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,
@ -1053,4 +1046,4 @@
"svg": null,
"color": ""
}
]
]

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",