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

@ -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

@ -12,7 +12,13 @@ import {
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";
@ -34,10 +40,27 @@ 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,
ctx,
gameCanvas,
render,
scoreDisplay,
} from "./render";
import {
pauseRecording,
recordOneFrame,
resumeRecording,
startRecordingGame,
} from "./recording";
import { newGameState } from "./newGameState"; import { newGameState } from "./newGameState";
import {alertsOpen, asyncAlert, AsyncAlertAction, closeModal, requiredAsyncAlert,} from "./asyncAlert"; import {
alertsOpen,
asyncAlert,
AsyncAlertAction,
closeModal,
requiredAsyncAlert,
} from "./asyncAlert";
import { isOptionOn, options, toggleOption } from "./options"; import { isOptionOn, options, toggleOption } from "./options";
import { hashCode } from "./getLevelBackground"; import { hashCode } from "./getLevelBackground";
import { premiumMenuEntry } from "./premium"; import { premiumMenuEntry } from "./premium";
@ -161,13 +184,10 @@ setInterval(() => {
}, 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 = "",
@ -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,
}); });
@ -564,7 +584,7 @@ async function openSettingsMenu() {
value: () => { value: () => {
toggleOption(key); toggleOption(key);
fitSize(); fitSize();
applyFullScreenChoice() applyFullScreenChoice();
openSettingsMenu(); openSettingsMenu();
}, },
}); });
@ -786,40 +806,36 @@ 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
@ -831,7 +847,7 @@ async function openUnlocksList() {
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)
@ -849,11 +865,12 @@ async function openUnlocksList() {
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 }),
@ -863,7 +880,6 @@ async function openUnlocksList() {
...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);
} }
@ -970,7 +985,6 @@ 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",
@ -983,8 +997,7 @@ restart(
clairvoyant: 3, clairvoyant: 3,
bigger_explosions: 2, bigger_explosions: 2,
sapper: 2, sapper: 2,
unbounded:1 unbounded: 1,
}, },
levelsPerLoop: 2, levelsPerLoop: 2,
}) || }) ||

View file

@ -31,10 +31,22 @@ import {
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 {
addToTotalScore,
getCurrentMaxCoins,
getCurrentMaxParticles,
} from "./settings";
import { background } from "./render"; import { background } from "./render";
import { gameOver } from "./gameOver"; import { gameOver } from "./gameOver";
import {brickIndex, fitSize, gameState, hasBrick, hitsSomething, openUpgradesPicker, pause,} from "./game"; import {
brickIndex,
fitSize,
gameState,
hasBrick,
hitsSomething,
openUpgradesPicker,
pause,
} from "./game";
import { stopRecording } from "./recording"; import { stopRecording } from "./recording";
import { isOptionOn } from "./options"; import { isOptionOn } from "./options";
import { isPremium } from "./premium"; import { isPremium } from "./premium";
@ -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++;
@ -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;
}) });
} }
} }
} }
@ -579,7 +601,7 @@ export async function gotoNextLoop(gameState: GameState) {
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;
} }
@ -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) {
@ -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,10 +1299,10 @@ 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;
@ -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,15 +1525,14 @@ 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++;
@ -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;
}); });
} }
@ -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

@ -15,9 +15,7 @@ export function sample<T>(arr: T[]): T {
} }
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,
@ -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");
@ -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" : "";
@ -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;
@ -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";
@ -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");

8
src/types.d.ts vendored
View file

@ -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;

View file

@ -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"),
}, },
{ {
@ -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"),
}, },
{ {
@ -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"),
}, },
{ {
@ -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"),
}, },