mirror of
https://gitlab.com/lecarore/breakout71.git
synced 2025-04-24 14:06:16 -04:00
5 colors /level, sound when ball or brick change color
This commit is contained in:
parent
b6fe46c9bc
commit
2e3ab3011f
21 changed files with 1379 additions and 598 deletions
58
src/combo.ts
Normal file
58
src/combo.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import {GameState} from "./types";
|
||||
import {sounds} from "./sounds";
|
||||
|
||||
export function baseCombo(gameState: GameState) {
|
||||
return 1 + gameState.perks.base_combo * 3 + gameState.perks.smaller_puck * 5;
|
||||
}
|
||||
|
||||
export function resetCombo(gameState: GameState, x: number | undefined, y: number | undefined) {
|
||||
const prev = gameState.combo;
|
||||
gameState.combo = baseCombo(gameState);
|
||||
if (!gameState.levelTime) {
|
||||
gameState.combo += gameState.perks.hot_start * 15;
|
||||
}
|
||||
if (prev > gameState.combo && gameState.perks.soft_reset) {
|
||||
gameState.combo += Math.floor((prev - gameState.combo) / (1 + gameState.perks.soft_reset));
|
||||
}
|
||||
const lost = Math.max(0, prev - gameState.combo);
|
||||
if (lost) {
|
||||
for (let i = 0; i < lost && i < 8; i++) {
|
||||
setTimeout(() => sounds.comboDecrease(), i * 100);
|
||||
}
|
||||
if (typeof x !== "undefined" && typeof y !== "undefined") {
|
||||
gameState.flashes.push({
|
||||
type: "text",
|
||||
text: "-" + lost,
|
||||
time: gameState.levelTime,
|
||||
color: "red",
|
||||
x: x,
|
||||
y: y,
|
||||
duration: 150,
|
||||
size: gameState.puckHeight,
|
||||
});
|
||||
}
|
||||
}
|
||||
return lost;
|
||||
}
|
||||
|
||||
export function decreaseCombo(gameState: GameState, by: number, x: number, y: number) {
|
||||
const prev = gameState.combo;
|
||||
gameState.combo = Math.max(baseCombo(gameState), gameState.combo - by);
|
||||
const lost = Math.max(0, prev - gameState.combo);
|
||||
|
||||
if (lost) {
|
||||
sounds.comboDecrease();
|
||||
if (typeof x !== "undefined" && typeof y !== "undefined") {
|
||||
gameState.flashes.push({
|
||||
type: "text",
|
||||
text: "-" + lost,
|
||||
time: gameState.levelTime,
|
||||
color: "red",
|
||||
x: x,
|
||||
y: y,
|
||||
duration: 300,
|
||||
size: gameState.puckHeight,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
144
src/game.ts
144
src/game.ts
|
@ -6,18 +6,19 @@ import {
|
|||
colorString,
|
||||
GameState,
|
||||
PerkId,
|
||||
PerksMap,
|
||||
RunHistoryItem, RunParams,
|
||||
RunHistoryItem,
|
||||
RunParams,
|
||||
RunStats,
|
||||
Upgrade,
|
||||
} from "./types";
|
||||
import {OptionId, options} from "./options";
|
||||
import {getAudioContext, getAudioRecordingTrack, sounds} from "./sounds";
|
||||
import {putBallsAtPuck, resetBalls} from "./resetBalls";
|
||||
import {sumOfKeys} from "./game_utils";
|
||||
import {makeEmptyPerksMap, sumOfKeys} from "./game_utils";
|
||||
import {baseCombo, decreaseCombo, resetCombo} from "./combo";
|
||||
|
||||
|
||||
export const gameCanvas = document.getElementById("game") as HTMLCanvasElement;
|
||||
const gameCanvas = document.getElementById("game") as HTMLCanvasElement;
|
||||
const ctx = gameCanvas.getContext("2d", {
|
||||
alpha: false,
|
||||
}) as CanvasRenderingContext2D;
|
||||
|
@ -29,69 +30,6 @@ bombSVG.src =
|
|||
<path d="m6.1528 26.516c-2.6992-3.4942-2.9332-8.281-.58305-11.981a10.454 10.454 0 017.3701-4.7582c1.962-.27726 4.1646.05953 5.8835.90027l.45013.22017.89782-.87417c.83748-.81464.91169-.87499 1.0992-.90271.40528-.058713.58876.03425 1.1971.6116l.55451.52679 1.0821-1.0821c1.1963-1.1963 1.383-1.3357 2.1039-1.5877.57898-.20223 1.5681-.19816 2.1691.00897 1.4613.50314 2.3673 1.7622 2.3567 3.2773-.0058.95654-.24464 1.5795-.90924 2.3746-.40936.48928-.55533.81057-.57898 1.2737-.02039.41018.1109.77714.42322 1.1792.30172.38816.3694.61323.2797.93044-.12803.45666-.56674.71598-1.0242.60507-.601-.14597-1.3031-1.3088-1.3969-2.3126-.09459-1.0161.19245-1.8682.92392-2.7432.42567-.50885.5643-.82851.5643-1.3031 0-.50151-.14026-.83177-.51211-1.2028-.50966-.50966-1.0968-.64829-1.781-.41996l-.37348.12477-2.1006 2.1006.52597.55696c.45421.48194.5325.58876.57898.78855.09622.41588.07502.45014-.88396 1.4548l-.87173.9125.26339.57979a10.193 10.193 0 01.9231 4.1001c.03996 2.046-.41996 3.8082-1.4442 5.537-.55044.928-1.0185 1.5013-1.8968 2.3241-.83503.78284-1.5526 1.2827-2.4904 1.7361-3.4266 1.657-7.4721 1.3422-10.549-.82035-.73473-.51782-1.7312-1.4621-2.2515-2.1357zm21.869-4.5584c-.0579-.19734-.05871-2.2662 0-2.4545.11906-.39142.57898-.63361 1.0038-.53005.23812.05708.54147.32455.6116.5382.06279.19163.06769 2.1805.0065 2.3811-.12558.40773-.61649.67602-1.0462.57164-.234-.05708-.51615-.30498-.57568-.50722m3.0417-2.6013c-.12313-.6222.37837-1.1049 1.0479-1.0079.18348.0261.25279.08399 1.0071.83911.75838.75838.81301.82362.84074 1.0112.10193.68499-.40365 1.1938-1.034 1.0405-.1949-.0473-.28786-.12558-1.0144-.85216-.7649-.76409-.80241-.81057-.84645-1.0316m.61323-3.0629a.85623.85623 0 01.59284-.99975c.28949-.09214 2.1814-.08318 2.3917.01141.38734.17369.6279.61078.53984.98181-.06035.25606-.35391.57327-.60181.64992-.25279.07747-2.2278.053-2.4097-.03017-.26013-.11906-.46318-.36125-.51374-.61323" fill="#fff" opacity="0.3"/>
|
||||
</svg>`);
|
||||
|
||||
const makeEmptyPerksMap = () => {
|
||||
const p = {} as any;
|
||||
upgrades.forEach((u) => (p[u.id] = 0));
|
||||
return p as PerksMap;
|
||||
};
|
||||
|
||||
|
||||
export function baseCombo() {
|
||||
return 1 + gameState.perks.base_combo * 3 + gameState.perks.smaller_puck * 5;
|
||||
}
|
||||
|
||||
export function resetCombo(x: number | undefined, y: number | undefined) {
|
||||
const prev = gameState.combo;
|
||||
gameState.combo = baseCombo();
|
||||
if (!gameState.levelTime) {
|
||||
gameState.combo += gameState.perks.hot_start * 15;
|
||||
}
|
||||
if (prev > gameState.combo && gameState.perks.soft_reset) {
|
||||
gameState.combo += Math.floor((prev - gameState.combo) / (1 + gameState.perks.soft_reset));
|
||||
}
|
||||
const lost = Math.max(0, prev - gameState.combo);
|
||||
if (lost) {
|
||||
for (let i = 0; i < lost && i < 8; i++) {
|
||||
setTimeout(() => sounds.comboDecrease(), i * 100);
|
||||
}
|
||||
if (typeof x !== "undefined" && typeof y !== "undefined") {
|
||||
gameState.flashes.push({
|
||||
type: "text",
|
||||
text: "-" + lost,
|
||||
time: gameState.levelTime,
|
||||
color: "red",
|
||||
x: x,
|
||||
y: y,
|
||||
duration: 150,
|
||||
size: gameState.puckHeight,
|
||||
});
|
||||
}
|
||||
}
|
||||
return lost;
|
||||
}
|
||||
|
||||
export function decreaseCombo(by: number, x: number, y: number) {
|
||||
const prev = gameState.combo;
|
||||
gameState.combo = Math.max(baseCombo(), gameState.combo - by);
|
||||
const lost = Math.max(0, prev - gameState.combo);
|
||||
|
||||
if (lost) {
|
||||
sounds.comboDecrease();
|
||||
if (typeof x !== "undefined" && typeof y !== "undefined") {
|
||||
gameState.flashes.push({
|
||||
type: "text",
|
||||
text: "-" + lost,
|
||||
time: gameState.levelTime,
|
||||
color: "red",
|
||||
x: x,
|
||||
y: y,
|
||||
duration: 300,
|
||||
size: gameState.puckHeight,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function play() {
|
||||
if (gameState.running) return;
|
||||
|
@ -341,7 +279,7 @@ async function openUpgradesPicker() {
|
|||
|
||||
gameState.runStatistics.upgrades_picked++;
|
||||
}
|
||||
resetCombo(undefined, undefined);
|
||||
resetCombo(gameState, undefined, undefined);
|
||||
resetBalls(gameState);
|
||||
}
|
||||
|
||||
|
@ -361,7 +299,7 @@ export function setLevel(l: number) {
|
|||
gameState.levelMisses = 0;
|
||||
gameState.runStatistics.levelsPlayed++;
|
||||
|
||||
resetCombo(undefined, undefined);
|
||||
resetCombo(gameState, undefined, undefined);
|
||||
recomputeTargetBaseSpeed();
|
||||
resetBalls(gameState);
|
||||
|
||||
|
@ -536,7 +474,7 @@ export function shouldPierceByColor(
|
|||
|
||||
export function coinBrickHitCheck(coin: Coin) {
|
||||
// Make ball/coin bonce, and return bricks that were hit
|
||||
const radius = gameState.coinSize / 2;
|
||||
const radius = coin.size / 2;
|
||||
const {x, y, previousX, previousY} = coin;
|
||||
|
||||
const vhit = hitsSomething(previousX, y, radius);
|
||||
|
@ -646,7 +584,7 @@ export function tick() {
|
|||
|
||||
if (gameState.levelTime > gameState.lastTickDown + 1000 && gameState.perks.hot_start) {
|
||||
gameState.lastTickDown = gameState.levelTime;
|
||||
decreaseCombo(gameState.perks.hot_start, gameState.puckPosition, gameState.gameZoneHeight - 2 * gameState.puckHeight);
|
||||
decreaseCombo(gameState, gameState.perks.hot_start, gameState.puckPosition, gameState.gameZoneHeight - 2 * gameState.puckHeight);
|
||||
}
|
||||
|
||||
if (remainingBricks <= gameState.perks.skip_last && !gameState.autoCleanUses) {
|
||||
|
@ -662,8 +600,8 @@ export function tick() {
|
|||
setLevel(gameState.currentLevel + 1);
|
||||
} else {
|
||||
gameOver(
|
||||
"Run finished with " + gameState.score + " points",
|
||||
"You cleared all levels for this run.",
|
||||
"Run finished ",
|
||||
`You cleared all levels for this run, catching ${gameState.score} coins in total.`,
|
||||
);
|
||||
}
|
||||
} else if (gameState.running || gameState.levelTime) {
|
||||
|
@ -698,7 +636,7 @@ export function tick() {
|
|||
coin.vy += delta * coin.weight * 0.8;
|
||||
|
||||
const speed = Math.abs(coin.sx) + Math.abs(coin.sx);
|
||||
const hitBorder = bordersHitCheck(coin, coinRadius, delta);
|
||||
const hitBorder = bordersHitCheck(coin, coin.size / 2, delta);
|
||||
|
||||
if (
|
||||
coin.y > gameState.gameZoneHeight - coinRadius - gameState.puckHeight &&
|
||||
|
@ -712,7 +650,7 @@ export function tick() {
|
|||
} else if (coin.y > gameState.canvasHeight + coinRadius) {
|
||||
coin.destroyed = true;
|
||||
if (gameState.perks.compound_interest) {
|
||||
resetCombo(coin.x, coin.y);
|
||||
resetCombo(gameState, coin.x, coin.y);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -727,6 +665,7 @@ export function tick() {
|
|||
) {
|
||||
gameState.bricks[hitBrick] = coin.color;
|
||||
coin.coloredABrick = true;
|
||||
sounds.colorChange(coin.x,0.3)
|
||||
}
|
||||
}
|
||||
if (typeof hitBrick !== "undefined" || hitBorder) {
|
||||
|
@ -783,10 +722,10 @@ export function tick() {
|
|||
});
|
||||
}
|
||||
|
||||
if (gameState.combo > baseCombo()) {
|
||||
if (gameState.combo > baseCombo(gameState)) {
|
||||
// The red should still be visible on a white bg
|
||||
const baseParticle = !isSettingOn("basic") &&
|
||||
(gameState.combo - baseCombo()) * Math.random() > 5 &&
|
||||
(gameState.combo - baseCombo(gameState)) * Math.random() > 5 &&
|
||||
gameState.running && {
|
||||
type: "particle" as const,
|
||||
duration: 100 * (Math.random() + 1),
|
||||
|
@ -955,7 +894,7 @@ export function ballTick(ball: Ball, delta: number) {
|
|||
borderHitCode % 2 &&
|
||||
ball.x < gameState.offsetX + gameState.gameZoneWidth / 2
|
||||
) {
|
||||
resetCombo(ball.x, ball.y);
|
||||
resetCombo(gameState, ball.x, ball.y);
|
||||
}
|
||||
|
||||
if (
|
||||
|
@ -963,11 +902,11 @@ export function ballTick(ball: Ball, delta: number) {
|
|||
borderHitCode % 2 &&
|
||||
ball.x > gameState.offsetX + gameState.gameZoneWidth / 2
|
||||
) {
|
||||
resetCombo(ball.x, ball.y);
|
||||
resetCombo(gameState, ball.x, ball.y);
|
||||
}
|
||||
|
||||
if (gameState.perks.top_is_lava && borderHitCode >= 2) {
|
||||
resetCombo(ball.x, ball.y + gameState.ballSize);
|
||||
resetCombo(gameState, ball.x, ball.y + gameState.ballSize);
|
||||
}
|
||||
sounds.wallBeep(ball.x);
|
||||
ball.bouncesList?.push({x: ball.previousX, y: ball.previousY});
|
||||
|
@ -1010,7 +949,7 @@ export function ballTick(ball: Ball, delta: number) {
|
|||
}
|
||||
}
|
||||
if (gameState.perks.streak_shots) {
|
||||
resetCombo(ball.x, ball.y);
|
||||
resetCombo(gameState, ball.x, ball.y);
|
||||
}
|
||||
|
||||
if (gameState.perks.respawn) {
|
||||
|
@ -1025,7 +964,7 @@ export function ballTick(ball: Ball, delta: number) {
|
|||
if (!ball.hitSinceBounce) {
|
||||
gameState.runStatistics.misses++;
|
||||
gameState.levelMisses++;
|
||||
resetCombo(ball.x, ball.y);
|
||||
resetCombo(gameState, ball.x, ball.y);
|
||||
gameState.flashes.push({
|
||||
type: "text",
|
||||
text: "miss",
|
||||
|
@ -1245,6 +1184,11 @@ export function gameOver(title: string, intro: string) {
|
|||
});
|
||||
}
|
||||
|
||||
let unlockedItems = list.filter((u) => u.threshold > startTs && u.threshold < endTs);
|
||||
if (unlockedItems.length) {
|
||||
unlocksInfo += `<p>You unlocked ${unlockedItems.length} item(s) : ${unlockedItems.map(u => u.title).join(', ')}</p>`
|
||||
}
|
||||
|
||||
// Avoid the sad sound right as we restart a new games
|
||||
gameState.combo = 1;
|
||||
|
||||
|
@ -1254,6 +1198,7 @@ export function gameOver(title: string, intro: string) {
|
|||
text: `
|
||||
${gameState.isCreativeModeRun ? "<p>This test run and its score are not being recorded</p>" : ""}
|
||||
<p>${intro}</p>
|
||||
<p>Your total cumulative score went from ${startTs} to ${endTs}.</p>
|
||||
${unlocksInfo}
|
||||
`,
|
||||
actions: [
|
||||
|
@ -1480,6 +1425,7 @@ export function explodeBrick(index: number, ball: Ball, isExplosion: boolean) {
|
|||
|
||||
gameState.coins.push({
|
||||
points,
|
||||
size: gameState.coinSize,//-Math.floor(Math.log2(points)),
|
||||
color: gameState.perks.metamorphosis ? color : "gold",
|
||||
x: cx,
|
||||
y: cy,
|
||||
|
@ -1515,10 +1461,16 @@ export function explodeBrick(index: number, ball: Ball, isExplosion: boolean) {
|
|||
color
|
||||
) {
|
||||
if (gameState.perks.picky_eater) {
|
||||
resetCombo(ball.x, ball.y);
|
||||
resetCombo(gameState, ball.x, ball.y);
|
||||
}
|
||||
|
||||
sounds.colorChange(ball.x,0.8)
|
||||
gameState.lastExplosion=gameState.levelTime
|
||||
gameState.ballsColor = color;
|
||||
if(!isSettingOn('basic')) {
|
||||
gameState.balls.forEach(ball=>{
|
||||
spawnExplosion(7, ball.previousX, ball.previousY, color, 150, 15)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
sounds.comboIncreaseMaybe(gameState.combo, ball.x, 1);
|
||||
}
|
||||
|
@ -1668,7 +1620,7 @@ export function render() {
|
|||
drawCoin(
|
||||
ctx,
|
||||
coin.color,
|
||||
gameState.coinSize,
|
||||
coin.size,
|
||||
coin.x,
|
||||
coin.y,
|
||||
level.color || "black",
|
||||
|
@ -1738,7 +1690,7 @@ export function render() {
|
|||
// The puck
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.globalCompositeOperation = "source-over";
|
||||
if (gameState.perks.streak_shots && gameState.combo > baseCombo()) {
|
||||
if (gameState.perks.streak_shots && gameState.combo > baseCombo(gameState)) {
|
||||
drawPuck(ctx, "red", gameState.puckWidth, gameState.puckHeight, -2);
|
||||
}
|
||||
drawPuck(ctx, gameState.puckColor, gameState.puckWidth, gameState.puckHeight);
|
||||
|
@ -1781,7 +1733,7 @@ export function render() {
|
|||
}
|
||||
}
|
||||
// Borders
|
||||
const hasCombo = gameState.combo > baseCombo();
|
||||
const hasCombo = gameState.combo > baseCombo(gameState);
|
||||
ctx.globalCompositeOperation = "source-over";
|
||||
if (gameState.offsetXRoundedDown) {
|
||||
// draw outside of gaming area to avoid capturing borders in recordings
|
||||
|
@ -1795,11 +1747,11 @@ export function render() {
|
|||
if (hasCombo && gameState.perks.right_is_lava) ctx.fillRect(width - 1, 0, 1, height);
|
||||
}
|
||||
|
||||
if (gameState.perks.top_is_lava && gameState.combo > baseCombo()) {
|
||||
if (gameState.perks.top_is_lava && gameState.combo > baseCombo(gameState)) {
|
||||
ctx.fillStyle = "red";
|
||||
ctx.fillRect(gameState.offsetXRoundedDown, 0, gameState.gameZoneWidthRoundedUp, 1);
|
||||
}
|
||||
const redBottom = gameState.perks.compound_interest && gameState.combo > baseCombo();
|
||||
const redBottom = gameState.perks.compound_interest && gameState.combo > baseCombo(gameState);
|
||||
ctx.fillStyle = redBottom ? "red" : gameState.puckColor;
|
||||
if (isSettingOn("mobile-mode")) {
|
||||
ctx.fillRect(gameState.offsetXRoundedDown, gameState.gameZoneHeight, gameState.gameZoneWidthRoundedUp, 1);
|
||||
|
@ -1837,7 +1789,7 @@ export function renderAllBricks() {
|
|||
ctx.globalAlpha = 1;
|
||||
|
||||
const redBorderOnBricksWithWrongColor =
|
||||
gameState.combo > baseCombo() && gameState.perks.picky_eater;
|
||||
gameState.combo > baseCombo(gameState) && gameState.perks.picky_eater;
|
||||
|
||||
const newKey =
|
||||
gameState.gameZoneWidth +
|
||||
|
@ -2447,11 +2399,11 @@ async function openSettingsPanel() {
|
|||
}
|
||||
}
|
||||
actions.push({
|
||||
text: "Creative mode",
|
||||
text: "Sandbox mode",
|
||||
help:
|
||||
getTotalScore() < creativeModeThreshold
|
||||
? "Unlocks at total score $" + creativeModeThreshold
|
||||
: "Test runs with custom perks",
|
||||
: "Test any perk combination",
|
||||
disabled: getTotalScore() < creativeModeThreshold,
|
||||
async value() {
|
||||
let creativeModePerks: Partial<{ [id in PerkId]: number }> = {},
|
||||
|
@ -2549,7 +2501,7 @@ async function openUnlocksList() {
|
|||
help:
|
||||
ts >= threshold ? fullHelp : `Unlocks at total score ${threshold}.`,
|
||||
disabled: ts < threshold,
|
||||
value: {perk: id} as RunParams,
|
||||
value: {perks: {[id]: 1}} as RunParams,
|
||||
icon,
|
||||
})),
|
||||
...allLevels
|
||||
|
@ -2944,7 +2896,7 @@ document.addEventListener("keydown", (e) => {
|
|||
e.preventDefault();
|
||||
});
|
||||
|
||||
document.addEventListener("keyup", (e) => {
|
||||
document.addEventListener("keyup", async (e) => {
|
||||
const focused = document.querySelector("button:focus");
|
||||
if (e.key in pressed) {
|
||||
setKeyPressed(e.key, 0);
|
||||
|
@ -2966,6 +2918,8 @@ document.addEventListener("keyup", (e) => {
|
|||
openSettingsPanel();
|
||||
} else if (e.key.toLowerCase() === "s" && !alertsOpen) {
|
||||
openScorePanel();
|
||||
} else if (e.key.toLowerCase() === "r" && !alertsOpen) {
|
||||
// TODO
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
@ -2987,7 +2941,7 @@ function newGameState(params: RunParams): GameState {
|
|||
restInRandomOrder.slice(0, 7 + 3).sort((a, b) => a.sortKey - b.sortKey),
|
||||
);
|
||||
|
||||
const perks = {...makeEmptyPerksMap(), ...(params?.perks || {})}
|
||||
const perks = {...makeEmptyPerksMap(upgrades), ...(params?.perks || {})}
|
||||
|
||||
const gameState: GameState = {
|
||||
runLevels,
|
||||
|
|
42
src/game_utils.test.ts
Normal file
42
src/game_utils.test.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
import {getMajorityValue, sample, sumOfKeys} from "./game_utils";
|
||||
|
||||
describe('getMajorityValue', ()=>{
|
||||
|
||||
it('returns the most common string',()=>{
|
||||
expect(getMajorityValue(['1','1','2','2','3','2','3','2','2','1'])).toStrictEqual('2')
|
||||
})
|
||||
it('returns the only string',()=>{
|
||||
expect(getMajorityValue(['1'])).toStrictEqual('1')
|
||||
})
|
||||
it('returns nothing for empty array',()=>{
|
||||
expect(getMajorityValue([])).toStrictEqual(undefined)
|
||||
})
|
||||
|
||||
})
|
||||
describe('sample', ()=>{
|
||||
|
||||
it('returns a random pick from the array',()=>{
|
||||
expect(['1','2','3'].includes(sample(['1','2','3']))).toBeTruthy()
|
||||
})
|
||||
it('returns the only item if there is just one',()=>{
|
||||
expect(sample(['1'])).toStrictEqual('1')
|
||||
})
|
||||
it('returns nothing for empty array',()=>{
|
||||
expect(sample([])).toStrictEqual(undefined)
|
||||
})
|
||||
|
||||
})
|
||||
describe('sumOfKeys', ()=>{
|
||||
it('returns the sum of the keys of an array',()=>{
|
||||
expect(sumOfKeys({a:1,b:2})).toEqual(3)
|
||||
})
|
||||
it('returns 0 for an empty object',()=>{
|
||||
expect(sumOfKeys({})).toEqual(0)
|
||||
})
|
||||
it('returns 0 for undefined',()=>{
|
||||
expect(sumOfKeys(undefined)).toEqual(0)
|
||||
})
|
||||
it('returns 0 for null',()=>{
|
||||
expect(sumOfKeys(null)).toEqual(0)
|
||||
})
|
||||
})
|
|
@ -1,4 +1,5 @@
|
|||
|
||||
import {PerksMap, Upgrade} from "./types";
|
||||
|
||||
export function getMajorityValue(arr: string[]): string {
|
||||
const count: { [k: string]: number } = {};
|
||||
|
@ -16,4 +17,10 @@ export function sample<T>(arr: T[]): T {
|
|||
export function sumOfKeys(obj:{[key:string]:number} | undefined | null){
|
||||
if(!obj) return 0
|
||||
return Object.values(obj)?.reduce((a,b)=>a+b,0) ||0
|
||||
}
|
||||
}
|
||||
|
||||
export const makeEmptyPerksMap = (upgrades:Upgrade[]) => {
|
||||
const p = {} as any;
|
||||
upgrades.forEach((u) => (p[u.id] = 0));
|
||||
return p as PerksMap;
|
||||
};
|
|
@ -41,7 +41,7 @@
|
|||
{
|
||||
"name": "Dots",
|
||||
"size": 9,
|
||||
"bricks": "b_t_a_c_C__________b_t_a_c__________v_b_t_a_c__________v_b_t_a__________p_v_b_t_a",
|
||||
"bricks": "b_t_a_c____________b_t_a_c__________P_b_t_a_c__________P_b_t_a____________P_b_t_a",
|
||||
"svg": null
|
||||
},
|
||||
{
|
||||
|
@ -95,7 +95,7 @@
|
|||
{
|
||||
"name": "Temple",
|
||||
"size": 11,
|
||||
"bricks": "_______________WWW______WWWWWWW___WWWWWWWWW___t_t_t_t____b_b_b_b____v_v_v_v____p_p_p_p____P_P_P_P____WWWWWWW___WWWWWWWWW_",
|
||||
"bricks": "_______________WWW______WWWWWWW___WWWWWWWWW___b_b_b_b____b_b_b_b____v_v_v_v____P_P_P_P____P_P_P_P____WWWWWWW___WWWWWWWWW_",
|
||||
"svg": null,
|
||||
"color": ""
|
||||
},
|
||||
|
@ -109,7 +109,7 @@
|
|||
{
|
||||
"name": "Ship",
|
||||
"size": 11,
|
||||
"bricks": "____sWW________sWWW_______sWWW_______s___OOOOOOOOOOOOOO_OBOBOBOBOO__OOOOOOOO_bbbbbbbbgbbbbgbbbbggbbbggbbbbbbbb",
|
||||
"bricks": "____sWW________sWWW_______sWWW_______s___OOOOOOOOOOOOOO_OBOBOBOBOO__OOOOOOOO_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb___________",
|
||||
"svg": 19
|
||||
},
|
||||
{
|
||||
|
@ -279,7 +279,7 @@
|
|||
{
|
||||
"name": "Ocean Sunrise",
|
||||
"size": 8,
|
||||
"bricks": "SSSSSSSSSSSyySSSSSyyyySSSyyWWyySbttaattbbbttttbbbbbttbbbbbbbbbbb",
|
||||
"bricks": "SSSSSSSSSSSyySSSSSyyyySSSyyyyyySbttttttbbbttttbbbbbttbbbbbbbbbbb",
|
||||
"svg": 12,
|
||||
"color": ""
|
||||
},
|
||||
|
@ -648,7 +648,7 @@
|
|||
{
|
||||
"name": "Bird",
|
||||
"size": 13,
|
||||
"bricks": "_______RRR____R____RSSSR___RR__RSSWWWR__RSR_RSWWBWR__RSSRRSW_WWyy_RSSSRSWWWR___RSSSSSSRR_____RRSSyyyy______RSyyyyy___RRRRSyyyy____RSSSRyyy_____RRRR________",
|
||||
"bricks": "_______RRR____R____RSSSR___RR__RSSWWWR__RSR_RSWWBWR__RSSRRSWWWWyy_RSSSRSWWWR___RSSSSSSRR_____RRSSyyyy______RSyyyyy___RRRRSyyyy____RSSSRyyy_____RRRR______________________",
|
||||
"svg": null,
|
||||
"color": ""
|
||||
},
|
||||
|
|
|
@ -45,7 +45,10 @@ function App() {
|
|||
|
||||
|
||||
|
||||
return <div onMouseUp={() => setApplying('')} onMouseLeave={() => setApplying('')}>
|
||||
return <div onMouseUp={() => {
|
||||
console.log('mouse up')
|
||||
setApplying('')
|
||||
}} >
|
||||
<div id={"levels"}>
|
||||
{
|
||||
levels.map((level, li) => {
|
||||
|
@ -58,9 +61,12 @@ function App() {
|
|||
brickButtons.push(<button
|
||||
key={index}
|
||||
onMouseDown={() => {
|
||||
const color = selected === bricks[index] ? '_' : applying
|
||||
setApplying(color)
|
||||
updateLevel(li, setBrick(level, index, color))
|
||||
if(!applying){
|
||||
console.log(selected, bricks[index],applying)
|
||||
const color = selected === bricks[index] ? '_' : selected
|
||||
setApplying(color)
|
||||
updateLevel(li, setBrick(level, index, color))
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
if (applying) {
|
||||
|
|
|
@ -33,6 +33,7 @@ export function moveLevel(level: RawLevel, dx: number, dy: number) {
|
|||
}
|
||||
|
||||
export function setBrick(level: RawLevel, index: number, colorCode: string) {
|
||||
console.log('setBrick', level.name, index, colorCode)
|
||||
const {size} = level
|
||||
const newBricks=[]
|
||||
for (let x = 0; x < size; x++) {
|
||||
|
|
28
src/loadGameData.test.ts
Normal file
28
src/loadGameData.test.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
|
||||
import _palette from "./palette.json";
|
||||
import _rawLevelsList from "./levels.json";
|
||||
import _appVersion from "./version.json";
|
||||
|
||||
describe('json data checks', ()=>{
|
||||
it('_rawLevelsList has icon levels', ()=>{
|
||||
expect(_rawLevelsList.filter(l=>l.name.startsWith('icon:')).length).toBeGreaterThan(10)
|
||||
})
|
||||
it('_rawLevelsList has non-icon few levels', ()=>{
|
||||
expect(_rawLevelsList.filter(l=>!l.name.startsWith('icon:')).length).toBeGreaterThan(10)
|
||||
})
|
||||
|
||||
it('_rawLevelsList has max 5 colors per level', ()=>{
|
||||
const levelsWithManyBrickColors=_rawLevelsList.filter(l=>{
|
||||
|
||||
const uniqueBricks = l.bricks.split('').filter(b=>b!=='_' && b!=='black').filter((a,b,c)=>c.indexOf(a)===b)
|
||||
return uniqueBricks.length>5
|
||||
}).map(l=>l.name)
|
||||
expect(levelsWithManyBrickColors).toEqual([])
|
||||
})
|
||||
it('Has a few colors', ()=>{
|
||||
expect(Object.keys(_palette).length).toBeGreaterThan(10)
|
||||
})
|
||||
it('Has an _appVersion', ()=>{
|
||||
expect(parseInt(_appVersion)).toBeGreaterThan(2000)
|
||||
})
|
||||
})
|
|
@ -19,7 +19,6 @@ const levelIconHTMLCanvasCtx = levelIconHTMLCanvas.getContext("2d", {
|
|||
function levelIconHTML(
|
||||
bricks: string[],
|
||||
levelSize: number,
|
||||
levelName: string,
|
||||
color: string,
|
||||
) {
|
||||
const size = 40;
|
||||
|
@ -49,8 +48,8 @@ function levelIconHTML(
|
|||
}
|
||||
}
|
||||
}
|
||||
// I don't think many blind people will benefit for this, but it's nice to have something to put in "alt"
|
||||
return `<img alt="${levelName}" width="${size}" height="${size}" src="${c.toDataURL()}"/>`;
|
||||
|
||||
return `<img alt="" width="${size}" height="${size}" src="${c.toDataURL()}"/>`;
|
||||
}
|
||||
|
||||
export const icons = {} as { [k: string]: string };
|
||||
|
@ -61,7 +60,7 @@ export const allLevels = rawLevelsList
|
|||
.split("")
|
||||
.map((c) => palette[c])
|
||||
.slice(0, level.size * level.size);
|
||||
const icon = levelIconHTML(bricks, level.size, level.name, level.color);
|
||||
const icon = levelIconHTML(bricks, level.size, level.color);
|
||||
icons[level.name] = icon;
|
||||
return {
|
||||
...level,
|
||||
|
|
340
src/sounds.ts
340
src/sounds.ts
|
@ -1,233 +1,237 @@
|
|||
import {
|
||||
gameState,
|
||||
isSettingOn,
|
||||
gameState,
|
||||
isSettingOn,
|
||||
} from "./game";
|
||||
|
||||
export const sounds = {
|
||||
wallBeep: (pan: number) => {
|
||||
if (!isSettingOn("sound")) return;
|
||||
createSingleBounceSound(800, pixelsToPan(pan));
|
||||
},
|
||||
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;
|
||||
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");
|
||||
},
|
||||
colorChange(pan: number, volume: number) {
|
||||
createSingleBounceSound(400, pixelsToPan(pan), volume , 0.5, "sine")
|
||||
createSingleBounceSound(800, pixelsToPan(pan), volume * 0.5, 0.2, "square")
|
||||
}
|
||||
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;
|
||||
audioRecordingTrack: MediaStreamAudioDestinationNode;
|
||||
|
||||
export function getAudioContext() {
|
||||
if (!audioContext) {
|
||||
if (!isSettingOn("sound")) return null;
|
||||
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
audioRecordingTrack = audioContext.createMediaStreamDestination();
|
||||
}
|
||||
return audioContext;
|
||||
if (!audioContext) {
|
||||
if (!isSettingOn("sound")) return null;
|
||||
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
audioRecordingTrack = audioContext.createMediaStreamDestination();
|
||||
}
|
||||
return audioContext;
|
||||
}
|
||||
|
||||
export function getAudioRecordingTrack() {
|
||||
getAudioContext();
|
||||
return audioRecordingTrack;
|
||||
getAudioContext();
|
||||
return audioRecordingTrack;
|
||||
}
|
||||
|
||||
function createSingleBounceSound(
|
||||
baseFreq = 800,
|
||||
pan = 0.5,
|
||||
volume = 1,
|
||||
duration = 0.1,
|
||||
type: OscillatorType = "sine",
|
||||
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);
|
||||
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 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);
|
||||
// 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
|
||||
// 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);
|
||||
// Start the oscillator
|
||||
oscillator.start(context.currentTime);
|
||||
|
||||
// Stop the oscillator after the decay
|
||||
oscillator.stop(context.currentTime + duration);
|
||||
// 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);
|
||||
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;
|
||||
// Fill the buffer with random noise
|
||||
for (let i = 0; i < bufferSize; i++) {
|
||||
output[i] = Math.random() * 2 - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return noiseBuffer;
|
||||
return noiseBuffer;
|
||||
}
|
||||
|
||||
function createExplosionSound(pan = 0.5) {
|
||||
const context = getAudioContext();
|
||||
if (!context) return;
|
||||
// Create an audio buffer
|
||||
const context = getAudioContext();
|
||||
if (!context) return;
|
||||
// Create an audio buffer
|
||||
|
||||
// Create a noise source
|
||||
const noiseSource = context.createBufferSource();
|
||||
noiseSource.buffer = getNoiseBuffer(context);
|
||||
// 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 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 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
|
||||
// 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);
|
||||
// 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);
|
||||
// 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);
|
||||
// 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);
|
||||
// Start the noise source
|
||||
noiseSource.start(context.currentTime);
|
||||
|
||||
// Stop the noise source after the sound has played
|
||||
noiseSource.stop(context.currentTime + 1);
|
||||
// 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 - gameState.offsetXRoundedDown) / gameState.gameZoneWidthRoundedUp),
|
||||
);
|
||||
return Math.max(
|
||||
0,
|
||||
Math.min(1, (pan - gameState.offsetXRoundedDown) / gameState.gameZoneWidthRoundedUp),
|
||||
);
|
||||
}
|
||||
|
||||
let lastComboPlayed = NaN,
|
||||
shepard = 6;
|
||||
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 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;
|
||||
};
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
// 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);
|
||||
gainNode.connect(panner);
|
||||
}
|
||||
|
||||
// Helper function to create an oscillator with a specific frequency
|
||||
function createOscillator(
|
||||
context: AudioContext,
|
||||
frequency: number,
|
||||
type: OscillatorType,
|
||||
context: AudioContext,
|
||||
frequency: number,
|
||||
type: OscillatorType,
|
||||
) {
|
||||
const oscillator = context.createOscillator();
|
||||
oscillator.type = type;
|
||||
oscillator.frequency.setValueAtTime(frequency, context.currentTime);
|
||||
return oscillator;
|
||||
const oscillator = context.createOscillator();
|
||||
oscillator.type = type;
|
||||
oscillator.frequency.setValueAtTime(frequency, context.currentTime);
|
||||
return oscillator;
|
||||
}
|
||||
|
|
1
src/types.d.ts
vendored
1
src/types.d.ts
vendored
|
@ -69,6 +69,7 @@ export type Coin = {
|
|||
color: colorString;
|
||||
x: number;
|
||||
y: number;
|
||||
size: number;
|
||||
previousX: number;
|
||||
previousY: number;
|
||||
vx: number;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue