This commit is contained in:
Renan LE CARO 2025-04-06 15:38:30 +02:00
parent 2f51f83514
commit 42abc6bc01
25 changed files with 1514 additions and 1328 deletions

View file

@ -18,11 +18,13 @@ Break colourful bricks, catch bouncing coins and select powerful upgrades !
## To do
- avoid showing a +1 and -1 at the same time when a combo increase is reset
- add unlock conditions for levels in the form "reach high score X with perk A,B,C but without perk B,C,D"
- mention unlock conditions in help
- show unlock condition in unlocks menu for perks as tooltip
- fallback for mobile user to see unlock conditions
## Done
- add unlock conditions for levels in the form "reach high score X with perk A,B,C but without perk B,C,D"
- remove loop mode :
- remove basecombo
- remove mode
@ -244,6 +246,8 @@ Break colourful bricks, catch bouncing coins and select powerful upgrades !
- +lvl combo per bricks / resets after 5/lvl seconds without coin catch ?
- +lvl combo per bricks / resets after 5/lvl seconds without ball color change ?
- +lvl combo per bricks / resets after 5/lvl seconds without sides hit ?
- + lvl x n combo when destroying a brick after bouncing on a side/top n times ?
- make stats a clairvoyant thing
## Medium difficulty perks ideas
- balls collision split them into 4 smaller balls, lvl times (requires rework)
@ -261,10 +265,12 @@ Break colourful bricks, catch bouncing coins and select powerful upgrades !
- 2x speed after bouncing on puck
- the more balls are close to a brick, the more combo is gained when breaking it. If only one ball, loose one point or reset
- ball avoids brick of wrong color
- puck slowly follows desired position, but +1 combo
## Hard perk ideas
- accelerometer controls coins and balls
- [colin] side pucks - same as above but with two side pucks : hard to know where to put them
- [colin] Perk: second puck in the middle of the screen
## ideas to sort
- wind : move coins based on puck movement not position

View file

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

File diff suppressed because one or more lines are too long

249
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 = "29064070";
const VERSION = "29065772";
// The name of the cache
const CACHE_NAME = `breakout-71-${VERSION}`;

View file

