This commit is contained in:
Renan LE CARO 2025-04-15 16:47:04 +02:00
parent 64a85200b9
commit 47ad04c49b
26 changed files with 313 additions and 247 deletions

View file

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

View file

@ -933,12 +933,6 @@
"bricks": "__bbbb___bbggbb_bbggggbbbggggggbbggggggbbbggggbb_bbggbb___bbbb__",
"color": ""
},
{
"name": "icon:particles",
"size": 8,
"bricks": "_b_b_b__________b_bbb_b___bbb___b_bbb__b_____b___b_b__b________b",
"color": ""
},
{
"name": "icon:reset",
"size": 8,

View file

@ -1 +1 @@
"29077162"
"29077593"

View file

@ -71,8 +71,8 @@ canvas:not(#game) {
transition: color 0.01s;
}
&.hidden {
display: none;
&.computer_controlled {
pointer-events: none;
}
span {
@ -521,6 +521,7 @@ h2.histogram-title strong {
}
}
.toast {
position: fixed;
left: 0;
@ -534,22 +535,18 @@ h2.histogram-title strong {
border-radius: 2px;
padding-right: 10px;
pointer-events: none;
animation: toast forwards;
}
@keyframes toast {
0%,
100% {
transition: opacity 200ms, transform 200ms;
&.hidden{
opacity: 0;
transform: translate(-20px, -20px) scale(0.5);
}
10%,
90% {
&.visible{
opacity: 0.8;
transform: none;
}
}
.gridEdit > div > span,
.palette > span {
display: inline-flex;

View file

@ -44,6 +44,7 @@ import {
import {
forEachLiveOne,
gameStateTick,
liveCount,
normalizeGameState,
pickRandomUpgrades,
setLevel,
@ -96,6 +97,7 @@ import { runHistoryViewerMenuEntry } from "./runHistoryViewer";
import { getNearestUnlockHTML, openScorePanel } from "./openScorePanel";
import { monitorLevelsUnlocks } from "./monitorLevelsUnlocks";
import { levelEditorMenuEntry } from "./levelEditor";
import {toast} from "./toast";
export async function play() {
if (await applyFullScreenChoice()) return;
@ -470,6 +472,7 @@ export let lastMeasuredFPS = 60;
setInterval(() => {
lastMeasuredFPS = FPSCounter;
FPSCounter = 0;
}, 1000);
setInterval(() => {
@ -768,15 +771,6 @@ async function openSettingsMenu() {
await openSettingsMenu();
},
});
actions.push({
icon: icons["icon:particles"],
text: t("settings.max_particles", { max: getCurrentMaxParticles() }),
help: t("settings.max_particles_help"),
async value() {
cycleMaxParticles();
await openSettingsMenu();
},
});
actions.push({
icon: icons["icon:reset"],
@ -1022,22 +1016,29 @@ export function restart(params: RunParams) {
play();
}
}
if (window.location.search.includes("autoplay")) {
if (window.location.search.includes("autoplay")) {
startComputerControlledGame();
} else {
} else if (window.location.search.includes("stress")) {
if(!isOptionOn('show_fps'))
toggleOption('show_fps')
restart({
level:allLevels.find(l=>l.name=='Worms'),
perks:{base_combo:5000, pierce:20, rainbow:3, sapper:2, etherealcoins:1}
});
}else {
restart({});
}
export function startComputerControlledGame() {
const perks: Partial<PerksMap> = { base_combo: 7, pierce: 3 };
const perks: Partial<PerksMap> = { base_combo: 20, pierce: 3 };
for (let i = 0; i < 10; i++) {
const u = sample(upgrades);
perks[u.id] = Math.floor(Math.random() * u.max) + 1;
perks[u.id] ||= Math.floor(Math.random() * u.max) + 1;
}
perks.superhot = 0;
restart({
level: sample(allLevels)?.name,
level: sample(allLevels.filter((l) => l.color === "#000000")),
computer_controlled: true,
perks,
});

View file

@ -9,7 +9,7 @@ import {
pickedUpgradesHTMl,
reasonLevelIsLocked,
} from "./game_utils";
import { getSettingValue, getTotalScore } from "./settings";
import {getSettingValue, getTotalScore, setSettingValue} from "./settings";
import { stopRecording } from "./recording";
import { asyncAlert } from "./asyncAlert";
import { rawUpgrades } from "./upgrades";
@ -18,13 +18,8 @@ import { editRawLevelList } from "./levelEditor";
export function addToTotalPlayTime(ms: number) {
try {
localStorage.setItem(
"breakout_71_total_play_time",
JSON.stringify(
JSON.parse(localStorage.getItem("breakout_71_total_play_time") || "0") +
ms,
),
);
setSettingValue('breakout_71_total_play_time', getSettingValue('breakout_71_total_play_time',0)+ms)
} catch (e) {}
}

View file

@ -1160,8 +1160,12 @@ export function gameStateTick(
const ratio =
1 -
((gameState.perks.viscosity * 0.03 + 0.002) * frames) /
((gameState.perks.viscosity * 0.03 +
0.002 +
(coin.y > gameState.gameZoneHeight ? 0.2 : 0)) *
frames) /
(1 + gameState.perks.etherealcoins);
if (!gameState.perks.etherealcoins) {
coin.vy *= ratio;
coin.vx *= ratio;
@ -1214,6 +1218,9 @@ export function gameStateTick(
const speed = (Math.abs(coin.vx) + Math.abs(coin.vy)) * 10;
const hitBorder = bordersHitCheck(gameState, coin, coin.size / 2, frames);
if(coin.previousY<gameState.gameZoneHeight && coin.y>gameState.gameZoneHeight && coin.vy>0 && speed > 20) {
schedulGameSound(gameState, "plouf", coin.x, clamp(speed, 20,100)/100*0.2);
}
if (
coin.y > gameState.gameZoneHeight - coinRadius - gameState.puckHeight &&

View file

@ -240,6 +240,7 @@ export function defaultSounds() {
explode: { vol: 0, x: 0 },
lifeLost: { vol: 0, x: 0 },
coinCatch: { vol: 0, x: 0 },
plouf: { vol: 0, x: 0 },
colorChange: { vol: 0, x: 0 },
},
};

View file

@ -186,8 +186,6 @@
"settings.load_save_file_help": "حدد ملف الحفظ على جهازك",
"settings.max_coins": " {{max}} عملات معدنية على الشاشة كحد أقصى",
"settings.max_coins_help": "تجميلي فقط، لا يؤثر على النتيجة",
"settings.max_particles": " {{max}} جسيمات كحد أقصى",
"settings.max_particles_help": "يحدد عدد الجسيمات التي تظهر على الشاشة للتأثير البصري.",
"settings.mobile": "الوضع المحمول",
"settings.mobile_help": "يترك مساحة تحت المجداف.",
"settings.pointer_lock": "قفل مؤشر الماوس",

View file

@ -6647,76 +6647,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>max_particles</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>true</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>true</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>max_particles_help</name>
<description/>
<comment/>
<translations>
<translation>
<language>ar-LB</language>
<approved>false</approved>
</translation>
<translation>
<language>de-DE</language>
<approved>false</approved>
</translation>
<translation>
<language>en-US</language>
<approved>true</approved>
</translation>
<translation>
<language>es-CL</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>true</approved>
</translation>
<translation>
<language>ru-RU</language>
<approved>false</approved>
</translation>
<translation>
<language>tr-TR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>mobile</name>
<description/>

View file

@ -186,8 +186,6 @@
"settings.load_save_file_help": "Wählen Sie eine Speicherdatei auf Ihrem Gerät",
"settings.max_coins": " {{max}} Münzen auf dem Bildschirm maximal",
"settings.max_coins_help": "Nur kosmetisch, keine Auswirkung auf das Ergebnis",
"settings.max_particles": " {{max}} Teilchen maximal",
"settings.max_particles_help": "Begrenzt die Anzahl der auf dem Bildschirm angezeigten Partikel für visuelle Effekte.",
"settings.mobile": "Mobiler Modus",
"settings.mobile_help": "Lässt Platz unter dem Paddel.",
"settings.pointer_lock": "Mauszeigersperre",

View file

@ -186,8 +186,6 @@
"settings.load_save_file_help": "Select a save file on your device",
"settings.max_coins": " {{max}} coins on screen maximum",
"settings.max_coins_help": "Cosmetic only, no effect on score",
"settings.max_particles": " {{max}} particles maximum",
"settings.max_particles_help": "Limits the number of particles show on screen for visual effect. ",
"settings.mobile": "Mobile mode",
"settings.mobile_help": "Leaves space under the paddle.",
"settings.pointer_lock": "Mouse pointer lock",

View file

@ -186,8 +186,6 @@
"settings.load_save_file_help": "Seleccione un archivo guardado en su dispositivo",
"settings.max_coins": " {{max}} monedas en pantalla máximo",
"settings.max_coins_help": "Solo cosmético, sin efecto en la puntuación.",
"settings.max_particles": " {{max}} partículas máximo",
"settings.max_particles_help": "Limita la cantidad de partículas que se muestran en la pantalla para lograr un efecto visual.",
"settings.mobile": "Modo móvil",
"settings.mobile_help": "Deja espacio debajo de la paleta.",
"settings.pointer_lock": "Bloqueo del puntero del ratón",

View file

@ -186,8 +186,6 @@
"settings.load_save_file_help": "Depuis un fichier ",
"settings.max_coins": "{{max}} pièces affichées maximum",
"settings.max_coins_help": "Visuel uniquement, pas d'impact sur le score",
"settings.max_particles": " {{max}} particules maximum",
"settings.max_particles_help": "Limite le nombre de particules affichées à l'écran pour les effets visuels",
"settings.mobile": "Mode mobile",
"settings.mobile_help": "Laisse un espace sous la raquette.",
"settings.pointer_lock": "Verrouillage du pointeur",

View file

@ -186,8 +186,6 @@
"settings.load_save_file_help": "Выберите файл сохранения на вашем устройстве",
"settings.max_coins": " {{max}} монет на экране максимум",
"settings.max_coins_help": "Только косметика, не влияет на результат",
"settings.max_particles": " {{max}} частиц максимум",
"settings.max_particles_help": "Ограничивает количество частиц, отображаемых на экране для визуального эффекта.",
"settings.mobile": "Мобильный режим",
"settings.mobile_help": "Оставляет место под лопаткой.",
"settings.pointer_lock": "Блокировка указателя мыши",

View file

@ -186,8 +186,6 @@
"settings.load_save_file_help": "Cihazınızda bir kayıt dosyası seçin",
"settings.max_coins": "Ekranda maksimum {{max}} jeton var",
"settings.max_coins_help": "Sadece kozmetik, puan üzerinde etkisi yok",
"settings.max_particles": " {{max}} parçacık maksimum",
"settings.max_particles_help": "Görsel efekt için ekranda gösterilen parçacık sayısını sınırlar.",
"settings.mobile": "Mobil mod",
"settings.mobile_help": "Kürek altında boşluk bırakır.",
"settings.pointer_lock": "Fare işaretçisi kilidi",

View file

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

View file

@ -72,7 +72,7 @@ export function render(gameState: GameState) {
: 1;
scoreDisplay.innerHTML =
(isOptionOn("show_fps")
(isOptionOn("show_fps") || gameState.computer_controlled
? `
<span class="${(Math.abs(lastMeasuredFPS - 60) < 2 && " ") || (Math.abs(lastMeasuredFPS - 60) < 10 && "good") || "bad"}">
${lastMeasuredFPS} FPS
@ -99,7 +99,7 @@ export function render(gameState: GameState) {
`<span class="score" data-tooltip="${t("play.score_tooltip")}">$${gameState.score}</span>`;
scoreDisplay.className =
(gameState.computer_controlled && "hidden") ||
(gameState.computer_controlled && "computer_controlled") ||
(gameState.lastScoreIncrease > gameState.levelTime - 500 && "active") ||
"";
@ -549,6 +549,18 @@ export function render(gameState: GameState) {
ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = 1;
if (isOptionOn("mobile-mode") && gameState.computer_controlled) {
drawText(
ctx,
"breakout.lecaro.me?autoplay",
gameState.puckColor,
gameState.puckHeight,
gameState.canvasWidth / 2,
gameState.gameZoneHeight +
(gameState.canvasHeight - gameState.gameZoneHeight) / 2,
);
}
if (isOptionOn("mobile-mode") && !gameState.running) {
drawText(
ctx,
@ -561,6 +573,15 @@ export function render(gameState: GameState) {
);
}
// if(isOptionOn('mobile-mode')) {
// ctx.globalCompositeOperation = "source-over";
// ctx.globalAlpha = 0.5;
// ctx.fillStyle = 'black'
// ctx.fillRect(0,gameState.gameZoneHeight, gameState.canvasWidth, gameState.canvasHeight-gameState.gameZoneHeight)
// }
// ctx.globalAlpha=1
askForWakeLock(gameState);
if (shaked) {
ctx.resetTransform();
}
@ -1111,3 +1132,23 @@ function getCoinRenderColor(gameState: GameState, coin: Coin) {
return coin.color;
return "#ffd300";
}
let wakeLock = null,
wakeLockPending = false;
function askForWakeLock(gameState: GameState) {
if (gameState.computer_controlled && !wakeLock && !wakeLockPending) {
wakeLockPending = true;
try {
navigator.wakeLock.request("screen").then((lock) => {
wakeLock = lock;
wakeLockPending = false;
lock.addEventListener("release", () => {
// the wake lock has been released
wakeLock = null;
});
});
} catch (e) {
console.warn("askForWakeLock error", e);
}
}
}

View file

@ -28,17 +28,11 @@ export function getTotalScore() {
}
export function getCurrentMaxCoins() {
return Math.pow(2, getSettingValue("max_coins", 1)) * 200;
return Math.pow(2, getSettingValue("max_coins", 6)) * 200;
}
export function getCurrentMaxParticles() {
return Math.pow(2, getSettingValue("max_particles", 1)) * 200;
return getCurrentMaxCoins()
}
export function cycleMaxCoins() {
setSettingValue("max_coins", (getSettingValue("max_coins", 1) + 1) % 6);
}
export function cycleMaxParticles() {
setSettingValue(
"max_particles",
(getSettingValue("max_particles", 1) + 1) % 6,
);
setSettingValue("max_coins", (getSettingValue("max_coins", 6) + 1) % 6);
}

View file

@ -16,7 +16,7 @@ export function playPendingSounds(gameState: GameState) {
};
if (ex.vol) {
sounds[soundName](
Math.min(2, ex.vol),
Math.min(1, ex.vol),
pixelsToPan(gameState, ex.x),
gameState.combo,
);
@ -25,13 +25,21 @@ export function playPendingSounds(gameState: GameState) {
}
}
export const sounds = {
wallBeep: (vol: number, pan: number, combo: number) => {
wallBeep: (volume: number, pan: number) => {
if (!isOptionOn("sound")) return;
createSingleBounceSound(800, pan, vol);
createSingleBounceSound(800, pan, volume);
},
plouf: (volume: number, pan: number) => {
if (!isOptionOn("sound")) return;
createSingleBounceSound(240, pan, volume*0.5);
// createWaterDropSound(800, pan, volume*0.2, 0.2,'triangle')
},
comboIncreaseMaybe: (volume: number, pan: number, combo: number) => {
if (!isOptionOn("sound")) return;
let delta = 0;
if (!isNaN(lastComboPlayed)) {
if (lastComboPlayed < combo) delta = 1;
@ -269,3 +277,44 @@ function createOscillator(
oscillator.frequency.setValueAtTime(frequency, context.currentTime);
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,17 @@
let onScreen = 0;
export function toast(html) {
const div = document.createElement("div");
div.classList = "toast";
div.innerHTML = html;
const lasts = 1500 + onScreen * 200;
div.style.animationDuration = lasts + "ms";
div.style.top = 40 + onScreen * 50 + "px";
let div= document.createElement("div");
div.classList = 'hidden toast';
document.body.appendChild(div);
onScreen++;
setTimeout(() => {
div.remove();
onScreen--;
}, lasts);
let timeout: NodeJS.Timeout|undefined;
export function toast(html) {
div.classList = "toast visible";
div.innerHTML = html;
if(timeout) {
clearTimeout(timeout)
}
timeout=setTimeout(() => {
timeout=undefined
div.classList = 'hidden toast';
}, 1500);
}

1
src/types.d.ts vendored
View file

@ -276,6 +276,7 @@ export type GameState = {
explode: { vol: number; x: number };
lifeLost: { vol: number; x: number };
coinCatch: { vol: number; x: number };
plouf: { vol: number; x: number };
colorChange: { vol: number; x: number };
};
rerolls: number;