mirror of
https://gitlab.com/lecarore/breakout71.git
synced 2025-04-21 20:46:14 -04:00
Balancing
- New perk : addiction, reward faster gameplay - Balancing : hot start effect doubled - Balancing : you earn an extra perk when playing well, and a reroll when playing perfectly - Balancing : telekinesis limited to level 1
This commit is contained in:
parent
27a2cd686e
commit
7e316391d8
22 changed files with 799 additions and 897 deletions
66
Readme.md
66
Readme.md
|
@ -4,41 +4,50 @@ Break colourful bricks, catch bouncing coins and select powerful upgrades !
|
||||||
|
|
||||||
- [Play now](https://breakout.lecaro.me/)
|
- [Play now](https://breakout.lecaro.me/)
|
||||||
- [Donate](https://paypal.me/renanlecaro)
|
- [Donate](https://paypal.me/renanlecaro)
|
||||||
|
- Bitcoin : bc1qlh8kywy3ttsuqqa08yx2rdc8dqhdvyt43wlxmr
|
||||||
- [Discord](https://discord.gg/DZSPqyJkwP)
|
- [Discord](https://discord.gg/DZSPqyJkwP)
|
||||||
- [Post your comments on itch.io](https://renanlecaro.itch.io/breakout71)
|
- [Post your comments on itch.io](https://renanlecaro.itch.io/breakout71)
|
||||||
- [Help and tips about the game](./Help.md)
|
- [Help and tips about the game](./Help.md)
|
||||||
|
|
||||||
- [F-Droid](https://f-droid.org/en/packages/me.lecaro.breakout/)
|
- [F-Droid](https://f-droid.org/en/packages/me.lecaro.breakout/)
|
||||||
- [Google Play](https://play.google.com/store/apps/details?id=me.lecaro.breakout)
|
- [Google Play](https://play.google.com/store/apps/details?id=me.lecaro.breakout)
|
||||||
- [GitLab](https://gitlab.com/lecarore/breakout71)
|
- [GitLab](https://gitlab.com/lecarore/breakout71)
|
||||||
- [HackerNews thread](https://news.ycombinator.com/item?id=43183131)
|
|
||||||
|
|
||||||
# System requirements
|
# System requirements
|
||||||
|
|
||||||
Breakout 71 can work offline (add it to home screen) and perform well even on low-end devices.
|
Breakout 71 can work offline (add it to home screen) and perform well even on low-end devices.
|
||||||
It's very lean and does not take much storage space (Roughly 0.1MB).
|
It's very lean and does not take much storage space (Roughly 0.1MB).
|
||||||
If the app stutters, turn on "fast mode" in the settings to render a simplified view that should be faster.
|
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).
|
view that should be faster. There's also an easy mode for kids (slower ball).
|
||||||
|
|
||||||
# Looping
|
# Looping
|
||||||
|
|
||||||
Premium users can loop the game to continue player a harder and harder version of the game.
|
Once you have unlocked all upgrades, you'll get an option to play a longer game.
|
||||||
At the end of the last level of each run, they can start a new loop. They'll be taken back
|
In the mode, you can "loop" the game up to 7 times after your initial run.
|
||||||
to level 1, with only one of their perks, leveled up. All the other perks they used in the run
|
|
||||||
will be banned from the pool. The perk they decide to keep will gain one level, even if it was
|
|
||||||
already maxed out.
|
|
||||||
|
|
||||||
# next
|
Each loop is shorter, and you can't use the perk levels you used in past levels, except
|
||||||
|
for one perk that you can keep improving. If you completely exhaust a perk, it won't be
|
||||||
|
offered at all in later loops. The perk you keep improving instantly gains +1 level and +2 max levels
|
||||||
|
Each loop is one level shorter. The ball starts slightly faster.
|
||||||
|
The game is over when you run loose the ball or finish the 7th loop.
|
||||||
|
|
||||||
|
# next goals
|
||||||
|
|
||||||
|
- Chain reaction : +lvl*lvl combo per brick broken by an explosion, combo resets after explosion is over
|
||||||
|
|
||||||
|
- [vikingerik] : reward multiballs with combo
|
||||||
- wind : move coins based on puck movement not position
|
- wind : move coins based on puck movement not position
|
||||||
- [jaceys] Move the restart button out of the menu, so that it is more easily accessible
|
- [jaceys] Move the restart button out of the menu, so that it is more easily accessible
|
||||||
- [jaceys] A visual indication of whether a ball has hit a brick this serve
|
- [jaceys] A visual indication of whether a ball has hit a brick this serve
|
||||||
- Top down /reach: punishing now, maybe only reset if you hit the lowest populate row of the level, if it's not a full width row
|
- Top down /reach: punishing now, maybe only reset if you hit the lowest populate row of the level, if it's not a full width row
|
||||||
|
|
||||||
# To do before next release
|
# Release candidate
|
||||||
|
- New perk : addiction, reward faster gameplay
|
||||||
# Next
|
- Balancing : hot start effect doubled
|
||||||
|
- Balancing : you earn an extra perk when playing well, and a reroll when playing perfectly
|
||||||
|
- Balancing : telekinesis limited to level 1
|
||||||
|
- Limit levels to 7-loop+extra_levels.
|
||||||
|
- Limit max by used levels on loop.
|
||||||
|
- forget about "premium" but added a prominent "donate" link after 5h of playing, and setting to hide it permanently
|
||||||
- disabled auto-release on fdroid of every web version, as there's a big delay and bugs woudl stay for a long time
|
- disabled auto-release on fdroid of every web version, as there's a big delay and bugs woudl stay for a long time
|
||||||
- [jaceys] Counters for coins lost, misses, and boundary bounces, as well as a timer.
|
- [jaceys] Counters for coins lost, misses, and boundary bounces, as well as a timer.
|
||||||
- added a white border around all coins, to make dark ones visible on dark bg
|
- added a white border around all coins, to make dark ones visible on dark bg
|
||||||
|
@ -137,7 +146,7 @@ already maxed out.
|
||||||
|
|
||||||
|
|
||||||
# Medium difficulty perks ideas
|
# Medium difficulty perks ideas
|
||||||
|
- balls collision split them into 4 smaller balls, lvl times (requires rework)
|
||||||
- offer next level choice after upgrade pick
|
- 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)
|
- 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] 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.
|
||||||
|
@ -181,6 +190,8 @@ already maxed out.
|
||||||
- [colin] perk: analyzer - permet de voir les caractéristiques cachées des blocs (sturdy…)
|
- [colin] perk: analyzer - permet de voir les caractéristiques cachées des blocs (sturdy…)
|
||||||
- [colin] perk: roulette - gagne instantanément 2 perks aléatoires
|
- [colin] perk: roulette - gagne instantanément 2 perks aléatoires
|
||||||
- combo climbs every time a ball bounces on puck (but bounce is random?)
|
- combo climbs every time a ball bounces on puck (but bounce is random?)
|
||||||
|
- [colin] reward the player with more choices/perks for breaking a brick while having reached an increasing combo thresholds. 5 combo, then 10, then 20, then 40 etc… once a threshold is reached you aren't rewarded for that threshold again until you start a rew run
|
||||||
|
- [colin] inspired by Balatro's score system : have some perks add to the multiplicator, and some others to the amount of coins in a brick (or the raw value of coins inside), so that you users want to improve both for maximized profit ! maybe tie one of the to perks that help you, and the other to perks that are bad to you, so that gambling players are forced to make their life harder
|
||||||
|
|
||||||
# Probably not
|
# Probably not
|
||||||
- colored coins only (coins should be of the color of the ball to count, otherwise what ? i'd rather avoid negative points)
|
- colored coins only (coins should be of the color of the ball to count, otherwise what ? i'd rather avoid negative points)
|
||||||
|
@ -234,22 +245,10 @@ Instead of automatically unlocking things at the end of each run, add the coins
|
||||||
and let them spend those coins on upgrades. The upgrades would then be explained. They could have a condition like
|
and let them spend those coins on upgrades. The upgrades would then be explained. They could have a condition like
|
||||||
"reach high score of 1000" or 'reach high score of 99999 without using "hot start"'.
|
"reach high score of 1000" or 'reach high score of 99999 without using "hot start"'.
|
||||||
This requires recording a bit more info about each run.
|
This requires recording a bit more info about each run.
|
||||||
I could unlock the "pro stand" at $999 that just holds the play area higher.
|
|
||||||
|
|
||||||
# increase skill ceiling
|
|
||||||
|
|
||||||
- reroll mechanic, rerolls are reward for better play
|
|
||||||
- make puck smaller as combo increases ?
|
|
||||||
|
|
||||||
- final bosses (large vertical level that scrolls down faster and faster)
|
- final bosses (large vertical level that scrolls down faster and faster)
|
||||||
- when the player reaches the last level, allow them to loop the run, unlocking a permanent bonus for this run. For example: +5 combo, +1 life per loop… the counterpart would be hazards that slowly populate the levels.
|
|
||||||
|
|
||||||
|
|
||||||
# Colin's feedback (cwpute/obigre)
|
|
||||||
|
|
||||||
* reward the player with more choices/perks for breaking a brick while having reached an increasing combo thresholds. 5 combo, then 10, then 20, then 40 etc… once a threshold is reached you aren't rewarded for that threshold again until you start a rew run
|
|
||||||
* inspired by Balatro's score system : have some perks add to the multiplicator, and some others to the amount of coins in a brick (or the raw value of coins inside), so that you users want to improve both for maximized profit ! maybe tie one of the to perks that help you, and the other to perks that are bad to you, so that gambling players are forced to make their life harder
|
|
||||||
|
|
||||||
|
|
||||||
# Credits
|
# Credits
|
||||||
|
|
||||||
|
@ -271,12 +270,21 @@ https://prohama.com/dog-21-pattern/
|
||||||
I wanted an APK to start in fullscreen and be able to list it on fdroid and the play store. I started with an empty view and went to work trimming it down, with the help of that tutorial
|
I wanted an APK to start in fullscreen and be able to list it on fdroid and the play store. I started with an empty view and went to work trimming it down, with the help of that tutorial
|
||||||
https://github.com/fractalwrench/ApkGolf/blob/master/blog/BLOG_POST.md
|
https://github.com/fractalwrench/ApkGolf/blob/master/blog/BLOG_POST.md
|
||||||
|
|
||||||
|
Colin (obigre) brought a lot of fantastic ideas to the game, here's his website (in French) : https://colin-crapahute.bearblog.dev/
|
||||||
|
|
||||||
# Other noteworthy games in the breakout genre
|
# Other noteworthy games in the breakout genre
|
||||||
|
|
||||||
LBreakoutHD : https://sourceforge.net/p/lgames/code/HEAD/tree/trunk/lbreakouthd/
|
LBreakoutHD : nice and open source remake
|
||||||
|
https://sourceforge.net/p/lgames/code/HEAD/tree/trunk/lbreakouthd/
|
||||||
|
|
||||||
Wizorb https://store.steampowered.com/app/207420/Wizorb/
|
Wizorb
|
||||||
|
https://store.steampowered.com/app/207420/Wizorb/
|
||||||
|
|
||||||
Rollers of the realm : narratif, chaque balle est un aventurier
|
Rollers of the realm : narratif, chaque balle est un aventurier
|
||||||
https://store.steampowered.com/app/262470/Rollers_of_the_Realm/
|
https://store.steampowered.com/app/262470/Rollers_of_the_Realm/
|
||||||
|
|
||||||
|
Breakout multiplayer : pvp with air console style multiplayer
|
||||||
|
https://casmo.itch.io/breakout-multiplayer
|
||||||
|
|
||||||
|
Breakout Hero (made as part of a PICO-8 tutorial )
|
||||||
|
https://krystman.itch.io/breakout-hero
|
|
@ -11,8 +11,8 @@ android {
|
||||||
applicationId = "me.lecaro.breakout"
|
applicationId = "me.lecaro.breakout"
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 29054664
|
versionCode = 29056022
|
||||||
versionName = "29054664"
|
versionName = "29056022"
|
||||||
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
506
dist/index.html
vendored
506
dist/index.html
vendored
File diff suppressed because one or more lines are too long
|
@ -1,5 +1,5 @@
|
||||||
// The version of the cache.
|
// The version of the cache.
|
||||||
const VERSION = "29054664";
|
const VERSION = "29056022";
|
||||||
|
|
||||||
// The name of the cache
|
// The name of the cache
|
||||||
const CACHE_NAME = `breakout-71-${VERSION}`;
|
const CACHE_NAME = `breakout-71-${VERSION}`;
|
||||||
|
|
|
@ -1021,13 +1021,6 @@
|
||||||
{
|
{
|
||||||
"name": "icon:premium",
|
"name": "icon:premium",
|
||||||
"size": 11,
|
"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_____________",
|
"bricks": "__y____y___y____y____y_y__yby__y______y______yy_yty_yy_ybbytttybbyybbytttybby_ybytttyby___yyyyyyy____yyyyyyy_____________",
|
||||||
"svg": null,
|
"svg": null,
|
||||||
"color": ""
|
"color": ""
|
||||||
|
@ -1045,5 +1038,19 @@
|
||||||
"bricks": "__llllll_llBlBlelllllleBWWWWWeeeWBWBWeBeWWWWWeeeWBWBWBe_WWWWWe__",
|
"bricks": "__llllll_llBlBlelllllleBWWWWWeeeWBWBWeBeWWWWWeeeWBWBWBe_WWWWWe__",
|
||||||
"svg": null,
|
"svg": null,
|
||||||
"color": ""
|
"color": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "icon:loop",
|
||||||
|
"size": 10,
|
||||||
|
"bricks": "____yyyy_____yy__yy_________yy__a______y_aaa_____yccccc____y__c_____GG__cc___GG____GGGGG____________",
|
||||||
|
"svg": null,
|
||||||
|
"color": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "icon:addiction",
|
||||||
|
"size": 9,
|
||||||
|
"bricks": "__________________________l__WWWWW_lWWWyylllly_WWWWW_ly_______l__________________",
|
||||||
|
"svg": null,
|
||||||
|
"color": ""
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
"29054664"
|
"29056022"
|
||||||
|
|
|
@ -45,7 +45,8 @@ body {
|
||||||
min-width: 40px;
|
min-width: 40px;
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
|
max-width: calc(100vw - 80px);
|
||||||
|
overflow: hidden;
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus {
|
&:focus {
|
||||||
background: rgba(0, 0, 0, 0.3);
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
@ -75,6 +76,7 @@ body {
|
||||||
&.good {
|
&.good {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.bad {
|
&.bad {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
@ -356,21 +358,55 @@ h2.histogram-title strong {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//#statsdisplay{
|
.upgrade {
|
||||||
// z-index: 1;
|
display: flex;
|
||||||
// white-space: nowrap;
|
gap: 2px;
|
||||||
// line-height: 20px;
|
margin: 0 0 10px 0;
|
||||||
// pointer-events: none;
|
|
||||||
// user-select: none;
|
img {
|
||||||
// color: white;
|
width: 32px;
|
||||||
// position: fixed;
|
height: 32px;
|
||||||
// padding: 0;
|
}
|
||||||
// bottom: -20px;
|
p {
|
||||||
// right: 0;
|
flex-grow: 1;
|
||||||
// width: 20px;
|
color: rgba(255, 255, 255, 0.6);
|
||||||
// overflow: visible;
|
|
||||||
//
|
margin: 0 20px;
|
||||||
// transform-origin: top left;
|
}
|
||||||
// transform: rotate(-90deg);
|
&.used p strong {
|
||||||
//
|
color: white;
|
||||||
//}
|
}
|
||||||
|
& > span {
|
||||||
|
flex-grow: 0;
|
||||||
|
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 5px;
|
||||||
|
display: inline-block;
|
||||||
|
height: 32px;
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
|
&.used {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.free {
|
||||||
|
background: #fff;
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.banned {
|
||||||
|
background: red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.used {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.free {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.banned {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
136
src/game.ts
136
src/game.ts
|
@ -34,7 +34,6 @@ import {
|
||||||
import {
|
import {
|
||||||
forEachLiveOne,
|
forEachLiveOne,
|
||||||
gameStateTick,
|
gameStateTick,
|
||||||
liveCount,
|
|
||||||
normalizeGameState,
|
normalizeGameState,
|
||||||
pickRandomUpgrades,
|
pickRandomUpgrades,
|
||||||
setLevel,
|
setLevel,
|
||||||
|
@ -63,7 +62,7 @@ import {
|
||||||
} from "./asyncAlert";
|
} 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 { hoursSpentPlaying } from "./pure_functions";
|
||||||
|
|
||||||
export function play() {
|
export function play() {
|
||||||
if (applyFullScreenChoice()) return;
|
if (applyFullScreenChoice()) return;
|
||||||
|
@ -195,37 +194,37 @@ export async function openUpgradesPicker(gameState: GameState) {
|
||||||
wallHitsGain = "",
|
wallHitsGain = "",
|
||||||
missesGain = "";
|
missesGain = "";
|
||||||
|
|
||||||
if (gameState.levelWallBounces == 0) {
|
if (gameState.levelWallBounces < 3) {
|
||||||
repeats++;
|
repeats++;
|
||||||
gameState.rerolls++;
|
gameState.rerolls++;
|
||||||
|
wallHitsGain = t("level_up.plus_one_upgrade_and_reroll");
|
||||||
|
} else if (gameState.levelWallBounces < 10) {
|
||||||
|
repeats++;
|
||||||
wallHitsGain = t("level_up.plus_one_upgrade");
|
wallHitsGain = t("level_up.plus_one_upgrade");
|
||||||
} else if (gameState.levelWallBounces < 5) {
|
|
||||||
gameState.rerolls++;
|
|
||||||
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_and_reroll");
|
||||||
} else if (gameState.levelTime < 60 * 1000) {
|
} else if (gameState.levelTime < 60 * 1000) {
|
||||||
gameState.rerolls++;
|
repeats++;
|
||||||
timeGain = t("level_up.plus_one_choice");
|
timeGain = t("level_up.plus_one_upgrade");
|
||||||
}
|
}
|
||||||
if (catchRate === 1) {
|
if (catchRate > 0.95) {
|
||||||
repeats++;
|
repeats++;
|
||||||
gameState.rerolls++;
|
gameState.rerolls++;
|
||||||
catchGain = t("level_up.plus_one_upgrade");
|
catchGain = t("level_up.plus_one_upgrade_and_reroll");
|
||||||
} else if (catchRate > 0.9) {
|
} else if (catchRate > 0.9) {
|
||||||
gameState.rerolls++;
|
repeats++;
|
||||||
catchGain = t("level_up.plus_one_choice");
|
catchGain = t("level_up.plus_one_upgrade");
|
||||||
}
|
}
|
||||||
if (gameState.levelMisses === 0) {
|
if (gameState.levelMisses < 3) {
|
||||||
repeats++;
|
repeats++;
|
||||||
gameState.rerolls++;
|
gameState.rerolls++;
|
||||||
|
missesGain = t("level_up.plus_one_upgrade_and_reroll");
|
||||||
|
} else if (gameState.levelMisses < 6) {
|
||||||
|
repeats++;
|
||||||
missesGain = t("level_up.plus_one_upgrade");
|
missesGain = t("level_up.plus_one_upgrade");
|
||||||
} else if (gameState.levelMisses <= 3) {
|
|
||||||
gameState.rerolls++;
|
|
||||||
missesGain = t("level_up.plus_one_choice");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
while (repeats--) {
|
while (repeats--) {
|
||||||
|
@ -248,17 +247,6 @@ export async function openUpgradesPicker(gameState: GameState) {
|
||||||
icon: icons["icon:reroll"],
|
icon: icons["icon:reroll"],
|
||||||
});
|
});
|
||||||
|
|
||||||
let textAfterButtons = `
|
|
||||||
<p>${t("level_up.after_buttons", {
|
|
||||||
level: gameState.currentLevel + 1,
|
|
||||||
max: max_levels(gameState),
|
|
||||||
})} </p>
|
|
||||||
|
|
||||||
${pickedUpgradesHTMl(gameState)}
|
|
||||||
<div id="level-recording-container"></div>
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
const compliment =
|
const compliment =
|
||||||
(timeGain &&
|
(timeGain &&
|
||||||
catchGain &&
|
catchGain &&
|
||||||
|
@ -287,11 +275,16 @@ export async function openUpgradesPicker(gameState: GameState) {
|
||||||
compliment,
|
compliment,
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
|
<p>${t("level_up.after_buttons", {
|
||||||
|
level: gameState.currentLevel + 1,
|
||||||
|
max: max_levels(gameState),
|
||||||
|
})} </p>
|
||||||
<p>${levelsListHTMl(gameState)}</p>
|
<p>${levelsListHTMl(gameState)}</p>
|
||||||
`,
|
`,
|
||||||
...actions,
|
...actions,
|
||||||
textAfterButtons,
|
pickedUpgradesHTMl(gameState),
|
||||||
|
|
||||||
|
`<div id="level-recording-container"></div>`,
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -435,11 +428,6 @@ document.addEventListener("visibilitychange", () => {
|
||||||
async function openScorePanel() {
|
async function openScorePanel() {
|
||||||
pause(true);
|
pause(true);
|
||||||
|
|
||||||
const banned = upgrades
|
|
||||||
.filter((u) => gameState.bannedPerks[u.id])
|
|
||||||
.map((u) => u.name)
|
|
||||||
.join(", ");
|
|
||||||
|
|
||||||
const cb = await asyncAlert({
|
const cb = await asyncAlert({
|
||||||
title: gameState.loop
|
title: gameState.loop
|
||||||
? t("score_panel.title_looped", {
|
? t("score_panel.title_looped", {
|
||||||
|
@ -461,7 +449,6 @@ async function openScorePanel() {
|
||||||
gameState.rerolls
|
gameState.rerolls
|
||||||
? t("score_panel.rerolls_count", { rerolls: gameState.rerolls })
|
? t("score_panel.rerolls_count", { rerolls: gameState.rerolls })
|
||||||
: "",
|
: "",
|
||||||
banned && t("score_panel.banned", { banned }),
|
|
||||||
],
|
],
|
||||||
allowClose: true,
|
allowClose: true,
|
||||||
});
|
});
|
||||||
|
@ -487,7 +474,7 @@ export async function openMainMenu() {
|
||||||
text: t("main_menu.normal"),
|
text: t("main_menu.normal"),
|
||||||
help: t("main_menu.normal_help"),
|
help: t("main_menu.normal_help"),
|
||||||
value: () => {
|
value: () => {
|
||||||
restart({ levelToAvoid: currentLevelInfo(gameState).name });
|
restart({ levelToAvoid: currentLevelInfo(gameState).name, maxLoop: 0 });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -498,6 +485,19 @@ export async function openMainMenu() {
|
||||||
openUnlocksList();
|
openUnlocksList();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: icons["icon:loop"],
|
||||||
|
text: t("main_menu.loop_run"),
|
||||||
|
help:
|
||||||
|
getTotalScore() < creativeModeThreshold
|
||||||
|
? t("sandbox.unlocks_at", { score: creativeModeThreshold })
|
||||||
|
: t("main_menu.loop_run_help"),
|
||||||
|
|
||||||
|
value: () => {
|
||||||
|
restart({ levelToAvoid: currentLevelInfo(gameState).name, maxLoop: 7 });
|
||||||
|
},
|
||||||
|
disabled: getTotalScore() < creativeModeThreshold,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: icons["icon:sandbox"],
|
icon: icons["icon:sandbox"],
|
||||||
text: t("sandbox.title"),
|
text: t("sandbox.title"),
|
||||||
|
@ -546,7 +546,7 @@ export async function openMainMenu() {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
premiumMenuEntry(gameState),
|
...donationNag(gameState),
|
||||||
{
|
{
|
||||||
text: t("main_menu.settings_title"),
|
text: t("main_menu.settings_title"),
|
||||||
help: t("main_menu.settings_help"),
|
help: t("main_menu.settings_help"),
|
||||||
|
@ -568,6 +568,22 @@ export async function openMainMenu() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function donationNag(gameState) {
|
||||||
|
if (!isOptionOn("donation_reminder")) return [];
|
||||||
|
const hours = hoursSpentPlaying();
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: t("main_menu.donate", { hours }),
|
||||||
|
help: t("main_menu.donate_help", {
|
||||||
|
suggestion: Math.min(20, Math.max(1, 0.2 * hours)).toFixed(0),
|
||||||
|
}),
|
||||||
|
icon: icons["icon:premium"],
|
||||||
|
value() {
|
||||||
|
window.open("https://paypal.me/renanlecaro", "_blank");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
async function openSettingsMenu() {
|
async function openSettingsMenu() {
|
||||||
pause(true);
|
pause(true);
|
||||||
|
|
||||||
|
@ -842,8 +858,6 @@ async function openUnlocksList() {
|
||||||
.sort((a, b) => a.threshold - b.threshold)
|
.sort((a, b) => a.threshold - b.threshold)
|
||||||
.map(({ name, id, threshold, icon, help }) => ({
|
.map(({ name, id, threshold, icon, help }) => ({
|
||||||
text: name,
|
text: name,
|
||||||
// help:
|
|
||||||
// ts >= threshold ? help(1) : t("unlocks.unlocks_at", { threshold }),
|
|
||||||
disabled: ts < threshold,
|
disabled: ts < threshold,
|
||||||
value: { perks: { [id]: 1 } } as RunParams,
|
value: { perks: { [id]: 1 } } as RunParams,
|
||||||
icon,
|
icon,
|
||||||
|
@ -855,12 +869,6 @@ async function openUnlocksList() {
|
||||||
const available = ts >= l.threshold;
|
const available = ts >= l.threshold;
|
||||||
return {
|
return {
|
||||||
text: l.name,
|
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 }),
|
|
||||||
disabled: !available,
|
disabled: !available,
|
||||||
value: { level: l.name } as RunParams,
|
value: { level: l.name } as RunParams,
|
||||||
icon: icons[l.name],
|
icon: icons[l.name],
|
||||||
|
@ -886,13 +894,14 @@ async function openUnlocksList() {
|
||||||
});
|
});
|
||||||
if (tryOn) {
|
if (tryOn) {
|
||||||
if (await confirmRestart(gameState)) {
|
if (await confirmRestart(gameState)) {
|
||||||
restart(tryOn);
|
restart({ ...tryOn, maxLoop: 0 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function confirmRestart(gameState) {
|
export async function confirmRestart(gameState) {
|
||||||
if (!gameState.currentLevel) return true;
|
if (!gameState.currentLevel) return true;
|
||||||
|
if (alertsOpen) return true;
|
||||||
|
|
||||||
return asyncAlert({
|
return asyncAlert({
|
||||||
title: t("confirmRestart.title"),
|
title: t("confirmRestart.title"),
|
||||||
|
@ -925,7 +934,7 @@ export function setKeyPressed(key: string, on: 0 | 1) {
|
||||||
50;
|
50;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("keydown", (e) => {
|
document.addEventListener("keydown", async (e) => {
|
||||||
if (e.key.toLowerCase() === "f" && !e.ctrlKey && !e.metaKey) {
|
if (e.key.toLowerCase() === "f" && !e.ctrlKey && !e.metaKey) {
|
||||||
toggleOption("fullscreen");
|
toggleOption("fullscreen");
|
||||||
applyFullScreenChoice();
|
applyFullScreenChoice();
|
||||||
|
@ -944,6 +953,7 @@ document.addEventListener("keydown", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let pageLoad = new Date();
|
||||||
document.addEventListener("keyup", async (e) => {
|
document.addEventListener("keyup", async (e) => {
|
||||||
const focused = document.querySelector("button:focus");
|
const focused = document.querySelector("button:focus");
|
||||||
if (e.key in pressed) {
|
if (e.key in pressed) {
|
||||||
|
@ -966,7 +976,12 @@ document.addEventListener("keyup", async (e) => {
|
||||||
openMainMenu().then();
|
openMainMenu().then();
|
||||||
} else if (e.key.toLowerCase() === "s" && !alertsOpen) {
|
} else if (e.key.toLowerCase() === "s" && !alertsOpen) {
|
||||||
openScorePanel().then();
|
openScorePanel().then();
|
||||||
} else if (e.key.toLowerCase() === "r" && !alertsOpen) {
|
} else if (
|
||||||
|
e.key.toLowerCase() === "r" &&
|
||||||
|
!alertsOpen &&
|
||||||
|
pageLoad > Date.now() + 1000
|
||||||
|
) {
|
||||||
|
// When doing ctrl + R in dev to refresh, i don't want to instantly restart a run
|
||||||
if (await confirmRestart(gameState)) {
|
if (await confirmRestart(gameState)) {
|
||||||
restart({ levelToAvoid: currentLevelInfo(gameState).name });
|
restart({ levelToAvoid: currentLevelInfo(gameState).name });
|
||||||
}
|
}
|
||||||
|
@ -979,6 +994,7 @@ document.addEventListener("keyup", async (e) => {
|
||||||
export const gameState = newGameState({});
|
export const gameState = newGameState({});
|
||||||
|
|
||||||
export function restart(params: RunParams) {
|
export function restart(params: RunParams) {
|
||||||
|
console.log("restart : ", params);
|
||||||
fitSize();
|
fitSize();
|
||||||
Object.assign(gameState, newGameState(params));
|
Object.assign(gameState, newGameState(params));
|
||||||
pauseRecording();
|
pauseRecording();
|
||||||
|
@ -987,18 +1003,20 @@ export function restart(params: RunParams) {
|
||||||
|
|
||||||
restart(
|
restart(
|
||||||
(window.location.search.includes("stressTest") && {
|
(window.location.search.includes("stressTest") && {
|
||||||
level: "Bird",
|
// level: "Bird",
|
||||||
perks: {
|
perks: {
|
||||||
shocks: 10,
|
// shocks: 10,
|
||||||
multiball: 6,
|
// multiball: 6,
|
||||||
telekinesis: 2,
|
// telekinesis: 2,
|
||||||
ghost_coins: 1,
|
// ghost_coins: 1,
|
||||||
pierce: 4,
|
pierce: 2,
|
||||||
clairvoyant: 3,
|
// clairvoyant: 2,
|
||||||
bigger_explosions: 2,
|
// sturdy_bricks:2,
|
||||||
sapper: 2,
|
bigger_explosions: 10,
|
||||||
unbounded: 1,
|
sapper: 3,
|
||||||
|
// unbounded: 1,
|
||||||
},
|
},
|
||||||
|
|
||||||
levelsPerLoop: 2,
|
levelsPerLoop: 2,
|
||||||
}) ||
|
}) ||
|
||||||
{},
|
{},
|
||||||
|
|
|
@ -49,7 +49,6 @@ import {
|
||||||
} from "./game";
|
} from "./game";
|
||||||
import { stopRecording } from "./recording";
|
import { stopRecording } from "./recording";
|
||||||
import { isOptionOn } from "./options";
|
import { isOptionOn } from "./options";
|
||||||
import { isPremium } from "./premium";
|
|
||||||
import { getRunLevels } from "./newGameState";
|
import { getRunLevels } from "./newGameState";
|
||||||
import { requiredAsyncAlert } from "./asyncAlert";
|
import { requiredAsyncAlert } from "./asyncAlert";
|
||||||
import { clamp, comboKeepingRate } from "./pure_functions";
|
import { clamp, comboKeepingRate } from "./pure_functions";
|
||||||
|
@ -343,6 +342,8 @@ export function explodeBrick(
|
||||||
const color = gameState.bricks[index];
|
const color = gameState.bricks[index];
|
||||||
if (!color) return;
|
if (!color) return;
|
||||||
|
|
||||||
|
gameState.lastBrickBroken = gameState.levelTime;
|
||||||
|
|
||||||
if (color === "black") {
|
if (color === "black") {
|
||||||
const x = brickCenterX(gameState, index),
|
const x = brickCenterX(gameState, index),
|
||||||
y = brickCenterY(gameState, index);
|
y = brickCenterY(gameState, index);
|
||||||
|
@ -366,7 +367,7 @@ export function explodeBrick(
|
||||||
if (gameState.perks.sturdy_bricks) {
|
if (gameState.perks.sturdy_bricks) {
|
||||||
// +10% per level
|
// +10% per level
|
||||||
coinsToSpawn += Math.ceil(
|
coinsToSpawn += Math.ceil(
|
||||||
((10 + gameState.perks.sturdy_bricks) / 10) * coinsToSpawn,
|
((2 + gameState.perks.sturdy_bricks) / 2) * coinsToSpawn,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -420,6 +421,7 @@ export function explodeBrick(
|
||||||
gameState.perks.zen +
|
gameState.perks.zen +
|
||||||
gameState.perks.passive_income +
|
gameState.perks.passive_income +
|
||||||
gameState.perks.nbricks +
|
gameState.perks.nbricks +
|
||||||
|
gameState.perks.addiction +
|
||||||
gameState.perks.unbounded;
|
gameState.perks.unbounded;
|
||||||
|
|
||||||
if (gameState.perks.side_kick) {
|
if (gameState.perks.side_kick) {
|
||||||
|
@ -511,8 +513,7 @@ export function pickRandomUpgrades(gameState: GameState, count: number) {
|
||||||
score: Math.random() + (gameState.lastOffered[u.id] || 0),
|
score: Math.random() + (gameState.lastOffered[u.id] || 0),
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => a.score - b.score)
|
.sort((a, b) => a.score - b.score)
|
||||||
.filter((u) => gameState.perks[u.id] < u.max)
|
.filter((u) => gameState.perks[u.id] < u.max - gameState.bannedPerks[u.id])
|
||||||
.filter((u) => !gameState.bannedPerks[u.id])
|
|
||||||
.slice(0, count)
|
.slice(0, count)
|
||||||
.sort((a, b) => (a.id > b.id ? 1 : -1));
|
.sort((a, b) => (a.id > b.id ? 1 : -1));
|
||||||
|
|
||||||
|
@ -616,15 +617,19 @@ export async function gotoNextLoop(gameState: GameState) {
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
// Decrease max level of all perks
|
||||||
userPerks.forEach((u) => {
|
userPerks.forEach((u) => {
|
||||||
if (u.id !== keep) {
|
if (u.id !== keep) {
|
||||||
gameState.bannedPerks[u.id] = 1;
|
gameState.bannedPerks[u.id] += gameState.perks[u.id];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Increase max level of kept perk by 2
|
||||||
|
gameState.bannedPerks[keep] -= 2;
|
||||||
|
|
||||||
|
// Increase current level of kept perk by 1
|
||||||
Object.assign(gameState.perks, makeEmptyPerksMap(upgrades), {
|
Object.assign(gameState.perks, makeEmptyPerksMap(upgrades), {
|
||||||
[keep]: gameState.perks[keep],
|
[keep]: gameState.perks[keep] + 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
await setLevel(gameState, 0);
|
await setLevel(gameState, 0);
|
||||||
|
@ -655,6 +660,7 @@ export async function setLevel(gameState: GameState, l: number) {
|
||||||
gameState.levelSpawnedCoins = 0;
|
gameState.levelSpawnedCoins = 0;
|
||||||
gameState.levelLostCoins = 0;
|
gameState.levelLostCoins = 0;
|
||||||
gameState.levelMisses = 0;
|
gameState.levelMisses = 0;
|
||||||
|
gameState.lastBrickBroken = 0;
|
||||||
gameState.runStatistics.levelsPlayed++;
|
gameState.runStatistics.levelsPlayed++;
|
||||||
|
|
||||||
// Reset combo silently
|
// Reset combo silently
|
||||||
|
@ -669,7 +675,8 @@ export async function setLevel(gameState: GameState, l: number) {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
gameState.combo += gameState.perks.hot_start * 15;
|
|
||||||
|
gameState.combo += gameState.perks.hot_start * 30;
|
||||||
|
|
||||||
const lvl = currentLevelInfo(gameState);
|
const lvl = currentLevelInfo(gameState);
|
||||||
if (lvl.size !== gameState.gridSize) {
|
if (lvl.size !== gameState.gridSize) {
|
||||||
|
@ -923,6 +930,19 @@ export function gameStateTick(
|
||||||
gameState.combo,
|
gameState.combo,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
gameState.perks.addiction &&
|
||||||
|
gameState.lastBrickBroken &&
|
||||||
|
gameState.lastBrickBroken <
|
||||||
|
gameState.levelTime - 5000 / gameState.perks.addiction
|
||||||
|
) {
|
||||||
|
resetCombo(
|
||||||
|
gameState,
|
||||||
|
gameState.puckPosition,
|
||||||
|
gameState.gameZoneHeight - gameState.puckHeight * 2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
gameState.balls = gameState.balls.filter((ball) => !ball.destroyed);
|
gameState.balls = gameState.balls.filter((ball) => !ball.destroyed);
|
||||||
|
|
||||||
const remainingBricks = gameState.bricks.filter(
|
const remainingBricks = gameState.bricks.filter(
|
||||||
|
@ -974,15 +994,13 @@ export function gameStateTick(
|
||||||
) {
|
) {
|
||||||
if (gameState.currentLevel + 1 < max_levels(gameState)) {
|
if (gameState.currentLevel + 1 < max_levels(gameState)) {
|
||||||
setLevel(gameState, gameState.currentLevel + 1);
|
setLevel(gameState, gameState.currentLevel + 1);
|
||||||
|
} else if (gameState.loop < gameState.maxLoop) {
|
||||||
|
gotoNextLoop(gameState);
|
||||||
} else {
|
} else {
|
||||||
if (isPremium()) {
|
gameOver(
|
||||||
gotoNextLoop(gameState);
|
t("gameOver.7_loop.title", { loop: gameState.loop }),
|
||||||
} else {
|
t("gameOver.7_loop.summary", { score: gameState.score }),
|
||||||
gameOver(
|
);
|
||||||
t("gameOver.win.title"),
|
|
||||||
t("gameOver.win.summary", { score: gameState.score }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (gameState.running || gameState.levelTime) {
|
} else if (gameState.running || gameState.levelTime) {
|
||||||
const coinRadius = Math.round(gameState.coinSize / 2);
|
const coinRadius = Math.round(gameState.coinSize / 2);
|
||||||
|
@ -1558,6 +1576,7 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
|
||||||
const hitBrick = vhit ?? hhit ?? chit;
|
const hitBrick = vhit ?? hhit ?? chit;
|
||||||
|
|
||||||
if (typeof hitBrick !== "undefined") {
|
if (typeof hitBrick !== "undefined") {
|
||||||
|
const initialBrickColor = gameState.bricks[hitBrick];
|
||||||
ball.hitSinceBounce++;
|
ball.hitSinceBounce++;
|
||||||
let pierce = false;
|
let pierce = false;
|
||||||
let damage =
|
let damage =
|
||||||
|
@ -1570,7 +1589,7 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
|
||||||
|
|
||||||
const used = Math.min(
|
const used = Math.min(
|
||||||
ball.piercePoints,
|
ball.piercePoints,
|
||||||
Math.max(1, gameState.brickHP[hitBrick]),
|
Math.max(1, gameState.brickHP[hitBrick] + 1),
|
||||||
);
|
);
|
||||||
gameState.brickHP[hitBrick] -= used;
|
gameState.brickHP[hitBrick] -= used;
|
||||||
ball.piercePoints -= used;
|
ball.piercePoints -= used;
|
||||||
|
@ -1592,8 +1611,13 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("After bounce", {
|
||||||
|
pierce,
|
||||||
|
initialBrickColor,
|
||||||
|
hitBrick,
|
||||||
|
hp: gameState.brickHP[hitBrick],
|
||||||
|
});
|
||||||
if (!gameState.brickHP[hitBrick]) {
|
if (!gameState.brickHP[hitBrick]) {
|
||||||
const initialBrickColor = gameState.bricks[hitBrick];
|
|
||||||
ball.brokenSinceBounce++;
|
ball.brokenSinceBounce++;
|
||||||
|
|
||||||
explodeBrick(gameState, hitBrick, ball, false);
|
explodeBrick(gameState, hitBrick, ball, false);
|
||||||
|
@ -1602,6 +1626,7 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
|
||||||
initialBrickColor !== "black" && // don't replace a brick that bounced with sturdy_bricks
|
initialBrickColor !== "black" && // don't replace a brick that bounced with sturdy_bricks
|
||||||
!gameState.bricks[hitBrick]
|
!gameState.bricks[hitBrick]
|
||||||
) {
|
) {
|
||||||
|
console.log("sapper use", hitBrick);
|
||||||
setBrick(gameState, hitBrick, "black");
|
setBrick(gameState, hitBrick, "black");
|
||||||
ball.sapperUses++;
|
ball.sapperUses++;
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,26 +53,52 @@ export function getRowColIndex(gameState: GameState, row: number, col: number) {
|
||||||
|
|
||||||
export function getPossibleUpgrades(gameState: GameState) {
|
export function getPossibleUpgrades(gameState: GameState) {
|
||||||
return upgrades
|
return upgrades
|
||||||
.filter(
|
.filter((u) => gameState.totalScoreAtRunStart >= u.threshold)
|
||||||
(u) =>
|
|
||||||
gameState.totalScoreAtRunStart >= u.threshold || gameState.loop > 0,
|
|
||||||
)
|
|
||||||
.filter((u) => !u?.requires || gameState.perks[u?.requires]);
|
.filter((u) => !u?.requires || gameState.perks[u?.requires]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function max_levels(gameState: GameState) {
|
export function max_levels(gameState: GameState) {
|
||||||
return gameState.levelsPerLoop + gameState.perks.extra_levels;
|
return Math.max(
|
||||||
|
gameState.levelsPerLoop + gameState.perks.extra_levels - gameState.loop,
|
||||||
|
1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pickedUpgradesHTMl(gameState: GameState) {
|
export function pickedUpgradesHTMl(gameState: GameState) {
|
||||||
let list = "";
|
const upgradesList = getPossibleUpgrades(gameState)
|
||||||
for (let u of upgrades) {
|
.map((u) => {
|
||||||
for (let i = 0; i < gameState.perks[u.id]; i++)
|
const newMax = Math.max(0, u.max - gameState.bannedPerks[u.id]);
|
||||||
list += `<span title="${u.name} : ${u.help(gameState.perks[u.id])}">${icons["icon:" + u.id]}</span>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!list) return "";
|
let bars = "";
|
||||||
return ` <p>${t("score_panel.upgrades_picked")}</p> <p>${list}</p>`;
|
for (let i = 0; i < Math.max(u.max, newMax, gameState.perks[u.id]); i++) {
|
||||||
|
if (i < gameState.perks[u.id]) {
|
||||||
|
bars += '<span class="used"></span>';
|
||||||
|
} else if (i < newMax) {
|
||||||
|
bars += '<span class="free"></span>';
|
||||||
|
} else {
|
||||||
|
bars += '<span class="banned"></span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = (!newMax && 2) || (!gameState.perks[u.id] && 1) || 0;
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
html: `
|
||||||
|
<div class="upgrade ${["used", "free", "banned"][state]}">
|
||||||
|
${u.icon}
|
||||||
|
<p>
|
||||||
|
<strong>${u.name}</strong>
|
||||||
|
${u.help(Math.max(1, gameState.perks[u.id]))}
|
||||||
|
</p>
|
||||||
|
${bars}
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => a.state - b.state)
|
||||||
|
.map((a) => a.html);
|
||||||
|
|
||||||
|
return ` <p>${t("score_panel.upgrades_picked")}</p>` + upgradesList.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function levelsListHTMl(gameState: GameState) {
|
export function levelsListHTMl(gameState: GameState) {
|
||||||
|
@ -131,8 +157,6 @@ export function defaultSounds() {
|
||||||
lifeLost: { vol: 0, x: 0 },
|
lifeLost: { vol: 0, x: 0 },
|
||||||
coinCatch: { vol: 0, x: 0 },
|
coinCatch: { vol: 0, x: 0 },
|
||||||
colorChange: { vol: 0, x: 0 },
|
colorChange: { vol: 0, x: 0 },
|
||||||
void: { vol: 0, x: 0 },
|
|
||||||
freeze: { vol: 0, x: 0 },
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,6 +87,41 @@
|
||||||
<folder_node>
|
<folder_node>
|
||||||
<name>gameOver</name>
|
<name>gameOver</name>
|
||||||
<children>
|
<children>
|
||||||
|
<folder_node>
|
||||||
|
<name>7_loop</name>
|
||||||
|
<children>
|
||||||
|
<concept_node>
|
||||||
|
<name>summary</name>
|
||||||
|
<description/>
|
||||||
|
<comment/>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>true</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>true</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-FR</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
</children>
|
||||||
|
</folder_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>cumulative_total</name>
|
<name>cumulative_total</name>
|
||||||
<description/>
|
<description/>
|
||||||
|
@ -543,7 +578,7 @@
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>plus_one_choice</name>
|
<name>plus_one_upgrade</name>
|
||||||
<description/>
|
<description/>
|
||||||
<comment/>
|
<comment/>
|
||||||
<translations>
|
<translations>
|
||||||
|
@ -558,7 +593,7 @@
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>plus_one_upgrade</name>
|
<name>plus_one_upgrade_and_reroll</name>
|
||||||
<description/>
|
<description/>
|
||||||
<comment/>
|
<comment/>
|
||||||
<translations>
|
<translations>
|
||||||
|
@ -777,6 +812,66 @@
|
||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>donate</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>donate_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>donation_reminder</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>donation_reminder_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>
|
<concept_node>
|
||||||
<name>download_save_file</name>
|
<name>download_save_file</name>
|
||||||
<description/>
|
<description/>
|
||||||
|
@ -942,6 +1037,36 @@
|
||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>loop_run</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>loop_run_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>
|
<concept_node>
|
||||||
<name>max_coins</name>
|
<name>max_coins</name>
|
||||||
<description/>
|
<description/>
|
||||||
|
@ -1564,221 +1689,6 @@
|
||||||
</concept_node>
|
</concept_node>
|
||||||
</children>
|
</children>
|
||||||
</folder_node>
|
</folder_node>
|
||||||
<folder_node>
|
|
||||||
<name>premium</name>
|
|
||||||
<children>
|
|
||||||
<concept_node>
|
|
||||||
<name>back</name>
|
|
||||||
<description/>
|
|
||||||
<comment/>
|
|
||||||
<translations>
|
|
||||||
<translation>
|
|
||||||
<language>en-US</language>
|
|
||||||
<approved>true</approved>
|
|
||||||
</translation>
|
|
||||||
<translation>
|
|
||||||
<language>fr-FR</language>
|
|
||||||
<approved>true</approved>
|
|
||||||
</translation>
|
|
||||||
</translations>
|
|
||||||
</concept_node>
|
|
||||||
<concept_node>
|
|
||||||
<name>back_help</name>
|
|
||||||
<description/>
|
|
||||||
<comment/>
|
|
||||||
<translations>
|
|
||||||
<translation>
|
|
||||||
<language>en-US</language>
|
|
||||||
<approved>true</approved>
|
|
||||||
</translation>
|
|
||||||
<translation>
|
|
||||||
<language>fr-FR</language>
|
|
||||||
<approved>true</approved>
|
|
||||||
</translation>
|
|
||||||
</translations>
|
|
||||||
</concept_node>
|
|
||||||
<concept_node>
|
|
||||||
<name>buy</name>
|
|
||||||
<description/>
|
|
||||||
<comment/>
|
|
||||||
<translations>
|
|
||||||
<translation>
|
|
||||||
<language>en-US</language>
|
|
||||||
<approved>true</approved>
|
|
||||||
</translation>
|
|
||||||
<translation>
|
|
||||||
<language>fr-FR</language>
|
|
||||||
<approved>true</approved>
|
|
||||||
</translation>
|
|
||||||
</translations>
|
|
||||||
</concept_node>
|
|
||||||
<concept_node>
|
|
||||||
<name>buy_disabled_help</name>
|
|
||||||
<description/>
|
|
||||||
<comment/>
|
|
||||||
<translations>
|
|
||||||
<translation>
|
|
||||||
<language>en-US</language>
|
|
||||||
<approved>true</approved>
|
|
||||||
</translation>
|
|
||||||
<translation>
|
|
||||||
<language>fr-FR</language>
|
|
||||||
<approved>true</approved>
|
|
||||||
</translation>
|
|
||||||
</translations>
|
|
||||||
</concept_node>
|
|
||||||
<concept_node>
|
|
||||||
<name>buy_help</name>
|
|
||||||
<description/>
|
|
||||||
<comment/>
|
|
||||||
<translations>
|
|
||||||
<translation>
|
|
||||||
<language>en-US</language>
|
|
||||||
<approved>true</approved>
|
|
||||||
</translation>
|
|
||||||
<translation>
|
|
||||||
<language>fr-FR</language>
|
|
||||||
<approved>true</approved>
|
|
||||||
</translation>
|
|
||||||
</translations>
|
|
||||||
</concept_node>
|
|
||||||
<concept_node>
|
|
||||||
<name>enter</name>
|
|
||||||
<description/>
|
|
||||||
<comment/>
|
|
||||||
<translations>
|
|
||||||
<translation>
|
|
||||||
<language>en-US</language>
|
|
||||||
<approved>true</approved>
|
|
||||||
</translation>
|
|
||||||
<translation>
|
|
||||||
<language>fr-FR</language>
|
|
||||||
<approved>true</approved>
|
|
||||||
</translation>
|
|
||||||
</translations>
|
|
||||||
</concept_node>
|
|
||||||
<concept_node>
|
|
||||||
<name>enter_help</name>
|
|
||||||
<description/>
|
|
||||||
<comment/>
|
|
||||||
<translations>
|
|
||||||
<translation>
|
|
||||||
<language>en-US</language>
|
|
||||||
<approved>true</approved>
|
|
||||||
</translation>
|
|
||||||
<translation>
|
|
||||||
<language>fr-FR</language>
|
|
||||||
<approved>true</approved>
|
|
||||||
</translation>
|
|
||||||
</translations>
|
|
||||||
</concept_node>
|
|
||||||
<concept_node>
|
|
||||||
<name>help</name>
|
|
||||||
<description/>
|
|
||||||
<comment/>
|
|
||||||
<translations>
|
|
||||||
<translation>
|
|
||||||
<language>en-US</language>
|
|
||||||
<approved>true</approved>
|
|
||||||
</translation>
|
|
||||||
<translation>
|
|
||||||
<language>fr-FR</language>
|
|
||||||
<approved>true</approved>
|
|
||||||
</translation>
|
|
||||||
</translations>
|
|
||||||
</concept_node>
|
|
||||||
<concept_node>
|
|
||||||
<name>help_google</name>
|
|
||||||
<description/>
|
|
||||||
<comment/>
|
|
||||||
<translations>
|
|
||||||
<translation>
|
|
||||||
<language>en-US</language>
|
|
||||||
<approved>true</approved>
|
|
||||||
</translation>
|
|
||||||
<translation>
|
|
||||||
<language>fr-FR</language>
|
|
||||||
<approved>true</approved>
|
|
||||||
</translation>
|
|
||||||
</translations>
|
|
||||||
</concept_node>
|
|
||||||
<concept_node>
|
|
||||||
<name>per_hours</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>per_hours_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>thanks</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>thanks_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>
|
<folder_node>
|
||||||
<name>sandbox</name>
|
<name>sandbox</name>
|
||||||
<children>
|
<children>
|
||||||
|
@ -1862,21 +1772,6 @@
|
||||||
<folder_node>
|
<folder_node>
|
||||||
<name>score_panel</name>
|
<name>score_panel</name>
|
||||||
<children>
|
<children>
|
||||||
<concept_node>
|
|
||||||
<name>banned</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>
|
<concept_node>
|
||||||
<name>rerolls_count</name>
|
<name>rerolls_count</name>
|
||||||
<description/>
|
<description/>
|
||||||
|
@ -2067,6 +1962,56 @@
|
||||||
<folder_node>
|
<folder_node>
|
||||||
<name>upgrades</name>
|
<name>upgrades</name>
|
||||||
<children>
|
<children>
|
||||||
|
<folder_node>
|
||||||
|
<name>addiction</name>
|
||||||
|
<children>
|
||||||
|
<concept_node>
|
||||||
|
<name>fullHelp</name>
|
||||||
|
<description/>
|
||||||
|
<comment/>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>true</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-FR</language>
|
||||||
|
<approved>true</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>help</name>
|
||||||
|
<description/>
|
||||||
|
<comment/>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>true</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-FR</language>
|
||||||
|
<approved>true</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>name</name>
|
||||||
|
<description/>
|
||||||
|
<comment/>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>true</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-FR</language>
|
||||||
|
<approved>true</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
</children>
|
||||||
|
</folder_node>
|
||||||
<folder_node>
|
<folder_node>
|
||||||
<name>asceticism</name>
|
<name>asceticism</name>
|
||||||
<children>
|
<children>
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
"confirmRestart.text": "You're about to start a new run, is that really what you wanted ?",
|
"confirmRestart.text": "You're about to start a new run, is that really what you wanted ?",
|
||||||
"confirmRestart.title": "Start a new run ?",
|
"confirmRestart.title": "Start a new run ?",
|
||||||
"confirmRestart.yes": "Restart game",
|
"confirmRestart.yes": "Restart game",
|
||||||
|
"gameOver.7_loop.summary": "This run is over. You stashed {{score}} coins. ",
|
||||||
|
"gameOver.7_loop.title": "You completed this run",
|
||||||
"gameOver.cumulative_total": "Your total cumulative score went from {{startTs}} to {{endTs}}.",
|
"gameOver.cumulative_total": "Your total cumulative score went from {{startTs}} to {{endTs}}.",
|
||||||
"gameOver.lost.summary": "You dropped the ball after catching {{score}} coins.",
|
"gameOver.lost.summary": "You dropped the ball after catching {{score}} coins.",
|
||||||
"gameOver.lost.title": "Game Over",
|
"gameOver.lost.title": "Game Over",
|
||||||
|
@ -26,27 +28,31 @@
|
||||||
"gameOver.upgrades_picked": "Upgrades active at the end of the run",
|
"gameOver.upgrades_picked": "Upgrades active at the end of the run",
|
||||||
"gameOver.win.summary": "You cleared all levels for this run, catching {{score}} coins in total.",
|
"gameOver.win.summary": "You cleared all levels for this run, catching {{score}} coins in total.",
|
||||||
"gameOver.win.title": "Run finished",
|
"gameOver.win.title": "Run finished",
|
||||||
"level_up.after_buttons": "You just finished level {{level}}/{{max}} and picked those upgrades so far :",
|
"level_up.after_buttons": "You just finished level {{level}}/{{max}}.",
|
||||||
"level_up.before_buttons": "You caught {{score}} coins {{catchGain}} out of {{levelSpawnedCoins}} in {{time}} seconds {{timeGain}}.\n\nYou missed {{levelMisses}} times {{missesGain}} and hit the walls or ceiling {{levelWallBounces}} times{{wallHitsGain}}.\n\n{{compliment}}",
|
"level_up.before_buttons": "You caught {{score}} coins {{catchGain}} out of {{levelSpawnedCoins}} in {{time}} seconds {{timeGain}}.\n\nYou missed {{levelMisses}} times {{missesGain}} and hit the walls or ceiling {{levelWallBounces}} times{{wallHitsGain}}.\n\n{{compliment}}",
|
||||||
"level_up.compliment_advice": "Try to catch all coins, never miss the bricks, never hit the walls/ceiling or clear the level under 30s to gain additional choices and upgrades.",
|
"level_up.compliment_advice": "Try to catch all coins, never miss the bricks, never hit the walls/ceiling or clear the level under 30s to gain additional choices and upgrades.",
|
||||||
"level_up.compliment_good": "Well done !",
|
"level_up.compliment_good": "Well done !",
|
||||||
"level_up.compliment_perfect": "Impressive, keep it up !",
|
"level_up.compliment_perfect": "Impressive, keep it up !",
|
||||||
"level_up.pick_upgrade_title": "Pick an upgrade",
|
"level_up.pick_upgrade_title": "Pick an upgrade",
|
||||||
"level_up.plus_one_choice": "(+1 re-roll)",
|
"level_up.plus_one_upgrade": "(+1 upgrade)",
|
||||||
"level_up.plus_one_upgrade": "(+1 upgrade and +1 re-roll)",
|
"level_up.plus_one_upgrade_and_reroll": "(+1 upgrade and +1 re-roll)",
|
||||||
"level_up.reroll": "Re-roll ({{count}})",
|
"level_up.reroll": "Re-roll ({{count}})",
|
||||||
"level_up.reroll_help": "Offer new choices",
|
"level_up.reroll_help": "Offer new choices",
|
||||||
"level_up.unlocked_level": " (Level)",
|
"level_up.unlocked_level": " (Level)",
|
||||||
"level_up.unlocked_perk": " (Perk)",
|
"level_up.unlocked_perk": " (Perk)",
|
||||||
"level_up.upgrade_perk_to_level": " lvl {{level}}",
|
"level_up.upgrade_perk_to_level": " lvl {{level}}",
|
||||||
"loop.converted_rerolls": "Your {{n}} leftover re-rolls where converted to +{{n}} base combo.",
|
"loop.converted_rerolls": "Your {{n}} leftover re-rolls where converted to +{{n}} base combo.",
|
||||||
"loop.instructions": "All perks you have now will be banned for the rest of the run, except the one that you pick below. Your pick will be leveled up, even beyond the maximum normally allowed for that perk.",
|
"loop.instructions": "All perks you have now will have lower max levels depending on their current level. You can pick one below that will gain one level and one max level instead. Your pick will be leveled up, even beyond the maximum normally allowed for that perk.",
|
||||||
"loop.no_rerolls": "You didn't have any leftover re-rolls, so your base combo stayed the same. ",
|
"loop.no_rerolls": "You didn't have any leftover re-rolls, so your base combo stayed the same. ",
|
||||||
"loop.title": "Starting loop {{loop}}",
|
"loop.title": "Starting loop {{loop}}",
|
||||||
"main_menu.basic": "Basic graphics",
|
"main_menu.basic": "Basic graphics",
|
||||||
"main_menu.basic_help": "Better performance.",
|
"main_menu.basic_help": "Better performance.",
|
||||||
"main_menu.colorful_coins": "Colorful coins",
|
"main_menu.colorful_coins": "Colorful coins",
|
||||||
"main_menu.colorful_coins_help": "Coins always spawn of the color of the brick",
|
"main_menu.colorful_coins_help": "Coins always spawn of the color of the brick",
|
||||||
|
"main_menu.donate": "You've played for {{hours}} hours",
|
||||||
|
"main_menu.donate_help": "How about donating {{suggestion}} € ? You can hide this reminder in the settings. ",
|
||||||
|
"main_menu.donation_reminder": "Remind me to donate",
|
||||||
|
"main_menu.donation_reminder_help": "See time played and donation link in main menu",
|
||||||
"main_menu.download_save_file": "Download score and stats",
|
"main_menu.download_save_file": "Download score and stats",
|
||||||
"main_menu.download_save_file_help": "Get a save file",
|
"main_menu.download_save_file_help": "Get a save file",
|
||||||
"main_menu.footer_html": "<p> \n<span>Made in France by <a href=\"https://lecaro.me\">Renan LE CARO</a>.</span> \n<a href=\"https://paypal.me/renanlecaro\" target=\"_blank\">Donate</a>\n<a href=\"https://discord.gg/DZSPqyJkwP\" target=\"_blank\">Discord</a>\n<a href=\"https://f-droid.org/en/packages/me.lecaro.breakout/\" target=\"_blank\">F-Droid</a>\n<a href=\"https://play.google.com/store/apps/details?id=me.lecaro.breakout\" target=\"_blank\">Google Play</a>\n<a href=\"https://renanlecaro.itch.io/breakout71\" target=\"_blank\">itch.io</a> \n<a href=\"https://gitlab.com/lecarore/breakout71\" target=\"_blank\">Gitlab</a>\n<a href=\"https://breakout.lecaro.me/\" target=\"_blank\">Web version</a>\n<a href=\"https://news.ycombinator.com/item?id=43183131\" target=\"_blank\">HackerNews</a>\n<a href=\"https://breakout.lecaro.me/privacy.html\" target=\"_blank\">Privacy Policy</a>\n<span>v.{{appVersion}}</span>\n</p>\n",
|
"main_menu.footer_html": "<p> \n<span>Made in France by <a href=\"https://lecaro.me\">Renan LE CARO</a>.</span> \n<a href=\"https://paypal.me/renanlecaro\" target=\"_blank\">Donate</a>\n<a href=\"https://discord.gg/DZSPqyJkwP\" target=\"_blank\">Discord</a>\n<a href=\"https://f-droid.org/en/packages/me.lecaro.breakout/\" target=\"_blank\">F-Droid</a>\n<a href=\"https://play.google.com/store/apps/details?id=me.lecaro.breakout\" target=\"_blank\">Google Play</a>\n<a href=\"https://renanlecaro.itch.io/breakout71\" target=\"_blank\">itch.io</a> \n<a href=\"https://gitlab.com/lecarore/breakout71\" target=\"_blank\">Gitlab</a>\n<a href=\"https://breakout.lecaro.me/\" target=\"_blank\">Web version</a>\n<a href=\"https://news.ycombinator.com/item?id=43183131\" target=\"_blank\">HackerNews</a>\n<a href=\"https://breakout.lecaro.me/privacy.html\" target=\"_blank\">Privacy Policy</a>\n<span>v.{{appVersion}}</span>\n</p>\n",
|
||||||
|
@ -58,13 +64,15 @@
|
||||||
"main_menu.language_help": "Choose the game's language",
|
"main_menu.language_help": "Choose the game's language",
|
||||||
"main_menu.load_save_file": "Load save file",
|
"main_menu.load_save_file": "Load save file",
|
||||||
"main_menu.load_save_file_help": "Select a save file on your device",
|
"main_menu.load_save_file_help": "Select a save file on your device",
|
||||||
|
"main_menu.loop_run": "New long run",
|
||||||
|
"main_menu.loop_run_help": "Allows you to loop up to 7 times",
|
||||||
"main_menu.max_coins": " {{max}} coins on screen maximum",
|
"main_menu.max_coins": " {{max}} coins on screen maximum",
|
||||||
"main_menu.max_coins_help": "Cosmetic only, no effect on score",
|
"main_menu.max_coins_help": "Cosmetic only, no effect on score",
|
||||||
"main_menu.max_particles": " {{max}} particles maximum",
|
"main_menu.max_particles": " {{max}} particles maximum",
|
||||||
"main_menu.max_particles_help": "Limits the number of particles show on screen for visual effect. ",
|
"main_menu.max_particles_help": "Limits the number of particles show on screen for visual effect. ",
|
||||||
"main_menu.mobile": "Mobile mode",
|
"main_menu.mobile": "Mobile mode",
|
||||||
"main_menu.mobile_help": "Leaves space under the puck.",
|
"main_menu.mobile_help": "Leaves space under the puck.",
|
||||||
"main_menu.normal": "New run",
|
"main_menu.normal": "New short run",
|
||||||
"main_menu.normal_help": "Start a quick run with random starting perk",
|
"main_menu.normal_help": "Start a quick run with random starting perk",
|
||||||
"main_menu.pointer_lock": "Mouse pointer lock",
|
"main_menu.pointer_lock": "Mouse pointer lock",
|
||||||
"main_menu.pointer_lock_help": "Locks and hides the mouse cursor.",
|
"main_menu.pointer_lock_help": "Locks and hides the mouse cursor.",
|
||||||
|
@ -74,7 +82,7 @@
|
||||||
"main_menu.reset": "Reset Game",
|
"main_menu.reset": "Reset Game",
|
||||||
"main_menu.reset_cancel": "No",
|
"main_menu.reset_cancel": "No",
|
||||||
"main_menu.reset_confirm": "Yes",
|
"main_menu.reset_confirm": "Yes",
|
||||||
"main_menu.reset_help": "Erase high score, license and statistics",
|
"main_menu.reset_help": "Erase high score, play time and statistics",
|
||||||
"main_menu.reset_instruction": "You will loose all progress you made in the game, are you sure ?",
|
"main_menu.reset_instruction": "You will loose all progress you made in the game, are you sure ?",
|
||||||
"main_menu.resume": "Resume",
|
"main_menu.resume": "Resume",
|
||||||
"main_menu.resume_help": "Return to your run",
|
"main_menu.resume_help": "Return to your run",
|
||||||
|
@ -99,26 +107,11 @@
|
||||||
"play.menu_label": "menu",
|
"play.menu_label": "menu",
|
||||||
"play.missed_ball": "miss",
|
"play.missed_ball": "miss",
|
||||||
"play.mobile_press_to_play": "Press and hold here to play",
|
"play.mobile_press_to_play": "Press and hold here to play",
|
||||||
"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 looping 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.per_hours": "You've played for {{hours}} hours",
|
|
||||||
"premium.per_hours_help": "Donate 4.99€ to get premium",
|
|
||||||
"premium.thanks": "You are premium, thanks ! ",
|
|
||||||
"premium.thanks_help": "Copy your license key",
|
|
||||||
"premium.title": "Unlock looping with premium ",
|
|
||||||
"sandbox.help": "Test any perk combination",
|
"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.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",
|
"sandbox.start": "Start test run",
|
||||||
"sandbox.title": "Sandbox mode",
|
"sandbox.title": "Sandbox mode",
|
||||||
"sandbox.unlocks_at": "Unlocks at total score {{score}}",
|
"sandbox.unlocks_at": "Unlocks at total score {{score}}",
|
||||||
"score_panel.banned": "Banned perks : {{banned}}",
|
|
||||||
"score_panel.rerolls_count": "You have accumulated {{rerolls}} rerolls",
|
"score_panel.rerolls_count": "You have accumulated {{rerolls}} rerolls",
|
||||||
"score_panel.test_run": "This is a test run, score is not recorded permanently",
|
"score_panel.test_run": "This is a test run, score is not recorded permanently",
|
||||||
"score_panel.title": "{{score}} points at level {{level}}/{{max}} ",
|
"score_panel.title": "{{score}} points at level {{level}}/{{max}} ",
|
||||||
|
@ -126,11 +119,14 @@
|
||||||
"score_panel.upcoming_levels": "Upcoming levels :",
|
"score_panel.upcoming_levels": "Upcoming levels :",
|
||||||
"score_panel.upgrades_picked": "Upgrades picked so far : ",
|
"score_panel.upgrades_picked": "Upgrades picked so far : ",
|
||||||
"unlocks.greyed_out_help": "The greyed out ones can be unlocked by increasing your total score. The total score increases every time you score in game.",
|
"unlocks.greyed_out_help": "The greyed out ones can be unlocked by increasing your total score. The total score increases every time you score in game.",
|
||||||
"unlocks.intro": "Your total score is {{ts}}. Below are all the upgrades and levels the games has to offer. Click an upgrade or level below to start a run with it .Your high score is {{highScore}}.",
|
"unlocks.intro": "Your total score is {{ts}}. Below are all the upgrades and levels the games has to offer. Click an upgrade or level below to start a short run with it .Your high score is {{highScore}}.",
|
||||||
"unlocks.level": "Here are all the game levels, click one to start a run with that starting level. ",
|
"unlocks.level": "Here are all the game levels, click one to start a run with that starting level. ",
|
||||||
"unlocks.level_description": "A {{size}}x{{size}} level with {{bricks}} bricks",
|
"unlocks.level_description": "A {{size}}x{{size}} level with {{bricks}} bricks",
|
||||||
"unlocks.title": "You unlocked {{percentUnlock}}% of the game. ",
|
"unlocks.title": "You unlocked {{percentUnlock}}% of the game. ",
|
||||||
"unlocks.unlocks_at": "Unlocks at total score {{threshold}}.",
|
"unlocks.unlocks_at": "Unlocks at total score {{threshold}}.",
|
||||||
|
"upgrades.addiction.fullHelp": "The countdown only starts after breaking the first brick of each level",
|
||||||
|
"upgrades.addiction.help": "+{{lvl}} combo / brick, combo resets {{delay}}s after breaking a brick. ",
|
||||||
|
"upgrades.addiction.name": "Addiction",
|
||||||
"upgrades.asceticism.fullHelp": "You'll need to store the coins somewhere while your combo climbs. ",
|
"upgrades.asceticism.fullHelp": "You'll need to store the coins somewhere while your combo climbs. ",
|
||||||
"upgrades.asceticism.help": "+{{combo}} combo / brick, combo resets on coin catch",
|
"upgrades.asceticism.help": "+{{combo}} combo / brick, combo resets on coin catch",
|
||||||
"upgrades.asceticism.name": "Asceticism",
|
"upgrades.asceticism.name": "Asceticism",
|
||||||
|
@ -190,7 +186,7 @@
|
||||||
"upgrades.helium.help": "Gravity reversed left and right of puck",
|
"upgrades.helium.help": "Gravity reversed left and right of puck",
|
||||||
"upgrades.helium.name": "Helium",
|
"upgrades.helium.name": "Helium",
|
||||||
"upgrades.hot_start.fullHelp": "At the start of every level, your combo will start at +15 points, but then every second it will be decreased by one.\n\nThis means the first 15 seconds in a level will spawn many more coins than the following ones, and you should make sure that you clear the level quickly. \n\nThe effect stacks with other combo related perks, so you might be able to raise the combo after the 15s timeout, but it will keep ticking down. \n\nEvery time you take the perk again, the effect will be more dramatic.",
|
"upgrades.hot_start.fullHelp": "At the start of every level, your combo will start at +15 points, but then every second it will be decreased by one.\n\nThis means the first 15 seconds in a level will spawn many more coins than the following ones, and you should make sure that you clear the level quickly. \n\nThe effect stacks with other combo related perks, so you might be able to raise the combo after the 15s timeout, but it will keep ticking down. \n\nEvery time you take the perk again, the effect will be more dramatic.",
|
||||||
"upgrades.hot_start.help": "Start at combo {{start}}, -{{lvl}} combo per second",
|
"upgrades.hot_start.help": "Start at combo {{start}}, -{{loss}} combo per second",
|
||||||
"upgrades.hot_start.name": "Hot start",
|
"upgrades.hot_start.name": "Hot start",
|
||||||
"upgrades.implosions.fullHelp": "The explosion force is applied the other way. ",
|
"upgrades.implosions.fullHelp": "The explosion force is applied the other way. ",
|
||||||
"upgrades.implosions.help": "Explosions suck coins in instead of blowing them out",
|
"upgrades.implosions.help": "Explosions suck coins in instead of blowing them out",
|
||||||
|
@ -233,7 +229,7 @@
|
||||||
"upgrades.reach.help": "+{{lvl}} combo / bricks , lowest brick of a pile resets combo",
|
"upgrades.reach.help": "+{{lvl}} combo / bricks , lowest brick of a pile resets combo",
|
||||||
"upgrades.reach.name": "Top down",
|
"upgrades.reach.name": "Top down",
|
||||||
"upgrades.respawn.fullHelp": "Some particle effect will let you know where bricks will appear. ",
|
"upgrades.respawn.fullHelp": "Some particle effect will let you know where bricks will appear. ",
|
||||||
"upgrades.respawn.help": "{{percent}} of bricks re-spawn after {{delay}}s.",
|
"upgrades.respawn.help": "{{percent}}% of bricks re-spawn after {{delay}}s.",
|
||||||
"upgrades.respawn.name": "Re-spawn",
|
"upgrades.respawn.name": "Re-spawn",
|
||||||
"upgrades.right_is_lava.fullHelp": "Whenever you break a brick, your combo will increase by one, so you'll get one more coin from all the next bricks you break.\n\nHowever, your combo will reset as soon as your ball hits the right side . \n\nAs soon as your combo rises, the right side becomes red to remind you that you should avoid hitting them\n\nThe effect stacks with other combo perks, combo rises faster with more upgrades but will also reset if any\nof the reset conditions are met.",
|
"upgrades.right_is_lava.fullHelp": "Whenever you break a brick, your combo will increase by one, so you'll get one more coin from all the next bricks you break.\n\nHowever, your combo will reset as soon as your ball hits the right side . \n\nAs soon as your combo rises, the right side becomes red to remind you that you should avoid hitting them\n\nThe effect stacks with other combo perks, combo rises faster with more upgrades but will also reset if any\nof the reset conditions are met.",
|
||||||
"upgrades.right_is_lava.help": "+{{lvl}} combo per brick broken, resets on right side hit",
|
"upgrades.right_is_lava.help": "+{{lvl}} combo per brick broken, resets on right side hit",
|
||||||
|
|
|
@ -3,11 +3,13 @@
|
||||||
"confirmRestart.text": "Vous êtes sur le point de commencer une nouvelle partie, est-ce vraiment ce que vous vouliez ?",
|
"confirmRestart.text": "Vous êtes sur le point de commencer une nouvelle partie, est-ce vraiment ce que vous vouliez ?",
|
||||||
"confirmRestart.title": "Démarrer une nouvelle partie ?",
|
"confirmRestart.title": "Démarrer une nouvelle partie ?",
|
||||||
"confirmRestart.yes": "Commencer une nouvelle partie",
|
"confirmRestart.yes": "Commencer une nouvelle partie",
|
||||||
|
"gameOver.7_loop.summary": "Cette partie est terminée. Vous avez accumulé {{score}} pièces.",
|
||||||
|
"gameOver.7_loop.title": "Vous avez terminé cette partie",
|
||||||
"gameOver.cumulative_total": "Votre score total cumulé est passé de {{startTs}} à {{endTs}}.",
|
"gameOver.cumulative_total": "Votre score total cumulé est passé de {{startTs}} à {{endTs}}.",
|
||||||
"gameOver.lost.summary": "Vous avez fait tomber la balle après avoir attrapé {{score}} pièces.",
|
"gameOver.lost.summary": "Vous avez fait tomber la balle après avoir attrapé {{score}} pièces.",
|
||||||
"gameOver.lost.title": "Balle perdue",
|
"gameOver.lost.title": "Balle perdue",
|
||||||
"gameOver.next_unlock": "Marquez {{points}} points supplémentaires pour débloquer la prochaine amélioration ou le prochain niveau.",
|
"gameOver.next_unlock": "Marquez {{points}} points supplémentaires pour débloquer la prochaine amélioration ou le prochain niveau.",
|
||||||
"gameOver.restart": "Nouvelle partie",
|
"gameOver.restart": "Nouvelle partie ",
|
||||||
"gameOver.stats.balls_lost": "Balles perdues",
|
"gameOver.stats.balls_lost": "Balles perdues",
|
||||||
"gameOver.stats.bricks_broken": "Briques cassées",
|
"gameOver.stats.bricks_broken": "Briques cassées",
|
||||||
"gameOver.stats.bricks_per_minute": "Briques cassées par minute",
|
"gameOver.stats.bricks_per_minute": "Briques cassées par minute",
|
||||||
|
@ -26,14 +28,14 @@
|
||||||
"gameOver.upgrades_picked": "Amélioration actives en fin de partie",
|
"gameOver.upgrades_picked": "Amélioration actives en fin de partie",
|
||||||
"gameOver.win.summary": "Vous avez nettoyé tous les niveaux pour cette partie, en attrapant {{score}} pièces au total.",
|
"gameOver.win.summary": "Vous avez nettoyé tous les niveaux pour cette partie, en attrapant {{score}} pièces au total.",
|
||||||
"gameOver.win.title": "Partie terminée",
|
"gameOver.win.title": "Partie terminée",
|
||||||
"level_up.after_buttons": "Vous venez de terminer le niveau {{level}}/{{max}} et vous avez choisi ces améliorations jusqu'à présent :",
|
"level_up.after_buttons": "Vous venez de terminer le niveau {{level}}/{{max}}.",
|
||||||
"level_up.before_buttons": "Vous avez attrapé {{score}} pièces {{catchGain}} sur {{levelSpawnedCoins}} en {{time}} secondes {{timeGain}}.\n\nVous avez raté les briques {{levelMisses}} fois {{missesGain}} et touché les bords de la zone de jeu {{levelWallBounces}} fois {{wallHitsGain}}.\n\n{{compliment}}",
|
"level_up.before_buttons": "Vous avez attrapé {{score}} pièces {{catchGain}} sur {{levelSpawnedCoins}} en {{time}} secondes {{timeGain}}.\n\nVous avez raté les briques {{levelMisses}} fois {{missesGain}} et touché les bords de la zone de jeu {{levelWallBounces}} fois {{wallHitsGain}}.\n\n{{compliment}}",
|
||||||
"level_up.compliment_advice": "Essayez d'attraper toutes les pièces, de ne jamais rater les briques, de ne pas toucher les murs ou de terminer le niveau en moins de 30 secondes pour obtenir des choix supplémentaires et des améliorations.",
|
"level_up.compliment_advice": "Essayez d'attraper toutes les pièces, de ne jamais rater les briques, de ne pas toucher les murs ou de terminer le niveau en moins de 30 secondes pour obtenir des choix supplémentaires et des améliorations.",
|
||||||
"level_up.compliment_good": "Bravo !",
|
"level_up.compliment_good": "Bravo !",
|
||||||
"level_up.compliment_perfect": "Impressionnant, continuez comme ça !",
|
"level_up.compliment_perfect": "Impressionnant, continuez comme ça !",
|
||||||
"level_up.pick_upgrade_title": "Choisir une amélioration",
|
"level_up.pick_upgrade_title": "Choisir une amélioration",
|
||||||
"level_up.plus_one_choice": "(+1 re-roll)",
|
"level_up.plus_one_upgrade": "(+1 upgrade)",
|
||||||
"level_up.plus_one_upgrade": "(+1 amélioration et +1 re-roll)",
|
"level_up.plus_one_upgrade_and_reroll": "(+1 amélioration et +1 re-roll)",
|
||||||
"level_up.reroll": "Re-roll ({{count}})",
|
"level_up.reroll": "Re-roll ({{count}})",
|
||||||
"level_up.reroll_help": "Nouveaux choix",
|
"level_up.reroll_help": "Nouveaux choix",
|
||||||
"level_up.unlocked_level": " (Niveau)",
|
"level_up.unlocked_level": " (Niveau)",
|
||||||
|
@ -47,6 +49,10 @@
|
||||||
"main_menu.basic_help": "Meilleures performances.",
|
"main_menu.basic_help": "Meilleures performances.",
|
||||||
"main_menu.colorful_coins": "Pièces colorées",
|
"main_menu.colorful_coins": "Pièces colorées",
|
||||||
"main_menu.colorful_coins_help": "Les pièces apparaissent toujours de la couleur de la brique",
|
"main_menu.colorful_coins_help": "Les pièces apparaissent toujours de la couleur de la brique",
|
||||||
|
"main_menu.donate": "Vous avez joué {{hours}} heures",
|
||||||
|
"main_menu.donate_help": "Pourriez-vous donner {{suggestion}} € ? Vous pouvez masquer ce rappel dans les paramètres.",
|
||||||
|
"main_menu.donation_reminder": "Me rappeler de donner",
|
||||||
|
"main_menu.donation_reminder_help": "Afficher le temps de jeu et un lien pour donner dans le menu principal",
|
||||||
"main_menu.download_save_file": "Sauvegarder mes progrès",
|
"main_menu.download_save_file": "Sauvegarder mes progrès",
|
||||||
"main_menu.download_save_file_help": "Obtenir un fichier de sauvegarde",
|
"main_menu.download_save_file_help": "Obtenir un fichier de sauvegarde",
|
||||||
"main_menu.footer_html": " <p> \n<span>Programmé en France par <a href=\"https://lecaro.me\">Renan LE CARO</a>.</span>\n<a href=\"https://paypal.me/renanlecaro\" target=\"_blank\">Donner</a>\n<a href=\"https://discord.gg/DZSPqyJkwP\" target=\"_blank\">Discord</a>\n<a href=\"https://f-droid.org/en/packages/me.lecaro.breakout/\" target=\"_blank\">F-Droid</a>\n<a href=\"https://play.google.com/store/apps/details?id=me.lecaro.breakout\" target=\"_blank\">Google Play</a>\n<a href=\"https://renanlecaro.itch.io/breakout71\" target=\"_blank\">itch.io</a>\n<a href=\"https://gitlab.com/lecarore/breakout71\" target=\"_blank\">Gitlab</a>\n<a href=\"https://breakout.lecaro.me/\" target=\"_blank\">Version web</a>\n<a href=\"https://news.ycombinator.com/item?id=43183131\" target=\"_blank\">HackerNews</a>\n<a href=\"https://breakout.lecaro.me/privacy.html\" target=\"_blank\">Politique de confidentialité</a> \n<span>v.{{appVersion}}</span>\n</p>",
|
"main_menu.footer_html": " <p> \n<span>Programmé en France par <a href=\"https://lecaro.me\">Renan LE CARO</a>.</span>\n<a href=\"https://paypal.me/renanlecaro\" target=\"_blank\">Donner</a>\n<a href=\"https://discord.gg/DZSPqyJkwP\" target=\"_blank\">Discord</a>\n<a href=\"https://f-droid.org/en/packages/me.lecaro.breakout/\" target=\"_blank\">F-Droid</a>\n<a href=\"https://play.google.com/store/apps/details?id=me.lecaro.breakout\" target=\"_blank\">Google Play</a>\n<a href=\"https://renanlecaro.itch.io/breakout71\" target=\"_blank\">itch.io</a>\n<a href=\"https://gitlab.com/lecarore/breakout71\" target=\"_blank\">Gitlab</a>\n<a href=\"https://breakout.lecaro.me/\" target=\"_blank\">Version web</a>\n<a href=\"https://news.ycombinator.com/item?id=43183131\" target=\"_blank\">HackerNews</a>\n<a href=\"https://breakout.lecaro.me/privacy.html\" target=\"_blank\">Politique de confidentialité</a> \n<span>v.{{appVersion}}</span>\n</p>",
|
||||||
|
@ -58,13 +64,15 @@
|
||||||
"main_menu.language_help": "Changer la langue d'affichage",
|
"main_menu.language_help": "Changer la langue d'affichage",
|
||||||
"main_menu.load_save_file": "Charger une sauvegarde",
|
"main_menu.load_save_file": "Charger une sauvegarde",
|
||||||
"main_menu.load_save_file_help": "Depuis un fichier ",
|
"main_menu.load_save_file_help": "Depuis un fichier ",
|
||||||
|
"main_menu.loop_run": "Nouvelle partie longue",
|
||||||
|
"main_menu.loop_run_help": "Permet de boucler le jeu jusqu'à 7 fois",
|
||||||
"main_menu.max_coins": "{{max}} pièces affichées maximum",
|
"main_menu.max_coins": "{{max}} pièces affichées maximum",
|
||||||
"main_menu.max_coins_help": "Visuel uniquement, pas d'impact sur le score",
|
"main_menu.max_coins_help": "Visuel uniquement, pas d'impact sur le score",
|
||||||
"main_menu.max_particles": " {{max}} particules maximum",
|
"main_menu.max_particles": " {{max}} particules maximum",
|
||||||
"main_menu.max_particles_help": "Limite le nombre de particules affichées à l'écran pour les effets visuels",
|
"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": "Mode mobile",
|
||||||
"main_menu.mobile_help": "Laisse un espace sous le palet.",
|
"main_menu.mobile_help": "Laisse un espace sous le palet.",
|
||||||
"main_menu.normal": "Nouvelle partie",
|
"main_menu.normal": "Nouvelle partie rapide",
|
||||||
"main_menu.normal_help": "Avec un avantage de départ aléatoire",
|
"main_menu.normal_help": "Avec un avantage de départ aléatoire",
|
||||||
"main_menu.pointer_lock": "Verrouillage du pointeur",
|
"main_menu.pointer_lock": "Verrouillage du pointeur",
|
||||||
"main_menu.pointer_lock_help": "Cache aussi le curseur de la souris.",
|
"main_menu.pointer_lock_help": "Cache aussi le curseur de la souris.",
|
||||||
|
@ -74,7 +82,7 @@
|
||||||
"main_menu.reset": "Réinitialiser le jeu",
|
"main_menu.reset": "Réinitialiser le jeu",
|
||||||
"main_menu.reset_cancel": "Non",
|
"main_menu.reset_cancel": "Non",
|
||||||
"main_menu.reset_confirm": "Oui",
|
"main_menu.reset_confirm": "Oui",
|
||||||
"main_menu.reset_help": "Effacer les scores, statistiques et licences",
|
"main_menu.reset_help": "Effacer les scores, statistiques et temps de jeu",
|
||||||
"main_menu.reset_instruction": "Vous perdrez tous les progrès que vous avez faits dans le jeu, êtes-vous sûr ?",
|
"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": "Retourner à la partie",
|
||||||
"main_menu.resume_help": "Continuer la partie en cours",
|
"main_menu.resume_help": "Continuer la partie en cours",
|
||||||
|
@ -85,8 +93,8 @@
|
||||||
"main_menu.settings_help": "Adaptez le jeu à vos besoins",
|
"main_menu.settings_help": "Adaptez le jeu à vos besoins",
|
||||||
"main_menu.settings_title": "Paramètre",
|
"main_menu.settings_title": "Paramètre",
|
||||||
"main_menu.show_fps": "Compteur de FPS",
|
"main_menu.show_fps": "Compteur de FPS",
|
||||||
"main_menu.show_fps_help": "Surveiller la perf du jeu",
|
"main_menu.show_fps_help": "Surveiller la performance du jeu",
|
||||||
"main_menu.show_stats": "Afficher les statistiques en temps réel",
|
"main_menu.show_stats": "Statistiques en temps réel",
|
||||||
"main_menu.show_stats_help": "Pièces, temps, rebonds, ratés",
|
"main_menu.show_stats_help": "Pièces, temps, rebonds, ratés",
|
||||||
"main_menu.sounds": "Sons du jeu",
|
"main_menu.sounds": "Sons du jeu",
|
||||||
"main_menu.sounds_help": "Ralentis certains téléphones.",
|
"main_menu.sounds_help": "Ralentis certains téléphones.",
|
||||||
|
@ -99,26 +107,11 @@
|
||||||
"play.menu_label": "Menu",
|
"play.menu_label": "Menu",
|
||||||
"play.missed_ball": "raté",
|
"play.missed_ball": "raté",
|
||||||
"play.mobile_press_to_play": "Gardez le doigt ici pour jouer",
|
"play.mobile_press_to_play": "Gardez le doigt ici pour jouer",
|
||||||
"premium.back": "Retour",
|
|
||||||
"premium.back_help": "Retour au menu principal",
|
|
||||||
"premium.buy": "Acheter une clé de licence",
|
|
||||||
"premium.buy_disabled_help": "À venir",
|
|
||||||
"premium.buy_help": "Vous serez redirigé vers un formulaire pour payer et recevrez la licence par e-mail. Revenez ensuite pour la saisir ici.",
|
|
||||||
"premium.enter": "Entrez la clé de licence",
|
|
||||||
"premium.enter_help": "Collez la licence dans la fenêtre qui s'ouvre",
|
|
||||||
"premium.help": "Achetez une licence pour Breakout 71 pour débloquer le bouclage du jeu et soutenir le développement. Elle coûte 4,99 € et est illimitée dans le temps. Vous pouvez l'utiliser sur plusieurs appareils, mais ne la partagez pas en ligne.",
|
|
||||||
"premium.help_google": "Bien que je prévoie de proposer des licences premium via Google Play, je n'ai pas encore eu l'occasion de le faire ; il n'y a donc pas de lien d'achat ici. Si vous possédez déjà une clé de licence, vous pouvez la saisir ci-dessous.",
|
|
||||||
"premium.per_hours": "Vous avez passé {{hours}} heures à jouer",
|
|
||||||
"premium.per_hours_help": "Donnez 4.99€ pour être premium",
|
|
||||||
"premium.thanks": "Vous êtes premium, merci !",
|
|
||||||
"premium.thanks_help": "Copiez votre clé de licence",
|
|
||||||
"premium.title": "Débloquez la boucle avec Premium",
|
|
||||||
"sandbox.help": "Tester n'importe quelle combinaison d'améliorations",
|
"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.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",
|
"sandbox.start": "Démarrer la partie de test",
|
||||||
"sandbox.title": "Mode bac à sable",
|
"sandbox.title": "Mode bac à sable",
|
||||||
"sandbox.unlocks_at": "Déverrouillé à partir d'un score total de {{score}}",
|
"sandbox.unlocks_at": "Déverrouillé à partir d'un score total de {{score}}",
|
||||||
"score_panel.banned": "Améliorations bannies : {{banned}}",
|
|
||||||
"score_panel.rerolls_count": "Vous avez accumulé {{rerolls}} rerolls",
|
"score_panel.rerolls_count": "Vous avez accumulé {{rerolls}} rerolls",
|
||||||
"score_panel.test_run": "Il s'agit d'une partie d'essai, le score n'est pas enregistré.",
|
"score_panel.test_run": "Il s'agit d'une partie d'essai, le score n'est pas enregistré.",
|
||||||
"score_panel.title": "{{score}} points au niveau {{level}}/{{max}} ",
|
"score_panel.title": "{{score}} points au niveau {{level}}/{{max}} ",
|
||||||
|
@ -126,11 +119,14 @@
|
||||||
"score_panel.upcoming_levels": "Niveaux de la parties : ",
|
"score_panel.upcoming_levels": "Niveaux de la parties : ",
|
||||||
"score_panel.upgrades_picked": "Améliorations choisies jusqu'à présent :",
|
"score_panel.upgrades_picked": "Améliorations choisies jusqu'à présent :",
|
||||||
"unlocks.greyed_out_help": "Les éléments grisées peuvent être débloquées en augmentant votre score total. Le score total augmente à chaque fois que vous marquez des points dans le jeu.",
|
"unlocks.greyed_out_help": "Les éléments grisées peuvent être débloquées en augmentant votre score total. Le score total augmente à chaque fois que vous marquez des points dans le jeu.",
|
||||||
"unlocks.intro": "Votre score total est de {{ts}}. Vous trouverez ci-dessous toutes les améliorations et tous les niveaux que le jeu peut offrir. Cliquez sur l'un d'entre eux pour commencer une nouvelle partie. ",
|
"unlocks.intro": "Votre score total est de {{ts}}. Vous trouverez ci-dessous toutes les améliorations et tous les niveaux que le jeu peut offrir. Cliquez sur l'un d'entre eux pour commencer une nouvelle partie rapide. ",
|
||||||
"unlocks.level": "Voci tous les niveaux du jeu. Cliquez sur un niveau pour commencer une nouvelle partie avec ce niveau de départ. ",
|
"unlocks.level": "Voci tous les niveaux du jeu. Cliquez sur un niveau pour commencer une nouvelle partie avec ce niveau de départ. ",
|
||||||
"unlocks.level_description": "Un niveau {{size}}x{{size}} avec {{bricks}} briques",
|
"unlocks.level_description": "Un niveau {{size}}x{{size}} avec {{bricks}} briques",
|
||||||
"unlocks.title": "Vous avez débloqué {{percentUnlock}}% du jeu.",
|
"unlocks.title": "Vous avez débloqué {{percentUnlock}}% du jeu.",
|
||||||
"unlocks.unlocks_at": "Déverrouillé au score total {{threshold}}.",
|
"unlocks.unlocks_at": "Déverrouillé au score total {{threshold}}.",
|
||||||
|
"upgrades.addiction.fullHelp": "Le décompte ne commence qu'à parti de la destruction de la première brique du niveau. ",
|
||||||
|
"upgrades.addiction.help": "+{{lvl}} combo / brique, le combo RAZ après {{delay}}s sans casser de briques",
|
||||||
|
"upgrades.addiction.name": "Addiction",
|
||||||
"upgrades.asceticism.fullHelp": "Il faudra trouver un moyen de stocker les pièces pendant que le combo grandis. ",
|
"upgrades.asceticism.fullHelp": "Il faudra trouver un moyen de stocker les pièces pendant que le combo grandis. ",
|
||||||
"upgrades.asceticism.help": "+{{combo}} combo par brique cassée, RAZ quand une pièce est attrapée",
|
"upgrades.asceticism.help": "+{{combo}} combo par brique cassée, RAZ quand une pièce est attrapée",
|
||||||
"upgrades.asceticism.name": "Ascétisme",
|
"upgrades.asceticism.name": "Ascétisme",
|
||||||
|
@ -190,7 +186,7 @@
|
||||||
"upgrades.helium.help": "Les pièce flottent au lieu de tomber autours du palet",
|
"upgrades.helium.help": "Les pièce flottent au lieu de tomber autours du palet",
|
||||||
"upgrades.helium.name": "Helium",
|
"upgrades.helium.name": "Helium",
|
||||||
"upgrades.hot_start.fullHelp": "Au début de chaque niveau, votre combo commencera à +15 points, mais à chaque seconde, il sera diminué d'un point. Cela signifie que les 15 premières secondes d'un niveau produiront beaucoup plus de pièces que les suivantes.\nVous devez vous assurer de terminer le niveau rapidement. L'effet se cumule avec d'autres avantages liés au combo, ce qui vous permet d'augmenter le combo après les 15 secondes, mais il continuera à diminuer chaque seconde. Chaque fois que vous reprenez la compétence, l'effet est encore plus prononcé.",
|
"upgrades.hot_start.fullHelp": "Au début de chaque niveau, votre combo commencera à +15 points, mais à chaque seconde, il sera diminué d'un point. Cela signifie que les 15 premières secondes d'un niveau produiront beaucoup plus de pièces que les suivantes.\nVous devez vous assurer de terminer le niveau rapidement. L'effet se cumule avec d'autres avantages liés au combo, ce qui vous permet d'augmenter le combo après les 15 secondes, mais il continuera à diminuer chaque seconde. Chaque fois que vous reprenez la compétence, l'effet est encore plus prononcé.",
|
||||||
"upgrades.hot_start.help": "Combo à {{start}}, -{{lvl}} combo par seconde",
|
"upgrades.hot_start.help": "Combo à {{start}}, -{{loss}} combo par seconde",
|
||||||
"upgrades.hot_start.name": "Démarrage à chaud",
|
"upgrades.hot_start.name": "Démarrage à chaud",
|
||||||
"upgrades.implosions.fullHelp": "La force d’explosion est appliquée dans l’autre sens.",
|
"upgrades.implosions.fullHelp": "La force d’explosion est appliquée dans l’autre sens.",
|
||||||
"upgrades.implosions.help": "Les explosions aspirent les pièces au lieu de les faire exploser.",
|
"upgrades.implosions.help": "Les explosions aspirent les pièces au lieu de les faire exploser.",
|
||||||
|
@ -233,7 +229,7 @@
|
||||||
"upgrades.reach.help": "+{{lvl}} combo / brique, la plus basse d'une colonne RAZ le combo",
|
"upgrades.reach.help": "+{{lvl}} combo / brique, la plus basse d'une colonne RAZ le combo",
|
||||||
"upgrades.reach.name": "Attaque aérienne",
|
"upgrades.reach.name": "Attaque aérienne",
|
||||||
"upgrades.respawn.fullHelp": "Des effets de particules vous indiqueront où les briques apparaîtront. ",
|
"upgrades.respawn.fullHelp": "Des effets de particules vous indiqueront où les briques apparaîtront. ",
|
||||||
"upgrades.respawn.help": "{{percent}} des briques réapparaissent après {{delay}}s.",
|
"upgrades.respawn.help": "{{percent}}% des briques réapparaissent après {{delay}}s.",
|
||||||
"upgrades.respawn.name": "Réapparition ",
|
"upgrades.respawn.name": "Réapparition ",
|
||||||
"upgrades.right_is_lava.fullHelp": "Chaque fois que vous cassez une brique, votre combo augmente d'une unité, ce qui vous permet d'obtenir une pièce de plus à chaque fois que vous cassez les briques suivantes.\n\nCependant, votre combinaison se réinitialise dès que votre balle touche le côté droit de la zone de jeu.\n\nDès que votre combo augmente, le côté droit devient rouge pour vous rappeler que vous devez éviter de le frapper.\n\nL'effet se cumule avec d'autres avantages de combo, le combo augmente plus rapidement avec plus d'améliorations, mais il se réinitialise également si l'une des conditions de réinitialisation est remplie.",
|
"upgrades.right_is_lava.fullHelp": "Chaque fois que vous cassez une brique, votre combo augmente d'une unité, ce qui vous permet d'obtenir une pièce de plus à chaque fois que vous cassez les briques suivantes.\n\nCependant, votre combinaison se réinitialise dès que votre balle touche le côté droit de la zone de jeu.\n\nDès que votre combo augmente, le côté droit devient rouge pour vous rappeler que vous devez éviter de le frapper.\n\nL'effet se cumule avec d'autres avantages de combo, le combo augmente plus rapidement avec plus d'améliorations, mais il se réinitialise également si l'une des conditions de réinitialisation est remplie.",
|
||||||
"upgrades.right_is_lava.help": "+{{lvl}} combo par brique, RAZ en cas de choc avec le coté droit",
|
"upgrades.right_is_lava.help": "+{{lvl}} combo par brique, RAZ en cas de choc avec le coté droit",
|
||||||
|
|
|
@ -62,6 +62,7 @@ export function newGameState(params: RunParams): GameState {
|
||||||
score: 0,
|
score: 0,
|
||||||
lastScoreIncrease: -1000,
|
lastScoreIncrease: -1000,
|
||||||
lastExplosion: -1000,
|
lastExplosion: -1000,
|
||||||
|
lastBrickBroken: 0,
|
||||||
highScore: parseFloat(localStorage.getItem("breakout-3-hs") || "0"),
|
highScore: parseFloat(localStorage.getItem("breakout-3-hs") || "0"),
|
||||||
balls: [],
|
balls: [],
|
||||||
ballsColor: "white",
|
ballsColor: "white",
|
||||||
|
@ -111,6 +112,7 @@ export function newGameState(params: RunParams): GameState {
|
||||||
loop: 0,
|
loop: 0,
|
||||||
baseCombo: 1,
|
baseCombo: 1,
|
||||||
levelsPerLoop: params?.levelsPerLoop ?? 7,
|
levelsPerLoop: params?.levelsPerLoop ?? 7,
|
||||||
|
maxLoop: params?.maxLoop ?? 0,
|
||||||
};
|
};
|
||||||
resetBalls(gameState);
|
resetBalls(gameState);
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { t } from "./i18n/i18n";
|
||||||
|
|
||||||
import { OptionDef, OptionId } from "./types";
|
import { OptionDef, OptionId } from "./types";
|
||||||
import { getSettingValue, setSettingValue } from "./settings";
|
import { getSettingValue, setSettingValue } from "./settings";
|
||||||
|
import { hoursSpentPlaying } from "./pure_functions";
|
||||||
|
|
||||||
export const options = {
|
export const options = {
|
||||||
sound: {
|
sound: {
|
||||||
|
@ -55,6 +56,11 @@ export const options = {
|
||||||
name: t("main_menu.fullscreen"),
|
name: t("main_menu.fullscreen"),
|
||||||
help: t("main_menu.fullscreen_help"),
|
help: t("main_menu.fullscreen_help"),
|
||||||
},
|
},
|
||||||
|
donation_reminder: {
|
||||||
|
default: hoursSpentPlaying() > 5,
|
||||||
|
name: t("main_menu.donation_reminder"),
|
||||||
|
help: t("main_menu.donation_reminder_help"),
|
||||||
|
},
|
||||||
} as const satisfies { [k: string]: OptionDef };
|
} as const satisfies { [k: string]: OptionDef };
|
||||||
|
|
||||||
export function isOptionOn(key: OptionId) {
|
export function isOptionOn(key: OptionId) {
|
||||||
|
|
181
src/premium.ts
181
src/premium.ts
|
@ -1,181 +0,0 @@
|
||||||
import { GameState } from "./types";
|
|
||||||
import { icons } from "./loadGameData";
|
|
||||||
import { t } from "./i18n/i18n";
|
|
||||||
import { getSettingValue, setSettingValue } from "./settings";
|
|
||||||
import { asyncAlert } from "./asyncAlert";
|
|
||||||
import { openMainMenu } 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:premium_active"],
|
|
||||||
text: t("premium.thanks"),
|
|
||||||
help: t("premium.thanks_help"),
|
|
||||||
value: async () => {
|
|
||||||
navigator.clipboard.writeText(getSettingValue("license", ""));
|
|
||||||
openMainMenu();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let text = t("premium.title");
|
|
||||||
let help = t("premium.buy");
|
|
||||||
try {
|
|
||||||
const timePlayed = localStorage.getItem("breakout_71_total_play_time");
|
|
||||||
if (timePlayed && !isGooglePlayInstall) {
|
|
||||||
const hours = parseFloat(timePlayed) / 1000 / 60 / 60;
|
|
||||||
const pricePerHours = 4.99 / hours;
|
|
||||||
const args = {
|
|
||||||
hours: Math.floor(hours),
|
|
||||||
pricePerHours: pricePerHours.toFixed(2),
|
|
||||||
};
|
|
||||||
if (pricePerHours > 0 && pricePerHours < 0.5) {
|
|
||||||
text = t("premium.per_hours", args);
|
|
||||||
help = t("premium.per_hours_help", args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
icon: icons["icon:premium"],
|
|
||||||
text,
|
|
||||||
help,
|
|
||||||
value: () => openPremiumMenu(""),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const isGooglePlayInstall =
|
|
||||||
new URLSearchParams(location.search).get("source") === "com.android.vending";
|
|
||||||
|
|
||||||
async function openPremiumMenu(text) {
|
|
||||||
const cb = await asyncAlert({
|
|
||||||
title: t("premium.title"),
|
|
||||||
content: [
|
|
||||||
text ||
|
|
||||||
(isGooglePlayInstall && t("premium.help_google")) ||
|
|
||||||
t("premium.help"),
|
|
||||||
{
|
|
||||||
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();
|
|
||||||
}
|
|
|
@ -5,3 +5,13 @@ export function clamp(value: number, min: number, max: number) {
|
||||||
export function comboKeepingRate(level: number) {
|
export function comboKeepingRate(level: number) {
|
||||||
return clamp(1 - (1 / (1 + level)) * 1.5, 0, 1);
|
return clamp(1 - (1 / (1 + level)) * 1.5, 0, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hoursSpentPlaying() {
|
||||||
|
try {
|
||||||
|
const timePlayed =
|
||||||
|
localStorage.getItem("breakout_71_total_play_time") || "0";
|
||||||
|
return Math.floor(parseFloat(timePlayed) / 1000 / 60 / 60);
|
||||||
|
} catch (e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ bombSVG.src =
|
||||||
bombSVG.onload = () => (gameState.needsRender = true);
|
bombSVG.onload = () => (gameState.needsRender = true);
|
||||||
|
|
||||||
export const background = document.createElement("img");
|
export const background = document.createElement("img");
|
||||||
|
background.onload = () => (gameState.needsRender = true);
|
||||||
export const backgroundCanvas = document.createElement("canvas");
|
export const backgroundCanvas = document.createElement("canvas");
|
||||||
|
|
||||||
export function render(gameState: GameState) {
|
export function render(gameState: GameState) {
|
||||||
|
@ -67,16 +68,16 @@ export function render(gameState: GameState) {
|
||||||
: "") +
|
: "") +
|
||||||
(isOptionOn("show_stats")
|
(isOptionOn("show_stats")
|
||||||
? `
|
? `
|
||||||
<span class="${(catchRate == 1 && "great") || (catchRate > 0.9 && "good") || ""}">
|
<span class="${(catchRate > 0.95 && "great") || (catchRate > 0.9 && "good") || ""}">
|
||||||
${Math.floor(catchRate * 100)}%
|
${Math.floor(catchRate * 100)}%
|
||||||
</span><span> / </span>
|
</span><span> / </span>
|
||||||
<span class="${(gameState.levelWallBounces == 0 && "great") || (gameState.levelWallBounces < 5 && "good") || ""}">
|
|
||||||
${gameState.levelWallBounces} B
|
|
||||||
</span><span> / </span>
|
|
||||||
<span class="${(gameState.levelTime < 30000 && "great") || (gameState.levelTime < 60000 && "good") || ""}">
|
<span class="${(gameState.levelTime < 30000 && "great") || (gameState.levelTime < 60000 && "good") || ""}">
|
||||||
${Math.ceil(gameState.levelTime / 1000)}s
|
${Math.ceil(gameState.levelTime / 1000)}s
|
||||||
</span><span> / </span>
|
</span><span> / </span>
|
||||||
<span class="${(gameState.levelMisses == 0 && "great") || (gameState.levelMisses <= 3 && "good") || ""}">
|
<span class="${(gameState.levelWallBounces < 3 && "great") || (gameState.levelWallBounces < 10 && "good") || ""}">
|
||||||
|
${gameState.levelWallBounces} B
|
||||||
|
</span><span> / </span>
|
||||||
|
<span class="${(gameState.levelMisses < 3 && "great") || (gameState.levelMisses < 6 && "good") || ""}">
|
||||||
${gameState.levelMisses} M
|
${gameState.levelMisses} M
|
||||||
</span><span> / </span>
|
</span><span> / </span>
|
||||||
`
|
`
|
||||||
|
@ -628,11 +629,12 @@ export function renderAllBricks() {
|
||||||
gameState.perks.clairvoyant >= 2,
|
gameState.perks.clairvoyant >= 2,
|
||||||
);
|
);
|
||||||
if (gameState.brickHP[index] > 1 && gameState.perks.clairvoyant) {
|
if (gameState.brickHP[index] > 1 && gameState.perks.clairvoyant) {
|
||||||
canctx.globalCompositeOperation = "destination-out";
|
canctx.globalCompositeOperation =
|
||||||
|
gameState.perks.clairvoyant >= 2 ? "source-over" : "destination-out";
|
||||||
drawText(
|
drawText(
|
||||||
canctx,
|
canctx,
|
||||||
gameState.brickHP[index].toString(),
|
gameState.brickHP[index].toString(),
|
||||||
"white",
|
color,
|
||||||
gameState.puckHeight,
|
gameState.puckHeight,
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
|
|
|
@ -49,16 +49,16 @@ export const sounds = {
|
||||||
if (!isOptionOn("sound")) return;
|
if (!isOptionOn("sound")) return;
|
||||||
createSingleBounceSound(1200, pan, volume, 0.1, "triangle");
|
createSingleBounceSound(1200, pan, volume, 0.1, "triangle");
|
||||||
},
|
},
|
||||||
void: (volume: number, pan: number) => {
|
// void: (volume: number, pan: number) => {
|
||||||
if (!isOptionOn("sound")) return;
|
// if (!isOptionOn("sound")) return;
|
||||||
createSingleBounceSound(1200, pan, volume, 0.5, "sawtooth");
|
// createSingleBounceSound(1200, pan, volume, 0.5, "sawtooth");
|
||||||
createSingleBounceSound(600, pan, volume, 0.3, "sawtooth");
|
// createSingleBounceSound(600, pan, volume, 0.3, "sawtooth");
|
||||||
},
|
// },
|
||||||
freeze: (volume: number, pan: number) => {
|
// freeze: (volume: number, pan: number) => {
|
||||||
if (!isOptionOn("sound")) return;
|
// if (!isOptionOn("sound")) return;
|
||||||
createSingleBounceSound(220, pan, volume, 0.5, "square");
|
// createSingleBounceSound(220, pan, volume, 0.5, "square");
|
||||||
createSingleBounceSound(440, pan, volume, 0.5, "square");
|
// createSingleBounceSound(440, pan, volume, 0.5, "square");
|
||||||
},
|
// },
|
||||||
explode: (volume: number, pan: number, combo: number) => {
|
explode: (volume: number, pan: number, combo: number) => {
|
||||||
if (!isOptionOn("sound")) return;
|
if (!isOptionOn("sound")) return;
|
||||||
createExplosionSound(pan);
|
createExplosionSound(pan);
|
||||||
|
|
5
src/types.d.ts
vendored
5
src/types.d.ts
vendored
|
@ -221,6 +221,7 @@ export type GameState = {
|
||||||
lastScoreIncrease: number;
|
lastScoreIncrease: number;
|
||||||
// levelTime of the last explosion, for screen shake
|
// levelTime of the last explosion, for screen shake
|
||||||
lastExplosion: number;
|
lastExplosion: number;
|
||||||
|
lastBrickBroken: number;
|
||||||
// High score at the beginning of the run
|
// High score at the beginning of the run
|
||||||
highScore: number;
|
highScore: number;
|
||||||
// Balls currently in game, game over if it's empty
|
// Balls currently in game, game over if it's empty
|
||||||
|
@ -278,13 +279,12 @@ export type GameState = {
|
||||||
lifeLost: { vol: number; x: number };
|
lifeLost: { vol: number; x: number };
|
||||||
coinCatch: { vol: number; x: number };
|
coinCatch: { vol: number; x: number };
|
||||||
colorChange: { vol: number; x: number };
|
colorChange: { vol: number; x: number };
|
||||||
void: { vol: number; x: number };
|
|
||||||
freeze: { vol: number; x: number };
|
|
||||||
};
|
};
|
||||||
rerolls: number;
|
rerolls: number;
|
||||||
loop: number;
|
loop: number;
|
||||||
baseCombo: number;
|
baseCombo: number;
|
||||||
levelsPerLoop: number;
|
levelsPerLoop: number;
|
||||||
|
maxLoop: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RunParams = {
|
export type RunParams = {
|
||||||
|
@ -292,6 +292,7 @@ export type RunParams = {
|
||||||
levelToAvoid?: string;
|
levelToAvoid?: string;
|
||||||
perks?: Partial<PerksMap>;
|
perks?: Partial<PerksMap>;
|
||||||
levelsPerLoop?: number;
|
levelsPerLoop?: number;
|
||||||
|
maxLoop?: number;
|
||||||
};
|
};
|
||||||
export type OptionDef = {
|
export type OptionDef = {
|
||||||
default: boolean;
|
default: boolean;
|
||||||
|
|
|
@ -128,8 +128,8 @@ export const rawUpgrades = [
|
||||||
|
|
||||||
threshold: 500,
|
threshold: 500,
|
||||||
id: "telekinesis",
|
id: "telekinesis",
|
||||||
giftable: false,
|
giftable: true,
|
||||||
max: 2,
|
max: 1,
|
||||||
name: t("upgrades.telekinesis.name"),
|
name: t("upgrades.telekinesis.name"),
|
||||||
help: (lvl: number) =>
|
help: (lvl: number) =>
|
||||||
lvl == 1
|
lvl == 1
|
||||||
|
@ -156,7 +156,7 @@ export const rawUpgrades = [
|
||||||
|
|
||||||
threshold: 1500,
|
threshold: 1500,
|
||||||
id: "multiball",
|
id: "multiball",
|
||||||
giftable: false,
|
giftable: true,
|
||||||
max: 6,
|
max: 6,
|
||||||
name: t("upgrades.multiball.name"),
|
name: t("upgrades.multiball.name"),
|
||||||
help: (lvl: number) => t("upgrades.multiball.help", { count: lvl + 1 }),
|
help: (lvl: number) => t("upgrades.multiball.help", { count: lvl + 1 }),
|
||||||
|
@ -229,8 +229,8 @@ export const rawUpgrades = [
|
||||||
name: t("upgrades.hot_start.name"),
|
name: t("upgrades.hot_start.name"),
|
||||||
help: (lvl: number) =>
|
help: (lvl: number) =>
|
||||||
t("upgrades.hot_start.help", {
|
t("upgrades.hot_start.help", {
|
||||||
start: lvl * 15 + 1,
|
start: lvl * 30 + 1,
|
||||||
lvl,
|
loss: lvl,
|
||||||
}),
|
}),
|
||||||
fullHelp: t("upgrades.hot_start.fullHelp"),
|
fullHelp: t("upgrades.hot_start.fullHelp"),
|
||||||
},
|
},
|
||||||
|
@ -357,7 +357,7 @@ export const rawUpgrades = [
|
||||||
name: t("upgrades.sturdy_bricks.name"),
|
name: t("upgrades.sturdy_bricks.name"),
|
||||||
help: (lvl: number) =>
|
help: (lvl: number) =>
|
||||||
// lvl == 1
|
// lvl == 1
|
||||||
t("upgrades.sturdy_bricks.help", { lvl, percent: lvl * 10 }),
|
t("upgrades.sturdy_bricks.help", { lvl, percent: lvl * 50 }),
|
||||||
// ?
|
// ?
|
||||||
// : t("upgrades.sturdy_bricks.help_plural"),
|
// : t("upgrades.sturdy_bricks.help_plural"),
|
||||||
fullHelp: t("upgrades.sturdy_bricks.fullHelp"),
|
fullHelp: t("upgrades.sturdy_bricks.fullHelp"),
|
||||||
|
@ -465,7 +465,7 @@ export const rawUpgrades = [
|
||||||
threshold: 85000,
|
threshold: 85000,
|
||||||
giftable: false,
|
giftable: false,
|
||||||
id: "yoyo",
|
id: "yoyo",
|
||||||
max: 2,
|
max: 1,
|
||||||
name: t("upgrades.yoyo.name"),
|
name: t("upgrades.yoyo.name"),
|
||||||
help: (lvl: number) => t("upgrades.yoyo.help"),
|
help: (lvl: number) => t("upgrades.yoyo.help"),
|
||||||
fullHelp: t("upgrades.yoyo.fullHelp"),
|
fullHelp: t("upgrades.yoyo.fullHelp"),
|
||||||
|
@ -596,7 +596,7 @@ export const rawUpgrades = [
|
||||||
threshold: 145000,
|
threshold: 145000,
|
||||||
giftable: false,
|
giftable: false,
|
||||||
id: "clairvoyant",
|
id: "clairvoyant",
|
||||||
max: 1,
|
max: 3,
|
||||||
name: t("upgrades.clairvoyant.name"),
|
name: t("upgrades.clairvoyant.name"),
|
||||||
help: (lvl: number) => t("upgrades.clairvoyant.help"),
|
help: (lvl: number) => t("upgrades.clairvoyant.help"),
|
||||||
fullHelp: t("upgrades.clairvoyant.fullHelp"),
|
fullHelp: t("upgrades.clairvoyant.fullHelp"),
|
||||||
|
@ -632,4 +632,15 @@ export const rawUpgrades = [
|
||||||
help: (lvl: number) => t("upgrades.corner_shot.help"),
|
help: (lvl: number) => t("upgrades.corner_shot.help"),
|
||||||
fullHelp: t("upgrades.corner_shot.fullHelp"),
|
fullHelp: t("upgrades.corner_shot.fullHelp"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
requires: "",
|
||||||
|
threshold: 165000,
|
||||||
|
giftable: false,
|
||||||
|
id: "addiction",
|
||||||
|
max: 10,
|
||||||
|
name: t("upgrades.addiction.name"),
|
||||||
|
help: (lvl: number) =>
|
||||||
|
t("upgrades.addiction.help", { lvl, delay: (5 / lvl).toFixed(2) }),
|
||||||
|
fullHelp: t("upgrades.addiction.fullHelp"),
|
||||||
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue