Adventure mode wip, not really fun

This commit is contained in:
Renan LE CARO 2025-03-27 10:52:31 +01:00
parent 6cf8fabf16
commit 59ef24c865
26 changed files with 1482 additions and 676 deletions

View file

@ -34,30 +34,6 @@ The score is your "fuel", and lets you pick the next level from a list. Each lev
Each downgrade acts as a score multiplier.
Your goal is no longer to score higher, but to go farther
# Challenges
Possible challenges :
- Add negative coins that make the coin magnet less usage
- add negative bricks that clear coins and reset combo
- add a brick eating enemy that forces you to play fast
- add a force field for 10s that negates hots start
- other perks can be randomly turned off
- ball keeps accelerating until unplayable
- 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
- add red anti-coins that apply downgrades
- destroy your combo
- hurt your score
- behave like heavier coins.
- deactivate a perk for this level
- reduce your number of coins
- destroy all coins on screen
- lowers your combo
- reduce your choice for your next perk
# System requirements

View file

@ -11,8 +11,8 @@ android {
applicationId = "me.lecaro.breakout"
minSdk = 21
targetSdk = 34
versionCode = 29049575
versionName = "29049575"
versionCode = 29050375
versionName = "29050375"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true

File diff suppressed because one or more lines are too long

745
dist/index.html vendored

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,5 @@
// The version of the cache.
const VERSION = "29049575";
const VERSION = "29050375";
// The name of the cache
const CACHE_NAME = `breakout-71-${VERSION}`;

View file

@ -1,110 +1,159 @@
import {GameState, PerksMap} from "./types";
import {sample, sumOfValues} from "./game_utils";
import {allLevels, icons, upgrades} from "./loadGameData";
import {t} from "./i18n/i18n";
import {hashCode} from "./getLevelBackground";
import {requiredAsyncAlert} from "./asyncAlert";
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";
const MAX_DIFFICULTY = 3+4
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) {
let maxDifficulty = 3;
// 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) {
maxDifficulty++;
levelChoiceCount++;
}
if (gameState.levelTime < 30 * 1000) {
maxDifficulty++;
levelChoiceCount++;
}
if (catchRate === 1) {
maxDifficulty++;
levelChoiceCount++;
}
if (gameState.levelMisses === 0) {
maxDifficulty++;
levelChoiceCount++;
}
let actions = range(0, maxDifficulty).map(difficulty => getPerksForPath(gameState.score, gameState.currentLevel, gameState.seed, gameState.adventurePath, gameState.perks, difficulty))
let perkChoices = 2 + gameState.perks.one_more_choice + levelChoiceCount;
return requiredAsyncAlert({
title: 'Choose your next step',
text: 'Click one of the options below to continue',
actions,
})
}
const priceMultiplier =
1 +
Math.ceil(
gameState.currentLevel * Math.pow(1.05, gameState.currentLevel) * 10,
);
function getPerksForPath(score: number, currentLevel: number, seed: string, path: string, basePerks: PerksMap, difficulty: number) {
const hashSeed = seed + path
let cost = (1 + difficulty) * Math.pow(1.5, currentLevel) * 10
if (!difficulty && cost > score) {
cost = score
}
const levels = allLevels
.sort((a, b) => hashCode(hashSeed + a.name) - hashCode(hashSeed + b.name))
.slice(0, MAX_DIFFICULTY)
.sort((a,b)=>a.size-b.size)
let level = levels[difficulty]
let text = level.name + ' $' + cost, help = []
let perks = {}
// TODO exclude irrelevant perks
upgrades
.filter((u) => !u?.requires || basePerks[u?.requires])
.filter(u => basePerks[u.id] < u.max)
.sort((a, b) => hashCode(hashSeed + difficulty+a.id) - hashCode(hashSeed + difficulty+b.id))
.slice(0, difficulty+1)
.forEach(u => {
perks[u.id] = basePerks[u.id] + 1
help.push(u.name +
(basePerks[u.id]
? t("level_up.upgrade_perk_to_level", {
level: basePerks[u.id] + 1,
})
: ""))
})
let totalPerksValue = sumOfValues({...basePerks, ...perks})
let targetPerks = 10 + difficulty * 3
let toRemove = Math.max(0, totalPerksValue - targetPerks)
while (toRemove) {
const possibleDowngrades = Object.keys(basePerks).filter(
k => !perks[k] && basePerks[k] > 0
)
if (!possibleDowngrades.length) {
break
}
const downGraded = sample(possibleDowngrades)
perks[downGraded] = basePerks[downGraded] - 1
if (!perks[downGraded]) {
help.push(t('level_up.perk_loss') + upgrades.find(u => u.id == downGraded)?.help(1))
} else {
help.push(t('level_up.downgrade') + upgrades.find(u => u.id == downGraded)?.help(perks[downGraded]))
}
toRemove--
}
return {
value: {
cost,
level,
perks,
difficulty
},
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],
disabled: cost > score,
text, help: help.join('/') || 'No change to perks',
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;
}
}
}
function range(start: number, end: number): number[] {
const result = []
for (let i = start; i < end; i++) result.push(i)
return result
}

