diff --git a/src/App.vue b/src/App.vue index 333dda0..8a6634f 100644 --- a/src/App.vue +++ b/src/App.vue @@ -16,8 +16,8 @@ import MapEditor from '@/components/screens/MapEditor.vue' import BackgroundImageLoader from '@/components/utilities/BackgroundImageLoader.vue' import Debug from '@/components/utilities/Debug.vue' import Notifications from '@/components/utilities/Notifications.vue' -import { useSoundComposable } from '@/composables/useSoundComposable' import { useMapEditorComposable } from '@/composables/useMapEditorComposable' +import { useSoundComposable } from '@/composables/useSoundComposable' import { useGameStore } from '@/stores/gameStore' import { computed, watch } from 'vue' diff --git a/src/components/game/character/Character.vue b/src/components/game/character/Character.vue index 29961c0..41d38c3 100644 --- a/src/components/game/character/Character.vue +++ b/src/components/game/character/Character.vue @@ -43,6 +43,9 @@ const handlePositionUpdate = (newValues: any, oldValues: any) => { } } +/** + * Plays walk sound when character is moving + */ watch( () => props.mapCharacter.isMoving, (newValue) => { @@ -54,6 +57,9 @@ watch( } ) +/** + * Plays attack animation and sound when character is attacking + */ watch( () => props.mapCharacter.isAttacking, (newValue) => { @@ -67,6 +73,9 @@ watch( } ) +/** + * Handles position updates and movement delay + */ watch( () => ({ positionX: props.mapCharacter.character.positionX, @@ -82,7 +91,6 @@ watch( onMounted(async () => { await initializeSprite() - if (props.mapCharacter.character.id === gameStore.character!.id) { mapStore.setCharacterLoaded(true) scene.cameras.main.startFollow(characterContainer.value as Phaser.GameObjects.Container) diff --git a/src/composables/useSoundComposable.ts b/src/composables/useSoundComposable.ts index 55af069..dfcf1e0 100644 --- a/src/composables/useSoundComposable.ts +++ b/src/composables/useSoundComposable.ts @@ -1,47 +1,27 @@ import { SoundStorage } from '@/storage/storages' -// Use a WeakMap for better garbage collection +interface CachedSound { + id: string + name: string + base64: string +} + +// Core storage instances +const soundStorage = new SoundStorage() const activeSounds = new Map() const audioCache = new Map() -const soundStorage = new SoundStorage() export function useSoundComposable() { - // Preload function to reduce initial playback delay - const preloadSound = async (soundUrl: string): Promise => { - if (audioCache.has(soundUrl)) { - return audioCache.get(soundUrl)! - } - - let audio: HTMLAudioElement - const cachedSound = await soundStorage.get(soundUrl) - - if (cachedSound) { - audio = new Audio(`data:audio/mpeg;base64,${cachedSound.base64}`) - } else { - try { - const base64 = await soundToBase64(soundUrl) - await soundStorage.add({ - id: soundUrl, - name: soundUrl.split('/').pop() || soundUrl, - base64 - }) - audio = new Audio(`data:audio/mpeg;base64,${base64}`) - } catch (error) { - console.error(`Failed to load and cache sound ${soundUrl}:`, error) - audio = new Audio(soundUrl) - } - } - - // Preload the audio - audio.load() - audioCache.set(soundUrl, audio) - return audio - } - - const soundToBase64 = async (url: string): Promise => { + /** + * Converts a sound URL to base64 format + */ + async function soundToBase64(url: string): Promise { try { const response = await fetch(url) - if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`) + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + const blob = await response.blob() return new Promise((resolve, reject) => { const reader = new FileReader() @@ -55,30 +35,70 @@ export function useSoundComposable() { } } - const playSound = async (soundUrl: string, loop: boolean = false, ignoreIfPlaying: boolean = false) => { + /** + * Preloads a sound file and caches it for future use + */ + async function preloadSound(soundUrl: string): Promise { + if (audioCache.has(soundUrl)) { + return audioCache.get(soundUrl)! + } + + let audio: HTMLAudioElement + const cachedSound = await soundStorage.get(soundUrl) + + if (cachedSound) { + audio = new Audio(`data:audio/mpeg;base64,${cachedSound.base64}`) + } else { + try { + const base64 = await soundToBase64(soundUrl) + const soundData: CachedSound = { + id: soundUrl, + name: soundUrl.split('/').pop() || soundUrl, + base64 + } + await soundStorage.add(soundData) + audio = new Audio(`data:audio/mpeg;base64,${base64}`) + } catch (error) { + console.error(`Failed to load and cache sound ${soundUrl}:`, error) + audio = new Audio(soundUrl) + } + } + + audio.load() + audioCache.set(soundUrl, audio) + return audio + } + + /** + * Plays a sound with optional looping and duplicate prevention + */ + async function playSound(soundUrl: string, loop: boolean = false, ignoreIfPlaying: boolean = false): Promise { try { - // Check if sound is already playing const playingSounds = activeSounds.get(soundUrl) || [] - if (ignoreIfPlaying && playingSounds.some(audio => !audio.paused)) { + + if (ignoreIfPlaying && playingSounds.some((audio) => !audio.paused)) { return } - // Stop previous instances if they exist stopSound(soundUrl) const audio = await preloadSound(soundUrl) const playingAudio = audio.cloneNode() as HTMLAudioElement playingAudio.loop = loop - // Initialize or get the array of active sounds for this URL if (!activeSounds.has(soundUrl)) { activeSounds.set(soundUrl, []) } + activeSounds.get(soundUrl)!.push(playingAudio) - playingAudio.addEventListener('ended', () => { - const sounds = activeSounds.get(soundUrl) - if (sounds) { + // Cleanup when sound ends + playingAudio.addEventListener( + 'ended', + () => { + const sounds = activeSounds.get(soundUrl) + if (!sounds) return + const index = sounds.indexOf(playingAudio) if (index > -1) { sounds.splice(index, 1) @@ -86,8 +106,9 @@ export function useSoundComposable() { if (sounds.length === 0) { activeSounds.delete(soundUrl) } - } - }, { once: true }) + }, + { once: true } + ) await playingAudio.play() } catch (error) { @@ -95,33 +116,41 @@ export function useSoundComposable() { } } - const stopSound = (soundUrl: string) => { + /** + * Stops all instances of a specific sound + */ + function stopSound(soundUrl: string): void { const sounds = activeSounds.get(soundUrl) if (!sounds) return - sounds.forEach(audio => { + sounds.forEach((audio) => { audio.pause() audio.currentTime = 0 }) activeSounds.delete(soundUrl) } - const stopAllSounds = () => { - activeSounds.forEach((sounds, url) => { - stopSound(url) - }) + /** + * Stops all currently playing sounds + */ + function stopAllSounds(): void { + activeSounds.forEach((_, url) => stopSound(url)) activeSounds.clear() } - const clearSoundCache = async () => { + /** + * Clears all cached sounds and stops playback + */ + async function clearSoundCache(): Promise { stopAllSounds() audioCache.clear() await soundStorage.reset() } - - // New method to preload multiple sounds - const preloadSounds = async (soundUrls: string[]) => { + /** + * Preloads multiple sounds simultaneously + */ + async function preloadSounds(soundUrls: string[]): Promise { return Promise.all(soundUrls.map(preloadSound)) } @@ -132,4 +161,4 @@ export function useSoundComposable() { clearSoundCache, preloadSounds } -} \ No newline at end of file +} diff --git a/src/storage/storages.ts b/src/storage/storages.ts index c9be338..b26ba47 100644 --- a/src/storage/storages.ts +++ b/src/storage/storages.ts @@ -51,4 +51,4 @@ export class SoundStorage extends BaseStorage<{ id: string; name: string; base64 constructor() { super('sounds', 'id, name, createdAt, updatedAt') } -} \ No newline at end of file +}