import type { KeyboardEvent as ReactKeyboardEvent } from "react"; import { formatControlEffectValue, getControlTargetRefKey } from "../controls/control-surface"; import { type ProjectDialogueLibrary, getProjectDialogues } from "../dialogues/project-dialogues"; import { getProjectScheduleEffectOptionId, getProjectScheduleTargetOptionByKey, listProjectScheduleEffectOptions, type ProjectScheduleEffectOptionId, type ProjectScheduleTargetOption } from "../scheduler/project-schedule-control-options"; import { getSequenceEffectLabel, type SequenceEffect, type SequenceVisibilityMode } from "../sequencer/project-sequence-steps"; import { getProjectSequences, type ProjectSequenceLibrary } from "../sequencer/project-sequences"; interface ProjectSequencesPanelProps { sequences: ProjectSequenceLibrary; dialogues: ProjectDialogueLibrary; targetOptions: ProjectScheduleTargetOption[]; teleportTargetOptions: Array<{ entityId: string; label: string; }>; sceneTransitionTargetOptions: Array<{ targetKey: string; label: string; }>; visibilityTargetOptions: Array<{ targetKey: string; label: string; }>; preferredControlTargetKey?: string | null; selectedSequenceId: string | null; onSelectSequence(sequenceId: string | null): void; onAddSequence(): void; onDeleteSequence(sequenceId: string): void; onSetSequenceTitle(sequenceId: string, title: string): void; onAddControlEffect( sequenceId: string, targetKey: string, effectOptionId: ProjectScheduleEffectOptionId ): void; onAddDialogueStep(sequenceId: string, dialogueId: string): void; onAddTeleportStep(sequenceId: string, targetEntityId: string): void; onAddSceneTransitionStep(sequenceId: string, targetKey: string): void; onAddVisibilityStep(sequenceId: string, targetKey: string): void; onDeleteStep(sequenceId: string, stepIndex: number): void; onSetControlStepTarget( sequenceId: string, stepIndex: number, targetKey: string ): void; onSetControlStepEffectOption( sequenceId: string, stepIndex: number, effectOptionId: ProjectScheduleEffectOptionId ): void; onSetControlStepNumericValue( sequenceId: string, stepIndex: number, value: number ): void; onSetControlStepColorValue( sequenceId: string, stepIndex: number, colorHex: string ): void; onSetControlStepAnimationClip( sequenceId: string, stepIndex: number, clipName: string ): void; onSetControlStepAnimationLoop( sequenceId: string, stepIndex: number, loop: boolean ): void; onSetControlStepPathId( sequenceId: string, stepIndex: number, pathId: string ): void; onSetControlStepPathSpeed( sequenceId: string, stepIndex: number, speed: number ): void; onSetControlStepPathLoop( sequenceId: string, stepIndex: number, loop: boolean ): void; onSetDialogueStepDialogueId( sequenceId: string, stepIndex: number, dialogueId: string ): void; onSetTeleportStepTarget( sequenceId: string, stepIndex: number, targetEntityId: string ): void; onSetSceneTransitionStepTarget( sequenceId: string, stepIndex: number, targetKey: string ): void; onSetVisibilityStepTarget( sequenceId: string, stepIndex: number, targetKey: string ): void; onSetVisibilityStepMode( sequenceId: string, stepIndex: number, mode: SequenceVisibilityMode ): void; } function commitOnEnter( event: ReactKeyboardEvent, commit: () => void ) { if (event.key !== "Enter") { return; } event.currentTarget.blur(); commit(); } function getControlEffectNumericValue( effect: Extract ): number | null { switch (effect.effect.type) { case "setSoundVolume": return effect.effect.volume; case "setLightIntensity": case "setAmbientLightIntensity": case "setSunLightIntensity": return effect.effect.intensity; default: return null; } } function getControlEffectColorValue( effect: Extract ): string | null { switch (effect.effect.type) { case "setLightColor": case "setAmbientLightColor": case "setSunLightColor": return effect.effect.colorHex; default: return null; } } export function ProjectSequencesPanel({ sequences, dialogues, targetOptions, teleportTargetOptions, sceneTransitionTargetOptions, visibilityTargetOptions, preferredControlTargetKey = null, selectedSequenceId, onSelectSequence, onAddSequence, onDeleteSequence, onSetSequenceTitle, onAddControlEffect, onAddDialogueStep, onAddTeleportStep, onAddSceneTransitionStep, onAddVisibilityStep, onDeleteStep, onSetControlStepTarget, onSetControlStepEffectOption, onSetControlStepNumericValue, onSetControlStepColorValue, onSetControlStepAnimationClip, onSetControlStepAnimationLoop, onSetControlStepPathId, onSetControlStepPathSpeed, onSetControlStepPathLoop, onSetDialogueStepDialogueId, onSetTeleportStepTarget, onSetSceneTransitionStepTarget, onSetVisibilityStepTarget, onSetVisibilityStepMode }: ProjectSequencesPanelProps) { const sequenceList = getProjectSequences(sequences); const dialogueList = getProjectDialogues(dialogues); const editableTargetOptions = targetOptions.filter( (targetOption) => listProjectScheduleEffectOptions(targetOption).length > 0 ); const preferredControlTargetOption = editableTargetOptions.find( (targetOption) => targetOption.key === preferredControlTargetKey ) ?? null; const addableControlTargetOptions = preferredControlTargetOption === null ? editableTargetOptions : [preferredControlTargetOption]; const addableControlEffects = addableControlTargetOptions.flatMap((targetOption) => listProjectScheduleEffectOptions(targetOption).map((effectOption) => ({ targetKey: targetOption.key, effectOptionId: effectOption.id, label: preferredControlTargetOption === null ? `Add ${targetOption.label} ${effectOption.label} Effect` : `Add ${effectOption.label} Effect` })) ); const selectedSequence = selectedSequenceId === null ? null : sequences.sequences[selectedSequenceId] ?? null; return (
Sequences
{sequenceList.length === 0 ? (
No project sequences authored yet.
) : (
{sequenceList.map((sequence) => (
))}
)}
{selectedSequence === null ? (
Select a sequence to edit its title and effects.
) : (
A sequence is a reusable bundle of engine effects. Some effects stay active while a timeline placement is active. Others fire once when the sequence starts.
Effects
{selectedSequence.effects.length === 0 ? (
Add an effect to define what this sequence does.
) : (
{selectedSequence.effects.map((effect, effectIndex) => { if (effect.type === "controlEffect") { const targetKey = getControlTargetRefKey(effect.effect.target); const targetOption = getProjectScheduleTargetOptionByKey(targetOptions, targetKey); const effectOptions = targetOption === null ? [] : listProjectScheduleEffectOptions(targetOption); const effectOptionId = targetOption === null ? null : (() => { try { return getProjectScheduleEffectOptionId(effect.effect); } catch { return null; } })(); return (
{getSequenceEffectLabel(effect)}
{targetOption === null || effectOptionId === null ? (
{formatControlEffectValue(effect.effect)}. This effect is preserved, but the current editor can only edit targets and effects exposed through the existing control catalog.
) : ( <>
{effectOptions.find((option) => option.id === effectOptionId) ?.valueKind === "number" ? ( ) : null} {effectOptions.find((option) => option.id === effectOptionId) ?.valueKind === "color" ? ( ) : null} {effect.effect.type === "playModelAnimation" || effect.effect.type === "playActorAnimation" ? ( <> ) : null} {effect.effect.type === "followActorPath" ? ( <> ) : null} )}
); } if (effect.type === "startDialogue") { return (
{getSequenceEffectLabel(effect)}
); } if (effect.type === "teleportPlayer") { return (
{getSequenceEffectLabel(effect)}
); } if (effect.type === "startSceneTransition") { return (
{getSequenceEffectLabel(effect)}
); } if (effect.type === "setVisibility") { return (
{getSequenceEffectLabel(effect)}
); } return (
{getSequenceEffectLabel(effect)}
This effect is preserved, but the current editor does not expose direct editing for it yet.
); })}
)}
{addableControlEffects.map((effectButton) => ( ))}
)}
); }