Starting adventure mode

This commit is contained in:
Renan LE CARO 2025-03-26 08:01:12 +01:00
parent a134821a94
commit 0ada53a063
13 changed files with 818 additions and 197 deletions

View file

@ -972,7 +972,7 @@
{
"name": "icon:unlocks",
"size": 7,
"bricks": "eeee___e__e___e__e______llll___llll___llll___llll",
"bricks": "eeee___e__e___e__e______ctCb___Gbsc___tOGO___OCbs",
"svg": null,
"color": ""
},
@ -1017,5 +1017,33 @@
"bricks": "___W____y___W_y______W___y____W_y______W___y____W______W_W_WWW_WW_W_WWWWWW_W_WWWW",
"svg": null,
"color": ""
},
{
"name": "icon:premium",
"size": 11,
"bricks": "________________y_________yey_________y______yy_yey_yy_yeeyeeeyeeyyeeyeeeyeey_yeyeeeyey___yyyyyyy____yyyyyyy_____________",
"svg": 11,
"color": ""
},
{
"name": "icon:premium_active",
"size": 11,
"bricks": "__y____y___y____y____y_y__yby__y______y______yy_yty_yy_ybbytttybbyybbytttybby_ybytttyby___yyyyyyy____yyyyyyy_____________",
"svg": null,
"color": ""
},
{
"name": "icon:adventure_mode",
"size": 17,
"bricks": "________aaaaaaaaa_______a_____________aaaabbbbbbbbb___a___t___________a_____ttttttttt__a________________a_____sssssssss__a____s_________WWWsssssrrrrrrrrr__c____R___________c_____RRRRRRRRR__c________________c_____kkkkkkkkk___c___k_____________ccccGGGGGGGGG_______c_________________ccccccccc",
"svg": null,
"color": ""
},
{
"name": "icon:7_levels_run",
"size": 7,
"bricks": "___a______at__cGCa_b_c_____ycGCa_b____at_____a___",
"svg": null,
"color": ""
}
]
]

View file

