Implement core logic for synchronizing scene schedules with runtime clock state
This commit is contained in:
301
src/runtime-three/runtime-schedule-sync.ts
Normal file
301
src/runtime-three/runtime-schedule-sync.ts
Normal file
@@ -0,0 +1,301 @@
|
||||
import type { RuntimeClockState } from "./runtime-project-time";
|
||||
import {
|
||||
applyRuntimeProjectScheduleToControlState,
|
||||
resolveRuntimeProjectScheduleState,
|
||||
type RuntimeResolvedActorScheduleState,
|
||||
type RuntimeResolvedProjectScheduleState
|
||||
} from "./runtime-project-scheduler";
|
||||
import {
|
||||
applyActorScheduleStateToNpcDefinition,
|
||||
buildRuntimeNpcCollider,
|
||||
createRuntimeNpcFromDefinition,
|
||||
type RuntimeNpcDefinition,
|
||||
type RuntimeSceneDefinition
|
||||
} from "./runtime-scene-build";
|
||||
|
||||
export interface RuntimeScheduleSyncContext {
|
||||
readonly runtimeScene: RuntimeSceneDefinition;
|
||||
readonly pathsById: ReadonlyMap<string, RuntimeSceneDefinition["paths"][number]>;
|
||||
readonly actorIds: readonly string[];
|
||||
readonly actorStatesByActorId: ReadonlyMap<
|
||||
string,
|
||||
RuntimeResolvedActorScheduleState
|
||||
>;
|
||||
}
|
||||
|
||||
export interface RuntimeScheduleNpcChange {
|
||||
npc: RuntimeNpcDefinition;
|
||||
previousActive: boolean;
|
||||
previousRoutineId: string | null;
|
||||
previousRoutineTitle: string | null;
|
||||
previousAnimationClipName: string | null | undefined;
|
||||
previousAnimationLoop: boolean | undefined;
|
||||
previousYawDegrees: number;
|
||||
previousPosition: {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
};
|
||||
previousPathId: string | null;
|
||||
previousPathProgress: number | null;
|
||||
activeChanged: boolean;
|
||||
animationChanged: boolean;
|
||||
transformChanged: boolean;
|
||||
runtimeNpcChanged: boolean;
|
||||
colliderChanged: boolean;
|
||||
}
|
||||
|
||||
export interface RuntimeScheduleSyncResult {
|
||||
resolvedScheduler: RuntimeResolvedProjectScheduleState;
|
||||
resolvedControl: RuntimeSceneDefinition["control"]["resolved"];
|
||||
actorStatesByActorId: ReadonlyMap<string, RuntimeResolvedActorScheduleState>;
|
||||
nextActiveImpulseRoutineIds: Set<string>;
|
||||
npcChanges: RuntimeScheduleNpcChange[];
|
||||
npcEntityCollectionChanged: boolean;
|
||||
npcColliderCollectionChanged: boolean;
|
||||
}
|
||||
|
||||
function isNonNull<TValue>(value: TValue | null): value is TValue {
|
||||
return value !== null;
|
||||
}
|
||||
|
||||
function createRuntimePathLookup(
|
||||
runtimeScene: RuntimeSceneDefinition
|
||||
): ReadonlyMap<string, RuntimeSceneDefinition["paths"][number]> {
|
||||
return new Map(runtimeScene.paths.map((path) => [path.id, path]));
|
||||
}
|
||||
|
||||
function createRuntimeActorIds(runtimeScene: RuntimeSceneDefinition): string[] {
|
||||
return [...new Set(runtimeScene.npcDefinitions.map((npc) => npc.actorId))];
|
||||
}
|
||||
|
||||
function getMutableActorStatesByActorId(
|
||||
context: RuntimeScheduleSyncContext
|
||||
): Map<string, RuntimeResolvedActorScheduleState> {
|
||||
return context.actorStatesByActorId as Map<
|
||||
string,
|
||||
RuntimeResolvedActorScheduleState
|
||||
>;
|
||||
}
|
||||
|
||||
function hasNpcScheduleOutputChanged(change: RuntimeScheduleNpcChange): boolean {
|
||||
return (
|
||||
change.activeChanged ||
|
||||
change.previousRoutineTitle !== change.npc.activeRoutineTitle ||
|
||||
change.animationChanged ||
|
||||
change.transformChanged ||
|
||||
(change.npc.resolvedPath?.pathId ?? null) !== change.previousPathId ||
|
||||
(change.npc.resolvedPath?.progress ?? null) !== change.previousPathProgress
|
||||
);
|
||||
}
|
||||
|
||||
function hasNpcColliderChanged(change: RuntimeScheduleNpcChange): boolean {
|
||||
if (change.npc.collider.mode === "none") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return change.activeChanged || (change.npc.active && change.transformChanged);
|
||||
}
|
||||
|
||||
function createNpcChange(
|
||||
npc: RuntimeNpcDefinition,
|
||||
previous: {
|
||||
active: boolean;
|
||||
routineId: string | null;
|
||||
routineTitle: string | null;
|
||||
animationClipName: string | null | undefined;
|
||||
animationLoop: boolean | undefined;
|
||||
yawDegrees: number;
|
||||
position: {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
};
|
||||
pathId: string | null;
|
||||
pathProgress: number | null;
|
||||
}
|
||||
): RuntimeScheduleNpcChange | null {
|
||||
const activeChanged = npc.active !== previous.active;
|
||||
const animationChanged =
|
||||
npc.animationClipName !== previous.animationClipName ||
|
||||
npc.animationLoop !== previous.animationLoop;
|
||||
const transformChanged =
|
||||
npc.yawDegrees !== previous.yawDegrees ||
|
||||
npc.position.x !== previous.position.x ||
|
||||
npc.position.y !== previous.position.y ||
|
||||
npc.position.z !== previous.position.z;
|
||||
const routineChanged =
|
||||
npc.activeRoutineId !== previous.routineId ||
|
||||
npc.activeRoutineTitle !== previous.routineTitle;
|
||||
const pathChanged =
|
||||
(npc.resolvedPath?.pathId ?? null) !== previous.pathId ||
|
||||
(npc.resolvedPath?.progress ?? null) !== previous.pathProgress;
|
||||
|
||||
if (
|
||||
!activeChanged &&
|
||||
!animationChanged &&
|
||||
!transformChanged &&
|
||||
!routineChanged &&
|
||||
!pathChanged
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const change: RuntimeScheduleNpcChange = {
|
||||
npc,
|
||||
previousActive: previous.active,
|
||||
previousRoutineId: previous.routineId,
|
||||
previousRoutineTitle: previous.routineTitle,
|
||||
previousAnimationClipName: previous.animationClipName,
|
||||
previousAnimationLoop: previous.animationLoop,
|
||||
previousYawDegrees: previous.yawDegrees,
|
||||
previousPosition: previous.position,
|
||||
previousPathId: previous.pathId,
|
||||
previousPathProgress: previous.pathProgress,
|
||||
activeChanged,
|
||||
animationChanged,
|
||||
transformChanged,
|
||||
runtimeNpcChanged: false,
|
||||
colliderChanged: false
|
||||
};
|
||||
|
||||
change.runtimeNpcChanged = hasNpcScheduleOutputChanged(change);
|
||||
change.colliderChanged = hasNpcColliderChanged(change);
|
||||
|
||||
return change;
|
||||
}
|
||||
|
||||
function refreshRuntimeNpcEntities(runtimeScene: RuntimeSceneDefinition): void {
|
||||
runtimeScene.entities.npcs = runtimeScene.npcDefinitions
|
||||
.filter((npc) => npc.active)
|
||||
.map((npc) => createRuntimeNpcFromDefinition(npc));
|
||||
}
|
||||
|
||||
function refreshRuntimeNpcColliders(runtimeScene: RuntimeSceneDefinition): void {
|
||||
runtimeScene.colliders = [
|
||||
...runtimeScene.staticColliders,
|
||||
...runtimeScene.entities.npcs
|
||||
.map((npc) => buildRuntimeNpcCollider(npc))
|
||||
.filter(isNonNull)
|
||||
];
|
||||
}
|
||||
|
||||
export function createRuntimeScheduleSyncContext(
|
||||
runtimeScene: RuntimeSceneDefinition
|
||||
): RuntimeScheduleSyncContext {
|
||||
return {
|
||||
runtimeScene,
|
||||
pathsById: createRuntimePathLookup(runtimeScene),
|
||||
actorIds: createRuntimeActorIds(runtimeScene),
|
||||
actorStatesByActorId: new Map()
|
||||
};
|
||||
}
|
||||
|
||||
export function assertRuntimeScheduleSyncContextForScene(
|
||||
context: RuntimeScheduleSyncContext,
|
||||
runtimeScene: RuntimeSceneDefinition
|
||||
): void {
|
||||
if (context.runtimeScene !== runtimeScene) {
|
||||
throw new Error(
|
||||
"Runtime schedule sync context does not match the runtime scene."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function syncRuntimeSceneScheduleToClock(options: {
|
||||
runtimeScene: RuntimeSceneDefinition;
|
||||
clock: RuntimeClockState;
|
||||
context: RuntimeScheduleSyncContext;
|
||||
}): RuntimeScheduleSyncResult {
|
||||
const { runtimeScene, clock, context } = options;
|
||||
|
||||
assertRuntimeScheduleSyncContextForScene(context, runtimeScene);
|
||||
|
||||
const resolvedScheduler = resolveRuntimeProjectScheduleState({
|
||||
scheduler: runtimeScene.scheduler.document,
|
||||
sequences: runtimeScene.sequences,
|
||||
actorIds: [...context.actorIds],
|
||||
dayNumber: clock.dayCount + 1,
|
||||
timeOfDayHours: clock.timeOfDayHours,
|
||||
pathsById: context.pathsById
|
||||
});
|
||||
const actorStatesByActorId = getMutableActorStatesByActorId(context);
|
||||
const nextActiveImpulseRoutineIds = new Set(
|
||||
resolvedScheduler.impulses.map((routine) => routine.routineId)
|
||||
);
|
||||
const npcChanges: RuntimeScheduleNpcChange[] = [];
|
||||
let npcEntityCollectionChanged = false;
|
||||
let npcColliderCollectionChanged = false;
|
||||
|
||||
actorStatesByActorId.clear();
|
||||
for (const actorState of resolvedScheduler.actors) {
|
||||
actorStatesByActorId.set(actorState.actorId, actorState);
|
||||
}
|
||||
|
||||
for (const npc of runtimeScene.npcDefinitions) {
|
||||
const previous = {
|
||||
active: npc.active,
|
||||
routineId: npc.activeRoutineId,
|
||||
routineTitle: npc.activeRoutineTitle,
|
||||
animationClipName: npc.animationClipName,
|
||||
animationLoop: npc.animationLoop,
|
||||
yawDegrees: npc.yawDegrees,
|
||||
position: {
|
||||
x: npc.position.x,
|
||||
y: npc.position.y,
|
||||
z: npc.position.z
|
||||
},
|
||||
pathId: npc.resolvedPath?.pathId ?? null,
|
||||
pathProgress: npc.resolvedPath?.progress ?? null
|
||||
};
|
||||
|
||||
applyActorScheduleStateToNpcDefinition(
|
||||
npc,
|
||||
actorStatesByActorId.get(npc.actorId) ?? null
|
||||
);
|
||||
|
||||
const change = createNpcChange(npc, previous);
|
||||
|
||||
if (change === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
npcChanges.push(change);
|
||||
npcEntityCollectionChanged ||= change.runtimeNpcChanged;
|
||||
npcColliderCollectionChanged ||= change.colliderChanged;
|
||||
}
|
||||
|
||||
if (npcEntityCollectionChanged) {
|
||||
refreshRuntimeNpcEntities(runtimeScene);
|
||||
}
|
||||
|
||||
if (npcColliderCollectionChanged) {
|
||||
if (!npcEntityCollectionChanged) {
|
||||
refreshRuntimeNpcEntities(runtimeScene);
|
||||
}
|
||||
|
||||
refreshRuntimeNpcColliders(runtimeScene);
|
||||
}
|
||||
|
||||
return {
|
||||
resolvedScheduler,
|
||||
resolvedControl: applyRuntimeProjectScheduleToControlState(
|
||||
runtimeScene.control.resolved,
|
||||
resolvedScheduler,
|
||||
runtimeScene.control.baselineResolved
|
||||
),
|
||||
actorStatesByActorId: context.actorStatesByActorId,
|
||||
nextActiveImpulseRoutineIds,
|
||||
npcChanges,
|
||||
npcEntityCollectionChanged,
|
||||
npcColliderCollectionChanged
|
||||
};
|
||||
}
|
||||
|
||||
export function commitRuntimeScheduleSyncResult(
|
||||
runtimeScene: RuntimeSceneDefinition,
|
||||
result: RuntimeScheduleSyncResult
|
||||
): void {
|
||||
runtimeScene.scheduler.resolved = result.resolvedScheduler;
|
||||
runtimeScene.control.resolved = result.resolvedControl;
|
||||
}
|
||||
Reference in New Issue
Block a user