mirror of
https://gitlab.com/lecarore/breakout71.git
synced 2025-04-20 20:16:16 -04:00
Looping mode
This commit is contained in:
parent
3d5547e786
commit
5012076039
21 changed files with 2852 additions and 2696 deletions
32
Readme.md
32
Readme.md
|
@ -14,16 +14,33 @@ Break colourful bricks, catch bouncing coins and select powerful upgrades !
|
||||||
- [HackerNews thread](https://news.ycombinator.com/item?id=43183131)
|
- [HackerNews thread](https://news.ycombinator.com/item?id=43183131)
|
||||||
|
|
||||||
|
|
||||||
|
# Premium: allow looping
|
||||||
|
|
||||||
|
Allow players to loop the game :
|
||||||
|
- [x] keep your score
|
||||||
|
- [x] keep 1 perk
|
||||||
|
- [x] add one hasard
|
||||||
|
- [x] add one HP to all bricks - as a debuff
|
||||||
|
- [ ] advertise looping in normal game over screen
|
||||||
|
- real time stats as the option says.
|
||||||
|
- [x] Noise of coins against side is annoying.
|
||||||
|
- Change look of loop, to avoid picking randomly at loop end.
|
||||||
|
- make red coins scarier,
|
||||||
|
- add blue coins that only freeze puck.
|
||||||
|
- Make fullscreen an option and turn it back on when playing
|
||||||
|
- +1 combo de base par rerolls
|
||||||
|
- +1 combo de base par vie restantes (pas attrapable)
|
||||||
|
|
||||||
# Todo
|
# Todo
|
||||||
|
- [jaceys] Counters for coins lost, misses, and boundary bounces, as well as a timer.
|
||||||
|
|
||||||
- people assume unbounded allows for wrap around
|
- people assume unbounded allows for wrap around
|
||||||
- coin magnet and viscosity : only one level ~2.5
|
- coin magnet and viscosity : only one level ~2.5
|
||||||
- Boost Ascetism : give +2 or even +3 combo per brick destroyed
|
- Boost Ascetism : give +2 or even +3 combo per brick destroyed
|
||||||
- wind : move coins based on puck movement not position
|
- wind : move coins based on puck movement not position
|
||||||
- show -N points in red when combo resets
|
- show -N points in red when combo resets
|
||||||
- reach : this is too punishing now, maybe only reset if you hit the lowest populate row of the level, if it's not a full width row
|
- reach is too punishing now, maybe only reset if you hit the lowest populate row of the level, if it's not a full width row
|
||||||
- respawn: N% of bricks respawn after N seconds
|
- respawn: N% of bricks respawn after N seconds
|
||||||
- [jaceys] Counters for coins lost, misses, and boundary bounces, as well as a timer.
|
|
||||||
- [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
|
||||||
- [obigre] Offer to level ups perks separately
|
- [obigre] Offer to level ups perks separately
|
||||||
|
@ -31,17 +48,6 @@ Break colourful bricks, catch bouncing coins and select powerful upgrades !
|
||||||
- https://weblate.org/fr/
|
- https://weblate.org/fr/
|
||||||
|
|
||||||
|
|
||||||
# Premium: allow looping
|
|
||||||
|
|
||||||
Allow players to loop the game :
|
|
||||||
- [x] keep your score
|
|
||||||
- [x] keep 1 perk
|
|
||||||
- [x] add one hasard
|
|
||||||
- [ ] add one HP to all bricks
|
|
||||||
- [ ] advertise looping in normal game over screen
|
|
||||||
- [ ] save score at the end of first loop, in addition to the final one ?
|
|
||||||
- [ ] check that stats like max level are correct
|
|
||||||
|
|
||||||
|
|
||||||
# System requirements
|
# System requirements
|
||||||
|
|
||||||
|
|
|
@ -11,8 +11,8 @@ android {
|
||||||
applicationId = "me.lecaro.breakout"
|
applicationId = "me.lecaro.breakout"
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 29050375
|
versionCode = 29053110
|
||||||
versionName = "29050375"
|
versionName = "29053110"
|
||||||
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
102
dist/index.html
vendored
102
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 = "29050375";
|
const VERSION = "29053110";
|
||||||
|
|
||||||
// The name of the cache
|
// The name of the cache
|
||||||
const CACHE_NAME = `breakout-71-${VERSION}`;
|
const CACHE_NAME = `breakout-71-${VERSION}`;
|
||||||
|
|
|
@ -1046,4 +1046,4 @@
|
||||||
"svg": null,
|
"svg": null,
|
||||||
"color": ""
|
"color": ""
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
"29050375"
|
"29053110"
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { t } from "./i18n/i18n";
|
import { t } from "./i18n/i18n";
|
||||||
import {Debuff} from "./types";
|
import { Debuff } from "./types";
|
||||||
|
|
||||||
export const debuffs = [
|
export const debuffs = [
|
||||||
{
|
{
|
||||||
id: "negative_coins",
|
id: "negative_coins",
|
||||||
max: 20,
|
max: 20,
|
||||||
name: (lvl: number) => t("debuffs.negative_coins.help",{lvl}),
|
name: (lvl: number) => t("debuffs.negative_coins.help", { lvl }),
|
||||||
help: (lvl: number) => t("debuffs.negative_coins.help", { lvl }),
|
help: (lvl: number) => t("debuffs.negative_coins.help", { lvl }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -17,8 +17,10 @@ export const debuffs = [
|
||||||
{
|
{
|
||||||
id: "banned",
|
id: "banned",
|
||||||
max: 50,
|
max: 50,
|
||||||
name: (lvl: number,banned:string) => t("debuffs.banned.description",{lvl,banned}),
|
name: (lvl: number, banned: string) =>
|
||||||
help: (lvl: number,perk:string) => t("debuffs.banned.help", { lvl,perk }),
|
t("debuffs.banned.description", { lvl, banned }),
|
||||||
|
help: (lvl: number, perk: string) =>
|
||||||
|
t("debuffs.banned.help", { lvl, perk }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "interference",
|
id: "interference",
|
||||||
|
@ -30,8 +32,14 @@ export const debuffs = [
|
||||||
{
|
{
|
||||||
id: "fragility",
|
id: "fragility",
|
||||||
max: 5,
|
max: 5,
|
||||||
name: (lvl: number) => t("debuffs.fragility.help", { percent:lvl*20 }),
|
name: (lvl: number) => t("debuffs.fragility.help", { percent: lvl * 20 }),
|
||||||
help: (lvl: number) => t("debuffs.fragility.help", { percent:lvl*20 }),
|
help: (lvl: number) => t("debuffs.fragility.help", { percent: lvl * 20 }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sturdiness",
|
||||||
|
max: 5,
|
||||||
|
name: (lvl: number) => t("debuffs.sturdiness.help", { lvl }),
|
||||||
|
help: (lvl: number) => t("debuffs.sturdiness.help", { lvl }),
|
||||||
},
|
},
|
||||||
] as const as Debuff[];
|
] as const as Debuff[];
|
||||||
|
|
||||||
|
|
43
src/game.ts
43
src/game.ts
|
@ -14,7 +14,8 @@ import {
|
||||||
import { getAudioContext, playPendingSounds } from "./sounds";
|
import { getAudioContext, playPendingSounds } from "./sounds";
|
||||||
import {
|
import {
|
||||||
bannedUpgradesHTMl,
|
bannedUpgradesHTMl,
|
||||||
currentLevelInfo, debuffsHTMl,
|
currentLevelInfo,
|
||||||
|
debuffsHTMl,
|
||||||
getRowColIndex,
|
getRowColIndex,
|
||||||
levelsListHTMl,
|
levelsListHTMl,
|
||||||
max_levels,
|
max_levels,
|
||||||
|
@ -447,25 +448,24 @@ document.addEventListener("visibilitychange", () => {
|
||||||
async function openScorePanel() {
|
async function openScorePanel() {
|
||||||
pause(true);
|
pause(true);
|
||||||
const cb = await asyncAlert({
|
const cb = await asyncAlert({
|
||||||
title:
|
title: gameState.loop
|
||||||
gameState.loop ?
|
? t("score_panel.title_looped", {
|
||||||
t("score_panel.title_looped", {
|
loop: gameState.loop,
|
||||||
loop:gameState.loop,
|
|
||||||
score: gameState.score,
|
score: gameState.score,
|
||||||
level: gameState.currentLevel + 1,
|
level: gameState.currentLevel + 1,
|
||||||
max: max_levels(gameState),
|
max: max_levels(gameState),
|
||||||
}):
|
})
|
||||||
t("score_panel.title", {
|
: t("score_panel.title", {
|
||||||
score: gameState.score,
|
score: gameState.score,
|
||||||
level: gameState.currentLevel + 1,
|
level: gameState.currentLevel + 1,
|
||||||
max: max_levels(gameState),
|
max: max_levels(gameState),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
content: [
|
content: [
|
||||||
gameState.isCreativeModeRun ? `<p>${t("score_panel.test_run")}</p>` : "",
|
gameState.isCreativeModeRun ? `<p>${t("score_panel.test_run")}</p>` : "",
|
||||||
pickedUpgradesHTMl(gameState),
|
pickedUpgradesHTMl(gameState),
|
||||||
levelsListHTMl(gameState),
|
levelsListHTMl(gameState),
|
||||||
debuffsHTMl(gameState),
|
debuffsHTMl(gameState),
|
||||||
],
|
],
|
||||||
allowClose: true,
|
allowClose: true,
|
||||||
});
|
});
|
||||||
|
@ -1013,22 +1013,23 @@ restart(
|
||||||
// // unbounded: 1,
|
// // unbounded: 1,
|
||||||
// // pierce_color: 1,
|
// // pierce_color: 1,
|
||||||
// pierce: 1,
|
// pierce: 1,
|
||||||
streak_shots:1,
|
// streak_shots: 1,
|
||||||
// multiball: 6,
|
// multiball: 6,
|
||||||
// base_combo: 7,
|
base_combo: 7,
|
||||||
// telekinesis: 2,
|
telekinesis: 2,
|
||||||
// yoyo: 2,
|
yoyo: 2,
|
||||||
pierce:10,
|
pierce: 10,
|
||||||
// metamorphosis: 1,
|
// metamorphosis: 1,
|
||||||
// implosions: 1,
|
// implosions: 1,
|
||||||
// sturdy_bricks:5
|
// sturdy_bricks:5
|
||||||
extra_life:3
|
coin_magnet:2,
|
||||||
|
extra_life: 3,
|
||||||
},
|
},
|
||||||
debuffs:{
|
debuffs: {
|
||||||
// fragility:3
|
// fragility:3
|
||||||
negative_coins:1
|
negative_coins: 100,
|
||||||
// interference:20,
|
// interference:20,
|
||||||
}
|
},
|
||||||
}) ||
|
}) ||
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
|
|
@ -136,7 +136,7 @@ export function gameOver(title: string, intro: string) {
|
||||||
],
|
],
|
||||||
}).then(() =>
|
}).then(() =>
|
||||||
restart({
|
restart({
|
||||||
levelToAvoid: currentLevelInfo(gameState).name
|
levelToAvoid: currentLevelInfo(gameState).name,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -271,6 +271,7 @@ export function getHistograms() {
|
||||||
(r) => r.max_combo,
|
(r) => r.max_combo,
|
||||||
"",
|
"",
|
||||||
);
|
);
|
||||||
|
runStats += makeHistogram(t("gameOver.stats.loops"), (r) => r.loops, "");
|
||||||
|
|
||||||
if (runStats) {
|
if (runStats) {
|
||||||
runStats =
|
runStats =
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,7 +1,7 @@
|
||||||
import { Ball, GameState, PerkId, PerksMap } from "./types";
|
import { Ball, GameState, PerkId, PerksMap } from "./types";
|
||||||
import { icons, upgrades } from "./loadGameData";
|
import { icons, upgrades } from "./loadGameData";
|
||||||
import { t } from "./i18n/i18n";
|
import { t } from "./i18n/i18n";
|
||||||
import {debuffs} from "./debuffs";
|
import { debuffs } from "./debuffs";
|
||||||
|
|
||||||
export function getMajorityValue(arr: string[]): string {
|
export function getMajorityValue(arr: string[]): string {
|
||||||
const count: { [k: string]: number } = {};
|
const count: { [k: string]: number } = {};
|
||||||
|
@ -55,7 +55,6 @@ export function getPossibleUpgrades(gameState: GameState) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function max_levels(gameState: GameState) {
|
export function max_levels(gameState: GameState) {
|
||||||
|
|
||||||
return 7 + gameState.perks.extra_levels;
|
return 7 + gameState.perks.extra_levels;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,10 +69,15 @@ export function pickedUpgradesHTMl(gameState: GameState) {
|
||||||
return ` <p>${t("score_panel.upgrades_picked")}</p> <p>${list}</p>`;
|
return ` <p>${t("score_panel.upgrades_picked")}</p> <p>${list}</p>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function debuffsHTMl(gameState: GameState): string {
|
||||||
export function debuffsHTMl(gameState: GameState):string {
|
const banned = upgrades
|
||||||
const banned = upgrades.filter(u=>gameState.bannedPerks[u.id]).map(u=>u.name).join(', ')
|
.filter((u) => gameState.bannedPerks[u.id])
|
||||||
let list = debuffs.filter(d=>gameState.debuffs[d.id]).map(d=>d.name(gameState.debuffs[d.id], banned)).join(' ');
|
.map((u) => u.name)
|
||||||
|
.join(", ");
|
||||||
|
let list = debuffs
|
||||||
|
.filter((d) => gameState.debuffs[d.id])
|
||||||
|
.map((d) => d.name(gameState.debuffs[d.id], banned))
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
if (!list) return "";
|
if (!list) return "";
|
||||||
return `<p>${t("score_panel.bebuffs_list")} ${list}</p>`;
|
return `<p>${t("score_panel.bebuffs_list")} ${list}</p>`;
|
||||||
|
|
|
@ -202,6 +202,26 @@
|
||||||
</concept_node>
|
</concept_node>
|
||||||
</children>
|
</children>
|
||||||
</folder_node>
|
</folder_node>
|
||||||
|
<folder_node>
|
||||||
|
<name>sturdiness</name>
|
||||||
|
<children>
|
||||||
|
<concept_node>
|
||||||
|
<name>help</name>
|
||||||
|
<description/>
|
||||||
|
<comment/>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-FR</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
</children>
|
||||||
|
</folder_node>
|
||||||
</children>
|
</children>
|
||||||
</folder_node>
|
</folder_node>
|
||||||
<folder_node>
|
<folder_node>
|
||||||
|
@ -470,6 +490,21 @@
|
||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>loops</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>total_score</name>
|
<name>total_score</name>
|
||||||
<description/>
|
<description/>
|
||||||
|
@ -787,6 +822,21 @@
|
||||||
<folder_node>
|
<folder_node>
|
||||||
<name>loop</name>
|
<name>loop</name>
|
||||||
<children>
|
<children>
|
||||||
|
<concept_node>
|
||||||
|
<name>converted_rerolls</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>instructions</name>
|
<name>instructions</name>
|
||||||
<description/>
|
<description/>
|
||||||
|
@ -802,6 +852,21 @@
|
||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>no_rerolls</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>title</name>
|
<name>title</name>
|
||||||
<description/>
|
<description/>
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
"debuffs.interference.help": "Telekinesis and yo-yo glitch for {{lvl}}s every 7s.",
|
"debuffs.interference.help": "Telekinesis and yo-yo glitch for {{lvl}}s every 7s.",
|
||||||
"debuffs.more_bombs.help": "{{lvl}} bricks replaced by bombs.",
|
"debuffs.more_bombs.help": "{{lvl}} bricks replaced by bombs.",
|
||||||
"debuffs.negative_coins.help": "{{lvl}}/10000 of coins spawn cursed, blinking red. Game over if you catch them.",
|
"debuffs.negative_coins.help": "{{lvl}}/10000 of coins spawn cursed, blinking red. Game over if you catch them.",
|
||||||
|
"debuffs.sturdiness.help": "All bricks have +{{lvl}} HP",
|
||||||
"gameOver.because_cursed_coin": "Game over",
|
"gameOver.because_cursed_coin": "Game over",
|
||||||
"gameOver.because_cursed_coin_intro": "You cough a cursed coin (bright red coins) and didn't have a extra life to spare. ",
|
"gameOver.because_cursed_coin_intro": "You cough a cursed coin (bright red coins) and didn't have a extra life to spare. ",
|
||||||
"gameOver.cumulative_total": "Your total cumulative score went from {{startTs}} to {{endTs}}.",
|
"gameOver.cumulative_total": "Your total cumulative score went from {{startTs}} to {{endTs}}.",
|
||||||
|
@ -26,6 +27,7 @@
|
||||||
"gameOver.stats.hit_rate": "Hit rate",
|
"gameOver.stats.hit_rate": "Hit rate",
|
||||||
"gameOver.stats.intro": "Find below your run statistics compared to your {{count}} best runs.",
|
"gameOver.stats.intro": "Find below your run statistics compared to your {{count}} best runs.",
|
||||||
"gameOver.stats.level_reached": "Level reached",
|
"gameOver.stats.level_reached": "Level reached",
|
||||||
|
"gameOver.stats.loops": "Loops",
|
||||||
"gameOver.stats.total_score": "Total score",
|
"gameOver.stats.total_score": "Total score",
|
||||||
"gameOver.stats.upgrades_applied": "Upgrades applied",
|
"gameOver.stats.upgrades_applied": "Upgrades applied",
|
||||||
"gameOver.test_run": "This test run and its score are not being recorded",
|
"gameOver.test_run": "This test run and its score are not being recorded",
|
||||||
|
@ -46,7 +48,9 @@
|
||||||
"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.instructions": "All your perks will be erased except one that you can pick below. Each option comes with an additional hazard will appear on all levels going forward. ",
|
"loop.instructions": "All your perks will be erased except one that you can pick below. Each option comes with an additional hazard will appear on all levels going forward. ",
|
||||||
|
"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.",
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
"debuffs.interference.help": "Télékinésie et problème de yo-yo pendant {{lvl}}s toutes les 7 s.",
|
"debuffs.interference.help": "Télékinésie et problème de yo-yo pendant {{lvl}}s toutes les 7 s.",
|
||||||
"debuffs.more_bombs.help": "{{lvl}} briques remplacées par des bombes.",
|
"debuffs.more_bombs.help": "{{lvl}} briques remplacées par des bombes.",
|
||||||
"debuffs.negative_coins.help": "{{lvl}}/10000 pièces apparaissent maudites et clignotent en rouge. La partie est terminée si vous les attrapez.",
|
"debuffs.negative_coins.help": "{{lvl}}/10000 pièces apparaissent maudites et clignotent en rouge. La partie est terminée si vous les attrapez.",
|
||||||
|
"debuffs.sturdiness.help": "Toutes les briques résistent à +{{lvl}} chocs",
|
||||||
"gameOver.because_cursed_coin": "Jeu terminé",
|
"gameOver.because_cursed_coin": "Jeu terminé",
|
||||||
"gameOver.because_cursed_coin_intro": "Vous avez craché une pièce maudite (pièces rouge vif) et vous n'aviez pas de vie supplémentaire à revendre.",
|
"gameOver.because_cursed_coin_intro": "Vous avez craché une pièce maudite (pièces rouge vif) et vous n'aviez pas de vie supplémentaire à revendre.",
|
||||||
"gameOver.cumulative_total": "Votre score total cumulé est passé de {{startTs}} à {{endTs}}.",
|
"gameOver.cumulative_total": "Votre score total cumulé est passé de {{startTs}} à {{endTs}}.",
|
||||||
|
@ -26,6 +27,7 @@
|
||||||
"gameOver.stats.hit_rate": "Précision",
|
"gameOver.stats.hit_rate": "Précision",
|
||||||
"gameOver.stats.intro": "Vous trouverez ci-dessous les statistiques de cette partie comparées à vos {{count}} meilleures parties.",
|
"gameOver.stats.intro": "Vous trouverez ci-dessous les statistiques de cette partie comparées à vos {{count}} meilleures parties.",
|
||||||
"gameOver.stats.level_reached": "Niveau atteint",
|
"gameOver.stats.level_reached": "Niveau atteint",
|
||||||
|
"gameOver.stats.loops": "Boucles",
|
||||||
"gameOver.stats.total_score": "Score total",
|
"gameOver.stats.total_score": "Score total",
|
||||||
"gameOver.stats.upgrades_applied": "Mises à jour appliquées",
|
"gameOver.stats.upgrades_applied": "Mises à jour appliquées",
|
||||||
"gameOver.test_run": "Cette partie de test et son score ne sont pas enregistrés.",
|
"gameOver.test_run": "Cette partie de test et son score ne sont pas enregistrés.",
|
||||||
|
@ -46,7 +48,9 @@
|
||||||
"level_up.unlocked_level": " (Niveau)",
|
"level_up.unlocked_level": " (Niveau)",
|
||||||
"level_up.unlocked_perk": " (Amélioration)",
|
"level_up.unlocked_perk": " (Amélioration)",
|
||||||
"level_up.upgrade_perk_to_level": " niveau {{level}}",
|
"level_up.upgrade_perk_to_level": " niveau {{level}}",
|
||||||
|
"loop.converted_rerolls": "",
|
||||||
"loop.instructions": "Tous vos avantages seront supprimés, sauf un que vous pouvez choisir ci-dessous. Chaque option comporte un danger supplémentaire qui apparaîtra à tous les niveaux.",
|
"loop.instructions": "Tous vos avantages seront supprimés, sauf un que vous pouvez choisir ci-dessous. Chaque option comporte un danger supplémentaire qui apparaîtra à tous les niveaux.",
|
||||||
|
"loop.no_rerolls": "",
|
||||||
"loop.title": "Boucle de départ {{loop}}",
|
"loop.title": "Boucle de départ {{loop}}",
|
||||||
"main_menu.basic": "Graphismes simplifiés",
|
"main_menu.basic": "Graphismes simplifiés",
|
||||||
"main_menu.basic_help": "Meilleures performances.",
|
"main_menu.basic_help": "Meilleures performances.",
|
||||||
|
|
|
@ -45,5 +45,5 @@ export const allLevels = rawLevelsList
|
||||||
|
|
||||||
export const upgrades = rawUpgrades.map((u) => ({
|
export const upgrades = rawUpgrades.map((u) => ({
|
||||||
...u,
|
...u,
|
||||||
icon: icons["icon:" + u.id]
|
icon: icons["icon:" + u.id],
|
||||||
})) as Upgrade[];
|
})) as Upgrade[];
|
||||||
|
|
|
@ -11,10 +11,9 @@ import { dontOfferTooSoon, resetBalls } from "./gameStateMutators";
|
||||||
import { isOptionOn } from "./options";
|
import { isOptionOn } from "./options";
|
||||||
import { debuffs } from "./debuffs";
|
import { debuffs } from "./debuffs";
|
||||||
|
|
||||||
|
export function getRunLevels(totalScoreAtRunStart: number, params: RunParams) {
|
||||||
export function getRunLevels(totalScoreAtRunStart:number, params: RunParams){
|
const firstLevel = params?.level
|
||||||
const firstLevel =
|
? allLevels.filter((l) => l.name === params?.level)
|
||||||
params?.level ? allLevels.filter((l) => l.name === params?.level)
|
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const restInRandomOrder = allLevels
|
const restInRandomOrder = allLevels
|
||||||
|
@ -23,7 +22,7 @@ export function getRunLevels(totalScoreAtRunStart:number, params: RunParams){
|
||||||
.filter((l) => l.name !== params?.levelToAvoid)
|
.filter((l) => l.name !== params?.levelToAvoid)
|
||||||
.sort(() => Math.random() - 0.5);
|
.sort(() => Math.random() - 0.5);
|
||||||
|
|
||||||
return firstLevel.concat(
|
return firstLevel.concat(
|
||||||
restInRandomOrder.slice(0, 7 + 3).sort((a, b) => a.sortKey - b.sortKey),
|
restInRandomOrder.slice(0, 7 + 3).sort((a, b) => a.sortKey - b.sortKey),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -31,7 +30,7 @@ return firstLevel.concat(
|
||||||
export function newGameState(params: RunParams): GameState {
|
export function newGameState(params: RunParams): GameState {
|
||||||
const totalScoreAtRunStart = getTotalScore();
|
const totalScoreAtRunStart = getTotalScore();
|
||||||
|
|
||||||
const runLevels =getRunLevels(totalScoreAtRunStart, params)
|
const runLevels = getRunLevels(totalScoreAtRunStart, params);
|
||||||
|
|
||||||
const perks = { ...makeEmptyPerksMap(upgrades), ...(params?.perks || {}) };
|
const perks = { ...makeEmptyPerksMap(upgrades), ...(params?.perks || {}) };
|
||||||
|
|
||||||
|
@ -41,7 +40,7 @@ export function newGameState(params: RunParams): GameState {
|
||||||
currentLevel: 0,
|
currentLevel: 0,
|
||||||
upgradesOfferedFor: -1,
|
upgradesOfferedFor: -1,
|
||||||
perks,
|
perks,
|
||||||
bannedPerks:makeEmptyPerksMap(upgrades),
|
bannedPerks: makeEmptyPerksMap(upgrades),
|
||||||
debuffs: { ...emptyDebuffsMap(), ...(params?.debuffs || {}) },
|
debuffs: { ...emptyDebuffsMap(), ...(params?.debuffs || {}) },
|
||||||
puckWidth: 200,
|
puckWidth: 200,
|
||||||
baseSpeed: 12,
|
baseSpeed: 12,
|
||||||
|
@ -110,7 +109,8 @@ export function newGameState(params: RunParams): GameState {
|
||||||
autoCleanUses: 0,
|
autoCleanUses: 0,
|
||||||
...defaultSounds(),
|
...defaultSounds(),
|
||||||
rerolls: 0,
|
rerolls: 0,
|
||||||
loop:0
|
loop: 0,
|
||||||
|
baseCombo: 1,
|
||||||
};
|
};
|
||||||
resetBalls(gameState);
|
resetBalls(gameState);
|
||||||
|
|
||||||
|
|
275
src/premium.ts
275
src/premium.ts
|
@ -1,9 +1,9 @@
|
||||||
import {GameState} from "./types";
|
import { GameState } from "./types";
|
||||||
import {icons} from "./loadGameData";
|
import { icons } from "./loadGameData";
|
||||||
import {t} from "./i18n/i18n";
|
import { t } from "./i18n/i18n";
|
||||||
import {getSettingValue, setSettingValue} from "./settings";
|
import { getSettingValue, setSettingValue } from "./settings";
|
||||||
import {asyncAlert} from "./asyncAlert";
|
import { asyncAlert } from "./asyncAlert";
|
||||||
import {openMainMenu} from "./game";
|
import { openMainMenu } from "./game";
|
||||||
|
|
||||||
const publicKeyString = `-----BEGIN PUBLIC KEY-----
|
const publicKeyString = `-----BEGIN PUBLIC KEY-----
|
||||||
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyGQJgs6gxa0Fd86TuZ2q
|
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyGQJgs6gxa0Fd86TuZ2q
|
||||||
|
@ -21,49 +21,49 @@ dZpWPx3O+rZbRQb0gHcvN4+n2Y7fWAeC9mxVZtADqvVr/GTumMbLj7DdhWtt1Ogu
|
||||||
-----END PUBLIC KEY-----`;
|
-----END PUBLIC KEY-----`;
|
||||||
|
|
||||||
function pemToArrayBuffer(pem: string) {
|
function pemToArrayBuffer(pem: string) {
|
||||||
const b64 = pem
|
const b64 = pem
|
||||||
.replace(/-----BEGIN PUBLIC KEY-----/, "")
|
.replace(/-----BEGIN PUBLIC KEY-----/, "")
|
||||||
.replace(/-----END PUBLIC KEY-----/, "")
|
.replace(/-----END PUBLIC KEY-----/, "")
|
||||||
.replace(/\s+/g, "");
|
.replace(/\s+/g, "");
|
||||||
const binaryDerString = atob(b64);
|
const binaryDerString = atob(b64);
|
||||||
const binaryDer = new Uint8Array(binaryDerString.length);
|
const binaryDer = new Uint8Array(binaryDerString.length);
|
||||||
for (let i = 0; i < binaryDerString.length; i++) {
|
for (let i = 0; i < binaryDerString.length; i++) {
|
||||||
binaryDer[i] = binaryDerString.charCodeAt(i);
|
binaryDer[i] = binaryDerString.charCodeAt(i);
|
||||||
}
|
}
|
||||||
return binaryDer.buffer;
|
return binaryDer.buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getPriceId(key: string, pem: string) {
|
async function getPriceId(key: string, pem: string) {
|
||||||
// Split the key into its components
|
// Split the key into its components
|
||||||
const [priceId, timestamp, signature] = key.split(":");
|
const [priceId, timestamp, signature] = key.split(":");
|
||||||
const data = `${priceId}:${timestamp}`;
|
const data = `${priceId}:${timestamp}`;
|
||||||
|
|
||||||
const publicKeyBuffer = pemToArrayBuffer(pem);
|
const publicKeyBuffer = pemToArrayBuffer(pem);
|
||||||
|
|
||||||
const publicKey = await crypto.subtle.importKey(
|
const publicKey = await crypto.subtle.importKey(
|
||||||
"spki",
|
"spki",
|
||||||
publicKeyBuffer,
|
publicKeyBuffer,
|
||||||
{
|
{
|
||||||
name: "RSA-PSS",
|
name: "RSA-PSS",
|
||||||
hash: "SHA-256",
|
hash: "SHA-256",
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
["verify"],
|
["verify"],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Verify the signature using ECDSA
|
// Verify the signature using ECDSA
|
||||||
const isValid = await crypto.subtle.verify(
|
const isValid = await crypto.subtle.verify(
|
||||||
{
|
{
|
||||||
name: "RSA-PSS",
|
name: "RSA-PSS",
|
||||||
saltLength: 32,
|
saltLength: 32,
|
||||||
},
|
},
|
||||||
publicKey,
|
publicKey,
|
||||||
new Uint8Array(Array.from(atob(signature), (c) => c.charCodeAt(0))),
|
new Uint8Array(Array.from(atob(signature), (c) => c.charCodeAt(0))),
|
||||||
new TextEncoder().encode(data),
|
new TextEncoder().encode(data),
|
||||||
);
|
);
|
||||||
if (!isValid) throw new Error("Invalid key signature");
|
if (!isValid) throw new Error("Invalid key signature");
|
||||||
|
|
||||||
return priceId;
|
return priceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
let premium = false;
|
let premium = false;
|
||||||
|
@ -71,116 +71,113 @@ const gamePriceId = "price_1R6YaEGRf74lr2EkSo2GPvuO";
|
||||||
checkKey(getSettingValue("license", "")).then();
|
checkKey(getSettingValue("license", "")).then();
|
||||||
|
|
||||||
async function checkKey(key: string) {
|
async function checkKey(key: string) {
|
||||||
if (!key) return "No key";
|
if (!key) return "No key";
|
||||||
try {
|
try {
|
||||||
if (gamePriceId !== (await getPriceId(key, publicKeyString))) {
|
if (gamePriceId !== (await getPriceId(key, publicKeyString))) {
|
||||||
return "Wrong product";
|
return "Wrong product";
|
||||||
}
|
|
||||||
premium = true;
|
|
||||||
return "";
|
|
||||||
} catch (e) {
|
|
||||||
return "Could not upgrade : " + e.message;
|
|
||||||
}
|
}
|
||||||
|
premium = true;
|
||||||
|
return "";
|
||||||
|
} catch (e) {
|
||||||
|
return "Could not upgrade : " + e.message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isPremium() {
|
export function isPremium() {
|
||||||
return premium;
|
return premium;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function premiumMenuEntry(gameState: GameState) {
|
export function premiumMenuEntry(gameState: GameState) {
|
||||||
if (isPremium()) {
|
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log({args})
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
icon: icons["icon:premium"],
|
icon: icons["icon:premium_active"],
|
||||||
text,
|
text: t("premium.thanks"),
|
||||||
help,
|
help: t("premium.thanks_help"),
|
||||||
value: () => openPremiumMenu(""),
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log({ args });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
icon: icons["icon:premium"],
|
||||||
|
text,
|
||||||
|
help,
|
||||||
|
value: () => openPremiumMenu(""),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const isGooglePlayInstall =
|
const isGooglePlayInstall =
|
||||||
new URLSearchParams(location.search).get("source") ===
|
new URLSearchParams(location.search).get("source") === "com.android.vending";
|
||||||
"com.android.vending";
|
|
||||||
|
|
||||||
async function openPremiumMenu(text) {
|
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 || "");
|
||||||
const cb = await asyncAlert({
|
if (problem) {
|
||||||
title: t("premium.title"),
|
openPremiumMenu(problem).then();
|
||||||
content: [
|
} else {
|
||||||
text ||
|
setSettingValue("license", value);
|
||||||
(isGooglePlayInstall && t("premium.help_google")) ||
|
openMainMenu().then();
|
||||||
t("premium.help"),
|
}
|
||||||
{
|
},
|
||||||
text: t("premium.buy"),
|
},
|
||||||
disabled: isGooglePlayInstall,
|
{
|
||||||
help: isGooglePlayInstall
|
text: t("premium.back"),
|
||||||
? t("premium.buy_disabled_help")
|
help: t("premium.back_help"),
|
||||||
: t("premium.buy_help"),
|
value() {
|
||||||
value() {
|
openMainMenu().then();
|
||||||
window.open(
|
},
|
||||||
"https://licenses.lecaro.me/buy/price_1R6YaEGRf74lr2EkSo2GPvuO",
|
},
|
||||||
"_blank",
|
],
|
||||||
);
|
});
|
||||||
},
|
if (cb) cb();
|
||||||
},
|
|
||||||
{
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,9 +52,7 @@ export function drawMainCanvasOnSmallCanvas(gameState: GameState) {
|
||||||
|
|
||||||
recordCanvasCtx.textAlign = "left";
|
recordCanvasCtx.textAlign = "left";
|
||||||
recordCanvasCtx.fillText(
|
recordCanvasCtx.fillText(
|
||||||
"Level " +
|
"Level " + (gameState.currentLevel + 1) + "/" + max_levels(gameState),
|
||||||
(gameState.currentLevel + 1) +
|
|
||||||
"/" + max_levels(gameState),
|
|
||||||
12,
|
12,
|
||||||
12,
|
12,
|
||||||
);
|
);
|
||||||
|
|
1702
src/render.ts
1702
src/render.ts
File diff suppressed because it is too large
Load diff
12
src/types.d.ts
vendored
12
src/types.d.ts
vendored
|
@ -83,6 +83,7 @@ export type Coin = {
|
||||||
sa: number;
|
sa: number;
|
||||||
weight: number;
|
weight: number;
|
||||||
destroyed?: boolean;
|
destroyed?: boolean;
|
||||||
|
collidedLastFrame?: boolean;
|
||||||
coloredABrick?: boolean;
|
coloredABrick?: boolean;
|
||||||
};
|
};
|
||||||
export type Ball = {
|
export type Ball = {
|
||||||
|
@ -155,12 +156,12 @@ export type PerksMap = {
|
||||||
[k in PerkId]: number;
|
[k in PerkId]: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Debuff={
|
type Debuff = {
|
||||||
id: DebuffId;
|
id: DebuffId;
|
||||||
max:number;
|
max: number;
|
||||||
name:(lvl: number,banned:string)=>string;
|
name: (lvl: number, banned: string) => string;
|
||||||
help:(lvl: number,perk:string)=>string;
|
help: (lvl: number, perk: string) => string;
|
||||||
}
|
};
|
||||||
export type DebuffId = (typeof debuffs)[number]["id"];
|
export type DebuffId = (typeof debuffs)[number]["id"];
|
||||||
|
|
||||||
export type DebuffsMap = {
|
export type DebuffsMap = {
|
||||||
|
@ -287,6 +288,7 @@ export type GameState = {
|
||||||
};
|
};
|
||||||
rerolls: number;
|
rerolls: number;
|
||||||
loop: number;
|
loop: number;
|
||||||
|
baseCombo: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RunParams = {
|
export type RunParams = {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue