Add project sequence steps and update related files

This commit is contained in:
2026-04-15 01:08:53 +02:00
parent 4ba26dcd2a
commit cfcb131b56
4 changed files with 410 additions and 79 deletions

View File

@@ -32,7 +32,8 @@ import {
type ProjectSequenceLibrary
} from "../sequencer/project-sequences";
export const SCENE_DOCUMENT_VERSION = 55 as const;
export const SCENE_DOCUMENT_VERSION = 56 as const;
export const PROJECT_SEQUENCE_EFFECTS_SCENE_DOCUMENT_VERSION = 56 as const;
export const PROJECT_SEQUENCE_TIMING_SCENE_DOCUMENT_VERSION = 55 as const;
export const PROJECT_SEQUENCE_CLIPS_SCENE_DOCUMENT_VERSION = 54 as const;
export const PROJECT_SEQUENCE_LIBRARY_SCENE_DOCUMENT_VERSION = 53 as const;

View File

@@ -28,8 +28,8 @@ import {
} from "../sequencer/project-sequencer";
import {
findHeldSequenceControlEffect,
getProjectScheduleRoutineHeldSteps,
getProjectScheduleRoutineResolvedHeldControlEffectsAtMinute
getHeldSequenceControlEffects,
getProjectScheduleRoutineHeldSteps
} from "../sequencer/project-sequence-steps";
import type { ProjectSequenceLibrary } from "../sequencer/project-sequences";
@@ -431,42 +431,23 @@ export function resolveRuntimeActorScheduleState(options: {
activeRoutine,
options.sequences
);
const elapsedHours = getProjectScheduleRoutineElapsedHoursAt(
activeRoutine,
options.dayNumber,
options.timeOfDayHours
);
const resolvedHeldEffects =
elapsedHours === null
? []
: getProjectScheduleRoutineResolvedHeldControlEffectsAtMinute(
activeRoutine,
options.sequences,
elapsedHours * 60
);
const presenceEffect =
(resolvedHeldEffects.find(
(entry): entry is typeof entry & { effect: SetActorPresenceControlEffect } =>
entry.effect.type === "setActorPresence"
)?.effect ??
findHeldSequenceControlEffect(heldSteps, "setActorPresence")) ??
findHeldSequenceControlEffect(heldSteps, "setActorPresence") ??
createSetActorPresenceControlEffect({
target: createActorControlTargetRef(options.actorId),
active: true
});
const animationEffect =
resolvedHeldEffects.find(
(entry): entry is typeof entry & { effect: PlayActorAnimationControlEffect } =>
entry.effect.type === "playActorAnimation"
)?.effect ?? findHeldSequenceControlEffect(heldSteps, "playActorAnimation");
const pathEffectEntry = resolvedHeldEffects.find(
(entry): entry is typeof entry & { effect: FollowActorPathControlEffect } =>
entry.effect.type === "followActorPath"
findHeldSequenceControlEffect(heldSteps, "playActorAnimation");
const pathEffect = findHeldSequenceControlEffect(
heldSteps,
"followActorPath"
);
const elapsedHours = getProjectScheduleRoutineElapsedHoursAt(
activeRoutine,
options.dayNumber,
options.timeOfDayHours
);
const pathEffect =
pathEffectEntry?.effect ??
findHeldSequenceControlEffect(heldSteps, "followActorPath");
return {
actorId: options.actorId,
@@ -483,10 +464,7 @@ export function resolveRuntimeActorScheduleState(options: {
? null
: resolveActorSchedulePathState({
effect: pathEffect,
elapsedHours:
pathEffectEntry === undefined
? elapsedHours
: pathEffectEntry.elapsedMinutes / 60,
elapsedHours,
path: options.pathsById?.get(pathEffect.pathId) ?? null
})
};
@@ -540,18 +518,9 @@ function resolveRuntimeScheduledControlRoutines(options: {
continue;
}
const elapsedHours = getProjectScheduleRoutineElapsedHoursAt(
routine,
options.dayNumber,
options.timeOfDayHours
);
const effects = getProjectScheduleRoutineResolvedHeldControlEffectsAtMinute(
routine,
options.sequences,
elapsedHours === null ? null : elapsedHours * 60
).map((entry) => cloneControlEffect(entry.effect));
for (const effect of effects) {
for (const effect of getHeldSequenceControlEffects(
getProjectScheduleRoutineHeldSteps(routine, options.sequences)
)) {
const resolutionKey = getControlEffectResolutionKey(effect);
if (seenResolutionKeys.has(resolutionKey)) {

View File

@@ -0,0 +1,378 @@
import {
cloneControlEffect,
getControlEffectLabel,
type ControlEffect
} from "../controls/control-surface";
import {
getInteractionActionControlEffect,
type InteractionLink
} from "../interactions/interaction-links";
import type { ProjectScheduleRoutine } from "../scheduler/project-scheduler";
export interface HeldControlSequenceEffect {
stepClass: "held";
type: "controlEffect";
effect: ControlEffect;
}
export interface ImpulseControlSequenceEffect {
stepClass: "impulse";
type: "controlEffect";
effect: ControlEffect;
}
export interface StartDialogueSequenceEffect {
stepClass: "impulse";
type: "startDialogue";
dialogueId: string;
}
export interface TeleportPlayerSequenceEffect {
stepClass: "impulse";
type: "teleportPlayer";
targetEntityId: string;
}
export interface ToggleVisibilitySequenceEffect {
stepClass: "impulse";
type: "toggleVisibility";
targetBrushId: string;
visible?: boolean;
}
type SequenceDefinitionLike = {
id: string;
effects: SequenceEffect[];
};
type SequenceLibraryLike = {
sequences: Record<string, SequenceDefinitionLike>;
};
export type HeldSequenceStep = HeldControlSequenceEffect;
export type ImpulseSequenceStep =
| ImpulseControlSequenceEffect
| StartDialogueSequenceEffect
| TeleportPlayerSequenceEffect
| ToggleVisibilitySequenceEffect;
export type SequenceEffect = HeldSequenceStep | ImpulseSequenceStep;
export type SequenceClip = SequenceEffect;
export type SequenceStep = SequenceEffect;
export function cloneSequenceEffect(effect: SequenceEffect): SequenceEffect {
switch (effect.type) {
case "controlEffect":
return {
stepClass: effect.stepClass,
type: "controlEffect",
effect: cloneControlEffect(effect.effect)
};
case "startDialogue":
return {
stepClass: "impulse",
type: "startDialogue",
dialogueId: effect.dialogueId
};
case "teleportPlayer":
return {
stepClass: "impulse",
type: "teleportPlayer",
targetEntityId: effect.targetEntityId
};
case "toggleVisibility":
return {
stepClass: "impulse",
type: "toggleVisibility",
targetBrushId: effect.targetBrushId,
visible: effect.visible
};
}
}
export function cloneSequenceClip(clip: SequenceClip): SequenceClip {
return cloneSequenceEffect(clip);
}
export function cloneSequenceStep(step: SequenceStep): SequenceStep {
return cloneSequenceEffect(step);
}
export function cloneSequenceEffects(effects: SequenceEffect[]): SequenceEffect[] {
return effects.map(cloneSequenceEffect);
}
export function cloneSequenceClips(clips: SequenceClip[]): SequenceClip[] {
return cloneSequenceEffects(clips);
}
export function cloneSequenceSteps(steps: SequenceStep[]): SequenceStep[] {
return cloneSequenceEffects(steps);
}
export function getSequenceEffectLabel(effect: SequenceEffect): string {
switch (effect.type) {
case "controlEffect":
return `${effect.stepClass === "held" ? "Held" : "Impulse"}: ${getControlEffectLabel(effect.effect)}`;
case "startDialogue":
return "Impulse: Start Dialogue";
case "teleportPlayer":
return "Impulse: Teleport Player";
case "toggleVisibility":
return "Impulse: Toggle Visibility";
}
}
export function getSequenceClipLabel(clip: SequenceClip): string {
return getSequenceEffectLabel(clip);
}
export function getSequenceStepLabel(step: SequenceStep): string {
return getSequenceEffectLabel(step);
}
export function getHeldSequenceEffects(
effects: readonly SequenceEffect[]
): HeldSequenceStep[] {
return effects
.filter((effect): effect is HeldSequenceStep => effect.stepClass === "held")
.map(cloneSequenceEffect) as HeldSequenceStep[];
}
export function getHeldSequenceClips(
clips: readonly SequenceClip[]
): HeldSequenceStep[] {
return getHeldSequenceEffects(clips);
}
export function getHeldSequenceSteps(
steps: readonly SequenceStep[]
): HeldSequenceStep[] {
return getHeldSequenceEffects(steps);
}
export function getImpulseSequenceEffects(
effects: readonly SequenceEffect[]
): ImpulseSequenceStep[] {
return effects
.filter((effect): effect is ImpulseSequenceStep => effect.stepClass === "impulse")
.map(cloneSequenceEffect) as ImpulseSequenceStep[];
}
export function getImpulseSequenceClips(
clips: readonly SequenceClip[]
): ImpulseSequenceStep[] {
return getImpulseSequenceEffects(clips);
}
export function getImpulseSequenceSteps(
steps: readonly SequenceStep[]
): ImpulseSequenceStep[] {
return getImpulseSequenceEffects(steps);
}
export function getProjectSequenceHeldEffects(
sequence: SequenceDefinitionLike
): HeldSequenceStep[] {
return getHeldSequenceEffects(sequence.effects);
}
export function getProjectSequenceHeldClips(
sequence: SequenceDefinitionLike
): HeldSequenceStep[] {
return getProjectSequenceHeldEffects(sequence);
}
export function getProjectSequenceHeldSteps(
sequence: SequenceDefinitionLike
): HeldSequenceStep[] {
return getProjectSequenceHeldEffects(sequence);
}
export function getProjectSequenceImpulseEffects(
sequence: SequenceDefinitionLike
): ImpulseSequenceStep[] {
return getImpulseSequenceEffects(sequence.effects);
}
export function getProjectSequenceImpulseClips(
sequence: SequenceDefinitionLike
): ImpulseSequenceStep[] {
return getProjectSequenceImpulseEffects(sequence);
}
export function getProjectSequenceImpulseSteps(
sequence: SequenceDefinitionLike
): ImpulseSequenceStep[] {
return getProjectSequenceImpulseEffects(sequence);
}
export function getInteractionLinkImpulseEffects(
link: InteractionLink,
sequenceLibrary?: SequenceLibraryLike | null
): ImpulseSequenceStep[] {
const controlEffect = getInteractionActionControlEffect(link.action);
if (controlEffect !== null) {
return [
{
stepClass: "impulse",
type: "controlEffect",
effect: controlEffect
}
];
}
switch (link.action.type) {
case "teleportPlayer":
return [
{
stepClass: "impulse",
type: "teleportPlayer",
targetEntityId: link.action.targetEntityId
}
];
case "toggleVisibility":
return [
{
stepClass: "impulse",
type: "toggleVisibility",
targetBrushId: link.action.targetBrushId,
visible: link.action.visible
}
];
case "startDialogue":
return [
{
stepClass: "impulse",
type: "startDialogue",
dialogueId: link.action.dialogueId
}
];
case "runSequence": {
const sequence =
sequenceLibrary?.sequences[link.action.sequenceId] ?? null;
return sequence === null ? [] : getProjectSequenceImpulseEffects(sequence);
}
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 effect.`
);
}
}
export function getInteractionLinkImpulseClips(
link: InteractionLink,
sequenceLibrary?: SequenceLibraryLike | null
): ImpulseSequenceStep[] {
return getInteractionLinkImpulseEffects(link, sequenceLibrary);
}
export function getInteractionLinkImpulseSteps(
link: InteractionLink,
sequenceLibrary?: SequenceLibraryLike | null
): ImpulseSequenceStep[] {
return getInteractionLinkImpulseEffects(link, sequenceLibrary);
}
export function getInteractionLinkSequenceEffects(
link: InteractionLink,
sequenceLibrary?: SequenceLibraryLike | null
): SequenceEffect[] {
return getInteractionLinkImpulseEffects(link, sequenceLibrary);
}
export function getInteractionLinkSequenceClips(
link: InteractionLink,
sequenceLibrary?: SequenceLibraryLike | null
): SequenceClip[] {
return getInteractionLinkSequenceEffects(link, sequenceLibrary);
}
export function getInteractionLinkSequenceSteps(
link: InteractionLink,
sequenceLibrary?: SequenceLibraryLike | null
): SequenceStep[] {
return getInteractionLinkSequenceEffects(link, sequenceLibrary);
}
export function getProjectScheduleRoutineHeldEffects(
routine: ProjectScheduleRoutine,
sequenceLibrary?: SequenceLibraryLike | null
): HeldSequenceStep[] {
if (routine.sequenceId !== null) {
const sequence = sequenceLibrary?.sequences[routine.sequenceId] ?? null;
if (sequence !== null) {
return getProjectSequenceHeldEffects(sequence);
}
}
return routine.effects.map((effect) => ({
stepClass: "held" as const,
type: "controlEffect" as const,
effect: cloneControlEffect(effect)
}));
}
export function getProjectScheduleRoutineHeldClips(
routine: ProjectScheduleRoutine,
sequenceLibrary?: SequenceLibraryLike | null
): HeldSequenceStep[] {
return getProjectScheduleRoutineHeldEffects(routine, sequenceLibrary);
}
export function getProjectScheduleRoutineHeldSteps(
routine: ProjectScheduleRoutine,
sequenceLibrary?: SequenceLibraryLike | null
): HeldSequenceStep[] {
return getProjectScheduleRoutineHeldEffects(routine, sequenceLibrary);
}
export function getProjectScheduleRoutineSequenceEffects(
routine: ProjectScheduleRoutine,
sequenceLibrary?: SequenceLibraryLike | null
): SequenceEffect[] {
return getProjectScheduleRoutineHeldEffects(routine, sequenceLibrary);
}
export function getProjectScheduleRoutineSequenceClips(
routine: ProjectScheduleRoutine,
sequenceLibrary?: SequenceLibraryLike | null
): SequenceClip[] {
return getProjectScheduleRoutineSequenceEffects(routine, sequenceLibrary);
}
export function getProjectScheduleRoutineSequenceSteps(
routine: ProjectScheduleRoutine,
sequenceLibrary?: SequenceLibraryLike | null
): SequenceStep[] {
return getProjectScheduleRoutineSequenceEffects(routine, sequenceLibrary);
}
export function getHeldSequenceControlEffects(
steps: readonly HeldSequenceStep[]
): ControlEffect[] {
return steps
.filter((step): step is HeldControlSequenceEffect => 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
);
}

View File

@@ -1,18 +1,16 @@
import { createOpaqueId } from "../core/ids";
import {
cloneSequenceClip,
DEFAULT_PROJECT_SEQUENCE_DURATION_MINUTES,
getProjectSequenceDurationMinutes,
cloneSequenceEffect,
type SequenceClip,
type SequenceEffect,
type SequenceStep
} from "./project-sequence-steps";
export interface ProjectSequence {
id: string;
title: string;
durationMinutes: number;
clips: SequenceClip[];
effects: SequenceEffect[];
}
export interface ProjectSequenceLibrary {
@@ -29,16 +27,6 @@ function normalizeProjectSequenceTitle(title: string | undefined): string {
return normalizedTitle;
}
function normalizeProjectSequenceDurationMinutes(
value: number | undefined
): number {
if (value === undefined || !Number.isFinite(value)) {
return DEFAULT_PROJECT_SEQUENCE_DURATION_MINUTES;
}
return Math.max(1, Math.trunc(value));
}
export function createEmptyProjectSequenceLibrary(): ProjectSequenceLibrary {
return {
sequences: {}
@@ -46,21 +34,18 @@ export function createEmptyProjectSequenceLibrary(): ProjectSequenceLibrary {
}
export function createProjectSequence(
overrides: Partial<Pick<ProjectSequence, "id" | "title" | "durationMinutes">> & {
overrides: Partial<Pick<ProjectSequence, "id" | "title">> & {
effects?: SequenceEffect[];
clips?: SequenceClip[];
steps?: SequenceStep[];
} = {}
): ProjectSequence {
const clips = (overrides.clips ?? overrides.steps)?.map(cloneSequenceClip) ?? [];
return {
id: overrides.id ?? createOpaqueId("sequence"),
title: normalizeProjectSequenceTitle(overrides.title ?? "Sequence"),
durationMinutes: normalizeProjectSequenceDurationMinutes(
overrides.durationMinutes ??
(clips.length > 0 ? getProjectSequenceDurationMinutes({ id: "sequence", clips }) : undefined)
),
clips
effects: (overrides.effects ?? overrides.clips ?? overrides.steps)?.map(
cloneSequenceEffect
) ?? []
};
}
@@ -70,8 +55,7 @@ export function cloneProjectSequence(
return {
id: sequence.id,
title: sequence.title,
durationMinutes: sequence.durationMinutes,
clips: sequence.clips.map(cloneSequenceClip)
effects: sequence.effects.map(cloneSequenceEffect)
};
}
@@ -95,20 +79,19 @@ export function areProjectSequencesEqual(
if (
left.id !== right.id ||
left.title !== right.title ||
left.durationMinutes !== right.durationMinutes ||
left.clips.length !== right.clips.length
left.effects.length !== right.effects.length
) {
return false;
}
return left.clips.every((clip, index) => {
const rightClip = right.clips[index];
return left.effects.every((effect, index) => {
const rightEffect = right.effects[index];
if (rightClip === undefined) {
if (rightEffect === undefined) {
return false;
}
return JSON.stringify(clip) === JSON.stringify(rightClip);
return JSON.stringify(effect) === JSON.stringify(rightEffect);
});
}