2026-04-14 23:12:32 +02:00
|
|
|
import {
|
2026-04-14 23:33:13 +02:00
|
|
|
getControlEffectLabel,
|
2026-04-14 23:12:32 +02:00
|
|
|
cloneControlEffect,
|
2026-04-15 00:47:28 +02:00
|
|
|
getControlEffectResolutionKey,
|
2026-04-14 23:12:32 +02:00
|
|
|
type ControlEffect
|
|
|
|
|
} from "../controls/control-surface";
|
2026-04-15 00:46:06 +02:00
|
|
|
import { HOURS_PER_DAY } from "../document/project-time-settings";
|
2026-04-14 23:12:32 +02:00
|
|
|
import {
|
|
|
|
|
getInteractionActionControlEffect,
|
|
|
|
|
type InteractionLink
|
|
|
|
|
} from "../interactions/interaction-links";
|
|
|
|
|
import type { ProjectScheduleRoutine } from "../scheduler/project-scheduler";
|
|
|
|
|
|
2026-04-15 00:46:06 +02:00
|
|
|
export const DEFAULT_PROJECT_SEQUENCE_DURATION_MINUTES = 240 as const;
|
|
|
|
|
export const LEGACY_PROJECT_SEQUENCE_DURATION_MINUTES = HOURS_PER_DAY * 60;
|
|
|
|
|
export const DEFAULT_HELD_SEQUENCE_CLIP_DURATION_MINUTES = 60 as const;
|
|
|
|
|
export const DEFAULT_IMPULSE_SEQUENCE_CLIP_DURATION_MINUTES = 1 as const;
|
|
|
|
|
export const MIN_PROJECT_SEQUENCE_CLIP_DURATION_MINUTES = 1 as const;
|
|
|
|
|
|
|
|
|
|
export interface SequenceClipTiming {
|
|
|
|
|
startMinute: number;
|
|
|
|
|
durationMinutes: number;
|
|
|
|
|
lane: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface BaseSequenceClip extends SequenceClipTiming {
|
|
|
|
|
stepClass: "held" | "impulse";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface HeldControlSequenceStep extends BaseSequenceClip {
|
2026-04-14 23:20:05 +02:00
|
|
|
stepClass: "held";
|
|
|
|
|
type: "controlEffect";
|
|
|
|
|
effect: ControlEffect;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 00:46:06 +02:00
|
|
|
export interface ImpulseControlSequenceStep extends BaseSequenceClip {
|
2026-04-14 23:20:05 +02:00
|
|
|
stepClass: "impulse";
|
2026-04-14 23:12:32 +02:00
|
|
|
type: "controlEffect";
|
|
|
|
|
effect: ControlEffect;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 00:46:06 +02:00
|
|
|
export interface StartDialogueSequenceStep extends BaseSequenceClip {
|
2026-04-14 23:20:05 +02:00
|
|
|
stepClass: "impulse";
|
2026-04-14 23:12:32 +02:00
|
|
|
type: "startDialogue";
|
|
|
|
|
dialogueId: string;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 00:46:06 +02:00
|
|
|
export interface TeleportPlayerSequenceStep extends BaseSequenceClip {
|
2026-04-14 23:20:05 +02:00
|
|
|
stepClass: "impulse";
|
2026-04-14 23:12:32 +02:00
|
|
|
type: "teleportPlayer";
|
|
|
|
|
targetEntityId: string;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 00:46:06 +02:00
|
|
|
export interface ToggleVisibilitySequenceStep extends BaseSequenceClip {
|
2026-04-14 23:20:05 +02:00
|
|
|
stepClass: "impulse";
|
2026-04-14 23:12:32 +02:00
|
|
|
type: "toggleVisibility";
|
|
|
|
|
targetBrushId: string;
|
|
|
|
|
visible?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 23:33:13 +02:00
|
|
|
type SequenceDefinitionLike = {
|
|
|
|
|
id: string;
|
2026-04-15 00:17:56 +02:00
|
|
|
clips: SequenceClip[];
|
2026-04-14 23:33:13 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type SequenceLibraryLike = {
|
|
|
|
|
sequences: Record<string, SequenceDefinitionLike>;
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-14 23:20:05 +02:00
|
|
|
export type HeldSequenceStep = HeldControlSequenceStep;
|
|
|
|
|
|
|
|
|
|
export type ImpulseSequenceStep =
|
|
|
|
|
| ImpulseControlSequenceStep
|
2026-04-14 23:12:32 +02:00
|
|
|
| StartDialogueSequenceStep
|
|
|
|
|
| TeleportPlayerSequenceStep
|
|
|
|
|
| ToggleVisibilitySequenceStep;
|
|
|
|
|
|
2026-04-15 00:17:56 +02:00
|
|
|
export type SequenceClip = HeldSequenceStep | ImpulseSequenceStep;
|
|
|
|
|
export type SequenceStep = SequenceClip;
|
2026-04-14 23:20:05 +02:00
|
|
|
|
2026-04-15 00:46:06 +02:00
|
|
|
function normalizeSequenceClipStartMinute(value: number | undefined): number {
|
|
|
|
|
if (value === undefined || !Number.isFinite(value)) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Math.max(0, Math.trunc(value));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getDefaultSequenceClipDurationMinutes(
|
|
|
|
|
stepClass: SequenceClip["stepClass"]
|
|
|
|
|
): number {
|
|
|
|
|
return stepClass === "held"
|
|
|
|
|
? LEGACY_PROJECT_SEQUENCE_DURATION_MINUTES
|
|
|
|
|
: DEFAULT_IMPULSE_SEQUENCE_CLIP_DURATION_MINUTES;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeSequenceClipDurationMinutes(
|
|
|
|
|
value: number | undefined,
|
|
|
|
|
stepClass: SequenceClip["stepClass"]
|
|
|
|
|
): number {
|
|
|
|
|
if (value === undefined || !Number.isFinite(value)) {
|
|
|
|
|
return getDefaultSequenceClipDurationMinutes(stepClass);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Math.max(
|
|
|
|
|
MIN_PROJECT_SEQUENCE_CLIP_DURATION_MINUTES,
|
|
|
|
|
Math.trunc(value)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeSequenceClipLane(value: number | undefined): number {
|
|
|
|
|
if (value === undefined || !Number.isFinite(value)) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Math.max(0, Math.trunc(value));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function cloneSequenceClipTiming(
|
|
|
|
|
clip: Pick<SequenceClip, "stepClass" | "startMinute" | "durationMinutes" | "lane">
|
|
|
|
|
): SequenceClipTiming {
|
|
|
|
|
return {
|
|
|
|
|
startMinute: normalizeSequenceClipStartMinute(clip.startMinute),
|
|
|
|
|
durationMinutes: normalizeSequenceClipDurationMinutes(
|
|
|
|
|
clip.durationMinutes,
|
|
|
|
|
clip.stepClass
|
|
|
|
|
),
|
|
|
|
|
lane: normalizeSequenceClipLane(clip.lane)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 00:17:56 +02:00
|
|
|
export function cloneSequenceClip(clip: SequenceClip): SequenceClip {
|
2026-04-15 00:46:06 +02:00
|
|
|
const timing = cloneSequenceClipTiming(clip);
|
|
|
|
|
|
2026-04-15 00:17:56 +02:00
|
|
|
switch (clip.type) {
|
2026-04-14 23:12:32 +02:00
|
|
|
case "controlEffect":
|
|
|
|
|
return {
|
2026-04-15 00:46:06 +02:00
|
|
|
...timing,
|
2026-04-15 00:17:56 +02:00
|
|
|
stepClass: clip.stepClass,
|
2026-04-14 23:12:32 +02:00
|
|
|
type: "controlEffect",
|
2026-04-15 00:17:56 +02:00
|
|
|
effect: cloneControlEffect(clip.effect)
|
2026-04-14 23:12:32 +02:00
|
|
|
};
|
|
|
|
|
case "startDialogue":
|
|
|
|
|
return {
|
2026-04-15 00:46:06 +02:00
|
|
|
...timing,
|
2026-04-14 23:20:05 +02:00
|
|
|
stepClass: "impulse",
|
2026-04-14 23:12:32 +02:00
|
|
|
type: "startDialogue",
|
2026-04-15 00:17:56 +02:00
|
|
|
dialogueId: clip.dialogueId
|
2026-04-14 23:12:32 +02:00
|
|
|
};
|
|
|
|
|
case "teleportPlayer":
|
|
|
|
|
return {
|
2026-04-15 00:46:06 +02:00
|
|
|
...timing,
|
2026-04-14 23:20:05 +02:00
|
|
|
stepClass: "impulse",
|
2026-04-14 23:12:32 +02:00
|
|
|
type: "teleportPlayer",
|
2026-04-15 00:17:56 +02:00
|
|
|
targetEntityId: clip.targetEntityId
|
2026-04-14 23:12:32 +02:00
|
|
|
};
|
|
|
|
|
case "toggleVisibility":
|
|
|
|
|
return {
|
2026-04-15 00:46:06 +02:00
|
|
|
...timing,
|
2026-04-14 23:20:05 +02:00
|
|
|
stepClass: "impulse",
|
2026-04-14 23:12:32 +02:00
|
|
|
type: "toggleVisibility",
|
2026-04-15 00:17:56 +02:00
|
|
|
targetBrushId: clip.targetBrushId,
|
|
|
|
|
visible: clip.visible
|
2026-04-14 23:12:32 +02:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 00:17:56 +02:00
|
|
|
export function cloneSequenceStep(step: SequenceStep): SequenceStep {
|
|
|
|
|
return cloneSequenceClip(step);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function cloneSequenceClips(clips: SequenceClip[]): SequenceClip[] {
|
|
|
|
|
return clips.map(cloneSequenceClip);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 23:12:32 +02:00
|
|
|
export function cloneSequenceSteps(steps: SequenceStep[]): SequenceStep[] {
|
2026-04-15 00:17:56 +02:00
|
|
|
return cloneSequenceClips(steps);
|
2026-04-14 23:12:32 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-15 00:17:56 +02:00
|
|
|
export function getSequenceClipLabel(clip: SequenceClip): string {
|
|
|
|
|
switch (clip.type) {
|
2026-04-14 23:33:13 +02:00
|
|
|
case "controlEffect":
|
2026-04-15 00:17:56 +02:00
|
|
|
return `${clip.stepClass === "held" ? "Held" : "Impulse"}: ${getControlEffectLabel(clip.effect)}`;
|
2026-04-14 23:33:13 +02:00
|
|
|
case "startDialogue":
|
|
|
|
|
return "Impulse: Start Dialogue";
|
|
|
|
|
case "teleportPlayer":
|
|
|
|
|
return "Impulse: Teleport Player";
|
|
|
|
|
case "toggleVisibility":
|
|
|
|
|
return "Impulse: Toggle Visibility";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 00:17:56 +02:00
|
|
|
export function getSequenceStepLabel(step: SequenceStep): string {
|
|
|
|
|
return getSequenceClipLabel(step);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 23:33:13 +02:00
|
|
|
export function getHeldSequenceSteps(
|
|
|
|
|
steps: readonly SequenceStep[]
|
|
|
|
|
): HeldSequenceStep[] {
|
2026-04-15 00:17:56 +02:00
|
|
|
return getHeldSequenceClips(steps);
|
2026-04-14 23:33:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getImpulseSequenceSteps(
|
|
|
|
|
steps: readonly SequenceStep[]
|
|
|
|
|
): ImpulseSequenceStep[] {
|
2026-04-15 00:17:56 +02:00
|
|
|
return getImpulseSequenceClips(steps);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getHeldSequenceClips(
|
|
|
|
|
clips: readonly SequenceClip[]
|
|
|
|
|
): HeldSequenceStep[] {
|
|
|
|
|
return clips
|
|
|
|
|
.filter((clip): clip is HeldSequenceStep => clip.stepClass === "held")
|
|
|
|
|
.map(cloneSequenceClip) as HeldSequenceStep[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getImpulseSequenceClips(
|
|
|
|
|
clips: readonly SequenceClip[]
|
|
|
|
|
): ImpulseSequenceStep[] {
|
|
|
|
|
return clips
|
|
|
|
|
.filter((clip): clip is ImpulseSequenceStep => clip.stepClass === "impulse")
|
|
|
|
|
.map(cloneSequenceClip) as ImpulseSequenceStep[];
|
2026-04-14 23:33:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getProjectSequenceHeldSteps(
|
|
|
|
|
sequence: SequenceDefinitionLike
|
|
|
|
|
): HeldSequenceStep[] {
|
2026-04-15 00:17:56 +02:00
|
|
|
return getProjectSequenceHeldClips(sequence);
|
2026-04-14 23:33:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getProjectSequenceImpulseSteps(
|
|
|
|
|
sequence: SequenceDefinitionLike
|
|
|
|
|
): ImpulseSequenceStep[] {
|
2026-04-15 00:17:56 +02:00
|
|
|
return getProjectSequenceImpulseClips(sequence);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getProjectSequenceHeldClips(
|
|
|
|
|
sequence: SequenceDefinitionLike
|
|
|
|
|
): HeldSequenceStep[] {
|
|
|
|
|
return getHeldSequenceClips(sequence.clips);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getProjectSequenceImpulseClips(
|
|
|
|
|
sequence: SequenceDefinitionLike
|
|
|
|
|
): ImpulseSequenceStep[] {
|
|
|
|
|
return getImpulseSequenceClips(sequence.clips);
|
2026-04-14 23:33:13 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-15 00:46:06 +02:00
|
|
|
export function getSequenceClipEndMinute(
|
|
|
|
|
clip: Pick<SequenceClip, "startMinute" | "durationMinutes">
|
|
|
|
|
): number {
|
|
|
|
|
return clip.startMinute + clip.durationMinutes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getProjectSequenceDurationMinutes(sequence: SequenceDefinitionLike): number {
|
|
|
|
|
return sequence.clips.reduce((maxDuration, clip) => {
|
|
|
|
|
return Math.max(maxDuration, getSequenceClipEndMinute(clip));
|
|
|
|
|
}, DEFAULT_PROJECT_SEQUENCE_DURATION_MINUTES);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function isHeldSequenceClipActiveAtMinute(
|
|
|
|
|
clip: HeldSequenceStep,
|
|
|
|
|
minute: number
|
|
|
|
|
): boolean {
|
|
|
|
|
if (!Number.isFinite(minute) || minute < 0) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return minute >= clip.startMinute && minute < getSequenceClipEndMinute(clip);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ActiveHeldSequenceClip<
|
|
|
|
|
TClip extends HeldSequenceStep = HeldSequenceStep
|
|
|
|
|
> {
|
|
|
|
|
clip: TClip;
|
|
|
|
|
elapsedMinutes: number;
|
|
|
|
|
clipIndex: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getActiveHeldSequenceClipsAtMinute(
|
|
|
|
|
sequence: SequenceDefinitionLike,
|
|
|
|
|
minute: number
|
|
|
|
|
): ActiveHeldSequenceClip[] {
|
|
|
|
|
if (!Number.isFinite(minute) || minute < 0) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const active: ActiveHeldSequenceClip[] = [];
|
|
|
|
|
|
|
|
|
|
for (const [clipIndex, clip] of sequence.clips.entries()) {
|
|
|
|
|
if (clip.stepClass !== "held" || !isHeldSequenceClipActiveAtMinute(clip, minute)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
active.push({
|
|
|
|
|
clip,
|
|
|
|
|
elapsedMinutes: minute - clip.startMinute,
|
|
|
|
|
clipIndex
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return active;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ResolvedHeldControlSequenceEffect {
|
|
|
|
|
effect: ControlEffect;
|
|
|
|
|
elapsedMinutes: number;
|
|
|
|
|
source: "inline" | "sequence";
|
|
|
|
|
clipIndex: number | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getProjectScheduleRoutineResolvedHeldControlEffectsAtMinute(
|
|
|
|
|
routine: ProjectScheduleRoutine,
|
|
|
|
|
sequenceLibrary: SequenceLibraryLike | null | undefined,
|
|
|
|
|
routineElapsedMinutes: number | null
|
|
|
|
|
): ResolvedHeldControlSequenceEffect[] {
|
|
|
|
|
const resolvedByKey = new Map<string, ResolvedHeldControlSequenceEffect>();
|
|
|
|
|
|
|
|
|
|
for (const effect of routine.effects) {
|
2026-04-15 00:47:28 +02:00
|
|
|
resolvedByKey.set(getControlEffectResolutionKey(effect), {
|
2026-04-15 00:46:06 +02:00
|
|
|
effect: cloneControlEffect(effect),
|
|
|
|
|
elapsedMinutes: Math.max(0, routineElapsedMinutes ?? 0),
|
|
|
|
|
source: "inline",
|
|
|
|
|
clipIndex: null
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (routine.sequenceId === null || routineElapsedMinutes === null) {
|
|
|
|
|
return [...resolvedByKey.values()];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sequence = sequenceLibrary?.sequences[routine.sequenceId] ?? null;
|
|
|
|
|
|
|
|
|
|
if (sequence === null) {
|
|
|
|
|
return [...resolvedByKey.values()];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const activeClip of getActiveHeldSequenceClipsAtMinute(
|
|
|
|
|
sequence,
|
|
|
|
|
routineElapsedMinutes
|
|
|
|
|
)) {
|
|
|
|
|
if (activeClip.clip.type !== "controlEffect") {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resolvedByKey.set(
|
2026-04-15 00:47:28 +02:00
|
|
|
getControlEffectResolutionKey(activeClip.clip.effect),
|
2026-04-15 00:46:06 +02:00
|
|
|
{
|
|
|
|
|
effect: cloneControlEffect(activeClip.clip.effect),
|
|
|
|
|
elapsedMinutes: activeClip.elapsedMinutes,
|
|
|
|
|
source: "sequence",
|
|
|
|
|
clipIndex: activeClip.clipIndex
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [...resolvedByKey.values()];
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 23:20:05 +02:00
|
|
|
export function getInteractionLinkImpulseSteps(
|
2026-04-14 23:33:13 +02:00
|
|
|
link: InteractionLink,
|
|
|
|
|
sequenceLibrary?: SequenceLibraryLike | null
|
2026-04-15 00:17:56 +02:00
|
|
|
): ImpulseSequenceStep[] {
|
|
|
|
|
return getInteractionLinkImpulseClips(link, sequenceLibrary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getInteractionLinkImpulseClips(
|
|
|
|
|
link: InteractionLink,
|
|
|
|
|
sequenceLibrary?: SequenceLibraryLike | null
|
2026-04-14 23:20:05 +02:00
|
|
|
): ImpulseSequenceStep[] {
|
2026-04-14 23:12:32 +02:00
|
|
|
const controlEffect = getInteractionActionControlEffect(link.action);
|
|
|
|
|
|
|
|
|
|
if (controlEffect !== null) {
|
|
|
|
|
return [
|
|
|
|
|
{
|
2026-04-15 00:46:06 +02:00
|
|
|
startMinute: 0,
|
|
|
|
|
durationMinutes: DEFAULT_IMPULSE_SEQUENCE_CLIP_DURATION_MINUTES,
|
|
|
|
|
lane: 0,
|
2026-04-14 23:20:05 +02:00
|
|
|
stepClass: "impulse",
|
2026-04-14 23:12:32 +02:00
|
|
|
type: "controlEffect",
|
|
|
|
|
effect: controlEffect
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (link.action.type) {
|
|
|
|
|
case "teleportPlayer":
|
|
|
|
|
return [
|
|
|
|
|
{
|
2026-04-15 00:46:06 +02:00
|
|
|
startMinute: 0,
|
|
|
|
|
durationMinutes: DEFAULT_IMPULSE_SEQUENCE_CLIP_DURATION_MINUTES,
|
|
|
|
|
lane: 0,
|
2026-04-14 23:20:05 +02:00
|
|
|
stepClass: "impulse",
|
2026-04-14 23:12:32 +02:00
|
|
|
type: "teleportPlayer",
|
|
|
|
|
targetEntityId: link.action.targetEntityId
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
case "toggleVisibility":
|
|
|
|
|
return [
|
|
|
|
|
{
|
2026-04-15 00:46:06 +02:00
|
|
|
startMinute: 0,
|
|
|
|
|
durationMinutes: DEFAULT_IMPULSE_SEQUENCE_CLIP_DURATION_MINUTES,
|
|
|
|
|
lane: 0,
|
2026-04-14 23:20:05 +02:00
|
|
|
stepClass: "impulse",
|
2026-04-14 23:12:32 +02:00
|
|
|
type: "toggleVisibility",
|
|
|
|
|
targetBrushId: link.action.targetBrushId,
|
|
|
|
|
visible: link.action.visible
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
case "startDialogue":
|
|
|
|
|
return [
|
|
|
|
|
{
|
2026-04-15 00:46:06 +02:00
|
|
|
startMinute: 0,
|
|
|
|
|
durationMinutes: DEFAULT_IMPULSE_SEQUENCE_CLIP_DURATION_MINUTES,
|
|
|
|
|
lane: 0,
|
2026-04-14 23:20:05 +02:00
|
|
|
stepClass: "impulse",
|
2026-04-14 23:12:32 +02:00
|
|
|
type: "startDialogue",
|
|
|
|
|
dialogueId: link.action.dialogueId
|
|
|
|
|
}
|
|
|
|
|
];
|
2026-04-14 23:33:13 +02:00
|
|
|
case "runSequence": {
|
|
|
|
|
const sequence =
|
|
|
|
|
sequenceLibrary?.sequences[link.action.sequenceId] ?? null;
|
2026-04-15 00:17:56 +02:00
|
|
|
return sequence === null ? [] : getProjectSequenceImpulseClips(sequence);
|
2026-04-14 23:33:13 +02:00
|
|
|
}
|
2026-04-14 23:12:32 +02:00
|
|
|
case "playAnimation":
|
|
|
|
|
case "stopAnimation":
|
|
|
|
|
case "playSound":
|
|
|
|
|
case "stopSound":
|
|
|
|
|
case "control":
|
|
|
|
|
throw new Error(
|
|
|
|
|
`Interaction action ${link.action.type} should have normalized to a controlEffect sequence step.`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 23:20:05 +02:00
|
|
|
export function getInteractionLinkSequenceSteps(
|
2026-04-14 23:33:13 +02:00
|
|
|
link: InteractionLink,
|
|
|
|
|
sequenceLibrary?: SequenceLibraryLike | null
|
2026-04-14 23:12:32 +02:00
|
|
|
): SequenceStep[] {
|
2026-04-15 00:17:56 +02:00
|
|
|
return getInteractionLinkSequenceClips(link, sequenceLibrary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getInteractionLinkSequenceClips(
|
|
|
|
|
link: InteractionLink,
|
|
|
|
|
sequenceLibrary?: SequenceLibraryLike | null
|
|
|
|
|
): SequenceClip[] {
|
|
|
|
|
return getInteractionLinkImpulseClips(link, sequenceLibrary);
|
2026-04-14 23:20:05 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getProjectScheduleRoutineHeldSteps(
|
2026-04-14 23:33:13 +02:00
|
|
|
routine: ProjectScheduleRoutine,
|
|
|
|
|
sequenceLibrary?: SequenceLibraryLike | null
|
2026-04-15 00:17:56 +02:00
|
|
|
): HeldSequenceStep[] {
|
|
|
|
|
return getProjectScheduleRoutineHeldClips(routine, sequenceLibrary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getProjectScheduleRoutineHeldClips(
|
|
|
|
|
routine: ProjectScheduleRoutine,
|
|
|
|
|
sequenceLibrary?: SequenceLibraryLike | null
|
2026-04-14 23:20:05 +02:00
|
|
|
): HeldSequenceStep[] {
|
2026-04-14 23:33:13 +02:00
|
|
|
if (routine.sequenceId !== null) {
|
|
|
|
|
const sequence = sequenceLibrary?.sequences[routine.sequenceId] ?? null;
|
|
|
|
|
|
|
|
|
|
if (sequence !== null) {
|
2026-04-15 00:17:56 +02:00
|
|
|
return getProjectSequenceHeldClips(sequence);
|
2026-04-14 23:33:13 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 23:12:32 +02:00
|
|
|
return routine.effects.map((effect) => ({
|
2026-04-15 00:46:06 +02:00
|
|
|
startMinute: 0,
|
|
|
|
|
durationMinutes: LEGACY_PROJECT_SEQUENCE_DURATION_MINUTES,
|
|
|
|
|
lane: 0,
|
2026-04-14 23:20:05 +02:00
|
|
|
stepClass: "held" as const,
|
2026-04-14 23:12:32 +02:00
|
|
|
type: "controlEffect" as const,
|
|
|
|
|
effect: cloneControlEffect(effect)
|
|
|
|
|
}));
|
|
|
|
|
}
|
2026-04-14 23:20:05 +02:00
|
|
|
|
|
|
|
|
export function getProjectScheduleRoutineSequenceSteps(
|
2026-04-14 23:33:13 +02:00
|
|
|
routine: ProjectScheduleRoutine,
|
|
|
|
|
sequenceLibrary?: SequenceLibraryLike | null
|
2026-04-14 23:20:05 +02:00
|
|
|
): SequenceStep[] {
|
2026-04-15 00:17:56 +02:00
|
|
|
return getProjectScheduleRoutineSequenceClips(routine, sequenceLibrary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getProjectScheduleRoutineSequenceClips(
|
|
|
|
|
routine: ProjectScheduleRoutine,
|
|
|
|
|
sequenceLibrary?: SequenceLibraryLike | null
|
|
|
|
|
): SequenceClip[] {
|
|
|
|
|
return getProjectScheduleRoutineHeldClips(routine, sequenceLibrary);
|
2026-04-14 23:20:05 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getHeldSequenceControlEffects(
|
|
|
|
|
steps: readonly HeldSequenceStep[]
|
|
|
|
|
): ControlEffect[] {
|
|
|
|
|
return steps
|
|
|
|
|
.filter((step): step is HeldControlSequenceStep => step.type === "controlEffect")
|
|
|
|
|
.map((step) => cloneControlEffect(step.effect));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function findHeldSequenceControlEffect<
|
|
|
|
|
TType extends ControlEffect["type"]
|
|
|
|
|
>(
|
|
|
|
|
steps: readonly HeldSequenceStep[],
|
|
|
|
|
type: TType
|
|
|
|
|
): Extract<ControlEffect, { type: TType }> | null {
|
|
|
|
|
return (
|
|
|
|
|
getHeldSequenceControlEffects(steps).find(
|
|
|
|
|
(effect): effect is Extract<ControlEffect, { type: TType }> =>
|
|
|
|
|
effect.type === type
|
|
|
|
|
) ?? null
|
|
|
|
|
);
|
|
|
|
|
}
|