Added save mechanic

This commit is contained in:
Renan LE CARO 2025-03-17 11:50:13 +01:00
parent 7aff5be9cd
commit 659d79bcd0
23 changed files with 333 additions and 20292 deletions

View file

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

View file

@ -1 +1 @@
"29035871"
"29036807"

View file

@ -52,12 +52,6 @@ import {
} from "./asyncAlert";
import { isOptionOn, options, toggleOption } from "./options";
bombSVG.src =
"data:image/svg+xml;base64," +
btoa(`<svg width="144" height="144" viewBox="0 0 38.101 38.099" xmlns="http://www.w3.org/2000/svg">
<path d="m6.1528 26.516c-2.6992-3.4942-2.9332-8.281-.58305-11.981a10.454 10.454 0 017.3701-4.7582c1.962-.27726 4.1646.05953 5.8835.90027l.45013.22017.89782-.87417c.83748-.81464.91169-.87499 1.0992-.90271.40528-.058713.58876.03425 1.1971.6116l.55451.52679 1.0821-1.0821c1.1963-1.1963 1.383-1.3357 2.1039-1.5877.57898-.20223 1.5681-.19816 2.1691.00897 1.4613.50314 2.3673 1.7622 2.3567 3.2773-.0058.95654-.24464 1.5795-.90924 2.3746-.40936.48928-.55533.81057-.57898 1.2737-.02039.41018.1109.77714.42322 1.1792.30172.38816.3694.61323.2797.93044-.12803.45666-.56674.71598-1.0242.60507-.601-.14597-1.3031-1.3088-1.3969-2.3126-.09459-1.0161.19245-1.8682.92392-2.7432.42567-.50885.5643-.82851.5643-1.3031 0-.50151-.14026-.83177-.51211-1.2028-.50966-.50966-1.0968-.64829-1.781-.41996l-.37348.12477-2.1006 2.1006.52597.55696c.45421.48194.5325.58876.57898.78855.09622.41588.07502.45014-.88396 1.4548l-.87173.9125.26339.57979a10.193 10.193 0 01.9231 4.1001c.03996 2.046-.41996 3.8082-1.4442 5.537-.55044.928-1.0185 1.5013-1.8968 2.3241-.83503.78284-1.5526 1.2827-2.4904 1.7361-3.4266 1.657-7.4721 1.3422-10.549-.82035-.73473-.51782-1.7312-1.4621-2.2515-2.1357zm21.869-4.5584c-.0579-.19734-.05871-2.2662 0-2.4545.11906-.39142.57898-.63361 1.0038-.53005.23812.05708.54147.32455.6116.5382.06279.19163.06769 2.1805.0065 2.3811-.12558.40773-.61649.67602-1.0462.57164-.234-.05708-.51615-.30498-.57568-.50722m3.0417-2.6013c-.12313-.6222.37837-1.1049 1.0479-1.0079.18348.0261.25279.08399 1.0071.83911.75838.75838.81301.82362.84074 1.0112.10193.68499-.40365 1.1938-1.034 1.0405-.1949-.0473-.28786-.12558-1.0144-.85216-.7649-.76409-.80241-.81057-.84645-1.0316m.61323-3.0629a.85623.85623 0 01.59284-.99975c.28949-.09214 2.1814-.08318 2.3917.01141.38734.17369.6279.61078.53984.98181-.06035.25606-.35391.57327-.60181.64992-.25279.07747-2.2278.053-2.4097-.03017-.26013-.11906-.46318-.36125-.51374-.61323" fill="#fff" opacity="0.3"/>
</svg>`);
export function play() {
if (gameState.running) return;
gameState.running = true;
@ -261,8 +255,8 @@ gameCanvas.addEventListener("mouseup", (e) => {
pause(true);
} else {
play();
if (isOptionOn("pointerLock")) {
gameCanvas.requestPointerLock();
if (isOptionOn("pointerLock") && gameCanvas.requestPointerLock) {
gameCanvas.requestPointerLock().then();
}
}
});
@ -543,7 +537,6 @@ async function openSettingsPanel() {
for (const key of Object.keys(options) as OptionId[]) {
if (options[key])
actions.push({
disabled: options[key].disabled(),
icon: isOptionOn(key)
? icons["icon:checkmark_checked"]
: icons["icon:checkmark_unchecked"],
@ -655,6 +648,116 @@ async function openSettingsPanel() {
},
});
actions.push({
text: t("main_menu.download_save_file"),
help: t("main_menu.download_save_file_help"),
async value() {
const localStorageContent: Record<string, string> = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i) as string;
const value = localStorage.getItem(key) as string;
// Store the key-value pair in the object
localStorageContent[key] = value;
}
const dlLink = document.createElement("a");
dlLink.setAttribute(
"href",
"data:application/json;base64," +
btoa(
JSON.stringify({
fileType: "B71-save-file",
appVersion,
localStorageContent,
}),
),
);
dlLink.setAttribute(
"download",
"b71-save-" +
new Date()
.toISOString()
.slice(0, 19)
.replace(/[^0-9]+/gi, "-") +
".b71",
);
document.body.appendChild(dlLink);
dlLink.click();
setTimeout(() => document.body.removeChild(dlLink), 1000);
},
});
actions.push({
text: t("main_menu.load_save_file"),
help: t("main_menu.load_save_file_help"),
async value() {
if (!document.getElementById("save_file_picker")) {
let input: HTMLInputElement = document.createElement("input");
input.setAttribute("type", "file");
input.setAttribute("id", "save_file_picker");
input.setAttribute("accept", ".b71");
input.style.position = "absolute";
input.style.left = "-1000px";
input.addEventListener("change", async (e) => {
try {
const file = input && input.files?.item(0);
if (file) {
const content = await new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = function () {
resolve(reader.result?.toString() || "");
};
reader.onerror = function () {
reject(reader.error);
};
// Read the file as a text string
reader.readAsText(file);
});
const {
fileType,
appVersion: fileVersion,
localStorageContent,
} = JSON.parse(content);
if (fileType !== "B71-save-file")
throw new Error("Not a B71 save file");
if (fileVersion > appVersion)
throw new Error(
"Please update your app first, this file is for version " +
fileVersion +
" or newer.",
);
localStorage.clear();
for (let key in localStorageContent) {
localStorage.setItem(key, localStorageContent[key]);
}
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") }],
});
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") }],
});
}
input.value = "";
});
document.body.appendChild(input);
}
document.getElementById("save_file_picker")?.click();
},
});
actions.push({
text: t("main_menu.language"),
help: t("main_menu.language_help"),

View file

@ -622,6 +622,36 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>download_save_file</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>download_save_file_help</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>footer_html</name>
<description/>
@ -757,6 +787,36 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>load_save_file</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>load_save_file_help</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>mobile</name>
<description/>
@ -967,6 +1027,66 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>save_file_error</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>save_file_loaded</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>save_file_loaded_help</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>save_file_loaded_ok</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>sounds</name>
<description/>

View file

@ -37,6 +37,8 @@
"level_up.upgrade_perk_to_level": " lvl {{level}}",
"main_menu.basic": "Basic graphics",
"main_menu.basic_help": "Fewer particles and flashes, better performance.",
"main_menu.download_save_file": "Download save file",
"main_menu.download_save_file_help": "Get a transferable .b71 file with your score and stats",
"main_menu.footer_html": " <p> <span>Made in France by <a href=\"https://lecaro.me\">Renan LE CARO</a>.</span> \n <a href=\"https://breakout.lecaro.me/privacy.html\" target=\"_blank\">Privacy Policy</a>\n <a href=\"https://f-droid.org/en/packages/me.lecaro.breakout/\" target=\"_blank\">F-Droid</a>\n <a href=\"https://play.google.com/store/apps/details?id=me.lecaro.breakout\" target=\"_blank\">Google Play</a>\n <a href=\"https://renanlecaro.itch.io/breakout71\" target=\"_blank\">itch.io</a> \n <a href=\"https://gitlab.com/lecarore/breakout71\" target=\"_blank\">Gitlab</a>\n <a href=\"https://breakout.lecaro.me/\" target=\"_blank\">Web version</a>\n <a href=\"https://news.ycombinator.com/item?id=43183131\" target=\"_blank\">HackerNews</a>\n <span>v.{{appVersion}}</span></p>",
"main_menu.fullscreen": "Fullscreen",
"main_menu.fullscreen_exit": "Exit Fullscreen",
@ -46,6 +48,8 @@
"main_menu.kid_help": "Start future runs with \"slower ball\".",
"main_menu.language": "Language",
"main_menu.language_help": "Choose the game's language",
"main_menu.load_save_file": "Load save file",
"main_menu.load_save_file_help": "Select a .b71 file on your device",
"main_menu.mobile": "Mobile mode",
"main_menu.mobile_help": "Leaves space for your thumb under the puck.",
"main_menu.pointer_lock": "Mouse pointer lock",
@ -60,6 +64,10 @@
"main_menu.reset_instruction": "You will loose all progress you made in the game, are you sure ?",
"main_menu.resume": "Resume",
"main_menu.resume_help": "Return to your run",
"main_menu.save_file_error": "Error loading save file",
"main_menu.save_file_loaded": "Save file loaded",
"main_menu.save_file_loaded_help": "The app will now reload to apply your save",
"main_menu.save_file_loaded_ok": "Ok",
"main_menu.sounds": "Game sounds",
"main_menu.sounds_help": "Can slow down some phones.",
"main_menu.title": "Breakout 71",

View file

@ -37,6 +37,8 @@
"level_up.upgrade_perk_to_level": " niveau {{level}}",
"main_menu.basic": "Graphismes simplifiés",
"main_menu.basic_help": "Moins de particules et effets, meilleures performances.",
"main_menu.download_save_file": "Sauvegarder mes progrès",
"main_menu.download_save_file_help": "Obtenir un fichier de sauvegarde .b71 transférable",
"main_menu.footer_html": " <p> <span>Programmé en France par <a href=\"https://lecaro.me\">Renan LE CARO</a>.</span> <a href=\"https://breakout.lecaro.me/privacy.html\" target=\"_blank\">Politique de confidentialité</a> <a href=\"https://f-droid.org/en/packages/me.lecaro.breakout/\" target=\"_blank\">F-Droid</a> <a href=\"https://play.google.com/store/apps/details?id=me.lecaro.breakout\" target=\"_blank\">Google Play</a> <a href=\"https://renanlecaro.itch.io/breakout71\" target=\"_blank\">itch.io</a> <a href=\"https://gitlab.com/lecarore/breakout71\" target=\"_blank\">Gitlab</a> <a href=\"https://breakout.lecaro.me/\" target=\"_blank\">Version web</a> <a href=\"https://news.ycombinator.com/item?id=43183131\" target=\"_blank\">HackerNews</a> <span>v.{{appVersion}}</span></p>",
"main_menu.fullscreen": "Plein écran",
"main_menu.fullscreen_exit": "Quitter le plein écran",
@ -46,6 +48,8 @@
"main_menu.kid_help": "Balle plus lente",
"main_menu.language": "Langue",
"main_menu.language_help": "Changer la langue d'affichage",
"main_menu.load_save_file": "Charger une sauvegarde",
"main_menu.load_save_file_help": "Sélectionnez un fichier .b71 sur votre appareil ",
"main_menu.mobile": "Mode mobile",
"main_menu.mobile_help": "Laisse un espace pour le pouce sous le palet.",
"main_menu.pointer_lock": "Verrouillage du pointeur de la souris",
@ -60,6 +64,10 @@
"main_menu.reset_instruction": "Vous perdrez tous les progrès que vous avez faits dans le jeu, êtes-vous sûr ?",
"main_menu.resume": "Retourner à la partie",
"main_menu.resume_help": "Continuer la partie en cours",
"main_menu.save_file_error": "Erreur lors du chargement du fichier de sauvegarde",
"main_menu.save_file_loaded": "Sauvegarde chargée",
"main_menu.save_file_loaded_help": "L'appli va redémarrer",
"main_menu.save_file_loaded_ok": "Ok",
"main_menu.sounds": "Sons du jeu",
"main_menu.sounds_help": "Ralentis certains téléphones.",
"main_menu.title": "Breakout 71",

View file

@ -8,40 +8,32 @@ export const options = {
default: true,
name: t("main_menu.sounds"),
help: t("main_menu.sounds_help"),
disabled: () => false,
},
"mobile-mode": {
default: window.innerHeight > window.innerWidth,
name: t("main_menu.mobile"),
help: t("main_menu.mobile_help"),
disabled: () => false,
},
basic: {
default: false,
name: t("main_menu.basic"),
help: t("main_menu.basic_help"),
disabled: () => false,
},
pointerLock: {
default: false,
name: t("main_menu.pointer_lock"),
help: t("main_menu.pointer_lock_help"),
disabled: () => !document.body.requestPointerLock,
},
easy: {
default: false,
name: t("main_menu.kid"),
help: t("main_menu.kid_help"),
disabled: () => false,
},
// 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,
name: t("main_menu.record"),
help: t("main_menu.record_help"),
disabled() {
return window.location.search.includes("isInWebView=true");
},
},
} as const satisfies { [k: string]: OptionDef };