@ -2,7 +2,12 @@ import { GameState, Level, PerkId, Upgrade } from "./types";
import { allLevels, icons, upgrades } from "./loadGameData";
import { t } from "./i18n/i18n";
import { getSettingValue, getTotalScore, setSettingValue } from "./settings";
import {confirmRestart, creativeModeThreshold, gameState, restart} from "./game";
import {
confirmRestart,
creativeModeThreshold,
gameState,
restart,
} from "./game";
import { asyncAlert, requiredAsyncAlert } from "./asyncAlert";
import { describeLevel, highScoreText, sumOfValues } from "./game_utils";
@ -17,15 +22,12 @@ export function creativeMode(gameState: GameState) {
t("lab.help"),
disabled: getTotalScore() < creativeModeThreshold,
async value() {
openCreativeModePerksPicker()
openCreativeModePerksPicker();
},
};
}
export async function openCreativeModePerksPicker() {
let creativeModePerks: Partial<{ [id in PerkId]: number }> = getSettingValue(
"creativeModePerks",
{},
@ -55,10 +57,7 @@ export async function openCreativeModePerksPicker() {
.map((u) => ({
icon: u.icon,
text: u.name,
help:
(creativeModePerks[u.id] || 0) +
"/" +
u.max,
help: (creativeModePerks[u.id] || 0) + "/" + (u.max+creativeModePerks.limitless),
value: u,
className: creativeModePerks[u.id]
? "sandbox"
@ -84,14 +83,12 @@ export async function openCreativeModePerksPicker() {
if (await confirmRestart(gameState)) {
restart({ perks: creativeModePerks, level: choice.name });
}
return
return;
} else if (choice) {
creativeModePerks[choice.id] =
((creativeModePerks[choice.id] || 0) + 1) %
(choice.max +1);
((creativeModePerks[choice.id] || 0) + 1) % (choice.max + 1 + creativeModePerks.limitless);
} else {
return
return;
}
}
}

View file

@ -1 +1 @@
"29064070"
"29065772"

View file

@ -158,7 +158,8 @@ body:not(.has-alert-open) #popup {
&[disabled] {
opacity: 0.5;
filter: saturate(0);
pointer-events: none;
//pointer-events: none;
cursor: not-allowed;
}
& > div {

View file

@ -17,6 +17,7 @@ import {
describeLevel,
getRowColIndex,
highScoreText,
reasonLevelIsLocked,
levelsListHTMl,
max_levels,
pickedUpgradesHTMl,
@ -72,7 +73,7 @@ import { creativeMode } from "./creative";
import { setupTooltips } from "./tooltip";
import { startingPerkMenuButton } from "./startingPerks";
import "./migrations";
import {getCreativeModeWarning} from "./gameOver";
import { getCreativeModeWarning, getHistory } from "./gameOver";
export async function play() {
if (await applyFullScreenChoice()) return;
@ -441,7 +442,8 @@ document.addEventListener("visibilitychange", () => {
async function openScorePanel() {
pause(true);
const cb = await asyncAlert({
await asyncAlert({
title: t("score_panel.title", {
score: gameState.score,
level: gameState.currentLevel + 1,
@ -449,7 +451,6 @@ async function openScorePanel() {
}),
content: [
getCreativeModeWarning(gameState),
pickedUpgradesHTMl(gameState),
levelsListHTMl(gameState, gameState.currentLevel),
@ -485,7 +486,7 @@ export async function openMainMenu() {
help: highScoreText() || t("main_menu.normal_help"),
value: () => {
restart({
levelToAvoid: currentLevelInfo(gameState).name
levelToAvoid: currentLevelInfo(gameState).name,
});
},
},
@ -816,6 +817,8 @@ async function applyFullScreenChoice() {
async function openUnlocksList() {
const ts = getTotalScore();
const hintField = isOptionOn("mobile-mode") ? "help" : "tooltip";
const upgradeActions = upgrades
.sort((a, b) => a.threshold - b.threshold)
.map(({ name, id, threshold, icon, help }) => ({
@ -823,39 +826,40 @@ async function openUnlocksList() {
disabled: ts < threshold,
value: { perks: { [id]: 1 } } as RunParams,
icon,
tooltip: help(1),
[hintField]:
ts < threshold
? t("unlocks.minTotalScore", { score: threshold })
: help(1),
}));
const levelActions = allLevels
.sort((a, b) => a.threshold - b.threshold)
.map((l) => {
const available = ts >= l.threshold;
const levelActions = allLevels.map((l, li) => {
const problem = reasonLevelIsLocked(li, getHistory());
return {
text: l.name,
disabled: !available,
disabled: !!problem,
value: { level: l.name } as RunParams,
icon: icons[l.name],
tooltip: describeLevel(l),
[hintField]: problem || describeLevel(l),
};
});
const tryOn = await asyncAlert<RunParams>({
title: t("unlocks.title_upgrades", {
unlocked: upgradeActions.filter((a) => !a.disabled).length,
out_of:upgradeActions.length
out_of: upgradeActions.length,
}),
content: [
`<p>${t("unlocks.intro", { ts })}
${upgradeActions.find(u=>u.disabled)? t("unlocks.greyed_out_help") : ""}</p> `,
${upgradeActions.find((u) => u.disabled) ? t("unlocks.greyed_out_help") : ""}</p> `,
...upgradeActions,
t("unlocks.level", {
unlocked: levelActions.filter((a) => !a.disabled).length,
out_of:levelActions.length
out_of: levelActions.length,
}),
...levelActions,
],
allowClose: true,
className: "actionsAsGrid",
className: isOptionOn("mobile-mode") ? "" : "actionsAsGrid",
});
if (tryOn) {
if (await confirmRestart(gameState)) {
@ -865,10 +869,9 @@ async function openUnlocksList() {
}
export async function confirmRestart(gameState) {
if (!gameState.currentLevel) return true;
if (alertsOpen) return true;
pause(true)
pause(true);
return asyncAlert({
title: t("confirmRestart.title"),
content: [
@ -950,7 +953,7 @@ document.addEventListener("keyup", async (e) => {
// When doing ctrl + R in dev to refresh, i don't want to instantly restart a run
if (await confirmRestart(gameState)) {
restart({
levelToAvoid: currentLevelInfo(gameState).name
levelToAvoid: currentLevelInfo(gameState).name,
});
}
} else {

View file

@ -2,34 +2,17 @@ import {allLevels, appVersion, icons, upgrades} from "./loadGameData";
import { t } from "./i18n/i18n";
import { GameState, RunHistoryItem } from "./types";
import { gameState, pause, restart } from "./game";
import { currentLevelInfo, findLast, pickedUpgradesHTMl } from "./game_utils";
import {
currentLevelInfo,
describeLevel,
findLast,
pickedUpgradesHTMl,
reasonLevelIsLocked,
} from "./game_utils";
import { getTotalScore } from "./settings";
import { stopRecording } from "./recording";
import { asyncAlert } from "./asyncAlert";
export function getUpgraderUnlockPoints() {
let list = [] as { threshold: number; title: string }[];
upgrades.forEach((u) => {
if (u.threshold) {
list.push({
threshold: u.threshold,
title: u.name + " " + t("level_up.unlocked_perk"),
});
}
});
allLevels.forEach((l) => {
list.push({
threshold: l.threshold,
title: l.name + " " + t("level_up.unlocked_level"),
});
});
return list
.filter((o) => o.threshold)
.sort((a, b) => a.threshold - b.threshold);
}
import { rawUpgrades } from "./upgrades";
export function addToTotalPlayTime(ms: number) {
try {
@ -58,58 +41,33 @@ export function gameOver(title: string, intro: string) {
return "animation-delay:" + animationDelay + "ms;";
};
// unlocks
let unlocksInfo = "";
const endTs = getTotalScore();
const startTs = endTs - gameState.score;
const list = getUpgraderUnlockPoints();
list
.filter((u) => u.threshold > startTs && u.threshold < endTs)
.forEach((u) => {
unlocksInfo += `
<p class="progress" >
<span>${u.title}</span>
<span class="progress_bar_part" style="${getDelay()}"></span>
</p>
`;
});
const previousUnlockAt =
findLast(list, (u) => u.threshold <= endTs)?.threshold || 0;
const nextUnlock = list.find((u) => u.threshold > endTs);
if (nextUnlock) {
const total = nextUnlock?.threshold - previousUnlockAt;
const done = endTs - previousUnlockAt;
intro += t("gameOver.next_unlock", {
points: nextUnlock.threshold - endTs,
});
const scaleX = (done / total).toFixed(2);
unlocksInfo += `
<p class="progress" >
<span>${nextUnlock.title}</span>
<span style="transform: scale(${scaleX},1);${getDelay()}" class="progress_bar_part"></span>
</p>
`;
list
.slice(list.indexOf(nextUnlock) + 1)
.slice(0, 3)
.forEach((u) => {
unlocksInfo += `
<p class="progress" >
<span>${u.title}</span>
</p>
`;
});
}
let unlockedItems = list.filter(
(u) => u.threshold > startTs && u.threshold < endTs,
const unlockedPerks = rawUpgrades.filter(
(o) => o.threshold > startTs && o.threshold < endTs,
);
if (unlockedItems.length) {
unlocksInfo += `<p>${t("gameOver.unlocked_count", { count: unlockedItems.length })} ${unlockedItems.map((u) => u.title).join(", ")}</p>`;
}
let unlocksInfo = unlockedPerks.length
? `
<h2>${unlockedPerks.length === 1 ? t("gameOver.unlocked_perk") : t("gameOver.unlocked_perk_plural", { count: unlockedPerks.length })}</h2>
${unlockedPerks
.map(
(u) => `
<div class="upgrade used">
${icons["icon:" + u.id]}
<p>
<strong>${u.name}</strong>
${u.help(1)}
</p>
</div>
`,
)
.join("\n")}
`
: "";
// Avoid the sad sound right as we restart a new games
gameState.combo = 1;
@ -123,42 +81,54 @@ export function gameOver(title: string, intro: string) {
<p>${intro}</p>
<p>${t("gameOver.cumulative_total", { startTs, endTs })}</p>
`,
unlocksInfo,
{
icon: icons["icon:new_run"],
value: null,
text: t("gameOver.restart"),
help: "",
},
`<div id="level-recording-container"></div>
${pickedUpgradesHTMl(gameState)}
${getHistograms(gameState)}
`,
`<div id="level-recording-container"></div>`,
// pickedUpgradesHTMl(gameState),
unlocksInfo,
getHistograms(gameState),
],
}).then(() =>
restart({
levelToAvoid: currentLevelInfo(gameState).name
levelToAvoid: currentLevelInfo(gameState).name,
}),
);
}
export function getCreativeModeWarning(gameState: GameState) {
if (gameState.creative) {
return '<p>'+t('gameOver.creative')+'</p>'
return "<p>" + t("gameOver.creative") + "</p>";
}
return ''
return "";
}
export function getHistograms(gameState: GameState) {
if(gameState.creative) return ''
let runStats = "";
let runsHistory = [];
try {
// Stores only top 100 runs
let runsHistory = JSON.parse(
runsHistory = JSON.parse(
localStorage.getItem("breakout_71_runs_history") || "[]",
) as RunHistoryItem[];
} catch (e) {}
export function getHistory() {
return runsHistory;
}
runsHistory.sort((a, b) => a.score - b.score).reverse();
runsHistory = runsHistory.slice(0, 100);
export function getHistograms(gameState: GameState) {
if (gameState.creative) return "";
let unlockedLevels = "";
let runStats = "";
try {
const locked = allLevels
.map((l, li) => ({
li,
l,
r: reasonLevelIsLocked(li, runsHistory),
}))
.filter((l) => l.r);
runsHistory.push({
...gameState.runStatistics,
@ -166,6 +136,30 @@ export function getHistograms(gameState: GameState) {
appVersion,
});
const unlocked = locked.filter(
({ li }) => !reasonLevelIsLocked(li, runsHistory),
);
if (unlocked.length) {
unlockedLevels = `
<h2>${unlocked.length === 1 ? t("unlocks.just_unlocked") : t("unlocks.just_unlocked_plural", { count: unlocked.length })}</h2>
${unlocked
.map(
({ l, r }) => `
<div class="upgrade used">
${icons[l.name]}
<p>
<strong>${l.name}</strong>
${describeLevel(l)}
</p>
</div>
`,
)
.join("\n")}
`;
}
// Generate some histogram
localStorage.setItem(
@ -178,8 +172,7 @@ export function getHistograms(gameState: GameState) {
getter: (hi: RunHistoryItem) => number,
unit: string,
) => {
let values = runsHistory
.map((h) => getter(h) || 0);
let values = runsHistory.map((h) => getter(h) || 0);
let min = Math.min(...values);
let max = Math.max(...values);
// No point
@ -290,5 +283,5 @@ export function getHistograms(gameState: GameState) {
} catch (e) {
console.warn(e);
}
return runStats;
return runStats + unlockedLevels;
}

View file

@ -131,7 +131,7 @@ export function normalizeGameState(gameState: GameState) {
gameState.gameZoneWidth / 12 / 10 +
gameState.currentLevel / 3 +
gameState.levelTime / (30 * 1000) -
gameState.perks.slow_down * 2
gameState.perks.slow_down * 2,
);
gameState.puckWidth = Math.max(
@ -172,11 +172,7 @@ export function normalizeGameState(gameState: GameState) {
}
export function baseCombo(gameState: GameState) {
return (
1 +
gameState.perks.base_combo * 3 +
gameState.perks.smaller_puck * 5
);
return 1 + gameState.perks.base_combo * 3 + gameState.perks.smaller_puck * 5;
}
export function resetCombo(
@ -527,7 +523,7 @@ export function pickRandomUpgrades(gameState: GameState, count: number) {
score: Math.random() + (gameState.lastOffered[u.id] || 0),
}))
.sort((a, b) => a.score - b.score)
.filter((u) => gameState.perks[u.id] < u.max - gameState.bannedPerks[u.id])
.filter((u) => gameState.perks[u.id] < u.max + gameState.perks.limitless)
.slice(0, count)
.sort((a, b) => (a.id > b.id ? 1 : -1));
@ -564,16 +560,12 @@ export function schedulGameSound(
}
export function addToScore(gameState: GameState, coin: Coin) {
gameState.score += coin.points;
gameState.lastScoreIncrease = gameState.levelTime;
addToTotalScore(gameState, coin.points);
if (gameState.score > gameState.highScore && !gameState.creative) {
gameState.highScore = gameState.score;
localStorage.setItem(
"breakout-3-hs-short" ,
gameState.score.toString(),
);
localStorage.setItem("breakout-3-hs-short", gameState.score.toString());
}
if (!isOptionOn("basic")) {
makeParticle(
@ -701,16 +693,16 @@ function setBrick(gameState: GameState, index: number, color: string) {
}
const rainbow = [
'#ff2e2e',
'#ffe02e',
'#70ff33',
'#33ffa7',
'#38acff',
'#7038ff',
'#ff3de5',
]
"#ff2e2e",
"#ffe02e",
"#70ff33",
"#33ffa7",
"#38acff",
"#7038ff",
"#ff3de5",
];
export function rainbowColor(): colorString {
return rainbow[Math.floor(gameState.levelTime / 50) %rainbow.length ]
return rainbow[Math.floor(gameState.levelTime / 50) % rainbow.length];
}
export function repulse(

View file

@ -1,10 +1,16 @@
import {Ball, Coin, GameState, Level, PerkId, PerksMap} from "./types";
import {
Ball,
GameState,
Level,
PerkId,
PerksMap,
RunHistoryItem,
} from "./types";
import { icons, upgrades } from "./loadGameData";
import { t } from "./i18n/i18n";
import { brickAt } from "./level_editor/levels_editor_util";
import { clamp } from "./pure_functions";
import {isOptionOn} from "./options";
import { rawUpgrades } from "./upgrades";
import { hashCode } from "./getLevelBackground";
export function describeLevel(level: Level) {
let bricks = 0,
@ -81,14 +87,14 @@ export function getPossibleUpgrades(gameState: GameState) {
export function max_levels(gameState: GameState) {
if (gameState.creative) return 1;
return 7 + gameState.perks.extra_levels
return 7 + gameState.perks.extra_levels;
}
export function pickedUpgradesHTMl(gameState: GameState) {
const upgradesList = getPossibleUpgrades(gameState)
.filter((u) => gameState.bannedPerks[u.id] || gameState.perks[u.id])
.filter((u) => gameState.perks[u.id])
.map((u) => {
const newMax = Math.max(0, u.max - gameState.bannedPerks[u.id]);
const newMax = Math.max(0, u.max +gameState.perks.limitless);
let bars = [];
for (let i = 0; i < Math.max(u.max, newMax, gameState.perks[u.id]); i++) {
@ -171,6 +177,7 @@ export function telekinesisEffectRate(gameState: GameState, ball: Ball) {
0
);
}
export function yoyoEffectRate(gameState: GameState, ball: Ball) {
return (
(gameState.perks.yoyo &&
@ -259,47 +266,63 @@ export function isMovingWhilePassiveIncome(gameState: GameState) {
export function getHighScore() {
try {
return parseInt(
localStorage.getItem("breakout-3-hs-short" ) || "0",
);
return parseInt(localStorage.getItem("breakout-3-hs-short") || "0");
} catch (e) {}
return 0
return 0;
}
export function highScoreText() {
if (getHighScore()) {
return t("main_menu.high_score", { score: getHighScore() });
}
return "";
}
export function unlockCondition(levelIndex:number){
if(levelIndex<7){
return {}
export function reasonLevelIsLocked(
levelIndex: number,
history: RunHistoryItem[],
) {
// Returns "" if level is unlocked, otherwise a string explaining how to unlock it
if (levelIndex <= 10) {
return "";
}
if (levelIndex < 20) {
return {
minScore:100*levelIndex
}
const minScore = 100 * levelIndex;
return history.find((r) => r.score >= minScore)
? ""
: t("unlocks.minScore", { minScore });
}
const excluded: PerkId[] = [
'extra_levels','extra_life', "one_more_choice", "instant_upgrade"
]
"extra_levels",
"extra_life",
"one_more_choice",
"instant_upgrade",
];
const possibletargets = rawUpgrades.slice(0,Math.floor(levelIndex/2))
.map(u=>u.id)
.filter(u=>!excluded.includes(u))
.sort((a,b)=>Math.random()-0.5)
const possibletargets = rawUpgrades
.slice(0, Math.floor(levelIndex / 2))
.map((u) => u)
.filter((u) => !excluded.includes(u.id))
.sort((a, b) => hashCode(levelIndex + a.id) - hashCode(levelIndex + b.id));
const length=Math.ceil(levelIndex/30)
return {
minScore:100*levelIndex*Math.pow(1.02,levelIndex),
withUpgrades : possibletargets.slice(0,length),
withoutUpgrades : possibletargets.slice(length,length+length),
const length = Math.ceil(levelIndex / 30);
const required = possibletargets.slice(0, length);
const forbidden = possibletargets.slice(length, length + length);
const minScore = 100 * levelIndex * Math.floor(Math.pow(1.01, levelIndex));
if (
history.find(
(r) =>
r.score >= minScore &&
!required.find((u) => !r?.perks?.[u.id]) &&
!forbidden.find((u) => r?.perks?.[u.id]),
)
) {
return "";
} else {
return t("unlocks.minScoreWithPerks", {
minScore,
required: required.map((u) => u.name).join(", "),
forbidden: forbidden.map((u) => u.name).join(", "),
});
}
}

View file

@ -152,21 +152,6 @@
</concept_node>
</children>
</folder_node>
<concept_node>
<name>next_unlock</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>restart</name>
<description/>
@ -368,7 +353,22 @@
</children>
</folder_node>
<concept_node>
<name>unlocked_count</name>
<name>unlocked_perk</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>unlocked_perk_plural</name>
<description/>
<comment/>
<translations>
@ -448,7 +448,7 @@
</translation>
<translation>
<language>fr-FR</language>
<approved>true</approved>
<approved>false</approved>
</translation>
</translations>
</concept_node>
@ -2167,6 +2167,36 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>just_unlocked</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>just_unlocked_plural</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>level</name>
<description/>
@ -2197,6 +2227,51 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>minScore</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>minScoreWithPerks</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>minTotalScore</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_upgrades</name>
<description/>
@ -3477,6 +3552,56 @@
</concept_node>
</children>
</folder_node>
<folder_node>
<name>limitless</name>
<children>
<concept_node>
<name>fullHelp</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>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>true</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
<name>metamorphosis</name>
<children>

View file

@ -7,7 +7,6 @@
"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",
"gameOver.next_unlock": "Score {{points}} more points to reach the next unlock.",
"gameOver.restart": "Start a new game",
"gameOver.stats.balls_lost": "Balls lost",
"gameOver.stats.bricks_broken": "Bricks broken",
@ -21,11 +20,12 @@
"gameOver.stats.level_reached": "Level reached",
"gameOver.stats.total_score": "Total score",
"gameOver.stats.upgrades_applied": "Upgrades applied",
"gameOver.unlocked_count": "You unlocked {{count}} items :",
"gameOver.unlocked_perk": "You just unlocked a perk",
"gameOver.unlocked_perk_plural": "You just unlocked {{count}} perks",
"gameOver.win.summary": "This game is over. You stashed {{score}} coins. ",
"gameOver.win.title": "You completed this game",
"lab.help": "Try any build you want",
"lab.instructions": "Select upgrades below, then pick the level to play. ",
"lab.instructions": "Select upgrades below, then pick the level to play. Creative mode runs are ignored in unlocks, high score, total score and statistics, and only last one level.",
"lab.menu_entry": "Creative mode",
"lab.reset": "Reset all to 0",
"lab.select_level": "Select a level to play on",
@ -138,8 +138,13 @@
"score_panel.upgrades_picked": "Upgrades picked so far : ",
"unlocks.greyed_out_help": "The greyed out ones can be unlocked by increasing your total score. The total score increases every time you score in game.",
"unlocks.intro": "Your total score is {{ts}}. Below are all the upgrades and levels the games has to offer. Click an upgrade or level below to start a game with it.",
"unlocks.just_unlocked": "You just unlocked a level",
"unlocks.just_unlocked_plural": "You just unlocked {{count}} levels",
"unlocks.level": "<h2>You unlocked {{unlocked}} levels out of {{out_of}}</h2>\n<p>Here are all the game levels, click one to start a game with that starting level. </p> ",
"unlocks.level_description": "A {{size}}x{{size}} level with {{bricks}} bricks, {{colors}} colors and {{bombs}} bombs.",
"unlocks.minScore": "Reach ${{minScore}}",
"unlocks.minScoreWithPerks": "Reach ${{minScore}} in a run with {{required}} but without {{forbidden}}",
"unlocks.minTotalScore": "Accumulate a total of ${{score}}",
"unlocks.title_upgrades": "You unlocked {{unlocked}} upgrades out of {{out_of}}",
"upgrades.addiction.fullHelp": "The countdown only starts after breaking the first brick of each level. It stops as soon as all bricks are destroyed.",
"upgrades.addiction.help": "+{{lvl}} combo / brick, combo resets {{delay}}s after breaking a brick. ",
@ -217,6 +222,9 @@
"upgrades.left_is_lava.fullHelp": "Whenever you break a brick, your combo will increase by one, so you'll get one more coin from all the next bricks you break.\n\nHowever, your combo will reset as soon as your ball hits the left side . \n\nAs soon as your combo rises, the left side becomes red to remind you that you should avoid hitting them. \n",
"upgrades.left_is_lava.help": "+{{lvl}} combo per brick broken, resets on left side hit",
"upgrades.left_is_lava.name": "Avoid left side",
"upgrades.limitless.fullHelp": "Choosing this perk also raises his own limit by one, letting you pick it again.",
"upgrades.limitless.help": "Raise all upgrade's maximum level by {{lvl}} ",
"upgrades.limitless.name": "Limitless",
"upgrades.metamorphosis.fullHelp": "With this perk, coins will be of the color of the brick they come from, and will color the first brick they touch in the same color. \n\nCoins spawn with the speed of the ball that broke them, which means you can aim a bit in the direction of the bricks you want to \"paint\".",
"upgrades.metamorphosis.help": "Each coins can stain {{lvl}} brick(s) with its color",
"upgrades.metamorphosis.name": "Metamorphosis",

View file

@ -7,7 +7,6 @@
"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",
"gameOver.next_unlock": "Marquez {{points}} points supplémentaires pour débloquer la prochaine amélioration ou le prochain niveau.",
"gameOver.restart": "Nouvelle partie",
"gameOver.stats.balls_lost": "Balles perdues",
"gameOver.stats.bricks_broken": "Briques cassées",
@ -21,11 +20,12 @@
"gameOver.stats.level_reached": "Niveau atteint",
"gameOver.stats.total_score": "Score total",
"gameOver.stats.upgrades_applied": "Mises à jour appliquées",
"gameOver.unlocked_count": "Vous avez débloqué {{count}} objet(s) :",
"gameOver.unlocked_perk": "Vous avez débloqué une amélioration",
"gameOver.unlocked_perk_plural": "Vous avez débloqué {{count}} améliorations",
"gameOver.win.summary": "Cette partie est terminée. Vous avez accumulé {{score}} pièces. ",
"gameOver.win.title": "Vous avez terminé cette partie",
"lab.help": "Essayez n'importe quel build",
"lab.instructions": "Sélectionnez les améliorations ci-dessous, puis choisissez le niveau à jouer. ",
"lab.instructions": "Sélectionnez les améliorations ci-dessous, puis choisissez le niveau à jouer. Les parties en mode créatif sont ignorées dans les déblocages, le meilleur score, le score total et les statistiques, et ne durent qu'un seul niveau.",
"lab.menu_entry": "Mode créatif",
"lab.reset": "RAZ toutes les améliorations",
"lab.select_level": "Sélectionnez un niveau sur lequel jouer",
@ -138,8 +138,13 @@
"score_panel.upgrades_picked": "Améliorations choisies jusqu'à présent :",
"unlocks.greyed_out_help": "Les éléments grisées peuvent être débloquées en augmentant votre score total. Le score total augmente à chaque fois que vous marquez des points dans le jeu.",
"unlocks.intro": "Votre score total est de {{ts}}. Vous trouverez ci-dessous toutes les améliorations et tous les niveaux que le jeu peut offrir. Cliquez sur l'un d'entre eux pour commencer une nouvelle partie. ",
"unlocks.just_unlocked": "Vous venez de débloquer un niveau",
"unlocks.just_unlocked_plural": "Vous venez de débloquer {{count}} niveaux",
"unlocks.level": "<h2>Vous avez débloqué {{unlocked}} niveaux sur {{out_of}}</h2>\n<p>Voici tous les niveaux du jeu, cliquez sur l'un d'eux pour démarrer une partie avec ce niveau de départ. </p> ",
"unlocks.level_description": "Un niveau {{size}}x{{size}} avec {{bricks}} briques, {{colors}} couleurs et {{bombs}} bombes.",
"unlocks.minScore": "Atteindre ${{minScore}}",
"unlocks.minScoreWithPerks": "Atteignez ${{minScore}} dans une course avec {{required}} mais sans {{forbidden}}",
"unlocks.minTotalScore": "Accumuler un total de ${{score}}",
"unlocks.title_upgrades": "Vous avez débloqué {{unlocked}} améliorations sur {{out_of}}",
"upgrades.addiction.fullHelp": "Le décompte ne commence qu'à parti de la destruction de la première brique du niveau, et s'arrête dès qu'il n'y a plus de briques. ",
"upgrades.addiction.help": "+{{lvl}} combo / brique, le combo RAZ après {{delay}}s sans casser de briques",
@ -217,6 +222,9 @@
"upgrades.left_is_lava.fullHelp": "Chaque fois que vous cassez une brique, votre combo augmente d'une unité, ce qui vous permet d'obtenir une pièce de plus à chaque fois que vous cassez une brique.\n\nCependant, votre combinaison se réinitialise dès que votre balle touche le côté gauche.\n\nDès que votre combo augmente, le côté gauche devient rouge pour vous rappeler que vous devez éviter de le frapper.",
"upgrades.left_is_lava.help": "+{{lvl}} combo par brique, RAZ en touchant le bord gauche",
"upgrades.left_is_lava.name": "Éviter le côté gauche",
"upgrades.limitless.fullHelp": "",
"upgrades.limitless.help": "",
"upgrades.limitless.name": "Sans limites",
"upgrades.metamorphosis.fullHelp": "Avec cette amélioration, les pièces seront de la couleur de la brique d'où elles proviennent et coloreront la première brique qu'elles toucheront. \n\nLes pièces apparaissent à la vitesse de la balle qui les a cassées, ce qui signifie que vous pouvez viser un peu dans la direction des briques que vous voulez \"peindre\".",
"upgrades.metamorphosis.help": "Chaque pièces peut tacher {{lvl}} brique(s) avec sa couleur",
"upgrades.metamorphosis.name": "Métamorphose",

View file

@ -12,34 +12,35 @@ const backgrounds = _backgrounds as string[];
const palette = _palette as Palette;
// let allLevels = _allLevels ;
let allLevels=null
let allLevels = null;
function App() {
const [selected, setSelected] = useState("W");
const [applying, setApplying] = useState("");
const [levels, setLevels] = useState([]);
useEffect(() => {
fetch('http://localhost:4400/src/data/levels.json')
.then(r=>r.json())
.then(list=>{
setLevels(list as RawLevel[])
allLevels=list
})
},[])
fetch("http://localhost:4400/src/data/levels.json")
.then((r) => r.json())
.then((list) => {
setLevels(list as RawLevel[]);
allLevels = list;
});
}, []);
const updateLevel = (index: number, change: Partial<RawLevel>) => {
setLevels((list) =>
list.map((l, li) => (li === index ? { ...l, ...change } : l)),
);
}
};
const deleteLevel = (index: number) => {
if (confirm("Delete level")) {
setLevels(levels.filter((l, i) => i !== index));
}
}
};
useEffect(() => {
if(!allLevels||JSON.stringify(allLevels) === JSON.stringify(levels)) return
if (!allLevels || JSON.stringify(allLevels) === JSON.stringify(levels))
return;
const timoutId = setTimeout(() => {
return fetch("http://localhost:4400/src/data/levels.json", {
method: "POST",

View file

@ -35,12 +35,6 @@ export const allLevels = rawLevelsList
.filter((l) => !l.name.startsWith("icon:"))
.map((l, li) => ({
...l,
threshold:
li < 8
? 0
: Math.round(
Math.min(Math.pow(10, 1 + (li + l.size) / 30) * 10, 5000) * li,
),
sortKey: ((Math.random() + 3) / 3.5) * l.bricksCount,
})) as Level[];

View file

@ -41,17 +41,14 @@ migrate("remove_long_and_creative_mode_data", () => {
localStorage.getItem("breakout_71_runs_history") || "[]",
) as RunHistoryItem[];
let cleaned=runsHistory.filter(r=> {
if('mode' in r){
if(r.mode !== 'short'){
return false
let cleaned = runsHistory.filter((r) => {
if ("mode" in r) {
if (r.mode !== "short") {
return false;
}
}
return true
})
if(cleaned.length!==runsHistory.length)
localStorage.setItem(
"breakout_71_runs_history",
JSON.stringify(cleaned),
);
return true;
});
if (cleaned.length !== runsHistory.length)
localStorage.setItem("breakout_71_runs_history", JSON.stringify(cleaned));
});

View file

@ -1,22 +1,30 @@
import { GameState, RunParams } from "./types";
import { getTotalScore } from "./settings";
import { allLevels, upgrades } from "./loadGameData";
import {
defaultSounds, getHighScore,
getPossibleUpgrades, highScoreText,
defaultSounds,
getHighScore,
getPossibleUpgrades,
highScoreText,
reasonLevelIsLocked,
makeEmptyPerksMap,
sumOfValues,
} from "./game_utils";
import { dontOfferTooSoon, resetBalls } from "./gameStateMutators";
import { isOptionOn } from "./options";
import { getHistory } from "./gameOver";
import { getTotalScore } from "./settings";
export function getRunLevels(params: RunParams) {
const history = getHistory();
const unlocked = allLevels.filter(
(l, li) => !reasonLevelIsLocked(li, history),
);
export function getRunLevels(totalScoreAtRunStart: number, params: RunParams) {
const firstLevel = params?.level
? allLevels.filter((l) => l.name === params?.level)
? unlocked.filter((l) => l.name === params?.level)
: [];
const restInRandomOrder = allLevels
.filter((l) => totalScoreAtRunStart >= l.threshold)
const restInRandomOrder = unlocked
.filter((l) => l.name !== params?.level)
.filter((l) => l.name !== params?.levelToAvoid)
.sort(() => Math.random() - 0.5);
@ -27,9 +35,7 @@ export function getRunLevels(totalScoreAtRunStart: number, params: RunParams) {
}
export function newGameState(params: RunParams): GameState {
const totalScoreAtRunStart = getTotalScore();
const runLevels = getRunLevels(totalScoreAtRunStart, params);
const runLevels = getRunLevels(params);
const perks = { ...makeEmptyPerksMap(upgrades), ...(params?.perks || {}) };
@ -39,7 +45,6 @@ export function newGameState(params: RunParams): GameState {
currentLevel: 0,
upgradesOfferedFor: -1,
perks,
bannedPerks: makeEmptyPerksMap(upgrades),
puckWidth: 200,
baseSpeed: 12,
combo: 1,
@ -80,7 +85,7 @@ export function newGameState(params: RunParams): GameState {
ballSize: 20,
coinSize: 14,
puckHeight: 20,
totalScoreAtRunStart,
totalScoreAtRunStart: getTotalScore(),
pauseUsesDuringRun: 0,
keyboardPuckSpeed: 0,
lastTick: performance.now(),
@ -110,7 +115,7 @@ export function newGameState(params: RunParams): GameState {
autoCleanUses: 0,
...defaultSounds(),
rerolls: 0,
creative: sumOfValues(params.perks)>1
creative: sumOfValues(params.perks) > 1,
};
resetBalls(gameState);

View file

@ -97,13 +97,12 @@ export function render(gameState: GameState) {
haloCanvasCtx.fillStyle = level.color;
haloCanvasCtx.fillRect(0, 0, width / haloScale, height / haloScale);
const brightness = isOptionOn("extra_bright") ? 3:1
const brightness = isOptionOn("extra_bright") ? 3 : 1;
haloCanvasCtx.globalCompositeOperation = "lighten";
haloCanvasCtx.globalAlpha = 0.1;
forEachLiveOne(gameState.coins, (coin) => {
const color = getCoinRenderColor(gameState, coin)
const color = getCoinRenderColor(gameState, coin);
drawFuzzyBall(
haloCanvasCtx,
color,
@ -111,7 +110,6 @@ export function render(gameState: GameState) {
coin.x / haloScale,
coin.y / haloScale,
);
});
haloCanvasCtx.globalAlpha = 0.1;
gameState.balls.forEach((ball) => {
@ -122,9 +120,8 @@ export function render(gameState: GameState) {
ball.x / haloScale,
ball.y / haloScale,
);
});
haloCanvasCtx.globalAlpha = 0.05
haloCanvasCtx.globalAlpha = 0.05;
gameState.bricks.forEach((color, index) => {
if (!color) return;
const x = brickCenterX(gameState, index),
@ -151,7 +148,6 @@ export function render(gameState: GameState) {
x / haloScale,
y / haloScale,
);
});
ctx.globalAlpha = 1;
@ -237,10 +233,10 @@ export function render(gameState: GameState) {
// Coins
ctx.globalAlpha = 1;
forEachLiveOne(gameState.coins, (coin) => {
const color = getCoinRenderColor(gameState,coin)
const color = getCoinRenderColor(gameState, coin);
// ctx.globalCompositeOperation = "source-over";
ctx.globalCompositeOperation = "source-over"
ctx.globalCompositeOperation = "source-over";
drawCoin(
ctx,
color,
@ -249,12 +245,14 @@ export function render(gameState: GameState) {
coin.y,
(hasCombo && gameState.perks.asceticism && "#FF0000") ||
(color === "#ffd300" && "#ffd300") ||
(color == "#231f20" && gameState.level.color == "#000000" && "#FFFFFF")
|| (gameState.level.color ),
(color == "#231f20" &&
gameState.level.color == "#000000" &&
"#FFFFFF") ||
gameState.level.color,
coin.a,
);
});
console.log(gameState.level.color)
console.log(gameState.level.color);
// Black shadow around balls
if (!isOptionOn("basic")) {
ctx.globalCompositeOperation = "source-over";
@ -278,7 +276,15 @@ export function render(gameState: GameState) {
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(gameState, ctx, color, x, y, -1, gameState.perks.clairvoyant >= 2);
drawBrick(
gameState,
ctx,
color,
x,
y,
-1,
gameState.perks.clairvoyant >= 2,
);
});
ctx.globalCompositeOperation = "screen";
@ -587,7 +593,7 @@ export function renderAllBricks() {
const redColorOnAllBricks = hasCombo && isMovingWhilePassiveIncome(gameState);
const redRowReach = reachRedRowIndex(gameState);
const {clairvoyant}=gameState.perks
const { clairvoyant } = gameState.perks;
let offset = getDashOffset(gameState);
if (
!(
@ -653,7 +659,8 @@ export function renderAllBricks() {
redColorOnAllBricks;
canctx.globalCompositeOperation = "source-over";
drawBrick(gameState,
drawBrick(
gameState,
canctx,
color,
x,
@ -662,8 +669,7 @@ export function renderAllBricks() {
clairvoyant >= 2,
);
if (gameState.brickHP[index] > 1 && clairvoyant) {
canctx.globalCompositeOperation = "source-over"
canctx.globalCompositeOperation = "source-over";
drawText(
canctx,
gameState.brickHP[index].toString(),
@ -891,7 +897,7 @@ export function drawFuzzyBall(
size / 2,
);
gradient.addColorStop(0, color);
console.log(color)
console.log(color);
gradient.addColorStop(0.3, color + "88");
gradient.addColorStop(0.6, color + "22");
gradient.addColorStop(1, "transparent");
@ -923,7 +929,11 @@ export function drawBrick(
const width = brx - tlx,
height = bry - tly;
const whiteBorder = (offset == -1 && color == "#231f20" && gameState.level.color == "#000000" && "#FFFFFF")
const whiteBorder =
offset == -1 &&
color == "#231f20" &&
gameState.level.color == "#000000" &&
"#FFFFFF";
const key =
"brick" +
@ -936,7 +946,9 @@ export function drawBrick(
"_" +
offset +
"_" +
borderOnly + '_' + whiteBorder;
borderOnly +
"_" +
whiteBorder;
if (!cachedGraphics[key]) {
const can = document.createElement("canvas");
@ -1072,7 +1084,6 @@ export function getDashOffset(gameState: GameState) {
function getCoinRenderColor(gameState: GameState, coin: Coin) {
if (gameState.perks.metamorphosis || isOptionOn("colorful_coins"))
return coin.color
return "#ffd300"
return coin.color;
return "#ffd300";
}

2
src/types.d.ts vendored
View file

@ -18,7 +18,6 @@ export type Level = {
bricksCount: number;
svg: string;
color: string;
threshold: number;
sortKey: number;
credit?: string;
};
@ -202,7 +201,6 @@ export type GameState = {
puckWidth: number;
// perks the user currently has
perks: PerksMap;
bannedPerks: PerksMap;
// Base speed of the ball in pixels/tick
baseSpeed: number;
// Score multiplier

View file

@ -654,4 +654,15 @@ export const rawUpgrades = [
t("upgrades.fountain_toss.help", { lvl, max: lvl * 30 }),
fullHelp: t("upgrades.fountain_toss.fullHelp"),
},
{
requires: "",
threshold: 175000,
giftable: false,
id: "limitless",
max: 1,
name: t("upgrades.limitless.name"),
help: (lvl: number) =>
t("upgrades.limitless.help", { lvl }),
fullHelp: t("upgrades.limitless.fullHelp"),
},
] as const;