Update project scheduler to handle multiple effects and improve effect normalization logic

This commit is contained in:
2026-04-14 13:41:01 +02:00
parent a54cadc073
commit bdfbdd329a

View File

@@ -3,6 +3,7 @@ import {
areControlEffectsEqual, areControlEffectsEqual,
cloneControlEffect, cloneControlEffect,
cloneControlTargetRef, cloneControlTargetRef,
getControlEffectResolutionKey,
type ControlEffect, type ControlEffect,
type ControlTargetRef, type ControlTargetRef,
createActorControlTargetRef, createActorControlTargetRef,
@@ -49,7 +50,7 @@ export interface ProjectScheduleRoutine {
startHour: number; startHour: number;
endHour: number; endHour: number;
priority: number; priority: number;
effect: ControlEffect; effects: ControlEffect[];
} }
export interface ProjectScheduler { export interface ProjectScheduler {
@@ -123,30 +124,98 @@ function normalizeProjectScheduleHours(value: number, label: string): number {
return normalizeTimeOfDayHours(value); return normalizeTimeOfDayHours(value);
} }
function normalizeProjectScheduleEffectTarget( function compareProjectScheduleEffectOrder(
target: ControlTargetRef, left: ControlEffect,
effect: ControlEffect | undefined right: ControlEffect
): ControlEffect { ): number {
if (effect === undefined) { const getOrder = (effect: ControlEffect): number => {
if (target.kind !== "actor") { switch (effect.type) {
throw new Error( case "setActorPresence":
"Project schedule routines must author an explicit control effect for non-actor targets." return 0;
); case "playActorAnimation":
return 1;
case "followActorPath":
return 2;
default:
return 10;
} }
};
return createSetActorPresenceControlEffect({ return (
target, getOrder(left) - getOrder(right) ||
active: true getControlEffectResolutionKey(left).localeCompare(
}); getControlEffectResolutionKey(right)
)
);
}
function normalizeProjectScheduleEffects(
target: ControlTargetRef,
options: {
effect?: ControlEffect;
effects?: ControlEffect[];
} }
): ControlEffect[] {
const authoredEffects =
options.effects !== undefined
? options.effects
: options.effect === undefined
? []
: [options.effect];
const normalizedEffects = authoredEffects.map(cloneControlEffect);
if (getControlTargetRefKey(effect.target) !== getControlTargetRefKey(target)) { if (normalizedEffects.length === 0 && target.kind !== "actor") {
throw new Error( throw new Error(
"Project schedule routine effects must target the same authored control target." "Project schedule routines must author an explicit control effect for non-actor targets."
); );
} }
return cloneControlEffect(effect); for (const effect of normalizedEffects) {
if (getControlTargetRefKey(effect.target) !== getControlTargetRefKey(target)) {
throw new Error(
"Project schedule routine effects must target the same authored control target."
);
}
}
if (
target.kind === "actor" &&
!normalizedEffects.some((effect) => effect.type === "setActorPresence")
) {
normalizedEffects.push(
createSetActorPresenceControlEffect({
target,
active: true
})
);
}
if (normalizedEffects.length === 0) {
throw new Error("Project schedule routines must contain at least one control effect.");
}
if (target.kind !== "actor" && normalizedEffects.length !== 1) {
throw new Error(
"Non-actor project schedule routines must currently author exactly one control effect."
);
}
const seenResolutionKeys = new Set<string>();
for (const effect of normalizedEffects) {
const resolutionKey = getControlEffectResolutionKey(effect);
if (seenResolutionKeys.has(resolutionKey)) {
throw new Error(
`Project schedule routines cannot author multiple effects for ${resolutionKey}.`
);
}
seenResolutionKeys.add(resolutionKey);
}
normalizedEffects.sort(compareProjectScheduleEffectOrder);
return normalizedEffects;
} }
export function isProjectScheduleWeekday( export function isProjectScheduleWeekday(
@@ -180,9 +249,11 @@ export function createProjectScheduleRoutine(
overrides: Partial< overrides: Partial<
Pick< Pick<
ProjectScheduleRoutine, ProjectScheduleRoutine,
"id" | "title" | "enabled" | "target" | "days" | "startHour" | "endHour" | "priority" | "effect" "id" | "title" | "enabled" | "target" | "days" | "startHour" | "endHour" | "priority" | "effects"
> >
> & > & {
effect?: ControlEffect;
} &
Pick<ProjectScheduleRoutine, "target" | "title"> = { Pick<ProjectScheduleRoutine, "target" | "title"> = {
target: createActorControlTargetRef("actor-default"), target: createActorControlTargetRef("actor-default"),
title: "Routine" title: "Routine"
@@ -211,7 +282,10 @@ export function createProjectScheduleRoutine(
startHour, startHour,
endHour, endHour,
priority: normalizeProjectSchedulePriority(overrides.priority), priority: normalizeProjectSchedulePriority(overrides.priority),
effect: normalizeProjectScheduleEffectTarget(target, overrides.effect) effects: normalizeProjectScheduleEffects(target, {
effect: overrides.effect,
effects: overrides.effects
})
}; };
} }
@@ -238,7 +312,7 @@ export function cloneProjectScheduleRoutine(
startHour: routine.startHour, startHour: routine.startHour,
endHour: routine.endHour, endHour: routine.endHour,
priority: routine.priority, priority: routine.priority,
effect: cloneControlEffect(routine.effect) effects: routine.effects.map(cloneControlEffect)
}; };
} }
@@ -287,7 +361,24 @@ export function areProjectScheduleRoutinesEqual(
left.startHour === right.startHour && left.startHour === right.startHour &&
left.endHour === right.endHour && left.endHour === right.endHour &&
left.priority === right.priority && left.priority === right.priority &&
areControlEffectsEqual(left.effect, right.effect) left.effects.length === right.effects.length &&
left.effects.every((effect, index) =>
areControlEffectsEqual(effect, right.effects[index] as ControlEffect)
)
);
}
export function findProjectScheduleRoutineEffect<
TType extends ControlEffect["type"]
>(
routine: ProjectScheduleRoutine,
type: TType
): Extract<ControlEffect, { type: TType }> | null {
return (
routine.effects.find(
(effect): effect is Extract<ControlEffect, { type: TType }> =>
effect.type === type
) ?? null
); );
} }
@@ -381,6 +472,40 @@ export function isProjectScheduleRoutineActiveAt(
); );
} }
export function getProjectScheduleRoutineDurationHours(
routine: Pick<ProjectScheduleRoutine, "startHour" | "endHour">
): number {
return routine.startHour < routine.endHour
? routine.endHour - routine.startHour
: HOURS_PER_DAY - routine.startHour + routine.endHour;
}
export function getProjectScheduleRoutineElapsedHoursAt(
routine: ProjectScheduleRoutine,
dayNumber: number,
timeOfDayHours: number
): number | null {
if (!isProjectScheduleRoutineActiveAt(routine, dayNumber, timeOfDayHours)) {
return null;
}
const normalizedTimeOfDayHours = normalizeTimeOfDayHours(timeOfDayHours);
const weekday = resolveProjectScheduleWeekday(dayNumber);
if (routine.startHour < routine.endHour) {
return normalizedTimeOfDayHours - routine.startHour;
}
if (
isProjectScheduleDaySelectionActive(routine.days, weekday) &&
normalizedTimeOfDayHours >= routine.startHour
) {
return normalizedTimeOfDayHours - routine.startHour;
}
return HOURS_PER_DAY - routine.startHour + normalizedTimeOfDayHours;
}
export function compareProjectScheduleRoutinePriority( export function compareProjectScheduleRoutinePriority(
left: ProjectScheduleRoutine, left: ProjectScheduleRoutine,
right: ProjectScheduleRoutine right: ProjectScheduleRoutine