mirror of
https://gitlab.com/lecarore/breakout71.git
synced 2025-04-21 12:36:15 -04:00
Lives just save the ball once and show as a line.
This commit is contained in:
parent
752a5713d7
commit
2b9bb30000
5 changed files with 6626 additions and 2805 deletions
|
@ -17,6 +17,7 @@ set -x
|
|||
# the version number is just a unix timestamp in minutes
|
||||
versionCode=$(($(date +%s) / 60))
|
||||
|
||||
|
||||
# Replace the version code and name in gradle for fdroid and play store
|
||||
sed -i -e "s/^[[:space:]]*versionCode = .*/ versionCode = $versionCode/" \
|
||||
-e "s/^[[:space:]]*versionName = .*/ versionName = \"$versionCode\"/" \
|
||||
|
@ -27,6 +28,9 @@ echo "\"$versionCode\"" > src/version.json
|
|||
# remove all exif metadata from pictures, because i think fdroid doesn't like that. odd
|
||||
find -name '*.jp*g' -o -name '*.png' | xargs exiftool -all=
|
||||
|
||||
# expose the git log to the app itself
|
||||
git log --pretty=format:' %s' > src/git-log.txt
|
||||
|
||||
npx prettier --write src/
|
||||
|
||||
npm run build
|
||||
|
|
3843
dist/index.html
vendored
3843
dist/index.html
vendored
File diff suppressed because one or more lines are too long
423
src/game.ts
423
src/game.ts
|
@ -1,4 +1,4 @@
|
|||
import { allLevels, appVersion, icons, upgrades } from "./loadGameData";
|
||||
import {allLevels, appVersion, icons, upgrades} from "./loadGameData";
|
||||
import {
|
||||
Ball,
|
||||
BallLike,
|
||||
|
@ -12,7 +12,8 @@ import {
|
|||
RunStats,
|
||||
Upgrade,
|
||||
} from "./types";
|
||||
import { OptionId, options } from "./options";
|
||||
import {OptionId, options} from "./options";
|
||||
import {getAudioContext, getAudioRecordingTrack, sounds} from "./sounds";
|
||||
|
||||
const MAX_COINS = 400;
|
||||
const MAX_PARTICLES = 600;
|
||||
|
@ -116,9 +117,7 @@ let running = false,
|
|||
function play() {
|
||||
if (running) return;
|
||||
running = true;
|
||||
if (audioContext) {
|
||||
audioContext.resume().then();
|
||||
}
|
||||
getAudioContext()?.resume().then();
|
||||
resumeRecording();
|
||||
document.body.className = running ? " running " : " paused ";
|
||||
}
|
||||
|
@ -131,11 +130,11 @@ function pause(playerAskedForPause: boolean) {
|
|||
() => {
|
||||
running = false;
|
||||
needsRender = true;
|
||||
if (audioContext) {
|
||||
|
||||
setTimeout(() => {
|
||||
if (!running) audioContext.suspend().then();
|
||||
if (!running) getAudioContext()?.suspend().then();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
pauseRecording();
|
||||
pauseTimeout = null;
|
||||
document.body.className = running ? " running " : " paused ";
|
||||
|
@ -153,7 +152,7 @@ function pause(playerAskedForPause: boolean) {
|
|||
}
|
||||
}
|
||||
|
||||
let offsetX: number,
|
||||
export let offsetX: number,
|
||||
offsetXRoundedDown: number,
|
||||
gameZoneWidth: number,
|
||||
gameZoneWidthRoundedUp: number,
|
||||
|
@ -170,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,8 +205,8 @@ window.addEventListener("resize", fitSize);
|
|||
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();
|
||||
// Sometimes, the page changes size without triggering the event (when switching to fullscreen, closing debug panel...)
|
||||
const {width, height} = gameCanvas.getBoundingClientRect();
|
||||
if (width !== lastWidth || height !== lastHeight) fitSize();
|
||||
}, 1000);
|
||||
|
||||
|
@ -543,7 +542,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)
|
||||
|
@ -704,7 +703,7 @@ function shouldPierceByColor(
|
|||
function ballBrickHitCheck(ball: Ball) {
|
||||
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);
|
||||
|
@ -746,7 +745,7 @@ function ballBrickHitCheck(ball: Ball) {
|
|||
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);
|
||||
|
@ -804,17 +803,17 @@ function bordersHitCheck(coin: Coin | Ball, radius: number, delta: number) {
|
|||
hhit = 0;
|
||||
|
||||
if (coin.x < offsetXRoundedDown + radius) {
|
||||
coin.x = offsetXRoundedDown + radius;
|
||||
coin.x = offsetXRoundedDown + radius + (offsetXRoundedDown + radius - coin.x);
|
||||
coin.vx *= -1;
|
||||
hhit = 1;
|
||||
}
|
||||
if (coin.y < radius) {
|
||||
coin.y = radius;
|
||||
coin.y = radius + (radius - coin.y);
|
||||
coin.vy *= -1;
|
||||
vhit = 1;
|
||||
}
|
||||
if (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;
|
||||
}
|
||||
|
@ -822,6 +821,7 @@ function bordersHitCheck(coin: Coin | Ball, radius: number, delta: number) {
|
|||
return hhit + vhit * 2;
|
||||
}
|
||||
|
||||
|
||||
let lastTickDown = 0;
|
||||
|
||||
function tick() {
|
||||
|
@ -878,13 +878,14 @@ function tick() {
|
|||
coins.forEach((coin) => {
|
||||
if (coin.destroyed) return;
|
||||
if (perks.coin_magnet) {
|
||||
coin.vx +=
|
||||
((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
|
||||
}
|
||||
|
||||
const ratio = 1 - (perks.viscosity * 0.03 + 0.005) * delta;
|
||||
|
@ -1130,7 +1131,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;
|
||||
|
@ -1173,22 +1174,48 @@ 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
|
||||
if (
|
||||
ball.y > ylimit &&
|
||||
Math.abs(ball.x - puck) < ballSize / 2 + puckWidth / 2 &&
|
||||
ball.vy > 0
|
||||
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);
|
||||
ball.vx = speed * Math.cos(angle);
|
||||
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)
|
||||
if (!isSettingOn("basic")) {
|
||||
for (let i = 0; i < 10; i++)
|
||||
flashes.push({
|
||||
type: 'particle',
|
||||
ethereal: false,
|
||||
color: 'red',
|
||||
destroyed: false,
|
||||
duration: 150,
|
||||
size: coinSize / 2,
|
||||
time: levelTime,
|
||||
x: ball.x,
|
||||
y: ball.y,
|
||||
vx: Math.random() * baseSpeed * 3,
|
||||
vy: baseSpeed * 3
|
||||
})
|
||||
}
|
||||
}
|
||||
if (perks.streak_shots) {
|
||||
resetCombo(ball.x, ball.y);
|
||||
}
|
||||
|
@ -1197,7 +1224,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;
|
||||
});
|
||||
}
|
||||
|
@ -1233,29 +1260,12 @@ function ballTick(ball: Ball, delta: number) {
|
|||
ball.destroyed = true;
|
||||
runStatistics.balls_lost++;
|
||||
if (!balls.find((b) => !b.destroyed)) {
|
||||
if (perks.extra_life) {
|
||||
perks.extra_life--;
|
||||
resetBalls();
|
||||
sounds.revive();
|
||||
pause(false);
|
||||
coins = [];
|
||||
flashes.push({
|
||||
type: "ball",
|
||||
duration: 500,
|
||||
time: levelTime,
|
||||
size: brickWidth * 2,
|
||||
color: "white",
|
||||
x: ball.x,
|
||||
y: ball.y,
|
||||
});
|
||||
} else {
|
||||
gameOver(
|
||||
"Game Over",
|
||||
"You dropped the ball after catching " + score + " coins. ",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
const hitBrick = ballBrickHitCheck(ball);
|
||||
if (typeof hitBrick !== "undefined") {
|
||||
const initialBrickColor = bricks[hitBrick];
|
||||
|
@ -1328,7 +1338,8 @@ function addToTotalScore(points: number) {
|
|||
"breakout_71_total_score",
|
||||
JSON.stringify(getTotalScore() + points),
|
||||
);
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
function addToTotalPlayTime(ms: number) {
|
||||
|
@ -1340,7 +1351,8 @@ function addToTotalPlayTime(ms: number) {
|
|||
ms,
|
||||
),
|
||||
);
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
function gameOver(title: string, intro: string) {
|
||||
|
@ -1433,7 +1445,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)
|
||||
|
@ -1625,7 +1637,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;
|
||||
}
|
||||
|
||||
|
@ -1676,7 +1688,7 @@ function explodeBrick(index: number, ball: Ball, isExplosion: boolean) {
|
|||
|
||||
ballsColor = color;
|
||||
} else {
|
||||
sounds.comboIncreaseMaybe(ball.x, 1);
|
||||
sounds.comboIncreaseMaybe(combo, ball.x, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1712,18 +1724,10 @@ function render() {
|
|||
needsRender = false;
|
||||
|
||||
const level = currentLevelInfo();
|
||||
const { width, height } = gameCanvas;
|
||||
const {width, height} = gameCanvas;
|
||||
if (!width || !height) return;
|
||||
|
||||
let scoreInfo = "";
|
||||
for (let i = 0; i < perks.extra_life; i++) {
|
||||
scoreInfo += "🖤 ";
|
||||
}
|
||||
|
||||
scoreInfo += "L" + (currentLevel + 1) + "/" + max_levels() + " ";
|
||||
scoreInfo += "$" + score.toString();
|
||||
|
||||
scoreDisplay.innerText = scoreInfo;
|
||||
scoreDisplay.innerText = `L${currentLevel + 1}/${max_levels()} $${score}`;
|
||||
// Clear
|
||||
if (!isSettingOn("basic") && !level.color && level.svg) {
|
||||
// Without this the light trails everything
|
||||
|
@ -1750,7 +1754,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") {
|
||||
|
@ -1798,7 +1802,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") {
|
||||
|
@ -1819,16 +1823,48 @@ function render() {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
// Coins
|
||||
ctx.globalAlpha = 1;
|
||||
|
||||
|
||||
coins.forEach((coin) => {
|
||||
if (!coin.destroyed) {
|
||||
|
||||
ctx.globalCompositeOperation = (coin.color === 'gold' || level.color ? "source-over" : "screen");
|
||||
drawCoin(
|
||||
ctx,
|
||||
coin.color,
|
||||
coinSize,
|
||||
coin.x,
|
||||
coin.y,
|
||||
level.color || "black",
|
||||
coin.a,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Black shadow around balls
|
||||
if (!isSettingOn("basic")) {
|
||||
ctx.globalCompositeOperation = "source-over";
|
||||
ctx.globalAlpha = Math.min(0.8, coins.length / 20);
|
||||
balls.forEach((ball) => {
|
||||
drawBall(ctx, level.color || "#000", ballSize * 6, ball.x, ball.y);
|
||||
});
|
||||
}
|
||||
|
||||
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") {
|
||||
|
@ -1841,28 +1877,15 @@ function render() {
|
|||
}
|
||||
});
|
||||
|
||||
// Coins
|
||||
|
||||
if (perks.extra_life) {
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.globalCompositeOperation = "source-over";
|
||||
coins.forEach((coin) => {
|
||||
if (!coin.destroyed)
|
||||
drawCoin(
|
||||
ctx,
|
||||
coin.color,
|
||||
coinSize,
|
||||
coin.x,
|
||||
coin.y,
|
||||
level.color || "black",
|
||||
coin.a,
|
||||
);
|
||||
});
|
||||
ctx.fillStyle = puckColor;
|
||||
for (let i = 0; i < perks.extra_life; i++) {
|
||||
|
||||
// Black shadow around balls
|
||||
if (coins.length > 10 && !isSettingOn("basic")) {
|
||||
ctx.globalAlpha = Math.min(0.8, (coins.length - 10) / 50);
|
||||
balls.forEach((ball) => {
|
||||
drawBall(ctx, level.color || "#000", ballSize * 6, ball.x, ball.y);
|
||||
});
|
||||
ctx.fillRect(offsetXRoundedDown, gameZoneHeight - puckHeight / 2 + 2 * i, gameZoneWidthRoundedUp, 1);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.globalAlpha = 1;
|
||||
|
@ -1922,6 +1945,7 @@ function render() {
|
|||
false,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
// Borders
|
||||
const hasCombo = combo > baseCombo();
|
||||
|
@ -2343,220 +2367,6 @@ function drawText(
|
|||
);
|
||||
}
|
||||
|
||||
function pixelsToPan(pan: number) {
|
||||
return (pan - offsetX) / gameZoneWidth;
|
||||
}
|
||||
|
||||
let lastComboPlayed = NaN,
|
||||
shepard = 6;
|
||||
|
||||
function playShepard(delta: number, pan: number, volume: number) {
|
||||
const shepardMax = 11,
|
||||
factor = 1.05945594920268,
|
||||
baseNote = 392;
|
||||
shepard += delta;
|
||||
if (shepard > shepardMax) shepard = 0;
|
||||
if (shepard < 0) shepard = shepardMax;
|
||||
|
||||
const play = (note: number) => {
|
||||
const freq = baseNote * Math.pow(factor, note);
|
||||
const diff = Math.abs(note - shepardMax * 0.5);
|
||||
const maxDistanceToIdeal = 1.5 * shepardMax;
|
||||
const vol = Math.max(0, volume * (1 - diff / maxDistanceToIdeal));
|
||||
createSingleBounceSound(freq, pan, vol);
|
||||
return freq.toFixed(2) + " at " + Math.floor(vol * 100) + "% diff " + diff;
|
||||
};
|
||||
|
||||
play(1 + shepardMax + shepard);
|
||||
play(shepard);
|
||||
play(-1 - shepardMax + shepard);
|
||||
}
|
||||
|
||||
const sounds = {
|
||||
wallBeep: (pan: number) => {
|
||||
if (!isSettingOn("sound")) return;
|
||||
createSingleBounceSound(800, pixelsToPan(pan));
|
||||
},
|
||||
|
||||
comboIncreaseMaybe: (x: number, volume: number) => {
|
||||
if (!isSettingOn("sound")) return;
|
||||
let delta = 0;
|
||||
if (!isNaN(lastComboPlayed)) {
|
||||
if (lastComboPlayed < combo) delta = 1;
|
||||
if (lastComboPlayed > combo) delta = -1;
|
||||
}
|
||||
playShepard(delta, pixelsToPan(x), volume);
|
||||
lastComboPlayed = combo;
|
||||
},
|
||||
|
||||
comboDecrease() {
|
||||
if (!isSettingOn("sound")) return;
|
||||
playShepard(-1, 0.5, 0.5);
|
||||
},
|
||||
coinBounce: (pan: number, volume: number) => {
|
||||
if (!isSettingOn("sound")) return;
|
||||
createSingleBounceSound(1200, pixelsToPan(pan), volume, 0.1, "triangle");
|
||||
},
|
||||
explode: (pan: number) => {
|
||||
if (!isSettingOn("sound")) return;
|
||||
createExplosionSound(pixelsToPan(pan));
|
||||
},
|
||||
revive: () => {
|
||||
if (!isSettingOn("sound")) return;
|
||||
createRevivalSound(500);
|
||||
},
|
||||
coinCatch(pan: number) {
|
||||
if (!isSettingOn("sound")) return;
|
||||
createSingleBounceSound(900, pixelsToPan(pan), 0.8, 0.1, "triangle");
|
||||
},
|
||||
};
|
||||
|
||||
// How to play the code on the leftconst context = new window.AudioContext();
|
||||
let audioContext: AudioContext,
|
||||
audioRecordingTrack: MediaStreamAudioDestinationNode;
|
||||
|
||||
function getAudioContext() {
|
||||
if (!audioContext) {
|
||||
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
audioRecordingTrack = audioContext.createMediaStreamDestination();
|
||||
}
|
||||
return audioContext;
|
||||
}
|
||||
|
||||
function createSingleBounceSound(
|
||||
baseFreq = 800,
|
||||
pan = 0.5,
|
||||
volume = 1,
|
||||
duration = 0.1,
|
||||
type: OscillatorType = "sine",
|
||||
) {
|
||||
const context = getAudioContext();
|
||||
// Frequency for the metal "ping"
|
||||
const baseFrequency = baseFreq; // Hz
|
||||
|
||||
// Create an oscillator for the impact sound
|
||||
const oscillator = context.createOscillator();
|
||||
oscillator.type = type;
|
||||
oscillator.frequency.setValueAtTime(baseFrequency, context.currentTime);
|
||||
|
||||
// Create a gain node to control the volume
|
||||
const gainNode = context.createGain();
|
||||
oscillator.connect(gainNode);
|
||||
|
||||
// Create a stereo panner node for left-right panning
|
||||
const panner = context.createStereoPanner();
|
||||
panner.pan.setValueAtTime(pan * 2 - 1, context.currentTime);
|
||||
gainNode.connect(panner);
|
||||
panner.connect(context.destination);
|
||||
panner.connect(audioRecordingTrack);
|
||||
|
||||
// Set up the gain envelope to simulate the impact and quick decay
|
||||
gainNode.gain.setValueAtTime(0.8 * volume, context.currentTime); // Initial impact
|
||||
gainNode.gain.exponentialRampToValueAtTime(
|
||||
0.001,
|
||||
context.currentTime + duration,
|
||||
); // Quick decay
|
||||
|
||||
// Start the oscillator
|
||||
oscillator.start(context.currentTime);
|
||||
|
||||
// Stop the oscillator after the decay
|
||||
oscillator.stop(context.currentTime + duration);
|
||||
}
|
||||
|
||||
function createRevivalSound(baseFreq = 440) {
|
||||
const context = getAudioContext();
|
||||
|
||||
// Create multiple oscillators for a richer sound
|
||||
const oscillators = [
|
||||
context.createOscillator(),
|
||||
context.createOscillator(),
|
||||
context.createOscillator(),
|
||||
];
|
||||
|
||||
// Set the type and frequency for each oscillator
|
||||
oscillators.forEach((osc, index) => {
|
||||
osc.type = "sine";
|
||||
osc.frequency.setValueAtTime(baseFreq + index * 2, context.currentTime); // Slight detuning
|
||||
});
|
||||
|
||||
// Create a gain node to control the volume
|
||||
const gainNode = context.createGain();
|
||||
|
||||
// Connect all oscillators to the gain node
|
||||
oscillators.forEach((osc) => osc.connect(gainNode));
|
||||
|
||||
// Create a stereo panner node for left-right panning
|
||||
const panner = context.createStereoPanner();
|
||||
panner.pan.setValueAtTime(0, context.currentTime); // Center panning
|
||||
gainNode.connect(panner);
|
||||
panner.connect(context.destination);
|
||||
panner.connect(audioRecordingTrack);
|
||||
|
||||
// Set up the gain envelope to simulate a smooth attack and decay
|
||||
gainNode.gain.setValueAtTime(0, context.currentTime); // Start at zero
|
||||
gainNode.gain.linearRampToValueAtTime(0.5, context.currentTime + 0.5); // Ramp up to full volume
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.001, context.currentTime + 2); // Slow decay
|
||||
|
||||
// Start all oscillators
|
||||
oscillators.forEach((osc) => osc.start(context.currentTime));
|
||||
|
||||
// Stop all oscillators after the decay
|
||||
oscillators.forEach((osc) => osc.stop(context.currentTime + 2));
|
||||
}
|
||||
|
||||
let noiseBuffer: AudioBuffer;
|
||||
|
||||
function createExplosionSound(pan = 0.5) {
|
||||
const context = getAudioContext();
|
||||
// Create an audio buffer
|
||||
if (!noiseBuffer) {
|
||||
const bufferSize = context.sampleRate * 2; // 2 seconds
|
||||
noiseBuffer = context.createBuffer(1, bufferSize, context.sampleRate);
|
||||
const output = noiseBuffer.getChannelData(0);
|
||||
|
||||
// Fill the buffer with random noise
|
||||
for (let i = 0; i < bufferSize; i++) {
|
||||
output[i] = Math.random() * 2 - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a noise source
|
||||
const noiseSource = context.createBufferSource();
|
||||
noiseSource.buffer = noiseBuffer;
|
||||
|
||||
// Create a gain node to control the volume
|
||||
const gainNode = context.createGain();
|
||||
noiseSource.connect(gainNode);
|
||||
|
||||
// Create a filter to shape the explosion sound
|
||||
const filter = context.createBiquadFilter();
|
||||
filter.type = "lowpass";
|
||||
filter.frequency.setValueAtTime(1000, context.currentTime); // Set the initial frequency
|
||||
gainNode.connect(filter);
|
||||
|
||||
// Create a stereo panner node for left-right panning
|
||||
const panner = context.createStereoPanner();
|
||||
panner.pan.setValueAtTime(pan * 2 - 1, context.currentTime); // pan 0 to 1 maps to -1 to 1
|
||||
|
||||
// Connect filter to panner and then to the destination (speakers)
|
||||
filter.connect(panner);
|
||||
panner.connect(context.destination);
|
||||
panner.connect(audioRecordingTrack);
|
||||
|
||||
// Ramp down the gain to simulate the explosion's fade-out
|
||||
gainNode.gain.setValueAtTime(1, context.currentTime);
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.01, context.currentTime + 1);
|
||||
|
||||
// Lower the filter frequency over time to create the "explosive" effect
|
||||
filter.frequency.exponentialRampToValueAtTime(60, context.currentTime + 1);
|
||||
|
||||
// Start the noise source
|
||||
noiseSource.start(context.currentTime);
|
||||
|
||||
// Stop the noise source after the sound has played
|
||||
noiseSource.stop(context.currentTime + 1);
|
||||
}
|
||||
|
||||
let levelTime = 0;
|
||||
// Limits skip last to one use per level
|
||||
|
@ -2588,7 +2398,7 @@ function asyncAlert<t>({
|
|||
allowClose = true,
|
||||
textAfterButtons = "",
|
||||
actionsAsGrid = false,
|
||||
}: {
|
||||
}: {
|
||||
title?: string;
|
||||
text?: string;
|
||||
actions?: AsyncAlertAction<t>[];
|
||||
|
@ -2643,7 +2453,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 = `
|
||||
|
@ -2736,7 +2546,8 @@ async function openScorePanel() {
|
|||
{
|
||||
text: "Resume",
|
||||
help: "Return to your run",
|
||||
value: () => {},
|
||||
value: () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
text: "Restart",
|
||||
|
@ -2764,7 +2575,8 @@ async function openSettingsPanel() {
|
|||
{
|
||||
text: "Resume",
|
||||
help: "Return to your run",
|
||||
value() {},
|
||||
value() {
|
||||
},
|
||||
},
|
||||
{
|
||||
text: "Starting perk",
|
||||
|
@ -2911,12 +2723,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
|
||||
|
@ -2929,7 +2741,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],
|
||||
};
|
||||
}),
|
||||
|
@ -3156,8 +2968,9 @@ function startRecordingGame() {
|
|||
captureTrack =
|
||||
captureStream.getVideoTracks()[0] as CanvasCaptureMediaStreamTrack;
|
||||
|
||||
if (isSettingOn("sound") && getAudioContext() && audioRecordingTrack) {
|
||||
captureStream.addTrack(audioRecordingTrack.stream.getAudioTracks()[0]);
|
||||
const track = getAudioRecordingTrack()
|
||||
if (track) {
|
||||
captureStream.addTrack(track.stream.getAudioTracks()[0]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3179,7 +2992,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 (
|
||||
|
|
124
src/git-log.txt
Normal file
124
src/git-log.txt
Normal file
|
@ -0,0 +1,124 @@
|
|||
Lives just save the ball once and show as a line.
|
||||
Deduplicated backgrounds to make the game smaller
|
||||
wip
|
||||
Ignore build files
|
||||
Build and deploy of version 29028296
|
||||
Updated to use typescript strict typing
|
||||
wip
|
||||
Build and deploy of version 29022953
|
||||
Adjustments to creative mode
|
||||
Creative mode, cleanup loop fix
|
||||
Farming points with respawn should be fixed, the sapper-generated bombs no longer count as a broken brick
|
||||
Typed existing game.ts
|
||||
Tried to use ts to catch bugs, it's pretty useless for now.
|
||||
wip
|
||||
Build and deploy of version 29020191
|
||||
Build and deploy of version 29020186
|
||||
Build and deploy of version 29020162
|
||||
Build and deploy of version 29020161
|
||||
Build and deploy of version 29020161
|
||||
Build and deploy of version 29020156
|
||||
Move to parcel
|
||||
Ingnore parcel files
|
||||
When trying a perk or level, actually play the run
|
||||
Automatic deploy 29019801
|
||||
Try to get the horizontally bouncing balls unstuck, and allow ball to go fast when tethered
|
||||
Avoid caching angles of coins that look the same, more coin angles
|
||||
wip
|
||||
Automatic deploy 29019704
|
||||
Better handling of large combo
|
||||
Automatic deploy 29018692
|
||||
Automatic deploy 29017278
|
||||
Avoid fullscreen on ctrl+f
|
||||
Automatic deploy 29017275
|
||||
Added a palette for bricks color
|
||||
wip
|
||||
wip
|
||||
Automatic deploy 29017191
|
||||
Automatic deploy 29015398
|
||||
Added random patterns as background of levels instead of repeating their name
|
||||
Automatic deploy 29015054
|
||||
Fix : bg color of dollar level, puck width and coins centering
|
||||
Automatic deploy 29014379
|
||||
Automatic deploy 29014360
|
||||
Removed gif recording, increased webm recording resolution
|
||||
Feedback
|
||||
Automatic deploy 29014300
|
||||
Removed console.log that were triggering GC, and made all balls the same color to avoid another memory leak and simplify code
|
||||
Automatic deploy 29014045
|
||||
Typos
|
||||
Automatic deploy 29013936
|
||||
Explanation for perks in help, coins speed limit to avoid clipping, adapted coin spawn rate
|
||||
Automatic deploy 29011564
|
||||
Track total play time and added meta description
|
||||
Automatic deploy 29011519
|
||||
Automatic deploy 29011397
|
||||
Typo
|
||||
Automatic deploy 29011331
|
||||
Pause abuse delay, f for fullscreen toggle, keyboard support
|
||||
Automatic deploy 29010156
|
||||
Automatic deploy 29010123
|
||||
Automatic deploy 29010003
|
||||
Automatic deploy 29009984
|
||||
Automatic deploy 29009918
|
||||
Automatic deploy 29008583
|
||||
Automatic deploy 29008176
|
||||
Automatic deploy 29007858
|
||||
wip
|
||||
Automatic deploy 29007301
|
||||
Automatic deploy 29007124
|
||||
Automatic deploy 29005750
|
||||
Automatic deploy 29005697
|
||||
Added statistics (the last ones weren't actually recording anything)
|
||||
Automatic deploy 29002316
|
||||
Automatic deploy 29002312
|
||||
Automatic deploy 29002304
|
||||
Automatic deploy 29002302
|
||||
Automatic deploy 29002301
|
||||
Automatic deploy 29002295
|
||||
wip
|
||||
Automatic deploy 29000827
|
||||
Automatic deploy 29000798
|
||||
Automatic deploy 29000794
|
||||
Automatic deploy 28999986
|
||||
Wind perk
|
||||
wip
|
||||
Automatic deploy 28999931
|
||||
wip
|
||||
Automatic deploy 28999417
|
||||
wip
|
||||
wip
|
||||
Automatic deploy 28998184
|
||||
Adjusted neon effect, more screenshake after bigger_explosion perk is chosen
|
||||
Automatic deploy 28996852
|
||||
Automatic deploy 28996655
|
||||
Automatic deploy 28996651
|
||||
wip
|
||||
Automatic deploy 28996415
|
||||
Fixed small issues
|
||||
wip
|
||||
wip
|
||||
wip
|
||||
wip
|
||||
wip
|
||||
Automatic deploy 28994348
|
||||
wip
|
||||
wip
|
||||
wip
|
||||
Added some fastlane screenshots in repo for fdroid
|
||||
Automatic deploy 28994244
|
||||
Automatic deploy 28994243
|
||||
wip
|
||||
Automatic deploy 28994240
|
||||
wip
|
||||
Automatic deploy 28994204
|
||||
wip
|
||||
wip
|
||||
Automatic deploy 28994174
|
||||
wip
|
||||
Automatic deploy 28994149
|
||||
Automatic deploy 28994147
|
||||
wip
|
||||
wip
|
||||
Added GPL3 license
|
||||
Initial commit
|
223
src/sounds.ts
Normal file
223
src/sounds.ts
Normal file
|
@ -0,0 +1,223 @@
|
|||
import {gameZoneWidthRoundedUp, isSettingOn, offsetX, offsetXRoundedDown} from "./game";
|
||||
|
||||
export const sounds = {
|
||||
wallBeep: (pan: number) => {
|
||||
if (!isSettingOn("sound")) return;
|
||||
createSingleBounceSound(800, pixelsToPan(pan));
|
||||
},
|
||||
|
||||
comboIncreaseMaybe: (combo: number, x: number, volume: number) => {
|
||||
if (!isSettingOn("sound")) return;
|
||||
let delta = 0;
|
||||
if (!isNaN(lastComboPlayed)) {
|
||||
if (lastComboPlayed < combo) delta = 1;
|
||||
if (lastComboPlayed > combo) delta = -1;
|
||||
}
|
||||
playShepard(delta, pixelsToPan(x), volume);
|
||||
lastComboPlayed = combo;
|
||||
},
|
||||
|
||||
comboDecrease() {
|
||||
if (!isSettingOn("sound")) return;
|
||||
playShepard(-1, 0.5, 0.5);
|
||||
},
|
||||
coinBounce: (pan: number, volume: number) => {
|
||||
if (!isSettingOn("sound")) return;
|
||||
createSingleBounceSound(1200, pixelsToPan(pan), volume, 0.1, "triangle");
|
||||
},
|
||||
explode: (pan: number) => {
|
||||
if (!isSettingOn("sound")) return;
|
||||
createExplosionSound(pixelsToPan(pan));
|
||||
},
|
||||
lifeLost(pan:number){
|
||||
if (!isSettingOn("sound")) return;
|
||||
createShatteredGlassSound(pixelsToPan(pan))
|
||||
},
|
||||
|
||||
coinCatch(pan: number) {
|
||||
if (!isSettingOn("sound")) return;
|
||||
createSingleBounceSound(900, pixelsToPan(pan), 0.8, 0.1, "triangle");
|
||||
},
|
||||
};
|
||||
|
||||
// How to play the code on the leftconst context = new window.AudioContext();
|
||||
let audioContext: AudioContext, audioRecordingTrack: MediaStreamAudioDestinationNode;
|
||||
|
||||
export function getAudioContext() {
|
||||
if (!audioContext) {
|
||||
if (!isSettingOn('sound')) return null
|
||||
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
audioRecordingTrack = audioContext.createMediaStreamDestination();
|
||||
}
|
||||
return audioContext;
|
||||
}
|
||||
|
||||
export function getAudioRecordingTrack() {
|
||||
getAudioContext()
|
||||
return audioRecordingTrack
|
||||
}
|
||||
|
||||
function createSingleBounceSound(
|
||||
baseFreq = 800,
|
||||
pan = 0.5,
|
||||
volume = 1,
|
||||
duration = 0.1,
|
||||
type: OscillatorType = "sine",
|
||||
) {
|
||||
const context = getAudioContext();
|
||||
if (!context) return
|
||||
const oscillator = createOscillator(context, baseFreq, type);
|
||||
|
||||
// Create a gain node to control the volume
|
||||
const gainNode = context.createGain();
|
||||
oscillator.connect(gainNode);
|
||||
|
||||
// Create a stereo panner node for left-right panning
|
||||
const panner = context.createStereoPanner();
|
||||
panner.pan.setValueAtTime(pan * 2 - 1, context.currentTime);
|
||||
gainNode.connect(panner);
|
||||
panner.connect(context.destination);
|
||||
panner.connect(audioRecordingTrack);
|
||||
|
||||
// Set up the gain envelope to simulate the impact and quick decay
|
||||
gainNode.gain.setValueAtTime(0.8 * volume, context.currentTime); // Initial impact
|
||||
gainNode.gain.exponentialRampToValueAtTime(
|
||||
0.001,
|
||||
context.currentTime + duration,
|
||||
); // Quick decay
|
||||
|
||||
// Start the oscillator
|
||||
oscillator.start(context.currentTime);
|
||||
|
||||
// Stop the oscillator after the decay
|
||||
oscillator.stop(context.currentTime + duration);
|
||||
}
|
||||
|
||||
let noiseBuffer: AudioBuffer;
|
||||
|
||||
function getNoiseBuffer(context:AudioContext) {
|
||||
|
||||
if (!noiseBuffer) {
|
||||
const bufferSize = context.sampleRate * 2; // 2 seconds
|
||||
noiseBuffer = context.createBuffer(1, bufferSize, context.sampleRate);
|
||||
const output = noiseBuffer.getChannelData(0);
|
||||
|
||||
// Fill the buffer with random noise
|
||||
for (let i = 0; i < bufferSize; i++) {
|
||||
output[i] = Math.random() * 2 - 1;
|
||||
}
|
||||
}
|
||||
return noiseBuffer
|
||||
}
|
||||
|
||||
function createExplosionSound(pan = 0.5) {
|
||||
const context = getAudioContext();
|
||||
if (!context) return
|
||||
// Create an audio buffer
|
||||
|
||||
|
||||
// Create a noise source
|
||||
const noiseSource = context.createBufferSource();
|
||||
noiseSource.buffer = getNoiseBuffer(context);
|
||||
|
||||
// Create a gain node to control the volume
|
||||
const gainNode = context.createGain();
|
||||
noiseSource.connect(gainNode);
|
||||
|
||||
// Create a filter to shape the explosion sound
|
||||
const filter = context.createBiquadFilter();
|
||||
filter.type = "lowpass";
|
||||
filter.frequency.setValueAtTime(1000, context.currentTime); // Set the initial frequency
|
||||
gainNode.connect(filter);
|
||||
|
||||
// Create a stereo panner node for left-right panning
|
||||
const panner = context.createStereoPanner();
|
||||
panner.pan.setValueAtTime(pan * 2 - 1, context.currentTime); // pan 0 to 1 maps to -1 to 1
|
||||
|
||||
// Connect filter to panner and then to the destination (speakers)
|
||||
filter.connect(panner);
|
||||
panner.connect(context.destination);
|
||||
panner.connect(audioRecordingTrack);
|
||||
|
||||
// Ramp down the gain to simulate the explosion's fade-out
|
||||
gainNode.gain.setValueAtTime(1, context.currentTime);
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.01, context.currentTime + 1);
|
||||
|
||||
// Lower the filter frequency over time to create the "explosive" effect
|
||||
filter.frequency.exponentialRampToValueAtTime(60, context.currentTime + 1);
|
||||
|
||||
// Start the noise source
|
||||
noiseSource.start(context.currentTime);
|
||||
|
||||
// Stop the noise source after the sound has played
|
||||
noiseSource.stop(context.currentTime + 1);
|
||||
}
|
||||
|
||||
function pixelsToPan(pan: number) {
|
||||
return Math.max(0, Math.min(1, (pan - offsetXRoundedDown) / gameZoneWidthRoundedUp));
|
||||
}
|
||||
|
||||
let lastComboPlayed = NaN,
|
||||
shepard = 6;
|
||||
|
||||
function playShepard(delta: number, pan: number, volume: number) {
|
||||
const shepardMax = 11,
|
||||
factor = 1.05945594920268,
|
||||
baseNote = 392;
|
||||
shepard += delta;
|
||||
if (shepard > shepardMax) shepard = 0;
|
||||
if (shepard < 0) shepard = shepardMax;
|
||||
|
||||
const play = (note: number) => {
|
||||
const freq = baseNote * Math.pow(factor, note);
|
||||
const diff = Math.abs(note - shepardMax * 0.5);
|
||||
const maxDistanceToIdeal = 1.5 * shepardMax;
|
||||
const vol = Math.max(0, volume * (1 - diff / maxDistanceToIdeal));
|
||||
createSingleBounceSound(freq, pan, vol);
|
||||
return freq.toFixed(2) + " at " + Math.floor(vol * 100) + "% diff " + diff;
|
||||
};
|
||||
|
||||
play(1 + shepardMax + shepard);
|
||||
play(shepard);
|
||||
play(-1 - shepardMax + shepard);
|
||||
}
|
||||
|
||||
function createShatteredGlassSound(pan:number) {
|
||||
const context = getAudioContext();
|
||||
if (!context) return
|
||||
const oscillators = [
|
||||
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));
|
||||
noiseSource.connect(gainNode);
|
||||
gainNode.gain.setValueAtTime(0.2, context.currentTime);
|
||||
oscillators.forEach(oscillator => oscillator.start());
|
||||
noiseSource.start();
|
||||
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);
|
||||
gainNode.connect(panner);
|
||||
panner.connect(context.destination);
|
||||
panner.connect(audioRecordingTrack);
|
||||
|
||||
gainNode.connect(panner);
|
||||
}
|
||||
|
||||
// Helper function to create an oscillator with a specific frequency
|
||||
function createOscillator(context:AudioContext, frequency:number, type:OscillatorType) {
|
||||
const oscillator = context.createOscillator();
|
||||
oscillator.type =type
|
||||
oscillator.frequency.setValueAtTime(frequency, context.currentTime);
|
||||
return oscillator;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue