Build 29079087

This commit is contained in:
Renan LE CARO 2025-04-15 21:28:00 +02:00
parent 06843047d2
commit 8e4e67e33b
17 changed files with 199 additions and 234 deletions

View file

@ -32,7 +32,8 @@ languages, I may add features again.
- measured and improve the performance (test here https://breakout.lecaro.me/?stresstest) - measured and improve the performance (test here https://breakout.lecaro.me/?stresstest)
- added a few levels - added a few levels
- autoplay mode (with wake lock and computer play https://breakout.lecaro.me/?autoplay ) - autoplay mode (with wake lock and computer play https://breakout.lecaro.me/?autoplay )
- slower coins fall once they are past the paddle - Added particle and sound effect when coin drops below the "waterline" of the puck
- slower coins fall once they are under the paddle
- in game level editor - in game level editor
- allow loading newer save in outdated app (for rollback) - allow loading newer save in outdated app (for rollback)
- game crashes when reaching level 12 (no level info in runLevels) - game crashes when reaching level 12 (no level info in runLevels)

View file

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

111
dist/index.html vendored

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,5 @@
// The version of the cache. // The version of the cache.
const VERSION = "29077593"; const VERSION = "29079087";
// 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 @@
"29077593" "29079087"

View file

@ -521,7 +521,6 @@ h2.histogram-title strong {
} }
} }
.toast { .toast {
position: fixed; position: fixed;
left: 0; left: 0;
@ -535,18 +534,19 @@ h2.histogram-title strong {
border-radius: 2px; border-radius: 2px;
padding-right: 10px; padding-right: 10px;
pointer-events: none; pointer-events: none;
transition: opacity 200ms, transform 200ms; transition:
&.hidden{ opacity 200ms,
transform 200ms;
&.hidden {
opacity: 0; opacity: 0;
transform: translate(-20px, -20px) scale(0.5); transform: translate(-20px, -20px) scale(0.5);
} }
&.visible{ &.visible {
opacity: 0.8; opacity: 0.8;
transform: none; transform: none;
} }
} }
.gridEdit > div > span, .gridEdit > div > span,
.palette > span { .palette > span {
display: inline-flex; display: inline-flex;

View file

@ -27,7 +27,8 @@ import {
max_levels, max_levels,
pickedUpgradesHTMl, pickedUpgradesHTMl,
reasonLevelIsLocked, reasonLevelIsLocked,
sample, sumOfValues, sample,
sumOfValues,
} from "./game_utils"; } from "./game_utils";
import "./PWA/sw_loader"; import "./PWA/sw_loader";
@ -41,7 +42,8 @@ import {
} from "./settings"; } from "./settings";
import { import {
forEachLiveOne, forEachLiveOne,
gameStateTick, liveCount, gameStateTick,
liveCount,
normalizeGameState, normalizeGameState,
pickRandomUpgrades, pickRandomUpgrades,
setLevel, setLevel,
@ -420,9 +422,8 @@ export function hitsSomething(x: number, y: number, radius: number) {
); );
} }
export function tick() { export function tick() {
startWork('tick init') startWork("tick init");
const currentTick = performance.now(); const currentTick = performance.now();
const timeDeltaMs = currentTick - gameState.lastTick; const timeDeltaMs = currentTick - gameState.lastTick;
@ -443,9 +444,9 @@ startWork('tick init')
); );
} }
startWork('normalizeGameState') startWork("normalizeGameState");
normalizeGameState(gameState); normalizeGameState(gameState);
startWork('gameStateTick') startWork("gameStateTick");
if (gameState.running) { if (gameState.running) {
gameState.levelTime += timeDeltaMs * frames; gameState.levelTime += timeDeltaMs * frames;
gameState.runStatistics.runTime += timeDeltaMs * frames; gameState.runStatistics.runTime += timeDeltaMs * frames;
@ -456,15 +457,15 @@ startWork('gameStateTick')
gameState.needsRender = false; gameState.needsRender = false;
render(gameState); render(gameState);
} }
startWork('recordOneFrame') startWork("recordOneFrame");
if (gameState.running) { if (gameState.running) {
recordOneFrame(gameState); recordOneFrame(gameState);
} }
startWork('playPendingSounds') startWork("playPendingSounds");
if (isOptionOn("sound")) { if (isOptionOn("sound")) {
playPendingSounds(gameState); playPendingSounds(gameState);
} }
startWork('idle') startWork("idle");
requestAnimationFrame(tick); requestAnimationFrame(tick);
FPSCounter++; FPSCounter++;
@ -477,28 +478,41 @@ setInterval(() => {
FPSCounter = 0; FPSCounter = 0;
}, 1000); }, 1000);
const showStats= window.location.search.includes("stress") const showStats = window.location.search.includes("stress");
let total={} let total = {};
let lastTick=performance.now(); let lastTick = performance.now();
let doing= '' let doing = "";
export function startWork(what){ export function startWork(what) {
if(!showStats) return if (!showStats) return;
const newNow=performance.now(); const newNow = performance.now();
if(doing) { if (doing) {
total[doing] = (total[doing]||0) + ( newNow-lastTick ) total[doing] = (total[doing] || 0) + (newNow - lastTick);
} }
lastTick=newNow lastTick = newNow;
doing=what doing = what;
} }
if(showStats) if (showStats)
setInterval(()=>{ setInterval(() => {
const totalTime = sumOfValues(total) const totalTime = sumOfValues(total);
console.debug( console.debug(
liveCount(gameState.coins) +' coins\n'+ liveCount(gameState.coins) +
Object.entries(total).sort((a,b)=>b[1]-a[1]).filter(a=>a[1]>1).map(t=>t[0]+':'+(t[1]/totalTime*100).toFixed(2)+'% ('+t[1]+'ms)').join('\n')) " coins\n" +
total={} Object.entries(total)
},2000) .sort((a, b) => b[1] - a[1])
.filter((a) => a[1] > 1)
.map(
(t) =>
t[0] +
":" +
((t[1] / totalTime) * 100).toFixed(2) +
"% (" +
t[1] +
"ms)",
)
.join("\n"),
);
total = {};
}, 2000);
setInterval(() => { setInterval(() => {
monitorLevelsUnlocks(gameState); monitorLevelsUnlocks(gameState);
@ -1041,22 +1055,26 @@ export function restart(params: RunParams) {
play(); play();
} }
} }
if (window.location.search.match(/autoplay|stress/) ) { if (window.location.search.match(/autoplay|stress/)) {
startComputerControlledGame(); startComputerControlledGame();
if(!isOptionOn('show_fps')) if (!isOptionOn("show_fps")) toggleOption("show_fps");
toggleOption('show_fps')
} else { } else {
restart({}); restart({});
} }
export function startComputerControlledGame() { export function startComputerControlledGame() {
const perks: Partial<PerksMap> = { base_combo: 20, pierce: 3 }; const perks: Partial<PerksMap> = { base_combo: 20, pierce: 3 };
if(window.location.search.includes("stress")){ if (window.location.search.includes("stress")) {
Object.assign(perks, {
Object.assign(perks,{base_combo:5000, pierce:20, rainbow:3, sapper:2, etherealcoins:1, bricks_attract_ball:1, respawn:3}) base_combo: 5000,
pierce: 20,
}else{ rainbow: 3,
sapper: 2,
etherealcoins: 1,
bricks_attract_ball: 1,
respawn: 3,
});
} else {
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
const u = sample(upgrades); const u = sample(upgrades);

View file

@ -9,7 +9,7 @@ import {
pickedUpgradesHTMl, pickedUpgradesHTMl,
reasonLevelIsLocked, reasonLevelIsLocked,
} from "./game_utils"; } from "./game_utils";
import {getSettingValue, getTotalScore, setSettingValue} from "./settings"; import { getSettingValue, getTotalScore, setSettingValue } from "./settings";
import { stopRecording } from "./recording"; import { stopRecording } from "./recording";
import { asyncAlert } from "./asyncAlert"; import { asyncAlert } from "./asyncAlert";
import { rawUpgrades } from "./upgrades"; import { rawUpgrades } from "./upgrades";
@ -17,7 +17,10 @@ import { run } from "jest";
import { editRawLevelList } from "./levelEditor"; import { editRawLevelList } from "./levelEditor";
export function addToTotalPlayTime(ms: number) { export function addToTotalPlayTime(ms: number) {
setSettingValue('breakout_71_total_play_time', getSettingValue('breakout_71_total_play_time',0)+ms) setSettingValue(
"breakout_71_total_play_time",
getSettingValue("breakout_71_total_play_time", 0) + ms,
);
} }
export function gameOver(title: string, intro: string) { export function gameOver(title: string, intro: string) {

View file

@ -461,11 +461,11 @@ export function explodeBrick(
gameState.runStatistics.coins_spawned += coinsToSpawn; gameState.runStatistics.coins_spawned += coinsToSpawn;
gameState.runStatistics.bricks_broken++; gameState.runStatistics.bricks_broken++;
const maxCoins = getCurrentMaxCoins() const maxCoins = getCurrentMaxCoins();
const spawnableCoins = const spawnableCoins =
liveCount(gameState.coins) > getCurrentMaxCoins() liveCount(gameState.coins) > getCurrentMaxCoins()
? 1 ? 1
: Math.floor((maxCoins - liveCount(gameState.coins)) /2) ; : Math.floor((maxCoins - liveCount(gameState.coins)) / 2);
const pointsPerCoin = Math.max(1, Math.ceil(coinsToSpawn / spawnableCoins)); const pointsPerCoin = Math.max(1, Math.ceil(coinsToSpawn / spawnableCoins));
@ -1218,10 +1218,28 @@ 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);
if(coin.previousY<gameState.gameZoneHeight && coin.y>gameState.gameZoneHeight && coin.vy>0 && speed > 20) { if (
schedulGameSound(gameState, "plouf", coin.x, clamp(speed, 20,100)/100*0.2); coin.previousY < gameState.gameZoneHeight &&
if(!isOptionOn('basic')){ coin.y > gameState.gameZoneHeight &&
makeParticle(gameState, coin.x,gameState.gameZoneHeight, -coin.vx/5, -coin.vy/5, coin.color, false ) coin.vy > 0 &&
speed > 20
) {
schedulGameSound(
gameState,
"plouf",
coin.x,
(clamp(speed, 20, 100) / 100) * 0.2,
);
if (!isOptionOn("basic")) {
makeParticle(
gameState,
coin.x,
gameState.gameZoneHeight,
-coin.vx / 5,
-coin.vy / 5,
coin.color,
false,
);
} }
} }

View file

@ -1,4 +1,4 @@
import {getSettingValue} from "./settings"; import { getSettingValue } from "./settings";
export function clamp(value: number, min: number, max: number) { export function clamp(value: number, min: number, max: number) {
return Math.max(min, Math.min(value, max)); return Math.max(min, Math.min(value, max));
@ -10,7 +10,7 @@ export function comboKeepingRate(level: number) {
export function hoursSpentPlaying() { export function hoursSpentPlaying() {
try { try {
const timePlayed = getSettingValue('breakout_71_total_play_time',0) const timePlayed = getSettingValue("breakout_71_total_play_time", 0);
return Math.floor(timePlayed / 1000 / 60 / 60); return Math.floor(timePlayed / 1000 / 60 / 60);
} catch (e) { } catch (e) {
return 0; return 0;

View file

@ -12,7 +12,6 @@ let mediaRecorder: MediaRecorder | null,
recordCanvasCtx: CanvasRenderingContext2D; recordCanvasCtx: CanvasRenderingContext2D;
export function recordOneFrame(gameState: GameState) { export function recordOneFrame(gameState: GameState) {
if (!isOptionOn("record")) { if (!isOptionOn("record")) {
return; return;
} }

View file

@ -13,7 +13,7 @@ import {
} from "./game_utils"; } from "./game_utils";
import { Coin, colorString, GameState } from "./types"; import { Coin, colorString, GameState } from "./types";
import { t } from "./i18n/i18n"; import { t } from "./i18n/i18n";
import {gameState, lastMeasuredFPS, startWork} from "./game"; import { gameState, lastMeasuredFPS, startWork } from "./game";
import { isOptionOn } from "./options"; import { isOptionOn } from "./options";
import { import {
catchRateBest, catchRateBest,
@ -25,7 +25,7 @@ import {
wallBouncedBest, wallBouncedBest,
wallBouncedGood, wallBouncedGood,
} from "./pure_functions"; } from "./pure_functions";
import {getCurrentMaxCoins} from "./settings"; import { getCurrentMaxCoins } from "./settings";
export const gameCanvas = document.getElementById("game") as HTMLCanvasElement; export const gameCanvas = document.getElementById("game") as HTMLCanvasElement;
export const ctx = gameCanvas.getContext("2d", { export const ctx = gameCanvas.getContext("2d", {
@ -52,7 +52,7 @@ const haloCanvasCtx = haloCanvas.getContext("2d", {
export const haloScale = 16; export const haloScale = 16;
export function render(gameState: GameState) { export function render(gameState: GameState) {
startWork('render:init') startWork("render:init");
const level = currentLevelInfo(gameState); const level = currentLevelInfo(gameState);
const hasCombo = gameState.combo > baseCombo(gameState); const hasCombo = gameState.combo > baseCombo(gameState);
@ -72,12 +72,12 @@ startWork('render:init')
? (gameState.levelSpawnedCoins - gameState.levelLostCoins) / ? (gameState.levelSpawnedCoins - gameState.levelLostCoins) /
gameState.levelSpawnedCoins gameState.levelSpawnedCoins
: 1; : 1;
startWork('render:scoreDisplay') startWork("render:scoreDisplay");
scoreDisplay.innerHTML = scoreDisplay.innerHTML =
(isOptionOn("show_fps") || gameState.computer_controlled (isOptionOn("show_fps") || gameState.computer_controlled
? ` ? `
<span> <span>
${Math.floor(liveCount(gameState.coins) / getCurrentMaxCoins() * 100)} % ${Math.floor((liveCount(gameState.coins) / getCurrentMaxCoins()) * 100)} %
</span><span> / </span> </span><span> / </span>
<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
@ -109,8 +109,7 @@ startWork('render:scoreDisplay')
""; "";
// Clear // Clear
if (!isOptionOn("basic") && level.svg && level.color === "#000000") { if (!isOptionOn("basic") && level.svg && level.color === "#000000") {
startWork("render:halo:clear");
startWork('render:halo:clear')
haloCanvasCtx.globalCompositeOperation = "source-over"; haloCanvasCtx.globalCompositeOperation = "source-over";
haloCanvasCtx.globalAlpha = 0.99; haloCanvasCtx.globalAlpha = 0.99;
haloCanvasCtx.fillStyle = level.color; haloCanvasCtx.fillStyle = level.color;
@ -120,7 +119,7 @@ startWork('render:scoreDisplay')
haloCanvasCtx.globalCompositeOperation = "lighten"; haloCanvasCtx.globalCompositeOperation = "lighten";
haloCanvasCtx.globalAlpha = haloCanvasCtx.globalAlpha =
0.1 + (0.5 * 10) / (liveCount(gameState.coins) + 10); 0.1 + (0.5 * 10) / (liveCount(gameState.coins) + 10);
startWork('render:halo:coins') startWork("render:halo:coins");
forEachLiveOne(gameState.coins, (coin) => { forEachLiveOne(gameState.coins, (coin) => {
const color = getCoinRenderColor(gameState, coin); const color = getCoinRenderColor(gameState, coin);
drawFuzzyBall( drawFuzzyBall(
@ -132,7 +131,7 @@ startWork('render:scoreDisplay')
); );
}); });
startWork('render:halo:balls') startWork("render:halo:balls");
gameState.balls.forEach((ball) => { gameState.balls.forEach((ball) => {
haloCanvasCtx.globalAlpha = 0.3 * (1 - ballTransparency(ball, gameState)); haloCanvasCtx.globalAlpha = 0.3 * (1 - ballTransparency(ball, gameState));
drawFuzzyBall( drawFuzzyBall(
@ -144,7 +143,7 @@ startWork('render:scoreDisplay')
); );
}); });
startWork('render:halo:bricks') startWork("render:halo:bricks");
haloCanvasCtx.globalAlpha = 0.05; haloCanvasCtx.globalAlpha = 0.05;
gameState.bricks.forEach((color, index) => { gameState.bricks.forEach((color, index) => {
if (!color) return; if (!color) return;
@ -160,7 +159,7 @@ startWork('render:scoreDisplay')
); );
}); });
startWork('render:halo:particles') startWork("render:halo:particles");
haloCanvasCtx.globalCompositeOperation = "screen"; haloCanvasCtx.globalCompositeOperation = "screen";
forEachLiveOne(gameState.particles, (flash) => { forEachLiveOne(gameState.particles, (flash) => {
const { x, y, time, color, size, duration } = flash; const { x, y, time, color, size, duration } = flash;
@ -184,7 +183,7 @@ startWork('render:scoreDisplay')
ctx.drawImage(haloCanvas, 0, 0, width, height); ctx.drawImage(haloCanvas, 0, 0, width, height);
ctx.imageSmoothingEnabled = false; ctx.imageSmoothingEnabled = false;
startWork('render:halo:pattern') startWork("render:halo:pattern");
ctx.globalAlpha = 1; ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "multiply"; ctx.globalCompositeOperation = "multiply";
if (level.svg && background.width && background.complete) { if (level.svg && background.width && background.complete) {
@ -236,8 +235,7 @@ startWork('render:scoreDisplay')
ctx.fillRect(0, 0, width, height); ctx.fillRect(0, 0, width, height);
} }
} else { } else {
startWork("render:halo-basic");
startWork('render:halo-basic')
ctx.globalAlpha = 1; ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over"; ctx.globalCompositeOperation = "source-over";
ctx.fillStyle = level.color || "#000"; ctx.fillStyle = level.color || "#000";
@ -250,7 +248,7 @@ startWork('render:scoreDisplay')
}); });
} }
startWork('render:explosionshake') startWork("render:explosionshake");
ctx.globalAlpha = 1; ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over"; ctx.globalCompositeOperation = "source-over";
const lastExplosionDelay = Date.now() - gameState.lastExplosion + 5; const lastExplosionDelay = Date.now() - gameState.lastExplosion + 5;
@ -263,7 +261,7 @@ startWork('render:explosionshake')
Math.sin(Date.now() + 36) * amplitude, Math.sin(Date.now() + 36) * amplitude,
); );
} }
startWork('render:coins') startWork("render:coins");
// Coins // Coins
ctx.globalAlpha = 1; ctx.globalAlpha = 1;
forEachLiveOne(gameState.coins, (coin) => { forEachLiveOne(gameState.coins, (coin) => {
@ -286,7 +284,7 @@ startWork('render:coins')
coin.a, coin.a,
); );
}); });
startWork('render:ball shade') startWork("render:ball shade");
// Black shadow around balls // Black shadow around balls
if (!isOptionOn("basic")) { if (!isOptionOn("basic")) {
ctx.globalCompositeOperation = "source-over"; ctx.globalCompositeOperation = "source-over";
@ -304,11 +302,11 @@ startWork('render:coins')
); );
}); });
} }
startWork('render:bricks') startWork("render:bricks");
ctx.globalCompositeOperation = "source-over"; ctx.globalCompositeOperation = "source-over";
renderAllBricks(); renderAllBricks();
startWork('render:lights') startWork("render:lights");
ctx.globalCompositeOperation = "screen"; ctx.globalCompositeOperation = "screen";
forEachLiveOne(gameState.lights, (flash) => { forEachLiveOne(gameState.lights, (flash) => {
const { x, y, time, color, size, duration } = flash; const { x, y, time, color, size, duration } = flash;
@ -325,7 +323,7 @@ startWork('render:lights')
); );
}); });
startWork('render:texts') startWork("render:texts");
ctx.globalCompositeOperation = "screen"; ctx.globalCompositeOperation = "screen";
forEachLiveOne(gameState.texts, (flash) => { forEachLiveOne(gameState.texts, (flash) => {
const { x, y, time, color, size, duration } = flash; const { x, y, time, color, size, duration } = flash;
@ -335,7 +333,7 @@ startWork('render:texts')
drawText(ctx, flash.text, color, size, x, y - elapsed / 10); drawText(ctx, flash.text, color, size, x, y - elapsed / 10);
}); });
startWork('render:particles') startWork("render:particles");
forEachLiveOne(gameState.particles, (particle) => { forEachLiveOne(gameState.particles, (particle) => {
const { x, y, time, color, size, duration } = particle; const { x, y, time, color, size, duration } = particle;
const elapsed = gameState.levelTime - time; const elapsed = gameState.levelTime - time;
@ -344,7 +342,7 @@ startWork('render:particles')
drawBall(ctx, color, size, x, y); drawBall(ctx, color, size, x, y);
}); });
startWork('render:extra_life') startWork("render:extra_life");
if (gameState.perks.extra_life) { if (gameState.perks.extra_life) {
ctx.globalAlpha = 1; ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over"; ctx.globalCompositeOperation = "source-over";
@ -359,7 +357,7 @@ startWork('render:extra_life')
} }
} }
startWork('render:balls') startWork("render:balls");
ctx.globalAlpha = 1; ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over"; ctx.globalCompositeOperation = "source-over";
gameState.balls.forEach((ball) => { gameState.balls.forEach((ball) => {
@ -411,7 +409,7 @@ startWork('render:balls')
} }
}); });
startWork('render:puck') startWork("render:puck");
ctx.globalAlpha = 1; ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over"; ctx.globalCompositeOperation = "source-over";
drawPuck( drawPuck(
@ -424,7 +422,7 @@ startWork('render:balls')
gameState.perks.streak_shots && hasCombo ? getDashOffset(gameState) : -1, gameState.perks.streak_shots && hasCombo ? getDashOffset(gameState) : -1,
); );
startWork('render:combotext') startWork("render:combotext");
if (gameState.combo > 1) { if (gameState.combo > 1) {
ctx.globalCompositeOperation = "source-over"; ctx.globalCompositeOperation = "source-over";
const comboText = "x " + gameState.combo; const comboText = "x " + gameState.combo;
@ -464,7 +462,7 @@ startWork('render:balls')
); );
} }
} }
startWork('render:Borders') startWork("render:Borders");
// Borders // Borders
ctx.globalCompositeOperation = "source-over"; ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = 1; ctx.globalAlpha = 1;
@ -548,7 +546,7 @@ startWork('render:balls')
1, 1,
); );
startWork('render:contrast') startWork("render:contrast");
if ( if (
!isOptionOn("basic") && !isOptionOn("basic") &&
isOptionOn("contrast") && isOptionOn("contrast") &&
@ -569,7 +567,7 @@ startWork('render:balls')
ctx.imageSmoothingEnabled = false; ctx.imageSmoothingEnabled = false;
} }
startWork('render:breakout.lecaro.me?autoplay') startWork("render:breakout.lecaro.me?autoplay");
ctx.globalCompositeOperation = "source-over"; ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = 1; ctx.globalAlpha = 1;
@ -584,7 +582,7 @@ startWork('render:balls')
(gameState.canvasHeight - gameState.gameZoneHeight) / 2, (gameState.canvasHeight - gameState.gameZoneHeight) / 2,
); );
} }
startWork('render:mobile_press_to_play') startWork("render:mobile_press_to_play");
if (isOptionOn("mobile-mode") && !gameState.running) { if (isOptionOn("mobile-mode") && !gameState.running) {
drawText( drawText(
ctx, ctx,
@ -604,10 +602,10 @@ startWork('render:balls')
// ctx.fillRect(0,gameState.gameZoneHeight, gameState.canvasWidth, gameState.canvasHeight-gameState.gameZoneHeight) // ctx.fillRect(0,gameState.gameZoneHeight, gameState.canvasWidth, gameState.canvasHeight-gameState.gameZoneHeight)
// } // }
// ctx.globalAlpha=1 // ctx.globalAlpha=1
startWork('render:askForWakeLock') startWork("render:askForWakeLock");
askForWakeLock(gameState); askForWakeLock(gameState);
startWork('render:resetTransform') startWork("render:resetTransform");
if (shaked) { if (shaked) {
ctx.resetTransform(); ctx.resetTransform();
} }

View file

@ -2,15 +2,14 @@
let cachedSettings: { [key: string]: unknown } = {}; let cachedSettings: { [key: string]: unknown } = {};
try { try {
for(let key in localStorage){ for (let key in localStorage) {
try {
try { cachedSettings[key] = JSON.parse(localStorage.getItem(key) || "null");
cachedSettings[key] = JSON.parse(localStorage.getItem(key)||'null') ; } catch (e) {
} catch (e) { console.warn(e);
console.warn(e); }
} }
}
} catch (e) { } catch (e) {
console.warn(e); console.warn(e);
} }
@ -20,24 +19,23 @@ export function getSettingValue<T>(key: string, defaultValue: T) {
} }
// We avoid using localstorage synchronously for perf reasons // We avoid using localstorage synchronously for perf reasons
let needsSaving= new Set() let needsSaving: Set<string> = new Set();
export function setSettingValue<T>(key: string, value: T) { export function setSettingValue<T>(key: string, value: T) {
if(cachedSettings[key] !==value){ if (cachedSettings[key] !== value) {
needsSaving.add(key) needsSaving.add(key);
cachedSettings[key] = value; cachedSettings[key] = value;
} }
} }
setInterval(()=>{ setInterval(() => {
try { try {
for(let key of needsSaving){ for (let key of needsSaving) {
localStorage.setItem(key, JSON.stringify(cachedSettings[key])); localStorage.setItem(key, JSON.stringify(cachedSettings[key]));
} }
needsSaving.clear() needsSaving.clear();
} catch (e) { } catch (e) {
console.warn(e); console.warn(e);
} }
}, 500) }, 500);
export function getTotalScore() { export function getTotalScore() {
return getSettingValue("breakout_71_total_score", 0); return getSettingValue("breakout_71_total_score", 0);
@ -47,7 +45,7 @@ export function getCurrentMaxCoins() {
return Math.pow(2, getSettingValue("max_coins", 2)) * 200; return Math.pow(2, getSettingValue("max_coins", 2)) * 200;
} }
export function getCurrentMaxParticles() { export function getCurrentMaxParticles() {
return getCurrentMaxCoins() return getCurrentMaxCoins();
} }
export function cycleMaxCoins() { export function cycleMaxCoins() {
setSettingValue("max_coins", (getSettingValue("max_coins", 2) + 1) % 7); setSettingValue("max_coins", (getSettingValue("max_coins", 2) + 1) % 7);

View file

@ -33,7 +33,7 @@ export const sounds = {
plouf: (volume: number, pan: number) => { plouf: (volume: number, pan: number) => {
if (!isOptionOn("sound")) return; if (!isOptionOn("sound")) return;
createSingleBounceSound(500, pan, volume*0.5); createSingleBounceSound(500, pan, volume * 0.5);
// createWaterDropSound(800, pan, volume*0.2, 0.2,'triangle') // createWaterDropSound(800, pan, volume*0.2, 0.2,'triangle')
}, },
@ -277,44 +277,3 @@ function createOscillator(
oscillator.frequency.setValueAtTime(frequency, context.currentTime); oscillator.frequency.setValueAtTime(frequency, context.currentTime);
return oscillator; return oscillator;
} }
// TODO
function createWaterDropSound(
baseFreq = 500,
pan = 0.5,
volume = 1,
duration = 0.6,
type: OscillatorType = "sine"
) {
const context = getAudioContext();
if (!context) return;
const oscillator = createOscillator(context, baseFreq, type);
const gainNode = context.createGain();
const panner = context.createStereoPanner();
// Connect nodes
oscillator.connect(gainNode);
gainNode.connect(panner);
panner.connect(context.destination);
panner.connect(audioRecordingTrack);
// Panning
panner.pan.setValueAtTime(pan * 2 - 1, context.currentTime);
const now = context.currentTime;
// Volume envelope: soft plop -> fade out
gainNode.gain.setValueAtTime(0.0001, now);
gainNode.gain.exponentialRampToValueAtTime(0.7 * volume, now + duration/100); // Quick swell
gainNode.gain.exponentialRampToValueAtTime(0.1, now + duration/3); // Fade out
gainNode.gain.exponentialRampToValueAtTime(0.001, now + duration); // Fade out
// Pitch envelope: slight downward pitch bend to simulate water tension
oscillator.frequency.setValueAtTime(baseFreq, now);
oscillator.frequency.exponentialRampToValueAtTime(baseFreq * 0.5, now + duration);
// Start and stop
oscillator.start(now);
oscillator.stop(now + duration);
}

View file

@ -1,17 +1,15 @@
let div = document.createElement("div");
div.classList = "hidden toast";
let div= document.createElement("div"); document.body.appendChild(div);
div.classList = 'hidden toast'; let timeout: NodeJS.Timeout | undefined;
document.body.appendChild(div);
let timeout: NodeJS.Timeout|undefined;
export function toast(html) { export function toast(html) {
div.classList = "toast visible"; div.classList = "toast visible";
div.innerHTML = html; div.innerHTML = html;
if(timeout) { if (timeout) {
clearTimeout(timeout) clearTimeout(timeout);
} }
timeout=setTimeout(() => { timeout = setTimeout(() => {
timeout=undefined timeout = undefined;
div.classList = 'hidden toast'; div.classList = "hidden toast";
}, 1500); }, 1500);
} }