Cache audio
This commit is contained in:
parent
ccb64fc048
commit
fb3a59aa59
@ -16,7 +16,7 @@ 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 { useGameComposable } from '@/composables/useGameComposable'
|
||||
import { useSoundComposable } from '@/composables/useSoundComposable'
|
||||
import { useMapEditorComposable } from '@/composables/useMapEditorComposable'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { computed, watch } from 'vue'
|
||||
@ -24,7 +24,7 @@ import { computed, watch } from 'vue'
|
||||
const gameStore = useGameStore()
|
||||
|
||||
const mapEditor = useMapEditorComposable()
|
||||
const { playSound } = useGameComposable()
|
||||
const { playSound } = useSoundComposable()
|
||||
|
||||
const currentScreen = computed(() => {
|
||||
if (!gameStore.game.isLoaded) return Loading
|
||||
|
@ -13,7 +13,7 @@ import CharacterHair from '@/components/game/character/partials/CharacterHair.vu
|
||||
import ChatBubble from '@/components/game/character/partials/ChatBubble.vue'
|
||||
import HealthBar from '@/components/game/character/partials/HealthBar.vue'
|
||||
import { useCharacterSpriteComposable } from '@/composables/useCharacterSpriteComposable'
|
||||
import { useGameComposable } from '@/composables/useGameComposable'
|
||||
import { useSoundComposable } from '@/composables/useSoundComposable'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useMapStore } from '@/stores/mapStore'
|
||||
import { Container, Sprite, useScene } from 'phavuer'
|
||||
@ -29,7 +29,7 @@ const mapStore = useMapStore()
|
||||
const scene = useScene()
|
||||
|
||||
const { characterContainer, characterSprite, currentPositionX, currentPositionY, isometricDepth, isFlippedX, updatePosition, playAnimation, calcDirection, updateSprite, initializeSprite, cleanup } = useCharacterSpriteComposable(scene, props.tileMap, props.mapCharacter)
|
||||
const { playSound, stopSound } = useGameComposable()
|
||||
const { playSound, stopSound } = useSoundComposable()
|
||||
|
||||
const handlePositionUpdate = (newValues: any, oldValues: any) => {
|
||||
if (!newValues) return
|
||||
@ -43,6 +43,30 @@ const handlePositionUpdate = (newValues: any, oldValues: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.mapCharacter.isMoving,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
playSound('/assets/sounds/walk.wav', false, true)
|
||||
} else {
|
||||
stopSound('/assets/sounds/walk.wav')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.mapCharacter.isAttacking,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
playAnimation('attack')
|
||||
playSound('/assets/sounds/attack.wav', false, true)
|
||||
} else {
|
||||
stopSound('/assets/sounds/attack.wav')
|
||||
}
|
||||
mapStore.updateCharacterProperty(props.mapCharacter.character.id, 'isAttacking', false)
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => ({
|
||||
positionX: props.mapCharacter.character.positionX,
|
||||
@ -53,25 +77,6 @@ watch(
|
||||
}),
|
||||
(oldValues, newValues) => {
|
||||
handlePositionUpdate(oldValues, newValues)
|
||||
if (props.mapCharacter.isAttacking) {
|
||||
// Play attack animation
|
||||
playAnimation('attack')
|
||||
// Play hit sound
|
||||
playSound('/assets/sounds/hit.wav')
|
||||
// Disable attack immediately after playing the animation
|
||||
mapStore.updateCharacterProperty(props.mapCharacter.character.id, 'isAttacking', false)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.mapCharacter.isMoving,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
playSound('/assets/sounds/walk.wav', true, true)
|
||||
} else {
|
||||
stopSound('/assets/sounds/walk.wav')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -124,12 +124,12 @@
|
||||
import config from '@/application/config'
|
||||
import { type CharacterHair, type Character as CharacterT, type Map } from '@/application/types'
|
||||
import Modal from '@/components/utilities/Modal.vue'
|
||||
import { useGameComposable } from '@/composables/useGameComposable'
|
||||
import { useSoundComposable } from '@/composables/useSoundComposable'
|
||||
import { CharacterHairStorage } from '@/storage/storages'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
const { playSound } = useGameComposable()
|
||||
const { playSound } = useSoundComposable()
|
||||
const gameStore = useGameStore()
|
||||
const isLoading = ref<boolean>(true)
|
||||
const characters = ref<CharacterT[]>([])
|
||||
|
@ -27,13 +27,13 @@ import Hotkeys from '@/components/game/gui/Hotkeys.vue'
|
||||
import Hud from '@/components/game/gui/Hud.vue'
|
||||
import Menu from '@/components/game/gui/Menu.vue'
|
||||
import Map from '@/components/game/map/Map.vue'
|
||||
import { useGameComposable } from '@/composables/useGameComposable'
|
||||
import { useSoundComposable } from '@/composables/useSoundComposable'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { Game, Scene } from 'phavuer'
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const { playSound, stopSound } = useGameComposable()
|
||||
const { playSound, stopSound } = useSoundComposable()
|
||||
|
||||
const gameConfig = {
|
||||
name: config.name,
|
||||
|
@ -1,40 +0,0 @@
|
||||
const activeSounds: { [key: string]: HTMLAudioElement } = {}
|
||||
|
||||
export function useGameComposable() {
|
||||
const playSound = (sound: string, loop: boolean = false, ignoreIfPlaying: boolean = false) => {
|
||||
// If sound is already playing and we want to ignore
|
||||
if (ignoreIfPlaying && activeSounds[sound] && !activeSounds[sound].paused) {
|
||||
return
|
||||
}
|
||||
|
||||
// Stop previous instance of this sound if it exists
|
||||
if (activeSounds[sound]) {
|
||||
activeSounds[sound].pause()
|
||||
activeSounds[sound].currentTime = 0
|
||||
}
|
||||
|
||||
const audio = new Audio(sound)
|
||||
audio.loop = loop
|
||||
|
||||
// Store reference to track playing state
|
||||
activeSounds[sound] = audio
|
||||
|
||||
audio.addEventListener('ended', () => {
|
||||
delete activeSounds[sound]
|
||||
})
|
||||
|
||||
audio.play().catch(console.error)
|
||||
}
|
||||
|
||||
const stopSound = (sound: string) => {
|
||||
if (!activeSounds[sound]) return
|
||||
activeSounds[sound].pause()
|
||||
activeSounds[sound].currentTime = 0
|
||||
delete activeSounds[sound]
|
||||
}
|
||||
|
||||
return {
|
||||
playSound,
|
||||
stopSound
|
||||
}
|
||||
}
|
135
src/composables/useSoundComposable.ts
Normal file
135
src/composables/useSoundComposable.ts
Normal file
@ -0,0 +1,135 @@
|
||||
import { SoundStorage } from '@/storage/storages'
|
||||
|
||||
// Use a WeakMap for better garbage collection
|
||||
const activeSounds = new Map<string, HTMLAudioElement[]>()
|
||||
const audioCache = new Map<string, HTMLAudioElement>()
|
||||
const soundStorage = new SoundStorage()
|
||||
|
||||
export function useSoundComposable() {
|
||||
// Preload function to reduce initial playback delay
|
||||
const preloadSound = async (soundUrl: string): Promise<HTMLAudioElement> => {
|
||||
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<string> => {
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
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()
|
||||
reader.onloadend = () => resolve((reader.result as string).split(',')[1])
|
||||
reader.onerror = reject
|
||||
reader.readAsDataURL(blob)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error converting sound to base64:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const playSound = async (soundUrl: string, loop: boolean = false, ignoreIfPlaying: boolean = false) => {
|
||||
try {
|
||||
// Check if sound is already playing
|
||||
const playingSounds = activeSounds.get(soundUrl) || []
|
||||
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) {
|
||||
const index = sounds.indexOf(playingAudio)
|
||||
if (index > -1) {
|
||||
sounds.splice(index, 1)
|
||||
}
|
||||
if (sounds.length === 0) {
|
||||
activeSounds.delete(soundUrl)
|
||||
}
|
||||
}
|
||||
}, { once: true })
|
||||
|
||||
await playingAudio.play()
|
||||
} catch (error) {
|
||||
console.error(`Failed to play sound ${soundUrl}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
const stopSound = (soundUrl: string) => {
|
||||
const sounds = activeSounds.get(soundUrl)
|
||||
if (!sounds) return
|
||||
|
||||
sounds.forEach(audio => {
|
||||
audio.pause()
|
||||
audio.currentTime = 0
|
||||
})
|
||||
activeSounds.delete(soundUrl)
|
||||
}
|
||||
|
||||
const stopAllSounds = () => {
|
||||
activeSounds.forEach((sounds, url) => {
|
||||
stopSound(url)
|
||||
})
|
||||
activeSounds.clear()
|
||||
}
|
||||
|
||||
const clearSoundCache = async () => {
|
||||
stopAllSounds()
|
||||
audioCache.clear()
|
||||
await soundStorage.reset()
|
||||
}
|
||||
|
||||
|
||||
// New method to preload multiple sounds
|
||||
const preloadSounds = async (soundUrls: string[]) => {
|
||||
return Promise.all(soundUrls.map(preloadSound))
|
||||
}
|
||||
|
||||
return {
|
||||
playSound,
|
||||
stopSound,
|
||||
stopAllSounds,
|
||||
clearSoundCache,
|
||||
preloadSounds
|
||||
}
|
||||
}
|
@ -46,3 +46,9 @@ export class CharacterHairStorage extends BaseStorage<any> {
|
||||
return characterType?.sprite
|
||||
}
|
||||
}
|
||||
|
||||
export class SoundStorage extends BaseStorage<{ id: string; name: string; base64: string }> {
|
||||
constructor() {
|
||||
super('sounds', 'id, name, createdAt, updatedAt')
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user