This commit is contained in:
Renan LE CARO 2025-03-26 08:35:49 +01:00
parent e3e61b12b8
commit 395968bc52
16 changed files with 1735 additions and 1847 deletions

View file

@ -25,10 +25,6 @@ There's also an easy mode for kids (slower ball).
- people assume unbounded allows for wrap around - people assume unbounded allows for wrap around
- popups not scrollable sometimes - popups not scrollable sometimes
- fdroid build - fdroid build
- deal with too many upgrades :
- disable some upgrades to remove them from the pool
- reroll mechanic
- extra option that just adds 10% to score
- coin magnet and viscosity : only one level ~2.5 - coin magnet and viscosity : only one level ~2.5
- Boost Ascetism : give +2 or even +3 combo per brick destroyed - Boost Ascetism : give +2 or even +3 combo per brick destroyed
- wind : move coins based on puck movement not position - wind : move coins based on puck movement not position

View file

@ -11,8 +11,8 @@ android {
applicationId = "me.lecaro.breakout" applicationId = "me.lecaro.breakout"
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 34
versionCode = 29048147 versionCode = 29049575
versionName = "29048147" versionName = "29049575"
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

149
dist/index.html vendored
View file

@ -631,7 +631,6 @@ var _newGameState = require("./newGameState");
var _asyncAlert = require("./asyncAlert"); var _asyncAlert = require("./asyncAlert");
var _options = require("./options"); var _options = require("./options");
var _getLevelBackground = require("./getLevelBackground"); var _getLevelBackground = require("./getLevelBackground");
var _premium = require("./premium");
function play() { function play() {
if (gameState.running) return; if (gameState.running) return;
gameState.running = true; gameState.running = true;
@ -754,8 +753,8 @@ async function openShortRunUpgradesPicker(gameState) {
count: gameState.rerolls count: gameState.rerolls
}), }),
help: (0, _i18N.t)("level_up.reroll_help"), help: (0, _i18N.t)("level_up.reroll_help"),
value: 'reroll', value: "reroll",
icon: (0, _loadGameData.icons)['icon:reroll'] icon: (0, _loadGameData.icons)["icon:reroll"]
}); });
if (!actions.length) break; if (!actions.length) break;
let textAfterButtons = ` let textAfterButtons = `
@ -790,7 +789,7 @@ async function openShortRunUpgradesPicker(gameState) {
allowClose: false, allowClose: false,
textAfterButtons textAfterButtons
}); });
if (upgradeId === 'reroll') { if (upgradeId === "reroll") {
repeats++; repeats++;
gameState.rerolls--; gameState.rerolls--;
} else { } else {
@ -962,14 +961,7 @@ async function openMainMenu() {
} }
} }
}, },
(0, _premium.premiumMenuEntry)(gameState), // premiumMenuEntry(gameState),
//
// {
// icon: icons["icon:continue"],
// text: t("main_menu.resume"),
// help: t("main_menu.resume_help"),
// value() {},
// },
{ {
text: (0, _i18N.t)("main_menu.settings_title"), text: (0, _i18N.t)("main_menu.settings_title"),
help: (0, _i18N.t)("main_menu.settings_help"), help: (0, _i18N.t)("main_menu.settings_help"),
@ -1343,7 +1335,7 @@ restart(window.location.search.includes("stressTest") ? {
} : {}); } : {});
tick(); tick();
},{"./loadGameData":"l1B4x","./sounds":"dQKPV","./game_utils":"cEeac","./PWA/sw_loader":"2n0gK","./i18n/i18n":"eNPRm","./settings":"5blfu","./gameStateMutators":"9ZeQl","./render":"9AS2t","./recording":"godmD","./newGameState":"aQN6X","./asyncAlert":"rSqLY","./options":"d5NoS","./getLevelBackground":"7OIPf","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3","./premium":"4GEPs"}],"l1B4x":[function(require,module,exports,__globalThis) { },{"./loadGameData":"l1B4x","./sounds":"dQKPV","./game_utils":"cEeac","./PWA/sw_loader":"2n0gK","./i18n/i18n":"eNPRm","./settings":"5blfu","./gameStateMutators":"9ZeQl","./render":"9AS2t","./recording":"godmD","./newGameState":"aQN6X","./asyncAlert":"rSqLY","./options":"d5NoS","./getLevelBackground":"7OIPf","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"l1B4x":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js"); var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports); parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "appVersion", ()=>appVersion); parcelHelpers.export(exports, "appVersion", ()=>appVersion);
@ -1384,7 +1376,7 @@ const upgrades = (0, _upgrades.rawUpgrades).map((u)=>({
})); }));
},{"./data/palette.json":"ktRBU","./data/levels.json":"8JSUc","./data/version.json":"iyP6E","./upgrades":"1u3Dx","./getLevelBackground":"7OIPf","./levelIcon":"6rQoT","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"iyP6E":[function(require,module,exports,__globalThis) { },{"./data/palette.json":"ktRBU","./data/levels.json":"8JSUc","./data/version.json":"iyP6E","./upgrades":"1u3Dx","./getLevelBackground":"7OIPf","./levelIcon":"6rQoT","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"iyP6E":[function(require,module,exports,__globalThis) {
module.exports = JSON.parse("\"29048147\""); module.exports = JSON.parse("\"29049575\"");
},{}],"1u3Dx":[function(require,module,exports,__globalThis) { },{}],"1u3Dx":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js"); var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
@ -4385,8 +4377,8 @@ function newGameState(params) {
autoCleanUses: 0, autoCleanUses: 0,
...(0, _gameUtils.defaultSounds)(), ...(0, _gameUtils.defaultSounds)(),
isAdventureMode: !!params?.adventure, isAdventureMode: !!params?.adventure,
adventurePath: '', adventurePath: "",
seed: 'Seed' + Math.random(), seed: "Seed" + Math.random(),
rerolls: 0 rerolls: 0
}; };
(0, _gameStateMutators.resetBalls)(gameState); (0, _gameStateMutators.resetBalls)(gameState);
@ -4400,130 +4392,7 @@ function newGameState(params) {
return gameState; return gameState;
} }
},{"./settings":"5blfu","./loadGameData":"l1B4x","./game_utils":"cEeac","./gameStateMutators":"9ZeQl","./options":"d5NoS","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"4GEPs":[function(require,module,exports,__globalThis) { },{"./settings":"5blfu","./loadGameData":"l1B4x","./game_utils":"cEeac","./gameStateMutators":"9ZeQl","./options":"d5NoS","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}]},["gVqJ6","67XFf"], "67XFf", "parcelRequire94c2")
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "isPremium", ()=>isPremium);
parcelHelpers.export(exports, "premiumMenuEntry", ()=>premiumMenuEntry);
var _loadGameData = require("./loadGameData");
var _i18N = require("./i18n/i18n");
var _settings = require("./settings");
var _asyncAlert = require("./asyncAlert");
var _game = require("./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) {
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, pem) {
// 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((0, _settings.getSettingValue)('license', '')).then();
async function checkKey(key) {
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;
}
}
function isPremium() {
return premium;
}
function premiumMenuEntry(gameState) {
if (isPremium()) return {
icon: (0, _loadGameData.icons)["icon:adventure_mode"],
text: (0, _i18N.t)("premium.adventure_mode"),
help: (0, _i18N.t)("premium.adventure_mode_help"),
value: async ()=>{
if (await (0, _game.confirmRestart)(gameState)) (0, _game.restart)({
adventure: true
});
}
};
return {
icon: (0, _loadGameData.icons)["icon:premium"],
text: (0, _i18N.t)("premium.title"),
help: (0, _i18N.t)("premium.short_help"),
value: ()=>openPremiumMenu('')
};
}
async function openPremiumMenu(text) {
const isGooglePlayInstall = new URLSearchParams(location.search).get('source') === 'com.android.vending';
const cb = await (0, _asyncAlert.asyncAlert)({
title: (0, _i18N.t)("premium.title"),
text: text || isGooglePlayInstall && (0, _i18N.t)("premium.help_google") || (0, _i18N.t)("premium.help"),
actions: [
{
text: (0, _i18N.t)("premium.buy"),
disabled: isGooglePlayInstall,
help: isGooglePlayInstall ? (0, _i18N.t)("premium.buy_disabled_help") : (0, _i18N.t)("premium.buy_help"),
value () {
window.open('https://licenses.lecaro.me/buy/price_1R6YaEGRf74lr2EkSo2GPvuO', '_blank');
}
},
{
text: (0, _i18N.t)("premium.enter"),
help: (0, _i18N.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 {
(0, _settings.setSettingValue)('license', value);
(0, _game.openMainMenu)().then();
}
}
},
{
text: (0, _i18N.t)("premium.back"),
help: (0, _i18N.t)("premium.back_help"),
value () {
(0, _game.openMainMenu)().then();
}
}
]
});
if (cb) cb();
}
},{"@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3","./loadGameData":"l1B4x","./i18n/i18n":"eNPRm","./settings":"5blfu","./asyncAlert":"rSqLY","./game":"edeGs"}]},["gVqJ6","67XFf"], "67XFf", "parcelRequire94c2")
</script> </script>
</body> </body>

