This commit is contained in:
Renan LE CARO 2025-03-26 08:35:49 +01:00
parent e3e61b12b8
commit 08640fa389
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
- popups not scrollable sometimes
- 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
- Boost Ascetism : give +2 or even +3 combo per brick destroyed
- wind : move coins based on puck movement not position

View file

@ -11,8 +11,8 @@ android {
applicationId = "me.lecaro.breakout"
minSdk = 21
targetSdk = 34
versionCode = 29048147
versionName = "29048147"
versionCode = 29049575
versionName = "29049575"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
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 _options = require("./options");
var _getLevelBackground = require("./getLevelBackground");
var _premium = require("./premium");
function play() {
if (gameState.running) return;
gameState.running = true;
@ -754,8 +753,8 @@ async function openShortRunUpgradesPicker(gameState) {
count: gameState.rerolls
}),
help: (0, _i18N.t)("level_up.reroll_help"),
value: 'reroll',
icon: (0, _loadGameData.icons)['icon:reroll']
value: "reroll",
icon: (0, _loadGameData.icons)["icon:reroll"]
});
if (!actions.length) break;
let textAfterButtons = `
@ -790,7 +789,7 @@ async function openShortRunUpgradesPicker(gameState) {
allowClose: false,
textAfterButtons
});
if (upgradeId === 'reroll') {
if (upgradeId === "reroll") {
repeats++;
gameState.rerolls--;
} else {
@ -962,14 +961,7 @@ async function openMainMenu() {
}
}
},
(0, _premium.premiumMenuEntry)(gameState),
//
// {
// icon: icons["icon:continue"],
// text: t("main_menu.resume"),
// help: t("main_menu.resume_help"),
// value() {},
// },
// premiumMenuEntry(gameState),
{
text: (0, _i18N.t)("main_menu.settings_title"),
help: (0, _i18N.t)("main_menu.settings_help"),
@ -1343,7 +1335,7 @@ restart(window.location.search.includes("stressTest") ? {
} : {});
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");
parcelHelpers.defineInteropFlag(exports);
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) {
module.exports = JSON.parse("\"29048147\"");
module.exports = JSON.parse("\"29049575\"");
},{}],"1u3Dx":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
@ -4385,8 +4377,8 @@ function newGameState(params) {
autoCleanUses: 0,
...(0, _gameUtils.defaultSounds)(),
isAdventureMode: !!params?.adventure,
adventurePath: '',
seed: 'Seed' + Math.random(),
adventurePath: "",
seed: "Seed" + Math.random(),
rerolls: 0
};
(0, _gameStateMutators.resetBalls)(gameState);
@ -4400,130 +4392,7 @@ function newGameState(params) {
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) {
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")
},{"./settings":"5blfu","./loadGameData":"l1B4x","./game_utils":"cEeac","./gameStateMutators":"9ZeQl","./options":"d5NoS","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}]},["gVqJ6","67XFf"], "67XFf", "parcelRequire94c2")
</script>
</body>

View file

@ -1,5 +1,5 @@
// The version of the cache.
const VERSION = "29048147";
const VERSION = "29049575";
// The name of the cache
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) {
let options=3
const catchRate =
let options = 3;
const catchRate =
(gameState.score - gameState.levelStartScore) /
(gameState.levelSpawnedCoins || 1);
if (gameState.levelWallBounces == 0) {
options++;
}
if (gameState.levelTime < 30 * 1000) {
options++
options++;
}
if (catchRate === 1) {
options++
options++;
}
if (gameState.levelMisses === 0) {
options++
options++;
}
const choices = []
for( let difficulty=0; difficulty<options;difficulty++){
choices.push({
})
const choices = [];
for (let difficulty = 0; difficulty < options; difficulty++) {
choices.push({});
}
}
}

View file

@ -1053,4 +1053,4 @@
"svg": null,
"color": ""
}
]
]

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

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,
@ -101,10 +101,10 @@ export function newGameState(params: RunParams): GameState {
autoCleanUses: 0,
...defaultSounds(),
isAdventureMode:!!params?.adventure,
adventurePath:'',
seed:'Seed'+Math.random(),
rerolls:0
isAdventureMode: !!params?.adventure,
adventurePath: "",
seed: "Seed" + Math.random(),
rerolls: 0,
};
resetBalls(gameState);

View file

@ -1,9 +1,9 @@
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";
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
@ -18,144 +18,148 @@ RCASTIjXW61E7PQKir5qIXwkQDlzJ+bpZ3PHyAvspRrBaDxIYvEEw14evpuqOgS+
v/IlgPe+CWSvZa9xxnQl/aWZrOrD7syu6KKCbgUyXEm+Alp0YT3e6nwjn0qiM/cj
dZpWPx3O+rZbRQb0gHcvN4+n2Y7fWAeC9mxVZtADqvVr/GTumMbLj7DdhWtt1Ogu
4EcvkQ5SKCL0JC93DyctjOMCAwEAAQ==
-----END PUBLIC KEY-----`
-----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;
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}`;
// Split the key into its components
const [priceId, timestamp, signature] = key.split(":");
const data = `${priceId}:${timestamp}`;
const publicKeyBuffer = pemToArrayBuffer(pem);
const publicKeyBuffer = pemToArrayBuffer(pem);
const publicKey = await crypto.subtle.importKey(
"spki",
publicKeyBuffer,
{
name: "RSA-PSS",
hash: "SHA-256",
},
true,
["verify"],
);
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");
// 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;
return priceId;
}
let premium = false
const gamePriceId = 'price_1R6YaEGRf74lr2EkSo2GPvuO'
checkKey(getSettingValue('license', '')).then()
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
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
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
})
}
},
}
}
if (isPremium()) {
return {
icon: icons["icon:premium"],
text: t("premium.title"),
help: t("premium.short_help"),
value: () => openPremiumMenu(''),
}
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()
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 &&

10
src/types.d.ts vendored
View file

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