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

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

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

@ -1,4 +1,4 @@
import {allLevels, appVersion, icons, upgrades} from "./loadGameData";
import { allLevels, appVersion, icons, upgrades } from "./loadGameData";
import {
Ball,
Coin,
@ -11,11 +11,17 @@ import {
TextFlash,
Upgrade,
} from "./types";
import {getAudioContext, playPendingSounds} from "./sounds";
import {currentLevelInfo, getRowColIndex, levelsListHTMl, max_levels, pickedUpgradesHTMl,} from "./game_utils";
import { getAudioContext, playPendingSounds } from "./sounds";
import {
currentLevelInfo,
getRowColIndex,
levelsListHTMl,
max_levels,
pickedUpgradesHTMl,
} from "./game_utils";
import "./PWA/sw_loader";
import {getCurrentLang, t} from "./i18n/i18n";
import { getCurrentLang, t } from "./i18n/i18n";
import {
cycleMaxCoins,
cycleMaxParticles,
@ -34,13 +40,30 @@ import {
setLevel,
setMousePos,
} from "./gameStateMutators";
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 {isOptionOn, options, toggleOption} from "./options";
import {hashCode} from "./getLevelBackground";
import {premiumMenuEntry} from "./premium";
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 { isOptionOn, options, toggleOption } from "./options";
import { hashCode } from "./getLevelBackground";
import { premiumMenuEntry } from "./premium";
export function play() {
if (applyFullScreenChoice()) return;
@ -93,7 +116,7 @@ export const fitSize = () => {
past_width = gameState.gameZoneWidthRoundedUp,
past_heigh = gameState.gameZoneHeight;
const {width, height} = gameCanvas.getBoundingClientRect();
const { width, height } = gameCanvas.getBoundingClientRect();
gameState.canvasWidth = width;
gameState.canvasHeight = height;
gameCanvas.width = width;
@ -155,19 +178,16 @@ window.addEventListener("fullscreenchange", fitSize);
setInterval(() => {
// 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)
fitSize();
}, 1000);
export async function openUpgradesPicker(gameState: GameState) {
const catchRate =
(gameState.score - gameState.levelStartScore) /
(gameState.levelSpawnedCoins || 1);
let repeats = 1;
let timeGain = "",
@ -222,7 +242,7 @@ export async function openUpgradesPicker(gameState: GameState) {
if (gameState.rerolls)
actions.push({
text: t("level_up.reroll", {count: gameState.rerolls}),
text: t("level_up.reroll", { count: gameState.rerolls }),
help: t("level_up.reroll_help"),
value: "reroll" as const,
icon: icons["icon:reroll"],
@ -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,
});
@ -467,7 +487,7 @@ export async function openMainMenu() {
text: t("main_menu.normal"),
help: t("main_menu.normal_help"),
value: () => {
restart({levelToAvoid: currentLevelInfo(gameState).name});
restart({ levelToAvoid: currentLevelInfo(gameState).name });
},
},
{
@ -483,7 +503,7 @@ export async function openMainMenu() {
text: t("sandbox.title"),
help:
getTotalScore() < creativeModeThreshold
? t("sandbox.unlocks_at", {score: creativeModeThreshold})
? t("sandbox.unlocks_at", { score: creativeModeThreshold })
: t("sandbox.help"),
disabled: getTotalScore() < creativeModeThreshold,
async value() {
@ -515,7 +535,7 @@ export async function openMainMenu() {
}))
) {
if (choice === "start") {
restart({perks: creativeModePerks});
restart({ perks: creativeModePerks });
break;
} else if (choice) {
creativeModePerks[choice.id] =
@ -539,7 +559,7 @@ export async function openMainMenu() {
const cb = await asyncAlert<() => void>({
title: t("main_menu.title"),
content: [...actions, t("main_menu.footer_html", {appVersion})],
content: [...actions, t("main_menu.footer_html", { appVersion })],
allowClose: true,
});
if (cb) {
@ -564,7 +584,7 @@ async function openSettingsMenu() {
value: () => {
toggleOption(key);
fitSize();
applyFullScreenChoice()
applyFullScreenChoice();
openSettingsMenu();
},
});
@ -706,7 +726,7 @@ async function openSettingsMenu() {
title: t("main_menu.save_file_loaded"),
content: [
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();
@ -716,7 +736,7 @@ async function openSettingsMenu() {
title: t("main_menu.save_file_error"),
content: [
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({
text: t("main_menu.max_coins", {max: getCurrentMaxCoins()}),
text: t("main_menu.max_coins", { max: getCurrentMaxCoins() }),
help: t("main_menu.max_coins_help"),
async value() {
cycleMaxCoins();
@ -767,7 +787,7 @@ async function openSettingsMenu() {
},
});
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"),
async value() {
cycleMaxParticles();
@ -786,52 +806,48 @@ 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
.sort((a, b) => a.threshold - b.threshold)
.map(({name, id, threshold, icon, help}) => ({
.map(({ name, id, threshold, icon, help }) => ({
text: name,
// help:
// ts >= threshold ? help(1) : t("unlocks.unlocks_at", { threshold }),
disabled: ts < threshold,
value: {perks: {[id]: 1}} as RunParams,
value: { perks: { [id]: 1 } } as RunParams,
icon,
}))
}));
const levelActions = allLevels
.sort((a, b) => a.threshold - b.threshold)
@ -846,24 +862,24 @@ async function openUnlocksList() {
// })
// : t("unlocks.unlocks_at", { threshold: l.threshold }),
disabled: !available,
value: {level: l.name} as RunParams,
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}),
title: t("unlocks.title", { percentUnlock }),
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> `,
...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);
}
@ -953,7 +968,7 @@ document.addEventListener("keyup", async (e) => {
openScorePanel().then();
} else if (e.key.toLowerCase() === "r" && !alertsOpen) {
if (await confirmRestart(gameState)) {
restart({levelToAvoid: currentLevelInfo(gameState).name});
restart({ levelToAvoid: currentLevelInfo(gameState).name });
}
} else {
return;
@ -970,21 +985,19 @@ export function restart(params: RunParams) {
setLevel(gameState, 0);
}
restart(
(window.location.search.includes("stressTest") && {
level: "Bird",
perks: {
shocks:10,
multiball:6,
telekinesis:2,
ghost_coins:1,
pierce:4,
clairvoyant:3,
bigger_explosions:2,
sapper:2,
unbounded:1
shocks: 10,
multiball: 6,
telekinesis: 2,
ghost_coins: 1,
pierce: 4,
clairvoyant: 3,
bigger_explosions: 2,
sapper: 2,
unbounded: 1,
},
levelsPerLoop: 2,
}) ||

View file

@ -28,19 +28,31 @@ import {
max_levels,
shouldPierceByColor,
} from "./game_utils";
import {t} from "./i18n/i18n";
import {icons, upgrades} from "./loadGameData";
import { t } from "./i18n/i18n";
import { icons, upgrades } from "./loadGameData";
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 {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";
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 { 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) {
gameState.puckPosition = x;
@ -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++;
@ -366,7 +384,7 @@ export function explodeBrick(
while (coinsToSpawn > 0) {
const points = Math.min(pointsPerCoin, coinsToSpawn);
if (points < 0 || isNaN(points)) {
console.error({points});
console.error({ points });
debugger;
}
@ -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;
});
}
}
}
@ -564,7 +586,7 @@ export async function gotoNextLoop(gameState: GameState) {
let comboText = "";
if (gameState.rerolls) {
comboText = t("loop.converted_rerolls", {n: gameState.rerolls});
comboText = t("loop.converted_rerolls", { n: gameState.rerolls });
gameState.baseCombo += gameState.rerolls;
gameState.rerolls = 0;
} else {
@ -574,12 +596,12 @@ export async function gotoNextLoop(gameState: GameState) {
const userPerks = upgrades.filter((u) => gameState.perks[u.id]);
const keep = await requiredAsyncAlert<PerkId>({
title: t("loop.title", {loop: gameState.loop}),
title: t("loop.title", { loop: gameState.loop }),
content: [
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;
}
@ -792,7 +814,7 @@ export function attract(gameState: GameState, a: Ball, b: Ball, power: number) {
export function coinBrickHitCheck(gameState: GameState, coin: Coin) {
// Make ball/coin bonce, and return bricks that were hit
const radius = coin.size / 2;
const {x, y, previousX, previousY} = coin;
const { x, y, previousX, previousY } = coin;
const vhit = hitsSomething(previousX, y, radius);
const hhit = hitsSomething(x, previousY, radius);
@ -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) {
@ -960,7 +980,7 @@ export function gameStateTick(
} else {
gameOver(
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 =
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,12 +1299,12 @@ 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 { index, color } = r;
const vertical = Math.random() > 0.5;
const dx = Math.random() > 0.5 ? 1 : -1;
const dy = Math.random() > 0.5 ? 1 : -1;
@ -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,28 +1525,27 @@ 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++;
if (!gameState.balls.find((b) => !b.destroyed)) {
gameOver(
t("gameOver.lost.title"),
t("gameOver.lost.summary", {score: gameState.score}),
t("gameOver.lost.summary", { score: gameState.score }),
);
}
}
const radius = gameState.ballSize / 2;
// 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 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) {
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;
});
}
@ -1772,7 +1791,7 @@ export function append<T>(
makeItem(where.list[where.indexMin]);
where.indexMin++;
} else {
const p = {destroyed: false};
const p = { destroyed: false };
makeItem(p);
where.list.push(p);
}
@ -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++
if (!i.destroyed) {
i.destroyed = true;
destroyed++;
}
});
return destroyed
return destroyed;
}
export function forEachLiveOne<T>(

View file

@ -1,6 +1,6 @@
import {Ball, GameState, PerkId, PerksMap} from "./types";
import {icons, upgrades} from "./loadGameData";
import {t} from "./i18n/i18n";
import { Ball, GameState, PerkId, PerksMap } from "./types";
import { icons, upgrades } from "./loadGameData";
import { t } from "./i18n/i18n";
export function getMajorityValue(arr: string[]): string {
const count: { [k: string]: number } = {};
@ -14,10 +14,8 @@ export function sample<T>(arr: T[]): T {
return arr[Math.floor(arr.length * Math.random())];
}
export function sampleN<T>(arr: T[],n:number): T[] {
return [...arr].sort(()=>Math.random()-0.5)
.slice(0,n)
export function sampleN<T>(arr: T[], n: number): T[] {
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,
@ -9,10 +9,10 @@ import {
isYoyoActive,
max_levels,
} from "./game_utils";
import {colorString, GameState} from "./types";
import {t} from "./i18n/i18n";
import {gameState, lastMeasuredFPS} from "./game";
import {isOptionOn} from "./options";
import { colorString, GameState } from "./types";
import { t } from "./i18n/i18n";
import { gameState, lastMeasuredFPS } from "./game";
import { isOptionOn } from "./options";
export const gameCanvas = document.getElementById("game") as HTMLCanvasElement;
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">
<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");
@ -33,7 +33,7 @@ export function render(gameState: GameState) {
const level = currentLevelInfo(gameState);
const hasCombo = gameState.combo > baseCombo(gameState);
const {width, height} = gameCanvas;
const { width, height } = gameCanvas;
if (!width || !height) return;
if (gameState.currentLevel || gameState.levelTime) {
@ -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'}">
scoreDisplay.innerHTML =
(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')||''}">
${Math.floor(catchRate*100)}%
`
: "") +
(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')||''}">
${Math.ceil(gameState.levelTime/1000)}s
<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" : "";
@ -122,7 +125,7 @@ export function render(gameState: GameState) {
ctx.globalAlpha = 1;
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;
ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2);
drawFuzzyBall(ctx, color, size * 3, x, y);
@ -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;
@ -185,7 +189,7 @@ export function render(gameState: GameState) {
ctx.fillStyle = level.color || "#000";
ctx.fillRect(0, 0, width, height);
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;
ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2);
drawBall(ctx, color, size, x, y);
@ -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";
@ -250,7 +253,7 @@ export function render(gameState: GameState) {
ctx.globalCompositeOperation = "screen";
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;
ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2) * 0.5;
drawBrick(ctx, color, x, y, -1, gameState.perks.clairvoyant >= 2);
@ -258,7 +261,7 @@ export function render(gameState: GameState) {
ctx.globalCompositeOperation = "screen";
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;
ctx.globalAlpha = Math.max(0, Math.min(1, 2 - (elapsed / duration) * 2));
ctx.globalCompositeOperation = "source-over";
@ -266,7 +269,7 @@ export function render(gameState: GameState) {
});
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;
ctx.globalAlpha = Math.max(0, Math.min(1, 2 - (elapsed / duration) * 2));
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(
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");

14
src/types.d.ts vendored
View file

@ -83,7 +83,7 @@ export type Coin = {
weight: number;
destroyed?: boolean;
collidedLastFrame?: boolean;
metamorphosisPoints:number;
metamorphosisPoints: number;
};
export type Ball = {
x: number;
@ -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;
@ -280,14 +284,14 @@ export type GameState = {
rerolls: number;
loop: number;
baseCombo: number;
levelsPerLoop:number;
levelsPerLoop: number;
};
export type RunParams = {
level?: string;
levelToAvoid?: string;
perks?: Partial<PerksMap>;
levelsPerLoop?:number;
levelsPerLoop?: number;
};
export type OptionDef = {
default: boolean;

View file

@ -1,6 +1,6 @@
import { t } from "./i18n/i18n";
import {comboKeepingRate} from "./pure_functions";
import { comboKeepingRate } from "./pure_functions";
export const rawUpgrades = [
{
@ -49,7 +49,7 @@ export const rawUpgrades = [
id: "slow_down",
max: 2,
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"),
},
{
@ -84,7 +84,7 @@ export const rawUpgrades = [
max: 1,
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"),
},
{
@ -95,7 +95,7 @@ export const rawUpgrades = [
giftable: true,
max: 1,
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"),
},
{
@ -106,7 +106,7 @@ export const rawUpgrades = [
giftable: true,
max: 1,
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"),
},
{
@ -195,7 +195,7 @@ export const rawUpgrades = [
giftable: true,
max: 1,
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"),
},
{
@ -206,7 +206,7 @@ export const rawUpgrades = [
id: "metamorphosis",
max: 1,
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"),
},
{
@ -217,7 +217,7 @@ export const rawUpgrades = [
giftable: true,
max: 1,
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"),
},
{
@ -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"),
},
{
@ -354,7 +357,7 @@ export const rawUpgrades = [
name: t("upgrades.sturdy_bricks.name"),
help: (lvl: number) =>
// 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"),
fullHelp: t("upgrades.sturdy_bricks.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"),
},
{
@ -378,7 +384,7 @@ export const rawUpgrades = [
id: "one_more_choice",
max: 3,
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"),
},
{
@ -390,7 +396,7 @@ export const rawUpgrades = [
max: 2,
adventure: false,
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"),
},
{
@ -422,7 +428,7 @@ export const rawUpgrades = [
id: "asceticism",
max: 1,
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"),
},
{
@ -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"),
},
{
@ -499,7 +509,7 @@ export const rawUpgrades = [
id: "zen",
max: 1,
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"),
},
{
@ -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"),
},