View file

@ -16,6 +16,13 @@ export const ctx = gameCanvas.getContext("2d", {
alpha: false,
}) as CanvasRenderingContext2D;
export const bombSVG = document.createElement("img");
bombSVG.src =
"data:image/svg+xml;base64," +
btoa(`<svg width="144" height="144" viewBox="0 0 38.101 38.099" xmlns="http://www.w3.org/2000/svg">
<path d="m6.1528 26.516c-2.6992-3.4942-2.9332-8.281-.58305-11.981a10.454 10.454 0 017.3701-4.7582c1.962-.27726 4.1646.05953 5.8835.90027l.45013.22017.89782-.87417c.83748-.81464.91169-.87499 1.0992-.90271.40528-.058713.58876.03425 1.1971.6116l.55451.52679 1.0821-1.0821c1.1963-1.1963 1.383-1.3357 2.1039-1.5877.57898-.20223 1.5681-.19816 2.1691.00897 1.4613.50314 2.3673 1.7622 2.3567 3.2773-.0058.95654-.24464 1.5795-.90924 2.3746-.40936.48928-.55533.81057-.57898 1.2737-.02039.41018.1109.77714.42322 1.1792.30172.38816.3694.61323.2797.93044-.12803.45666-.56674.71598-1.0242.60507-.601-.14597-1.3031-1.3088-1.3969-2.3126-.09459-1.0161.19245-1.8682.92392-2.7432.42567-.50885.5643-.82851.5643-1.3031 0-.50151-.14026-.83177-.51211-1.2028-.50966-.50966-1.0968-.64829-1.781-.41996l-.37348.12477-2.1006 2.1006.52597.55696c.45421.48194.5325.58876.57898.78855.09622.41588.07502.45014-.88396 1.4548l-.87173.9125.26339.57979a10.193 10.193 0 01.9231 4.1001c.03996 2.046-.41996 3.8082-1.4442 5.537-.55044.928-1.0185 1.5013-1.8968 2.3241-.83503.78284-1.5526 1.2827-2.4904 1.7361-3.4266 1.657-7.4721 1.3422-10.549-.82035-.73473-.51782-1.7312-1.4621-2.2515-2.1357zm21.869-4.5584c-.0579-.19734-.05871-2.2662 0-2.4545.11906-.39142.57898-.63361 1.0038-.53005.23812.05708.54147.32455.6116.5382.06279.19163.06769 2.1805.0065 2.3811-.12558.40773-.61649.67602-1.0462.57164-.234-.05708-.51615-.30498-.57568-.50722m3.0417-2.6013c-.12313-.6222.37837-1.1049 1.0479-1.0079.18348.0261.25279.08399 1.0071.83911.75838.75838.81301.82362.84074 1.0112.10193.68499-.40365 1.1938-1.034 1.0405-.1949-.0473-.28786-.12558-1.0144-.85216-.7649-.76409-.80241-.81057-.84645-1.0316m.61323-3.0629a.85623.85623 0 01.59284-.99975c.28949-.09214 2.1814-.08318 2.3917.01141.38734.17369.6279.61078.53984.98181-.06035.25606-.35391.57327-.60181.64992-.25279.07747-2.2278.053-2.4097-.03017-.26013-.11906-.46318-.36125-.51374-.61323" fill="#fff" opacity="0.3"/>
</svg>`);
export const background = document.createElement("img");
export const backgroundCanvas = document.createElement("canvas");

1
src/types.d.ts vendored
View file

@ -248,6 +248,5 @@ export type OptionDef = {
default: boolean;
name: string;
help: string;
disabled: () => boolean;
};
export type OptionId = keyof typeof options;