Checkpoint before line-grid scrolling refactor

This commit is contained in:
2026-05-16 13:44:03 +02:00
parent 42582352d6
commit fe33e4f0ab
25 changed files with 1989 additions and 840 deletions
+100 -26
View File
@@ -22,6 +22,7 @@ class AudioManagerModule extends BaseModule {
this.activeTtsPlaybackCount = 0;
this.ttsQueueEmpty = true;
this.pendingMusicPlayback = null;
this.currentMusicState = null;
this.assetRoots = {
images: '/images/',
music: '/music/',
@@ -203,14 +204,14 @@ class AudioManagerModule extends BaseModule {
this.currentLoop = audio;
} else {
this.currentAudio = audio.cloneNode(true);
this.currentAudio.volume = this.getSfxVolume();
this.setMediaVolume(this.currentAudio, this.getSfxVolume());
this.currentAudio.play().catch(error => {
console.error('Error playing audio:', error);
});
return this.currentAudio;
}
audio.volume = this.getMusicVolume();
this.setMediaVolume(audio, this.getMusicVolume());
audio.play().catch(error => {
console.error('Error playing audio:', error);
});
@@ -233,14 +234,14 @@ class AudioManagerModule extends BaseModule {
}
this.currentLoop = new Audio(url);
this.currentLoop.loop = true;
this.currentLoop.volume = this.getMusicVolume();
this.setMediaVolume(this.currentLoop, this.getMusicVolume());
this.currentLoop.play().catch(error => {
console.error('Error playing audio loop:', error);
});
return this.currentLoop;
} else {
this.currentAudio = new Audio(url);
this.currentAudio.volume = this.getSfxVolume();
this.setMediaVolume(this.currentAudio, this.getSfxVolume());
this.currentAudio.play().catch(error => {
console.error('Error playing audio:', error);
});
@@ -331,19 +332,19 @@ class AudioManagerModule extends BaseModule {
updateVolumes() {
this.sounds.forEach(audio => {
const isMusic = audio.loop;
audio.volume = this.masterVolume * (isMusic ? this.musicVolume : this.sfxVolume);
this.setMediaVolume(audio, this.masterVolume * (isMusic ? this.musicVolume : this.sfxVolume));
});
if (this.currentAudio) {
this.currentAudio.volume = this.masterVolume * this.sfxVolume;
this.setMediaVolume(this.currentAudio, this.masterVolume * this.sfxVolume);
}
if (this.currentLoop) {
this.currentLoop.volume = this.getMusicVolume();
this.setMediaVolume(this.currentLoop, this.getMusicVolume());
}
if (this.currentMusic) {
this.currentMusic.volume = this.getMusicVolume();
this.setMediaVolume(this.currentMusic, this.getMusicVolume());
}
}
@@ -351,6 +352,11 @@ class AudioManagerModule extends BaseModule {
return Math.max(0, Math.min(1, Number.isFinite(Number(volume)) ? Number(volume) : 1));
}
setMediaVolume(audio, volume) {
if (!audio) return;
audio.volume = this.clampVolume(volume);
}
getSfxVolume() {
return this.masterVolume * this.sfxVolume;
}
@@ -387,8 +393,8 @@ class AudioManagerModule extends BaseModule {
const audio = this.currentMusic;
const token = ++this.musicFadeToken;
const startVolume = audio.volume;
const targetVolume = this.getUnduckedMusicVolume() * this.musicDuckingFactor;
const startVolume = this.clampVolume(audio.volume);
const targetVolume = this.clampVolume(this.getUnduckedMusicVolume() * this.musicDuckingFactor);
const start = performance.now();
const tick = () => {
@@ -396,7 +402,7 @@ class AudioManagerModule extends BaseModule {
return;
}
const progress = Math.min(1, (performance.now() - start) / duration);
audio.volume = startVolume + ((targetVolume - startVolume) * progress);
this.setMediaVolume(audio, startVolume + ((targetVolume - startVolume) * progress));
if (progress < 1) {
requestAnimationFrame(tick);
}
@@ -428,7 +434,7 @@ class AudioManagerModule extends BaseModule {
const promise = new Promise((resolve, reject) => {
const audio = new Audio(url);
audio.preload = 'auto';
audio.volume = this.getSfxVolume();
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();
@@ -556,7 +562,7 @@ class AudioManagerModule extends BaseModule {
try {
const template = await this.preloadSfx(filename);
const audio = template.cloneNode(true);
audio.volume = this.getSfxVolume();
this.setMediaVolume(audio, this.getSfxVolume());
this.currentAudio = audio;
const maxDuration = Math.max(0, Number(options.maxDurationSeconds || options.maxDuration || 0)) * 1000;
const endMode = String(options.endMode || options.mode || 'stop').toLowerCase().startsWith('fade') ? 'fade' : 'stop';
@@ -595,12 +601,12 @@ class AudioManagerModule extends BaseModule {
fadeOutAudio(audio, duration = 1000) {
if (!audio) return Promise.resolve(false);
const startVolume = audio.volume;
const startVolume = this.clampVolume(audio.volume);
const startedAt = performance.now();
return new Promise(resolve => {
const step = () => {
const progress = Math.min(1, (performance.now() - startedAt) / duration);
audio.volume = startVolume * (1 - progress);
this.setMediaVolume(audio, startVolume * (1 - progress));
if (progress < 1 && !audio.paused && !audio.ended) {
requestAnimationFrame(step);
return;
@@ -617,6 +623,8 @@ class AudioManagerModule extends BaseModule {
async playMusic(filename, mode = 'crossfade', options = {}) {
const url = this.getAssetUrl('music', filename);
const shouldLoop = options.loop !== false;
const startAt = Math.max(0, Number(options.startAt ?? options.currentTime ?? 0) || 0);
const fadeInSeconds = Math.max(0, Number(options.fadeInSeconds ?? options.fadeIn ?? 0) || 0);
if (mode === 'queue' && this.currentMusic && !this.currentMusic.paused) {
this.queuedMusic = { filename, mode: 'cut', options: { loop: shouldLoop } };
@@ -631,22 +639,44 @@ class AudioManagerModule extends BaseModule {
const next = new Audio(url);
next.loop = shouldLoop;
next.volume = mode === 'crossfade' && this.currentMusic ? 0 : this.getMusicVolume();
this.setMediaVolume(next, (mode === 'crossfade' && this.currentMusic) || fadeInSeconds > 0 ? 0 : this.getMusicVolume());
if (startAt > 0) {
try {
next.currentTime = startAt;
} catch {
next.addEventListener('loadedmetadata', () => {
next.currentTime = Math.min(startAt, Number.isFinite(next.duration) ? next.duration : startAt);
}, { once: true });
}
}
next.addEventListener('ended', () => {
if (this.currentMusic === next) {
this.currentMusic = null;
this.currentMusicState = null;
}
});
const nextState = {
filename,
url,
loop: shouldLoop,
mode,
startedAt: Date.now()
};
if (mode === 'cut' || !this.currentMusic) {
this.stopCurrentMusic();
this.currentMusic = next;
this.currentMusicState = nextState;
await this.startMusicAudio(next, filename);
if (fadeInSeconds > 0) {
this.fadeAudioTo(next, this.getMusicVolume(), fadeInSeconds * 1000);
}
return next;
}
const previous = this.currentMusic;
this.currentMusic = next;
this.currentMusicState = nextState;
await this.startMusicAudio(next, filename);
this.crossfade(previous, next, 1500);
console.log(`AudioManager: Crossfading music to ${filename}`);
@@ -678,7 +708,7 @@ class AudioManagerModule extends BaseModule {
const pending = this.pendingMusicPlayback;
this.pendingMusicPlayback = null;
pending.audio.volume = this.getMusicVolume();
this.setMediaVolume(pending.audio, this.getMusicVolume());
try {
await pending.audio.play();
@@ -697,17 +727,61 @@ class AudioManagerModule extends BaseModule {
this.currentMusic.pause();
this.currentMusic.currentTime = 0;
this.currentMusic = null;
this.currentMusicState = null;
}
getMusicState() {
if (!this.currentMusic || this.currentMusic.paused || this.currentMusic.ended || !this.currentMusicState?.filename) {
return null;
}
return {
filename: this.currentMusicState.filename,
currentTime: Math.max(0, Number(this.currentMusic.currentTime || 0)),
loop: Boolean(this.currentMusic.loop),
mode: this.currentMusicState.mode || 'cut',
volume: this.currentMusic.volume,
duckingFactor: this.musicDuckingFactor
};
}
async restoreMusicState(state = null) {
if (!state?.filename) return null;
return this.playMusic(state.filename, 'cut', {
loop: state.loop !== false,
startAt: Number(state.currentTime || 0),
fadeInSeconds: 1.5
});
}
fadeAudioTo(audio, targetVolume, duration = 1000) {
if (!audio) return Promise.resolve(false);
const startVolume = this.clampVolume(audio.volume);
const target = this.clampVolume(targetVolume);
const startedAt = performance.now();
return new Promise(resolve => {
const step = (now) => {
const progress = duration <= 0 ? 1 : Math.min(1, (now - startedAt) / duration);
this.setMediaVolume(audio, startVolume + ((target - startVolume) * progress));
if (progress < 1 && !audio.paused && !audio.ended) {
requestAnimationFrame(step);
return;
}
resolve(true);
};
requestAnimationFrame(step);
});
}
crossfade(previous, next, duration = 1500) {
const start = performance.now();
const previousStart = previous ? previous.volume : 0;
const target = this.getMusicVolume();
const previousStart = previous ? this.clampVolume(previous.volume) : 0;
const target = this.clampVolume(this.getMusicVolume());
const tick = () => {
const progress = Math.min(1, (performance.now() - start) / duration);
if (previous) previous.volume = previousStart * (1 - progress);
next.volume = target * progress;
if (previous) this.setMediaVolume(previous, previousStart * (1 - progress));
this.setMediaVolume(next, target * progress);
if (progress < 1) {
requestAnimationFrame(tick);
@@ -718,7 +792,7 @@ class AudioManagerModule extends BaseModule {
previous.pause();
previous.currentTime = 0;
}
next.volume = this.getMusicVolume();
this.setMediaVolume(next, this.getMusicVolume());
};
tick();
@@ -747,7 +821,7 @@ class AudioManagerModule extends BaseModule {
}
const audio = this.currentAudio;
const initialVolume = audio.volume;
const initialVolume = this.clampVolume(audio.volume);
const volumeStep = initialVolume / (duration / 50);
let currentVolume = initialVolume;
@@ -757,10 +831,10 @@ class AudioManagerModule extends BaseModule {
clearInterval(fadeInterval);
audio.pause();
audio.currentTime = 0;
audio.volume = initialVolume; // Reset volume for future use
this.setMediaVolume(audio, initialVolume); // Reset volume for future use
resolve();
} else {
audio.volume = currentVolume;
this.setMediaVolume(audio, currentVolume);
}
}, 50);
});
@@ -806,7 +880,7 @@ class AudioManagerModule extends BaseModule {
}
// Apply master volume and speech volume
audio.volume = this.masterVolume * speechVolume * this._ttsVolume;
this.setMediaVolume(audio, this.masterVolume * speechVolume * this.ttsVolume);
// Set up cleanup
audio.onended = () => {