diff --git a/js13k2024/game/src/game-states/gameplay/bonus-logic.ts b/js13k2024/game/src/game-states/gameplay/bonus-logic.ts index c781b3c1..d4edc3d6 100644 --- a/js13k2024/game/src/game-states/gameplay/bonus-logic.ts +++ b/js13k2024/game/src/game-states/gameplay/bonus-logic.ts @@ -1,4 +1,4 @@ -import { soundEngine } from '../../sound/sound-engine'; +import { playEffect, SoundEffect } from '../../sound/sound-engine'; import { startGameOverAnimation } from './game-logic'; import { GameState, BonusType, ActiveBonus, LevelConfig, Position, Direction, BlasterShot } from './gameplay-types'; import { @@ -120,7 +120,7 @@ export const performTeleportation = (gameState: GameState, teleportPoint: Positi gameState.player.previousPosition = gameState.player.position; gameState.player.position = destinationPoint.position; gameState.player.teleportTimestamp = Date.now(); - soundEngine.playTeleport(); + playEffect(SoundEffect.Teleport); } }; @@ -205,7 +205,7 @@ export const handleBlasterShot = (gameState: GameState, direction: Direction, le }; gameState.blasterShots.push(shot); // Play the blaster sound effect - soundEngine.playBlasterSound(); + playEffect(SoundEffect.BlasterSound); // Check if the shot hits monsters along its path gameState.monsters = gameState.monsters.filter((monster) => { @@ -236,4 +236,4 @@ const isMonsterOnBlasterPath = ( (direction === Direction.Right && monsterPos.x >= start.x && monsterPos.x <= end.x)) ); } -}; +}; \ No newline at end of file diff --git a/js13k2024/game/src/game-states/gameplay/game-logic.ts b/js13k2024/game/src/game-states/gameplay/game-logic.ts index 590c4ec8..7f4e93ab 100644 --- a/js13k2024/game/src/game-states/gameplay/game-logic.ts +++ b/js13k2024/game/src/game-states/gameplay/game-logic.ts @@ -7,7 +7,7 @@ import { isInExplosionRange, } from './monster-logic'; import { MOVE_ANIMATION_DURATION, OBSTACLE_DESTRUCTION_DURATION } from './render/animation-utils'; -import { soundEngine } from '../../sound/sound-engine'; +import { playEffect, SoundEffect } from '../../sound/sound-engine'; import { getDirectionFromKey, getNewPosition, isPositionEqual, isPositionOccupied, isValidMove } from './move-utils'; import { applyBonus, @@ -60,11 +60,11 @@ export const doGameUpdate = (direction: Direction, gameState: GameState, levelCo ? { ...obstacle, isDestroying: true, creationTime: Date.now() } : obstacle, ); - soundEngine.playElectricalDischarge(); + playEffect(SoundEffect.ElectricalDischarge); } if (isValidMove(newPosition, newGameState, levelConfig)) { - soundEngine.playStep(); + playEffect(SoundEffect.Step); newGameState.player.position = newPosition; if (Date.now() - newGameState.player.moveTimestamp > MOVE_ANIMATION_DURATION) { newGameState.player.previousPosition = oldPosition; @@ -89,7 +89,7 @@ export const doGameUpdate = (direction: Direction, gameState: GameState, levelCo // Check for bonuses const collectedBonus = newGameState.bonuses.find((bonus) => isPositionEqual(bonus.position, newPosition)); if (collectedBonus) { - soundEngine.playBonusCollected(); + playEffect(SoundEffect.BonusCollected); newGameState.bonuses = newGameState.bonuses.filter((bonus) => !isPositionEqual(bonus.position, newPosition)); applyBonus(newGameState, collectedBonus.type); } @@ -143,7 +143,7 @@ export const doGameUpdate = (direction: Direction, gameState: GameState, levelCo newGameState.timeBombs = newGameState.timeBombs.map((bomb) => ({ ...bomb, timer: bomb.timer - 1 })); const explodedBombs = newGameState.timeBombs.filter((bomb) => bomb.timer === 0); if (explodedBombs.length > 0 || newExplosions.length > 0) { - soundEngine.playExplosion(); + playEffect(SoundEffect.Explosion); } newGameState.explosions = [ ...newGameState.explosions, @@ -188,7 +188,7 @@ export const doGameUpdate = (direction: Direction, gameState: GameState, levelCo ) ) { newGameState.obstacles.push(newObstacle); - soundEngine.playElectricalDischarge(); + playEffect(SoundEffect.ElectricalDischarge); } } } @@ -206,10 +206,10 @@ export const isGameEnding = (gameState: GameState): boolean => { export const startGameOverAnimation = (gameState: GameState): void => { gameState.player.isVanishing = true; - soundEngine.playGameOver(); + playEffect(SoundEffect.GameOver); }; const startLevelCompleteAnimation = (gameState: GameState): void => { gameState.player.isVictorious = true; - soundEngine.playLevelComplete(); -}; + playEffect(SoundEffect.LevelComplete); +}; \ No newline at end of file diff --git a/js13k2024/game/src/game-states/gameplay/level-updater.ts b/js13k2024/game/src/game-states/gameplay/level-updater.ts index c85be74c..4897f86b 100644 --- a/js13k2024/game/src/game-states/gameplay/level-updater.ts +++ b/js13k2024/game/src/game-states/gameplay/level-updater.ts @@ -1,4 +1,4 @@ -import { soundEngine } from '../../sound/sound-engine'; +import { playEffect, SoundEffect } from '../../sound/sound-engine'; import { GameState, LevelConfig } from './gameplay-types'; import { spawnMonster } from './monster-spawn'; @@ -8,7 +8,7 @@ export function simpleLevelUpdater(gameState: GameState, levelConfig: LevelConfi if (newMonster) { gameState.monsters.push(newMonster); gameState.monsterSpawnSteps = 0; - soundEngine.playMonsterSpawn(); + playEffect(SoundEffect.MonsterSpawn); } } -} +} \ No newline at end of file diff --git a/js13k2024/game/src/main.ts b/js13k2024/game/src/main.ts index eba5d6e0..127218c2 100644 --- a/js13k2024/game/src/main.ts +++ b/js13k2024/game/src/main.ts @@ -5,7 +5,7 @@ import { Gameplay } from './game-states/gameplay/gameplay'; import { GameOver } from './game-states/game-over/game-over'; import { LevelComplete } from './game-states/level-complete/level-complete'; import { LevelStory } from './game-states/level-story/level-story'; -import { soundEngine } from './sound/sound-engine'; +import { playEffect, SoundEffect } from './sound/sound-engine'; import { clearElement, createDiv } from './utils/dom'; import './global-styles.css'; @@ -170,7 +170,7 @@ export class MonsterStepsApp { } private gameComplete() { - soundEngine.playLevelComplete(); // We can reuse the level complete sound for game completion + playEffect(SoundEffect.LevelComplete); // We can reuse the level complete sound for game completion this.gameState = GameState.GameComplete; this.renderCurrentState(); } @@ -238,4 +238,4 @@ class GameComplete { return container; } -} +} \ No newline at end of file diff --git a/js13k2024/game/src/sound/sound-engine.ts b/js13k2024/game/src/sound/sound-engine.ts index bcfc5d42..c336300e 100644 --- a/js13k2024/game/src/sound/sound-engine.ts +++ b/js13k2024/game/src/sound/sound-engine.ts @@ -1,160 +1,126 @@ // Sound Engine for synthesizing and playing game sounds -class SoundEngine { - private audioContext: AudioContext; - private masterVolume: number = 0.025; // Default low volume - - constructor() { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - this.audioContext = new (window.AudioContext || (window as any).webkitAudioContext)(); - } - - private createOscillator(frequency: number, type: OscillatorType, duration: number): OscillatorNode { - const oscillator = this.audioContext.createOscillator(); - oscillator.type = type; - oscillator.frequency.setValueAtTime(frequency, this.audioContext.currentTime); - oscillator.start(); - oscillator.stop(this.audioContext.currentTime + duration); - return oscillator; - } - - private createGain(attackTime: number, releaseTime: number, peakValue: number = 1): GainNode { - const gain = this.audioContext.createGain(); - const adjustedPeakValue = peakValue * this.masterVolume; - gain.gain.setValueAtTime(0, this.audioContext.currentTime); - gain.gain.linearRampToValueAtTime(adjustedPeakValue, this.audioContext.currentTime + attackTime); - gain.gain.linearRampToValueAtTime(0, this.audioContext.currentTime + attackTime + releaseTime); - return gain; - } - - playStep() { - const oscillator = this.createOscillator(200, 'square', 0.1); - const gain = this.createGain(0.01, 0.09); - oscillator.connect(gain).connect(this.audioContext.destination); - } - - playElectricalDischarge() { - const oscillator = this.createOscillator(800, 'sawtooth', 0.2); - const gain = this.createGain(0.01, 0.19); - oscillator.frequency.linearRampToValueAtTime(200, this.audioContext.currentTime + 0.2); - oscillator.connect(gain).connect(this.audioContext.destination); - } - - playBonusCollected() { - const oscillator = this.createOscillator(600, 'sine', 0.15); - const gain = this.createGain(0.01, 0.14); - oscillator.frequency.linearRampToValueAtTime(800, this.audioContext.currentTime + 0.15); - oscillator.connect(gain).connect(this.audioContext.destination); - } - - playExplosion() { - const noise = this.audioContext.createBufferSource(); - const bufferSize = this.audioContext.sampleRate * 0.5; - const buffer = this.audioContext.createBuffer(1, bufferSize, this.audioContext.sampleRate); - const output = buffer.getChannelData(0); - - for (let i = 0; i < bufferSize; i++) { - output[i] = Math.random() * 2 - 1; - } - - noise.buffer = buffer; - const gain = this.createGain(0.01, 0.49, 0.5); - noise.connect(gain).connect(this.audioContext.destination); - noise.start(); - } - - playStateTransition() { - const oscillator = this.createOscillator(300, 'triangle', 0.3); - const gain = this.createGain(0.05, 0.25); - oscillator.frequency.linearRampToValueAtTime(600, this.audioContext.currentTime + 0.3); - oscillator.connect(gain).connect(this.audioContext.destination); - } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)(); +const masterVolume = 0.025; // Default low volume + +export enum SoundEffect { + Step, + ElectricalDischarge, + BonusCollected, + Explosion, + StateTransition, + GameOver, + LevelComplete, + MonsterSpawn, + Teleport, + BlasterSound +} - playGameOver() { - const oscillator = this.createOscillator(440, 'sine', 1); - const gain = this.createGain(0.01, 0.99); - oscillator.frequency.linearRampToValueAtTime(220, this.audioContext.currentTime + 1); - oscillator.connect(gain).connect(this.audioContext.destination); - } +function createOscillator(frequency: number, type: OscillatorType, duration: number): OscillatorNode { + const oscillator = audioContext.createOscillator(); + oscillator.type = type; + oscillator.frequency.setValueAtTime(frequency, audioContext.currentTime); + oscillator.start(); + oscillator.stop(audioContext.currentTime + duration); + return oscillator; +} - playLevelComplete() { - const oscillator1 = this.createOscillator(440, 'sine', 0.5); - const oscillator2 = this.createOscillator(550, 'sine', 0.5); - const gain1 = this.createGain(0.01, 0.49); - const gain2 = this.createGain(0.01, 0.49); - oscillator1.connect(gain1).connect(this.audioContext.destination); - oscillator2.connect(gain2).connect(this.audioContext.destination); - setTimeout(() => { - const oscillator3 = this.createOscillator(660, 'sine', 0.5); - const gain3 = this.createGain(0.01, 0.49); - oscillator3.connect(gain3).connect(this.audioContext.destination); - }, 250); - } +function createGain(attackTime: number, releaseTime: number, peakValue: number = 1): GainNode { + const gain = audioContext.createGain(); + const adjustedPeakValue = peakValue * masterVolume; + gain.gain.setValueAtTime(0, audioContext.currentTime); + gain.gain.linearRampToValueAtTime(adjustedPeakValue, audioContext.currentTime + attackTime); + gain.gain.linearRampToValueAtTime(0, audioContext.currentTime + attackTime + releaseTime); + return gain; +} - playMonsterSpawn() { - const oscillator = this.createOscillator(100, 'sawtooth', 0.3); - const gain = this.createGain(0.01, 0.29); - oscillator.frequency.exponentialRampToValueAtTime(300, this.audioContext.currentTime + 0.3); - oscillator.connect(gain).connect(this.audioContext.destination); +function playSimpleSound(frequency: number, type: OscillatorType, duration: number, attackTime: number, releaseTime: number, frequencyEnd?: number) { + const oscillator = createOscillator(frequency, type, duration); + const gain = createGain(attackTime, releaseTime); + if (frequencyEnd) { + oscillator.frequency.linearRampToValueAtTime(frequencyEnd, audioContext.currentTime + duration); } + oscillator.connect(gain).connect(audioContext.destination); +} - playTeleport() { - // Create a sweeping sound for teleportation - const oscillator1 = this.createOscillator(100, 'sine', 0.5); - const oscillator2 = this.createOscillator(200, 'sine', 0.5); - const gain1 = this.createGain(0.01, 0.49); - const gain2 = this.createGain(0.01, 0.49); - - // Frequency sweep - oscillator1.frequency.exponentialRampToValueAtTime(1000, this.audioContext.currentTime + 0.25); - oscillator2.frequency.exponentialRampToValueAtTime(2000, this.audioContext.currentTime + 0.25); - - oscillator1.connect(gain1).connect(this.audioContext.destination); - oscillator2.connect(gain2).connect(this.audioContext.destination); +function playNoise(duration: number, attackTime: number, releaseTime: number) { + const bufferSize = audioContext.sampleRate * duration; + const buffer = audioContext.createBuffer(1, bufferSize, audioContext.sampleRate); + const output = buffer.getChannelData(0); - // Add a "shimmer" effect - setTimeout(() => { - const shimmer = this.createOscillator(2000, 'sine', 0.2); - const shimmerGain = this.createGain(0.01, 0.19); - shimmer.frequency.exponentialRampToValueAtTime(4000, this.audioContext.currentTime + 0.2); - shimmer.connect(shimmerGain).connect(this.audioContext.destination); - }, 100); + for (let i = 0; i < bufferSize; i++) { + output[i] = Math.random() * 2 - 1; } - playBlasterSound() { - // Create a short, punchy sound with a slight pitch bend - const duration = 0.15; - const oscillator = this.audioContext.createOscillator(); - oscillator.type = 'sawtooth'; - oscillator.frequency.setValueAtTime(1000, this.audioContext.currentTime); - oscillator.frequency.exponentialRampToValueAtTime(500, this.audioContext.currentTime + duration); - - const gain = this.audioContext.createGain(); - gain.gain.setValueAtTime(0, this.audioContext.currentTime); - gain.gain.linearRampToValueAtTime(this.masterVolume, this.audioContext.currentTime + 0.01); - gain.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + duration); - - // Add a bit of noise for texture - const noiseBuffer = this.audioContext.createBuffer(1, this.audioContext.sampleRate * duration, this.audioContext.sampleRate); - const noiseData = noiseBuffer.getChannelData(0); - for (let i = 0; i < noiseBuffer.length; i++) { - noiseData[i] = Math.random() * 2 - 1; - } - const noiseSource = this.audioContext.createBufferSource(); - noiseSource.buffer = noiseBuffer; - - const noiseGain = this.audioContext.createGain(); - noiseGain.gain.setValueAtTime(this.masterVolume * 0.2, this.audioContext.currentTime); - noiseGain.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + duration); - - oscillator.connect(gain).connect(this.audioContext.destination); - noiseSource.connect(noiseGain).connect(this.audioContext.destination); + const noise = audioContext.createBufferSource(); + noise.buffer = buffer; + const gain = createGain(attackTime, releaseTime, 0.5); + noise.connect(gain).connect(audioContext.destination); + noise.start(); +} - oscillator.start(); - noiseSource.start(); - oscillator.stop(this.audioContext.currentTime + duration); - noiseSource.stop(this.audioContext.currentTime + duration); +export function playEffect(effect: SoundEffect): void { + switch (effect) { + case SoundEffect.Step: + playSimpleSound(200, 'square', 0.1, 0.01, 0.09); + break; + case SoundEffect.ElectricalDischarge: + playSimpleSound(800, 'sawtooth', 0.2, 0.01, 0.19, 200); + break; + case SoundEffect.BonusCollected: + playSimpleSound(600, 'sine', 0.15, 0.01, 0.14, 800); + break; + case SoundEffect.Explosion: + playNoise(0.5, 0.01, 0.49); + break; + case SoundEffect.StateTransition: + playSimpleSound(300, 'triangle', 0.3, 0.05, 0.25, 600); + break; + case SoundEffect.GameOver: + playSimpleSound(440, 'sine', 1, 0.01, 0.99, 220); + break; + case SoundEffect.LevelComplete: + playSimpleSound(440, 'sine', 0.5, 0.01, 0.49); + setTimeout(() => playSimpleSound(550, 'sine', 0.5, 0.01, 0.49), 0); + setTimeout(() => playSimpleSound(660, 'sine', 0.5, 0.01, 0.49), 250); + break; + case SoundEffect.MonsterSpawn: + playSimpleSound(100, 'sawtooth', 0.3, 0.01, 0.29, 300); + break; + case SoundEffect.Teleport: + playSimpleSound(100, 'sine', 0.5, 0.01, 0.49, 1000); + setTimeout(() => playSimpleSound(200, 'sine', 0.5, 0.01, 0.49, 2000), 0); + setTimeout(() => playSimpleSound(2000, 'sine', 0.2, 0.01, 0.19, 4000), 100); + break; + case SoundEffect.BlasterSound: + const duration = 0.15; + const oscillator = createOscillator(1000, 'sawtooth', duration); + oscillator.frequency.exponentialRampToValueAtTime(500, audioContext.currentTime + duration); + const gain = audioContext.createGain(); + gain.gain.setValueAtTime(0, audioContext.currentTime); + gain.gain.linearRampToValueAtTime(masterVolume, audioContext.currentTime + 0.01); + gain.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + duration); + oscillator.connect(gain).connect(audioContext.destination); + + const noiseBuffer = audioContext.createBuffer(1, audioContext.sampleRate * duration, audioContext.sampleRate); + const noiseData = noiseBuffer.getChannelData(0); + for (let i = 0; i < noiseBuffer.length; i++) { + noiseData[i] = Math.random() * 2 - 1; + } + const noiseSource = audioContext.createBufferSource(); + noiseSource.buffer = noiseBuffer; + const noiseGain = audioContext.createGain(); + noiseGain.gain.setValueAtTime(masterVolume * 0.2, audioContext.currentTime); + noiseGain.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + duration); + noiseSource.connect(noiseGain).connect(audioContext.destination); + noiseSource.start(); + noiseSource.stop(audioContext.currentTime + duration); + break; } } -export const soundEngine = new SoundEngine(); \ No newline at end of file +// Export the soundEngine object +export const soundEngine = { + playEffect +}; \ No newline at end of file