Build and deploy of version 29030872

This commit is contained in:
Renan LE CARO 2025-03-13 08:53:02 +01:00
parent e14e958686
commit 57cb73128f
10 changed files with 2827 additions and 6487 deletions

View file

@ -11,8 +11,8 @@ android {
applicationId = "me.lecaro.breakout"
minSdk = 21
targetSdk = 34
versionCode = 29028296
versionName = "29028296"
versionCode = 29030872
versionName = "29030872"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true

File diff suppressed because one or more lines are too long

3854
dist/index.html vendored

File diff suppressed because one or more lines are too long

View file

@ -1,4 +1,4 @@
import {allLevels, appVersion, icons, upgrades} from "./loadGameData";
import { allLevels, appVersion, icons, upgrades } from "./loadGameData";
import {
Ball,
BallLike,
@ -12,8 +12,8 @@ import {
RunStats,
Upgrade,
} from "./types";
import {OptionId, options} from "./options";
import {getAudioContext, getAudioRecordingTrack, sounds} from "./sounds";
import { OptionId, options } from "./options";
import { getAudioContext, getAudioRecordingTrack, sounds } from "./sounds";
const MAX_COINS = 400;
const MAX_PARTICLES = 600;
@ -169,7 +169,7 @@ background.addEventListener("load", () => {
let lastWidth = 0,
lastHeight = 0;
export const fitSize = () => {
const {width, height} = gameCanvas.getBoundingClientRect();
const { width, height } = gameCanvas.getBoundingClientRect();
lastWidth = width;
lastHeight = height;
gameCanvas.width = width;
@ -206,7 +206,7 @@ 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 !== lastWidth || height !== lastHeight) fitSize();
}, 1000);
@ -495,9 +495,7 @@ function getPossibleUpgrades() {
function shuffleLevels(nameToAvoid: string | null = null) {
const target = nextRunOverrides?.level;
delete nextRunOverrides.level;
const firstLevel = target
? allLevels.filter((l) => l.name === target)
: [];
const firstLevel = target ? allLevels.filter((l) => l.name === target) : [];
const restInRandomOrder = allLevels
.filter((l) => totalScoreAtRunStart >= l.threshold)
@ -542,7 +540,7 @@ function dontOfferTooSoon(id: PerkId) {
function pickRandomUpgrades(count: number) {
let list = getPossibleUpgrades()
.map((u) => ({...u, score: Math.random() + (lastOffered[u.id] || 0)}))
.map((u) => ({ ...u, score: Math.random() + (lastOffered[u.id] || 0) }))
.sort((a, b) => a.score - b.score)
.filter((u) => perks[u.id] < u.max)
.slice(0, count)
@ -700,11 +698,10 @@ function shouldPierceByColor(
return true;
}
function coinBrickHitCheck(coin: Coin) {
// Make ball/coin bonce, and return bricks that were hit
const radius = coinSize / 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);
@ -762,7 +759,8 @@ function bordersHitCheck(coin: Coin | Ball, radius: number, delta: number) {
hhit = 0;
if (coin.x < offsetXRoundedDown + radius) {
coin.x = offsetXRoundedDown + radius + (offsetXRoundedDown + radius - coin.x);
coin.x =
offsetXRoundedDown + radius + (offsetXRoundedDown + radius - coin.x);
coin.vx *= -1;
hhit = 1;
}
@ -772,7 +770,11 @@ function bordersHitCheck(coin: Coin | Ball, radius: number, delta: number) {
vhit = 1;
}
if (coin.x > lastWidth - offsetXRoundedDown - radius) {
coin.x = lastWidth - offsetXRoundedDown - radius - (coin.x - (lastWidth - offsetXRoundedDown - radius));
coin.x =
lastWidth -
offsetXRoundedDown -
radius -
(coin.x - (lastWidth - offsetXRoundedDown - radius));
coin.vx *= -1;
hhit = 1;
}
@ -780,7 +782,6 @@ function bordersHitCheck(coin: Coin | Ball, radius: number, delta: number) {
return hhit + vhit * 2;
}
let lastTickDown = 0;
function tick() {
@ -837,14 +838,15 @@ function tick() {
coins.forEach((coin) => {
if (coin.destroyed) return;
if (perks.coin_magnet) {
const attractionX = ((delta * (puck - coin.x)) /
const attractionX =
((delta * (puck - coin.x)) /
(100 +
Math.pow(coin.y - gameZoneHeight, 2) +
Math.pow(coin.x - puck, 2))) *
perks.coin_magnet *
100
100;
coin.vx += attractionX;
coin.sa -= attractionX / 10
coin.sa -= attractionX / 10;
}
const ratio = 1 - (perks.viscosity * 0.03 + 0.005) * delta;
@ -1090,7 +1092,7 @@ function ballTick(ball: Ball, delta: number) {
if (perks.respawn && ball.hitItem?.length > 1 && !isSettingOn("basic")) {
for (let i = 0; i < ball.hitItem?.length - 1 && i < perks.respawn; i++) {
const {index, color} = ball.hitItem[i];
const { index, color } = ball.hitItem[i];
if (bricks[index] || color === "black") continue;
const vertical = Math.random() > 0.5;
const dx = Math.random() > 0.5 ? 1 : -1;
@ -1133,21 +1135,18 @@ function ballTick(ball: Ball, delta: number) {
resetCombo(ball.x, ball.y + ballSize);
}
sounds.wallBeep(ball.x);
ball.bouncesList?.push({x: ball.previousX, y: ball.previousY});
ball.bouncesList?.push({ x: ball.previousX, y: ball.previousY });
}
// Puck collision
const ylimit = gameZoneHeight - puckHeight - ballSize / 2;
const ballIsUnderPuck = Math.abs(ball.x - puck) < ballSize / 2 + puckWidth / 2
const ballIsUnderPuck =
Math.abs(ball.x - puck) < ballSize / 2 + puckWidth / 2;
if (
ball.y > ylimit &&
ball.vy > 0 && (
ballIsUnderPuck
|| (perks.extra_life && ball.y > ylimit + puckHeight / 2)
)
ball.vy > 0 &&
(ballIsUnderPuck || (perks.extra_life && ball.y > ylimit + puckHeight / 2))
) {
if (ballIsUnderPuck) {
const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
const angle = Math.atan2(-puckWidth / 2, ball.x - puck);
@ -1155,15 +1154,15 @@ function ballTick(ball: Ball, delta: number) {
ball.vy = speed * Math.sin(angle);
sounds.wallBeep(ball.x);
} else {
ball.vy *= -1
perks.extra_life = Math.max(0, perks.extra_life - 1)
sounds.lifeLost(ball.x)
ball.vy *= -1;
perks.extra_life = Math.max(0, perks.extra_life - 1);
sounds.lifeLost(ball.x);
if (!isSettingOn("basic")) {
for (let i = 0; i < 10; i++)
flashes.push({
type: 'particle',
type: "particle",
ethereal: false,
color: 'red',
color: "red",
destroyed: false,
duration: 150,
size: coinSize / 2,
@ -1171,8 +1170,8 @@ function ballTick(ball: Ball, delta: number) {
x: ball.x,
y: ball.y,
vx: Math.random() * baseSpeed * 3,
vy: baseSpeed * 3
})
vy: baseSpeed * 3,
});
}
}
if (perks.streak_shots) {
@ -1183,7 +1182,7 @@ function ballTick(ball: Ball, delta: number) {
ball.hitItem
.slice(0, -1)
.slice(0, perks.respawn)
.forEach(({index, color}) => {
.forEach(({ index, color }) => {
if (!bricks[index] && color !== "black") bricks[index] = color;
});
}
@ -1227,7 +1226,7 @@ function ballTick(ball: Ball, delta: number) {
}
const radius = 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);
@ -1238,15 +1237,19 @@ function ballTick(ball: Ball, delta: number) {
undefined;
const hitBrick = vhit ?? hhit ?? chit;
let sturdyBounce=hitBrick && bricks[hitBrick]!=='black' && perks.sturdy_bricks && perks.sturdy_bricks > Math.random() * 5
let sturdyBounce =
hitBrick &&
bricks[hitBrick] !== "black" &&
perks.sturdy_bricks &&
perks.sturdy_bricks > Math.random() * 5;
let pierce = false;
if(sturdyBounce || typeof hitBrick === "undefined") {
if (sturdyBounce || typeof hitBrick === "undefined") {
// cannot pierce
}else if(shouldPierceByColor(vhit, hhit, chit)){
} else if (shouldPierceByColor(vhit, hhit, chit)) {
pierce = true;
} else if (ball.piercedSinceBounce < perks.pierce * 3) {
pierce = true;
}else if (ball.piercedSinceBounce < perks.pierce * 3 ){
pierce=true
ball.piercedSinceBounce++;
}
@ -1263,9 +1266,9 @@ function ballTick(ball: Ball, delta: number) {
}
}
if(sturdyBounce){
sounds.wallBeep(x)
return
if (sturdyBounce) {
sounds.wallBeep(x);
return;
}
if (typeof hitBrick !== "undefined") {
const initialBrickColor = bricks[hitBrick];
@ -1338,8 +1341,7 @@ function addToTotalScore(points: number) {
"breakout_71_total_score",
JSON.stringify(getTotalScore() + points),
);
} catch (e) {
}
} catch (e) {}
}
function addToTotalPlayTime(ms: number) {
@ -1351,8 +1353,7 @@ function addToTotalPlayTime(ms: number) {
ms,
),
);
} catch (e) {
}
} catch (e) {}
}
function gameOver(title: string, intro: string) {
@ -1445,7 +1446,7 @@ function getHistograms() {
runsHistory.sort((a, b) => a.score - b.score).reverse();
runsHistory = runsHistory.slice(0, 100);
runsHistory.push({...runStatistics, perks, appVersion});
runsHistory.push({ ...runStatistics, perks, appVersion });
// Generate some histogram
if (!isCreativeModeRun)
@ -1568,7 +1569,8 @@ function explodeBrick(index: number, ball: Ball, isExplosion: boolean) {
const i = getRowColIndex(row + dy, col + dx);
if (bricks[i] && i !== -1) {
// Study bricks resist explisions too
if(bricks[i]!=='black' && perks.sturdy_bricks > Math.random() * 5) continue
if (bricks[i] !== "black" && perks.sturdy_bricks > Math.random() * 5)
continue;
explodeBrick(i, ball, true);
}
}
@ -1634,7 +1636,7 @@ function explodeBrick(index: number, ball: Ball, isExplosion: boolean) {
while (coinsToSpawn > 0) {
const points = Math.min(pointsPerCoin, coinsToSpawn);
if (points < 0 || isNaN(points)) {
console.error({points});
console.error({ points });
debugger;
}
@ -1721,7 +1723,7 @@ function render() {
needsRender = false;
const level = currentLevelInfo();
const {width, height} = gameCanvas;
const { width, height } = gameCanvas;
if (!width || !height) return;
scoreDisplay.innerText = `L${currentLevel + 1}/${max_levels()} $${score}`;
@ -1751,7 +1753,7 @@ function render() {
});
ctx.globalAlpha = 1;
flashes.forEach((flash) => {
const {x, y, time, color, size, type, duration} = flash;
const { x, y, time, color, size, type, duration } = flash;
const elapsed = levelTime - time;
ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2);
if (type === "ball") {
@ -1799,7 +1801,7 @@ function render() {
ctx.fillRect(0, 0, width, height);
flashes.forEach((flash) => {
const {x, y, time, color, size, type, duration} = flash;
const { x, y, time, color, size, type, duration } = flash;
const elapsed = levelTime - time;
ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2);
if (type === "particle") {
@ -1820,15 +1822,13 @@ function render() {
);
}
// Coins
ctx.globalAlpha = 1;
coins.forEach((coin) => {
if (!coin.destroyed) {
ctx.globalCompositeOperation = (coin.color === 'gold' || level.color ? "source-over" : "screen");
ctx.globalCompositeOperation =
coin.color === "gold" || level.color ? "source-over" : "screen";
drawCoin(
ctx,
coin.color,
@ -1841,7 +1841,6 @@ function render() {
}
});
// Black shadow around balls
if (!isSettingOn("basic")) {
ctx.globalCompositeOperation = "source-over";
@ -1854,14 +1853,13 @@ function render() {
ctx.globalCompositeOperation = "source-over";
renderAllBricks();
ctx.globalCompositeOperation = "screen";
flashes = flashes.filter(
(f) => levelTime - f.time < f.duration && !f.destroyed,
);
flashes.forEach((flash) => {
const {x, y, time, color, size, type, duration} = flash;
const { x, y, time, color, size, type, duration } = flash;
const elapsed = levelTime - time;
ctx.globalAlpha = Math.max(0, Math.min(1, 2 - (elapsed / duration) * 2));
if (type === "text") {
@ -1874,14 +1872,17 @@ function render() {
}
});
if (perks.extra_life) {
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
ctx.fillStyle = puckColor;
for (let i = 0; i < perks.extra_life; i++) {
ctx.fillRect(offsetXRoundedDown, gameZoneHeight - puckHeight / 2 + 2 * i, gameZoneWidthRoundedUp, 1);
ctx.fillRect(
offsetXRoundedDown,
gameZoneHeight - puckHeight / 2 + 2 * i,
gameZoneWidthRoundedUp,
1,
);
}
}
@ -1942,7 +1943,6 @@ function render() {
false,
);
}
}
// Borders
const hasCombo = combo > baseCombo();
@ -1959,7 +1959,7 @@ function render() {
if (hasCombo && perks.right_is_lava) ctx.fillRect(width - 1, 0, 1, height);
}
if (perks.top_is_lava && combo > baseCombo()){
if (perks.top_is_lava && combo > baseCombo()) {
ctx.fillStyle = "red";
ctx.fillRect(offsetXRoundedDown, 0, gameZoneWidthRoundedUp, 1);
}
@ -2293,7 +2293,6 @@ function roundRect(
ctx.closePath();
}
function drawIMG(
ctx: CanvasRenderingContext2D,
img: HTMLImageElement,
@ -2356,7 +2355,6 @@ function drawText(
);
}
let levelTime = 0;
// Limits skip last to one use per level
let level_skip_last_uses = 0;
@ -2387,7 +2385,7 @@ function asyncAlert<t>({
allowClose = true,
textAfterButtons = "",
actionsAsGrid = false,
}: {
}: {
title?: string;
text?: string;
actions?: AsyncAlertAction<t>[];
@ -2442,7 +2440,7 @@ function asyncAlert<t>({
actions
?.filter((i) => i)
.forEach(({text, value, help, disabled, className = "", icon = ""}) => {
.forEach(({ text, value, help, disabled, className = "", icon = "" }) => {
const button = document.createElement("button");
button.innerHTML = `
@ -2535,8 +2533,7 @@ async function openScorePanel() {
{
text: "Resume",
help: "Return to your run",
value: () => {
},
value: () => {},
},
{
text: "Restart",
@ -2564,8 +2561,7 @@ async function openSettingsPanel() {
{
text: "Resume",
help: "Return to your run",
value() {
},
value() {},
},
{
text: "Starting perk",
@ -2712,12 +2708,12 @@ async function openUnlocksList() {
const actions = [
...upgrades
.sort((a, b) => a.threshold - b.threshold)
.map(({name, id, threshold, icon, fullHelp}) => ({
.map(({ name, id, threshold, icon, fullHelp }) => ({
text: name,
help:
ts >= threshold ? fullHelp : `Unlocks at total score ${threshold}.`,
disabled: ts < threshold,
value: {perk: id} as RunOverrides,
value: { perk: id } as RunOverrides,
icon,
})),
...allLevels
@ -2730,7 +2726,7 @@ async function openUnlocksList() {
? `A ${l.size}x${l.size} level with ${l.bricks.filter((i) => i).length} bricks`
: `Unlocks at total score ${l.threshold}.`,
disabled: !available,
value: {level: l.name} as RunOverrides,
value: { level: l.name } as RunOverrides,
icon: icons[l.name],
};
}),
@ -2957,7 +2953,7 @@ function startRecordingGame() {
captureTrack =
captureStream.getVideoTracks()[0] as CanvasCaptureMediaStreamTrack;
const track = getAudioRecordingTrack()
const track = getAudioRecordingTrack();
if (track) {
captureStream.addTrack(track.stream.getAudioTracks()[0]);
}
@ -2981,7 +2977,7 @@ function startRecordingGame() {
instance.onstop = async function () {
let targetDiv: HTMLElement | null;
let blob = new Blob(recordedChunks, {type: "video/webm"});
let blob = new Blob(recordedChunks, { type: "video/webm" });
if (blob.size < 200000) return; // under 0.2MB, probably bugged out or pointlessly short
while (

View file

@ -4,14 +4,13 @@ import _rawLevelsList from "./levels.json";
import _appVersion from "./version.json";
import { rawUpgrades } from "./rawUpgrades";
import _backgrounds from "./backgrounds.json";
const backgrounds = _backgrounds as string[]
const backgrounds = _backgrounds as string[];
const palette = _palette as Palette;
const rawLevelsList = _rawLevelsList as RawLevel[];
export const appVersion = _appVersion as string;
let levelIconHTMLCanvas = document.createElement("canvas");
const levelIconHTMLCanvasCtx = levelIconHTMLCanvas.getContext("2d", {
antialias: false,
@ -65,7 +64,7 @@ export const allLevels = rawLevelsList
.slice(0, level.size * level.size);
const icon = levelIconHTML(bricks, level.size, level.name, level.color);
icons[level.name] = icon;
let svg = level.svg!==null && backgrounds[level.svg]
let svg = level.svg !== null && backgrounds[level.svg];
if (!level.color && !svg) {
svg = backgrounds[hashCode(level.name) % backgrounds.length];
@ -94,12 +93,11 @@ export const upgrades = rawUpgrades.map((u) => ({
icon: icons["icon:" + u.id],
})) as Upgrade[];
function hashCode(string:string){
function hashCode(string: string) {
let hash = 0;
for (let i = 0; i < string.length; i++) {
let code = string.charCodeAt(i);
hash = ((hash<<5)-hash)+code;
hash = (hash << 5) - hash + code;
hash = hash & hash; // Convert to 32bit integer
}
return Math.abs(hash);

View file

@ -1,4 +1,9 @@
import {gameZoneWidthRoundedUp, isSettingOn, offsetX, offsetXRoundedDown} from "./game";
import {
gameZoneWidthRoundedUp,
isSettingOn,
offsetX,
offsetXRoundedDown,
} from "./game";
export const sounds = {
wallBeep: (pan: number) => {
@ -29,9 +34,9 @@ export const sounds = {
if (!isSettingOn("sound")) return;
createExplosionSound(pixelsToPan(pan));
},
lifeLost(pan:number){
lifeLost(pan: number) {
if (!isSettingOn("sound")) return;
createShatteredGlassSound(pixelsToPan(pan))
createShatteredGlassSound(pixelsToPan(pan));
},
coinCatch(pan: number) {
@ -41,11 +46,12 @@ export const sounds = {
};
// How to play the code on the leftconst context = new window.AudioContext();
let audioContext: AudioContext, audioRecordingTrack: MediaStreamAudioDestinationNode;
let audioContext: AudioContext,
audioRecordingTrack: MediaStreamAudioDestinationNode;
export function getAudioContext() {
if (!audioContext) {
if (!isSettingOn('sound')) return null
if (!isSettingOn("sound")) return null;
audioContext = new (window.AudioContext || window.webkitAudioContext)();
audioRecordingTrack = audioContext.createMediaStreamDestination();
}
@ -53,8 +59,8 @@ export function getAudioContext() {
}
export function getAudioRecordingTrack() {
getAudioContext()
return audioRecordingTrack
getAudioContext();
return audioRecordingTrack;
}
function createSingleBounceSound(
@ -65,7 +71,7 @@ function createSingleBounceSound(
type: OscillatorType = "sine",
) {
const context = getAudioContext();
if (!context) return
if (!context) return;
const oscillator = createOscillator(context, baseFreq, type);
// Create a gain node to control the volume
@ -95,8 +101,7 @@ function createSingleBounceSound(
let noiseBuffer: AudioBuffer;
function getNoiseBuffer(context:AudioContext) {
function getNoiseBuffer(context: AudioContext) {
if (!noiseBuffer) {
const bufferSize = context.sampleRate * 2; // 2 seconds
noiseBuffer = context.createBuffer(1, bufferSize, context.sampleRate);
@ -107,15 +112,14 @@ function getNoiseBuffer(context:AudioContext) {
output[i] = Math.random() * 2 - 1;
}
}
return noiseBuffer
return noiseBuffer;
}
function createExplosionSound(pan = 0.5) {
const context = getAudioContext();
if (!context) return
if (!context) return;
// Create an audio buffer
// Create a noise source
const noiseSource = context.createBufferSource();
noiseSource.buffer = getNoiseBuffer(context);
@ -154,7 +158,10 @@ function createExplosionSound(pan = 0.5) {
}
function pixelsToPan(pan: number) {
return Math.max(0, Math.min(1, (pan - offsetXRoundedDown) / gameZoneWidthRoundedUp));
return Math.max(
0,
Math.min(1, (pan - offsetXRoundedDown) / gameZoneWidthRoundedUp),
);
}
let lastComboPlayed = NaN,
@ -182,28 +189,29 @@ function playShepard(delta: number, pan: number, volume: number) {
play(-1 - shepardMax + shepard);
}
function createShatteredGlassSound(pan:number) {
function createShatteredGlassSound(pan: number) {
const context = getAudioContext();
if (!context) return
if (!context) return;
const oscillators = [
createOscillator(context, 3000, 'square'),
createOscillator(context, 4500, 'square'),
createOscillator(context, 6000, 'square')
createOscillator(context, 3000, "square"),
createOscillator(context, 4500, "square"),
createOscillator(context, 6000, "square"),
];
const gainNode = context.createGain();
const noiseSource = context.createBufferSource();
noiseSource.buffer = getNoiseBuffer(context);
oscillators.forEach(oscillator => oscillator.connect(gainNode));
oscillators.forEach((oscillator) => oscillator.connect(gainNode));
noiseSource.connect(gainNode);
gainNode.gain.setValueAtTime(0.2, context.currentTime);
oscillators.forEach(oscillator => oscillator.start());
oscillators.forEach((oscillator) => oscillator.start());
noiseSource.start();
oscillators.forEach(oscillator => oscillator.stop(context.currentTime + 0.2));
oscillators.forEach((oscillator) =>
oscillator.stop(context.currentTime + 0.2),
);
noiseSource.stop(context.currentTime + 0.2);
gainNode.gain.exponentialRampToValueAtTime(0.001, context.currentTime + 0.2);
// Create a stereo panner node for left-right panning
const panner = context.createStereoPanner();
panner.pan.setValueAtTime(pan * 2 - 1, context.currentTime);
@ -215,9 +223,13 @@ function createShatteredGlassSound(pan:number) {
}
// Helper function to create an oscillator with a specific frequency
function createOscillator(context:AudioContext, frequency:number, type:OscillatorType) {
function createOscillator(
context: AudioContext,
frequency: number,
type: OscillatorType,
) {
const oscillator = context.createOscillator();
oscillator.type =type
oscillator.type = type;
oscillator.frequency.setValueAtTime(frequency, context.currentTime);
return oscillator;
}

2
src/types.d.ts vendored
View file

@ -6,7 +6,7 @@ export type RawLevel = {
name: string;
size: number;
bricks: string;
svg: number|null;
svg: number | null;
color: string;
};
export type Level = {

View file

@ -1 +1 @@
"29028296"
"29030872"