2043 lines
56 KiB
TypeScript
2043 lines
56 KiB
TypeScript
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<WhiteboxFaceId, RuntimeBrushFace>;
|
|
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<string, LoadedModelAsset>;
|
|
runtimeClock?: RuntimeClockState;
|
|
sceneEntryId?: string | null;
|
|
}
|
|
|
|
export function resolveRuntimeNavigationMode(
|
|
playerStartEntity: ReturnType<typeof getPrimaryEnabledPlayerStartEntity>,
|
|
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<WhiteboxFaceId, RuntimeBrushFace>
|
|
};
|
|
}
|
|
|
|
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<string, RuntimePath> {
|
|
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<string>();
|
|
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<typeof getPrimaryEnabledPlayerStartEntity>
|
|
): 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
|
|
)
|
|
};
|
|
}
|