Add RunSequenceAction and related functionality

This commit is contained in:
2026-04-14 23:33:13 +02:00
parent 328018154c
commit 5cf32b88d1
3 changed files with 146 additions and 7 deletions

View File

@@ -53,6 +53,11 @@ export interface StartDialogueAction {
dialogueId: string;
}
export interface RunSequenceAction {
type: "runSequence";
sequenceId: string;
}
export interface ControlInteractionAction {
type: "control";
effect: ControlEffect;
@@ -66,6 +71,7 @@ export type InteractionAction =
| PlaySoundAction
| StopSoundAction
| StartDialogueAction
| RunSequenceAction
| ControlInteractionAction;
export interface InteractionLink {
@@ -134,6 +140,13 @@ export interface CreateControlInteractionLinkOptions {
effect: ControlEffect;
}
export interface CreateRunSequenceInteractionLinkOptions {
id?: string;
sourceEntityId: string;
trigger?: InteractionTriggerKind;
sequenceId: string;
}
function assertNonEmptyString(value: string, label: string) {
if (value.trim().length === 0) {
throw new Error(`${label} must be non-empty.`);
@@ -180,6 +193,11 @@ function cloneAction(action: InteractionAction): InteractionAction {
type: "startDialogue",
dialogueId: action.dialogueId
};
case "runSequence":
return {
type: "runSequence",
sequenceId: action.sequenceId
};
case "control":
return {
type: "control",
@@ -357,6 +375,23 @@ export function createControlInteractionLink(
};
}
export function createRunSequenceInteractionLink(
options: CreateRunSequenceInteractionLinkOptions
): InteractionLink {
assertNonEmptyString(options.sourceEntityId, "Interaction source entity id");
assertNonEmptyString(options.sequenceId, "Sequence id");
return {
id: options.id ?? createOpaqueId("interaction-link"),
sourceEntityId: options.sourceEntityId,
trigger: options.trigger ?? "enter",
action: {
type: "runSequence",
sequenceId: options.sequenceId
}
};
}
export function getInteractionActionControlEffect(
action: InteractionAction
): ControlEffect | null {
@@ -384,6 +419,7 @@ export function getInteractionActionControlEffect(
target: createSoundEmitterControlTargetRef(action.targetSoundEmitterId)
});
case "startDialogue":
case "runSequence":
return null;
case "control":
return cloneControlEffect(action.effect);
@@ -409,6 +445,8 @@ export function getInteractionActionLabel(action: InteractionAction): string {
return "Stop Sound";
case "startDialogue":
return "Start Dialogue";
case "runSequence":
return "Run Sequence";
case "control":
return getControlEffectLabel(action.effect);
}
@@ -479,6 +517,10 @@ export function areInteractionLinksEqual(
left.action.dialogueId ===
(right.action as StartDialogueAction).dialogueId
);
case "runSequence":
return (
left.action.sequenceId === (right.action as RunSequenceAction).sequenceId
);
case "control":
return areControlEffectsEqual(
left.action.effect,

View File

@@ -46,6 +46,7 @@ export interface ProjectScheduleRoutine {
title: string;
enabled: boolean;
target: ControlTargetRef;
sequenceId: string | null;
days: ProjectScheduleDaySelection;
startHour: number;
endHour: number;
@@ -119,6 +120,22 @@ function normalizeProjectScheduleTitle(title: string | undefined): string {
return normalizedTitle;
}
function normalizeProjectScheduleSequenceId(
sequenceId: string | null | undefined
): string | null {
if (sequenceId === undefined || sequenceId === null) {
return null;
}
const normalizedSequenceId = sequenceId.trim();
if (normalizedSequenceId.length === 0) {
throw new Error("Project schedule routine sequence ids must be non-empty.");
}
return normalizedSequenceId;
}
function normalizeProjectScheduleHours(value: number, label: string): number {
assertFiniteNumber(value, label);
return normalizeTimeOfDayHours(value);
@@ -249,7 +266,16 @@ export function createProjectScheduleRoutine(
overrides: Partial<
Pick<
ProjectScheduleRoutine,
"id" | "title" | "enabled" | "target" | "days" | "startHour" | "endHour" | "priority" | "effects"
| "id"
| "title"
| "enabled"
| "target"
| "sequenceId"
| "days"
| "startHour"
| "endHour"
| "priority"
| "effects"
>
> & {
effect?: ControlEffect;
@@ -278,6 +304,7 @@ export function createProjectScheduleRoutine(
title: normalizeProjectScheduleTitle(overrides.title),
enabled: overrides.enabled ?? true,
target,
sequenceId: normalizeProjectScheduleSequenceId(overrides.sequenceId),
days: normalizeProjectScheduleDays(overrides.days),
startHour,
endHour,
@@ -308,6 +335,7 @@ export function cloneProjectScheduleRoutine(
title: routine.title,
enabled: routine.enabled,
target: cloneControlTargetRef(routine.target),
sequenceId: routine.sequenceId,
days: cloneProjectScheduleDaySelection(routine.days),
startHour: routine.startHour,
endHour: routine.endHour,
@@ -357,6 +385,7 @@ export function areProjectScheduleRoutinesEqual(
left.title === right.title &&
left.enabled === right.enabled &&
getControlTargetRefKey(left.target) === getControlTargetRefKey(right.target) &&
left.sequenceId === right.sequenceId &&
areProjectScheduleDaySelectionsEqual(left.days, right.days) &&
left.startHour === right.startHour &&
left.endHour === right.endHour &&

View File

@@ -1,4 +1,5 @@
import {
getControlEffectLabel,
cloneControlEffect,
type ControlEffect
} from "../controls/control-surface";
@@ -39,6 +40,15 @@ export interface ToggleVisibilitySequenceStep {
visible?: boolean;
}
type SequenceDefinitionLike = {
id: string;
steps: SequenceStep[];
};
type SequenceLibraryLike = {
sequences: Record<string, SequenceDefinitionLike>;
};
export type HeldSequenceStep = HeldControlSequenceStep;
export type ImpulseSequenceStep =
@@ -83,8 +93,50 @@ export function cloneSequenceSteps(steps: SequenceStep[]): SequenceStep[] {
return steps.map(cloneSequenceStep);
}
export function getSequenceStepLabel(step: SequenceStep): string {
switch (step.type) {
case "controlEffect":
return `${step.stepClass === "held" ? "Held" : "Impulse"}: ${getControlEffectLabel(step.effect)}`;
case "startDialogue":
return "Impulse: Start Dialogue";
case "teleportPlayer":
return "Impulse: Teleport Player";
case "toggleVisibility":
return "Impulse: Toggle Visibility";
}
}
export function getHeldSequenceSteps(
steps: readonly SequenceStep[]
): HeldSequenceStep[] {
return steps
.filter((step): step is HeldSequenceStep => step.stepClass === "held")
.map(cloneSequenceStep) as HeldSequenceStep[];
}
export function getImpulseSequenceSteps(
steps: readonly SequenceStep[]
): ImpulseSequenceStep[] {
return steps
.filter((step): step is ImpulseSequenceStep => step.stepClass === "impulse")
.map(cloneSequenceStep) as ImpulseSequenceStep[];
}
export function getProjectSequenceHeldSteps(
sequence: SequenceDefinitionLike
): HeldSequenceStep[] {
return getHeldSequenceSteps(sequence.steps);
}
export function getProjectSequenceImpulseSteps(
sequence: SequenceDefinitionLike
): ImpulseSequenceStep[] {
return getImpulseSequenceSteps(sequence.steps);
}
export function getInteractionLinkImpulseSteps(
link: InteractionLink
link: InteractionLink,
sequenceLibrary?: SequenceLibraryLike | null
): ImpulseSequenceStep[] {
const controlEffect = getInteractionActionControlEffect(link.action);
@@ -124,6 +176,11 @@ export function getInteractionLinkImpulseSteps(
dialogueId: link.action.dialogueId
}
];
case "runSequence": {
const sequence =
sequenceLibrary?.sequences[link.action.sequenceId] ?? null;
return sequence === null ? [] : getProjectSequenceImpulseSteps(sequence);
}
case "playAnimation":
case "stopAnimation":
case "playSound":
@@ -136,14 +193,24 @@ export function getInteractionLinkImpulseSteps(
}
export function getInteractionLinkSequenceSteps(
link: InteractionLink
link: InteractionLink,
sequenceLibrary?: SequenceLibraryLike | null
): SequenceStep[] {
return getInteractionLinkImpulseSteps(link);
return getInteractionLinkImpulseSteps(link, sequenceLibrary);
}
export function getProjectScheduleRoutineHeldSteps(
routine: ProjectScheduleRoutine
routine: ProjectScheduleRoutine,
sequenceLibrary?: SequenceLibraryLike | null
): HeldSequenceStep[] {
if (routine.sequenceId !== null) {
const sequence = sequenceLibrary?.sequences[routine.sequenceId] ?? null;
if (sequence !== null) {
return getProjectSequenceHeldSteps(sequence);
}
}
return routine.effects.map((effect) => ({
stepClass: "held" as const,
type: "controlEffect" as const,
@@ -152,9 +219,10 @@ export function getProjectScheduleRoutineHeldSteps(
}
export function getProjectScheduleRoutineSequenceSteps(
routine: ProjectScheduleRoutine
routine: ProjectScheduleRoutine,
sequenceLibrary?: SequenceLibraryLike | null
): SequenceStep[] {
return getProjectScheduleRoutineHeldSteps(routine);
return getProjectScheduleRoutineHeldSteps(routine, sequenceLibrary);
}
export function getHeldSequenceControlEffects(