Looping mode

This commit is contained in:
Renan LE CARO 2025-03-28 19:40:59 +01:00
parent 3d5547e786
commit 5012076039
21 changed files with 2852 additions and 2696 deletions

View file

@ -14,16 +14,33 @@ Break colourful bricks, catch bouncing coins and select powerful upgrades !
- [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
- [jaceys] Counters for coins lost, misses, and boundary bounces, as well as a timer.
- people assume unbounded allows for wrap around
- coin magnet and viscosity : only one level ~2.5
- Boost Ascetism : give +2 or even +3 combo per brick destroyed
- wind : move coins based on puck movement not position
- show -N points in red when combo resets
- reach : this is too punishing now, maybe only reset if you hit the lowest populate row of the level, if it's not a full width row
- 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
- [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] A visual indication of whether a ball has hit a brick this serve
- [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/
# 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

View file

@ -11,8 +11,8 @@ android {
applicationId = "me.lecaro.breakout"
minSdk = 21
targetSdk = 34
versionCode = 29050375
versionName = "29050375"
versionCode = 29053110
versionName = "29053110"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true

File diff suppressed because one or more lines are too long

102
dist/index.html vendored

File diff suppressed because one or more lines are too long

View file

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

View file

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

View file

@ -1 +1 @@
"29050375"
"29053110"

View file

@ -1,11 +1,11 @@
import { t } from "./i18n/i18n";
import {Debuff} from "./types";
import { Debuff } from "./types";
export const debuffs = [
{
id: "negative_coins",
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 }),
},
{
@ -17,8 +17,10 @@ export const debuffs = [
{
id: "banned",
max: 50,
name: (lvl: number,banned:string) => t("debuffs.banned.description",{lvl,banned}),
help: (lvl: number,perk:string) => t("debuffs.banned.help", { lvl,perk }),
name: (lvl: number, banned: string) =>
t("debuffs.banned.description", { lvl, banned }),
help: (lvl: number, perk: string) =>
t("debuffs.banned.help", { lvl, perk }),
},
{
id: "interference",
@ -30,8 +32,14 @@ export const debuffs = [
{
id: "fragility",
max: 5,
name: (lvl: number) => t("debuffs.fragility.help", { percent:lvl*20 }),
help: (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 }),
},
{
id: "sturdiness",
max: 5,
name: (lvl: number) => t("debuffs.sturdiness.help", { lvl }),
help: (lvl: number) => t("debuffs.sturdiness.help", { lvl }),
},
] as const as Debuff[];

View file

@ -14,7 +14,8 @@ import {
import { getAudioContext, playPendingSounds } from "./sounds";
import {
bannedUpgradesHTMl,
currentLevelInfo, debuffsHTMl,
currentLevelInfo,
debuffsHTMl,
getRowColIndex,
levelsListHTMl,
max_levels,
@ -447,25 +448,24 @@ document.addEventListener("visibilitychange", () => {
async function openScorePanel() {
pause(true);
const cb = await asyncAlert({
title:
gameState.loop ?
t("score_panel.title_looped", {
loop:gameState.loop,
title: gameState.loop
? t("score_panel.title_looped", {
loop: gameState.loop,
score: gameState.score,
level: gameState.currentLevel + 1,
max: max_levels(gameState),
}):
t("score_panel.title", {
})
: t("score_panel.title", {
score: gameState.score,
level: gameState.currentLevel + 1,
max: max_levels(gameState),
}),
content: [
gameState.isCreativeModeRun ? `<p>${t("score_panel.test_run")}</p>` : "",
pickedUpgradesHTMl(gameState),
levelsListHTMl(gameState),
debuffsHTMl(gameState),
gameState.isCreativeModeRun ? `<p>${t("score_panel.test_run")}</p>` : "",
pickedUpgradesHTMl(gameState),
levelsListHTMl(gameState),
debuffsHTMl(gameState),
],
allowClose: true,
});
@ -1013,22 +1013,23 @@ restart(
// // unbounded: 1,
// // pierce_color: 1,
// pierce: 1,
streak_shots:1,
// streak_shots: 1,
// multiball: 6,
// base_combo: 7,
// telekinesis: 2,
// yoyo: 2,
pierce:10,
base_combo: 7,
telekinesis: 2,
yoyo: 2,
pierce: 10,
// metamorphosis: 1,
// implosions: 1,
// sturdy_bricks:5
extra_life:3
coin_magnet:2,
extra_life: 3,
},
debuffs:{
debuffs: {
// fragility:3
negative_coins:1
// interference:20,
}
negative_coins: 100,
// interference:20,
},
}) ||
{},
);

View file

@ -136,7 +136,7 @@ export function gameOver(title: string, intro: string) {
],
}).then(() =>
restart({
levelToAvoid: currentLevelInfo(gameState).name
levelToAvoid: currentLevelInfo(gameState).name,
}),
);
}
@ -271,6 +271,7 @@ export function getHistograms() {
(r) => r.max_combo,
"",
);
runStats += makeHistogram(t("gameOver.stats.loops"), (r) => r.loops, "");
if (runStats) {
runStats =

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
import { Ball, GameState, PerkId, PerksMap } from "./types";
import { icons, upgrades } from "./loadGameData";
import { t } from "./i18n/i18n";
import {debuffs} from "./debuffs";
import { debuffs } from "./debuffs";
export function getMajorityValue(arr: string[]): string {
const count: { [k: string]: number } = {};
@ -55,7 +55,6 @@ export function getPossibleUpgrades(gameState: GameState) {
}
export function max_levels(gameState: GameState) {
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>`;
}
export function debuffsHTMl(gameState: GameState):string {
const banned = upgrades.filter(u=>gameState.bannedPerks[u.id]).map(u=>u.name).join(', ')
let list = debuffs.filter(d=>gameState.debuffs[d.id]).map(d=>d.name(gameState.debuffs[d.id], banned)).join(' ');
export function debuffsHTMl(gameState: GameState): string {
const banned = upgrades
.filter((u) => gameState.bannedPerks[u.id])
.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 "";
return `<p>${t("score_panel.bebuffs_list")} ${list}</p>`;

View file

@ -202,6 +202,26 @@
</concept_node>
</children>
</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>
</folder_node>
<folder_node>
@ -470,6 +490,21 @@
</translation>
</translations>
</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>
<name>total_score</name>
<description/>
@ -787,6 +822,21 @@
<folder_node>
<name>loop</name>
<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>
<name>instructions</name>
<description/>
@ -802,6 +852,21 @@
</translation>
</translations>
</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>
<name>title</name>
<description/>

View file

@ -9,6 +9,7 @@
"debuffs.interference.help": "Telekinesis and yo-yo glitch for {{lvl}}s every 7s.",
"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.sturdiness.help": "All bricks have +{{lvl}} HP",
"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.cumulative_total": "Your total cumulative score went from {{startTs}} to {{endTs}}.",
@ -26,6 +27,7 @@
"gameOver.stats.hit_rate": "Hit rate",
"gameOver.stats.intro": "Find below your run statistics compared to your {{count}} best runs.",
"gameOver.stats.level_reached": "Level reached",
"gameOver.stats.loops": "Loops",
"gameOver.stats.total_score": "Total score",
"gameOver.stats.upgrades_applied": "Upgrades applied",
"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_perk": " (Perk)",
"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.no_rerolls": "You didn't have any leftover re-rolls, so your base combo stayed the same. ",
"loop.title": "Starting loop {{loop}}",
"main_menu.basic": "Basic graphics",
"main_menu.basic_help": "Better performance.",

View file

@ -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.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.sturdiness.help": "Toutes les briques résistent à +{{lvl}} chocs",
"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.cumulative_total": "Votre score total cumulé est passé de {{startTs}} à {{endTs}}.",
@ -26,6 +27,7 @@
"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.level_reached": "Niveau atteint",
"gameOver.stats.loops": "Boucles",
"gameOver.stats.total_score": "Score total",
"gameOver.stats.upgrades_applied": "Mises à jour appliquées",
"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_perk": " (Amélioration)",
"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.no_rerolls": "",
"loop.title": "Boucle de départ {{loop}}",
"main_menu.basic": "Graphismes simplifiés",
"main_menu.basic_help": "Meilleures performances.",

View file

@ -45,5 +45,5 @@ export const allLevels = rawLevelsList
export const upgrades = rawUpgrades.map((u) => ({
...u,
icon: icons["icon:" + u.id]
icon: icons["icon:" + u.id],
})) as Upgrade[];

View file

@ -11,10 +11,9 @@ import { dontOfferTooSoon, resetBalls } from "./gameStateMutators";
import { isOptionOn } from "./options";
import { debuffs } from "./debuffs";
export function getRunLevels(totalScoreAtRunStart:number, params: RunParams){
const firstLevel =
params?.level ? allLevels.filter((l) => l.name === params?.level)
export function getRunLevels(totalScoreAtRunStart: number, params: RunParams) {
const firstLevel = params?.level
? allLevels.filter((l) => l.name === params?.level)
: [];
const restInRandomOrder = allLevels
@ -23,7 +22,7 @@ export function getRunLevels(totalScoreAtRunStart:number, params: RunParams){
.filter((l) => l.name !== params?.levelToAvoid)
.sort(() => Math.random() - 0.5);
return firstLevel.concat(
return firstLevel.concat(
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 {
const totalScoreAtRunStart = getTotalScore();
const runLevels =getRunLevels(totalScoreAtRunStart, params)
const runLevels = getRunLevels(totalScoreAtRunStart, params);
const perks = { ...makeEmptyPerksMap(upgrades), ...(params?.perks || {}) };
@ -41,7 +40,7 @@ export function newGameState(params: RunParams): GameState {
currentLevel: 0,
upgradesOfferedFor: -1,
perks,
bannedPerks:makeEmptyPerksMap(upgrades),
bannedPerks: makeEmptyPerksMap(upgrades),
debuffs: { ...emptyDebuffsMap(), ...(params?.debuffs || {}) },
puckWidth: 200,
baseSpeed: 12,
@ -110,7 +109,8 @@ export function newGameState(params: RunParams): GameState {
autoCleanUses: 0,
...defaultSounds(),
rerolls: 0,
loop:0
loop: 0,
baseCombo: 1,
};
resetBalls(gameState);

View file

@ -1,9 +1,9 @@
import {GameState} from "./types";
import {icons} from "./loadGameData";
import {t} from "./i18n/i18n";
import {getSettingValue, setSettingValue} from "./settings";
import {asyncAlert} from "./asyncAlert";
import {openMainMenu} from "./game";
import { GameState } from "./types";
import { icons } from "./loadGameData";
import { t } from "./i18n/i18n";
import { getSettingValue, setSettingValue } from "./settings";
import { asyncAlert } from "./asyncAlert";
import { openMainMenu } from "./game";
const publicKeyString = `-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyGQJgs6gxa0Fd86TuZ2q
@ -21,49 +21,49 @@ dZpWPx3O+rZbRQb0gHcvN4+n2Y7fWAeC9mxVZtADqvVr/GTumMbLj7DdhWtt1Ogu
-----END PUBLIC KEY-----`;
function pemToArrayBuffer(pem: string) {
const b64 = pem
.replace(/-----BEGIN PUBLIC KEY-----/, "")
.replace(/-----END PUBLIC KEY-----/, "")
.replace(/\s+/g, "");
const binaryDerString = atob(b64);
const binaryDer = new Uint8Array(binaryDerString.length);
for (let i = 0; i < binaryDerString.length; i++) {
binaryDer[i] = binaryDerString.charCodeAt(i);
}
return binaryDer.buffer;
const b64 = pem
.replace(/-----BEGIN PUBLIC KEY-----/, "")
.replace(/-----END PUBLIC KEY-----/, "")
.replace(/\s+/g, "");
const binaryDerString = atob(b64);
const binaryDer = new Uint8Array(binaryDerString.length);
for (let i = 0; i < binaryDerString.length; i++) {
binaryDer[i] = binaryDerString.charCodeAt(i);
}
return binaryDer.buffer;
}
async function getPriceId(key: string, pem: string) {
// Split the key into its components
const [priceId, timestamp, signature] = key.split(":");
const data = `${priceId}:${timestamp}`;
// Split the key into its components
const [priceId, timestamp, signature] = key.split(":");
const data = `${priceId}:${timestamp}`;
const publicKeyBuffer = pemToArrayBuffer(pem);
const publicKeyBuffer = pemToArrayBuffer(pem);
const publicKey = await crypto.subtle.importKey(
"spki",
publicKeyBuffer,
{
name: "RSA-PSS",
hash: "SHA-256",
},
true,
["verify"],
);
const publicKey = await crypto.subtle.importKey(
"spki",
publicKeyBuffer,
{
name: "RSA-PSS",
hash: "SHA-256",
},
true,
["verify"],
);
// Verify the signature using ECDSA
const isValid = await crypto.subtle.verify(
{
name: "RSA-PSS",
saltLength: 32,
},
publicKey,
new Uint8Array(Array.from(atob(signature), (c) => c.charCodeAt(0))),
new TextEncoder().encode(data),
);
if (!isValid) throw new Error("Invalid key signature");
// Verify the signature using ECDSA
const isValid = await crypto.subtle.verify(
{
name: "RSA-PSS",
saltLength: 32,
},
publicKey,
new Uint8Array(Array.from(atob(signature), (c) => c.charCodeAt(0))),
new TextEncoder().encode(data),
);
if (!isValid) throw new Error("Invalid key signature");
return priceId;
return priceId;
}
let premium = false;
@ -71,116 +71,113 @@ const gamePriceId = "price_1R6YaEGRf74lr2EkSo2GPvuO";
checkKey(getSettingValue("license", "")).then();
async function checkKey(key: string) {
if (!key) return "No key";
try {
if (gamePriceId !== (await getPriceId(key, publicKeyString))) {
return "Wrong product";
}
premium = true;
return "";
} catch (e) {
return "Could not upgrade : " + e.message;
if (!key) return "No key";
try {
if (gamePriceId !== (await getPriceId(key, publicKeyString))) {
return "Wrong product";
}
premium = true;
return "";
} catch (e) {
return "Could not upgrade : " + e.message;
}
}
export function isPremium() {
return premium;
return premium;
}
export function premiumMenuEntry(gameState: GameState) {
if (isPremium()) {
return {
icon: icons["icon: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)
}
if (isPremium()) {
return {
icon: icons["icon:premium"],
text,
help,
value: () => openPremiumMenu(""),
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 {
icon: icons["icon:premium"],
text,
help,
value: () => openPremiumMenu(""),
};
}
const isGooglePlayInstall =
new URLSearchParams(location.search).get("source") ===
"com.android.vending";
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 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();
const problem = await checkKey(value || "");
if (problem) {
openPremiumMenu(problem).then();
} else {
setSettingValue("license", value);
openMainMenu().then();
}
},
},
{
text: t("premium.back"),
help: t("premium.back_help"),
value() {
openMainMenu().then();
},
},
],
});
if (cb) cb();
}

View file

@ -52,9 +52,7 @@ export function drawMainCanvasOnSmallCanvas(gameState: GameState) {
recordCanvasCtx.textAlign = "left";
recordCanvasCtx.fillText(
"Level " +
(gameState.currentLevel + 1) +
"/" + max_levels(gameState),
"Level " + (gameState.currentLevel + 1) + "/" + max_levels(gameState),
12,
12,
);

File diff suppressed because it is too large Load diff

12
src/types.d.ts vendored
View file

@ -83,6 +83,7 @@ export type Coin = {
sa: number;
weight: number;
destroyed?: boolean;
collidedLastFrame?: boolean;
coloredABrick?: boolean;
};
export type Ball = {
@ -155,12 +156,12 @@ export type PerksMap = {
[k in PerkId]: number;
};
type Debuff={
type Debuff = {
id: DebuffId;
max:number;
name:(lvl: number,banned:string)=>string;
help:(lvl: number,perk:string)=>string;
}
max: number;
name: (lvl: number, banned: string) => string;
help: (lvl: number, perk: string) => string;
};
export type DebuffId = (typeof debuffs)[number]["id"];
export type DebuffsMap = {
@ -287,6 +288,7 @@ export type GameState = {
};
rerolls: number;
loop: number;
baseCombo: number;
};
export type RunParams = {