Preload media assets and refine process cursors

This commit is contained in:
2026-05-18 11:15:39 +02:00
parent 4f6300042c
commit 6e908037fb
4 changed files with 162 additions and 52 deletions
+77 -17
View File
@@ -9,6 +9,8 @@ class AudioManagerModule extends BaseModule {
super('audio-manager', 'Audio Manager');
this.sounds = new Map();
this.sfxCache = new Map();
this.musicCache = new Map();
this.imageCache = new Map();
this.currentAudio = null;
this.currentAudioRole = null;
this.currentLoop = null;
@@ -486,29 +488,87 @@ class AudioManagerModule extends BaseModule {
async preloadSfx(filename) {
const url = this.getAssetUrl('sounds', filename);
if (this.sfxCache.has(url)) {
return this.sfxCache.get(url);
}
const promise = new Promise((resolve, reject) => {
const audio = new Audio(url);
audio.preload = 'auto';
this.setMediaVolume(audio, this.getSfxVolume());
audio.addEventListener('canplaythrough', () => resolve(audio), { once: true });
audio.addEventListener('error', () => reject(new Error(`Failed to preload sound effect: ${url}`)), { once: true });
audio.load();
});
if (this.sfxCache.has(url)) return this.sfxCache.get(url);
const promise = this.preloadAudioUrl(url, 'sound effect')
.then(audio => {
this.setMediaVolume(audio, this.getSfxVolume());
return audio;
});
this.sfxCache.set(url, promise);
return promise;
}
async preloadMusic(filename) {
const url = this.getAssetUrl('music', filename);
if (this.musicCache.has(url)) return this.musicCache.get(url);
const promise = this.preloadAudioUrl(url, 'music track')
.then(audio => {
this.setMediaVolume(audio, this.getMusicVolume());
return audio;
});
this.musicCache.set(url, promise);
return promise;
}
preloadAudioUrl(url, label = 'audio') {
return new Promise((resolve, reject) => {
const audio = new Audio(url);
let settled = false;
const finish = (result, error = null) => {
if (settled) return;
settled = true;
audio.removeEventListener('canplaythrough', onReady);
audio.removeEventListener('loadeddata', onReady);
audio.removeEventListener('error', onError);
if (error) reject(error);
else resolve(result);
};
const onReady = () => finish(audio);
const onError = () => finish(null, new Error(`Failed to preload ${label}: ${url}`));
audio.preload = 'auto';
audio.addEventListener('canplaythrough', onReady, { once: true });
audio.addEventListener('loadeddata', onReady, { once: true });
audio.addEventListener('error', onError, { once: true });
audio.load();
});
}
async preloadImage(filename) {
const url = this.getAssetUrl('images', filename);
if (this.imageCache.has(url)) return this.imageCache.get(url);
const promise = new Promise((resolve, reject) => {
const image = new Image();
image.decoding = 'async';
image.onload = () => {
if (typeof image.decode === 'function') {
image.decode().catch(() => null).then(() => resolve(image));
} else {
resolve(image);
}
};
image.onerror = () => reject(new Error(`Failed to preload image: ${url}`));
image.src = url;
});
this.imageCache.set(url, promise);
return promise;
}
async preloadStructuredBlock(block = {}) {
const type = String(block.type || block.kind || '').toLowerCase();
const filename = block.filename || block.metadata?.filename;
if (!filename) return null;
if (type === 'image') return this.preloadImage(filename);
if (type === 'music') return this.preloadMusic(filename);
if (type === 'sfx' || type === 'sound') return this.preloadSfx(filename);
return null;
}
async preloadMediaCues(cues = []) {
const tasks = cues
.filter(cue => cue && cue.type === 'sfx' && cue.filename)
.map(cue => this.preloadSfx(cue.filename).catch(error => {
console.warn('AudioManager: SFX preload failed:', error);
return null;
.filter(cue => cue && cue.filename)
.map(cue => this.preloadStructuredBlock(cue).catch(error => {
console.warn('AudioManager: Media cue preload failed:', error);
throw error;
}));
await Promise.all(tasks);