This commit is contained in:
Renan LE CARO 2025-03-29 21:28:05 +01:00
parent a4e24fd397
commit 27a2cd686e
16 changed files with 3396 additions and 3332 deletions

View file

@ -11,8 +11,8 @@ android {
applicationId = "me.lecaro.breakout" applicationId = "me.lecaro.breakout"
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 34
versionCode = 29053158 versionCode = 29054664
versionName = "29053158" versionName = "29054664"
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

36
dist/index.html vendored
View file

@ -899,10 +899,10 @@ async function openScorePanel() {
gameState.isCreativeModeRun ? `<p>${(0, _i18N.t)("score_panel.test_run")}</p>` : "", gameState.isCreativeModeRun ? `<p>${(0, _i18N.t)("score_panel.test_run")}</p>` : "",
(0, _gameUtils.pickedUpgradesHTMl)(gameState), (0, _gameUtils.pickedUpgradesHTMl)(gameState),
(0, _gameUtils.levelsListHTMl)(gameState), (0, _gameUtils.levelsListHTMl)(gameState),
gameState.rerolls ? (0, _i18N.t)('score_panel.rerolls_count', { gameState.rerolls ? (0, _i18N.t)("score_panel.rerolls_count", {
rerolls: gameState.rerolls rerolls: gameState.rerolls
}) : '', }) : "",
banned && (0, _i18N.t)('score_panel.banned', { banned && (0, _i18N.t)("score_panel.banned", {
banned banned
}) })
], ],
@ -1293,7 +1293,7 @@ function setKeyPressed(key, on) {
} }
document.addEventListener("keydown", (e)=>{ document.addEventListener("keydown", (e)=>{
if (e.key.toLowerCase() === "f" && !e.ctrlKey && !e.metaKey) { if (e.key.toLowerCase() === "f" && !e.ctrlKey && !e.metaKey) {
(0, _options.toggleOption)('fullscreen'); (0, _options.toggleOption)("fullscreen");
applyFullScreenChoice(); applyFullScreenChoice();
} else if (e.key in pressed) setKeyPressed(e.key, 1); } else if (e.key in pressed) setKeyPressed(e.key, 1);
if (e.key === " " && !(0, _asyncAlert.alertsOpen)) { if (e.key === " " && !(0, _asyncAlert.alertsOpen)) {
@ -1385,7 +1385,7 @@ const upgrades = (0, _upgrades.rawUpgrades).map((u)=>({
})); }));
},{"./data/palette.json":"ktRBU","./data/levels.json":"8JSUc","./data/version.json":"iyP6E","./upgrades":"1u3Dx","./getLevelBackground":"7OIPf","./levelIcon":"6rQoT","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"iyP6E":[function(require,module,exports,__globalThis) { },{"./data/palette.json":"ktRBU","./data/levels.json":"8JSUc","./data/version.json":"iyP6E","./upgrades":"1u3Dx","./getLevelBackground":"7OIPf","./levelIcon":"6rQoT","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"iyP6E":[function(require,module,exports,__globalThis) {
module.exports = JSON.parse("\"29053158\""); module.exports = JSON.parse("\"29054664\"");
},{}],"1u3Dx":[function(require,module,exports,__globalThis) { },{}],"1u3Dx":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js"); var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
@ -2901,7 +2901,7 @@ async function gotoNextLoop(gameState) {
content: [ content: [
(0, _i18N.t)("loop.instructions"), (0, _i18N.t)("loop.instructions"),
comboText, comboText,
...userPerks.filter((u)=>u.id !== 'instant_upgrade').map((u)=>{ ...userPerks.filter((u)=>u.id !== "instant_upgrade").map((u)=>{
return { return {
text: u.name + (0, _i18N.t)("level_up.upgrade_perk_to_level", { text: u.name + (0, _i18N.t)("level_up.upgrade_perk_to_level", {
level: gameState.perks[u.id] + 1 level: gameState.perks[u.id] + 1
@ -3538,24 +3538,24 @@ function render(gameState) {
else menuLabel.innerText = (0, _i18N.t)("play.menu_label"); else menuLabel.innerText = (0, _i18N.t)("play.menu_label");
const catchRate = gameState.levelSpawnedCoins ? (gameState.levelSpawnedCoins - gameState.levelLostCoins) / gameState.levelSpawnedCoins : 1; const catchRate = gameState.levelSpawnedCoins ? (gameState.levelSpawnedCoins - gameState.levelLostCoins) / gameState.levelSpawnedCoins : 1;
scoreDisplay.innerHTML = ((0, _options.isOptionOn)("show_fps") ? ` scoreDisplay.innerHTML = ((0, _options.isOptionOn)("show_fps") ? `
<span class="${Math.abs((0, _game.lastMeasuredFPS) - 60) < 2 && ' ' || Math.abs((0, _game.lastMeasuredFPS) - 60) < 10 && 'good' || 'bad'}"> <span class="${Math.abs((0, _game.lastMeasuredFPS) - 60) < 2 && " " || Math.abs((0, _game.lastMeasuredFPS) - 60) < 10 && "good" || "bad"}">
${0, _game.lastMeasuredFPS} FPS ${0, _game.lastMeasuredFPS} FPS
</span><span> / </span> </span><span> / </span>
` : '') + ((0, _options.isOptionOn)('show_stats') ? ` ` : "") + ((0, _options.isOptionOn)("show_stats") ? `
<span class="${catchRate == 1 && 'great' || catchRate > 0.9 && 'good' || ''}"> <span class="${catchRate == 1 && "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' || ''}"> <span class="${gameState.levelWallBounces == 0 && "great" || gameState.levelWallBounces < 5 && "good" || ""}">
${gameState.levelWallBounces} B ${gameState.levelWallBounces} B
</span><span> / </span> </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.levelMisses == 0 && "great" || gameState.levelMisses <= 3 && "good" || ""}">
${gameState.levelMisses} M ${gameState.levelMisses} M
</span><span> / </span> </span><span> / </span>
` : '') + `$${gameState.score}`; ` : "") + `$${gameState.score}`;
scoreDisplay.className = gameState.lastScoreIncrease > gameState.levelTime - 500 ? "active" : ""; scoreDisplay.className = gameState.lastScoreIncrease > gameState.levelTime - 500 ? "active" : "";
// Clear // Clear
if (!(0, _options.isOptionOn)("basic") && !level.color && level.svg) { if (!(0, _options.isOptionOn)("basic") && !level.color && level.svg) {
@ -3602,14 +3602,14 @@ function render(gameState) {
bgctx.fillStyle = level.color || "#000"; bgctx.fillStyle = level.color || "#000";
bgctx.fillRect(0, 0, gameState.canvasWidth, gameState.canvasHeight); bgctx.fillRect(0, 0, gameState.canvasWidth, gameState.canvasHeight);
if (gameState.perks.clairvoyant >= 3) { if (gameState.perks.clairvoyant >= 3) {
const pageSource = document.body.innerHTML.replace(/\s+/gi, ''); const pageSource = document.body.innerHTML.replace(/\s+/gi, "");
const lineWidth = Math.ceil(gameState.canvasWidth / 15); const lineWidth = Math.ceil(gameState.canvasWidth / 15);
const lines = Math.ceil(gameState.canvasHeight / 20); const lines = Math.ceil(gameState.canvasHeight / 20);
const chars = lineWidth * lines; const chars = lineWidth * lines;
let start = Math.ceil(Math.random() * (pageSource.length - chars)); let start = Math.ceil(Math.random() * (pageSource.length - chars));
for(let i = 0; i < lines; i++){ for(let i = 0; i < lines; i++){
bgctx.fillStyle = 'white'; bgctx.fillStyle = "white";
bgctx.font = '20px Courier'; bgctx.font = "20px Courier";
bgctx.fillText(pageSource.slice(start + i * lineWidth, start + (i + 1) * lineWidth), 0, i * 20, gameState.canvasWidth); bgctx.fillText(pageSource.slice(start + i * lineWidth, start + (i + 1) * lineWidth), 0, i * 20, gameState.canvasWidth);
} }
} else { } else {
@ -3654,7 +3654,7 @@ function render(gameState) {
ctx.globalCompositeOperation = "source-over"; ctx.globalCompositeOperation = "source-over";
// ctx.globalCompositeOperation = // ctx.globalCompositeOperation =
// coin.color === "gold" || level.color ? "source-over" : "screen"; // coin.color === "gold" || level.color ? "source-over" : "screen";
drawCoin(ctx, coin.color, coin.size, coin.x, coin.y, hasCombo && gameState.perks.asceticism && "red" || coin.color === 'gold' && 'gold' || gameState.puckColor, coin.a); drawCoin(ctx, coin.color, coin.size, coin.x, coin.y, hasCombo && gameState.perks.asceticism && "red" || coin.color === "gold" && "gold" || gameState.puckColor, coin.a);
}); });
// Black shadow around balls // Black shadow around balls
if (!(0, _options.isOptionOn)("basic")) { if (!(0, _options.isOptionOn)("basic")) {
@ -3928,7 +3928,7 @@ function drawBrick(ctx, color, x, y, offset = 0, borderOnly) {
const brx = Math.ceil(x + (0, _game.gameState).brickWidth / 2) - 1; const brx = Math.ceil(x + (0, _game.gameState).brickWidth / 2) - 1;
const bry = Math.ceil(y + (0, _game.gameState).brickWidth / 2) - 1; const bry = Math.ceil(y + (0, _game.gameState).brickWidth / 2) - 1;
const width = brx - tlx, height = bry - tly; const width = brx - tlx, height = bry - tly;
const key = "brick" + color + "_" + "_" + width + "_" + height + "_" + offset + '_' + borderOnly; const key = "brick" + color + "_" + "_" + width + "_" + height + "_" + offset + "_" + borderOnly;
if (!cachedGraphics[key]) { if (!cachedGraphics[key]) {
const can = document.createElement("canvas"); const can = document.createElement("canvas");
can.width = width; can.width = width;

View file

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

View file

@ -82,7 +82,7 @@ export async function asyncAlert<t>({
content content
?.filter((i) => i) ?.filter((i) => i)
.forEach((entry, index) => { .forEach((entry, index) => {
if(!entry) return; if (!entry) return;
if (typeof entry == "string") { if (typeof entry == "string") {
const p = document.createElement("div"); const p = document.createElement("div");
p.innerHTML = entry; p.innerHTML = entry;

View file

@ -1 +1 @@
"29053158" "29054664"

View file

@ -1,5 +1,6 @@
* { * {
font-family: Courier New, font-family:
Courier New,
Courier, Courier,
Lucida Sans Typewriter, Lucida Sans Typewriter,
Lucida Typewriter, Lucida Typewriter,

View file

@ -1,4 +1,4 @@
import {allLevels, appVersion, icons, upgrades} from "./loadGameData"; import { allLevels, appVersion, icons, upgrades } from "./loadGameData";
import { import {
Ball, Ball,
Coin, Coin,
@ -11,11 +11,17 @@ import {
TextFlash, TextFlash,
Upgrade, Upgrade,
} from "./types"; } from "./types";
import {getAudioContext, playPendingSounds} from "./sounds"; import { getAudioContext, playPendingSounds } from "./sounds";
import {currentLevelInfo, getRowColIndex, levelsListHTMl, max_levels, pickedUpgradesHTMl,} from "./game_utils"; import {
currentLevelInfo,
getRowColIndex,
levelsListHTMl,
max_levels,
pickedUpgradesHTMl,
} from "./game_utils";
import "./PWA/sw_loader"; import "./PWA/sw_loader";
import {getCurrentLang, t} from "./i18n/i18n"; import { getCurrentLang, t } from "./i18n/i18n";
import { import {
cycleMaxCoins, cycleMaxCoins,
cycleMaxParticles, cycleMaxParticles,
@ -34,13 +40,30 @@ import {
setLevel, setLevel,
setMousePos, setMousePos,
} from "./gameStateMutators"; } from "./gameStateMutators";
import {backgroundCanvas, ctx, gameCanvas, render, scoreDisplay,} from "./render"; import {
import {pauseRecording, recordOneFrame, resumeRecording, startRecordingGame,} from "./recording"; backgroundCanvas,
import {newGameState} from "./newGameState"; ctx,
import {alertsOpen, asyncAlert, AsyncAlertAction, closeModal, requiredAsyncAlert,} from "./asyncAlert"; gameCanvas,
import {isOptionOn, options, toggleOption} from "./options"; render,
import {hashCode} from "./getLevelBackground"; scoreDisplay,
import {premiumMenuEntry} from "./premium"; } from "./render";
import {
pauseRecording,
recordOneFrame,
resumeRecording,
startRecordingGame,
} from "./recording";
import { newGameState } from "./newGameState";
import {
alertsOpen,
asyncAlert,
AsyncAlertAction,
closeModal,
requiredAsyncAlert,
} from "./asyncAlert";
import { isOptionOn, options, toggleOption } from "./options";
import { hashCode } from "./getLevelBackground";
import { premiumMenuEntry } from "./premium";
export function play() { export function play() {
if (applyFullScreenChoice()) return; if (applyFullScreenChoice()) return;
@ -93,7 +116,7 @@ export const fitSize = () => {
past_width = gameState.gameZoneWidthRoundedUp, past_width = gameState.gameZoneWidthRoundedUp,
past_heigh = gameState.gameZoneHeight; past_heigh = gameState.gameZoneHeight;
const {width, height} = gameCanvas.getBoundingClientRect(); const { width, height } = gameCanvas.getBoundingClientRect();
gameState.canvasWidth = width; gameState.canvasWidth = width;
gameState.canvasHeight = height; gameState.canvasHeight = height;
gameCanvas.width = width; gameCanvas.width = width;
@ -155,19 +178,16 @@ window.addEventListener("fullscreenchange", fitSize);
setInterval(() => { setInterval(() => {
// Sometimes, the page changes size without triggering the event (when switching to fullscreen, closing debug panel...) // Sometimes, the page changes size without triggering the event (when switching to fullscreen, closing debug panel...)
const {width, height} = gameCanvas.getBoundingClientRect(); const { width, height } = gameCanvas.getBoundingClientRect();
if (width !== gameState.canvasWidth || height !== gameState.canvasHeight) if (width !== gameState.canvasWidth || height !== gameState.canvasHeight)
fitSize(); fitSize();
}, 1000); }, 1000);
export async function openUpgradesPicker(gameState: GameState) { export async function openUpgradesPicker(gameState: GameState) {
const catchRate = const catchRate =
(gameState.score - gameState.levelStartScore) / (gameState.score - gameState.levelStartScore) /
(gameState.levelSpawnedCoins || 1); (gameState.levelSpawnedCoins || 1);
let repeats = 1; let repeats = 1;
let timeGain = "", let timeGain = "",
@ -222,7 +242,7 @@ export async function openUpgradesPicker(gameState: GameState) {
if (gameState.rerolls) if (gameState.rerolls)
actions.push({ actions.push({
text: t("level_up.reroll", {count: gameState.rerolls}), text: t("level_up.reroll", { count: gameState.rerolls }),
help: t("level_up.reroll_help"), help: t("level_up.reroll_help"),
value: "reroll" as const, value: "reroll" as const,
icon: icons["icon:reroll"], icon: icons["icon:reroll"],
@ -389,7 +409,7 @@ let FPSCounter = 0;
export let lastMeasuredFPS = 60; export let lastMeasuredFPS = 60;
setInterval(() => { setInterval(() => {
lastMeasuredFPS = FPSCounter lastMeasuredFPS = FPSCounter;
FPSCounter = 0; FPSCounter = 0;
}, 1000); }, 1000);
@ -420,7 +440,6 @@ async function openScorePanel() {
.map((u) => u.name) .map((u) => u.name)
.join(", "); .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", {
@ -439,9 +458,10 @@ async function openScorePanel() {
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),
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}) : "",
banned && t("score_panel.banned", { banned }),
], ],
allowClose: true, allowClose: true,
}); });
@ -467,7 +487,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 });
}, },
}, },
{ {
@ -483,7 +503,7 @@ export async function openMainMenu() {
text: t("sandbox.title"), text: t("sandbox.title"),
help: help:
getTotalScore() < creativeModeThreshold getTotalScore() < creativeModeThreshold
? t("sandbox.unlocks_at", {score: creativeModeThreshold}) ? t("sandbox.unlocks_at", { score: creativeModeThreshold })
: t("sandbox.help"), : t("sandbox.help"),
disabled: getTotalScore() < creativeModeThreshold, disabled: getTotalScore() < creativeModeThreshold,
async value() { async value() {
@ -515,7 +535,7 @@ export async function openMainMenu() {
})) }))
) { ) {
if (choice === "start") { if (choice === "start") {
restart({perks: creativeModePerks}); restart({ perks: creativeModePerks });
break; break;
} else if (choice) { } else if (choice) {
creativeModePerks[choice.id] = creativeModePerks[choice.id] =
@ -539,7 +559,7 @@ export async function openMainMenu() {
const cb = await asyncAlert<() => void>({ const cb = await asyncAlert<() => void>({
title: t("main_menu.title"), title: t("main_menu.title"),
content: [...actions, t("main_menu.footer_html", {appVersion})], content: [...actions, t("main_menu.footer_html", { appVersion })],
allowClose: true, allowClose: true,
}); });
if (cb) { if (cb) {
@ -564,7 +584,7 @@ async function openSettingsMenu() {
value: () => { value: () => {
toggleOption(key); toggleOption(key);
fitSize(); fitSize();
applyFullScreenChoice() applyFullScreenChoice();
openSettingsMenu(); openSettingsMenu();
}, },
}); });
@ -706,7 +726,7 @@ async function openSettingsMenu() {
title: t("main_menu.save_file_loaded"), title: t("main_menu.save_file_loaded"),
content: [ content: [
t("main_menu.save_file_loaded_help"), t("main_menu.save_file_loaded_help"),
{text: t("main_menu.save_file_loaded_ok")}, { text: t("main_menu.save_file_loaded_ok") },
], ],
}); });
window.location.reload(); window.location.reload();
@ -716,7 +736,7 @@ async function openSettingsMenu() {
title: t("main_menu.save_file_error"), title: t("main_menu.save_file_error"),
content: [ content: [
e.message, e.message,
{text: t("main_menu.save_file_loaded_ok")}, { text: t("main_menu.save_file_loaded_ok") },
], ],
}); });
} }
@ -759,7 +779,7 @@ async function openSettingsMenu() {
}); });
actions.push({ actions.push({
text: t("main_menu.max_coins", {max: getCurrentMaxCoins()}), text: t("main_menu.max_coins", { max: getCurrentMaxCoins() }),
help: t("main_menu.max_coins_help"), help: t("main_menu.max_coins_help"),
async value() { async value() {
cycleMaxCoins(); cycleMaxCoins();
@ -767,7 +787,7 @@ async function openSettingsMenu() {
}, },
}); });
actions.push({ actions.push({
text: t("main_menu.max_particles", {max: getCurrentMaxParticles()}), text: t("main_menu.max_particles", { max: getCurrentMaxParticles() }),
help: t("main_menu.max_particles_help"), help: t("main_menu.max_particles_help"),
async value() { async value() {
cycleMaxParticles(); cycleMaxParticles();
@ -786,52 +806,48 @@ async function openSettingsMenu() {
} }
} }
function applyFullScreenChoice(): boolean { function applyFullScreenChoice(): boolean {
try { try {
if (!(document.fullscreenEnabled || document.webkitFullscreenEnabled)) { if (!(document.fullscreenEnabled || document.webkitFullscreenEnabled)) {
return false return false;
} }
if (document.fullscreenElement !== null && !isOptionOn("fullscreen")) { if (document.fullscreenElement !== null && !isOptionOn("fullscreen")) {
if (document.exitFullscreen) { if (document.exitFullscreen) {
document.exitFullscreen(); document.exitFullscreen();
return true return true;
} else if (document.webkitCancelFullScreen) { } else if (document.webkitCancelFullScreen) {
document.webkitCancelFullScreen(); document.webkitCancelFullScreen();
return true return true;
} }
} else if (isOptionOn("fullscreen") && !document.fullscreenElement) { } else if (isOptionOn("fullscreen") && !document.fullscreenElement) {
const docel = document.documentElement; const docel = document.documentElement;
if (docel.requestFullscreen) { if (docel.requestFullscreen) {
docel.requestFullscreen(); docel.requestFullscreen();
return true return true;
} else if (docel.webkitRequestFullscreen) { } else if (docel.webkitRequestFullscreen) {
docel.webkitRequestFullscreen(); docel.webkitRequestFullscreen();
return true return true;
} }
} }
} catch (e) { } catch (e) {
console.warn(e); console.warn(e);
} }
return false return false;
} }
async function openUnlocksList() { async function openUnlocksList() {
const ts = getTotalScore(); const ts = getTotalScore();
const upgradeActions = upgrades const upgradeActions = upgrades
.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: // help:
// ts >= threshold ? help(1) : t("unlocks.unlocks_at", { threshold }), // 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,
})) }));
const levelActions = allLevels const levelActions = allLevels
.sort((a, b) => a.threshold - b.threshold) .sort((a, b) => a.threshold - b.threshold)
@ -846,24 +862,24 @@ async function openUnlocksList() {
// }) // })
// : t("unlocks.unlocks_at", { threshold: l.threshold }), // : 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],
}; };
}) });
const percentUnlock = Math.round( const percentUnlock = Math.round(
([...upgradeActions, ...levelActions].filter((a) => !a.disabled).length / (upgradeActions.length + ([...upgradeActions, ...levelActions].filter((a) => !a.disabled).length /
levelActions.length)) * 100, (upgradeActions.length + levelActions.length)) *
100,
); );
const tryOn = await asyncAlert<RunParams>({ const tryOn = await asyncAlert<RunParams>({
title: t("unlocks.title", {percentUnlock}), title: t("unlocks.title", { percentUnlock }),
content: [ content: [
`<p>${t("unlocks.intro", {ts, highScore: gameState.highScore})} `<p>${t("unlocks.intro", { ts, highScore: gameState.highScore })}
${percentUnlock < 100 ? t("unlocks.greyed_out_help") : ""}</p> `, ${percentUnlock < 100 ? t("unlocks.greyed_out_help") : ""}</p> `,
...upgradeActions, ...upgradeActions,
t("unlocks.level"), t("unlocks.level"),
...levelActions, ...levelActions,
], ],
allowClose: true, allowClose: true,
actionsAsGrid: true, actionsAsGrid: true,
@ -894,7 +910,6 @@ export async function confirmRestart(gameState) {
}); });
} }
const pressed: { [k: string]: number } = { const pressed: { [k: string]: number } = {
ArrowLeft: 0, ArrowLeft: 0,
ArrowRight: 0, ArrowRight: 0,
@ -912,8 +927,8 @@ export function setKeyPressed(key: string, on: 0 | 1) {
document.addEventListener("keydown", (e) => { document.addEventListener("keydown", (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();
} else if (e.key in pressed) { } else if (e.key in pressed) {
setKeyPressed(e.key, 1); setKeyPressed(e.key, 1);
} }
@ -953,7 +968,7 @@ document.addEventListener("keyup", async (e) => {
openScorePanel().then(); openScorePanel().then();
} else if (e.key.toLowerCase() === "r" && !alertsOpen) { } else if (e.key.toLowerCase() === "r" && !alertsOpen) {
if (await confirmRestart(gameState)) { if (await confirmRestart(gameState)) {
restart({levelToAvoid: currentLevelInfo(gameState).name}); restart({ levelToAvoid: currentLevelInfo(gameState).name });
} }
} else { } else {
return; return;
@ -970,21 +985,19 @@ export function restart(params: RunParams) {
setLevel(gameState, 0); setLevel(gameState, 0);
} }
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: 4,
clairvoyant:3, clairvoyant: 3,
bigger_explosions:2, bigger_explosions: 2,
sapper:2, sapper: 2,
unbounded:1 unbounded: 1,
}, },
levelsPerLoop: 2, levelsPerLoop: 2,
}) || }) ||

View file

@ -28,19 +28,31 @@ import {
max_levels, max_levels,
shouldPierceByColor, shouldPierceByColor,
} from "./game_utils"; } from "./game_utils";
import {t} from "./i18n/i18n"; import { t } from "./i18n/i18n";
import {icons, upgrades} from "./loadGameData"; import { icons, upgrades } from "./loadGameData";
import {addToTotalScore, getCurrentMaxCoins, getCurrentMaxParticles,} from "./settings"; import {
import {background} from "./render"; addToTotalScore,
import {gameOver} from "./gameOver"; getCurrentMaxCoins,
import {brickIndex, fitSize, gameState, hasBrick, hitsSomething, openUpgradesPicker, pause,} from "./game"; getCurrentMaxParticles,
import {stopRecording} from "./recording"; } from "./settings";
import {isOptionOn} from "./options"; import { background } from "./render";
import {isPremium} from "./premium"; import { gameOver } from "./gameOver";
import {getRunLevels} from "./newGameState"; import {
import {requiredAsyncAlert} from "./asyncAlert"; brickIndex,
import {clamp, comboKeepingRate} from "./pure_functions"; fitSize,
gameState,
hasBrick,
hitsSomething,
openUpgradesPicker,
pause,
} from "./game";
import { stopRecording } from "./recording";
import { isOptionOn } from "./options";
import { isPremium } from "./premium";
import { getRunLevels } from "./newGameState";
import { requiredAsyncAlert } from "./asyncAlert";
import { clamp, comboKeepingRate } from "./pure_functions";
export function setMousePos(gameState: GameState, x: number) { export function setMousePos(gameState: GameState, x: number) {
gameState.puckPosition = x; gameState.puckPosition = x;
@ -119,18 +131,27 @@ export function normalizeGameState(gameState: GameState) {
gameState.perks.slow_down * 2, gameState.perks.slow_down * 2,
); );
gameState.puckWidth = Math.max(gameState.ballSize, gameState.puckWidth = Math.max(
gameState.ballSize,
(gameState.gameZoneWidth / 12) * (gameState.gameZoneWidth / 12) *
Math.min(12, 3 - gameState.perks.smaller_puck + gameState.perks.bigger_puck)); Math.min(
12,
3 - gameState.perks.smaller_puck + gameState.perks.bigger_puck,
),
);
const corner = gameState.levelTime ? gameState.perks.corner_shot : 0 const corner = gameState.levelTime ? gameState.perks.corner_shot : 0;
let minX = gameState.offsetXRoundedDown + gameState.puckWidth / 2 - gameState.puckWidth * corner let minX =
gameState.offsetXRoundedDown +
gameState.puckWidth / 2 -
gameState.puckWidth * corner;
let maxX = gameState.offsetXRoundedDown + let maxX =
gameState.offsetXRoundedDown +
gameState.gameZoneWidthRoundedUp - gameState.gameZoneWidthRoundedUp -
gameState.puckWidth / 2 + gameState.puckWidth * corner; gameState.puckWidth / 2 +
gameState.puckWidth * corner;
gameState.puckPosition = clamp(gameState.puckPosition, minX, maxX); gameState.puckPosition = clamp(gameState.puckPosition, minX, maxX);
@ -165,7 +186,7 @@ export function resetCombo(
if (prev > gameState.combo && gameState.perks.soft_reset) { if (prev > gameState.combo && gameState.perks.soft_reset) {
gameState.combo += Math.floor( gameState.combo += Math.floor(
(prev - gameState.combo) * comboKeepingRate(gameState.perks.soft_reset) (prev - gameState.combo) * comboKeepingRate(gameState.perks.soft_reset),
); );
} }
const lost = Math.max(0, prev - gameState.combo); const lost = Math.max(0, prev - gameState.combo);
@ -177,8 +198,15 @@ export function resetCombo(
); );
} }
if (typeof x !== "undefined" && typeof y !== "undefined") { if (typeof x !== "undefined" && typeof y !== "undefined") {
makeText(
makeText(gameState, x, y, "red", "-" + lost, 20, 500 + clamp(lost, 0, 500)); gameState,
x,
y,
"red",
"-" + lost,
20,
500 + clamp(lost, 0, 500),
);
} }
} }
return lost; return lost;
@ -255,11 +283,13 @@ export function explosionAt(
x: number, x: number,
y: number, y: number,
ball: Ball, ball: Ball,
extraSize: number = 0 extraSize: number = 0,
) { ) {
const size = 1 + gameState.perks.bigger_explosions + const size =
Math.max(0, gameState.perks.implosions - 1) + extraSize 1 +
; gameState.perks.bigger_explosions +
Math.max(0, gameState.perks.implosions - 1) +
extraSize;
schedulGameSound(gameState, "explode", ball.x, 1); schedulGameSound(gameState, "explode", ball.x, 1);
if (index !== -1) { if (index !== -1) {
const col = index % gameState.gridSize; const col = index % gameState.gridSize;
@ -292,21 +322,9 @@ export function explosionAt(
// makeLight(gameState, x, y, "white", gameState.brickWidth * 2, 150); // makeLight(gameState, x, y, "white", gameState.brickWidth * 2, 150);
if (gameState.perks.implosions) { if (gameState.perks.implosions) {
spawnImplosion( spawnImplosion(gameState, 7 * size, x, y, "white");
gameState,
7 * size,
x,
y,
"white",
);
} else { } else {
spawnExplosion( spawnExplosion(gameState, 7 * size, x, y, "white");
gameState,
7 * size,
x,
y,
"white",
);
} }
gameState.runStatistics.bricks_broken++; gameState.runStatistics.bricks_broken++;
@ -366,7 +384,7 @@ export function explodeBrick(
while (coinsToSpawn > 0) { while (coinsToSpawn > 0) {
const points = Math.min(pointsPerCoin, coinsToSpawn); const points = Math.min(pointsPerCoin, coinsToSpawn);
if (points < 0 || isNaN(points)) { if (points < 0 || isNaN(points)) {
console.error({points}); console.error({ points });
debugger; debugger;
} }
@ -467,13 +485,17 @@ export function explodeBrick(
spawnExplosion(gameState, 5 + Math.min(gameState.combo, 30), x, y, color); spawnExplosion(gameState, 5 + Math.min(gameState.combo, 30), x, y, color);
} }
if (gameState.perks.respawn && color !== "black" && !gameState.bricks[index]) { if (
gameState.perks.respawn &&
color !== "black" &&
!gameState.bricks[index]
) {
if (Math.random() < comboKeepingRate(gameState.perks.respawn)) { if (Math.random() < comboKeepingRate(gameState.perks.respawn)) {
append(gameState.respawns, b => { append(gameState.respawns, (b) => {
b.color = color b.color = color;
b.index = index b.index = index;
b.time = gameState.levelTime + 3 * 1000 / gameState.perks.respawn b.time = gameState.levelTime + (3 * 1000) / gameState.perks.respawn;
}) });
} }
} }
} }
@ -564,7 +586,7 @@ export async function gotoNextLoop(gameState: GameState) {
let comboText = ""; let comboText = "";
if (gameState.rerolls) { if (gameState.rerolls) {
comboText = t("loop.converted_rerolls", {n: gameState.rerolls}); comboText = t("loop.converted_rerolls", { n: gameState.rerolls });
gameState.baseCombo += gameState.rerolls; gameState.baseCombo += gameState.rerolls;
gameState.rerolls = 0; gameState.rerolls = 0;
} else { } else {
@ -574,12 +596,12 @@ export async function gotoNextLoop(gameState: GameState) {
const userPerks = upgrades.filter((u) => gameState.perks[u.id]); const userPerks = upgrades.filter((u) => gameState.perks[u.id]);
const keep = await requiredAsyncAlert<PerkId>({ const keep = await requiredAsyncAlert<PerkId>({
title: t("loop.title", {loop: gameState.loop}), title: t("loop.title", { loop: gameState.loop }),
content: [ content: [
t("loop.instructions"), t("loop.instructions"),
comboText, comboText,
...userPerks ...userPerks
.filter(u => u.id !== 'instant_upgrade') .filter((u) => u.id !== "instant_upgrade")
.map((u) => { .map((u) => {
return { return {
text: text:
@ -595,11 +617,11 @@ export async function gotoNextLoop(gameState: GameState) {
], ],
}); });
userPerks.forEach(u => { userPerks.forEach((u) => {
if (u.id !== keep) { if (u.id !== keep) {
gameState.bannedPerks[u.id] = 1 gameState.bannedPerks[u.id] = 1;
} }
}) });
Object.assign(gameState.perks, makeEmptyPerksMap(upgrades), { Object.assign(gameState.perks, makeEmptyPerksMap(upgrades), {
[keep]: gameState.perks[keep], [keep]: gameState.perks[keep],
@ -642,7 +664,8 @@ export async function setLevel(gameState: GameState, l: number) {
gameState.combo += Math.round( gameState.combo += Math.round(
Math.max( Math.max(
0, 0,
(finalCombo - gameState.combo) * comboKeepingRate(gameState.perks.shunt), (finalCombo - gameState.combo) *
comboKeepingRate(gameState.perks.shunt),
), ),
); );
} }
@ -675,8 +698,7 @@ function setBrick(gameState: GameState, index: number, color: string) {
gameState.bricks[index] = color || ""; gameState.bricks[index] = color || "";
gameState.brickHP[index] = gameState.brickHP[index] =
(color === "black" && 1) || (color === "black" && 1) ||
(color && (color && 1 + gameState.perks.sturdy_bricks) ||
1 + gameState.perks.sturdy_bricks) ||
0; 0;
} }
@ -792,7 +814,7 @@ export function attract(gameState: GameState, a: Ball, b: Ball, power: number) {
export function coinBrickHitCheck(gameState: GameState, coin: Coin) { export function coinBrickHitCheck(gameState: GameState, coin: Coin) {
// Make ball/coin bonce, and return bricks that were hit // Make ball/coin bonce, and return bricks that were hit
const radius = coin.size / 2; const radius = coin.size / 2;
const {x, y, previousX, previousY} = coin; const { x, y, previousX, previousY } = coin;
const vhit = hitsSomething(previousX, y, radius); const vhit = hitsSomething(previousX, y, radius);
const hhit = hitsSomething(x, previousY, radius); const hhit = hitsSomething(x, previousY, radius);
@ -805,11 +827,9 @@ export function coinBrickHitCheck(gameState: GameState, coin: Coin) {
if (gameState.perks.ghost_coins) { if (gameState.perks.ghost_coins) {
// slow down // slow down
if (typeof (vhit ?? hhit ?? chit) !== "undefined") { if (typeof (vhit ?? hhit ?? chit) !== "undefined") {
coin.vy *= 1 - 0.2 / gameState.perks.ghost_coins; coin.vy *= 1 - 0.2 / gameState.perks.ghost_coins;
coin.vx *= 1 - 0.2 / gameState.perks.ghost_coins; coin.vx *= 1 - 0.2 / gameState.perks.ghost_coins;
} }
} else { } else {
if (typeof vhit !== "undefined" || typeof chit !== "undefined") { if (typeof vhit !== "undefined" || typeof chit !== "undefined") {
coin.y = coin.previousY; coin.y = coin.previousY;
@ -934,7 +954,7 @@ export function gameStateTick(
gameState.autoCleanUses++; gameState.autoCleanUses++;
} }
const hasPendingBricks = liveCount(gameState.respawns) const hasPendingBricks = liveCount(gameState.respawns);
if (gameState.running && !remainingBricks && !hasPendingBricks) { if (gameState.running && !remainingBricks && !hasPendingBricks) {
if (!gameState.winAt) { if (!gameState.winAt) {
@ -960,7 +980,7 @@ export function gameStateTick(
} else { } else {
gameOver( gameOver(
t("gameOver.win.title"), t("gameOver.win.title"),
t("gameOver.win.summary", {score: gameState.score}), t("gameOver.win.summary", { score: gameState.score }),
); );
} }
} }
@ -997,10 +1017,8 @@ export function gameStateTick(
const ratio = const ratio =
1 - 1 -
(gameState.perks.viscosity * ((gameState.perks.viscosity * 0.03 + 0.005) * frames) /
0.03 + (1 + gameState.perks.etherealcoins);
0.005) *
frames / (1 + gameState.perks.etherealcoins);
coin.vy *= ratio; coin.vy *= ratio;
coin.vx *= ratio; coin.vx *= ratio;
@ -1018,7 +1036,8 @@ export function gameStateTick(
gameState.perks.helium > 0 && gameState.perks.helium > 0 &&
Math.abs(coin.x - gameState.puckPosition) * 2 > Math.abs(coin.x - gameState.puckPosition) * 2 >
gameState.puckWidth + coin.size; gameState.puckWidth + coin.size;
coin.vy += frames * coin.weight * 0.8 * (flip ? -gameState.perks.helium : 1); coin.vy +=
frames * coin.weight * 0.8 * (flip ? -gameState.perks.helium : 1);
if (flip && !isOptionOn("basic") && Math.random() < 0.1) { if (flip && !isOptionOn("basic") && Math.random() < 0.1) {
makeParticle( makeParticle(
gameState, gameState,
@ -1034,7 +1053,6 @@ export function gameStateTick(
} }
} }
const speed = (Math.abs(coin.vx) + Math.abs(coin.vy)) * 10; const speed = (Math.abs(coin.vx) + Math.abs(coin.vy)) * 10;
const hitBorder = bordersHitCheck(gameState, coin, coin.size / 2, frames); const hitBorder = bordersHitCheck(gameState, coin, coin.size / 2, frames);
@ -1047,12 +1065,11 @@ export function gameStateTick(
// a bit of margin to be nice , negative in case it's a negative coin // a bit of margin to be nice , negative in case it's a negative coin
gameState.puckHeight * (coin.points ? 1 : -1) gameState.puckHeight * (coin.points ? 1 : -1)
) { ) {
addToScore(gameState, coin); addToScore(gameState, coin);
destroy(gameState.coins, coinIndex); destroy(gameState.coins, coinIndex);
} else if (coin.y > gameState.canvasHeight + coinRadius) { } else if (coin.y > gameState.canvasHeight + coinRadius) {
gameState.levelLostCoins+=coin.points gameState.levelLostCoins += coin.points;
destroy(gameState.coins, coinIndex); destroy(gameState.coins, coinIndex);
if (gameState.perks.compound_interest) { if (gameState.perks.compound_interest) {
resetCombo(gameState, coin.x, coin.y); resetCombo(gameState, coin.x, coin.y);
@ -1060,12 +1077,11 @@ export function gameStateTick(
} else if ( } else if (
gameState.perks.unbounded && gameState.perks.unbounded &&
(coin.x < -gameState.gameZoneWidth / 2 || (coin.x < -gameState.gameZoneWidth / 2 ||
coin.x > gameState.canvasWidth + gameState.gameZoneWidth / 2 coin.x > gameState.canvasWidth + gameState.gameZoneWidth / 2 ||
|| coin.y < -gameState.gameZoneWidth coin.y < -gameState.gameZoneWidth)
)
) { ) {
// Out of bound on sides // Out of bound on sides
gameState.levelLostCoins+=coin.points gameState.levelLostCoins += coin.points;
destroy(gameState.coins, coinIndex); destroy(gameState.coins, coinIndex);
} }
@ -1140,7 +1156,14 @@ export function gameStateTick(
((Math.random() - 0.5) * limit) / 3; ((Math.random() - 0.5) * limit) / 3;
let index = brickIndex(x, y); let index = brickIndex(x, y);
explosionAt(gameState, index, x, y, a, Math.max(0, gameState.perks.shocks - 1)); explosionAt(
gameState,
index,
x,
y,
a,
Math.max(0, gameState.perks.shocks - 1),
);
} }
}), }),
); );
@ -1276,12 +1299,12 @@ export function gameStateTick(
// Respawn what's needed, show particles // Respawn what's needed, show particles
forEachLiveOne(gameState.respawns, (r, ri) => { forEachLiveOne(gameState.respawns, (r, ri) => {
if (gameState.bricks[r.index]) { if (gameState.bricks[r.index]) {
destroy(gameState.respawns, ri) destroy(gameState.respawns, ri);
} else if (gameState.levelTime > r.time) { } else if (gameState.levelTime > r.time) {
setBrick(gameState, r.index, r.color) setBrick(gameState, r.index, r.color);
destroy(gameState.respawns, ri) destroy(gameState.respawns, ri);
} else if (!isOptionOn("basic")) { } else if (!isOptionOn("basic")) {
const {index, color} = r; const { index, color } = r;
const vertical = Math.random() > 0.5; const vertical = Math.random() > 0.5;
const dx = Math.random() > 0.5 ? 1 : -1; const dx = Math.random() > 0.5 ? 1 : -1;
const dy = Math.random() > 0.5 ? 1 : -1; const dy = Math.random() > 0.5 ? 1 : -1;
@ -1298,8 +1321,7 @@ export function gameStateTick(
250, 250,
); );
} }
}) });
forEachLiveOne(gameState.particles, (p, pi) => { forEachLiveOne(gameState.particles, (p, pi) => {
if (gameState.levelTime > p.time + p.duration) { if (gameState.levelTime > p.time + p.duration) {
@ -1334,14 +1356,12 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
ball.vx += ball.vx +=
((gameState.puckPosition - ball.x) / 1000) * ((gameState.puckPosition - ball.x) / 1000) *
delta * delta *
gameState.perks.telekinesis gameState.perks.telekinesis;
} }
if (isYoyoActive(gameState, ball)) { if (isYoyoActive(gameState, ball)) {
speedLimitDampener += 3; speedLimitDampener += 3;
ball.vx += ball.vx +=
((gameState.puckPosition - ball.x) / 1000) * ((gameState.puckPosition - ball.x) / 1000) * delta * gameState.perks.yoyo;
delta *
gameState.perks.yoyo
} }
if ( if (
ball.vx * ball.vx + ball.vy * ball.vy < ball.vx * ball.vx + ball.vy * ball.vy <
@ -1390,7 +1410,6 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
); );
} }
const borderHitCode = bordersHitCheck( const borderHitCode = bordersHitCheck(
gameState, gameState,
ball, ball,
@ -1449,7 +1468,9 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
const angle = Math.atan2( const angle = Math.atan2(
-gameState.puckWidth / 2, -gameState.puckWidth / 2,
(ball.x - gameState.puckPosition) * (ball.x - gameState.puckPosition) *
(gameState.perks.concave_puck ? -1 / (1 + gameState.perks.concave_puck) : 1), (gameState.perks.concave_puck
? -1 / (1 + gameState.perks.concave_puck)
: 1),
); );
ball.vx = speed * Math.cos(angle); ball.vx = speed * Math.cos(angle);
ball.vy = speed * Math.sin(angle); ball.vy = speed * Math.sin(angle);
@ -1471,7 +1492,6 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
resetCombo(gameState, ball.x, ball.y); resetCombo(gameState, ball.x, ball.y);
} }
if (!ball.hitSinceBounce && gameState.bricks.find((i) => i)) { if (!ball.hitSinceBounce && gameState.bricks.find((i) => i)) {
gameState.runStatistics.misses++; gameState.runStatistics.misses++;
if (gameState.perks.forgiving) { if (gameState.perks.forgiving) {
@ -1505,28 +1525,27 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
(gameState.perks.unbounded && ball.x < -gameState.gameZoneWidth / 2) || (gameState.perks.unbounded && ball.x < -gameState.gameZoneWidth / 2) ||
ball.x > gameState.canvasWidth + gameState.gameZoneWidth / 2; ball.x > gameState.canvasWidth + gameState.gameZoneWidth / 2;
const lostInTheSky = (gameState.perks.unbounded > 1 && const lostInTheSky =
ball.y < -gameState.gameZoneWidth / 2 gameState.perks.unbounded > 1 && ball.y < -gameState.gameZoneWidth / 2;
)
if ( if (
gameState.running && gameState.running &&
(ball.y > gameState.gameZoneHeight + gameState.ballSize / 2 || lostOnSides (ball.y > gameState.gameZoneHeight + gameState.ballSize / 2 ||
|| lostInTheSky lostOnSides ||
) lostInTheSky)
) { ) {
ball.destroyed = true; ball.destroyed = true;
gameState.runStatistics.balls_lost++; gameState.runStatistics.balls_lost++;
if (!gameState.balls.find((b) => !b.destroyed)) { if (!gameState.balls.find((b) => !b.destroyed)) {
gameOver( gameOver(
t("gameOver.lost.title"), t("gameOver.lost.title"),
t("gameOver.lost.summary", {score: gameState.score}), t("gameOver.lost.summary", { score: gameState.score }),
); );
} }
} }
const radius = gameState.ballSize / 2; const radius = gameState.ballSize / 2;
// Make ball/coin bonce, and return bricks that were hit // Make ball/coin bonce, and return bricks that were hit
const {x, y, previousX, previousY} = ball; const { x, y, previousX, previousY } = ball;
const vhit = hitsSomething(previousX, y, radius); const vhit = hitsSomething(previousX, y, radius);
const hhit = hitsSomething(x, previousY, radius); const hhit = hitsSomething(x, previousY, radius);
@ -1638,7 +1657,7 @@ function justLostALife(gameState: GameState, ball: Ball, x: number, y: number) {
if (gameState.perks.extra_life < 0) { if (gameState.perks.extra_life < 0) {
gameState.perks.extra_life = 0; gameState.perks.extra_life = 0;
} else if (gameState.perks.sacrifice) { } else if (gameState.perks.sacrifice) {
gameState.combo *= gameState.perks.sacrifice gameState.combo *= gameState.perks.sacrifice;
gameState.bricks.forEach( gameState.bricks.forEach(
(color, index) => color && explodeBrick(gameState, index, ball, true), (color, index) => color && explodeBrick(gameState, index, ball, true),
); );
@ -1671,8 +1690,8 @@ function makeCoin(
color = "gold", color = "gold",
points = 1, points = 1,
) { ) {
let weight = 0.8 + Math.random() * 0.2 + Math.min(2, points * 0.01) let weight = 0.8 + Math.random() * 0.2 + Math.min(2, points * 0.01);
weight *= 5 / (5 + gameState.perks.etherealcoins) weight *= 5 / (5 + gameState.perks.etherealcoins);
append(gameState.coins, (p: Partial<Coin>) => { append(gameState.coins, (p: Partial<Coin>) => {
p.x = x; p.x = x;
@ -1690,7 +1709,7 @@ function makeCoin(
p.sa = Math.random() - 0.5; p.sa = Math.random() - 0.5;
p.points = points; p.points = points;
p.weight = weight; p.weight = weight;
p.metamorphosisPoints = gameState.perks.metamorphosis p.metamorphosisPoints = gameState.perks.metamorphosis;
}); });
} }
@ -1772,7 +1791,7 @@ export function append<T>(
makeItem(where.list[where.indexMin]); makeItem(where.list[where.indexMin]);
where.indexMin++; where.indexMin++;
} else { } else {
const p = {destroyed: false}; const p = { destroyed: false };
makeItem(p); makeItem(p);
where.list.push(p); where.list.push(p);
} }
@ -1791,16 +1810,16 @@ export function liveCount<T>(where: ReusableArray<T>) {
} }
export function empty<T>(where: ReusableArray<T>) { export function empty<T>(where: ReusableArray<T>) {
let destroyed=0 let destroyed = 0;
where.total = 0; where.total = 0;
where.indexMin = 0; where.indexMin = 0;
where.list.forEach((i) => { where.list.forEach((i) => {
if(!i.destroyed) { if (!i.destroyed) {
i.destroyed = true i.destroyed = true;
destroyed++ destroyed++;
} }
}); });
return destroyed return destroyed;
} }
export function forEachLiveOne<T>( export function forEachLiveOne<T>(

View file

@ -1,6 +1,6 @@
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";
export function getMajorityValue(arr: string[]): string { export function getMajorityValue(arr: string[]): string {
const count: { [k: string]: number } = {}; const count: { [k: string]: number } = {};
@ -14,10 +14,8 @@ export function sample<T>(arr: T[]): T {
return arr[Math.floor(arr.length * Math.random())]; return arr[Math.floor(arr.length * Math.random())];
} }
export function sampleN<T>(arr: T[],n:number): T[] { export function sampleN<T>(arr: T[], n: number): T[] {
return [...arr].sort(() => Math.random() - 0.5).slice(0, n);
return [...arr].sort(()=>Math.random()-0.5)
.slice(0,n)
} }
export function sumOfValues(obj: { [key: string]: number } | undefined | null) { export function sumOfValues(obj: { [key: string]: number } | undefined | null) {
@ -55,7 +53,10 @@ export function getRowColIndex(gameState: GameState, row: number, col: number) {
export function getPossibleUpgrades(gameState: GameState) { export function getPossibleUpgrades(gameState: GameState) {
return upgrades return upgrades
.filter((u) => gameState.totalScoreAtRunStart >= u.threshold || gameState.loop>0) .filter(
(u) =>
gameState.totalScoreAtRunStart >= u.threshold || gameState.loop > 0,
)
.filter((u) => !u?.requires || gameState.perks[u?.requires]); .filter((u) => !u?.requires || gameState.perks[u?.requires]);
} }
@ -186,4 +187,3 @@ export function countBricksBelow(gameState: GameState, index: number) {
} }
return count; return count;
} }

View file

@ -129,5 +129,3 @@ export function newGameState(params: RunParams): GameState {
} }
return gameState; return gameState;
} }

View file

@ -115,7 +115,6 @@ export function premiumMenuEntry(gameState: GameState) {
text = t("premium.per_hours", args); text = t("premium.per_hours", args);
help = t("premium.per_hours_help", args); help = t("premium.per_hours_help", args);
} }
} }
} catch (e) { } catch (e) {
console.warn(e); console.warn(e);

View file

@ -3,5 +3,5 @@ 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);
} }

View file

@ -1,4 +1,4 @@
import {baseCombo, forEachLiveOne, liveCount,} from "./gameStateMutators"; import { baseCombo, forEachLiveOne, liveCount } from "./gameStateMutators";
import { import {
brickCenterX, brickCenterX,
brickCenterY, brickCenterY,
@ -9,10 +9,10 @@ import {
isYoyoActive, isYoyoActive,
max_levels, max_levels,
} from "./game_utils"; } from "./game_utils";
import {colorString, GameState} from "./types"; import { colorString, GameState } from "./types";
import {t} from "./i18n/i18n"; import { t } from "./i18n/i18n";
import {gameState, lastMeasuredFPS} from "./game"; import { gameState, lastMeasuredFPS } from "./game";
import {isOptionOn} from "./options"; import { isOptionOn } from "./options";
export const gameCanvas = document.getElementById("game") as HTMLCanvasElement; export const gameCanvas = document.getElementById("game") as HTMLCanvasElement;
export const ctx = gameCanvas.getContext("2d", { export const ctx = gameCanvas.getContext("2d", {
@ -24,7 +24,7 @@ bombSVG.src =
btoa(`<svg width="144" height="144" viewBox="0 0 38.101 38.099" xmlns="http://www.w3.org/2000/svg"> btoa(`<svg width="144" height="144" viewBox="0 0 38.101 38.099" xmlns="http://www.w3.org/2000/svg">
<path d="m6.1528 26.516c-2.6992-3.4942-2.9332-8.281-.58305-11.981a10.454 10.454 0 017.3701-4.7582c1.962-.27726 4.1646.05953 5.8835.90027l.45013.22017.89782-.87417c.83748-.81464.91169-.87499 1.0992-.90271.40528-.058713.58876.03425 1.1971.6116l.55451.52679 1.0821-1.0821c1.1963-1.1963 1.383-1.3357 2.1039-1.5877.57898-.20223 1.5681-.19816 2.1691.00897 1.4613.50314 2.3673 1.7622 2.3567 3.2773-.0058.95654-.24464 1.5795-.90924 2.3746-.40936.48928-.55533.81057-.57898 1.2737-.02039.41018.1109.77714.42322 1.1792.30172.38816.3694.61323.2797.93044-.12803.45666-.56674.71598-1.0242.60507-.601-.14597-1.3031-1.3088-1.3969-2.3126-.09459-1.0161.19245-1.8682.92392-2.7432.42567-.50885.5643-.82851.5643-1.3031 0-.50151-.14026-.83177-.51211-1.2028-.50966-.50966-1.0968-.64829-1.781-.41996l-.37348.12477-2.1006 2.1006.52597.55696c.45421.48194.5325.58876.57898.78855.09622.41588.07502.45014-.88396 1.4548l-.87173.9125.26339.57979a10.193 10.193 0 01.9231 4.1001c.03996 2.046-.41996 3.8082-1.4442 5.537-.55044.928-1.0185 1.5013-1.8968 2.3241-.83503.78284-1.5526 1.2827-2.4904 1.7361-3.4266 1.657-7.4721 1.3422-10.549-.82035-.73473-.51782-1.7312-1.4621-2.2515-2.1357zm21.869-4.5584c-.0579-.19734-.05871-2.2662 0-2.4545.11906-.39142.57898-.63361 1.0038-.53005.23812.05708.54147.32455.6116.5382.06279.19163.06769 2.1805.0065 2.3811-.12558.40773-.61649.67602-1.0462.57164-.234-.05708-.51615-.30498-.57568-.50722m3.0417-2.6013c-.12313-.6222.37837-1.1049 1.0479-1.0079.18348.0261.25279.08399 1.0071.83911.75838.75838.81301.82362.84074 1.0112.10193.68499-.40365 1.1938-1.034 1.0405-.1949-.0473-.28786-.12558-1.0144-.85216-.7649-.76409-.80241-.81057-.84645-1.0316m.61323-3.0629a.85623.85623 0 01.59284-.99975c.28949-.09214 2.1814-.08318 2.3917.01141.38734.17369.6279.61078.53984.98181-.06035.25606-.35391.57327-.60181.64992-.25279.07747-2.2278.053-2.4097-.03017-.26013-.11906-.46318-.36125-.51374-.61323" fill="#fff" opacity="0.3"/> <path d="m6.1528 26.516c-2.6992-3.4942-2.9332-8.281-.58305-11.981a10.454 10.454 0 017.3701-4.7582c1.962-.27726 4.1646.05953 5.8835.90027l.45013.22017.89782-.87417c.83748-.81464.91169-.87499 1.0992-.90271.40528-.058713.58876.03425 1.1971.6116l.55451.52679 1.0821-1.0821c1.1963-1.1963 1.383-1.3357 2.1039-1.5877.57898-.20223 1.5681-.19816 2.1691.00897 1.4613.50314 2.3673 1.7622 2.3567 3.2773-.0058.95654-.24464 1.5795-.90924 2.3746-.40936.48928-.55533.81057-.57898 1.2737-.02039.41018.1109.77714.42322 1.1792.30172.38816.3694.61323.2797.93044-.12803.45666-.56674.71598-1.0242.60507-.601-.14597-1.3031-1.3088-1.3969-2.3126-.09459-1.0161.19245-1.8682.92392-2.7432.42567-.50885.5643-.82851.5643-1.3031 0-.50151-.14026-.83177-.51211-1.2028-.50966-.50966-1.0968-.64829-1.781-.41996l-.37348.12477-2.1006 2.1006.52597.55696c.45421.48194.5325.58876.57898.78855.09622.41588.07502.45014-.88396 1.4548l-.87173.9125.26339.57979a10.193 10.193 0 01.9231 4.1001c.03996 2.046-.41996 3.8082-1.4442 5.537-.55044.928-1.0185 1.5013-1.8968 2.3241-.83503.78284-1.5526 1.2827-2.4904 1.7361-3.4266 1.657-7.4721 1.3422-10.549-.82035-.73473-.51782-1.7312-1.4621-2.2515-2.1357zm21.869-4.5584c-.0579-.19734-.05871-2.2662 0-2.4545.11906-.39142.57898-.63361 1.0038-.53005.23812.05708.54147.32455.6116.5382.06279.19163.06769 2.1805.0065 2.3811-.12558.40773-.61649.67602-1.0462.57164-.234-.05708-.51615-.30498-.57568-.50722m3.0417-2.6013c-.12313-.6222.37837-1.1049 1.0479-1.0079.18348.0261.25279.08399 1.0071.83911.75838.75838.81301.82362.84074 1.0112.10193.68499-.40365 1.1938-1.034 1.0405-.1949-.0473-.28786-.12558-1.0144-.85216-.7649-.76409-.80241-.81057-.84645-1.0316m.61323-3.0629a.85623.85623 0 01.59284-.99975c.28949-.09214 2.1814-.08318 2.3917.01141.38734.17369.6279.61078.53984.98181-.06035.25606-.35391.57327-.60181.64992-.25279.07747-2.2278.053-2.4097-.03017-.26013-.11906-.46318-.36125-.51374-.61323" fill="#fff" opacity="0.3"/>
</svg>`); </svg>`);
bombSVG.onload = () => gameState.needsRender = true bombSVG.onload = () => (gameState.needsRender = true);
export const background = document.createElement("img"); export const background = document.createElement("img");
export const backgroundCanvas = document.createElement("canvas"); export const backgroundCanvas = document.createElement("canvas");
@ -33,7 +33,7 @@ export function render(gameState: GameState) {
const level = currentLevelInfo(gameState); const level = currentLevelInfo(gameState);
const hasCombo = gameState.combo > baseCombo(gameState); const hasCombo = gameState.combo > baseCombo(gameState);
const {width, height} = gameCanvas; const { width, height } = gameCanvas;
if (!width || !height) return; if (!width || !height) return;
if (gameState.currentLevel || gameState.levelTime) { if (gameState.currentLevel || gameState.levelTime) {
@ -51,34 +51,37 @@ export function render(gameState: GameState) {
menuLabel.innerText = t("play.menu_label"); menuLabel.innerText = t("play.menu_label");
} }
const catchRate = gameState.levelSpawnedCoins ? const catchRate = gameState.levelSpawnedCoins
(gameState.levelSpawnedCoins - gameState.levelLostCoins)/gameState.levelSpawnedCoins :1 ? (gameState.levelSpawnedCoins - gameState.levelLostCoins) /
gameState.levelSpawnedCoins
: 1;
scoreDisplay.innerHTML= scoreDisplay.innerHTML =
(isOptionOn("show_fps") ? ` (isOptionOn("show_fps")
<span class="${(Math.abs(lastMeasuredFPS-60)<2 && ' ') || (Math.abs(lastMeasuredFPS-60)<10 && 'good')||'bad'}"> ? `
<span class="${(Math.abs(lastMeasuredFPS - 60) < 2 && " ") || (Math.abs(lastMeasuredFPS - 60) < 10 && "good") || "bad"}">
${lastMeasuredFPS} FPS ${lastMeasuredFPS} FPS
</span><span> / </span> </span><span> / </span>
`:'')+ `
: "") +
(isOptionOn("show_stats")
? `
(isOptionOn('show_stats') ? ` <span class="${(catchRate == 1 && "great") || (catchRate > 0.9 && "good") || ""}">
<span class="${(catchRate==1 && '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')||''}"> <span class="${(gameState.levelWallBounces == 0 && "great") || (gameState.levelWallBounces < 5 && "good") || ""}">
${gameState.levelWallBounces} B ${gameState.levelWallBounces} B
</span><span> / </span> </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.levelMisses == 0 && "great") || (gameState.levelMisses <= 3 && "good") || ""}">
${gameState.levelMisses} M ${gameState.levelMisses} M
</span><span> / </span> </span><span> / </span>
`: '' )+ `$${gameState.score}`; `
: "") +
`$${gameState.score}`;
scoreDisplay.className = scoreDisplay.className =
gameState.lastScoreIncrease > gameState.levelTime - 500 ? "active" : ""; gameState.lastScoreIncrease > gameState.levelTime - 500 ? "active" : "";
@ -122,7 +125,7 @@ export function render(gameState: GameState) {
ctx.globalAlpha = 1; ctx.globalAlpha = 1;
forEachLiveOne(gameState.particles, (flash) => { forEachLiveOne(gameState.particles, (flash) => {
const {x, y, time, color, size, duration} = flash; const { x, y, time, color, size, duration } = flash;
const elapsed = gameState.levelTime - time; const elapsed = gameState.levelTime - time;
ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2); ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2);
drawFuzzyBall(ctx, color, size * 3, x, y); drawFuzzyBall(ctx, color, size * 3, x, y);
@ -147,24 +150,25 @@ export function render(gameState: GameState) {
bgctx.fillStyle = level.color || "#000"; bgctx.fillStyle = level.color || "#000";
bgctx.fillRect(0, 0, gameState.canvasWidth, gameState.canvasHeight); bgctx.fillRect(0, 0, gameState.canvasWidth, gameState.canvasHeight);
if (gameState.perks.clairvoyant >= 3) { if (gameState.perks.clairvoyant >= 3) {
const pageSource = document.body.innerHTML.replace(/\s+/gi, '') const pageSource = document.body.innerHTML.replace(/\s+/gi, "");
const lineWidth = Math.ceil(gameState.canvasWidth / 15) const lineWidth = Math.ceil(gameState.canvasWidth / 15);
const lines = Math.ceil(gameState.canvasHeight / 20) const lines = Math.ceil(gameState.canvasHeight / 20);
const chars = lineWidth * lines const chars = lineWidth * lines;
let start = Math.ceil(Math.random() * (pageSource.length - chars)) let start = Math.ceil(Math.random() * (pageSource.length - chars));
for (let i = 0; i < lines; i++) { for (let i = 0; i < lines; i++) {
bgctx.fillStyle = 'white' bgctx.fillStyle = "white";
bgctx.font = '20px Courier' bgctx.font = "20px Courier";
bgctx.fillText(pageSource.slice( bgctx.fillText(
pageSource.slice(
start + i * lineWidth, start + i * lineWidth,
start + (i + 1) * lineWidth), start + (i + 1) * lineWidth,
),
0, 0,
i * 20, i * 20,
gameState.canvasWidth gameState.canvasWidth,
) );
} }
} else { } else {
const pattern = ctx.createPattern(background, "repeat"); const pattern = ctx.createPattern(background, "repeat");
if (pattern) { if (pattern) {
bgctx.fillStyle = pattern; bgctx.fillStyle = pattern;
@ -185,7 +189,7 @@ export function render(gameState: GameState) {
ctx.fillStyle = level.color || "#000"; ctx.fillStyle = level.color || "#000";
ctx.fillRect(0, 0, width, height); ctx.fillRect(0, 0, width, height);
forEachLiveOne(gameState.particles, (flash) => { forEachLiveOne(gameState.particles, (flash) => {
const {x, y, time, color, size, duration} = flash; const { x, y, time, color, size, duration } = flash;
const elapsed = gameState.levelTime - time; const elapsed = gameState.levelTime - time;
ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2); ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2);
drawBall(ctx, color, size, x, y); drawBall(ctx, color, size, x, y);
@ -223,7 +227,7 @@ export function render(gameState: GameState) {
coin.x, coin.x,
coin.y, coin.y,
(hasCombo && gameState.perks.asceticism && "red") || (hasCombo && gameState.perks.asceticism && "red") ||
(coin.color==='gold' && 'gold')|| (coin.color === "gold" && "gold") ||
gameState.puckColor, gameState.puckColor,
coin.a, coin.a,
); );
@ -242,7 +246,6 @@ export function render(gameState: GameState) {
ball.y, ball.y,
); );
}); });
} }
ctx.globalCompositeOperation = "source-over"; ctx.globalCompositeOperation = "source-over";
@ -250,7 +253,7 @@ export function render(gameState: GameState) {
ctx.globalCompositeOperation = "screen"; ctx.globalCompositeOperation = "screen";
forEachLiveOne(gameState.lights, (flash) => { forEachLiveOne(gameState.lights, (flash) => {
const {x, y, time, color, size, duration} = flash; const { x, y, time, color, size, duration } = flash;
const elapsed = gameState.levelTime - time; const elapsed = gameState.levelTime - time;
ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2) * 0.5; ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2) * 0.5;
drawBrick(ctx, color, x, y, -1, gameState.perks.clairvoyant >= 2); drawBrick(ctx, color, x, y, -1, gameState.perks.clairvoyant >= 2);
@ -258,7 +261,7 @@ export function render(gameState: GameState) {
ctx.globalCompositeOperation = "screen"; ctx.globalCompositeOperation = "screen";
forEachLiveOne(gameState.texts, (flash) => { forEachLiveOne(gameState.texts, (flash) => {
const {x, y, time, color, size, duration} = flash; const { x, y, time, color, size, duration } = flash;
const elapsed = gameState.levelTime - time; const elapsed = gameState.levelTime - time;
ctx.globalAlpha = Math.max(0, Math.min(1, 2 - (elapsed / duration) * 2)); ctx.globalAlpha = Math.max(0, Math.min(1, 2 - (elapsed / duration) * 2));
ctx.globalCompositeOperation = "source-over"; ctx.globalCompositeOperation = "source-over";
@ -266,7 +269,7 @@ export function render(gameState: GameState) {
}); });
forEachLiveOne(gameState.particles, (particle) => { forEachLiveOne(gameState.particles, (particle) => {
const {x, y, time, color, size, duration} = particle; const { x, y, time, color, size, duration } = particle;
const elapsed = gameState.levelTime - time; const elapsed = gameState.levelTime - time;
ctx.globalAlpha = Math.max(0, Math.min(1, 2 - (elapsed / duration) * 2)); ctx.globalAlpha = Math.max(0, Math.min(1, 2 - (elapsed / duration) * 2));
ctx.globalCompositeOperation = "screen"; ctx.globalCompositeOperation = "screen";
@ -458,7 +461,7 @@ export function render(gameState: GameState) {
); );
} }
ctx.globalAlpha = gameState.perks.unbounded > 1 ? 0.1 : 1 ctx.globalAlpha = gameState.perks.unbounded > 1 ? 0.1 : 1;
drawStraightLine( drawStraightLine(
ctx, ctx,
gameState, gameState,
@ -470,7 +473,7 @@ export function render(gameState: GameState) {
1, 1,
); );
ctx.globalAlpha = 1 ctx.globalAlpha = 1;
drawStraightLine( drawStraightLine(
ctx, ctx,
gameState, gameState,
@ -496,7 +499,6 @@ export function render(gameState: GameState) {
); );
} }
if (shaked) { if (shaked) {
ctx.resetTransform(); ctx.resetTransform();
} }
@ -608,7 +610,8 @@ export function renderAllBricks() {
countBricksAbove(gameState, index) && countBricksAbove(gameState, index) &&
!countBricksBelow(gameState, index); !countBricksBelow(gameState, index);
let redBorder = (gameState.ballsColor !== color && let redBorder =
(gameState.ballsColor !== color &&
color !== "black" && color !== "black" &&
redBorderOnBricksWithWrongColor) || redBorderOnBricksWithWrongColor) ||
(hasCombo && gameState.perks.zen && color === "black") || (hasCombo && gameState.perks.zen && color === "black") ||
@ -616,8 +619,14 @@ export function renderAllBricks() {
redColorOnAllBricks; redColorOnAllBricks;
canctx.globalCompositeOperation = "source-over"; canctx.globalCompositeOperation = "source-over";
drawBrick(canctx, drawBrick(
color, x, y, redBorder ? offset : -1, gameState.perks.clairvoyant >= 2); canctx,
color,
x,
y,
redBorder ? offset : -1,
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 = "destination-out";
drawText( drawText(
@ -677,9 +686,9 @@ export function drawPuck(
canctx.lineTo(0, puckHeight * 0.75); canctx.lineTo(0, puckHeight * 0.75);
canctx.bezierCurveTo( canctx.bezierCurveTo(
puckWidth / 2, puckWidth / 2,
puckHeight * (2 + concave_puck) / 3, (puckHeight * (2 + concave_puck)) / 3,
puckWidth / 2, puckWidth / 2,
puckHeight * (2 + concave_puck) / 3, (puckHeight * (2 + concave_puck)) / 3,
puckWidth, puckWidth,
puckHeight * 0.75, puckHeight * 0.75,
); );
@ -865,7 +874,7 @@ export function drawBrick(
x: number, x: number,
y: number, y: number,
offset: number = 0, offset: number = 0,
borderOnly: boolean borderOnly: boolean,
) { ) {
const tlx = Math.ceil(x - gameState.brickWidth / 2); const tlx = Math.ceil(x - gameState.brickWidth / 2);
const tly = Math.ceil(y - gameState.brickWidth / 2); const tly = Math.ceil(y - gameState.brickWidth / 2);
@ -874,7 +883,18 @@ export function drawBrick(
const width = brx - tlx, const width = brx - tlx,
height = bry - tly; height = bry - tly;
const key = "brick" + color + "_" + "_" + width + "_" + height + "_" + offset + '_' + borderOnly; const key =
"brick" +
color +
"_" +
"_" +
width +
"_" +
height +
"_" +
offset +
"_" +
borderOnly;
if (!cachedGraphics[key]) { if (!cachedGraphics[key]) {
const can = document.createElement("canvas"); const can = document.createElement("canvas");

14
src/types.d.ts vendored
View file

@ -83,7 +83,7 @@ export type Coin = {
weight: number; weight: number;
destroyed?: boolean; destroyed?: boolean;
collidedLastFrame?: boolean; collidedLastFrame?: boolean;
metamorphosisPoints:number; metamorphosisPoints: number;
}; };
export type Ball = { export type Ball = {
x: number; x: number;
@ -238,8 +238,12 @@ export type GameState = {
coins: ReusableArray<Coin>; coins: ReusableArray<Coin>;
// Bricks that should respawn destroyed // Bricks that should respawn destroyed
respawns: ReusableArray<{ index: number; color: string ; time:number; respawns: ReusableArray<{
destroyed?: boolean;}>; index: number;
color: string;
time: number;
destroyed?: boolean;
}>;
levelStartScore: number; levelStartScore: number;
levelMisses: number; levelMisses: number;
@ -280,14 +284,14 @@ export type GameState = {
rerolls: number; rerolls: number;
loop: number; loop: number;
baseCombo: number; baseCombo: number;
levelsPerLoop:number; levelsPerLoop: number;
}; };
export type RunParams = { export type RunParams = {
level?: string; level?: string;
levelToAvoid?: string; levelToAvoid?: string;
perks?: Partial<PerksMap>; perks?: Partial<PerksMap>;
levelsPerLoop?:number; levelsPerLoop?: number;
}; };
export type OptionDef = { export type OptionDef = {
default: boolean; default: boolean;

View file

@ -1,6 +1,6 @@
import { t } from "./i18n/i18n"; import { t } from "./i18n/i18n";
import {comboKeepingRate} from "./pure_functions"; import { comboKeepingRate } from "./pure_functions";
export const rawUpgrades = [ export const rawUpgrades = [
{ {
@ -49,7 +49,7 @@ export const rawUpgrades = [
id: "slow_down", id: "slow_down",
max: 2, max: 2,
name: t("upgrades.slow_down.name"), name: t("upgrades.slow_down.name"),
help: (lvl:number) => t("upgrades.slow_down.help",{ lvl }), help: (lvl: number) => t("upgrades.slow_down.help", { lvl }),
fullHelp: t("upgrades.slow_down.fullHelp"), fullHelp: t("upgrades.slow_down.fullHelp"),
}, },
{ {
@ -84,7 +84,7 @@ export const rawUpgrades = [
max: 1, max: 1,
name: t("upgrades.left_is_lava.name"), name: t("upgrades.left_is_lava.name"),
help: (lvl:number) => t("upgrades.left_is_lava.help",{ lvl }), help: (lvl: number) => t("upgrades.left_is_lava.help", { lvl }),
fullHelp: t("upgrades.left_is_lava.fullHelp"), fullHelp: t("upgrades.left_is_lava.fullHelp"),
}, },
{ {
@ -95,7 +95,7 @@ export const rawUpgrades = [
giftable: true, giftable: true,
max: 1, max: 1,
name: t("upgrades.right_is_lava.name"), name: t("upgrades.right_is_lava.name"),
help: (lvl:number) => t("upgrades.right_is_lava.help",{ lvl }), help: (lvl: number) => t("upgrades.right_is_lava.help", { lvl }),
fullHelp: t("upgrades.right_is_lava.fullHelp"), fullHelp: t("upgrades.right_is_lava.fullHelp"),
}, },
{ {
@ -106,7 +106,7 @@ export const rawUpgrades = [
giftable: true, giftable: true,
max: 1, max: 1,
name: t("upgrades.top_is_lava.name"), name: t("upgrades.top_is_lava.name"),
help: (lvl:number) => t("upgrades.top_is_lava.help",{ lvl }), help: (lvl: number) => t("upgrades.top_is_lava.help", { lvl }),
fullHelp: t("upgrades.top_is_lava.fullHelp"), fullHelp: t("upgrades.top_is_lava.fullHelp"),
}, },
{ {
@ -195,7 +195,7 @@ export const rawUpgrades = [
giftable: true, giftable: true,
max: 1, max: 1,
name: t("upgrades.picky_eater.name"), name: t("upgrades.picky_eater.name"),
help: (lvl: number) => t("upgrades.picky_eater.help",{lvl}), help: (lvl: number) => t("upgrades.picky_eater.help", { lvl }),
fullHelp: t("upgrades.picky_eater.fullHelp"), fullHelp: t("upgrades.picky_eater.fullHelp"),
}, },
{ {
@ -206,7 +206,7 @@ export const rawUpgrades = [
id: "metamorphosis", id: "metamorphosis",
max: 1, max: 1,
name: t("upgrades.metamorphosis.name"), name: t("upgrades.metamorphosis.name"),
help: (lvl: number) => t("upgrades.metamorphosis.help",{lvl}), help: (lvl: number) => t("upgrades.metamorphosis.help", { lvl }),
fullHelp: t("upgrades.metamorphosis.fullHelp"), fullHelp: t("upgrades.metamorphosis.fullHelp"),
}, },
{ {
@ -217,7 +217,7 @@ export const rawUpgrades = [
giftable: true, giftable: true,
max: 1, max: 1,
name: t("upgrades.compound_interest.name"), name: t("upgrades.compound_interest.name"),
help: (lvl: number) => t("upgrades.compound_interest.help",{lvl}), help: (lvl: number) => t("upgrades.compound_interest.help", { lvl }),
fullHelp: t("upgrades.compound_interest.fullHelp"), fullHelp: t("upgrades.compound_interest.fullHelp"),
}, },
{ {
@ -289,7 +289,10 @@ export const rawUpgrades = [
id: "soft_reset", id: "soft_reset",
max: 3, max: 3,
name: t("upgrades.soft_reset.name"), name: t("upgrades.soft_reset.name"),
help: (lvl: number) => t("upgrades.soft_reset.help", { percent: Math.round(comboKeepingRate(lvl) * 100)}), help: (lvl: number) =>
t("upgrades.soft_reset.help", {
percent: Math.round(comboKeepingRate(lvl) * 100),
}),
fullHelp: t("upgrades.soft_reset.fullHelp"), fullHelp: t("upgrades.soft_reset.fullHelp"),
}, },
{ {
@ -354,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 * 10 }),
// ? // ?
// : t("upgrades.sturdy_bricks.help_plural"), // : t("upgrades.sturdy_bricks.help_plural"),
fullHelp: t("upgrades.sturdy_bricks.fullHelp"), fullHelp: t("upgrades.sturdy_bricks.fullHelp"),
@ -368,7 +371,10 @@ export const rawUpgrades = [
max: 4, max: 4,
name: t("upgrades.respawn.name"), name: t("upgrades.respawn.name"),
help: (lvl: number) => help: (lvl: number) =>
t("upgrades.respawn.help",{percent:Math.floor(100*comboKeepingRate(lvl)),delay:(3/lvl).toFixed(2)}), t("upgrades.respawn.help", {
percent: Math.floor(100 * comboKeepingRate(lvl)),
delay: (3 / lvl).toFixed(2),
}),
fullHelp: t("upgrades.respawn.fullHelp"), fullHelp: t("upgrades.respawn.fullHelp"),
}, },
{ {
@ -378,7 +384,7 @@ export const rawUpgrades = [
id: "one_more_choice", id: "one_more_choice",
max: 3, max: 3,
name: t("upgrades.one_more_choice.name"), name: t("upgrades.one_more_choice.name"),
help: (lvl: number) => t("upgrades.one_more_choice.help", {lvl}), help: (lvl: number) => t("upgrades.one_more_choice.help", { lvl }),
fullHelp: t("upgrades.one_more_choice.fullHelp"), fullHelp: t("upgrades.one_more_choice.fullHelp"),
}, },
{ {
@ -390,7 +396,7 @@ export const rawUpgrades = [
max: 2, max: 2,
adventure: false, adventure: false,
name: t("upgrades.instant_upgrade.name"), name: t("upgrades.instant_upgrade.name"),
help: (lvl: number) => t("upgrades.instant_upgrade.help",{lvl}), help: (lvl: number) => t("upgrades.instant_upgrade.help", { lvl }),
fullHelp: t("upgrades.instant_upgrade.fullHelp"), fullHelp: t("upgrades.instant_upgrade.fullHelp"),
}, },
{ {
@ -422,7 +428,7 @@ export const rawUpgrades = [
id: "asceticism", id: "asceticism",
max: 1, max: 1,
name: t("upgrades.asceticism.name"), name: t("upgrades.asceticism.name"),
help: (lvl: number) => t("upgrades.asceticism.help",{combo:lvl*3}), help: (lvl: number) => t("upgrades.asceticism.help", { combo: lvl * 3 }),
fullHelp: t("upgrades.asceticism.fullHelp"), fullHelp: t("upgrades.asceticism.fullHelp"),
}, },
{ {
@ -433,9 +439,10 @@ export const rawUpgrades = [
id: "unbounded", id: "unbounded",
max: 1, max: 1,
name: t("upgrades.unbounded.name"), name: t("upgrades.unbounded.name"),
help: (lvl: number) => lvl > 1 ? help: (lvl: number) =>
t("upgrades.unbounded.help_no_ceiling",{lvl}): lvl > 1
t("upgrades.unbounded.help",{lvl}), ? t("upgrades.unbounded.help_no_ceiling", { lvl })
: t("upgrades.unbounded.help", { lvl }),
fullHelp: t("upgrades.unbounded.fullHelp"), fullHelp: t("upgrades.unbounded.fullHelp"),
}, },
{ {
@ -446,7 +453,10 @@ export const rawUpgrades = [
id: "shunt", id: "shunt",
max: 3, max: 3,
name: t("upgrades.shunt.name"), name: t("upgrades.shunt.name"),
help: (lvl: number) => t("upgrades.shunt.help", { percent: Math.round(comboKeepingRate(lvl) * 100) }), help: (lvl: number) =>
t("upgrades.shunt.help", {
percent: Math.round(comboKeepingRate(lvl) * 100),
}),
fullHelp: t("upgrades.shunt.fullHelp"), fullHelp: t("upgrades.shunt.fullHelp"),
}, },
{ {
@ -499,7 +509,7 @@ export const rawUpgrades = [
id: "zen", id: "zen",
max: 1, max: 1,
name: t("upgrades.zen.name"), name: t("upgrades.zen.name"),
help: (lvl: number) => t("upgrades.zen.help",{lvl}), help: (lvl: number) => t("upgrades.zen.help", { lvl }),
fullHelp: t("upgrades.zen.fullHelp"), fullHelp: t("upgrades.zen.fullHelp"),
}, },
{ {
@ -510,9 +520,9 @@ export const rawUpgrades = [
max: 1, max: 1,
name: t("upgrades.sacrifice.name"), name: t("upgrades.sacrifice.name"),
help: (lvl: number) => help: (lvl: number) =>
lvl==1 ? lvl == 1
t("upgrades.sacrifice.help_l1"): ? t("upgrades.sacrifice.help_l1")
t("upgrades.sacrifice.help_over",{lvl}), : t("upgrades.sacrifice.help_over", { lvl }),
fullHelp: t("upgrades.sacrifice.fullHelp"), fullHelp: t("upgrades.sacrifice.fullHelp"),
}, },