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: 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; nextActiveImpulseRoutineIds: Set; npcChanges: RuntimeScheduleNpcChange[]; npcEntityCollectionChanged: boolean; npcColliderCollectionChanged: boolean; } function isNonNull(value: TValue | null): value is TValue { return value !== null; } function createRuntimePathLookup( runtimeScene: RuntimeSceneDefinition ): ReadonlyMap { 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 { 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; } export function refreshRuntimeNpcEntities( runtimeScene: RuntimeSceneDefinition ): void { runtimeScene.entities.npcs = runtimeScene.npcDefinitions .filter((npc) => npc.active) .map((npc) => createRuntimeNpcFromDefinition(npc)); } export function refreshRuntimeNpcColliders( runtimeScene: RuntimeSceneDefinition ): void { runtimeScene.colliders = [ ...runtimeScene.staticColliders, ...runtimeScene.entities.npcs .map((npc) => buildRuntimeNpcCollider(npc)) .filter(isNonNull) ]; } export function refreshRuntimeNpcCollections( runtimeScene: RuntimeSceneDefinition ): void { refreshRuntimeNpcEntities(runtimeScene); refreshRuntimeNpcColliders(runtimeScene); } 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, actorIdsAreUnique: true, 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; }