This commit is contained in:
Renan LE CARO 2025-04-18 17:15:47 +02:00
parent 530e94f704
commit d43dd90a86
23 changed files with 268 additions and 152 deletions

View file

@ -13,26 +13,29 @@ Break colourful bricks, catch bouncing coins and select powerful upgrades !
# Current priorities # Current priorities
The goal of this project is to make a game used by many people. The game is already pretty fun. I'm now trying to The goal of this project is to make a game used by many people.
translate it to (Lebanese) Arabic, Russian and (Chilean) Spanish. Other translation are very welcome, contact me The game is already pretty fun.
if you'd like to submit one.
While translations are being written, I'll try to avoid adding features that require new translations. That means only I'm now trying to translate it to (Lebanese) Arabic, Russian and (Chilean) Spanish.
bug fixes and optimisations, maybe adding levels. Once we have a nice stable release available in 4 Other translation are very welcome, contact me if you'd like to submit one.
languages, I may add features again.
# Changelog # Changelog
## To do ## To do
- redo video presentation - redo video presentation
- Back to Creative Menu at the end of a Creative level
- make Corner Shot scale like Need Some Space - make Corner Shot scale like Need Some Space
## Done ## Done
- disable irrelevant options
- Back to Creative Menu at the end of a Creative level
## 29080170
- don't show unlock toast at first startup for levels that are unlocked by default - don't show unlock toast at first startup for levels that are unlocked by default
- Droplet particle color should be gold for gold coins - Droplet particle color should be gold for gold coins
- added levels: A Very Dangerous High-Five, The Boys
## 29079818 ## 29079818
@ -323,6 +326,7 @@ languages, I may add features again.
- make stats a clairvoyant thing - make stats a clairvoyant thing
- [colin]P ocket money — bricks absorb coins that touch them, which are released on brick destruction (with a bonus?) - [colin]P ocket money — bricks absorb coins that touch them, which are released on brick destruction (with a bonus?)
- [colin] turn ball gravity on after a top bar hit, and until bouncing on puck - [colin] turn ball gravity on after a top bar hit, and until bouncing on puck
- fan : paddle motion creates upward draft that lifts coins and balls
## Medium difficulty perks ideas ## Medium difficulty perks ideas
- balls collision split them into 4 smaller balls, lvl times (requires rework) - balls collision split them into 4 smaller balls, lvl times (requires rework)

View file

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

File diff suppressed because one or more lines are too long

242
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. // The version of the cache.
const VERSION = "29080170"; const VERSION = "29083143";
// The name of the cache // The name of the cache
const CACHE_NAME = `breakout-71-${VERSION}`; const CACHE_NAME = `breakout-71-${VERSION}`;

View file

@ -98,7 +98,11 @@ export async function openCreativeModePerksPicker() {
} else if ("bricks" in choice) { } else if ("bricks" in choice) {
setSettingValue("creativeModePerks", creativeModePerks); setSettingValue("creativeModePerks", creativeModePerks);
if (await confirmRestart(gameState)) { if (await confirmRestart(gameState)) {
restart({ perks: creativeModePerks, level: choice }); restart({
perks: creativeModePerks,
level: choice,
isCreativeRun: true,
});
} }
return; return;
} else if (choice) { } else if (choice) {

View file

@ -1 +1 @@
"29080170" "29083143"

View file

@ -113,7 +113,7 @@ export async function play() {
export function pause(playerAskedForPause: boolean) { export function pause(playerAskedForPause: boolean) {
if (!gameState.running) return; if (!gameState.running) return;
if (gameState.pauseTimeout) return; if (gameState.pauseTimeout) return;
if (gameState.computer_controlled) return; if (gameState.startParams.computer_controlled) return;
const stop = () => { const stop = () => {
gameState.running = false; gameState.running = false;
@ -451,7 +451,8 @@ export function tick() {
if (gameState.running) { if (gameState.running) {
gameState.levelTime += timeDeltaMs * frames; gameState.levelTime += timeDeltaMs * frames;
gameState.runStatistics.runTime += timeDeltaMs * frames; gameState.runStatistics.runTime += timeDeltaMs * frames;
gameStateTick(gameState, frames); const steps = isOptionOn("precise_physics") ? 4 : 1;
for (let i = 0; i < steps; i++) gameStateTick(gameState, frames / steps);
} }
if (gameState.running || gameState.needsRender) { if (gameState.running || gameState.needsRender) {
@ -665,13 +666,14 @@ async function openSettingsMenu() {
}, },
}); });
for (const key of Object.keys(options) as OptionId[]) { for (const key of Object.keys(options) as OptionId[]) {
if (options[key]) if (options[key]) {
actions.push({ actions.push({
icon: isOptionOn(key) icon: isOptionOn(key)
? icons["icon:checkmark_checked"] ? icons["icon:checkmark_checked"]
: icons["icon:checkmark_unchecked"], : icons["icon:checkmark_unchecked"],
text: options[key].name, text: options[key].name,
help: options[key].help, help: options[key].help,
disabled : (key=='extra_bright' && isOptionOn('basic')) || (key=='contrast' && isOptionOn('basic')) || false,
value: () => { value: () => {
toggleOption(key); toggleOption(key);
fitSize(gameState); fitSize(gameState);
@ -679,6 +681,7 @@ async function openSettingsMenu() {
openSettingsMenu(); openSettingsMenu();
}, },
}); });
}
} }
actions.push({ actions.push({
icon: icons["icon:download"], icon: icons["icon:download"],
@ -1030,7 +1033,7 @@ document.addEventListener("keyup", async (e) => {
!alertsOpen && !alertsOpen &&
pageLoad < Date.now() - 500 pageLoad < Date.now() - 500
) { ) {
if (gameState.computer_controlled) { if (gameState.startParams.computer_controlled) {
return startComputerControlledGame(); return startComputerControlledGame();
} }
// When doing ctrl + R in dev to refresh, i don't want to instantly restart a run // When doing ctrl + R in dev to refresh, i don't want to instantly restart a run

View file

@ -15,6 +15,7 @@ import { asyncAlert } from "./asyncAlert";
import { rawUpgrades } from "./upgrades"; import { rawUpgrades } from "./upgrades";
import { run } from "jest"; import { run } from "jest";
import { editRawLevelList } from "./levelEditor"; import { editRawLevelList } from "./levelEditor";
import { openCreativeModePerksPicker } from "./creative";
export function addToTotalPlayTime(ms: number) { export function addToTotalPlayTime(ms: number) {
setSettingValue( setSettingValue(
@ -32,8 +33,14 @@ export function gameOver(title: string, intro: string) {
stopRecording(); stopRecording();
addToTotalPlayTime(gameState.runStatistics.runTime); addToTotalPlayTime(gameState.runStatistics.runTime);
if (typeof gameState.isEditorTrialRun === "number") { if (typeof gameState.startParams.isEditorTrialRun === "number") {
editRawLevelList(gameState.isEditorTrialRun); editRawLevelList(gameState.startParams.isEditorTrialRun);
restart({});
return;
}
if (typeof gameState.startParams.isCreativeRun) {
openCreativeModePerksPicker();
restart({}); restart({});
return; return;
} }

View file

@ -54,7 +54,7 @@ import { addToTotalScore } from "./addToTotalScore";
import { hashCode } from "./getLevelBackground"; import { hashCode } from "./getLevelBackground";
export function setMousePos(gameState: GameState, x: number) { export function setMousePos(gameState: GameState, x: number) {
if (gameState.computer_controlled) return; if (gameState.startParams.computer_controlled) return;
gameState.puckPosition = x; gameState.puckPosition = x;
// Sets the puck position, and updates the ball position if they are supposed to follow it // Sets the puck position, and updates the ball position if they are supposed to follow it
@ -638,7 +638,7 @@ export function schedulGameSound(
) { ) {
if (!vol) return; if (!vol) return;
if (!isOptionOn("sound")) return; if (!isOptionOn("sound")) return;
if (gameState.computer_controlled) return; if (gameState.startParams.computer_controlled) return;
x ??= gameState.offsetX + gameState.gameZoneWidth / 2; x ??= gameState.offsetX + gameState.gameZoneWidth / 2;
const ex = gameState.aboutToPlaySound[sound] as { vol: number; x: number }; const ex = gameState.aboutToPlaySound[sound] as { vol: number; x: number };
@ -991,7 +991,7 @@ export function gameStateTick(
frames = 1, frames = 1,
) { ) {
// Ai movement of puck // Ai movement of puck
if (gameState.computer_controlled) computerControl(gameState); if (gameState.startParams.computer_controlled) computerControl(gameState);
gameState.runStatistics.max_combo = Math.max( gameState.runStatistics.max_combo = Math.max(
gameState.runStatistics.max_combo, gameState.runStatistics.max_combo,
@ -1067,7 +1067,7 @@ export function gameStateTick(
// instant win condition // instant win condition
(gameState.levelTime && !remainingBricks && !liveCount(gameState.coins)) (gameState.levelTime && !remainingBricks && !liveCount(gameState.coins))
) { ) {
if (gameState.computer_controlled) { if (gameState.startParams.computer_controlled) {
startComputerControlledGame(); startComputerControlledGame();
} else if (gameState.currentLevel + 1 < max_levels(gameState)) { } else if (gameState.currentLevel + 1 < max_levels(gameState)) {
setLevel(gameState, gameState.currentLevel + 1); setLevel(gameState, gameState.currentLevel + 1);
@ -1737,7 +1737,7 @@ export function ballTick(gameState: GameState, ball: Ball, frames: number) {
ball.destroyed = true; ball.destroyed = true;
gameState.runStatistics.balls_lost++; gameState.runStatistics.balls_lost++;
if (!gameState.balls.find((b) => !b.destroyed)) { if (!gameState.balls.find((b) => !b.destroyed)) {
if (gameState.computer_controlled) { if (gameState.startParams.computer_controlled) {
startComputerControlledGame(); startComputerControlledGame();
} else { } else {
gameOver( gameOver(

View file

@ -319,7 +319,6 @@ function isExcluded(id: PerkId) {
} }
export function getLevelUnlockCondition(levelIndex: number) { export function getLevelUnlockCondition(levelIndex: number) {
// Returns "" if level is unlocked, otherwise a string explaining how to unlock it
let required: UpgradeLike[] = [], let required: UpgradeLike[] = [],
forbidden: UpgradeLike[] = [], forbidden: UpgradeLike[] = [],
minScore = Math.max(-1000 + 100 * levelIndex, 0); minScore = Math.max(-1000 + 100 * levelIndex, 0);

View file

@ -190,6 +190,8 @@
"settings.mobile_help": "يترك مساحة تحت المجداف.", "settings.mobile_help": "يترك مساحة تحت المجداف.",
"settings.pointer_lock": "قفل مؤشر الماوس", "settings.pointer_lock": "قفل مؤشر الماوس",
"settings.pointer_lock_help": "يقوم بقفل وإخفاء مؤشر الماوس.", "settings.pointer_lock_help": "يقوم بقفل وإخفاء مؤشر الماوس.",
"settings.precise_physics": "",
"settings.precise_physics_help": "",
"settings.record": "تسجيل مقاطع فيديو للعبة", "settings.record": "تسجيل مقاطع فيديو للعبة",
"settings.record_download": "تنزيل الفيديو ({{size}} ميجابايت)", "settings.record_download": "تنزيل الفيديو ({{size}} ميجابايت)",
"settings.record_help": "احصل على فيديو لكل مستوى.", "settings.record_help": "احصل على فيديو لكل مستوى.",

View file

@ -6787,6 +6787,76 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>precise_physics</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>precise_physics_help</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>record</name> <name>record</name>
<description/> <description/>

View file

@ -190,6 +190,8 @@
"settings.mobile_help": "Lässt Platz unter dem Paddel.", "settings.mobile_help": "Lässt Platz unter dem Paddel.",
"settings.pointer_lock": "Mauszeigersperre", "settings.pointer_lock": "Mauszeigersperre",
"settings.pointer_lock_help": "Sperrt und versteckt den Mauszeiger.", "settings.pointer_lock_help": "Sperrt und versteckt den Mauszeiger.",
"settings.precise_physics": "",
"settings.precise_physics_help": "",
"settings.record": "Spielvideos aufnehmen", "settings.record": "Spielvideos aufnehmen",
"settings.record_download": "Video herunterladen ({{size}} MB)", "settings.record_download": "Video herunterladen ({{size}} MB)",
"settings.record_help": "Holen Sie sich ein Video von jedem Level.", "settings.record_help": "Holen Sie sich ein Video von jedem Level.",

View file

@ -190,6 +190,8 @@
"settings.mobile_help": "Leaves space under the paddle.", "settings.mobile_help": "Leaves space under the paddle.",
"settings.pointer_lock": "Mouse pointer lock", "settings.pointer_lock": "Mouse pointer lock",
"settings.pointer_lock_help": "Locks and hides the mouse cursor.", "settings.pointer_lock_help": "Locks and hides the mouse cursor.",
"settings.precise_physics": "More precise physics",
"settings.precise_physics_help": "Compute fast ball motion in smaller steps, might reduce performance",
"settings.record": "Record gameplay videos", "settings.record": "Record gameplay videos",
"settings.record_download": "Download video ({{size}} MB)", "settings.record_download": "Download video ({{size}} MB)",
"settings.record_help": "Get a video of each level.", "settings.record_help": "Get a video of each level.",

View file

@ -190,6 +190,8 @@
"settings.mobile_help": "Deja espacio debajo de la paleta.", "settings.mobile_help": "Deja espacio debajo de la paleta.",
"settings.pointer_lock": "Bloqueo del puntero del ratón", "settings.pointer_lock": "Bloqueo del puntero del ratón",
"settings.pointer_lock_help": "Bloquea y oculta el cursor del mouse.", "settings.pointer_lock_help": "Bloquea y oculta el cursor del mouse.",
"settings.precise_physics": "",
"settings.precise_physics_help": "",
"settings.record": "Grabar vídeos de juego", "settings.record": "Grabar vídeos de juego",
"settings.record_download": "Descargar vídeo ({{size}} MB)", "settings.record_download": "Descargar vídeo ({{size}} MB)",
"settings.record_help": "Obtenga un vídeo de cada nivel.", "settings.record_help": "Obtenga un vídeo de cada nivel.",

View file

@ -190,6 +190,8 @@
"settings.mobile_help": "Laisse un espace sous la raquette.", "settings.mobile_help": "Laisse un espace sous la raquette.",
"settings.pointer_lock": "Verrouillage du pointeur", "settings.pointer_lock": "Verrouillage du pointeur",
"settings.pointer_lock_help": "Cache aussi le curseur de la souris.", "settings.pointer_lock_help": "Cache aussi le curseur de la souris.",
"settings.precise_physics": "",
"settings.precise_physics_help": "",
"settings.record": "Enregistrer des vidéos de jeu", "settings.record": "Enregistrer des vidéos de jeu",
"settings.record_download": "Télécharger la vidéo ({{size}} MB)", "settings.record_download": "Télécharger la vidéo ({{size}} MB)",
"settings.record_help": "Obtenez une vidéo de chaque niveau.", "settings.record_help": "Obtenez une vidéo de chaque niveau.",

View file

@ -190,6 +190,8 @@
"settings.mobile_help": "Оставляет место под лопаткой.", "settings.mobile_help": "Оставляет место под лопаткой.",
"settings.pointer_lock": "Блокировка указателя мыши", "settings.pointer_lock": "Блокировка указателя мыши",
"settings.pointer_lock_help": "Фиксирует и скрывает курсор мыши.", "settings.pointer_lock_help": "Фиксирует и скрывает курсор мыши.",
"settings.precise_physics": "",
"settings.precise_physics_help": "",
"settings.record": "Запись видеороликов игрового процесса", "settings.record": "Запись видеороликов игрового процесса",
"settings.record_download": "Скачать видео ({{size}} МБ)", "settings.record_download": "Скачать видео ({{size}} МБ)",
"settings.record_help": "Получите видеозапись каждого уровня.", "settings.record_help": "Получите видеозапись каждого уровня.",

View file

@ -190,6 +190,8 @@
"settings.mobile_help": "Kürek altında boşluk bırakır.", "settings.mobile_help": "Kürek altında boşluk bırakır.",
"settings.pointer_lock": "Fare işaretçisi kilidi", "settings.pointer_lock": "Fare işaretçisi kilidi",
"settings.pointer_lock_help": "Fare imlecini kilitler ve gizler.", "settings.pointer_lock_help": "Fare imlecini kilitler ve gizler.",
"settings.precise_physics": "",
"settings.precise_physics_help": "",
"settings.record": "Oyun videolarını kaydedin", "settings.record": "Oyun videolarını kaydedin",
"settings.record_download": "Videoyu indir ({{size}} MB)", "settings.record_download": "Videoyu indir ({{size}} MB)",
"settings.record_help": "Her seviyenin videosunu edinin.", "settings.record_help": "Her seviyenin videosunu edinin.",

View file

@ -65,6 +65,7 @@ export function newGameState(params: RunParams): GameState {
const runLevels = getRunLevels(params, randomGift); const runLevels = getRunLevels(params, randomGift);
const gameState: GameState = { const gameState: GameState = {
startParams: params,
runLevels, runLevels,
level: runLevels[0], level: runLevels[0],
currentLevel: 0, currentLevel: 0,
@ -141,9 +142,7 @@ export function newGameState(params: RunParams): GameState {
creative: creative:
params?.computer_controlled || params?.computer_controlled ||
sumOfValues(params.perks) > 1 || sumOfValues(params.perks) > 1 ||
(params.level && !params.level.name.startsWith("icon:")), (params.level && !params.level.name.startsWith("icon:"))
computer_controlled: params?.computer_controlled || false,
isEditorTrialRun: params?.isEditorTrialRun,
}; };
resetBalls(gameState); resetBalls(gameState);

View file

@ -55,6 +55,11 @@ export const options = {
name: t("settings.kid"), name: t("settings.kid"),
help: t("settings.kid_help"), help: t("settings.kid_help"),
}, },
precise_physics: {
default: true,
name: t("settings.precise_physics"),
help: t("settings.precise_physics_help"),
},
// Could not get the sharing to work without loading androidx and all the modern android things so for now I'll just disable sharing in the android app // Could not get the sharing to work without loading androidx and all the modern android things so for now I'll just disable sharing in the android app
record: { record: {
default: false, default: false,

View file

@ -75,7 +75,7 @@ export function render(gameState: GameState) {
: 1; : 1;
startWork("render:scoreDisplay"); startWork("render:scoreDisplay");
scoreDisplay.innerHTML = scoreDisplay.innerHTML =
(isOptionOn("show_fps") || gameState.computer_controlled (isOptionOn("show_fps") || gameState.startParams.computer_controlled
? ` ? `
<span> <span>
${Math.floor((liveCount(gameState.coins) / getCurrentMaxCoins()) * 100)} % ${Math.floor((liveCount(gameState.coins) / getCurrentMaxCoins()) * 100)} %
@ -105,7 +105,7 @@ export function render(gameState: GameState) {
`<span class="score" data-tooltip="${t("play.score_tooltip")}">$${gameState.score}</span>`; `<span class="score" data-tooltip="${t("play.score_tooltip")}">$${gameState.score}</span>`;
scoreDisplay.className = scoreDisplay.className =
(gameState.computer_controlled && "computer_controlled") || (gameState.startParams.computer_controlled && "computer_controlled") ||
(gameState.lastScoreIncrease > gameState.levelTime - 500 && "active") || (gameState.lastScoreIncrease > gameState.levelTime - 500 && "active") ||
""; "";
// Clear // Clear
@ -578,7 +578,7 @@ export function render(gameState: GameState) {
startWork("render:text_under_puck"); startWork("render:text_under_puck");
ctx.globalCompositeOperation = "source-over"; ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = 1; ctx.globalAlpha = 1;
if (isOptionOn("mobile-mode") && gameState.computer_controlled) { if (isOptionOn("mobile-mode") && gameState.startParams.computer_controlled) {
drawText( drawText(
ctx, ctx,
"breakout.lecaro.me?autoplay", "breakout.lecaro.me?autoplay",
@ -1155,7 +1155,7 @@ export function getDashOffset(gameState: GameState) {
let wakeLock = null, let wakeLock = null,
wakeLockPending = false; wakeLockPending = false;
function askForWakeLock(gameState: GameState) { function askForWakeLock(gameState: GameState) {
if (gameState.computer_controlled && !wakeLock && !wakeLockPending) { if (gameState.startParams.computer_controlled && !wakeLock && !wakeLockPending) {
wakeLockPending = true; wakeLockPending = true;
try { try {
navigator.wakeLock.request("screen").then((lock) => { navigator.wakeLock.request("screen").then((lock) => {

4
src/types.d.ts vendored
View file

@ -281,8 +281,7 @@ export type GameState = {
}; };
rerolls: number; rerolls: number;
creative: boolean; creative: boolean;
computer_controlled: boolean; startParams: RunParams;
isEditorTrialRun?: number;
}; };
export type RunParams = { export type RunParams = {
@ -292,6 +291,7 @@ export type RunParams = {
perks?: Partial<PerksMap>; perks?: Partial<PerksMap>;
computer_controlled?: boolean; computer_controlled?: boolean;
isEditorTrialRun?: number; isEditorTrialRun?: number;
isCreativeRun?: boolean;
}; };
export type OptionDef = { export type OptionDef = {
default: boolean; default: boolean;