import type { LoadedModelAsset } from "../assets/gltf-model-import"; import type { Vec3 } from "../core/vector"; import { getModelInstances } from "../assets/model-instances"; import { createActorControlTargetRef, createActiveSceneControlTargetRef, createAmbientLightIntensityControlChannelDescriptor, createCameraRigControlTargetRef, createControlTargetDescriptor, createDefaultResolvedControlSource, createEmptyRuntimeResolvedControlState, createInteractionControlTargetRef, createLightControlTargetRef, createLightIntensityControlChannelDescriptor, createModelInstanceControlTargetRef, createProjectGlobalControlTargetRef, createResolvedAmbientLightColorState, createResolvedActorAnimationPlaybackState, createResolvedActorPathAssignmentState, createResolvedAmbientLightIntensityChannelValue, createResolvedCameraRigOverrideState, createResolvedInteractionEnabledState, createResolvedLightColorState, createResolvedLightEnabledState, createResolvedLightIntensityChannelValue, createResolvedModelAnimationPlaybackState, createResolvedModelInstanceVisibilityState, createResolvedProjectTimePausedState, createResolvedSoundPlaybackState, createResolvedSoundVolumeChannelValue, createResolvedSunLightColorState, createResolvedSunLightIntensityChannelValue, createRuntimeControlSurfaceDefinition, createSoundEmitterControlTargetRef, createSoundVolumeControlChannelDescriptor, createSunLightIntensityControlChannelDescriptor, type RuntimeControlSurfaceDefinition } from "../controls/control-surface"; import { cloneBrushGeometry, cloneBoxBrushVolumeSettings, cloneFaceUvState, type Brush, type BrushGeometry, type BrushKind, type BoxBrushLightFalloffMode, type BoxBrushVolumeSettings, type WhiteboxFaceId, type FaceUvState } from "../document/brushes"; import type { SceneDocument } from "../document/scene-document"; import { cloneProjectTimeSettings, type ProjectTimeSettings } from "../document/project-time-settings"; import { getScenePaths, resolveScenePath, type ScenePath, type ScenePathPoint } from "../document/paths"; import { getTerrainBounds, getTerrainFootprintDepth, getTerrainFootprintWidth, getTerrains, type Terrain } from "../document/terrains"; import { cloneWorldSettings, type WorldSettings } from "../document/world-settings"; import { cloneProjectDialogue, type ProjectDialogue } from "../dialogues/project-dialogues"; import { cloneProjectSequenceLibrary, type ProjectSequenceLibrary } from "../sequencer/project-sequences"; import { cloneCameraRigLookAroundSettings, cloneCameraRigTargetRef, type CharacterColliderSettings, type CameraRigLookAroundSettings, type CameraRigRailPlacementMode, type CameraRigTargetRef, type CameraRigTransitionMode, clonePlayerStartInputBindings, createPlayerStartMovementTemplate, createPlayerStartInputBindings, getEntityInstances, getPrimaryEnabledPlayerStartEntity, type EntityInstance, type PlayerStartInputBindings, type PlayerStartJumpSettings, type PlayerStartMovementCapabilities, type PlayerStartCrouchSettings, type PlayerStartSprintSettings, type PlayerStartMovementTemplate } from "../entities/entity-instances"; import { getBrushBounds } from "../geometry/whitebox-brush"; import { buildBoxBrushDerivedMeshData } from "../geometry/box-brush-mesh"; import { getBrushFaceIds } from "../geometry/whitebox-topology"; import { buildGeneratedModelCollider, type GeneratedColliderBounds, type GeneratedModelCollider } from "../geometry/model-instance-collider-generation"; import { cloneInteractionLink, getInteractionLinks, type InteractionLink } from "../interactions/interaction-links"; import { getInteractionLinkImpulseSteps } from "../sequencer/project-sequence-steps"; import { cloneMaterialDef, type MaterialDef } from "../materials/starter-material-library"; import { cloneProjectScheduler } from "../scheduler/project-scheduler"; import { applyRuntimeProjectScheduleToControlState, createRuntimeProjectSchedulerState, resolveRuntimeProjectScheduleState, type RuntimeProjectSchedulerState, type RuntimeResolvedProjectScheduleState } from "./runtime-project-scheduler"; import { deriveBoxLightVolumePointLights, type DerivedLightVolumePointLight } from "./light-volume-utils"; import { assertRuntimeSceneBuildable } from "./runtime-scene-validation"; import type { RuntimeClockState } from "./runtime-project-time"; import { FIRST_PERSON_PLAYER_SHAPE, type FirstPersonPlayerShape } from "./player-collision"; export type RuntimeNavigationMode = "firstPerson" | "thirdPerson"; export interface RuntimeBrushFace { materialId: string | null; material: MaterialDef | null; uv: FaceUvState; } export interface RuntimeBoxBrushInstance { id: string; kind: BrushKind; sideCount?: number; majorSegmentCount?: number; tubeSegmentCount?: number; visible: boolean; center: Vec3; rotationDegrees: Vec3; size: Vec3; geometry: BrushGeometry; faces: Record; volume: BoxBrushVolumeSettings; } export interface RuntimeFogVolume { brushId: string; center: Vec3; rotationDegrees: Vec3; size: Vec3; colorHex: string; density: number; padding: number; } export interface RuntimeWaterVolume { brushId: string; center: Vec3; rotationDegrees: Vec3; size: Vec3; colorHex: string; surfaceOpacity: number; waveStrength: number; } export interface RuntimeLightVolume { brushId: string; enabled: boolean; center: Vec3; rotationDegrees: Vec3; size: Vec3; colorHex: string; intensity: number; padding: number; falloff: BoxBrushLightFalloffMode; lights: DerivedLightVolumePointLight[]; } export interface RuntimeBoxVolumeCollection { fog: RuntimeFogVolume[]; water: RuntimeWaterVolume[]; light: RuntimeLightVolume[]; } export interface RuntimeTerrain { id: string; name?: string; visible: boolean; collisionEnabled: boolean; position: Vec3; sampleCountX: number; sampleCountZ: number; cellSize: number; heights: number[]; layers: Array<{ materialId: string | null; material: MaterialDef | null; }>; paintWeights: number[]; } export interface RuntimeTerrainHeightfieldCollider { kind: "heightfield"; source: "terrain"; terrainId: string; position: Vec3; rows: number; cols: number; heights: Float32Array; minX: number; maxX: number; minZ: number; maxZ: number; worldBounds: { min: Vec3; max: Vec3; }; } export interface RuntimeBrushTriMeshCollider { kind: "trimesh"; source: "brush"; brushId: string; center: Vec3; rotationDegrees: Vec3; vertices: Float32Array; indices: Uint32Array; worldBounds: { min: Vec3; max: Vec3; }; } export interface RuntimeNpcCollider { kind: "character"; source: "npc"; entityId: string; position: Vec3; rotationDegrees: Vec3; shape: FirstPersonPlayerShape; worldBounds: { min: Vec3; max: Vec3; }; } export type RuntimeSceneCollider = | RuntimeBrushTriMeshCollider | RuntimeTerrainHeightfieldCollider | GeneratedModelCollider | RuntimeNpcCollider; export interface RuntimeSceneBounds { min: Vec3; max: Vec3; center: Vec3; size: Vec3; } export interface RuntimePlayerStart { entityId: string; position: Vec3; yawDegrees: number; navigationMode: RuntimeNavigationMode; interactionReachMeters: number; interactionAngleDegrees: number; allowLookInputTargetSwitch: boolean; targetButtonCyclesActiveTarget: boolean; movement: RuntimePlayerMovement; inputBindings: PlayerStartInputBindings; collider: FirstPersonPlayerShape; } export interface RuntimePlayerMovement { templateKind: PlayerStartMovementTemplate["kind"]; moveSpeed: number; maxSpeed: number; maxStepHeight: number; capabilities: PlayerStartMovementCapabilities; jump: PlayerStartJumpSettings; sprint: PlayerStartSprintSettings; crouch: PlayerStartCrouchSettings; } export interface RuntimeSceneEntry { entityId: string; position: Vec3; yawDegrees: number; } interface RuntimeCameraRigBase { entityId: string; priority: number; defaultActive: boolean; target: CameraRigTargetRef; targetOffset: Vec3; transitionMode: CameraRigTransitionMode; transitionDurationSeconds: number; lookAround: CameraRigLookAroundSettings; } export interface RuntimeFixedCameraRig extends RuntimeCameraRigBase { rigType: "fixed"; position: Vec3; } interface RuntimeRailCameraRigBase extends RuntimeCameraRigBase { rigType: "rail"; pathId: string; railPlacementMode: CameraRigRailPlacementMode; } export interface RuntimeNearestRailCameraRig extends RuntimeRailCameraRigBase { railPlacementMode: "nearestToTarget"; } export interface RuntimeMappedRailCameraRig extends RuntimeRailCameraRigBase { railPlacementMode: "mapTargetBetweenPoints"; trackStartPoint: Vec3; trackEndPoint: Vec3; railStartProgress: number; railEndProgress: number; } export type RuntimeRailCameraRig = | RuntimeNearestRailCameraRig | RuntimeMappedRailCameraRig; export type RuntimeCameraRig = RuntimeFixedCameraRig | RuntimeRailCameraRig; export interface RuntimeNpc { entityId: string; actorId: string; name?: string; visible: boolean; position: Vec3; yawDegrees: number; modelAssetId: string | null; dialogues: ProjectDialogue[]; defaultDialogueId: string | null; collider: FirstPersonPlayerShape; activeRoutineTitle: string | null; animationClipName: string | null; animationLoop: boolean | undefined; resolvedPath: RuntimeResolvedNpcPathState | null; } export interface RuntimeNpcDefinition extends RuntimeNpc { active: boolean; activeRoutineId: string | null; authoredPosition: Vec3; authoredYawDegrees: number; } export interface RuntimeResolvedNpcPathState { pathId: string; progressMode: "deriveFromTime"; speed: number; loop: boolean; smoothPath: boolean; elapsedHours: number; distance: number; progress: number; position: Vec3; tangent: Vec3; yawDegrees: number | null; } export interface RuntimeSoundEmitter { entityId: string; position: Vec3; audioAssetId: string | null; volume: number; refDistance: number; maxDistance: number; autoplay: boolean; loop: boolean; } export interface RuntimeTriggerVolume { entityId: string; position: Vec3; size: Vec3; triggerOnEnter: boolean; triggerOnExit: boolean; } export interface RuntimeTeleportTarget { entityId: string; position: Vec3; yawDegrees: number; } export interface RuntimeInteractable { entityId: string; position: Vec3; radius: number; prompt: string; interactionEnabled: boolean; } export interface RuntimePointLight { entityId: string; enabled: boolean; position: Vec3; colorHex: string; intensity: number; distance: number; } export interface RuntimeSpotLight { entityId: string; enabled: boolean; position: Vec3; direction: Vec3; colorHex: string; intensity: number; distance: number; angleDegrees: number; } export interface RuntimeLocalLightCollection { pointLights: RuntimePointLight[]; spotLights: RuntimeSpotLight[]; } export interface RuntimeModelInstance { instanceId: string; assetId: string; name?: string; visible: boolean; position: Vec3; rotationDegrees: Vec3; scale: Vec3; animationClipName?: string; animationAutoplay?: boolean; animationLoop?: boolean; } export interface RuntimePathPoint { pointId: string; position: Vec3; } export interface RuntimePathSegment { index: number; startPointId: string; endPointId: string; start: Vec3; end: Vec3; length: number; distanceStart: number; distanceEnd: number; tangent: Vec3; } export interface RuntimePath { id: string; name?: string; visible: boolean; enabled: boolean; loop: boolean; points: RuntimePathPoint[]; segments: RuntimePathSegment[]; totalLength: number; } export interface RuntimeEntityCollection { playerStarts: RuntimePlayerStart[]; sceneEntries: RuntimeSceneEntry[]; cameraRigs: RuntimeCameraRig[]; npcs: RuntimeNpc[]; soundEmitters: RuntimeSoundEmitter[]; triggerVolumes: RuntimeTriggerVolume[]; teleportTargets: RuntimeTeleportTarget[]; interactables: RuntimeInteractable[]; } export interface RuntimeSpawnPoint { source: "playerStart" | "sceneEntry" | "fallback"; entityId: string | null; position: Vec3; yawDegrees: number; } export interface RuntimeSceneDefinition { time: ProjectTimeSettings; scheduler: RuntimeProjectSchedulerState; sequences: ProjectSequenceLibrary; world: WorldSettings; control: RuntimeControlSurfaceDefinition; localLights: RuntimeLocalLightCollection; brushes: RuntimeBoxBrushInstance[]; terrains: RuntimeTerrain[]; volumes: RuntimeBoxVolumeCollection; staticColliders: RuntimeSceneCollider[]; colliders: RuntimeSceneCollider[]; sceneBounds: RuntimeSceneBounds | null; modelInstances: RuntimeModelInstance[]; paths: RuntimePath[]; npcDefinitions: RuntimeNpcDefinition[]; entities: RuntimeEntityCollection; interactionLinks: InteractionLink[]; playerStart: RuntimePlayerStart | null; playerCollider: FirstPersonPlayerShape; playerMovement: RuntimePlayerMovement; playerInputBindings: PlayerStartInputBindings; navigationMode: RuntimeNavigationMode; spawn: RuntimeSpawnPoint; } export interface BuildRuntimeSceneOptions { navigationMode?: RuntimeNavigationMode; loadedModelAssets?: Record; runtimeClock?: RuntimeClockState; sceneEntryId?: string | null; } export function resolveRuntimeNavigationMode( playerStartEntity: ReturnType, authoredOverride?: RuntimeNavigationMode ): RuntimeNavigationMode { if (authoredOverride !== undefined) { return authoredOverride; } return playerStartEntity?.navigationMode ?? "thirdPerson"; } function cloneVec3(vector: Vec3): Vec3 { return { x: vector.x, y: vector.y, z: vector.z }; } function createRuntimeCharacterShape( collider: CharacterColliderSettings ): FirstPersonPlayerShape { switch (collider.mode) { case "capsule": return { mode: "capsule", radius: collider.capsuleRadius, height: collider.capsuleHeight, eyeHeight: collider.eyeHeight }; case "box": return { mode: "box", size: cloneVec3(collider.boxSize), eyeHeight: collider.eyeHeight }; case "none": return { mode: "none", eyeHeight: collider.eyeHeight }; } } function cloneRuntimeCharacterShape( shape: FirstPersonPlayerShape ): FirstPersonPlayerShape { switch (shape.mode) { case "capsule": return { mode: "capsule", radius: shape.radius, height: shape.height, eyeHeight: shape.eyeHeight }; case "box": return { mode: "box", size: cloneVec3(shape.size), eyeHeight: shape.eyeHeight }; case "none": return { mode: "none", eyeHeight: shape.eyeHeight }; } } function cloneRuntimeResolvedNpcPathState( pathState: RuntimeResolvedNpcPathState ): RuntimeResolvedNpcPathState { return { pathId: pathState.pathId, progressMode: pathState.progressMode, speed: pathState.speed, loop: pathState.loop, smoothPath: pathState.smoothPath, elapsedHours: pathState.elapsedHours, distance: pathState.distance, progress: pathState.progress, position: cloneVec3(pathState.position), tangent: cloneVec3(pathState.tangent), yawDegrees: pathState.yawDegrees }; } export function createRuntimeNpcFromDefinition( npc: RuntimeNpcDefinition ): RuntimeNpc { return { entityId: npc.entityId, actorId: npc.actorId, name: npc.name, visible: npc.visible, position: cloneVec3(npc.position), yawDegrees: npc.yawDegrees, modelAssetId: npc.modelAssetId, dialogues: npc.dialogues.map(cloneProjectDialogue), defaultDialogueId: npc.defaultDialogueId, collider: cloneRuntimeCharacterShape(npc.collider), activeRoutineTitle: npc.activeRoutineTitle, animationClipName: npc.animationClipName, animationLoop: npc.animationLoop, resolvedPath: npc.resolvedPath === null ? null : cloneRuntimeResolvedNpcPathState(npc.resolvedPath) }; } function clonePlayerStartMovementCapabilities( capabilities: PlayerStartMovementCapabilities ): PlayerStartMovementCapabilities { return { jump: capabilities.jump, sprint: capabilities.sprint, crouch: capabilities.crouch }; } function clonePlayerStartJumpSettings( jump: PlayerStartJumpSettings ): PlayerStartJumpSettings { return { speed: jump.speed, bufferMs: jump.bufferMs, coyoteTimeMs: jump.coyoteTimeMs, variableHeight: jump.variableHeight, maxHoldMs: jump.maxHoldMs, moveWhileJumping: jump.moveWhileJumping, moveWhileFalling: jump.moveWhileFalling, directionOnly: jump.directionOnly, bunnyHop: jump.bunnyHop, bunnyHopBoost: jump.bunnyHopBoost }; } function clonePlayerStartSprintSettings( sprint: PlayerStartSprintSettings ): PlayerStartSprintSettings { return { speedMultiplier: sprint.speedMultiplier }; } function clonePlayerStartCrouchSettings( crouch: PlayerStartCrouchSettings ): PlayerStartCrouchSettings { return { speedMultiplier: crouch.speedMultiplier }; } function cloneRuntimePlayerMovement( movement: RuntimePlayerMovement ): RuntimePlayerMovement { return { templateKind: movement.templateKind, moveSpeed: movement.moveSpeed, maxSpeed: movement.maxSpeed, maxStepHeight: movement.maxStepHeight, capabilities: clonePlayerStartMovementCapabilities(movement.capabilities), jump: clonePlayerStartJumpSettings(movement.jump), sprint: clonePlayerStartSprintSettings(movement.sprint), crouch: clonePlayerStartCrouchSettings(movement.crouch) }; } function buildRuntimePlayerMovement( template: PlayerStartMovementTemplate | undefined ): RuntimePlayerMovement { const resolvedTemplate = createPlayerStartMovementTemplate(template); return { templateKind: resolvedTemplate.kind, moveSpeed: resolvedTemplate.moveSpeed, maxSpeed: resolvedTemplate.maxSpeed, maxStepHeight: resolvedTemplate.maxStepHeight, capabilities: clonePlayerStartMovementCapabilities( resolvedTemplate.capabilities ), jump: clonePlayerStartJumpSettings(resolvedTemplate.jump), sprint: clonePlayerStartSprintSettings(resolvedTemplate.sprint), crouch: clonePlayerStartCrouchSettings(resolvedTemplate.crouch) }; } function resolveRuntimeMaterial( document: SceneDocument, materialId: string | null ): MaterialDef | null { if (materialId === null) { return null; } const material = document.materials[materialId]; if (material === undefined) { throw new Error(`Runtime build could not resolve material ${materialId}.`); } return cloneMaterialDef(material); } function buildRuntimeBrush( brush: Brush, document: SceneDocument ): RuntimeBoxBrushInstance { return { id: brush.id, kind: brush.kind, sideCount: brush.kind === "radialPrism" || brush.kind === "cone" ? brush.sideCount : undefined, majorSegmentCount: brush.kind === "torus" ? brush.majorSegmentCount : undefined, tubeSegmentCount: brush.kind === "torus" ? brush.tubeSegmentCount : undefined, visible: brush.visible, center: cloneVec3(brush.center), rotationDegrees: cloneVec3(brush.rotationDegrees), size: cloneVec3(brush.size), geometry: cloneBrushGeometry(brush.geometry), volume: cloneBoxBrushVolumeSettings(brush.volume), faces: Object.fromEntries( getBrushFaceIds(brush).map((faceId) => { const face = brush.faces[faceId]; return [ faceId, { materialId: face.materialId, material: resolveRuntimeMaterial(document, face.materialId), uv: cloneFaceUvState(face.uv) } ]; }) ) as Record }; } function buildRuntimeFogVolume(brush: Brush): RuntimeFogVolume { if (brush.volume.mode !== "fog") { throw new Error(`Cannot build fog volume from non-fog brush ${brush.id}.`); } return { brushId: brush.id, center: cloneVec3(brush.center), rotationDegrees: cloneVec3(brush.rotationDegrees), size: cloneVec3(brush.size), colorHex: brush.volume.fog.colorHex, density: brush.volume.fog.density, padding: brush.volume.fog.padding }; } function buildRuntimeWaterVolume(brush: Brush): RuntimeWaterVolume { if (brush.volume.mode !== "water") { throw new Error( `Cannot build water volume from non-water brush ${brush.id}.` ); } return { brushId: brush.id, center: cloneVec3(brush.center), rotationDegrees: cloneVec3(brush.rotationDegrees), size: cloneVec3(brush.size), colorHex: brush.volume.water.colorHex, surfaceOpacity: brush.volume.water.surfaceOpacity, waveStrength: brush.volume.water.waveStrength }; } function buildRuntimeLightVolume(brush: Brush): RuntimeLightVolume { if (brush.volume.mode !== "light") { throw new Error( `Cannot build light volume from non-light brush ${brush.id}.` ); } return { brushId: brush.id, enabled: brush.visible, center: cloneVec3(brush.center), rotationDegrees: cloneVec3(brush.rotationDegrees), size: cloneVec3(brush.size), colorHex: brush.volume.light.colorHex, intensity: brush.volume.light.intensity, padding: brush.volume.light.padding, falloff: brush.volume.light.falloff, lights: deriveBoxLightVolumePointLights({ size: brush.size, intensity: brush.volume.light.intensity, padding: brush.volume.light.padding, falloff: brush.volume.light.falloff }) }; } function buildRuntimeCollider(brush: Brush): RuntimeBrushTriMeshCollider { const bounds = getBrushBounds(brush); const derivedMesh = buildBoxBrushDerivedMeshData(brush); return { kind: "trimesh", source: "brush", brushId: brush.id, center: cloneVec3(brush.center), rotationDegrees: cloneVec3(brush.rotationDegrees), vertices: derivedMesh.colliderVertices, indices: derivedMesh.colliderIndices, worldBounds: { min: cloneVec3(bounds.min), max: cloneVec3(bounds.max) } }; } function buildRuntimeTerrain( terrain: Terrain, document: SceneDocument ): RuntimeTerrain { return { id: terrain.id, name: terrain.name, visible: terrain.visible, collisionEnabled: terrain.collisionEnabled, position: cloneVec3(terrain.position), sampleCountX: terrain.sampleCountX, sampleCountZ: terrain.sampleCountZ, cellSize: terrain.cellSize, heights: [...terrain.heights], layers: terrain.layers.map((layer) => ({ materialId: layer.materialId, material: resolveRuntimeMaterial(document, layer.materialId) })), paintWeights: [...terrain.paintWeights] }; } function buildRuntimeTerrainCollider( terrain: Terrain ): RuntimeTerrainHeightfieldCollider | null { if (!terrain.collisionEnabled) { return null; } const bounds = getTerrainBounds(terrain); return { kind: "heightfield", source: "terrain", terrainId: terrain.id, position: cloneVec3(terrain.position), rows: terrain.sampleCountX, cols: terrain.sampleCountZ, heights: new Float32Array(terrain.heights), minX: 0, maxX: getTerrainFootprintWidth(terrain), minZ: 0, maxZ: getTerrainFootprintDepth(terrain), worldBounds: { min: cloneVec3(bounds.min), max: cloneVec3(bounds.max) } }; } function buildRuntimeModelInstance( modelInstance: SceneDocument["modelInstances"][string] ): RuntimeModelInstance { return { instanceId: modelInstance.id, assetId: modelInstance.assetId, name: modelInstance.name, visible: modelInstance.visible, position: cloneVec3(modelInstance.position), rotationDegrees: cloneVec3(modelInstance.rotationDegrees), scale: cloneVec3(modelInstance.scale), animationClipName: modelInstance.animationClipName, animationAutoplay: modelInstance.animationAutoplay, animationLoop: undefined }; } function buildRuntimePathPoint(point: ScenePathPoint): RuntimePathPoint { return { pointId: point.id, position: cloneVec3(point.position) }; } function buildRuntimePath(path: ScenePath): RuntimePath { const resolvedPath = resolveScenePath(path); return { id: path.id, name: path.name, visible: path.visible, enabled: path.enabled, loop: path.loop, points: resolvedPath.points.map(buildRuntimePathPoint), segments: resolvedPath.segments.map((segment) => ({ index: segment.index, startPointId: segment.startPointId, endPointId: segment.endPointId, start: cloneVec3(segment.start), end: cloneVec3(segment.end), length: segment.length, distanceStart: segment.distanceStart, distanceEnd: segment.distanceEnd, tangent: cloneVec3(segment.tangent) })), totalLength: resolvedPath.totalLength }; } function createRuntimePathLookup( paths: RuntimePath[] ): ReadonlyMap { return new Map(paths.map((path) => [path.id, path])); } function cloneResolvedActorPathToNpcPathState( pathState: NonNullable< RuntimeResolvedProjectScheduleState["actors"][number]["resolvedPath"] > ): RuntimeResolvedNpcPathState { return { pathId: pathState.pathId, progressMode: pathState.progressMode, speed: pathState.speed, loop: pathState.loop, smoothPath: pathState.smoothPath, elapsedHours: pathState.elapsedHours, distance: pathState.distance, progress: pathState.progress, position: cloneVec3(pathState.position), tangent: cloneVec3(pathState.tangent), yawDegrees: pathState.yawDegrees }; } export function applyActorScheduleStateToNpcDefinition( npc: RuntimeNpcDefinition, actorState: RuntimeResolvedProjectScheduleState["actors"][number] | null ) { npc.active = actorState?.active ?? true; npc.activeRoutineId = actorState?.activeRoutineId ?? null; npc.activeRoutineTitle = actorState?.activeRoutineTitle ?? null; npc.animationClipName = actorState?.animationEffect?.clipName ?? null; npc.animationLoop = actorState?.animationEffect?.loop; npc.resolvedPath = actorState?.resolvedPath === null || actorState?.resolvedPath === undefined ? null : cloneResolvedActorPathToNpcPathState(actorState.resolvedPath); if (npc.resolvedPath !== null) { npc.position = cloneVec3(npc.resolvedPath.position); npc.yawDegrees = npc.resolvedPath.yawDegrees ?? npc.authoredYawDegrees; return; } npc.position = cloneVec3(npc.authoredPosition); npc.yawDegrees = npc.authoredYawDegrees; } function getColliderBounds( collider: RuntimeSceneCollider ): GeneratedColliderBounds { return { min: cloneVec3(collider.worldBounds.min), max: cloneVec3(collider.worldBounds.max) }; } export function buildRuntimeNpcCollider( npc: RuntimeNpc | RuntimeNpcDefinition ): RuntimeNpcCollider | null { if (npc.collider.mode === "none") { return null; } const rotationDegrees = { x: 0, y: npc.yawDegrees, z: 0 }; switch (npc.collider.mode) { case "capsule": { return { kind: "character", source: "npc", entityId: npc.entityId, position: cloneVec3(npc.position), rotationDegrees, shape: { mode: "capsule", radius: npc.collider.radius, height: npc.collider.height, eyeHeight: npc.collider.eyeHeight }, worldBounds: { min: { x: npc.position.x - npc.collider.radius, y: npc.position.y, z: npc.position.z - npc.collider.radius }, max: { x: npc.position.x + npc.collider.radius, y: npc.position.y + npc.collider.height, z: npc.position.z + npc.collider.radius } } }; } case "box": { const halfExtents = { x: npc.collider.size.x * 0.5, y: npc.collider.size.y * 0.5, z: npc.collider.size.z * 0.5 }; const yawRadians = (rotationDegrees.y * Math.PI) / 180; const cosine = Math.abs(Math.cos(yawRadians)); const sine = Math.abs(Math.sin(yawRadians)); const rotatedHalfX = halfExtents.x * cosine + halfExtents.z * sine; const rotatedHalfZ = halfExtents.x * sine + halfExtents.z * cosine; return { kind: "character", source: "npc", entityId: npc.entityId, position: cloneVec3(npc.position), rotationDegrees, shape: { mode: "box", size: cloneVec3(npc.collider.size), eyeHeight: npc.collider.eyeHeight }, worldBounds: { min: { x: npc.position.x - rotatedHalfX, y: npc.position.y, z: npc.position.z - rotatedHalfZ }, max: { x: npc.position.x + rotatedHalfX, y: npc.position.y + npc.collider.size.y, z: npc.position.z + rotatedHalfZ } } }; } } } function combineColliderBounds( colliders: RuntimeSceneCollider[] ): RuntimeSceneBounds | null { if (colliders.length === 0) { return null; } const firstBounds = getColliderBounds(colliders[0]); const min = cloneVec3(firstBounds.min); const max = cloneVec3(firstBounds.max); for (const collider of colliders.slice(1)) { const bounds = getColliderBounds(collider); min.x = Math.min(min.x, bounds.min.x); min.y = Math.min(min.y, bounds.min.y); min.z = Math.min(min.z, bounds.min.z); max.x = Math.max(max.x, bounds.max.x); max.y = Math.max(max.y, bounds.max.y); max.z = Math.max(max.z, bounds.max.z); } return { min, max, center: { x: (min.x + max.x) * 0.5, y: (min.y + max.y) * 0.5, z: (min.z + max.z) * 0.5 }, size: { x: max.x - min.x, y: max.y - min.y, z: max.z - min.z } }; } function getRuntimeTerrainSceneBounds(terrain: RuntimeTerrain): RuntimeSceneBounds { const bounds = getTerrainBounds({ ...terrain, kind: "terrain", enabled: true }); const min = cloneVec3(bounds.min); const max = cloneVec3(bounds.max); return { min, max, center: { x: (min.x + max.x) * 0.5, y: (min.y + max.y) * 0.5, z: (min.z + max.z) * 0.5 }, size: { x: max.x - min.x, y: max.y - min.y, z: max.z - min.z } }; } function combineSceneBounds( colliderBounds: RuntimeSceneBounds | null, terrains: RuntimeTerrain[] ): RuntimeSceneBounds | null { const terrainBounds = terrains.map(getRuntimeTerrainSceneBounds); if (colliderBounds === null && terrainBounds.length === 0) { return null; } const min = colliderBounds?.min ? cloneVec3(colliderBounds.min) : cloneVec3(terrainBounds[0]!.min); const max = colliderBounds?.max ? cloneVec3(colliderBounds.max) : cloneVec3(terrainBounds[0]!.max); for (const bounds of terrainBounds) { min.x = Math.min(min.x, bounds.min.x); min.y = Math.min(min.y, bounds.min.y); min.z = Math.min(min.z, bounds.min.z); max.x = Math.max(max.x, bounds.max.x); max.y = Math.max(max.y, bounds.max.y); max.z = Math.max(max.z, bounds.max.z); } return { min, max, center: { x: (min.x + max.x) * 0.5, y: (min.y + max.y) * 0.5, z: (min.z + max.z) * 0.5 }, size: { x: max.x - min.x, y: max.y - min.y, z: max.z - min.z } }; } function buildFallbackSpawn( sceneBounds: RuntimeSceneBounds | null ): RuntimeSpawnPoint { if (sceneBounds === null) { return { source: "fallback", entityId: null, position: { x: 0, y: 0, z: -4 }, yawDegrees: 0 }; } return { source: "fallback", entityId: null, position: { x: sceneBounds.center.x, y: sceneBounds.max.y + 0.1, z: sceneBounds.max.z + 3 }, yawDegrees: 180 }; } interface RuntimeSceneCollections { entities: RuntimeEntityCollection; localLights: RuntimeLocalLightCollection; npcDefinitions: RuntimeNpcDefinition[]; scheduler: RuntimeResolvedProjectScheduleState; } function buildRuntimeControlSurface( document: SceneDocument, collections: RuntimeSceneCollections, modelInstances: RuntimeModelInstance[] ): RuntimeControlSurfaceDefinition { const targets: RuntimeControlSurfaceDefinition["targets"] = []; const channels: RuntimeControlSurfaceDefinition["channels"] = []; const resolved = createEmptyRuntimeResolvedControlState(); const defaultSource = createDefaultResolvedControlSource(); const seenActorIds = new Set(); const globalTarget = createProjectGlobalControlTargetRef(); const sceneTarget = createActiveSceneControlTargetRef(); const ambientLightDescriptor = createAmbientLightIntensityControlChannelDescriptor({ target: sceneTarget, defaultValue: document.world.ambientLight.intensity }); const sunLightDescriptor = createSunLightIntensityControlChannelDescriptor({ target: sceneTarget, defaultValue: document.world.sunLight.intensity }); targets.push( createControlTargetDescriptor(globalTarget, ["projectTimePause"]) ); resolved.discrete.push( createResolvedProjectTimePausedState({ target: globalTarget, value: false, source: defaultSource }), createResolvedCameraRigOverrideState({ target: globalTarget, entityId: null, source: defaultSource }) ); targets.push( createControlTargetDescriptor(sceneTarget, [ "ambientLightIntensity", "ambientLightColor", "sunLightIntensity", "sunLightColor" ]) ); channels.push(ambientLightDescriptor, sunLightDescriptor); resolved.discrete.push( createResolvedAmbientLightColorState({ target: sceneTarget, value: document.world.ambientLight.colorHex, source: defaultSource }), createResolvedSunLightColorState({ target: sceneTarget, value: document.world.sunLight.colorHex, source: defaultSource }) ); resolved.channels.push( createResolvedAmbientLightIntensityChannelValue({ descriptor: ambientLightDescriptor, value: document.world.ambientLight.intensity, source: defaultSource }), createResolvedSunLightIntensityChannelValue({ descriptor: sunLightDescriptor, value: document.world.sunLight.intensity, source: defaultSource }) ); for (const npc of collections.npcDefinitions) { if (seenActorIds.has(npc.actorId)) { continue; } seenActorIds.add(npc.actorId); const target = createActorControlTargetRef(npc.actorId); const capabilities: Array< "actorPresence" | "actorAnimationPlayback" | "actorPathFollow" > = ["actorPresence"]; const actorModelAsset = npc.modelAssetId === null ? undefined : document.assets[npc.modelAssetId]; if ( actorModelAsset !== undefined && actorModelAsset.kind === "model" && actorModelAsset.metadata.animationNames.length > 0 ) { capabilities.push("actorAnimationPlayback"); } if (getScenePaths(document.paths).some((path) => path.enabled)) { capabilities.push("actorPathFollow"); } targets.push( createControlTargetDescriptor(target, capabilities) ); resolved.discrete.push( createResolvedActorAnimationPlaybackState({ target, clipName: null, loop: undefined, source: defaultSource }), createResolvedActorPathAssignmentState({ target, pathId: null, speed: null, loop: false, smoothPath: true, progressMode: null, source: defaultSource }) ); } for (const cameraRig of collections.entities.cameraRigs) { targets.push( createControlTargetDescriptor( createCameraRigControlTargetRef(cameraRig.entityId), ["cameraRigOverride"] ) ); } for (const pointLight of collections.localLights.pointLights) { const target = createLightControlTargetRef( "pointLight", pointLight.entityId ); const descriptor = createLightIntensityControlChannelDescriptor({ target, defaultValue: pointLight.intensity }); targets.push( createControlTargetDescriptor(target, [ "lightEnabled", "lightIntensity", "lightColor" ]) ); channels.push(descriptor); resolved.discrete.push( createResolvedLightEnabledState({ target, value: pointLight.enabled, source: defaultSource }), createResolvedLightColorState({ target, value: pointLight.colorHex, source: defaultSource }) ); resolved.channels.push( createResolvedLightIntensityChannelValue({ descriptor, value: pointLight.intensity, source: defaultSource }) ); } for (const spotLight of collections.localLights.spotLights) { const target = createLightControlTargetRef("spotLight", spotLight.entityId); const descriptor = createLightIntensityControlChannelDescriptor({ target, defaultValue: spotLight.intensity }); targets.push( createControlTargetDescriptor(target, [ "lightEnabled", "lightIntensity", "lightColor" ]) ); channels.push(descriptor); resolved.discrete.push( createResolvedLightEnabledState({ target, value: spotLight.enabled, source: defaultSource }), createResolvedLightColorState({ target, value: spotLight.colorHex, source: defaultSource }) ); resolved.channels.push( createResolvedLightIntensityChannelValue({ descriptor, value: spotLight.intensity, source: defaultSource }) ); } for (const soundEmitter of collections.entities.soundEmitters) { if (soundEmitter.audioAssetId === null) { continue; } const target = createSoundEmitterControlTargetRef(soundEmitter.entityId); const descriptor = createSoundVolumeControlChannelDescriptor({ target, defaultValue: soundEmitter.volume }); targets.push( createControlTargetDescriptor(target, ["soundPlayback", "soundVolume"]) ); channels.push(descriptor); resolved.discrete.push( createResolvedSoundPlaybackState({ target, value: soundEmitter.autoplay, source: defaultSource }) ); resolved.channels.push( createResolvedSoundVolumeChannelValue({ descriptor, value: soundEmitter.volume, source: defaultSource }) ); } for (const interactable of collections.entities.interactables) { const target = createInteractionControlTargetRef( "interactable", interactable.entityId ); targets.push( createControlTargetDescriptor(target, ["interactionAvailability"]) ); resolved.discrete.push( createResolvedInteractionEnabledState({ target, value: interactable.interactionEnabled, source: defaultSource }) ); } for (const modelInstance of modelInstances) { const authoredModelInstance = document.modelInstances[modelInstance.instanceId]; const asset = authoredModelInstance === undefined ? undefined : document.assets[authoredModelInstance.assetId]; const target = createModelInstanceControlTargetRef(modelInstance.instanceId); const hasAnimationPlayback = asset !== undefined && asset.kind === "model" && asset.metadata.animationNames.length > 0; const capabilities: Array<"animationPlayback" | "modelVisibility"> = [ "modelVisibility" ]; if (hasAnimationPlayback) { capabilities.unshift("animationPlayback"); } targets.push(createControlTargetDescriptor(target, capabilities)); resolved.discrete.push( createResolvedModelInstanceVisibilityState({ target, value: modelInstance.visible, source: defaultSource }) ); if (hasAnimationPlayback) { resolved.discrete.push( createResolvedModelAnimationPlaybackState({ target, clipName: modelInstance.animationAutoplay === true && typeof modelInstance.animationClipName === "string" ? modelInstance.animationClipName : null, loop: modelInstance.animationLoop, source: defaultSource }) ); } } return createRuntimeControlSurfaceDefinition({ targets, channels, baselineResolved: resolved, resolved: applyRuntimeProjectScheduleToControlState( resolved, collections.scheduler, resolved ) }); } function buildRuntimeSceneCollections( document: SceneDocument, runtimeClock: RuntimeClockState | null, paths: RuntimePath[] ): RuntimeSceneCollections { const runtimeEntities: RuntimeEntityCollection = { playerStarts: [], sceneEntries: [], cameraRigs: [], npcs: [], soundEmitters: [], triggerVolumes: [], teleportTargets: [], interactables: [] }; const localLights: RuntimeLocalLightCollection = { pointLights: [], spotLights: [] }; const npcDefinitions: RuntimeNpcDefinition[] = []; for (const entity of getEntityInstances(document.entities)) { if (!entity.enabled) { continue; } switch (entity.kind) { case "pointLight": localLights.pointLights.push({ entityId: entity.id, enabled: entity.visible, position: cloneVec3(entity.position), colorHex: entity.colorHex, intensity: entity.intensity, distance: entity.distance }); break; case "spotLight": localLights.spotLights.push({ entityId: entity.id, enabled: entity.visible, position: cloneVec3(entity.position), direction: cloneVec3(entity.direction), colorHex: entity.colorHex, intensity: entity.intensity, distance: entity.distance, angleDegrees: entity.angleDegrees }); break; case "playerStart": runtimeEntities.playerStarts.push({ entityId: entity.id, position: cloneVec3(entity.position), yawDegrees: entity.yawDegrees, navigationMode: entity.navigationMode, interactionReachMeters: entity.interactionReachMeters, interactionAngleDegrees: entity.interactionAngleDegrees, allowLookInputTargetSwitch: entity.allowLookInputTargetSwitch, targetButtonCyclesActiveTarget: entity.targetButtonCyclesActiveTarget, movement: buildRuntimePlayerMovement(entity.movementTemplate), inputBindings: clonePlayerStartInputBindings(entity.inputBindings), collider: buildRuntimePlayerShape(entity) }); break; case "sceneEntry": runtimeEntities.sceneEntries.push({ entityId: entity.id, position: cloneVec3(entity.position), yawDegrees: entity.yawDegrees }); break; case "cameraRig": runtimeEntities.cameraRigs.push( entity.rigType === "fixed" ? { entityId: entity.id, rigType: "fixed", priority: entity.priority, defaultActive: entity.defaultActive, position: cloneVec3(entity.position), target: cloneCameraRigTargetRef(entity.target), targetOffset: cloneVec3(entity.targetOffset), transitionMode: entity.transitionMode, transitionDurationSeconds: entity.transitionDurationSeconds, lookAround: cloneCameraRigLookAroundSettings(entity.lookAround) } : entity.railPlacementMode === "mapTargetBetweenPoints" ? { entityId: entity.id, rigType: "rail", priority: entity.priority, defaultActive: entity.defaultActive, pathId: entity.pathId, railPlacementMode: "mapTargetBetweenPoints", trackStartPoint: cloneVec3(entity.trackStartPoint), trackEndPoint: cloneVec3(entity.trackEndPoint), railStartProgress: entity.railStartProgress, railEndProgress: entity.railEndProgress, target: cloneCameraRigTargetRef(entity.target), targetOffset: cloneVec3(entity.targetOffset), transitionMode: entity.transitionMode, transitionDurationSeconds: entity.transitionDurationSeconds, lookAround: cloneCameraRigLookAroundSettings( entity.lookAround ) } : { entityId: entity.id, rigType: "rail", priority: entity.priority, defaultActive: entity.defaultActive, pathId: entity.pathId, railPlacementMode: "nearestToTarget", target: cloneCameraRigTargetRef(entity.target), targetOffset: cloneVec3(entity.targetOffset), transitionMode: entity.transitionMode, transitionDurationSeconds: entity.transitionDurationSeconds, lookAround: cloneCameraRigLookAroundSettings( entity.lookAround ) } ); break; case "npc": { const npc: RuntimeNpcDefinition = { entityId: entity.id, actorId: entity.actorId, name: entity.name, visible: entity.visible, position: cloneVec3(entity.position), active: true, activeRoutineId: null, activeRoutineTitle: null, authoredPosition: cloneVec3(entity.position), yawDegrees: entity.yawDegrees, authoredYawDegrees: entity.yawDegrees, modelAssetId: entity.modelAssetId, dialogues: entity.dialogues.map(cloneProjectDialogue), defaultDialogueId: entity.defaultDialogueId, collider: createRuntimeCharacterShape(entity.collider), animationClipName: null, animationLoop: undefined, resolvedPath: null }; npcDefinitions.push(npc); break; } case "soundEmitter": runtimeEntities.soundEmitters.push({ entityId: entity.id, position: cloneVec3(entity.position), audioAssetId: entity.audioAssetId, volume: entity.volume, refDistance: entity.refDistance, maxDistance: entity.maxDistance, autoplay: entity.autoplay, loop: entity.loop }); break; case "triggerVolume": runtimeEntities.triggerVolumes.push({ entityId: entity.id, position: cloneVec3(entity.position), size: cloneVec3(entity.size), // Derive from links so flags are always correct regardless of stored entity state triggerOnEnter: Object.values(document.interactionLinks).some( (l) => l.sourceEntityId === entity.id && l.trigger === "enter" ), triggerOnExit: Object.values(document.interactionLinks).some( (l) => l.sourceEntityId === entity.id && l.trigger === "exit" ) }); break; case "teleportTarget": runtimeEntities.teleportTargets.push({ entityId: entity.id, position: cloneVec3(entity.position), yawDegrees: entity.yawDegrees }); break; case "interactable": runtimeEntities.interactables.push({ entityId: entity.id, position: cloneVec3(entity.position), radius: entity.radius, prompt: entity.prompt, interactionEnabled: entity.interactionEnabled }); break; default: assertNever(entity); } } const scheduler = resolveRuntimeProjectScheduleState({ scheduler: document.scheduler, sequences: document.sequences, actorIds: npcDefinitions.map((npc) => npc.actorId), dayNumber: runtimeClock === null ? document.time.startDayNumber : runtimeClock.dayCount + 1, timeOfDayHours: runtimeClock === null ? document.time.startTimeOfDayHours : runtimeClock.timeOfDayHours, pathsById: createRuntimePathLookup(paths) }); const scheduleByActorId = new Map( scheduler.actors.map((actorState) => [actorState.actorId, actorState]) ); for (const npc of npcDefinitions) { const actorState = scheduleByActorId.get(npc.actorId); applyActorScheduleStateToNpcDefinition(npc, actorState ?? null); if (npc.active) { runtimeEntities.npcs.push(createRuntimeNpcFromDefinition(npc)); } } return { entities: runtimeEntities, localLights, npcDefinitions, scheduler }; } function assertNever(value: never): never { throw new Error( `Unsupported runtime entity: ${String((value as EntityInstance).kind)}` ); } function buildRuntimePlayerShape( playerStartEntity: ReturnType ): FirstPersonPlayerShape { if (playerStartEntity === null) { return FIRST_PERSON_PLAYER_SHAPE; } return createRuntimeCharacterShape(playerStartEntity.collider); } function resolveRuntimeSpawn( playerStart: RuntimePlayerStart | null, sceneEntries: RuntimeSceneEntry[], sceneBounds: RuntimeSceneBounds | null, sceneEntryId: string | null | undefined ): RuntimeSpawnPoint { if (sceneEntryId !== undefined && sceneEntryId !== null) { const sceneEntry = sceneEntries.find((entry) => entry.entityId === sceneEntryId) ?? null; if (sceneEntry === null) { throw new Error( `Runtime build could not resolve Scene Entry ${sceneEntryId}.` ); } return { source: "sceneEntry", entityId: sceneEntry.entityId, position: cloneVec3(sceneEntry.position), yawDegrees: sceneEntry.yawDegrees }; } if (playerStart !== null) { return { source: "playerStart", entityId: playerStart.entityId, position: cloneVec3(playerStart.position), yawDegrees: playerStart.yawDegrees }; } return buildFallbackSpawn(sceneBounds); } export function buildRuntimeSceneFromDocument( document: SceneDocument, options: BuildRuntimeSceneOptions = {} ): RuntimeSceneDefinition { const playerStartEntity = getPrimaryEnabledPlayerStartEntity( document.entities ); const navigationMode = resolveRuntimeNavigationMode( playerStartEntity, options.navigationMode ); assertRuntimeSceneBuildable(document, { navigationMode, loadedModelAssets: options.loadedModelAssets }); const enabledBrushes = Object.values(document.brushes).filter( (brush) => brush.enabled ); const brushes = enabledBrushes.map((brush) => buildRuntimeBrush(brush, document) ); const enabledTerrains = getTerrains(document.terrains).filter( (terrain) => terrain.enabled ); const terrains = enabledTerrains.map((terrain) => buildRuntimeTerrain(terrain, document) ); const staticColliders: RuntimeSceneCollider[] = []; const volumes: RuntimeBoxVolumeCollection = { fog: [], water: [], light: [] }; for (const brush of enabledBrushes) { if (brush.volume.mode === "none") { staticColliders.push(buildRuntimeCollider(brush)); continue; } if (brush.kind !== "box") { staticColliders.push(buildRuntimeCollider(brush)); continue; } if (brush.volume.mode === "fog") { volumes.fog.push(buildRuntimeFogVolume(brush)); continue; } if (brush.volume.mode === "light") { volumes.light.push(buildRuntimeLightVolume(brush)); continue; } volumes.water.push(buildRuntimeWaterVolume(brush)); } for (const terrain of enabledTerrains) { const terrainCollider = buildRuntimeTerrainCollider(terrain); if (terrainCollider !== null) { staticColliders.push(terrainCollider); } } const enabledModelInstances = getModelInstances( document.modelInstances ).filter((modelInstance) => modelInstance.enabled); const modelInstances = enabledModelInstances.map(buildRuntimeModelInstance); const paths = getScenePaths(document.paths) .filter((path) => path.enabled) .map(buildRuntimePath); const collections = buildRuntimeSceneCollections( document, options.runtimeClock ?? null, paths ); const control = buildRuntimeControlSurface( document, collections, modelInstances ); const enabledBrushIds = new Set(enabledBrushes.map((brush) => brush.id)); const enabledModelInstanceIds = new Set( enabledModelInstances.map((modelInstance) => modelInstance.id) ); const enabledEntityIds = new Set( getEntityInstances(document.entities) .filter((entity) => entity.enabled) .map((entity) => entity.id) ); const interactionLinks = getInteractionLinks(document.interactionLinks) .filter((link) => { if (!enabledEntityIds.has(link.sourceEntityId)) { return false; } switch (link.action.type) { case "teleportPlayer": return enabledEntityIds.has(link.action.targetEntityId); case "toggleVisibility": return enabledBrushIds.has(link.action.targetBrushId); case "playAnimation": case "stopAnimation": return enabledModelInstanceIds.has(link.action.targetModelInstanceId); case "playSound": case "stopSound": return enabledEntityIds.has(link.action.targetSoundEmitterId); case "runSequence": return getInteractionLinkImpulseSteps(link, document.sequences).length > 0; case "control": switch (link.action.effect.target.kind) { case "entity": case "interaction": return enabledEntityIds.has(link.action.effect.target.entityId); case "modelInstance": return enabledModelInstanceIds.has( link.action.effect.target.modelInstanceId ); } } }) .map((link) => cloneInteractionLink(link)); const playerCollider = buildRuntimePlayerShape(playerStartEntity); const playerMovement = buildRuntimePlayerMovement( playerStartEntity?.movementTemplate ); const playerInputBindings = createPlayerStartInputBindings( playerStartEntity?.inputBindings ); const colliders = [...staticColliders]; for (const npc of collections.entities.npcs) { const collider = buildRuntimeNpcCollider(npc); if (collider !== null) { colliders.push(collider); } } for (const modelInstance of enabledModelInstances) { const asset = document.assets[modelInstance.assetId]; if (asset === undefined || asset.kind !== "model") { continue; } const generatedCollider = buildGeneratedModelCollider( modelInstance, asset, options.loadedModelAssets?.[modelInstance.assetId] ); if (generatedCollider !== null) { staticColliders.push(generatedCollider); colliders.push(generatedCollider); } } const combinedSceneBounds = combineSceneBounds( combineColliderBounds(colliders), terrains ); const playerStart = playerStartEntity === null ? null : { entityId: playerStartEntity.id, position: cloneVec3(playerStartEntity.position), yawDegrees: playerStartEntity.yawDegrees, navigationMode, interactionReachMeters: playerStartEntity.interactionReachMeters, interactionAngleDegrees: playerStartEntity.interactionAngleDegrees, allowLookInputTargetSwitch: playerStartEntity.allowLookInputTargetSwitch, targetButtonCyclesActiveTarget: playerStartEntity.targetButtonCyclesActiveTarget, movement: cloneRuntimePlayerMovement(playerMovement), inputBindings: clonePlayerStartInputBindings(playerInputBindings), collider: playerCollider }; return { time: cloneProjectTimeSettings(document.time), scheduler: createRuntimeProjectSchedulerState({ document: cloneProjectScheduler(document.scheduler), resolved: collections.scheduler }), sequences: cloneProjectSequenceLibrary(document.sequences), world: cloneWorldSettings(document.world), control, localLights: collections.localLights, brushes, terrains, volumes, staticColliders, colliders, sceneBounds: combinedSceneBounds, modelInstances, paths, npcDefinitions: collections.npcDefinitions, entities: collections.entities, interactionLinks, playerStart, playerCollider, playerMovement, playerInputBindings, navigationMode, spawn: resolveRuntimeSpawn( playerStart, collections.entities.sceneEntries, combinedSceneBounds, options.sceneEntryId ) }; }