update: sync dialogue and cinematic
This commit is contained in:
@@ -4,6 +4,12 @@
|
|||||||
{
|
{
|
||||||
"id": "intro_overview",
|
"id": "intro_overview",
|
||||||
"timecode": 0,
|
"timecode": 0,
|
||||||
|
"dialogueCues": [
|
||||||
|
{
|
||||||
|
"time": 0,
|
||||||
|
"dialogueId": "narrateur_bienvenueaaltera"
|
||||||
|
}
|
||||||
|
],
|
||||||
"cameraKeyframes": [
|
"cameraKeyframes": [
|
||||||
{
|
{
|
||||||
"time": 0,
|
"time": 0,
|
||||||
|
|||||||
@@ -31,8 +31,7 @@
|
|||||||
"id": "narrateur_bienvenueaaltera",
|
"id": "narrateur_bienvenueaaltera",
|
||||||
"voice": "narrateur",
|
"voice": "narrateur",
|
||||||
"audio": "/sounds/dialogue/narrateur_bienvenueaaltera.mp3",
|
"audio": "/sounds/dialogue/narrateur_bienvenueaaltera.mp3",
|
||||||
"subtitleCueIndex": 1,
|
"subtitleCueIndex": 1
|
||||||
"timecode": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "narrateur_intro_prenom",
|
"id": "narrateur_intro_prenom",
|
||||||
|
|||||||
@@ -74,6 +74,16 @@ function getManifestErrors(manifest: CinematicManifest | null): string[] {
|
|||||||
errors.push(`${label}: les temps des keyframes doivent augmenter.`);
|
errors.push(`${label}: les temps des keyframes doivent augmenter.`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cinematic.dialogueCues?.forEach((cue, cueIndex) => {
|
||||||
|
if (!Number.isFinite(cue.time) || cue.time < 0) {
|
||||||
|
errors.push(`${label}: dialogue cue ${cueIndex + 1} time invalide.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cue.dialogueId.trim()) {
|
||||||
|
errors.push(`${label}: dialogue cue ${cueIndex + 1} id obligatoire.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
@@ -105,6 +115,11 @@ function getPatchedCinematic(
|
|||||||
cameraKeyframes: patch.cameraKeyframes ?? cinematic.cameraKeyframes,
|
cameraKeyframes: patch.cameraKeyframes ?? cinematic.cameraKeyframes,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const dialogueCues = patch.dialogueCues ?? cinematic.dialogueCues;
|
||||||
|
if (dialogueCues) {
|
||||||
|
nextCinematic.dialogueCues = dialogueCues;
|
||||||
|
}
|
||||||
|
|
||||||
if ("timecode" in patch) {
|
if ("timecode" in patch) {
|
||||||
if (patch.timecode !== undefined) nextCinematic.timecode = patch.timecode;
|
if (patch.timecode !== undefined) nextCinematic.timecode = patch.timecode;
|
||||||
} else if (cinematic.timecode !== undefined) {
|
} else if (cinematic.timecode !== undefined) {
|
||||||
|
|||||||
@@ -6,10 +6,16 @@ export interface CinematicCameraKeyframe {
|
|||||||
target: Vector3Tuple;
|
target: Vector3Tuple;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CinematicDialogueCue {
|
||||||
|
time: number;
|
||||||
|
dialogueId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CinematicDefinition {
|
export interface CinematicDefinition {
|
||||||
id: string;
|
id: string;
|
||||||
timecode?: number;
|
timecode?: number;
|
||||||
cameraKeyframes: CinematicCameraKeyframe[];
|
cameraKeyframes: CinematicCameraKeyframe[];
|
||||||
|
dialogueCues?: CinematicDialogueCue[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CinematicManifest {
|
export interface CinematicManifest {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type {
|
import type {
|
||||||
CinematicCameraKeyframe,
|
CinematicCameraKeyframe,
|
||||||
CinematicDefinition,
|
CinematicDefinition,
|
||||||
|
CinematicDialogueCue,
|
||||||
CinematicManifest,
|
CinematicManifest,
|
||||||
} from "@/types/cinematics/cinematics";
|
} from "@/types/cinematics/cinematics";
|
||||||
import type { Vector3Tuple } from "@/types/three/three";
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
@@ -50,9 +51,28 @@ function parseCinematicDefinition(data: unknown): CinematicDefinition {
|
|||||||
cinematic.timecode = data.timecode;
|
cinematic.timecode = data.timecode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(data.dialogueCues)) {
|
||||||
|
cinematic.dialogueCues = data.dialogueCues.map(parseDialogueCue);
|
||||||
|
}
|
||||||
|
|
||||||
return cinematic;
|
return cinematic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseDialogueCue(data: unknown): CinematicDialogueCue {
|
||||||
|
if (
|
||||||
|
!isRecord(data) ||
|
||||||
|
typeof data.time !== "number" ||
|
||||||
|
typeof data.dialogueId !== "string"
|
||||||
|
) {
|
||||||
|
throw new Error("Invalid cinematic dialogue cue");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
time: data.time,
|
||||||
|
dialogueId: data.dialogueId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function parseCameraKeyframe(data: unknown): CinematicCameraKeyframe {
|
function parseCameraKeyframe(data: unknown): CinematicCameraKeyframe {
|
||||||
if (!isRecord(data) || typeof data.time !== "number") {
|
if (!isRecord(data) || typeof data.time !== "number") {
|
||||||
throw new Error("Invalid cinematic camera keyframe");
|
throw new Error("Invalid cinematic camera keyframe");
|
||||||
|
|||||||
@@ -8,17 +8,24 @@ import type {
|
|||||||
CinematicDefinition,
|
CinematicDefinition,
|
||||||
CinematicManifest,
|
CinematicManifest,
|
||||||
} from "@/types/cinematics/cinematics";
|
} from "@/types/cinematics/cinematics";
|
||||||
|
import type { DialogueManifest } from "@/types/dialogues/dialogues";
|
||||||
import { logger } from "@/utils/core/logger";
|
import { logger } from "@/utils/core/logger";
|
||||||
import { loadCinematicManifest } from "@/utils/cinematics/loadCinematicManifest";
|
import { loadCinematicManifest } from "@/utils/cinematics/loadCinematicManifest";
|
||||||
|
import { loadDialogueManifest } from "@/utils/dialogues/loadDialogueManifest";
|
||||||
|
import { queueDialogueById } from "@/utils/dialogues/playDialogue";
|
||||||
|
|
||||||
export function GameCinematics(): null {
|
export function GameCinematics(): null {
|
||||||
const camera = useThree((state) => state.camera);
|
const camera = useThree((state) => state.camera);
|
||||||
const [manifest, setManifest] = useState<CinematicManifest | null>(null);
|
const [manifest, setManifest] = useState<CinematicManifest | null>(null);
|
||||||
|
const [dialogueManifest, setDialogueManifest] =
|
||||||
|
useState<DialogueManifest | null>(null);
|
||||||
const playedCinematicsRef = useRef(new Set<string>());
|
const playedCinematicsRef = useRef(new Set<string>());
|
||||||
const timelineRef = useRef<gsap.core.Timeline | null>(null);
|
const timelineRef = useRef<gsap.core.Timeline | null>(null);
|
||||||
|
const activeAudiosRef = useRef(new Set<HTMLAudioElement>());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let mounted = true;
|
let mounted = true;
|
||||||
|
const activeAudios = activeAudiosRef.current;
|
||||||
|
|
||||||
void loadCinematicManifest()
|
void loadCinematicManifest()
|
||||||
.then((loadedManifest) => {
|
.then((loadedManifest) => {
|
||||||
@@ -30,9 +37,21 @@ export function GameCinematics(): null {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
void loadDialogueManifest()
|
||||||
|
.then((loadedManifest) => {
|
||||||
|
if (mounted) setDialogueManifest(loadedManifest);
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
logger.error("GameCinematics", "Failed to load dialogue manifest", {
|
||||||
|
error: error instanceof Error ? error : String(error),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
mounted = false;
|
mounted = false;
|
||||||
stopActiveCinematic(timelineRef);
|
stopActiveCinematic(timelineRef);
|
||||||
|
activeAudios.forEach((audio) => audio.pause());
|
||||||
|
activeAudios.clear();
|
||||||
useGameStore.getState().setCinematicPlaying(false);
|
useGameStore.getState().setCinematicPlaying(false);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
@@ -45,10 +64,14 @@ export function GameCinematics(): null {
|
|||||||
manifest.cinematics.forEach((cinematic) => {
|
manifest.cinematics.forEach((cinematic) => {
|
||||||
if (cinematic.timecode === undefined) return;
|
if (cinematic.timecode === undefined) return;
|
||||||
if (cinematic.timecode > elapsedTime) return;
|
if (cinematic.timecode > elapsedTime) return;
|
||||||
|
if (cinematic.dialogueCues && !dialogueManifest) return;
|
||||||
if (playedCinematicsRef.current.has(cinematic.id)) return;
|
if (playedCinematicsRef.current.has(cinematic.id)) return;
|
||||||
|
|
||||||
playedCinematicsRef.current.add(cinematic.id);
|
playedCinematicsRef.current.add(cinematic.id);
|
||||||
playCinematic(camera, cinematic, timelineRef);
|
playCinematic(camera, cinematic, timelineRef, {
|
||||||
|
dialogueManifest,
|
||||||
|
activeAudiosRef,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -66,6 +89,10 @@ function playCinematic(
|
|||||||
camera: THREE.Camera,
|
camera: THREE.Camera,
|
||||||
cinematic: CinematicDefinition,
|
cinematic: CinematicDefinition,
|
||||||
timelineRef: MutableRefObject<gsap.core.Timeline | null>,
|
timelineRef: MutableRefObject<gsap.core.Timeline | null>,
|
||||||
|
dialogueOptions: {
|
||||||
|
dialogueManifest: DialogueManifest | null;
|
||||||
|
activeAudiosRef: MutableRefObject<Set<HTMLAudioElement>>;
|
||||||
|
},
|
||||||
): void {
|
): void {
|
||||||
const firstKeyframe = cinematic.cameraKeyframes[0];
|
const firstKeyframe = cinematic.cameraKeyframes[0];
|
||||||
if (!firstKeyframe) return;
|
if (!firstKeyframe) return;
|
||||||
@@ -115,5 +142,29 @@ function playCinematic(
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cinematic.dialogueCues?.forEach((cue) => {
|
||||||
|
timeline.call(
|
||||||
|
() => {
|
||||||
|
if (!dialogueOptions.dialogueManifest) return;
|
||||||
|
|
||||||
|
void queueDialogueById(
|
||||||
|
dialogueOptions.dialogueManifest,
|
||||||
|
cue.dialogueId,
|
||||||
|
).then((audio) => {
|
||||||
|
if (!audio) return;
|
||||||
|
|
||||||
|
dialogueOptions.activeAudiosRef.current.add(audio);
|
||||||
|
audio.addEventListener(
|
||||||
|
"ended",
|
||||||
|
() => dialogueOptions.activeAudiosRef.current.delete(audio),
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
cue.time,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
timelineRef.current = timeline;
|
timelineRef.current = timeline;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -283,9 +283,15 @@ interface CinematicManifestData {
|
|||||||
interface CinematicData {
|
interface CinematicData {
|
||||||
id: string;
|
id: string;
|
||||||
timecode?: number;
|
timecode?: number;
|
||||||
|
dialogueCues?: CinematicDialogueCueData[];
|
||||||
cameraKeyframes: CinematicKeyframeData[];
|
cameraKeyframes: CinematicKeyframeData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CinematicDialogueCueData {
|
||||||
|
time: number;
|
||||||
|
dialogueId: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface CinematicKeyframeData {
|
interface CinematicKeyframeData {
|
||||||
time: number;
|
time: number;
|
||||||
position: [number, number, number];
|
position: [number, number, number];
|
||||||
@@ -510,9 +516,35 @@ function parseCinematicData(data: unknown): CinematicData {
|
|||||||
cinematic.timecode = data.timecode;
|
cinematic.timecode = data.timecode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.dialogueCues !== undefined) {
|
||||||
|
if (!Array.isArray(data.dialogueCues)) {
|
||||||
|
throw new Error(`Cinematic ${data.id} has invalid dialogue cues`);
|
||||||
|
}
|
||||||
|
cinematic.dialogueCues = data.dialogueCues.map(
|
||||||
|
parseCinematicDialogueCueData,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return cinematic;
|
return cinematic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseCinematicDialogueCueData(
|
||||||
|
data: unknown,
|
||||||
|
): CinematicDialogueCueData {
|
||||||
|
if (
|
||||||
|
!isRecord(data) ||
|
||||||
|
typeof data.time !== "number" ||
|
||||||
|
typeof data.dialogueId !== "string"
|
||||||
|
) {
|
||||||
|
throw new Error("Invalid cinematic dialogue cue");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
time: data.time,
|
||||||
|
dialogueId: data.dialogueId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function parseCinematicKeyframeData(data: unknown): CinematicKeyframeData {
|
function parseCinematicKeyframeData(data: unknown): CinematicKeyframeData {
|
||||||
if (!isRecord(data) || typeof data.time !== "number") {
|
if (!isRecord(data) || typeof data.time !== "number") {
|
||||||
throw new Error("Invalid cinematic camera keyframe");
|
throw new Error("Invalid cinematic camera keyframe");
|
||||||
|
|||||||
Reference in New Issue
Block a user