Feat/env-manager #1

Merged
math-pixel merged 46 commits from feat/env-manager into develop 2026-05-11 15:23:32 +00:00
4 changed files with 100 additions and 6 deletions
Showing only changes of commit 2bb980c71c - Show all commits
+9 -5
View File
@@ -1,6 +1,8 @@
import { useSettingsStore } from "@/managers/stores/useSettingsStore"; import { useSettingsStore } from "@/managers/stores/useSettingsStore";
import { useSubtitleStore } from "@/managers/stores/useSubtitleStore";
import type { DialogueSpeaker } from "@/types/dialogues/dialogues";
export type SubtitleSpeaker = "Narrateur" | "Fermier" | "Leonie"; export type SubtitleSpeaker = DialogueSpeaker;
interface SubtitlesProps { interface SubtitlesProps {
speaker?: SubtitleSpeaker | null; speaker?: SubtitleSpeaker | null;
@@ -12,18 +14,20 @@ export function Subtitles({
text = null, text = null,
}: SubtitlesProps): React.JSX.Element | null { }: SubtitlesProps): React.JSX.Element | null {
const subtitlesEnabled = useSettingsStore((state) => state.subtitlesEnabled); const subtitlesEnabled = useSettingsStore((state) => state.subtitlesEnabled);
const content = text?.trim(); const activeSubtitle = useSubtitleStore((state) => state.activeSubtitle);
const subtitleSpeaker = speaker ?? activeSubtitle?.speaker ?? null;
const content = (text ?? activeSubtitle?.text)?.trim();
if (!subtitlesEnabled || !content) return null; if (!subtitlesEnabled || !content) return null;
return ( return (
<div className="subtitles" aria-live="polite"> <div className="subtitles" aria-live="polite">
<p> <p>
{speaker ? ( {subtitleSpeaker ? (
<span <span
className={`subtitles__speaker subtitles__speaker--${speaker.toLowerCase()}`} className={`subtitles__speaker subtitles__speaker--${subtitleSpeaker.toLowerCase()}`}
> >
{speaker}: {subtitleSpeaker}:
</span> </span>
) : null} ) : null}
{content} {content}
+7 -1
View File
@@ -54,7 +54,11 @@ export class AudioManager {
return this._categoryVolumes[category]; return this._categoryVolumes[category];
} }
playSound(path: string, volume = 1, options: PlaySoundOptions = {}): void { playSound(
path: string,
volume = 1,
options: PlaySoundOptions = {},
): HTMLAudioElement {
const audio = this._acquireAudio(path); const audio = this._acquireAudio(path);
const category = options.category ?? AudioManager.DEFAULT_SOUND_CATEGORY; const category = options.category ?? AudioManager.DEFAULT_SOUND_CATEGORY;
audio.volume = this._getEffectiveVolume(category, volume); audio.volume = this._getEffectiveVolume(category, volume);
@@ -75,6 +79,8 @@ export class AudioManager {
error: AudioManager._toLogValue(error), error: AudioManager._toLogValue(error),
}); });
}); });
return audio;
} }
playMusic(path: string, volume = 1): void { playMusic(path: string, volume = 1): void {
+24
View File
@@ -0,0 +1,24 @@
import { create } from "zustand";
import type { DialogueSpeaker } from "@/types/dialogues/dialogues";
interface ActiveSubtitle {
speaker: DialogueSpeaker;
text: string;
}
interface SubtitleState {
activeSubtitle: ActiveSubtitle | null;
}
interface SubtitleActions {
setActiveSubtitle: (subtitle: ActiveSubtitle | null) => void;
clearActiveSubtitle: () => void;
}
type SubtitleStore = SubtitleState & SubtitleActions;
export const useSubtitleStore = create<SubtitleStore>()((set) => ({
activeSubtitle: null,
setActiveSubtitle: (activeSubtitle) => set({ activeSubtitle }),
clearActiveSubtitle: () => set({ activeSubtitle: null }),
}));
+60
View File
@@ -0,0 +1,60 @@
import { AudioManager } from "@/managers/AudioManager";
import { useSettingsStore } from "@/managers/stores/useSettingsStore";
import { useSubtitleStore } from "@/managers/stores/useSubtitleStore";
import type { DialogueManifest } from "@/types/dialogues/dialogues";
import { loadDialogueSubtitleCue } from "@/utils/dialogues/loadDialogueManifest";
export async function playDialogueById(
manifest: DialogueManifest,
dialogueId: string,
): Promise<HTMLAudioElement | null> {
const dialogue = manifest.dialogues.find((item) => item.id === dialogueId);
if (!dialogue) return null;
const subtitleLanguage = useSettingsStore.getState().subtitleLanguage;
const subtitle = await loadDialogueSubtitleCue(
manifest,
dialogue,
subtitleLanguage,
);
const audio = AudioManager.getInstance().playSound(dialogue.audio, 1, {
category: "dialogue",
});
if (!subtitle) return audio;
const clearSubtitle = (): void => {
useSubtitleStore.getState().clearActiveSubtitle();
};
const cleanup = (): void => {
audio.removeEventListener("timeupdate", syncSubtitle);
audio.removeEventListener("ended", cleanup);
audio.removeEventListener("pause", cleanup);
clearSubtitle();
};
const syncSubtitle = (): void => {
const currentTime = audio.currentTime;
const shouldShowSubtitle =
currentTime >= subtitle.cue.startTime &&
currentTime <= subtitle.cue.endTime;
if (shouldShowSubtitle) {
useSubtitleStore.getState().setActiveSubtitle({
speaker: subtitle.voice.speaker,
text: subtitle.cue.text,
});
return;
}
clearSubtitle();
};
audio.addEventListener("timeupdate", syncSubtitle);
audio.addEventListener("ended", cleanup);
audio.addEventListener("pause", cleanup);
syncSubtitle();
return audio;
}