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

@ -22,9 +22,19 @@ If the app stutters, turn on "fast mode" in the settings to render a simplified
There's also an easy mode for kids (slower ball).
# Todo
- 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
- show -N points in red when combo resets
- reach : this is too punishing now, maybe only reset if you hit the lowest populate row of the level, if it's not a full width row
- respawn: N% of bricks respawn after N seconds
# UX
@ -57,7 +67,7 @@ There's also an easy mode for kids (slower ball).
- would be nice to have a leaderboard for not using each perk too. Like "best runs without hot start"
- restart run on r
- when missing, redo particle trail, but give speed to particle that matches ball direction
- Overgrowth — when the ball touches a bomb brick it turns into a regular green brick and spawns 1 more bricks near it (additional levels spawn 2 additional bricks)
# graphics
@ -79,7 +89,9 @@ There's also an easy mode for kids (slower ball).
# Medium difficulty perks ideas
- offer next level choice after upgrade pick
- Dividends — +1 combo per 10 coins lost (band-aid for players who struggle, useful addition when choosing Ascetism)
- [colin] mirror puck - a mirrored puck at the top of the screen follows as you move the bottom puck. it helps with keeping combos up and preventing the ball from touching the ceiling. it could appear as a hollow puck so as to not draw too much attention from the main bottom puck.
- [colin] Combos extrêmes: lvl2 pour tous les combos, qui fait que le combo rapporte double ou triple, mais si sur un niveau la condition n'est pas respectée alors le perk ne donne plus de combo bonus pour ce niveau.
- [colin] Mytosis - les blocs bombe n'explosent pas mais relâchent une nouvelle balle à la place (clashes with "shocks" and "sapper")
@ -98,13 +110,20 @@ There's also an easy mode for kids (slower ball).
- accelerometer controls coins and balls
- [colin] side pucks - same as above but with two side pucks : hard to know where to put them
# to sort
- [colin]Brambles — coins that touch the walls and ceiling get stuck and are thrown back when the last brick is destroyed
- [colin]Ball of Greed — the ball can collect coins (might be worth dividing into levels: lvl 1, can collect coins only after two bounces on bricks or walls. lvl 2, can collect after 1 bounce. lvl 3, can collect coins anytime)(or change the ball collection radius as the level grows)
- [colin]Fountain toss — each coin lost has a 1 in 10 chance to give +1 combo (until combo 50)
- [colin]Pocket money — bricks absorb coins that touch them, which are released on brick destruction (with a bonus?)
- [colin]Phantom ball — the ball phases through 2 bricks then becomes solid (lvl2: through 6 bricks, lvl3; through all bricks until it touches a wall)
- [colin]Cryptomoney — coins that should be generated by bricks are instantly collected, but count for half their value
- [colin]Relative time — ball speed depends on its position: if it's high up on thi screen it's fast, if it's lower it's slower
- [colin] turn ball gravity on after a top bar hit, and until bouncing on puck
- [colin] hitman - hit the marked brick for +5 combo. each level increases the combo you get for it.
- [colin] sweet spot - place your puck directly below a moving spot at the top of the level to increase your combo
- ball attracted by bricks of the color of the ball
- level flips horizontally every time a ball bounces on puck
- coins that hit the puck disappear, missed ones are scored
- [colin] close quarters - balle attirée par tous les blocs/par un bloc aléatoire, actif à portée de bloc (+1bloc au lvlup)/proportionnel à une force (+puissance au lvlup)…
@ -140,6 +159,7 @@ There's also an easy mode for kids (slower ball).
- Good games : FTL, Nova drift, Noita, Enter the gungeon, Zero Sivert, Factorio, Swarm
- letters and an associated word or name
- famous characters and movies
- fruits
- animals
- countries flags and shapes

View file

@ -26,7 +26,6 @@ import java.util.Date
import java.util.jar.Manifest
const val CHOOSE_FILE_REQUEST_CODE = 548459
const val PERM_REQUEST_CODE = 66622635
class MainActivity : android.app.Activity() {
@ -127,7 +126,10 @@ class MainActivity : android.app.Activity() {
webView.settings.domStorageEnabled = true
webView.settings.setSupportZoom(false)
webView.loadUrl("file:///android_asset/index.html?isInWebView=true")
val installerPackageName = packageManager.getInstallerPackageName(packageName)
webView.loadUrl("file:///android_asset/index.html?isInWebView=true&source=$installerPackageName")
val activity = this;
webView.webChromeClient = object : WebChromeClient() {

426
dist/index.html vendored

File diff suppressed because one or more lines are too long

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

@ -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()
}

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;