@ -62,6 +62,8 @@ import {
} from "./asyncAlert";
import { isOptionOn, options, toggleOption } from "./options";
import { hashCode } from "./getLevelBackground";
import {hasUncaughtExceptionCaptureCallback} from "process";
import {premiumMenuEntry} from "./premium";
export function play() {
if (gameState.running) return;
@ -453,17 +455,17 @@ async function openScorePanel() {
},
);
async function openMainMenu() {
export async function openMainMenu() {
pause(true);
const creativeModeThreshold = Math.max(...upgrades.map((u) => u.threshold));
const actions: AsyncAlertAction<() => void>[] = [
{
text: t("main_menu.settings_title"),
help: t("main_menu.settings_help"),
icon: icons["icon:settings"],
value() {
openSettingsMenu();
{
icon: icons["icon:7_levels_run"],
text: t("main_menu.normal"),
help: t("main_menu.normal_help"),
value: () => {
restart({ levelToAvoid: currentLevelInfo(gameState).name });
},
},
{
@ -522,20 +524,23 @@ async function openMainMenu() {
},
},
premiumMenuEntry(gameState)
,
//
// {
// icon: icons["icon:continue"],
// text: t("main_menu.resume"),
// help: t("main_menu.resume_help"),
// value() {},
// },
{
icon: icons["icon:restart"],
text: t("score_panel.restart"),
help: t("score_panel.restart_help"),
value: () => {
restart({ levelToAvoid: currentLevelInfo(gameState).name });
text: t("main_menu.settings_title"),
help: t("main_menu.settings_help"),
icon: icons["icon:settings"],
value() {
openSettingsMenu();
},
},
{
icon: icons["icon:continue"],
text: t("main_menu.resume"),
help: t("main_menu.resume_help"),
value() {},
},
];
const cb = await asyncAlert<() => void>({
@ -770,7 +775,7 @@ async function openSettingsMenu() {
],
allowClose: true,
});
if (pick && pick !== getCurrentLang() && (await confirmRestart())) {
if (pick && pick !== getCurrentLang() && (await confirmRestart(gameState))) {
setSettingValue("lang", pick);
window.location.reload();
}
@ -794,11 +799,11 @@ async function openSettingsMenu() {
},
});
actions.push({
text: t("main_menu.resume"),
help: t("main_menu.resume_help"),
value() {},
});
// actions.push({
// text: t("main_menu.resume"),
// help: t("main_menu.resume_help"),
// value() {},
// });
const cb = await asyncAlert<() => void>({
title: t("main_menu.settings_title"),
text: t("main_menu.settings_help"),
@ -816,10 +821,10 @@ async function openUnlocksList() {
const actions = [
...upgrades
.sort((a, b) => a.threshold - b.threshold)
.map(({ name, id, threshold, icon, fullHelp }) => ({
.map(({ name, id, threshold, icon, help }) => ({
text: name,
help:
ts >= threshold ? fullHelp : t("unlocks.unlocks_at", { threshold }),
// help:
// ts >= threshold ? help(1) : t("unlocks.unlocks_at", { threshold }),
disabled: ts < threshold,
value: { perks: { [id]: 1 } } as RunParams,
icon,
@ -830,12 +835,12 @@ async function openUnlocksList() {
const available = ts >= l.threshold;
return {
text: l.name,
help: available
? t("unlocks.level_description", {
size: l.size,
bricks: l.bricks.filter((i) => i).length,
})
: t("unlocks.unlocks_at", { threshold: l.threshold }),
// help: available
// ? t("unlocks.level_description", {
// size: l.size,
// bricks: l.bricks.filter((i) => i).length,
// })
// : t("unlocks.unlocks_at", { threshold: l.threshold }),
disabled: !available,
value: { level: l.name } as RunParams,
icon: icons[l.name],
@ -857,15 +862,16 @@ Click an item above to start a run with it.
</p>`,
actions,
allowClose: true,
actionsAsGrid:true
});
if (tryOn) {
if (await confirmRestart()) {
if (await confirmRestart(gameState)) {
restart(tryOn);
}
}
}
export async function confirmRestart() {
export async function confirmRestart(gameState) {
if (!gameState.currentLevel) return true;
return asyncAlert({
@ -961,7 +967,7 @@ document.addEventListener("keyup", async (e) => {
} else if (e.key.toLowerCase() === "s" && !alertsOpen) {
openScorePanel().then();
} else if (e.key.toLowerCase() === "r" && !alertsOpen) {
if (await confirmRestart()) {
if (await confirmRestart(gameState)) {
restart({ levelToAvoid: currentLevelInfo(gameState).name });
}
} else {

View file

@ -922,6 +922,36 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>normal</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>normal_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>pointer_lock</name>
<description/>
@ -1379,6 +1409,206 @@
</concept_node>
</children>
</folder_node>
<folder_node>
<name>premium</name>
<children>
<concept_node>
<name>adventure_mode</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>adventure_mode_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>back</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>back_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>buy</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>buy_disabled_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>buy_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>enter</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>enter_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>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>help_google</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>short_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>title</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>
</children>
</folder_node>
<folder_node>
<name>sandbox</name>
<children>

View file

@ -57,6 +57,8 @@
"main_menu.max_particles_help": "Limits the number of particles show on screen for visual effect. ",
"main_menu.mobile": "Mobile mode",
"main_menu.mobile_help": "Leaves space under the puck.",
"main_menu.normal": "New 7 levels run",
"main_menu.normal_help": "Start a quick run with random perk",
"main_menu.pointer_lock": "Mouse pointer lock",
"main_menu.pointer_lock_help": "Locks and hides the mouse cursor.",
"main_menu.record": "Record gameplay videos",
@ -65,7 +67,7 @@
"main_menu.reset": "Reset Game",
"main_menu.reset_cancel": "No",
"main_menu.reset_confirm": "Yes",
"main_menu.reset_help": "Erase high score and statistics",
"main_menu.reset_help": "Erase high score, license and statistics",
"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",
@ -80,13 +82,26 @@
"main_menu.sounds": "Game sounds",
"main_menu.sounds_help": "Can slow down some phones.",
"main_menu.title": "Breakout 71",
"main_menu.unlocks": "Starting perk",
"main_menu.unlocks": "Unlocked content",
"main_menu.unlocks_help": "Try perks and levels you unlocked",
"play.close_modale_window_tooltip": "close ",
"play.current_lvl": "L{{level}}/{{max}}",
"play.menu_label": "menu",
"play.missed_ball": "miss",
"play.mobile_press_to_play": "Press and hold here to play",
"premium.adventure_mode": "Adventure mode",
"premium.adventure_mode_help": "Start a new game in adventure mode",
"premium.back": "Back",
"premium.back_help": "Return to main menu",
"premium.buy": "Buy a license key",
"premium.buy_disabled_help": "Coming soon",
"premium.buy_help": "You'll be taken to a stripe form to pay and will receive the license by email. Come back to enter it here after.",
"premium.enter": "Enter license key",
"premium.enter_help": "Paste the license in the window that opens",
"premium.help": "Buy a license for Breakout 71 to unlock infinite mode and support development. It costs 4.99€ and lasts forever. You can use it on multiple devices, but please don't share it online. ",
"premium.help_google": "While I do plan to offer premium licenses through google play, I haven't gotten around it yet, so there's no buy link here. If you already have a license key, you can enter it below. ",
"premium.short_help": "Play as long as possible",
"premium.title": "Adventure mode",
"sandbox.help": "Test any perk combination",
"sandbox.instructions": "Select perks below and press \"start run\" to try them out in a test run. Scores and stats are not recorded.",
"sandbox.start": "Start test run",

View file

@ -57,6 +57,8 @@
"main_menu.max_particles_help": "Limite le nombre de particules affichées à l'écran pour les effets visuels",
"main_menu.mobile": "Mode mobile",
"main_menu.mobile_help": "Laisse un espace sous le palet.",
"main_menu.normal": "",
"main_menu.normal_help": "",
"main_menu.pointer_lock": "Verrouillage du pointeur",
"main_menu.pointer_lock_help": "Cache aussi le curseur de la souris.",
"main_menu.record": "Enregistrer des vidéos de jeu",
@ -65,7 +67,7 @@
"main_menu.reset": "Réinitialiser le jeu",
"main_menu.reset_cancel": "Non",
"main_menu.reset_confirm": "Oui",
"main_menu.reset_help": "Effacer les scores et statistiques",
"main_menu.reset_help": "Effacer les scores, statistiques et licences",
"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",
@ -80,13 +82,26 @@
"main_menu.sounds": "Sons du jeu",
"main_menu.sounds_help": "Ralentis certains téléphones.",
"main_menu.title": "Breakout 71",
"main_menu.unlocks": "Améliorations et niveaux",
"main_menu.unlocks": "Contenu débloqué",
"main_menu.unlocks_help": "Essayez les éléments débloqués",
"play.close_modale_window_tooltip": "Fermer",
"play.current_lvl": "Niveau {{level}}/{{max}}",
"play.menu_label": "Menu",
"play.missed_ball": "raté",
"play.mobile_press_to_play": "Gardez le doigt ici pour jouer",
"premium.adventure_mode": "",
"premium.adventure_mode_help": "",
"premium.back": "",
"premium.back_help": "",
"premium.buy": "",
"premium.buy_disabled_help": "",
"premium.buy_help": "",
"premium.enter": "",
"premium.enter_help": "",
"premium.help": "",
"premium.help_google": "",
"premium.short_help": "",
"premium.title": "",
"sandbox.help": "Tester n'importe quelle combinaison d'améliorations",
"sandbox.instructions": "Sélectionnez les amélioration ci-dessous et appuyez sur \"Démarrer la partie de test\" pour les tester. Les scores et les statistiques ne seront pas enregistrés.",
"sandbox.start": "Démarrer la partie de test",

View file

@ -12,7 +12,7 @@ export function levelIconHTML(
levelSize: number,
color: string,
) {
const size = 40;
const size = 46;
const c = levelIconHTMLCanvas;
const ctx = levelIconHTMLCanvasCtx;

View file

@ -28,7 +28,7 @@ export function newGameState(params: RunParams): GameState {
const perks = { ...makeEmptyPerksMap(upgrades), ...(params?.perks || {}) };
const gameState: GameState = {
const gameState: GameState= {
runLevels,
currentLevel: 0,
upgradesOfferedFor: -1,
@ -42,6 +42,7 @@ export function newGameState(params: RunParams): GameState {
ballStickToPuck: true,
puckPosition: 400,
lastPuckPosition: 400,
lastPuckMove: 0,
pauseTimeout: null,
canvasWidth: 0,
canvasHeight: 0,
@ -99,6 +100,11 @@ export function newGameState(params: RunParams): GameState {
needsRender: true,
autoCleanUses: 0,
...defaultSounds(),
isAdventureMode:!!params?.adventure,
adventurePath:'',
seed:'Seed'+Math.random()
};
resetBalls(gameState);

161
src/premium.ts Normal file
View file

@ -0,0 +1,161 @@
import {GameState} from "./types";
import {icons} from "./loadGameData";
import {t} from "./i18n/i18n";
import {getSettingValue, setSettingValue} from "./settings";
import {asyncAlert} from "./asyncAlert";
import {confirmRestart, openMainMenu, restart} from "./game";
const publicKeyString = `-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyGQJgs6gxa0Fd86TuZ2q
rGQ5ArSn8ug4VIKezru1QhIEkXeOT1lYXOLEryWaVUwXfOa9sVlKAGJY5y0TarAY
NF2m67ME8yzNPIoZWbKXutJ3CSCXNTjAqAxHgz7H+qxbNGZXAXw+ta8+PuZDzcCI
LbXT1u3/i0ahhA2Erdpv9XQBazKZt5AKzU31XhEEFh1jXZyk9D4XbatYXtvEwaJx
eSWmjSxJ6SJb6oH2mwm8V4E0PxYVIa0yX3cPgGuR0pZPMleOTc6o0T24I2AUQb0d
FckdFrr5U8bFIf/nwncMYVVNgt1vh88EuzWLjpc52nLrdOkVQNpiCN2uMgBBXQB7
iseIfdkGF0A4DBn8qdieDvaSY8zeRW/nAce4FNBidU1SebNRnIU9f/XpA493lJW+
Y/zXQBbmX/uSmeZDP4fjhKZv0Qa0ZeGzZiTdBKKb0BlIg/VYFFsqPytUVVyesO4J
RCASTIjXW61E7PQKir5qIXwkQDlzJ+bpZ3PHyAvspRrBaDxIYvEEw14evpuqOgS+
v/IlgPe+CWSvZa9xxnQl/aWZrOrD7syu6KKCbgUyXEm+Alp0YT3e6nwjn0qiM/cj
dZpWPx3O+rZbRQb0gHcvN4+n2Y7fWAeC9mxVZtADqvVr/GTumMbLj7DdhWtt1Ogu
4EcvkQ5SKCL0JC93DyctjOMCAwEAAQ==
-----END PUBLIC KEY-----`
function pemToArrayBuffer(pem: string) {
const b64 = pem
.replace(/-----BEGIN PUBLIC KEY-----/, '')
.replace(/-----END PUBLIC KEY-----/, '')
.replace(/\s+/g, '');
const binaryDerString = atob(b64);
const binaryDer = new Uint8Array(binaryDerString.length);
for (let i = 0; i < binaryDerString.length; i++) {
binaryDer[i] = binaryDerString.charCodeAt(i);
}
return binaryDer.buffer;
}
async function getPriceId(key: string, pem: string) {
// Split the key into its components
const [priceId, timestamp, signature] = key.split(':');
const data = `${priceId}:${timestamp}`;
const publicKeyBuffer = pemToArrayBuffer(pem);
const publicKey = await crypto.subtle.importKey(
'spki',
publicKeyBuffer,
{
name: 'RSA-PSS',
hash: 'SHA-256',
},
true,
['verify']
);
// Verify the signature using ECDSA
const isValid = await crypto.subtle.verify(
{
name: 'RSA-PSS',
saltLength: 32,
},
publicKey,
new Uint8Array(Array.from(atob(signature), c => c.charCodeAt(0))),
new TextEncoder().encode(data)
);
if (!isValid) throw new Error("Invalid key signature")
return priceId;
}
let premium = false
const gamePriceId = 'price_1R6YaEGRf74lr2EkSo2GPvuO'
checkKey(getSettingValue('license', '')).then()
async function checkKey(key: string) {
if (!key) return 'No key'
try {
if (gamePriceId !== await getPriceId(key, publicKeyString)) {
return 'Wrong product'
}
premium = true
return ''
} catch (e) {
return 'Could not upgrade : ' + e.message
}
}
export function isPremium() {
return premium
}
export function premiumMenuEntry(gameState: GameState) {
if (isPremium()) {
return {
icon: icons["icon:adventure_mode"],
text: t("premium.adventure_mode"),
help: t("premium.adventure_mode_help"),
value: async () => {
if (await confirmRestart(gameState)) {
restart({
adventure: true
})
}
},
}
}
return {
icon: icons["icon:premium"],
text: t("premium.title"),
help: t("premium.short_help"),
value: () => openPremiumMenu(''),
}
}
async function openPremiumMenu(text) {
const isGooglePlayInstall = new URLSearchParams(location.search).get('source') === 'com.android.vending'
const cb = await asyncAlert({
title: t("premium.title"),
text: text || (isGooglePlayInstall && t("premium.help_google")) || t("premium.help"),
actions: [
{
text: t("premium.buy"),
disabled: isGooglePlayInstall,
help: isGooglePlayInstall ? t("premium.buy_disabled_help") : t("premium.buy_help"),
value() {
window.open('https://licenses.lecaro.me/buy/price_1R6YaEGRf74lr2EkSo2GPvuO', '_blank');
}
},
{
text: t("premium.enter"),
help: t("premium.enter_help"),
async value() {
const value = (prompt('Please paste your license key') || '').replace(/\s+/g, '')
const problem = await checkKey(value)
if (problem) {
openPremiumMenu(problem).then()
} else {
setSettingValue('license', value)
openMainMenu().then()
}
}
},
{
text: t("premium.back"),
help: t("premium.back_help"),
value() {
openMainMenu().then()
}
}
]
})
if (cb) cb()
}

View file

@ -538,7 +538,7 @@ export function renderAllBricks() {
let redBecauseOfReach =
gameState.perks.reach &&
countBricksAbove(gameState, index) &&
!countBricksBelow(gameState, index);
!countBricksBelow(gameState, index) ;
let redBorder =
(gameState.ballsColor !== color &&

4
src/types.d.ts vendored
View file

@ -266,12 +266,16 @@ export type GameState = {
coinCatch: { vol: number; x: number };
colorChange: { vol: number; x: number };
};
isAdventureMode:boolean,
adventurePath:string,
seed:string
};
export type RunParams = {
level?: string;
levelToAvoid?: string;
perks?: Partial<PerksMap>;
adventure?:boolean;
};
export type OptionDef = {
default: boolean;