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"
minSdk = 21
targetSdk = 34
versionCode = 29053158
versionName = "29053158"
versionCode = 29054664
versionName = "29054664"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
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>` : "",
(0, _gameUtils.pickedUpgradesHTMl)(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
}) : '',
banned && (0, _i18N.t)('score_panel.banned', {
}) : "",
banned && (0, _i18N.t)("score_panel.banned", {
banned
})
],
@ -1293,7 +1293,7 @@ function setKeyPressed(key, on) {
}
document.addEventListener("keydown", (e)=>{
if (e.key.toLowerCase() === "f" && !e.ctrlKey && !e.metaKey) {
(0, _options.toggleOption)('fullscreen');
(0, _options.toggleOption)("fullscreen");
applyFullScreenChoice();
} else if (e.key in pressed) setKeyPressed(e.key, 1);
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) {
module.exports = JSON.parse("\"29053158\"");
module.exports = JSON.parse("\"29054664\"");
},{}],"1u3Dx":[function(require,module,exports,__globalThis) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
@ -2901,7 +2901,7 @@ async function gotoNextLoop(gameState) {
content: [
(0, _i18N.t)("loop.instructions"),
comboText,
...userPerks.filter((u)=>u.id !== 'instant_upgrade').map((u)=>{
...userPerks.filter((u)=>u.id !== "instant_upgrade").map((u)=>{
return {
text: u.name + (0, _i18N.t)("level_up.upgrade_perk_to_level", {
level: gameState.perks[u.id] + 1
@ -3538,24 +3538,24 @@ function render(gameState) {
else menuLabel.innerText = (0, _i18N.t)("play.menu_label");
const catchRate = gameState.levelSpawnedCoins ? (gameState.levelSpawnedCoins - gameState.levelLostCoins) / gameState.levelSpawnedCoins : 1;
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
</span><span> / </span>
` : '') + ((0, _options.isOptionOn)('show_stats') ? `
<span class="${catchRate == 1 && 'great' || catchRate > 0.9 && 'good' || ''}">
` : "") + ((0, _options.isOptionOn)("show_stats") ? `
<span class="${catchRate == 1 && "great" || catchRate > 0.9 && "good" || ""}">
${Math.floor(catchRate * 100)}%
</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
</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
</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
</span><span> / </span>
` : '') + `$${gameState.score}`;
` : "") + `$${gameState.score}`;
scoreDisplay.className = gameState.lastScoreIncrease > gameState.levelTime - 500 ? "active" : "";
// Clear
if (!(0, _options.isOptionOn)("basic") && !level.color && level.svg) {
@ -3602,14 +3602,14 @@ function render(gameState) {
bgctx.fillStyle = level.color || "#000";
bgctx.fillRect(0, 0, gameState.canvasWidth, gameState.canvasHeight);
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 lines = Math.ceil(gameState.canvasHeight / 20);
const chars = lineWidth * lines;
let start = Math.ceil(Math.random() * (pageSource.length - chars));
for(let i = 0; i < lines; i++){
bgctx.fillStyle = 'white';
bgctx.font = '20px Courier';
bgctx.fillStyle = "white";
bgctx.font = "20px Courier";
bgctx.fillText(pageSource.slice(start + i * lineWidth, start + (i + 1) * lineWidth), 0, i * 20, gameState.canvasWidth);
}
} else {
@ -3654,7 +3654,7 @@ function render(gameState) {
ctx.globalCompositeOperation = "source-over";
// ctx.globalCompositeOperation =
// 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
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 bry = Math.ceil(y + (0, _game.gameState).brickWidth / 2) - 1;
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]) {
const can = document.createElement("canvas");
can.width = width;

View file

@ -1,5 +1,5 @@
// The version of the cache.
const VERSION = "29053158";
const VERSION = "29054664";
// The name of the cache
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,
Lucida Sans Typewriter,
Lucida Typewriter,

View file

@ -12,7 +12,13 @@ import {
Upgrade,
} from "./types";
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 { getCurrentLang, t } from "./i18n/i18n";
@ -34,10 +40,27 @@ import {
setLevel,
setMousePos,
} from "./gameStateMutators";
import {backgroundCanvas, ctx, gameCanvas, render, scoreDisplay,} from "./render";
import {pauseRecording, recordOneFrame, resumeRecording, startRecordingGame,} from "./recording";
import {
backgroundCanvas,
ctx,
gameCanvas,
render,
scoreDisplay,
} from "./render";
import {
pauseRecording,
recordOneFrame,
resumeRecording,
startRecordingGame,
} from "./recording";
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 { hashCode } from "./getLevelBackground";
import { premiumMenuEntry } from "./premium";
@ -161,13 +184,10 @@ setInterval(() => {
}, 1000);
export async function openUpgradesPicker(gameState: GameState) {
const catchRate =
(gameState.score - gameState.levelStartScore) /
(gameState.levelSpawnedCoins || 1);
let repeats = 1;
let timeGain = "",
@ -389,7 +409,7 @@ let FPSCounter = 0;
export let lastMeasuredFPS = 60;
setInterval(() => {
lastMeasuredFPS = FPSCounter
lastMeasuredFPS = FPSCounter;
FPSCounter = 0;
}, 1000);
@ -420,7 +440,6 @@ async function openScorePanel() {
.map((u) => u.name)
.join(", ");
const cb = await asyncAlert({
title: gameState.loop
? t("score_panel.title_looped", {
@ -439,9 +458,10 @@ async function openScorePanel() {
gameState.isCreativeModeRun ? `<p>${t("score_panel.test_run")}</p>` : "",
pickedUpgradesHTMl(gameState),
levelsListHTMl(gameState),
gameState.rerolls ?
t('score_panel.rerolls_count', {rerolls: gameState.rerolls}) : '',
banned && t('score_panel.banned', {banned})
gameState.rerolls
? t("score_panel.rerolls_count", { rerolls: gameState.rerolls })
: "",
banned && t("score_panel.banned", { banned }),
],
allowClose: true,
});
@ -564,7 +584,7 @@ async function openSettingsMenu() {
value: () => {
toggleOption(key);
fitSize();
applyFullScreenChoice()
applyFullScreenChoice();
openSettingsMenu();
},
});
@ -786,40 +806,36 @@ async function openSettingsMenu() {
}
}
function applyFullScreenChoice(): boolean {
try {
if (!(document.fullscreenEnabled || document.webkitFullscreenEnabled)) {
return false
return false;
}
if (document.fullscreenElement !== null && !isOptionOn("fullscreen")) {
if (document.exitFullscreen) {
document.exitFullscreen();
return true
return true;
} else if (document.webkitCancelFullScreen) {
document.webkitCancelFullScreen();
return true
return true;
}
} else if (isOptionOn("fullscreen") && !document.fullscreenElement) {
const docel = document.documentElement;
if (docel.requestFullscreen) {
docel.requestFullscreen();
return true
return true;
} else if (docel.webkitRequestFullscreen) {
docel.webkitRequestFullscreen();
return true
return true;
}
}
} catch (e) {
console.warn(e);
}
return false
return false;
}
async function openUnlocksList() {
const ts = getTotalScore();
const upgradeActions = upgrades
@ -831,7 +847,7 @@ async function openUnlocksList() {
disabled: ts < threshold,
value: { perks: { [id]: 1 } } as RunParams,
icon,
}))
}));
const levelActions = allLevels
.sort((a, b) => a.threshold - b.threshold)
@ -849,11 +865,12 @@ async function openUnlocksList() {
value: { level: l.name } as RunParams,
icon: icons[l.name],
};
})
});
const percentUnlock = Math.round(
([...upgradeActions, ...levelActions].filter((a) => !a.disabled).length / (upgradeActions.length +
levelActions.length)) * 100,
([...upgradeActions, ...levelActions].filter((a) => !a.disabled).length /
(upgradeActions.length + levelActions.length)) *
100,
);
const tryOn = await asyncAlert<RunParams>({
title: t("unlocks.title", { percentUnlock }),
@ -863,7 +880,6 @@ async function openUnlocksList() {
...upgradeActions,
t("unlocks.level"),
...levelActions,
],
allowClose: true,
actionsAsGrid: true,
@ -894,7 +910,6 @@ export async function confirmRestart(gameState) {
});
}
const pressed: { [k: string]: number } = {
ArrowLeft: 0,
ArrowRight: 0,
@ -912,8 +927,8 @@ export function setKeyPressed(key: string, on: 0 | 1) {
document.addEventListener("keydown", (e) => {
if (e.key.toLowerCase() === "f" && !e.ctrlKey && !e.metaKey) {
toggleOption('fullscreen');
applyFullScreenChoice()
toggleOption("fullscreen");
applyFullScreenChoice();
} else if (e.key in pressed) {
setKeyPressed(e.key, 1);
}
@ -970,7 +985,6 @@ export function restart(params: RunParams) {
setLevel(gameState, 0);
}
restart(
(window.location.search.includes("stressTest") && {
level: "Bird",
@ -983,8 +997,7 @@ restart(
clairvoyant: 3,
bigger_explosions: 2,
sapper: 2,
unbounded:1
unbounded: 1,
},
levelsPerLoop: 2,
}) ||

View file

@ -31,10 +31,22 @@ import {
import { t } from "./i18n/i18n";
import { icons, upgrades } from "./loadGameData";
import {addToTotalScore, getCurrentMaxCoins, getCurrentMaxParticles,} from "./settings";
import {
addToTotalScore,
getCurrentMaxCoins,
getCurrentMaxParticles,
} from "./settings";
import { background } from "./render";
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 { isOptionOn } from "./options";
import { isPremium } from "./premium";
@ -119,18 +131,27 @@ export function normalizeGameState(gameState: GameState) {
gameState.perks.slow_down * 2,
);
gameState.puckWidth = Math.max(gameState.ballSize,
gameState.puckWidth = Math.max(
gameState.ballSize,
(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.puckWidth / 2 + gameState.puckWidth * corner;
gameState.puckWidth / 2 +
gameState.puckWidth * corner;
gameState.puckPosition = clamp(gameState.puckPosition, minX, maxX);
@ -165,7 +186,7 @@ export function resetCombo(
if (prev > gameState.combo && gameState.perks.soft_reset) {
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);
@ -177,8 +198,15 @@ export function resetCombo(
);
}
if (typeof x !== "undefined" && typeof y !== "undefined") {
makeText(gameState, x, y, "red", "-" + lost, 20, 500 + clamp(lost, 0, 500));
makeText(
gameState,
x,
y,
"red",
"-" + lost,
20,
500 + clamp(lost, 0, 500),
);
}
}
return lost;
@ -255,11 +283,13 @@ export function explosionAt(
x: number,
y: number,
ball: Ball,
extraSize: number = 0
extraSize: number = 0,
) {
const size = 1 + gameState.perks.bigger_explosions +
Math.max(0, gameState.perks.implosions - 1) + extraSize
;
const size =
1 +
gameState.perks.bigger_explosions +
Math.max(0, gameState.perks.implosions - 1) +
extraSize;
schedulGameSound(gameState, "explode", ball.x, 1);
if (index !== -1) {
const col = index % gameState.gridSize;
@ -292,21 +322,9 @@ export function explosionAt(
// makeLight(gameState, x, y, "white", gameState.brickWidth * 2, 150);
if (gameState.perks.implosions) {
spawnImplosion(
gameState,
7 * size,
x,
y,
"white",
);
spawnImplosion(gameState, 7 * size, x, y, "white");
} else {
spawnExplosion(
gameState,
7 * size,
x,
y,
"white",
);
spawnExplosion(gameState, 7 * size, x, y, "white");
}
gameState.runStatistics.bricks_broken++;
@ -467,13 +485,17 @@ export function explodeBrick(
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)) {
append(gameState.respawns, b => {
b.color = color
b.index = index
b.time = gameState.levelTime + 3 * 1000 / gameState.perks.respawn
})
append(gameState.respawns, (b) => {
b.color = color;
b.index = index;
b.time = gameState.levelTime + (3 * 1000) / gameState.perks.respawn;
});
}
}
}
@ -579,7 +601,7 @@ export async function gotoNextLoop(gameState: GameState) {
t("loop.instructions"),
comboText,
...userPerks
.filter(u => u.id !== 'instant_upgrade')
.filter((u) => u.id !== "instant_upgrade")
.map((u) => {
return {
text:
@ -595,11 +617,11 @@ export async function gotoNextLoop(gameState: GameState) {
],
});
userPerks.forEach(u => {
userPerks.forEach((u) => {
if (u.id !== keep) {
gameState.bannedPerks[u.id] = 1
gameState.bannedPerks[u.id] = 1;
}
})
});
Object.assign(gameState.perks, makeEmptyPerksMap(upgrades), {
[keep]: gameState.perks[keep],
@ -642,7 +664,8 @@ export async function setLevel(gameState: GameState, l: number) {
gameState.combo += Math.round(
Math.max(
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.brickHP[index] =
(color === "black" && 1) ||
(color &&
1 + gameState.perks.sturdy_bricks) ||
(color && 1 + gameState.perks.sturdy_bricks) ||
0;
}
@ -805,11 +827,9 @@ export function coinBrickHitCheck(gameState: GameState, coin: Coin) {
if (gameState.perks.ghost_coins) {
// slow down
if (typeof (vhit ?? hhit ?? chit) !== "undefined") {
coin.vy *= 1 - 0.2 / gameState.perks.ghost_coins;
coin.vx *= 1 - 0.2 / gameState.perks.ghost_coins;
}
} else {
if (typeof vhit !== "undefined" || typeof chit !== "undefined") {
coin.y = coin.previousY;
@ -934,7 +954,7 @@ export function gameStateTick(
gameState.autoCleanUses++;
}
const hasPendingBricks = liveCount(gameState.respawns)
const hasPendingBricks = liveCount(gameState.respawns);
if (gameState.running && !remainingBricks && !hasPendingBricks) {
if (!gameState.winAt) {
@ -997,10 +1017,8 @@ export function gameStateTick(
const ratio =
1 -
(gameState.perks.viscosity *
0.03 +
0.005) *
frames / (1 + gameState.perks.etherealcoins);
((gameState.perks.viscosity * 0.03 + 0.005) * frames) /
(1 + gameState.perks.etherealcoins);
coin.vy *= ratio;
coin.vx *= ratio;
@ -1018,7 +1036,8 @@ export function gameStateTick(
gameState.perks.helium > 0 &&
Math.abs(coin.x - gameState.puckPosition) * 2 >
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) {
makeParticle(
gameState,
@ -1034,7 +1053,6 @@ export function gameStateTick(
}
}
const speed = (Math.abs(coin.vx) + Math.abs(coin.vy)) * 10;
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
gameState.puckHeight * (coin.points ? 1 : -1)
) {
addToScore(gameState, coin);
destroy(gameState.coins, coinIndex);
} else if (coin.y > gameState.canvasHeight + coinRadius) {
gameState.levelLostCoins+=coin.points
gameState.levelLostCoins += coin.points;
destroy(gameState.coins, coinIndex);
if (gameState.perks.compound_interest) {
resetCombo(gameState, coin.x, coin.y);
@ -1060,12 +1077,11 @@ export function gameStateTick(
} else if (
gameState.perks.unbounded &&
(coin.x < -gameState.gameZoneWidth / 2 ||
coin.x > gameState.canvasWidth + gameState.gameZoneWidth / 2
|| coin.y < -gameState.gameZoneWidth
)
coin.x > gameState.canvasWidth + gameState.gameZoneWidth / 2 ||
coin.y < -gameState.gameZoneWidth)
) {
// Out of bound on sides
gameState.levelLostCoins+=coin.points
gameState.levelLostCoins += coin.points;
destroy(gameState.coins, coinIndex);
}
@ -1140,7 +1156,14 @@ export function gameStateTick(
((Math.random() - 0.5) * limit) / 3;
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
forEachLiveOne(gameState.respawns, (r, ri) => {
if (gameState.bricks[r.index]) {
destroy(gameState.respawns, ri)
destroy(gameState.respawns, ri);
} else if (gameState.levelTime > r.time) {
setBrick(gameState, r.index, r.color)
destroy(gameState.respawns, ri)
setBrick(gameState, r.index, r.color);
destroy(gameState.respawns, ri);
} else if (!isOptionOn("basic")) {
const { index, color } = r;
const vertical = Math.random() > 0.5;
@ -1298,8 +1321,7 @@ export function gameStateTick(
250,
);
}
})
});
forEachLiveOne(gameState.particles, (p, pi) => {
if (gameState.levelTime > p.time + p.duration) {
@ -1334,14 +1356,12 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
ball.vx +=
((gameState.puckPosition - ball.x) / 1000) *
delta *
gameState.perks.telekinesis
gameState.perks.telekinesis;
}
if (isYoyoActive(gameState, ball)) {
speedLimitDampener += 3;
ball.vx +=
((gameState.puckPosition - ball.x) / 1000) *
delta *
gameState.perks.yoyo
((gameState.puckPosition - ball.x) / 1000) * delta * gameState.perks.yoyo;
}
if (
ball.vx * ball.vx + ball.vy * ball.vy <
@ -1390,7 +1410,6 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
);
}
const borderHitCode = bordersHitCheck(
gameState,
ball,
@ -1449,7 +1468,9 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
const angle = Math.atan2(
-gameState.puckWidth / 2,
(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.vy = speed * Math.sin(angle);
@ -1471,7 +1492,6 @@ export function ballTick(gameState: GameState, ball: Ball, delta: number) {
resetCombo(gameState, ball.x, ball.y);
}
if (!ball.hitSinceBounce && gameState.bricks.find((i) => i)) {
gameState.runStatistics.misses++;
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) ||
ball.x > gameState.canvasWidth + gameState.gameZoneWidth / 2;
const lostInTheSky = (gameState.perks.unbounded > 1 &&
ball.y < -gameState.gameZoneWidth / 2
)
const lostInTheSky =
gameState.perks.unbounded > 1 && ball.y < -gameState.gameZoneWidth / 2;
if (
gameState.running &&
(ball.y > gameState.gameZoneHeight + gameState.ballSize / 2 || lostOnSides
|| lostInTheSky
)
(ball.y > gameState.gameZoneHeight + gameState.ballSize / 2 ||
lostOnSides ||
lostInTheSky)
) {
ball.destroyed = true;
gameState.runStatistics.balls_lost++;
@ -1638,7 +1657,7 @@ function justLostALife(gameState: GameState, ball: Ball, x: number, y: number) {
if (gameState.perks.extra_life < 0) {
gameState.perks.extra_life = 0;
} else if (gameState.perks.sacrifice) {
gameState.combo *= gameState.perks.sacrifice
gameState.combo *= gameState.perks.sacrifice;
gameState.bricks.forEach(
(color, index) => color && explodeBrick(gameState, index, ball, true),
);
@ -1671,8 +1690,8 @@ function makeCoin(
color = "gold",
points = 1,
) {
let weight = 0.8 + Math.random() * 0.2 + Math.min(2, points * 0.01)
weight *= 5 / (5 + gameState.perks.etherealcoins)
let weight = 0.8 + Math.random() * 0.2 + Math.min(2, points * 0.01);
weight *= 5 / (5 + gameState.perks.etherealcoins);
append(gameState.coins, (p: Partial<Coin>) => {
p.x = x;
@ -1690,7 +1709,7 @@ function makeCoin(
p.sa = Math.random() - 0.5;
p.points = points;
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>) {
let destroyed=0
let destroyed = 0;
where.total = 0;
where.indexMin = 0;
where.list.forEach((i) => {
if (!i.destroyed) {
i.destroyed = true
destroyed++
i.destroyed = true;
destroyed++;
}
});
return destroyed
return destroyed;
}
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[] {
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) {
@ -55,7 +53,10 @@ export function getRowColIndex(gameState: GameState, row: number, col: number) {
export function getPossibleUpgrades(gameState: GameState) {
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]);
}
@ -186,4 +187,3 @@ export function countBricksBelow(gameState: GameState, index: number) {
}
return count;
}

View file

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

View file

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

View file

@ -3,5 +3,5 @@ export function clamp(value: number, min: number, max: 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 {
brickCenterX,
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">
<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>`);
bombSVG.onload = () => gameState.needsRender = true
bombSVG.onload = () => (gameState.needsRender = true);
export const background = document.createElement("img");
export const backgroundCanvas = document.createElement("canvas");
@ -51,34 +51,37 @@ export function render(gameState: GameState) {
menuLabel.innerText = 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 =
(isOptionOn("show_fps") ? `
<span class="${(Math.abs(lastMeasuredFPS-60)<2 && ' ') || (Math.abs(lastMeasuredFPS-60)<10 && 'good')||'bad'}">
(isOptionOn("show_fps")
? `
<span class="${(Math.abs(lastMeasuredFPS - 60) < 2 && " ") || (Math.abs(lastMeasuredFPS - 60) < 10 && "good") || "bad"}">
${lastMeasuredFPS} FPS
</span><span> / </span>
`:'')+
(isOptionOn('show_stats') ? `
<span class="${(catchRate==1 && 'great') || (catchRate>0.9 && 'good')||''}">
`
: "") +
(isOptionOn("show_stats")
? `
<span class="${(catchRate == 1 && "great") || (catchRate > 0.9 && "good") || ""}">
${Math.floor(catchRate * 100)}%
</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
</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
</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
</span><span> / </span>
`: '' )+ `$${gameState.score}`;
`
: "") +
`$${gameState.score}`;
scoreDisplay.className =
gameState.lastScoreIncrease > gameState.levelTime - 500 ? "active" : "";
@ -147,24 +150,25 @@ export function render(gameState: GameState) {
bgctx.fillStyle = level.color || "#000";
bgctx.fillRect(0, 0, gameState.canvasWidth, gameState.canvasHeight);
if (gameState.perks.clairvoyant >= 3) {
const pageSource = document.body.innerHTML.replace(/\s+/gi, '')
const lineWidth = Math.ceil(gameState.canvasWidth / 15)
const lines = Math.ceil(gameState.canvasHeight / 20)
const chars = lineWidth * lines
let start = Math.ceil(Math.random() * (pageSource.length - chars))
const pageSource = document.body.innerHTML.replace(/\s+/gi, "");
const lineWidth = Math.ceil(gameState.canvasWidth / 15);
const lines = Math.ceil(gameState.canvasHeight / 20);
const chars = lineWidth * lines;
let start = Math.ceil(Math.random() * (pageSource.length - chars));
for (let i = 0; i < lines; i++) {
bgctx.fillStyle = 'white'
bgctx.font = '20px Courier'
bgctx.fillText(pageSource.slice(
bgctx.fillStyle = "white";
bgctx.font = "20px Courier";
bgctx.fillText(
pageSource.slice(
start + i * lineWidth,
start + (i + 1) * lineWidth),
start + (i + 1) * lineWidth,
),
0,
i * 20,
gameState.canvasWidth
)
gameState.canvasWidth,
);
}
} else {
const pattern = ctx.createPattern(background, "repeat");
if (pattern) {
bgctx.fillStyle = pattern;
@ -223,7 +227,7 @@ export function render(gameState: GameState) {
coin.x,
coin.y,
(hasCombo && gameState.perks.asceticism && "red") ||
(coin.color==='gold' && 'gold')||
(coin.color === "gold" && "gold") ||
gameState.puckColor,
coin.a,
);
@ -242,7 +246,6 @@ export function render(gameState: GameState) {
ball.y,
);
});
}
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(
ctx,
gameState,
@ -470,7 +473,7 @@ export function render(gameState: GameState) {
1,
);
ctx.globalAlpha = 1
ctx.globalAlpha = 1;
drawStraightLine(
ctx,
gameState,
@ -496,7 +499,6 @@ export function render(gameState: GameState) {
);
}
if (shaked) {
ctx.resetTransform();
}
@ -608,7 +610,8 @@ export function renderAllBricks() {
countBricksAbove(gameState, index) &&
!countBricksBelow(gameState, index);
let redBorder = (gameState.ballsColor !== color &&
let redBorder =
(gameState.ballsColor !== color &&
color !== "black" &&
redBorderOnBricksWithWrongColor) ||
(hasCombo && gameState.perks.zen && color === "black") ||
@ -616,8 +619,14 @@ export function renderAllBricks() {
redColorOnAllBricks;
canctx.globalCompositeOperation = "source-over";
drawBrick(canctx,
color, x, y, redBorder ? offset : -1, gameState.perks.clairvoyant >= 2);
drawBrick(
canctx,
color,
x,
y,
redBorder ? offset : -1,
gameState.perks.clairvoyant >= 2,
);
if (gameState.brickHP[index] > 1 && gameState.perks.clairvoyant) {
canctx.globalCompositeOperation = "destination-out";
drawText(
@ -677,9 +686,9 @@ export function drawPuck(
canctx.lineTo(0, puckHeight * 0.75);
canctx.bezierCurveTo(
puckWidth / 2,
puckHeight * (2 + concave_puck) / 3,
(puckHeight * (2 + concave_puck)) / 3,
puckWidth / 2,
puckHeight * (2 + concave_puck) / 3,
(puckHeight * (2 + concave_puck)) / 3,
puckWidth,
puckHeight * 0.75,
);
@ -865,7 +874,7 @@ export function drawBrick(
x: number,
y: number,
offset: number = 0,
borderOnly: boolean
borderOnly: boolean,
) {
const tlx = Math.ceil(x - gameState.brickWidth / 2);
const tly = Math.ceil(y - gameState.brickWidth / 2);
@ -874,7 +883,18 @@ export function drawBrick(
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]) {
const can = document.createElement("canvas");

8
src/types.d.ts vendored
View file

@ -238,8 +238,12 @@ export type GameState = {
coins: ReusableArray<Coin>;
// Bricks that should respawn destroyed
respawns: ReusableArray<{ index: number; color: string ; time:number;
destroyed?: boolean;}>;
respawns: ReusableArray<{
index: number;
color: string;
time: number;
destroyed?: boolean;
}>;
levelStartScore: number;
levelMisses: number;

View file

@ -289,7 +289,10 @@ export const rawUpgrades = [
id: "soft_reset",
max: 3,
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"),
},
{
@ -368,7 +371,10 @@ export const rawUpgrades = [
max: 4,
name: t("upgrades.respawn.name"),
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"),
},
{
@ -433,9 +439,10 @@ export const rawUpgrades = [
id: "unbounded",
max: 1,
name: t("upgrades.unbounded.name"),
help: (lvl: number) => lvl > 1 ?
t("upgrades.unbounded.help_no_ceiling",{lvl}):
t("upgrades.unbounded.help",{lvl}),
help: (lvl: number) =>
lvl > 1
? t("upgrades.unbounded.help_no_ceiling", { lvl })
: t("upgrades.unbounded.help", { lvl }),
fullHelp: t("upgrades.unbounded.fullHelp"),
},
{
@ -446,7 +453,10 @@ export const rawUpgrades = [
id: "shunt",
max: 3,
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"),
},
{
@ -510,9 +520,9 @@ export const rawUpgrades = [
max: 1,
name: t("upgrades.sacrifice.name"),
help: (lvl: number) =>
lvl==1 ?
t("upgrades.sacrifice.help_l1"):
t("upgrades.sacrifice.help_over",{lvl}),
lvl == 1
? t("upgrades.sacrifice.help_l1")
: t("upgrades.sacrifice.help_over", { lvl }),
fullHelp: t("upgrades.sacrifice.fullHelp"),
},