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
|
# the version number is just a unix timestamp in minutes
|
||||||
versionCode=$(($(date +%s) / 60))
|
versionCode=$(($(date +%s) / 60))
|
||||||
|
|
||||||
|
|
||||||
# Replace the version code and name in gradle for fdroid and play store
|
# Replace the version code and name in gradle for fdroid and play store
|
||||||
sed -i -e "s/^[[:space:]]*versionCode = .*/ versionCode = $versionCode/" \
|
sed -i -e "s/^[[:space:]]*versionCode = .*/ versionCode = $versionCode/" \
|
||||||
-e "s/^[[:space:]]*versionName = .*/ versionName = \"$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
|
# 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=
|
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/
|
npx prettier --write src/
|
||||||
|
|
||||||
npm run build
|
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 {
|
import {
|
||||||
Ball,
|
Ball,
|
||||||
BallLike,
|
BallLike,
|
||||||
|
@ -12,7 +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";
|
||||||
|
|
||||||
const MAX_COINS = 400;
|
const MAX_COINS = 400;
|
||||||
const MAX_PARTICLES = 600;
|
const MAX_PARTICLES = 600;
|
||||||
|
@ -116,9 +117,7 @@ let running = false,
|
||||||
function play() {
|
function play() {
|
||||||
if (running) return;
|
if (running) return;
|
||||||
running = true;
|
running = true;
|
||||||
if (audioContext) {
|
getAudioContext()?.resume().then();
|
||||||
audioContext.resume().then();
|
|
||||||
}
|
|
||||||
resumeRecording();
|
resumeRecording();
|
||||||
document.body.className = running ? " running " : " paused ";
|
document.body.className = running ? " running " : " paused ";
|
||||||
}
|
}
|
||||||
|
@ -131,11 +130,11 @@ function pause(playerAskedForPause: boolean) {
|
||||||
() => {
|
() => {
|
||||||
running = false;
|
running = false;
|
||||||
needsRender = true;
|
needsRender = true;
|
||||||
if (audioContext) {
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!running) audioContext.suspend().then();
|
if (!running) getAudioContext()?.suspend().then();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
|
||||||
pauseRecording();
|
pauseRecording();
|
||||||
pauseTimeout = null;
|
pauseTimeout = null;
|
||||||
document.body.className = running ? " running " : " paused ";
|
document.body.className = running ? " running " : " paused ";
|
||||||
|
@ -153,7 +152,7 @@ function pause(playerAskedForPause: boolean) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let offsetX: number,
|
export let offsetX: number,
|
||||||
offsetXRoundedDown: number,
|
offsetXRoundedDown: number,
|
||||||
gameZoneWidth: number,
|
gameZoneWidth: number,
|
||||||
gameZoneWidthRoundedUp: number,
|
gameZoneWidthRoundedUp: number,
|
||||||
|
@ -170,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,8 +205,8 @@ window.addEventListener("resize", fitSize);
|
||||||
window.addEventListener("fullscreenchange", fitSize);
|
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);
|
||||||
|
|
||||||
|
@ -543,7 +542,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)
|
||||||
|
@ -704,7 +703,7 @@ function shouldPierceByColor(
|
||||||
function ballBrickHitCheck(ball: Ball) {
|
function ballBrickHitCheck(ball: Ball) {
|
||||||
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);
|
||||||
|
@ -746,7 +745,7 @@ function ballBrickHitCheck(ball: Ball) {
|
||||||
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);
|
||||||
|
@ -804,17 +803,17 @@ 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;
|
coin.x = offsetXRoundedDown + radius + (offsetXRoundedDown + radius - coin.x);
|
||||||
coin.vx *= -1;
|
coin.vx *= -1;
|
||||||
hhit = 1;
|
hhit = 1;
|
||||||
}
|
}
|
||||||
if (coin.y < radius) {
|
if (coin.y < radius) {
|
||||||
coin.y = radius;
|
coin.y = radius + (radius - coin.y);
|
||||||
coin.vy *= -1;
|
coin.vy *= -1;
|
||||||
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.vx *= -1;
|
coin.vx *= -1;
|
||||||
hhit = 1;
|
hhit = 1;
|
||||||
}
|
}
|
||||||
|
@ -822,6 +821,7 @@ 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() {
|
||||||
|
@ -878,13 +878,14 @@ function tick() {
|
||||||
coins.forEach((coin) => {
|
coins.forEach((coin) => {
|
||||||
if (coin.destroyed) return;
|
if (coin.destroyed) return;
|
||||||
if (perks.coin_magnet) {
|
if (perks.coin_magnet) {
|
||||||
coin.vx +=
|
const attractionX = ((delta * (puck - coin.x)) /
|
||||||
((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.sa -= attractionX / 10
|
||||||
}
|
}
|
||||||
|
|
||||||
const ratio = 1 - (perks.viscosity * 0.03 + 0.005) * delta;
|
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")) {
|
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;
|
||||||
|
@ -1173,22 +1174,48 @@ 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
|
||||||
if (
|
if (
|
||||||
ball.y > ylimit &&
|
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 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);
|
||||||
ball.vx = speed * Math.cos(angle);
|
ball.vx = speed * Math.cos(angle);
|
||||||
ball.vy = speed * Math.sin(angle);
|
ball.vy = speed * Math.sin(angle);
|
||||||
|
|
||||||
sounds.wallBeep(ball.x);
|
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) {
|
if (perks.streak_shots) {
|
||||||
resetCombo(ball.x, ball.y);
|
resetCombo(ball.x, ball.y);
|
||||||
}
|
}
|
||||||
|
@ -1197,7 +1224,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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1233,29 +1260,12 @@ function ballTick(ball: Ball, delta: number) {
|
||||||
ball.destroyed = true;
|
ball.destroyed = true;
|
||||||
runStatistics.balls_lost++;
|
runStatistics.balls_lost++;
|
||||||
if (!balls.find((b) => !b.destroyed)) {
|
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(
|
gameOver(
|
||||||
"Game Over",
|
"Game Over",
|
||||||
"You dropped the ball after catching " + score + " coins. ",
|
"You dropped the ball after catching " + score + " coins. ",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
const hitBrick = ballBrickHitCheck(ball);
|
const hitBrick = ballBrickHitCheck(ball);
|
||||||
if (typeof hitBrick !== "undefined") {
|
if (typeof hitBrick !== "undefined") {
|
||||||
const initialBrickColor = bricks[hitBrick];
|
const initialBrickColor = bricks[hitBrick];
|
||||||
|
@ -1328,7 +1338,8 @@ 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) {
|
||||||
|
@ -1340,7 +1351,8 @@ function addToTotalPlayTime(ms: number) {
|
||||||
ms,
|
ms,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {}
|
} catch (e) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function gameOver(title: string, intro: string) {
|
function gameOver(title: string, intro: string) {
|
||||||
|
@ -1433,7 +1445,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)
|
||||||
|
@ -1625,7 +1637,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1676,7 +1688,7 @@ function explodeBrick(index: number, ball: Ball, isExplosion: boolean) {
|
||||||
|
|
||||||
ballsColor = color;
|
ballsColor = color;
|
||||||
} else {
|
} else {
|
||||||
sounds.comboIncreaseMaybe(ball.x, 1);
|
sounds.comboIncreaseMaybe(combo, ball.x, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1712,18 +1724,10 @@ 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;
|
||||||
|
|
||||||
let scoreInfo = "";
|
scoreDisplay.innerText = `L${currentLevel + 1}/${max_levels()} $${score}`;
|
||||||
for (let i = 0; i < perks.extra_life; i++) {
|
|
||||||
scoreInfo += "🖤 ";
|
|
||||||
}
|
|
||||||
|
|
||||||
scoreInfo += "L" + (currentLevel + 1) + "/" + max_levels() + " ";
|
|
||||||
scoreInfo += "$" + score.toString();
|
|
||||||
|
|
||||||
scoreDisplay.innerText = scoreInfo;
|
|
||||||
// Clear
|
// Clear
|
||||||
if (!isSettingOn("basic") && !level.color && level.svg) {
|
if (!isSettingOn("basic") && !level.color && level.svg) {
|
||||||
// Without this the light trails everything
|
// Without this the light trails everything
|
||||||
|
@ -1750,7 +1754,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") {
|
||||||
|
@ -1798,7 +1802,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") {
|
||||||
|
@ -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";
|
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") {
|
||||||
|
@ -1841,28 +1877,15 @@ function render() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Coins
|
|
||||||
|
if (perks.extra_life) {
|
||||||
ctx.globalAlpha = 1;
|
ctx.globalAlpha = 1;
|
||||||
ctx.globalCompositeOperation = "source-over";
|
ctx.globalCompositeOperation = "source-over";
|
||||||
coins.forEach((coin) => {
|
ctx.fillStyle = puckColor;
|
||||||
if (!coin.destroyed)
|
for (let i = 0; i < perks.extra_life; i++) {
|
||||||
drawCoin(
|
|
||||||
ctx,
|
|
||||||
coin.color,
|
|
||||||
coinSize,
|
|
||||||
coin.x,
|
|
||||||
coin.y,
|
|
||||||
level.color || "black",
|
|
||||||
coin.a,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Black shadow around balls
|
ctx.fillRect(offsetXRoundedDown, gameZoneHeight - puckHeight / 2 + 2 * i, gameZoneWidthRoundedUp, 1);
|
||||||
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.globalAlpha = 1;
|
ctx.globalAlpha = 1;
|
||||||
|
@ -1922,6 +1945,7 @@ function render() {
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
// Borders
|
// Borders
|
||||||
const hasCombo = combo > baseCombo();
|
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;
|
let levelTime = 0;
|
||||||
// Limits skip last to one use per level
|
// Limits skip last to one use per level
|
||||||
|
@ -2588,7 +2398,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>[];
|
||||||
|
@ -2643,7 +2453,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 = `
|
||||||
|
@ -2736,7 +2546,8 @@ async function openScorePanel() {
|
||||||
{
|
{
|
||||||
text: "Resume",
|
text: "Resume",
|
||||||
help: "Return to your run",
|
help: "Return to your run",
|
||||||
value: () => {},
|
value: () => {
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Restart",
|
text: "Restart",
|
||||||
|
@ -2764,7 +2575,8 @@ 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",
|
||||||
|
@ -2911,12 +2723,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
|
||||||
|
@ -2929,7 +2741,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],
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
@ -3156,8 +2968,9 @@ function startRecordingGame() {
|
||||||
captureTrack =
|
captureTrack =
|
||||||
captureStream.getVideoTracks()[0] as CanvasCaptureMediaStreamTrack;
|
captureStream.getVideoTracks()[0] as CanvasCaptureMediaStreamTrack;
|
||||||
|
|
||||||
if (isSettingOn("sound") && getAudioContext() && audioRecordingTrack) {
|
const track = getAudioRecordingTrack()
|
||||||
captureStream.addTrack(audioRecordingTrack.stream.getAudioTracks()[0]);
|
if (track) {
|
||||||
|
captureStream.addTrack(track.stream.getAudioTracks()[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3179,7 +2992,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 (
|
||||||
|
|
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