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
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
translate it to (Lebanese) Arabic, Russian and (Chilean) Spanish. Other translation are very welcome, contact me
if you'd like to submit one.
The goal of this project is to make a game used by many people.
The game is already pretty fun.
While translations are being written, I'll try to avoid adding features that require new translations. That means only
bug fixes and optimisations, maybe adding levels. Once we have a nice stable release available in 4
languages, I may add features again.
I'm now trying to translate it to (Lebanese) Arabic, Russian and (Chilean) Spanish.
Other translation are very welcome, contact me if you'd like to submit one.
# Changelog
## To do
- redo video presentation
- Back to Creative Menu at the end of a Creative level
- make Corner Shot scale like Need Some Space
## 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
- Droplet particle color should be gold for gold coins
- added levels: A Very Dangerous High-Five, The Boys
## 29079818
@ -323,6 +326,7 @@ languages, I may add features again.
- 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] 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
- balls collision split them into 4 smaller balls, lvl times (requires rework)

View file

@ -29,8 +29,8 @@ android {
applicationId = "me.lecaro.breakout"
minSdk = 21
targetSdk = 34
versionCode = 29080170
versionName = "29080170"
versionCode = 29083143
versionName = "29083143"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
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.
const VERSION = "29080170";
const VERSION = "29083143";
// The name of the cache
const CACHE_NAME = `breakout-71-${VERSION}`;

View file

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

View file

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

View file

@ -113,7 +113,7 @@ export async function play() {
export function pause(playerAskedForPause: boolean) {
if (!gameState.running) return;
if (gameState.pauseTimeout) return;
if (gameState.computer_controlled) return;
if (gameState.startParams.computer_controlled) return;
const stop = () => {
gameState.running = false;
@ -451,7 +451,8 @@ export function tick() {
if (gameState.running) {
gameState.levelTime += 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) {
@ -665,13 +666,14 @@ async function openSettingsMenu() {
},
});
for (const key of Object.keys(options) as OptionId[]) {
if (options[key])
if (options[key]) {
actions.push({
icon: isOptionOn(key)
? icons["icon:checkmark_checked"]
: icons["icon:checkmark_unchecked"],
? icons["icon:checkmark_checked"]
: icons["icon:checkmark_unchecked"],
text: options[key].name,
help: options[key].help,
disabled : (key=='extra_bright' && isOptionOn('basic')) || (key=='contrast' && isOptionOn('basic')) || false,
value: () => {
toggleOption(key);
fitSize(gameState);
@ -679,6 +681,7 @@ async function openSettingsMenu() {
openSettingsMenu();
},
});
}
}
actions.push({
icon: icons["icon:download"],
@ -1030,7 +1033,7 @@ document.addEventListener("keyup", async (e) => {
!alertsOpen &&
pageLoad < Date.now() - 500
) {
if (gameState.computer_controlled) {
if (gameState.startParams.computer_controlled) {
return startComputerControlledGame();
}
// 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 { run } from "jest";
import { editRawLevelList } from "./levelEditor";
import { openCreativeModePerksPicker } from "./creative";
export function addToTotalPlayTime(ms: number) {
setSettingValue(
@ -32,8 +33,14 @@ export function gameOver(title: string, intro: string) {
stopRecording();
addToTotalPlayTime(gameState.runStatistics.runTime);
if (typeof gameState.isEditorTrialRun === "number") {
editRawLevelList(gameState.isEditorTrialRun);
if (typeof gameState.startParams.isEditorTrialRun === "number") {
editRawLevelList(gameState.startParams.isEditorTrialRun);
restart({});
return;
}
if (typeof gameState.startParams.isCreativeRun) {
openCreativeModePerksPicker();
restart({});
return;
}

View file

@ -54,7 +54,7 @@ import { addToTotalScore } from "./addToTotalScore";
import { hashCode } from "./getLevelBackground";
export function setMousePos(gameState: GameState, x: number) {
if (gameState.computer_controlled) return;
if (gameState.startParams.computer_controlled) return;
gameState.puckPosition = x;
// 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 (!isOptionOn("sound")) return;
if (gameState.computer_controlled) return;
if (gameState.startParams.computer_controlled) return;
x ??= gameState.offsetX + gameState.gameZoneWidth / 2;
const ex = gameState.aboutToPlaySound[sound] as { vol: number; x: number };
@ -991,7 +991,7 @@ export function gameStateTick(
frames = 1,
) {
// 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,
@ -1067,7 +1067,7 @@ export function gameStateTick(
// instant win condition
(gameState.levelTime && !remainingBricks && !liveCount(gameState.coins))
) {
if (gameState.computer_controlled) {
if (gameState.startParams.computer_controlled) {
startComputerControlledGame();
} else if (gameState.currentLevel + 1 < max_levels(gameState)) {
setLevel(gameState, gameState.currentLevel + 1);
@ -1737,7 +1737,7 @@ export function ballTick(gameState: GameState, ball: Ball, frames: number) {
ball.destroyed = true;
gameState.runStatistics.balls_lost++;
if (!gameState.balls.find((b) => !b.destroyed)) {
if (gameState.computer_controlled) {
if (gameState.startParams.computer_controlled) {
startComputerControlledGame();
} else {
gameOver(

View file

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

View file

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

View file

@ -6787,6 +6787,76 @@
</translation>
</translations>
</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>
<name>record</name>
<description/>

View file

@ -190,6 +190,8 @@
"settings.mobile_help": "Lässt Platz unter dem Paddel.",
"settings.pointer_lock": "Mauszeigersperre",
"settings.pointer_lock_help": "Sperrt und versteckt den Mauszeiger.",
"settings.precise_physics": "",
"settings.precise_physics_help": "",
"settings.record": "Spielvideos aufnehmen",
"settings.record_download": "Video herunterladen ({{size}} MB)",
"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.pointer_lock": "Mouse pointer lock",
"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_download": "Download video ({{size}} MB)",
"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.pointer_lock": "Bloqueo del puntero del ratón",
"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_download": "Descargar vídeo ({{size}} MB)",
"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.pointer_lock": "Verrouillage du pointeur",
"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_download": "Télécharger la vidéo ({{size}} MB)",
"settings.record_help": "Obtenez une vidéo de chaque niveau.",

View file

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

View file

@ -190,6 +190,8 @@
"settings.mobile_help": "Kürek altında boşluk bırakır.",
"settings.pointer_lock": "Fare işaretçisi kilidi",
"settings.pointer_lock_help": "Fare imlecini kilitler ve gizler.",
"settings.precise_physics": "",
"settings.precise_physics_help": "",
"settings.record": "Oyun videolarını kaydedin",
"settings.record_download": "Videoyu indir ({{size}} MB)",
"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 gameState: GameState = {
startParams: params,
runLevels,
level: runLevels[0],
currentLevel: 0,
@ -141,9 +142,7 @@ export function newGameState(params: RunParams): GameState {
creative:
params?.computer_controlled ||
sumOfValues(params.perks) > 1 ||
(params.level && !params.level.name.startsWith("icon:")),
computer_controlled: params?.computer_controlled || false,
isEditorTrialRun: params?.isEditorTrialRun,
(params.level && !params.level.name.startsWith("icon:"))
};
resetBalls(gameState);

View file

@ -55,6 +55,11 @@ export const options = {
name: t("settings.kid"),
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
record: {
default: false,

View file

@ -75,7 +75,7 @@ export function render(gameState: GameState) {
: 1;
startWork("render:scoreDisplay");
scoreDisplay.innerHTML =
(isOptionOn("show_fps") || gameState.computer_controlled
(isOptionOn("show_fps") || gameState.startParams.computer_controlled
? `
<span>
${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>`;
scoreDisplay.className =
(gameState.computer_controlled && "computer_controlled") ||
(gameState.startParams.computer_controlled && "computer_controlled") ||
(gameState.lastScoreIncrease > gameState.levelTime - 500 && "active") ||
"";
// Clear
@ -578,7 +578,7 @@ export function render(gameState: GameState) {
startWork("render:text_under_puck");
ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = 1;
if (isOptionOn("mobile-mode") && gameState.computer_controlled) {
if (isOptionOn("mobile-mode") && gameState.startParams.computer_controlled) {
drawText(
ctx,
"breakout.lecaro.me?autoplay",
@ -1155,7 +1155,7 @@ export function getDashOffset(gameState: GameState) {
let wakeLock = null,
wakeLockPending = false;
function askForWakeLock(gameState: GameState) {
if (gameState.computer_controlled && !wakeLock && !wakeLockPending) {
if (gameState.startParams.computer_controlled && !wakeLock && !wakeLockPending) {
wakeLockPending = true;
try {
navigator.wakeLock.request("screen").then((lock) => {

4
src/types.d.ts vendored
View file

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