View file

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

View file

@ -1,30 +1,26 @@
import { GameState } from "./types"; import { GameState } from "./types";
export async function openAdventureRunUpgradesPicker(gameState: GameState) { export async function openAdventureRunUpgradesPicker(gameState: GameState) {
let options=3 let options = 3;
const catchRate = const catchRate =
(gameState.score - gameState.levelStartScore) / (gameState.score - gameState.levelStartScore) /
(gameState.levelSpawnedCoins || 1); (gameState.levelSpawnedCoins || 1);
if (gameState.levelWallBounces == 0) { if (gameState.levelWallBounces == 0) {
options++; options++;
} }
if (gameState.levelTime < 30 * 1000) { if (gameState.levelTime < 30 * 1000) {
options++ options++;
} }
if (catchRate === 1) { if (catchRate === 1) {
options++ options++;
} }
if (gameState.levelMisses === 0) { if (gameState.levelMisses === 0) {
options++ options++;
} }
const choices = [] const choices = [];
for (let difficulty = 0; difficulty < options; difficulty++) { for (let difficulty = 0; difficulty < options; difficulty++) {
choices.push({ choices.push({});
})
} }
} }

View file

@ -1 +1 @@
"29048147" "29049575"

View file

@ -12,7 +12,13 @@ import {
Upgrade, Upgrade,
} from "./types"; } from "./types";
import { getAudioContext, playPendingSounds } from "./sounds"; import { getAudioContext, playPendingSounds } from "./sounds";
import {currentLevelInfo, getRowColIndex, levelsListHTMl, max_levels, pickedUpgradesHTMl,} from "./game_utils"; import {
currentLevelInfo,
getRowColIndex,
levelsListHTMl,
max_levels,
pickedUpgradesHTMl,
} from "./game_utils";
import "./PWA/sw_loader"; import "./PWA/sw_loader";
import { getCurrentLang, t } from "./i18n/i18n"; import { getCurrentLang, t } from "./i18n/i18n";
@ -34,10 +40,26 @@ import {
setLevel, setLevel,
setMousePos, setMousePos,
} from "./gameStateMutators"; } from "./gameStateMutators";
import {backgroundCanvas, ctx, gameCanvas, render, scoreDisplay,} from "./render"; import {
import {pauseRecording, recordOneFrame, resumeRecording, startRecordingGame,} from "./recording"; backgroundCanvas,
ctx,
gameCanvas,
render,
scoreDisplay,
} from "./render";
import {
pauseRecording,
recordOneFrame,
resumeRecording,
startRecordingGame,
} from "./recording";
import { newGameState } from "./newGameState"; import { newGameState } from "./newGameState";
import {alertsOpen, asyncAlert, AsyncAlertAction, closeModal,} from "./asyncAlert"; import {
alertsOpen,
asyncAlert,
AsyncAlertAction,
closeModal,
} from "./asyncAlert";
import { isOptionOn, options, toggleOption } from "./options"; import { isOptionOn, options, toggleOption } from "./options";
import { hashCode } from "./getLevelBackground"; import { hashCode } from "./getLevelBackground";
import { premiumMenuEntry } from "./premium"; import { premiumMenuEntry } from "./premium";
@ -174,52 +196,50 @@ export async function openShortRunUpgradesPicker(gameState: GameState) {
if (gameState.levelWallBounces == 0) { if (gameState.levelWallBounces == 0) {
repeats++; repeats++;
gameState.rerolls++ gameState.rerolls++;
wallHitsGain = t("level_up.plus_one_upgrade"); wallHitsGain = t("level_up.plus_one_upgrade");
} else if (gameState.levelWallBounces < 5) { } else if (gameState.levelWallBounces < 5) {
gameState.rerolls++ gameState.rerolls++;
wallHitsGain = t("level_up.plus_one_choice"); wallHitsGain = t("level_up.plus_one_choice");
} }
if (gameState.levelTime < 30 * 1000) { if (gameState.levelTime < 30 * 1000) {
repeats++; repeats++;
gameState.rerolls++ gameState.rerolls++;
timeGain = t("level_up.plus_one_upgrade"); timeGain = t("level_up.plus_one_upgrade");
} else if (gameState.levelTime < 60 * 1000) { } else if (gameState.levelTime < 60 * 1000) {
gameState.rerolls++ gameState.rerolls++;
timeGain = t("level_up.plus_one_choice"); timeGain = t("level_up.plus_one_choice");
} }
if (catchRate === 1) { if (catchRate === 1) {
repeats++; repeats++;
gameState.rerolls++ gameState.rerolls++;
catchGain = t("level_up.plus_one_upgrade"); catchGain = t("level_up.plus_one_upgrade");
} else if (catchRate > 0.9) { } else if (catchRate > 0.9) {
gameState.rerolls++ gameState.rerolls++;
catchGain = t("level_up.plus_one_choice"); catchGain = t("level_up.plus_one_choice");
} }
if (gameState.levelMisses === 0) { if (gameState.levelMisses === 0) {
repeats++; repeats++;
gameState.rerolls++ gameState.rerolls++;
missesGain = t("level_up.plus_one_upgrade"); missesGain = t("level_up.plus_one_upgrade");
} else if (gameState.levelMisses <= 3) { } else if (gameState.levelMisses <= 3) {
gameState.rerolls++ gameState.rerolls++;
missesGain = t("level_up.plus_one_choice"); missesGain = t("level_up.plus_one_choice");
} }
while (repeats--) { while (repeats--) {
const actions = pickRandomUpgrades( const actions = pickRandomUpgrades(
gameState, gameState,
3 + 3 + gameState.perks.one_more_choice - gameState.perks.instant_upgrade,
gameState.perks.one_more_choice -
gameState.perks.instant_upgrade,
); );
if (gameState.rerolls) { if (gameState.rerolls) {
actions.push({ actions.push({
text: t("level_up.reroll", { count: gameState.rerolls }), text: t("level_up.reroll", { count: gameState.rerolls }),
help: t("level_up.reroll_help"), help: t("level_up.reroll_help"),
value: 'reroll', value: "reroll",
icon: icons['icon:reroll'] icon: icons["icon:reroll"],
}) });
} }
if (!actions.length) break; if (!actions.length) break;
let textAfterButtons = ` let textAfterButtons = `
@ -242,7 +262,7 @@ export async function openShortRunUpgradesPicker(gameState: GameState) {
t("level_up.compliment_good")) || t("level_up.compliment_good")) ||
t("level_up.compliment_advice"); t("level_up.compliment_advice");
const upgradeId = (await asyncAlert<PerkId|'reroll'>({ const upgradeId = (await asyncAlert<PerkId | "reroll">({
title: title:
t("level_up.pick_upgrade_title") + t("level_up.pick_upgrade_title") +
(repeats ? " (" + (repeats + 1) + ")" : ""), (repeats ? " (" + (repeats + 1) + ")" : ""),
@ -267,9 +287,9 @@ export async function openShortRunUpgradesPicker(gameState: GameState) {
textAfterButtons, textAfterButtons,
})) as PerkId; })) as PerkId;
if(upgradeId==='reroll'){ if (upgradeId === "reroll") {
repeats++ repeats++;
gameState.rerolls-- gameState.rerolls--;
} else { } else {
gameState.perks[upgradeId]++; gameState.perks[upgradeId]++;
if (upgradeId === "instant_upgrade") { if (upgradeId === "instant_upgrade") {
@ -280,7 +300,6 @@ export async function openShortRunUpgradesPicker(gameState: GameState) {
} }
} }
gameCanvas.addEventListener("mouseup", (e) => { gameCanvas.addEventListener("mouseup", (e) => {
if (e.button !== 0) return; if (e.button !== 0) return;
if (gameState.running) { if (gameState.running) {
@ -514,15 +533,7 @@ export async function openMainMenu() {
}, },
}, },
premiumMenuEntry(gameState) // premiumMenuEntry(gameState),
,
//
// {
// icon: icons["icon:continue"],
// text: t("main_menu.resume"),
// help: t("main_menu.resume_help"),
// value() {},
// },
{ {
text: t("main_menu.settings_title"), text: t("main_menu.settings_title"),
help: t("main_menu.settings_help"), help: t("main_menu.settings_help"),
@ -765,7 +776,11 @@ async function openSettingsMenu() {
], ],
allowClose: true, allowClose: true,
}); });
if (pick && pick !== getCurrentLang() && (await confirmRestart(gameState))) { if (
pick &&
pick !== getCurrentLang() &&
(await confirmRestart(gameState))
) {
setSettingValue("lang", pick); setSettingValue("lang", pick);
window.location.reload(); window.location.reload();
} }
@ -852,7 +867,7 @@ Click an item above to start a run with it.
</p>`, </p>`,
actions, actions,
allowClose: true, allowClose: true,
actionsAsGrid:true actionsAsGrid: true,
}); });
if (tryOn) { if (tryOn) {
if (await confirmRestart(gameState)) { if (await confirmRestart(gameState)) {

View file

@ -31,10 +31,22 @@ import {
import { t } from "./i18n/i18n"; import { t } from "./i18n/i18n";
import { icons } from "./loadGameData"; import { icons } from "./loadGameData";
import {addToTotalScore, getCurrentMaxCoins, getCurrentMaxParticles,} from "./settings"; import {
addToTotalScore,
getCurrentMaxCoins,
getCurrentMaxParticles,
} from "./settings";
import { background } from "./render"; import { background } from "./render";
import { gameOver } from "./gameOver"; import { gameOver } from "./gameOver";
import {brickIndex, fitSize, gameState, hasBrick, hitsSomething, openShortRunUpgradesPicker, pause,} from "./game"; import {
brickIndex,
fitSize,
gameState,
hasBrick,
hitsSomething,
openShortRunUpgradesPicker,
pause,
} from "./game";
import { stopRecording } from "./recording"; import { stopRecording } from "./recording";
import { isOptionOn } from "./options"; import { isOptionOn } from "./options";
import { openAdventureRunUpgradesPicker } from "./adventure"; import { openAdventureRunUpgradesPicker } from "./adventure";
@ -566,7 +578,6 @@ export async function setLevel(gameState: GameState, l: number) {
if (gameState.isCreativeModeRun) { if (gameState.isCreativeModeRun) {
await openAdventureRunUpgradesPicker(gameState); await openAdventureRunUpgradesPicker(gameState);
} else { } else {
await openShortRunUpgradesPicker(gameState); await openShortRunUpgradesPicker(gameState);
} }
} }
@ -1386,12 +1397,9 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
if (gameState.perks.extra_life < 0) { if (gameState.perks.extra_life < 0) {
gameState.perks.extra_life = 0; gameState.perks.extra_life = 0;
} else if (gameState.perks.sacrifice) { } else if (gameState.perks.sacrifice) {
gameState.bricks.forEach((color, index) => color && explodeBrick( gameState.bricks.forEach(
gameState, (color, index) => color && explodeBrick(gameState, index, ball, true),
index, );
ball,
true,
))
} }
schedulGameSound(gameState, "lifeLost", ball.x, 1); schedulGameSound(gameState, "lifeLost", ball.x, 1);

View file

@ -963,7 +963,7 @@
</translation> </translation>
<translation> <translation>
<language>fr-FR</language> <language>fr-FR</language>
<approved>false</approved> <approved>true</approved>
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
@ -978,7 +978,7 @@
</translation> </translation>
<translation> <translation>
<language>fr-FR</language> <language>fr-FR</language>
<approved>false</approved> <approved>true</approved>
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
@ -1453,7 +1453,7 @@
</translation> </translation>
<translation> <translation>
<language>fr-FR</language> <language>fr-FR</language>
<approved>false</approved> <approved>true</approved>
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
@ -1468,7 +1468,7 @@
</translation> </translation>
<translation> <translation>
<language>fr-FR</language> <language>fr-FR</language>
<approved>false</approved> <approved>true</approved>
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
@ -1483,7 +1483,7 @@
</translation> </translation>
<translation> <translation>
<language>fr-FR</language> <language>fr-FR</language>
<approved>false</approved> <approved>true</approved>
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
@ -1498,7 +1498,7 @@
</translation> </translation>
<translation> <translation>
<language>fr-FR</language> <language>fr-FR</language>
<approved>false</approved> <approved>true</approved>
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
@ -1513,7 +1513,7 @@
</translation> </translation>
<translation> <translation>
<language>fr-FR</language> <language>fr-FR</language>
<approved>false</approved> <approved>true</approved>
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
@ -1528,7 +1528,7 @@
</translation> </translation>
<translation> <translation>
<language>fr-FR</language> <language>fr-FR</language>
<approved>false</approved> <approved>true</approved>
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
@ -1543,7 +1543,7 @@
</translation> </translation>
<translation> <translation>
<language>fr-FR</language> <language>fr-FR</language>
<approved>false</approved> <approved>true</approved>
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
@ -1558,7 +1558,7 @@
</translation> </translation>
<translation> <translation>
<language>fr-FR</language> <language>fr-FR</language>
<approved>false</approved> <approved>true</approved>
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
@ -1573,7 +1573,7 @@
</translation> </translation>
<translation> <translation>
<language>fr-FR</language> <language>fr-FR</language>
<approved>false</approved> <approved>true</approved>
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
@ -1588,7 +1588,7 @@
</translation> </translation>
<translation> <translation>
<language>fr-FR</language> <language>fr-FR</language>
<approved>false</approved> <approved>true</approved>
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
@ -1603,7 +1603,7 @@
</translation> </translation>
<translation> <translation>
<language>fr-FR</language> <language>fr-FR</language>
<approved>false</approved> <approved>true</approved>
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
@ -1618,7 +1618,7 @@
</translation> </translation>
<translation> <translation>
<language>fr-FR</language> <language>fr-FR</language>
<approved>false</approved> <approved>true</approved>
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
@ -1633,7 +1633,7 @@
</translation> </translation>
<translation> <translation>
<language>fr-FR</language> <language>fr-FR</language>
<approved>false</approved> <approved>true</approved>
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>

View file

@ -21,7 +21,7 @@ describe("json data checks", () => {
.split("") .split("")
.filter((b) => b !== "_" && b !== "black") .filter((b) => b !== "_" && b !== "black")
.filter((a, b, c) => c.indexOf(a) === b); .filter((a, b, c) => c.indexOf(a) === b);
return uniqueBricks.length > 5; return uniqueBricks.length > 5 && !l.name.startsWith("icon:");
}) })
.map((l) => l.name); .map((l) => l.name);
expect(levelsWithManyBrickColors).toEqual([]); expect(levelsWithManyBrickColors).toEqual([]);

View file

@ -102,9 +102,9 @@ export function newGameState(params: RunParams): GameState {
...defaultSounds(), ...defaultSounds(),
isAdventureMode: !!params?.adventure, isAdventureMode: !!params?.adventure,
adventurePath:'', adventurePath: "",
seed:'Seed'+Math.random(), seed: "Seed" + Math.random(),
rerolls:0 rerolls: 0,
}; };
resetBalls(gameState); resetBalls(gameState);

View file

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