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" applicationId = "me.lecaro.breakout"
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 34
versionCode = 29028296 versionCode = 29030872
versionName = "29028296" versionName = "29030872"
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

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

View file

@ -4,14 +4,13 @@ import _rawLevelsList from "./levels.json";
import _appVersion from "./version.json"; import _appVersion from "./version.json";
import { rawUpgrades } from "./rawUpgrades"; import { rawUpgrades } from "./rawUpgrades";
import _backgrounds from "./backgrounds.json"; import _backgrounds from "./backgrounds.json";
const backgrounds = _backgrounds as string[] const backgrounds = _backgrounds as string[];
const palette = _palette as Palette; const palette = _palette as Palette;
const rawLevelsList = _rawLevelsList as RawLevel[]; const rawLevelsList = _rawLevelsList as RawLevel[];
export const appVersion = _appVersion as string; export const appVersion = _appVersion as string;
let levelIconHTMLCanvas = document.createElement("canvas"); let levelIconHTMLCanvas = document.createElement("canvas");
const levelIconHTMLCanvasCtx = levelIconHTMLCanvas.getContext("2d", { const levelIconHTMLCanvasCtx = levelIconHTMLCanvas.getContext("2d", {
antialias: false, antialias: false,
@ -65,7 +64,7 @@ export const allLevels = rawLevelsList
.slice(0, level.size * level.size); .slice(0, level.size * level.size);
const icon = levelIconHTML(bricks, level.size, level.name, level.color); const icon = levelIconHTML(bricks, level.size, level.name, level.color);
icons[level.name] = icon; icons[level.name] = icon;
let svg = level.svg!==null && backgrounds[level.svg] let svg = level.svg !== null && backgrounds[level.svg];
if (!level.color && !svg) { if (!level.color && !svg) {
svg = backgrounds[hashCode(level.name) % backgrounds.length]; svg = backgrounds[hashCode(level.name) % backgrounds.length];
@ -94,12 +93,11 @@ export const upgrades = rawUpgrades.map((u) => ({
icon: icons["icon:" + u.id], icon: icons["icon:" + u.id],
})) as Upgrade[]; })) as Upgrade[];
function hashCode(string: string) {
function hashCode(string:string){
let hash = 0; let hash = 0;
for (let i = 0; i < string.length; i++) { for (let i = 0; i < string.length; i++) {
let code = string.charCodeAt(i); let code = string.charCodeAt(i);
hash = ((hash<<5)-hash)+code; hash = (hash << 5) - hash + code;
hash = hash & hash; // Convert to 32bit integer hash = hash & hash; // Convert to 32bit integer
} }
return Math.abs(hash); 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 = { export const sounds = {
wallBeep: (pan: number) => { wallBeep: (pan: number) => {
@ -29,9 +34,9 @@ export const sounds = {
if (!isSettingOn("sound")) return; if (!isSettingOn("sound")) return;
createExplosionSound(pixelsToPan(pan)); createExplosionSound(pixelsToPan(pan));
}, },
lifeLost(pan:number){ lifeLost(pan: number) {
if (!isSettingOn("sound")) return; if (!isSettingOn("sound")) return;
createShatteredGlassSound(pixelsToPan(pan)) createShatteredGlassSound(pixelsToPan(pan));
}, },
coinCatch(pan: number) { coinCatch(pan: number) {
@ -41,11 +46,12 @@ export const sounds = {
}; };
// How to play the code on the leftconst context = new window.AudioContext(); // 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() { export function getAudioContext() {
if (!audioContext) { if (!audioContext) {
if (!isSettingOn('sound')) return null if (!isSettingOn("sound")) return null;
audioContext = new (window.AudioContext || window.webkitAudioContext)(); audioContext = new (window.AudioContext || window.webkitAudioContext)();
audioRecordingTrack = audioContext.createMediaStreamDestination(); audioRecordingTrack = audioContext.createMediaStreamDestination();
} }
@ -53,8 +59,8 @@ export function getAudioContext() {
} }
export function getAudioRecordingTrack() { export function getAudioRecordingTrack() {
getAudioContext() getAudioContext();
return audioRecordingTrack return audioRecordingTrack;
} }
function createSingleBounceSound( function createSingleBounceSound(
@ -65,7 +71,7 @@ function createSingleBounceSound(
type: OscillatorType = "sine", type: OscillatorType = "sine",
) { ) {
const context = getAudioContext(); const context = getAudioContext();
if (!context) return if (!context) return;
const oscillator = createOscillator(context, baseFreq, type); const oscillator = createOscillator(context, baseFreq, type);
// Create a gain node to control the volume // Create a gain node to control the volume
@ -95,8 +101,7 @@ function createSingleBounceSound(
let noiseBuffer: AudioBuffer; let noiseBuffer: AudioBuffer;
function getNoiseBuffer(context:AudioContext) { function getNoiseBuffer(context: AudioContext) {
if (!noiseBuffer) { if (!noiseBuffer) {
const bufferSize = context.sampleRate * 2; // 2 seconds const bufferSize = context.sampleRate * 2; // 2 seconds
noiseBuffer = context.createBuffer(1, bufferSize, context.sampleRate); noiseBuffer = context.createBuffer(1, bufferSize, context.sampleRate);
@ -107,15 +112,14 @@ function getNoiseBuffer(context:AudioContext) {
output[i] = Math.random() * 2 - 1; output[i] = Math.random() * 2 - 1;
} }
} }
return noiseBuffer return noiseBuffer;
} }
function createExplosionSound(pan = 0.5) { function createExplosionSound(pan = 0.5) {
const context = getAudioContext(); const context = getAudioContext();
if (!context) return if (!context) return;
// Create an audio buffer // Create an audio buffer
// Create a noise source // Create a noise source
const noiseSource = context.createBufferSource(); const noiseSource = context.createBufferSource();
noiseSource.buffer = getNoiseBuffer(context); noiseSource.buffer = getNoiseBuffer(context);
@ -154,7 +158,10 @@ function createExplosionSound(pan = 0.5) {
} }
function pixelsToPan(pan: number) { 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, let lastComboPlayed = NaN,
@ -182,28 +189,29 @@ function playShepard(delta: number, pan: number, volume: number) {
play(-1 - shepardMax + shepard); play(-1 - shepardMax + shepard);
} }
function createShatteredGlassSound(pan:number) { function createShatteredGlassSound(pan: number) {
const context = getAudioContext(); const context = getAudioContext();
if (!context) return if (!context) return;
const oscillators = [ const oscillators = [
createOscillator(context, 3000, 'square'), createOscillator(context, 3000, "square"),
createOscillator(context, 4500, 'square'), createOscillator(context, 4500, "square"),
createOscillator(context, 6000, 'square') createOscillator(context, 6000, "square"),
]; ];
const gainNode = context.createGain(); const gainNode = context.createGain();
const noiseSource = context.createBufferSource(); const noiseSource = context.createBufferSource();
noiseSource.buffer = getNoiseBuffer(context); noiseSource.buffer = getNoiseBuffer(context);
oscillators.forEach(oscillator => oscillator.connect(gainNode)); oscillators.forEach((oscillator) => oscillator.connect(gainNode));
noiseSource.connect(gainNode); noiseSource.connect(gainNode);
gainNode.gain.setValueAtTime(0.2, context.currentTime); gainNode.gain.setValueAtTime(0.2, context.currentTime);
oscillators.forEach(oscillator => oscillator.start()); oscillators.forEach((oscillator) => oscillator.start());
noiseSource.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); noiseSource.stop(context.currentTime + 0.2);
gainNode.gain.exponentialRampToValueAtTime(0.001, context.currentTime + 0.2); gainNode.gain.exponentialRampToValueAtTime(0.001, context.currentTime + 0.2);
// Create a stereo panner node for left-right panning // Create a stereo panner node for left-right panning
const panner = context.createStereoPanner(); const panner = context.createStereoPanner();
panner.pan.setValueAtTime(pan * 2 - 1, context.currentTime); 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 // 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(); const oscillator = context.createOscillator();
oscillator.type =type oscillator.type = type;
oscillator.frequency.setValueAtTime(frequency, context.currentTime); oscillator.frequency.setValueAtTime(frequency, context.currentTime);
return oscillator; return oscillator;
} }

2
src/types.d.ts vendored
View file

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

View file

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