View file

@ -27,26 +27,20 @@ let lastClickedItemIndex = -1;
export function requiredAsyncAlert<t>(p: {
title?: string;
text?: string;
actions?: AsyncAlertAction<t>[];
textAfterButtons?: string;
content: (string | AsyncAlertAction<t>)[];
actionsAsGrid?: boolean;
}):Promise<t>{
return asyncAlert({...p, allowClose:false})
}): Promise<t> {
return asyncAlert({ ...p, allowClose: false });
}
export async function asyncAlert<t>({
title,
text,
actions,
content = [],
allowClose = true,
textAfterButtons = "",
actionsAsGrid = false,
}: {
title?: string;
text?: string;
actions?: AsyncAlertAction<t>[];
textAfterButtons?: string;
content: (string | AsyncAlertAction<t>)[];
allowClose?: boolean;
actionsAsGrid?: boolean;
}): Promise<t | void> {
@ -57,6 +51,7 @@ export async function asyncAlert<t>({
const popup = document.createElement("div");
let closed = false;
function closeWithResult(value: t | undefined) {
if (closed) return;
closed = true;
@ -79,25 +74,39 @@ export async function asyncAlert<t>({
}
if (title) {
const p = document.createElement("h2");
p.innerHTML = title;
popup.appendChild(p);
const h2 = document.createElement("h2");
h2.innerHTML = title;
popup.appendChild(h2);
}
if (text) {
const p = document.createElement("div");
p.innerHTML = text;
popup.appendChild(p);
}
const buttons = document.createElement("section");
buttons.className = "actions";
popup.appendChild(buttons);
actions
content
?.filter((i) => i)
.forEach(
({ text, value, help, disabled, className = "", icon = "" }, index) => {
.forEach((entry, index) => {
if (typeof entry == "string") {
const p = document.createElement("div");
p.innerHTML = entry;
popup.appendChild(p);
return;
}
let addto: HTMLElement;
if (popup.lastChild?.nodeName == "SECTION") {
addto = popup.lastChild as HTMLElement;
} else {
addto = document.createElement("section");
addto.className = "actions";
popup.appendChild(addto);
}
const {
text,
value,
help,
disabled,
className = "",
icon = "",
} = entry;
const button = document.createElement("button");
button.innerHTML = `
@ -121,15 +130,9 @@ ${icon}
button.className =
className + (lastClickedItemIndex === index ? " needs-focus" : "");
buttons.appendChild(button);
},
);
if (textAfterButtons) {
const p = document.createElement("div");
p.className = "textAfterButtons";
p.innerHTML = textAfterButtons;
popup.appendChild(p);
}
addto.appendChild(button);
});
popupWrap.appendChild(popup);
(

View file

@ -1034,8 +1034,8 @@
},
{
"name": "icon:adventure_mode",
"size": 17,
"bricks": "________aaaaaaaaa_______a_____________aaaabbbbbbbbb___a___t___________a_____ttttttttt__a________________a_____sssssssss__a____s_________WWWsssssrrrrrrrrr__c____R___________c_____RRRRRRRRR__c________________c_____kkkkkkkkk___c___k_____________ccccGGGGGGGGG_______c_________________ccccccccc",
"size": 11,
"bricks": "__________________________________ttt___bbb_bttttbbbbbbbb__tbt__bbbbbbbbttttb_bbb___ttt__________________________________",
"svg": null,
"color": ""
},

View file

@ -1 +1 @@
"29049575"
"29050375"

64
src/debuffs.ts Normal file
View file

@ -0,0 +1,64 @@
import { t } from "./i18n/i18n";
export const debuffs = [
{
id: "negative_coins",
max: 20,
name: t("debuffs.negative_coins.name"),
help: (lvl: number) => t("debuffs.negative_coins.help", { lvl }),
},
{
id: "negative_bricks",
max: 20,
name: t("debuffs.negative_bricks.name"),
help: (lvl: number) => t("debuffs.negative_bricks.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;
/*
Possible challenges :
- add a force field for 10s that negates hots start
- other perks can be randomly turned off
- ball keeps accelerating until unplayable
- 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
- add red anti-coins that apply downgrades
- destroy your combo
- hurt your score
- behave like heavier coins.
- deactivate a perk for this level
- reduce your number of coins
- destroy all coins on screen
- lowers your combo
- reduce your choice for your next perk
*/

View file

@ -120,19 +120,13 @@ body:not(.has-alert-open) #popup {
& > * {
padding: 0;
margin: 0;
}
& > h2,
& > p {
margin-bottom: 20px;
margin: 0 0 20px 0;
}
& > section {
display: flex;
flex-direction: column;
align-items: stretch;
margin-top: 20px;
button {
font: inherit;
@ -350,3 +344,11 @@ h2.histogram-title {
h2.histogram-title strong {
color: #4049ca;
}
.red-icon {
background: red;
img {
filter: saturate(0);
mix-blend-mode: luminosity;
}
}

View file

@ -58,7 +58,8 @@ import {
alertsOpen,
asyncAlert,
AsyncAlertAction,
closeModal, requiredAsyncAlert,
closeModal,
requiredAsyncAlert,
} from "./asyncAlert";
import { isOptionOn, options, toggleOption } from "./options";
import { hashCode } from "./getLevelBackground";
@ -228,26 +229,32 @@ export async function openShortRunUpgradesPicker(gameState: GameState) {
}
while (repeats--) {
const actions = pickRandomUpgrades(
const actions: Array<{
text: string;
icon: string;
value: PerkId | "reroll";
help: string;
}> = pickRandomUpgrades(
gameState,
3 + gameState.perks.one_more_choice - gameState.perks.instant_upgrade,
);
if (!actions.length) break;
if (gameState.rerolls) {
if (gameState.rerolls)
actions.push({
text: t("level_up.reroll", { count: gameState.rerolls }),
help: t("level_up.reroll_help"),
value: "reroll",
value: "reroll" as const,
icon: icons["icon:reroll"],
});
}
if (!actions.length) break;
let textAfterButtons = `
<p>${t("level_up.after_buttons", {
level: gameState.currentLevel + 1,
max: max_levels(gameState),
})} </p>
<p>${pickedUpgradesHTMl(gameState)}</p>
${pickedUpgradesHTMl(gameState)}
<div id="level-recording-container"></div>
`;
@ -266,8 +273,8 @@ export async function openShortRunUpgradesPicker(gameState: GameState) {
title:
t("level_up.pick_upgrade_title") +
(repeats ? " (" + (repeats + 1) + ")" : ""),
actions,
text: `<p>${t("level_up.before_buttons", {
content: [
`<p>${t("level_up.before_buttons", {
score: gameState.score - gameState.levelStartScore,
catchGain,
levelSpawnedCoins: gameState.levelSpawnedCoins,
@ -283,8 +290,10 @@ export async function openShortRunUpgradesPicker(gameState: GameState) {
<p>${levelsListHTMl(gameState)}</p>
`,
...actions,
textAfterButtons,
}) ;
],
});
if (upgradeId === "reroll") {
repeats++;
@ -437,18 +446,26 @@ document.addEventListener("visibilitychange", () => {
async function openScorePanel() {
pause(true);
const cb = await asyncAlert({
title: t("score_panel.title", {
title: gameState.isAdventureMode
? t("score_panel.title_adventure", {
score: gameState.score,
level: gameState.currentLevel + 1,
max: max_levels(gameState),
})
: t("score_panel.title", {
score: gameState.score,
level: gameState.currentLevel + 1,
max: max_levels(gameState),
}),
text: `
content: [
`
${gameState.isCreativeModeRun ? `<p>${t("score_panel.test_run")}</p>` : ""}
<p>${t("score_panel.upgrades_picked")}</p>
<p>${pickedUpgradesHTMl(gameState)}</p>
${pickedUpgradesHTMl(gameState)}
<p>${levelsListHTMl(gameState)}</p>
`,
],
allowClose: true,
});
}
@ -500,9 +517,9 @@ export async function openMainMenu() {
while (
(choice = await asyncAlert<"start" | Upgrade>({
title: t("sandbox.title"),
text: t("sandbox.instructions"),
actionsAsGrid: true,
actions: [
content: [
t("sandbox.instructions"),
...upgrades.map((u) => ({
icon: u.icon,
text: u.name,
@ -545,10 +562,8 @@ export async function openMainMenu() {
const cb = await asyncAlert<() => void>({
title: t("main_menu.title"),
text: ``,
content: [...actions, t("main_menu.footer_html", { appVersion })],
allowClose: true,
actions,
textAfterButtons: t("main_menu.footer_html", { appVersion }),
});
if (cb) {
cb();
@ -608,8 +623,8 @@ async function openSettingsMenu() {
if (
await asyncAlert({
title: t("main_menu.reset"),
text: t("main_menu.reset_instruction"),
actions: [
content: [
t("main_menu.reset_instruction"),
{
text: t("main_menu.reset_confirm"),
value: true,
@ -736,16 +751,20 @@ async function openSettingsMenu() {
}
await asyncAlert({
title: t("main_menu.save_file_loaded"),
text: t("main_menu.save_file_loaded_help"),
actions: [{ text: t("main_menu.save_file_loaded_ok") }],
content: [
t("main_menu.save_file_loaded_help"),
{ text: t("main_menu.save_file_loaded_ok") },
],
});
window.location.reload();
}
} catch (e: any) {
await asyncAlert({
title: t("main_menu.save_file_error"),
text: e.message,
actions: [{ text: t("main_menu.save_file_loaded_ok") }],
content: [
e.message,
{ text: t("main_menu.save_file_loaded_ok") },
],
});
}
input.value = "";
@ -762,8 +781,8 @@ async function openSettingsMenu() {
async value() {
const pick = await asyncAlert({
title: t("main_menu.language"),
text: t("main_menu.language_help"),
actions: [
content: [
t("main_menu.language_help"),
{
text: "English",
value: "en",
@ -803,16 +822,10 @@ async function openSettingsMenu() {
},
});
// actions.push({
// text: t("main_menu.resume"),
// help: t("main_menu.resume_help"),
// value() {},
// });
const cb = await asyncAlert<() => void>({
title: t("main_menu.settings_title"),
text: t("main_menu.settings_help"),
content: [t("main_menu.settings_help"), ...actions],
allowClose: true,
actions,
});
if (cb) {
cb();
@ -857,14 +870,15 @@ async function openUnlocksList() {
);
const tryOn = await asyncAlert<RunParams>({
title: t("unlocks.title", { percentUnlock }),
text: `<p>${t("unlocks.intro", { ts })}
${percentUnlock < 100 ? t("unlocks.greyed_out_help") : ""}</p>
`,
textAfterButtons: `<p>
content: [
`<p>${t("unlocks.intro", { ts })}
${percentUnlock < 100 ? t("unlocks.greyed_out_help") : ""}</p> `,
...actions,
`<p>
Your high score is ${gameState.highScore}.
Click an item above to start a run with it.
</p>`,
actions,
],
allowClose: true,
actionsAsGrid: true,
});
@ -880,8 +894,8 @@ export async function confirmRestart(gameState) {
return asyncAlert({
title: t("confirmRestart.title"),
text: t("confirmRestart.text"),
actions: [
content: [
t("confirmRestart.text"),
{
value: true,
text: t("confirmRestart.yes"),
@ -1005,13 +1019,21 @@ restart(
metamorphosis: 1,
implosions: 1,
},
}) ||(window.location.search.includes("adventure") && {
adventure:true,
}) ||
(window.location.search.includes("adventure") && {
adventure: true,
perks: {
pierce:5
// pierce:15
},
}) || {},
debuffs: {
// side_wind:20
// negative_bricks:3,
// negative_coins:5,
// void_coins_on_touch: 1,
// void_brick_on_touch: 1,
},
}) ||
{},
);
tick();

View file

@ -117,29 +117,34 @@ export function gameOver(title: string, intro: string) {
asyncAlert({
allowClose: true,
title,
text: `
content: [
`
${gameState.isCreativeModeRun ? `<p>${t("gameOver.test_run")}</p> ` : ""}
<p>${intro}</p>
<p>${t("gameOver.cumulative_total", { startTs, endTs })}</p>
${unlocksInfo}
`,
actions: [
{
value: null,
text: t("gameOver.restart"),
help: "",
},
],
textAfterButtons: `<div id="level-recording-container"></div>
<p>${t("gameOver.upgrades_picked")}</p>
<p>${pickedUpgradesHTMl(gameState)}</p>
`<div id="level-recording-container"></div>
${pickedUpgradesHTMl(gameState)}
${getHistograms()}
`,
}).then(() => restart({ levelToAvoid: currentLevelInfo(gameState).name }));
],
}).then(() =>
restart({
levelToAvoid: currentLevelInfo(gameState).name,
adventure: gameState.isAdventureMode,
}),
);
}
export function getHistograms() {
let runStats = "";
// TODO separate adventure and normal runs
try {
// Stores only top 100 runs
let runsHistory = JSON.parse(

View file

@ -134,7 +134,7 @@ export function normalizeGameState(gameState: GameState) {
gameState.baseSpeed = Math.max(
3,
gameState.gameZoneWidth / 12 / 10 +
gameState.currentLevel / 3 +
gameState.currentLevel / (gameState.isAdventureMode ? 30 : 3) +
gameState.levelTime / (30 * 1000) -
gameState.perks.slow_down * 2,
);
@ -342,9 +342,14 @@ export function explodeBrick(
const color = gameState.bricks[index];
if (!color) return;
if (color === "black") {
if (color === "black" || color === "transparent") {
const x = brickCenterX(gameState, index),
y = brickCenterY(gameState, index);
if (color === "transparent") {
schedulGameSound(gameState, "void", x, 1);
resetCombo(gameState, x, y);
}
setBrick(gameState, index, "");
explosionAt(gameState, index, x, y, ball);
} else if (color) {
@ -444,8 +449,9 @@ export function explodeBrick(
if (
gameState.perks.nbricks &&
ball.brokenSinceBounce == gameState.perks.nbricks + 1
ball.brokenSinceBounce > gameState.perks.nbricks
) {
// We need to reset at each hit, otherwise it's just an OP version of single puck hit streak
resetCombo(gameState, ball.x, ball.y);
}
@ -492,7 +498,9 @@ export function pickRandomUpgrades(gameState: GameState, count: number) {
let list = getPossibleUpgrades(gameState)
.map((u) => ({
...u,
score: gameState.isAdventureMode ? 0:Math.random() + (gameState.lastOffered[u.id] || 0),
score: gameState.isAdventureMode
? 0
: Math.random() + (gameState.lastOffered[u.id] || 0),
}))
.sort((a, b) => a.score - b.score)
.filter((u) => gameState.perks[u.id] < u.max)
@ -536,7 +544,11 @@ export function addToScore(gameState: GameState, coin: Coin) {
gameState.lastScoreIncrease = gameState.levelTime;
addToTotalScore(gameState, coin.points);
if (gameState.score > gameState.highScore && !gameState.isCreativeModeRun) {
if (
gameState.score > gameState.highScore &&
!gameState.isCreativeModeRun &&
!gameState.isAdventureMode
) {
gameState.highScore = gameState.score;
localStorage.setItem("breakout-3-hs", gameState.score.toString());
}
@ -554,10 +566,10 @@ export function addToScore(gameState: GameState, coin: Coin) {
);
}
if (Date.now() - gameState.lastPlayedCoinGrab > 16) {
gameState.lastPlayedCoinGrab = Date.now();
if (coin.points > 0) {
schedulGameSound(gameState, "coinCatch", coin.x, 1);
} else {
resetCombo(gameState, coin.x, coin.y);
}
gameState.runStatistics.score += coin.points;
if (gameState.perks.asceticism) {
@ -575,22 +587,16 @@ export async function setLevel(gameState: GameState, l: number) {
pause(false);
stopRecording();
if (l > 0) {
if (gameState.isCreativeModeRun) {
const { cost,
level,
perks,difficulty} = await openAdventureRunUpgradesPicker(gameState);
gameState.score-=cost
gameState.runLevels[l] = level
gameState.adventurePath += difficulty
Object.assign(gameState.perks, perks)
if (gameState.isAdventureMode) {
await openAdventureRunUpgradesPicker(gameState);
} else {
await openShortRunUpgradesPicker(gameState);
}
}
gameState.currentLevel = l;
// That list is populated just before if you're in adventure mode
gameState.level = gameState.runLevels[l];
gameState.levelTime = 0;
gameState.winAt = 0;
gameState.levelWallBounces = 0;
@ -628,6 +634,22 @@ export async function setLevel(gameState: GameState, l: number) {
setBrick(gameState, i, lvl.bricks[i]);
}
if (gameState.debuffs.negative_bricks) {
let attemps = 0;
let changed = 0;
while (attemps < 100 && changed < gameState.debuffs.negative_bricks) {
attemps++;
const index = Math.floor(Math.random() * gameState.bricks.length);
if (
gameState.bricks[index] &&
gameState.bricks[index] !== "transparent"
) {
gameState.bricks[index] = "transparent";
gameState.brickHP[index] = 1;
changed++;
}
}
}
// Balls color will depend on most common brick color sometimes
resetBalls(gameState);
gameState.needsRender = true;
@ -640,6 +662,7 @@ function setBrick(gameState: GameState, index: number, color: string) {
gameState.bricks[index] = color || "";
gameState.brickHP[index] =
(color === "black" && 1) ||
(color === "transparent" && 1) ||
(color && 1 + gameState.perks.sturdy_bricks) ||
0;
}
@ -915,7 +938,10 @@ export function gameStateTick(
!remainingBricks &&
!liveCount(gameState.coins))
) {
if (gameState.currentLevel + 1 < max_levels(gameState)) {
if (
gameState.isAdventureMode ||
gameState.currentLevel + 1 < max_levels(gameState)
) {
if (gameState.running) {
setLevel(gameState, gameState.currentLevel + 1);
}
@ -998,8 +1024,9 @@ export function gameStateTick(
coin.y < gameState.gameZoneHeight + gameState.puckHeight + coin.vy &&
Math.abs(coin.x - gameState.puckPosition) <
coinRadius +
gameState.puckWidth / 2 + // a bit of margin to be nice
gameState.puckHeight
gameState.puckWidth / 2 +
// a bit of margin to be nice , negative in case it's a negative coin
gameState.puckHeight * (coin.points ? 1 : -1)
) {
addToScore(gameState, coin);
destroy(gameState.coins, coinIndex);
@ -1018,8 +1045,27 @@ export function gameStateTick(
}
const hitBrick = coinBrickHitCheck(gameState, coin);
if (gameState.perks.metamorphosis && typeof hitBrick !== "undefined") {
if (
gameState.debuffs.void_coins_on_touch &&
coin.points &&
typeof hitBrick !== "undefined" &&
gameState.bricks[hitBrick] == "transparent"
) {
coin.points = 0;
coin.color = "transparent";
schedulGameSound(gameState, "void", coin.x, 1);
} else if (
gameState.debuffs.void_brick_on_touch &&
!coin.points &&
typeof hitBrick !== "undefined" &&
gameState.bricks[hitBrick] !== "transparent"
) {
setBrick(gameState, hitBrick, "transparent");
schedulGameSound(gameState, "void", coin.x, 1);
} else if (
gameState.perks.metamorphosis &&
typeof hitBrick !== "undefined"
) {
if (
gameState.bricks[hitBrick] &&
coin.color !== gameState.bricks[hitBrick] &&
@ -1053,6 +1099,49 @@ export function gameStateTick(
gameState.balls.forEach((ball) => ballTick(gameState, ball, frames));
if (
!isOptionOn("basic") &&
gameState.debuffs.downward_wind &&
gameState.levelTime / 1000 < gameState.debuffs.downward_wind
) {
makeParticle(
gameState,
gameState.offsetXRoundedDown +
Math.random() * gameState.gameZoneWidthRoundedUp,
gameState.gameZoneHeight * Math.random(),
0,
gameState.baseSpeed,
"red",
true,
gameState.coinSize / 2,
150,
);
}
if (gameState.debuffs.side_wind) {
const dir =
(gameState.currentLevel % 2 ? -1 : 1) *
gameState.debuffs.side_wind *
gameState.baseSpeed;
gameState.balls.forEach((ball) => {
ball.vx += dir / 2000;
});
forEachLiveOne(gameState.coins, (c) => (c.vx += dir / 100));
if (!isOptionOn("basic"))
makeParticle(
gameState,
gameState.offsetXRoundedDown +
Math.random() * gameState.gameZoneWidthRoundedUp,
gameState.gameZoneHeight * Math.random(),
dir / 3,
0,
"red",
true,
gameState.coinSize / 2,
150,
);
}
if (gameState.perks.shocks) {
gameState.balls.forEach((a, ai) =>
gameState.balls.forEach((b, bi) => {
@ -1219,6 +1308,7 @@ export function gameStateTick(
}
}
forEachLiveOne(gameState.particles, (p, pi) => {
if (gameState.levelTime > p.time + p.duration) {
destroy(gameState.particles, pi);
@ -1247,6 +1337,13 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
gameState.perks.puck_repulse_ball +
gameState.perks.ball_attract_ball;
if (
gameState.debuffs.downward_wind &&
gameState.levelTime / 1000 < gameState.debuffs.downward_wind
) {
ball.vy += gameState.baseSpeed / 50;
speedLimitDampener += 10;
}
if (isTelekinesisActive(gameState, ball)) {
speedLimitDampener += 3;
ball.vx +=
@ -1606,6 +1703,10 @@ function makeCoin(
color = "gold",
points = 1,
) {
if (gameState.debuffs.negative_coins > Math.random() * 100) {
points = 0;
color = "transparent";
}
append(gameState.coins, (p: Partial<Coin>) => {
p.x = x;
p.y = y;

View file

@ -63,9 +63,12 @@ export function pickedUpgradesHTMl(gameState: GameState) {
for (let i = 0; i < gameState.perks[u.id]; i++)
list += `<span title="${u.name}">${icons["icon:" + u.id]}</span>`;
}
return list;
if (!list) return "";
return ` <p>${t("score_panel.upgrades_picked")}</p> <p>${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++) {
@ -75,9 +78,7 @@ export function levelsListHTMl(gameState: GameState) {
}
export function currentLevelInfo(gameState: GameState) {
return gameState.runLevels[
gameState.currentLevel % gameState.runLevels.length
];
return gameState.level;
}
export function isTelekinesisActive(gameState: GameState, ball: Ball) {
@ -126,6 +127,7 @@ export function defaultSounds() {
lifeLost: { vol: 0, x: 0 },
coinCatch: { vol: 0, x: 0 },
colorChange: { vol: 0, x: 0 },
void: { vol: 0, x: 0 },
},
};
}

View file

@ -84,6 +84,221 @@
</concept_node>
</children>
</folder_node>
<folder_node>
<name>debuffs</name>
<children>
<folder_node>
<name>downward_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>negative_bricks</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>negative_coins</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>true</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>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>
</folder_node>
<folder_node>
<name>gameOver</name>
<children>
@ -512,36 +727,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>downgrade</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>perk_loss</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>pick_upgrade_title</name>
<description/>
@ -1422,6 +1607,21 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>current_lvl_adventure</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>menu_label</name>
<description/>
@ -1577,6 +1777,51 @@
</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/>
@ -1637,6 +1882,51 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>next_step_title</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>pick_level</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>pick_perk</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>short_help</name>
<description/>
@ -1653,7 +1943,7 @@
</translations>
</concept_node>
<concept_node>
<name>title</name>
<name>upgrade_perk_to_level</name>
<description/>
<comment/>
<translations>
@ -1663,8 +1953,23 @@
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>your_upgrades</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>
@ -1842,6 +2147,21 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>title_adventure</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>upcoming_levels</name>
<description/>

View file

@ -3,6 +3,18 @@
"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",
"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",
@ -30,8 +42,6 @@
"level_up.compliment_advice": "Try to catch all coins, never miss the bricks, never hit the walls/ceiling or clear the level under 30s to gain additional choices and upgrades.",
"level_up.compliment_good": "Well done !",
"level_up.compliment_perfect": "Impressive, keep it up !",
"level_up.downgrade": "Downgrade :",
"level_up.perk_loss": "Perk lost : ",
"level_up.pick_upgrade_title": "Pick an upgrade",
"level_up.plus_one_choice": "(+1 re-roll)",
"level_up.plus_one_upgrade": "(+1 upgrade and +1 re-roll)",
@ -90,22 +100,30 @@
"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.menu_label": "menu",
"play.missed_ball": "miss",
"play.mobile_press_to_play": "Press and hold here to play",
"premium.adventure_mode": "Adventure mode",
"premium.adventure_mode_help": "Start a new game in adventure mode",
"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_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.title": "Adventure mode",
"premium.upgrade_perk_to_level": "Upgrade {{name}} to {{lvl}} for ${{cost}}",
"premium.your_upgrades": "Your upgrades so far : ",
"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",
@ -117,6 +135,7 @@
"score_panel.resume_help": "Return to your run",
"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.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,6 +3,18 @@
"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",
"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",
@ -30,8 +42,6 @@
"level_up.compliment_advice": "Essayez d'attraper toutes les pièces, de ne jamais rater les briques, de ne pas toucher les murs ou de terminer le niveau en moins de 30 secondes pour obtenir des choix supplémentaires et des améliorations.",
"level_up.compliment_good": "Bravo !",
"level_up.compliment_perfect": "Impressionnant, continuez comme ça !",
"level_up.downgrade": "",
"level_up.perk_loss": "",
"level_up.pick_upgrade_title": "Choisir une amélioration",
"level_up.plus_one_choice": "(+1 re-roll)",
"level_up.plus_one_upgrade": "(+1 amélioration et +1 re-roll)",
@ -90,22 +100,30 @@
"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.menu_label": "Menu",
"play.missed_ball": "raté",
"play.mobile_press_to_play": "Gardez le doigt ici pour jouer",
"premium.adventure_mode": "Mode aventure",
"premium.adventure_mode_help": "Démarrer une nouvelle partie en mode aventure",
"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_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.title": "Mode aventure",
"premium.upgrade_perk_to_level": "Passez de {{name}} à {{lvl}} pour{{cost}}$",
"premium.your_upgrades": "Vos mises à jour jusqu'à présent :",
"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",
@ -117,6 +135,7 @@
"score_panel.resume_help": "Fermer cette fenêtre pour retourner au jeu",
"score_panel.test_run": "Il s'agit d'une partie d'essai, le score n'est pas enregistré.",
"score_panel.title": "{{score}} points au niveau {{level}}/{{max}} ",
"score_panel.title_adventure": "{{score}} points au niveau {{level}} de l'aventure",
"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

@ -20,11 +20,13 @@ export const allLevels = rawLevelsList
.split("")
.map((c) => palette[c])
.slice(0, level.size * level.size);
const bricksCount = bricks.filter((i) => i).length;
const icon = levelIconHTML(bricks, level.size, level.color);
icons[level.name] = icon;
return {
...level,
bricks,
bricksCount,
icon,
svg: getLevelBackground(level),
};
@ -38,10 +40,12 @@ export const allLevels = rawLevelsList
: Math.round(
Math.min(Math.pow(10, 1 + (li + l.size) / 30) * 10, 5000) * li,
),
sortKey: ((Math.random() + 3) / 3.5) * l.bricks.filter((i) => i).length,
sortKey: ((Math.random() + 3) / 3.5) * l.bricksCount,
})) as Level[];
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,
})) as Upgrade[];

View file

@ -1,4 +1,4 @@
import { GameState, RunParams } from "./types";
import { DebuffsMap, GameState, RunParams } from "./types";
import { getTotalScore } from "./settings";
import { allLevels, upgrades } from "./loadGameData";
import {
@ -9,6 +9,7 @@ import {
} from "./game_utils";
import { dontOfferTooSoon, resetBalls } from "./gameStateMutators";
import { isOptionOn } from "./options";
import { debuffs } from "./debuffs";
export function newGameState(params: RunParams): GameState {
const totalScoreAtRunStart = getTotalScore();
@ -30,9 +31,11 @@ export function newGameState(params: RunParams): GameState {
const gameState: GameState = {
runLevels,
level: runLevels[0],
currentLevel: 0,
upgradesOfferedFor: -1,
perks,
debuffs: { ...emptyDebuffsMap(), ...(params?.debuffs || {}) },
puckWidth: 200,
baseSpeed: 12,
combo: 1,
@ -67,7 +70,6 @@ export function newGameState(params: RunParams): GameState {
levelStartScore: 0,
levelMisses: 0,
levelSpawnedCoins: 0,
lastPlayedCoinGrab: 0,
puckColor: "#FFF",
ballSize: 20,
coinSize: 14,
@ -102,13 +104,11 @@ export function newGameState(params: RunParams): GameState {
...defaultSounds(),
isAdventureMode: !!params?.adventure,
adventurePath: "",
seed: "Seed" + Math.random(),
rerolls: 0,
};
resetBalls(gameState);
if (!sumOfValues(gameState.perks)) {
if (!sumOfValues(gameState.perks) && !params?.adventure) {
const giftable = getPossibleUpgrades(gameState).filter((u) => u.giftable);
const randomGift =
(isOptionOn("easy") && "slow_down") ||
@ -123,3 +123,9 @@ export function newGameState(params: RunParams): GameState {
}
return gameState;
}
export function emptyDebuffsMap(): DebuffsMap {
const map = {};
debuffs.forEach((d) => (map[d.id] = 0));
return map as DebuffsMap;
}

View file

@ -104,7 +104,7 @@ export function premiumMenuEntry(gameState: GameState) {
}
return {
icon: icons["icon:premium"],
text: t("premium.title"),
text: t("premium.adventure_mode"),
help: t("premium.short_help"),
value: () => openPremiumMenu(""),
};
@ -116,12 +116,11 @@ async function openPremiumMenu(text) {
"com.android.vending";
const cb = await asyncAlert({
title: t("premium.title"),
text:
title: t("premium.adventure_mode"),
content: [
text ||
(isGooglePlayInstall && t("premium.help_google")) ||
t("premium.help"),
actions: [
{
text: t("premium.buy"),
disabled: isGooglePlayInstall,

View file

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

View file

@ -36,7 +36,11 @@ export function render(gameState: GameState) {
if (!width || !height) return;
if (gameState.currentLevel || gameState.levelTime) {
menuLabel.innerText = t("play.current_lvl", {
menuLabel.innerText = gameState.isAdventureMode
? t("play.current_lvl_adventure", {
level: gameState.currentLevel + 1,
})
: t("play.current_lvl", {
level: gameState.currentLevel + 1,
max: max_levels(gameState),
});
@ -171,6 +175,7 @@ export function render(gameState: GameState) {
coin.x,
coin.y,
(hasCombo && gameState.perks.asceticism && "red") ||
(!coin.points && "red") ||
level.color ||
"black",
coin.a,
@ -490,7 +495,8 @@ export function renderAllBricks() {
redBorderOnBricksWithWrongColor ||
redColorOnAllBricks ||
gameState.perks.reach ||
gameState.perks.zen
gameState.perks.zen ||
gameState.debuffs.negative_bricks
)
) {
offset = 0;
@ -541,6 +547,7 @@ export function renderAllBricks() {
!countBricksBelow(gameState, index);
let redBorder =
color === "transparent" ||
(gameState.ballsColor !== color &&
color !== "black" &&
redBorderOnBricksWithWrongColor) ||

View file

@ -49,6 +49,11 @@ export const sounds = {
if (!isOptionOn("sound")) return;
createSingleBounceSound(1200, pan, volume, 0.1, "triangle");
},
void: (volume: number, pan: number) => {
if (!isOptionOn("sound")) return;
createSingleBounceSound(1200, pan, volume, 0.5, "sawtooth");
createSingleBounceSound(600, pan, volume, 0.3, "sawtooth");
},
explode: (volume: number, pan: number, combo: number) => {
if (!isOptionOn("sound")) return;
createExplosionSound(pan);

20
src/types.d.ts vendored
View file

@ -1,5 +1,6 @@
import { rawUpgrades } from "./upgrades";
import { options } from "./options";
import { debuffs } from "./debuffs";
export type colorString = string;
@ -14,6 +15,7 @@ export type Level = {
name: string;
size: number;
bricks: colorString[];
bricksCount: number;
svg: string;
color: string;
threshold: number;
@ -25,6 +27,10 @@ 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;
@ -152,6 +158,12 @@ export type PerksMap = {
[k in PerkId]: number;
};
export type DebuffId = (typeof debuffs)[number]["id"];
export type DebuffsMap = {
[k in DebuffId]: number;
};
export type ReusableArray<T> = {
// All items below that index should not be destroyed
indexMin: number;
@ -190,10 +202,13 @@ export type GameState = {
// 10 levels selected randomly at start for the run
runLevels: Level[];
// Current level displayed
level: Level;
// Width of the puck in pixels, changed by some perks and resizes
puckWidth: number;
// perks the user currently has
perks: PerksMap;
debuffs: DebuffsMap;
// Base speed of the ball in pixels/tick
baseSpeed: number;
// Score multiplier
@ -235,7 +250,6 @@ export type GameState = {
levelStartScore: number;
levelMisses: number;
levelSpawnedCoins: number;
lastPlayedCoinGrab: number;
// MAX_COINS: number;
// MAX_PARTICLES: number;
@ -265,10 +279,9 @@ export type GameState = {
lifeLost: { vol: number; x: number };
coinCatch: { vol: number; x: number };
colorChange: { vol: number; x: number };
void: { vol: number; x: number };
};
isAdventureMode: boolean;
adventurePath: string;
seed: string;
rerolls: number;
};
@ -277,6 +290,7 @@ export type RunParams = {
levelToAvoid?: string;
perks?: Partial<PerksMap>;
adventure?: boolean;
debuffs?: boolean;
};
export type OptionDef = {
default: boolean;

View file

@ -263,6 +263,7 @@ export const rawUpgrades = [
rejects: "",
threshold: 13000,
giftable: false,
adventure: false,
id: "extra_levels",
max: 3,
name: t("upgrades.extra_levels.name"),
@ -387,6 +388,7 @@ export const rawUpgrades = [
giftable: false,
id: "instant_upgrade",
max: 2,
adventure: false,
name: t("upgrades.instant_upgrade.name"),
help: (lvl: number) => t("upgrades.instant_upgrade.help"),
fullHelp: t("upgrades.instant_upgrade.fullHelp"),