2026-04-04 07:51:38 +02:00
|
|
|
import type { LoadedModelAsset } from "../assets/gltf-model-import";
|
2026-03-31 03:04:15 +02:00
|
|
|
import type { Vec3 } from "../core/vector";
|
2026-03-31 17:32:26 +02:00
|
|
|
import { getModelInstances } from "../assets/model-instances";
|
2026-04-06 08:20:15 +02:00
|
|
|
import {
|
|
|
|
|
cloneBoxBrushGeometry,
|
|
|
|
|
cloneBoxBrushVolumeSettings,
|
|
|
|
|
cloneFaceUvState,
|
|
|
|
|
type BoxBrush,
|
|
|
|
|
type BoxBrushGeometry,
|
|
|
|
|
type BoxBrushVolumeSettings,
|
|
|
|
|
type BoxFaceId,
|
|
|
|
|
type FaceUvState
|
|
|
|
|
} from "../document/brushes";
|
2026-04-02 20:58:50 +02:00
|
|
|
import type { SceneDocument } from "../document/scene-document";
|
|
|
|
|
import { cloneWorldSettings, type WorldSettings } from "../document/world-settings";
|
2026-04-11 11:14:35 +02:00
|
|
|
import {
|
2026-04-11 12:12:48 +02:00
|
|
|
clonePlayerStartInputBindings,
|
2026-04-11 17:59:34 +02:00
|
|
|
createPlayerStartMovementTemplate,
|
2026-04-11 12:12:48 +02:00
|
|
|
createPlayerStartInputBindings,
|
2026-04-11 11:14:35 +02:00
|
|
|
getEntityInstances,
|
|
|
|
|
getPrimaryPlayerStartEntity,
|
2026-04-11 12:12:48 +02:00
|
|
|
type EntityInstance,
|
2026-04-11 17:59:34 +02:00
|
|
|
type PlayerStartInputBindings,
|
2026-04-11 20:09:06 +02:00
|
|
|
type PlayerStartJumpSettings,
|
2026-04-11 17:59:34 +02:00
|
|
|
type PlayerStartMovementCapabilities,
|
2026-04-11 20:09:06 +02:00
|
|
|
type PlayerStartCrouchSettings,
|
|
|
|
|
type PlayerStartSprintSettings,
|
2026-04-11 17:59:34 +02:00
|
|
|
type PlayerStartMovementTemplate
|
2026-04-11 11:14:35 +02:00
|
|
|
} from "../entities/entity-instances";
|
2026-03-31 03:04:15 +02:00
|
|
|
import { getBoxBrushBounds } from "../geometry/box-brush";
|
2026-04-05 02:29:54 +02:00
|
|
|
import { buildBoxBrushDerivedMeshData } from "../geometry/box-brush-mesh";
|
2026-04-04 07:51:38 +02:00
|
|
|
import { buildGeneratedModelCollider, type GeneratedColliderBounds, type GeneratedModelCollider } from "../geometry/model-instance-collider-generation";
|
2026-03-31 06:16:59 +02:00
|
|
|
import { cloneInteractionLink, getInteractionLinks, type InteractionLink } from "../interactions/interaction-links";
|
2026-03-31 03:04:15 +02:00
|
|
|
import { cloneMaterialDef, type MaterialDef } from "../materials/starter-material-library";
|
2026-03-31 03:41:13 +02:00
|
|
|
import { assertRuntimeSceneBuildable } from "./runtime-scene-validation";
|
2026-04-04 15:53:00 +02:00
|
|
|
import { FIRST_PERSON_PLAYER_SHAPE, type FirstPersonPlayerShape } from "./player-collision";
|
2026-03-31 03:04:15 +02:00
|
|
|
|
2026-04-11 11:14:35 +02:00
|
|
|
export type RuntimeNavigationMode = "firstPerson" | "thirdPerson";
|
2026-03-31 03:04:15 +02:00
|
|
|
|
|
|
|
|
export interface RuntimeBrushFace {
|
|
|
|
|
materialId: string | null;
|
|
|
|
|
material: MaterialDef | null;
|
|
|
|
|
uv: FaceUvState;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface RuntimeBoxBrushInstance {
|
|
|
|
|
id: string;
|
|
|
|
|
kind: "box";
|
|
|
|
|
center: Vec3;
|
2026-04-04 19:27:06 +02:00
|
|
|
rotationDegrees: Vec3;
|
2026-03-31 03:04:15 +02:00
|
|
|
size: Vec3;
|
2026-04-05 02:29:54 +02:00
|
|
|
geometry: BoxBrushGeometry;
|
2026-03-31 03:04:15 +02:00
|
|
|
faces: Record<BoxFaceId, RuntimeBrushFace>;
|
2026-04-06 08:20:15 +02:00
|
|
|
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 RuntimeBoxVolumeCollection {
|
|
|
|
|
fog: RuntimeFogVolume[];
|
|
|
|
|
water: RuntimeWaterVolume[];
|
2026-03-31 03:04:15 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-05 02:29:54 +02:00
|
|
|
export interface RuntimeBrushTriMeshCollider {
|
|
|
|
|
kind: "trimesh";
|
2026-04-04 07:51:38 +02:00
|
|
|
source: "brush";
|
2026-03-31 03:04:15 +02:00
|
|
|
brushId: string;
|
2026-04-04 19:27:06 +02:00
|
|
|
center: Vec3;
|
|
|
|
|
rotationDegrees: Vec3;
|
2026-04-05 02:29:54 +02:00
|
|
|
vertices: Float32Array;
|
|
|
|
|
indices: Uint32Array;
|
2026-04-04 19:27:06 +02:00
|
|
|
worldBounds: {
|
|
|
|
|
min: Vec3;
|
|
|
|
|
max: Vec3;
|
|
|
|
|
};
|
2026-03-31 03:04:15 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-05 02:29:54 +02:00
|
|
|
export type RuntimeSceneCollider = RuntimeBrushTriMeshCollider | GeneratedModelCollider;
|
2026-04-04 07:51:38 +02:00
|
|
|
|
2026-03-31 03:04:15 +02:00
|
|
|
export interface RuntimeSceneBounds {
|
|
|
|
|
min: Vec3;
|
|
|
|
|
max: Vec3;
|
|
|
|
|
center: Vec3;
|
|
|
|
|
size: Vec3;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface RuntimePlayerStart {
|
|
|
|
|
entityId: string;
|
|
|
|
|
position: Vec3;
|
|
|
|
|
yawDegrees: number;
|
2026-04-11 11:14:35 +02:00
|
|
|
navigationMode: RuntimeNavigationMode;
|
2026-04-11 17:59:44 +02:00
|
|
|
movement: RuntimePlayerMovement;
|
2026-04-11 12:12:48 +02:00
|
|
|
inputBindings: PlayerStartInputBindings;
|
2026-04-04 15:52:49 +02:00
|
|
|
collider: FirstPersonPlayerShape;
|
2026-03-31 03:04:15 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-11 17:59:44 +02:00
|
|
|
export interface RuntimePlayerMovement {
|
|
|
|
|
templateKind: PlayerStartMovementTemplate["kind"];
|
|
|
|
|
moveSpeed: number;
|
2026-04-11 20:30:39 +02:00
|
|
|
maxSpeed: number;
|
2026-04-11 17:59:44 +02:00
|
|
|
capabilities: PlayerStartMovementCapabilities;
|
2026-04-11 20:09:06 +02:00
|
|
|
jump: PlayerStartJumpSettings;
|
|
|
|
|
sprint: PlayerStartSprintSettings;
|
|
|
|
|
crouch: PlayerStartCrouchSettings;
|
2026-04-11 17:59:44 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-11 04:34:01 +02:00
|
|
|
export interface RuntimeSceneEntry {
|
|
|
|
|
entityId: string;
|
|
|
|
|
position: Vec3;
|
|
|
|
|
yawDegrees: number;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 05:52:01 +02:00
|
|
|
export interface RuntimeSoundEmitter {
|
|
|
|
|
entityId: string;
|
|
|
|
|
position: Vec3;
|
2026-04-02 19:38:16 +02:00
|
|
|
audioAssetId: string | null;
|
|
|
|
|
volume: number;
|
|
|
|
|
refDistance: number;
|
|
|
|
|
maxDistance: number;
|
2026-03-31 05:52:01 +02:00
|
|
|
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;
|
|
|
|
|
enabled: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 04:34:01 +02:00
|
|
|
export interface RuntimeSceneExit {
|
|
|
|
|
entityId: string;
|
|
|
|
|
position: Vec3;
|
|
|
|
|
radius: number;
|
|
|
|
|
prompt: string;
|
|
|
|
|
enabled: boolean;
|
|
|
|
|
targetSceneId: string;
|
|
|
|
|
targetEntryEntityId: string;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 20:04:49 +02:00
|
|
|
export interface RuntimePointLight {
|
|
|
|
|
entityId: string;
|
|
|
|
|
position: Vec3;
|
|
|
|
|
colorHex: string;
|
|
|
|
|
intensity: number;
|
|
|
|
|
distance: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface RuntimeSpotLight {
|
|
|
|
|
entityId: string;
|
|
|
|
|
position: Vec3;
|
|
|
|
|
direction: Vec3;
|
|
|
|
|
colorHex: string;
|
|
|
|
|
intensity: number;
|
|
|
|
|
distance: number;
|
|
|
|
|
angleDegrees: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface RuntimeLocalLightCollection {
|
|
|
|
|
pointLights: RuntimePointLight[];
|
|
|
|
|
spotLights: RuntimeSpotLight[];
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 17:32:26 +02:00
|
|
|
export interface RuntimeModelInstance {
|
|
|
|
|
instanceId: string;
|
|
|
|
|
assetId: string;
|
|
|
|
|
name?: string;
|
|
|
|
|
position: Vec3;
|
|
|
|
|
rotationDegrees: Vec3;
|
|
|
|
|
scale: Vec3;
|
2026-04-01 00:01:26 +02:00
|
|
|
animationClipName?: string;
|
|
|
|
|
animationAutoplay?: boolean;
|
2026-03-31 17:32:26 +02:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 05:52:01 +02:00
|
|
|
export interface RuntimeEntityCollection {
|
|
|
|
|
playerStarts: RuntimePlayerStart[];
|
2026-04-11 04:34:01 +02:00
|
|
|
sceneEntries: RuntimeSceneEntry[];
|
2026-03-31 05:52:01 +02:00
|
|
|
soundEmitters: RuntimeSoundEmitter[];
|
|
|
|
|
triggerVolumes: RuntimeTriggerVolume[];
|
|
|
|
|
teleportTargets: RuntimeTeleportTarget[];
|
|
|
|
|
interactables: RuntimeInteractable[];
|
2026-04-11 04:34:01 +02:00
|
|
|
sceneExits: RuntimeSceneExit[];
|
2026-03-31 05:52:01 +02:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 03:04:15 +02:00
|
|
|
export interface RuntimeSpawnPoint {
|
2026-04-11 04:34:01 +02:00
|
|
|
source: "playerStart" | "sceneEntry" | "fallback";
|
2026-03-31 03:04:15 +02:00
|
|
|
entityId: string | null;
|
|
|
|
|
position: Vec3;
|
|
|
|
|
yawDegrees: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface RuntimeSceneDefinition {
|
|
|
|
|
world: WorldSettings;
|
2026-03-31 20:04:49 +02:00
|
|
|
localLights: RuntimeLocalLightCollection;
|
2026-03-31 03:04:15 +02:00
|
|
|
brushes: RuntimeBoxBrushInstance[];
|
2026-04-06 08:20:15 +02:00
|
|
|
volumes: RuntimeBoxVolumeCollection;
|
2026-04-04 07:51:38 +02:00
|
|
|
colliders: RuntimeSceneCollider[];
|
2026-03-31 03:04:15 +02:00
|
|
|
sceneBounds: RuntimeSceneBounds | null;
|
2026-03-31 17:32:26 +02:00
|
|
|
modelInstances: RuntimeModelInstance[];
|
2026-03-31 05:52:01 +02:00
|
|
|
entities: RuntimeEntityCollection;
|
2026-03-31 06:16:59 +02:00
|
|
|
interactionLinks: InteractionLink[];
|
2026-03-31 03:04:15 +02:00
|
|
|
playerStart: RuntimePlayerStart | null;
|
2026-04-04 15:52:49 +02:00
|
|
|
playerCollider: FirstPersonPlayerShape;
|
2026-04-11 17:59:44 +02:00
|
|
|
playerMovement: RuntimePlayerMovement;
|
2026-04-11 12:12:48 +02:00
|
|
|
playerInputBindings: PlayerStartInputBindings;
|
2026-04-11 11:14:35 +02:00
|
|
|
navigationMode: RuntimeNavigationMode;
|
2026-03-31 03:04:15 +02:00
|
|
|
spawn: RuntimeSpawnPoint;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 04:34:01 +02:00
|
|
|
export interface BuildRuntimeSceneOptions {
|
2026-03-31 03:41:13 +02:00
|
|
|
navigationMode?: RuntimeNavigationMode;
|
2026-04-04 07:51:38 +02:00
|
|
|
loadedModelAssets?: Record<string, LoadedModelAsset>;
|
2026-04-11 04:34:01 +02:00
|
|
|
sceneEntryId?: string | null;
|
2026-03-31 03:41:13 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-11 11:14:35 +02:00
|
|
|
export function resolveRuntimeNavigationMode(
|
|
|
|
|
playerStartEntity: ReturnType<typeof getPrimaryPlayerStartEntity>,
|
|
|
|
|
authoredOverride?: RuntimeNavigationMode
|
|
|
|
|
): RuntimeNavigationMode {
|
|
|
|
|
if (authoredOverride !== undefined) {
|
|
|
|
|
return authoredOverride;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return playerStartEntity?.navigationMode ?? "thirdPerson";
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 03:04:15 +02:00
|
|
|
function cloneVec3(vector: Vec3): Vec3 {
|
|
|
|
|
return {
|
|
|
|
|
x: vector.x,
|
|
|
|
|
y: vector.y,
|
|
|
|
|
z: vector.z
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 18:00:06 +02:00
|
|
|
function clonePlayerStartMovementCapabilities(
|
|
|
|
|
capabilities: PlayerStartMovementCapabilities
|
|
|
|
|
): PlayerStartMovementCapabilities {
|
|
|
|
|
return {
|
|
|
|
|
jump: capabilities.jump,
|
|
|
|
|
sprint: capabilities.sprint,
|
|
|
|
|
crouch: capabilities.crouch
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 20:09:06 +02:00
|
|
|
function clonePlayerStartJumpSettings(
|
|
|
|
|
jump: PlayerStartJumpSettings
|
|
|
|
|
): PlayerStartJumpSettings {
|
|
|
|
|
return {
|
|
|
|
|
speed: jump.speed,
|
|
|
|
|
bufferMs: jump.bufferMs,
|
|
|
|
|
coyoteTimeMs: jump.coyoteTimeMs,
|
|
|
|
|
variableHeight: jump.variableHeight,
|
2026-04-11 20:30:39 +02:00
|
|
|
maxHoldMs: jump.maxHoldMs,
|
|
|
|
|
bunnyHop: jump.bunnyHop,
|
|
|
|
|
bunnyHopBoost: jump.bunnyHopBoost
|
2026-04-11 20:09:06 +02:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clonePlayerStartSprintSettings(
|
|
|
|
|
sprint: PlayerStartSprintSettings
|
|
|
|
|
): PlayerStartSprintSettings {
|
|
|
|
|
return {
|
|
|
|
|
speedMultiplier: sprint.speedMultiplier
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clonePlayerStartCrouchSettings(
|
|
|
|
|
crouch: PlayerStartCrouchSettings
|
|
|
|
|
): PlayerStartCrouchSettings {
|
|
|
|
|
return {
|
|
|
|
|
speedMultiplier: crouch.speedMultiplier
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 18:00:06 +02:00
|
|
|
function cloneRuntimePlayerMovement(
|
|
|
|
|
movement: RuntimePlayerMovement
|
|
|
|
|
): RuntimePlayerMovement {
|
|
|
|
|
return {
|
|
|
|
|
templateKind: movement.templateKind,
|
|
|
|
|
moveSpeed: movement.moveSpeed,
|
2026-04-11 20:30:39 +02:00
|
|
|
maxSpeed: movement.maxSpeed,
|
2026-04-11 20:09:06 +02:00
|
|
|
capabilities: clonePlayerStartMovementCapabilities(movement.capabilities),
|
|
|
|
|
jump: clonePlayerStartJumpSettings(movement.jump),
|
|
|
|
|
sprint: clonePlayerStartSprintSettings(movement.sprint),
|
|
|
|
|
crouch: clonePlayerStartCrouchSettings(movement.crouch)
|
2026-04-11 18:00:06 +02:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildRuntimePlayerMovement(
|
|
|
|
|
template: PlayerStartMovementTemplate | undefined
|
|
|
|
|
): RuntimePlayerMovement {
|
|
|
|
|
const resolvedTemplate = createPlayerStartMovementTemplate(template);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
templateKind: resolvedTemplate.kind,
|
|
|
|
|
moveSpeed: resolvedTemplate.moveSpeed,
|
2026-04-11 20:30:39 +02:00
|
|
|
maxSpeed: resolvedTemplate.maxSpeed,
|
2026-04-11 18:00:06 +02:00
|
|
|
capabilities: clonePlayerStartMovementCapabilities(
|
|
|
|
|
resolvedTemplate.capabilities
|
2026-04-11 20:09:06 +02:00
|
|
|
),
|
|
|
|
|
jump: clonePlayerStartJumpSettings(resolvedTemplate.jump),
|
|
|
|
|
sprint: clonePlayerStartSprintSettings(resolvedTemplate.sprint),
|
|
|
|
|
crouch: clonePlayerStartCrouchSettings(resolvedTemplate.crouch)
|
2026-04-11 18:00:06 +02:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 03:04:44 +02:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 03:04:15 +02:00
|
|
|
function buildRuntimeBrush(brush: BoxBrush, document: SceneDocument): RuntimeBoxBrushInstance {
|
|
|
|
|
return {
|
|
|
|
|
id: brush.id,
|
|
|
|
|
kind: "box",
|
|
|
|
|
center: cloneVec3(brush.center),
|
2026-04-04 19:27:06 +02:00
|
|
|
rotationDegrees: cloneVec3(brush.rotationDegrees),
|
2026-03-31 03:04:15 +02:00
|
|
|
size: cloneVec3(brush.size),
|
2026-04-05 02:29:54 +02:00
|
|
|
geometry: cloneBoxBrushGeometry(brush.geometry),
|
2026-04-06 08:20:15 +02:00
|
|
|
volume: cloneBoxBrushVolumeSettings(brush.volume),
|
2026-03-31 03:04:15 +02:00
|
|
|
faces: {
|
|
|
|
|
posX: {
|
|
|
|
|
materialId: brush.faces.posX.materialId,
|
2026-03-31 03:04:44 +02:00
|
|
|
material: resolveRuntimeMaterial(document, brush.faces.posX.materialId),
|
2026-03-31 03:04:15 +02:00
|
|
|
uv: cloneFaceUvState(brush.faces.posX.uv)
|
|
|
|
|
},
|
|
|
|
|
negX: {
|
|
|
|
|
materialId: brush.faces.negX.materialId,
|
2026-03-31 03:04:44 +02:00
|
|
|
material: resolveRuntimeMaterial(document, brush.faces.negX.materialId),
|
2026-03-31 03:04:15 +02:00
|
|
|
uv: cloneFaceUvState(brush.faces.negX.uv)
|
|
|
|
|
},
|
|
|
|
|
posY: {
|
|
|
|
|
materialId: brush.faces.posY.materialId,
|
2026-03-31 03:04:44 +02:00
|
|
|
material: resolveRuntimeMaterial(document, brush.faces.posY.materialId),
|
2026-03-31 03:04:15 +02:00
|
|
|
uv: cloneFaceUvState(brush.faces.posY.uv)
|
|
|
|
|
},
|
|
|
|
|
negY: {
|
|
|
|
|
materialId: brush.faces.negY.materialId,
|
2026-03-31 03:04:44 +02:00
|
|
|
material: resolveRuntimeMaterial(document, brush.faces.negY.materialId),
|
2026-03-31 03:04:15 +02:00
|
|
|
uv: cloneFaceUvState(brush.faces.negY.uv)
|
|
|
|
|
},
|
|
|
|
|
posZ: {
|
|
|
|
|
materialId: brush.faces.posZ.materialId,
|
2026-03-31 03:04:44 +02:00
|
|
|
material: resolveRuntimeMaterial(document, brush.faces.posZ.materialId),
|
2026-03-31 03:04:15 +02:00
|
|
|
uv: cloneFaceUvState(brush.faces.posZ.uv)
|
|
|
|
|
},
|
|
|
|
|
negZ: {
|
|
|
|
|
materialId: brush.faces.negZ.materialId,
|
2026-03-31 03:04:44 +02:00
|
|
|
material: resolveRuntimeMaterial(document, brush.faces.negZ.materialId),
|
2026-03-31 03:04:15 +02:00
|
|
|
uv: cloneFaceUvState(brush.faces.negZ.uv)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 08:20:15 +02:00
|
|
|
function buildRuntimeFogVolume(brush: BoxBrush): 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: BoxBrush): 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
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 02:29:54 +02:00
|
|
|
function buildRuntimeCollider(brush: BoxBrush): RuntimeBrushTriMeshCollider {
|
2026-03-31 03:04:15 +02:00
|
|
|
const bounds = getBoxBrushBounds(brush);
|
2026-04-05 02:29:54 +02:00
|
|
|
const derivedMesh = buildBoxBrushDerivedMeshData(brush);
|
2026-03-31 03:04:15 +02:00
|
|
|
|
|
|
|
|
return {
|
2026-04-05 02:29:54 +02:00
|
|
|
kind: "trimesh",
|
2026-04-04 07:51:38 +02:00
|
|
|
source: "brush",
|
2026-03-31 03:04:15 +02:00
|
|
|
brushId: brush.id,
|
2026-04-04 19:27:06 +02:00
|
|
|
center: cloneVec3(brush.center),
|
|
|
|
|
rotationDegrees: cloneVec3(brush.rotationDegrees),
|
2026-04-05 02:29:54 +02:00
|
|
|
vertices: derivedMesh.colliderVertices,
|
|
|
|
|
indices: derivedMesh.colliderIndices,
|
2026-04-04 19:27:06 +02:00
|
|
|
worldBounds: {
|
|
|
|
|
min: cloneVec3(bounds.min),
|
|
|
|
|
max: cloneVec3(bounds.max)
|
|
|
|
|
}
|
2026-03-31 03:04:15 +02:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 17:32:26 +02:00
|
|
|
function buildRuntimeModelInstance(modelInstance: SceneDocument["modelInstances"][string]): RuntimeModelInstance {
|
|
|
|
|
return {
|
|
|
|
|
instanceId: modelInstance.id,
|
|
|
|
|
assetId: modelInstance.assetId,
|
|
|
|
|
name: modelInstance.name,
|
|
|
|
|
position: cloneVec3(modelInstance.position),
|
|
|
|
|
rotationDegrees: cloneVec3(modelInstance.rotationDegrees),
|
2026-04-01 00:02:23 +02:00
|
|
|
scale: cloneVec3(modelInstance.scale),
|
|
|
|
|
animationClipName: modelInstance.animationClipName,
|
|
|
|
|
animationAutoplay: modelInstance.animationAutoplay
|
2026-03-31 17:32:26 +02:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 07:51:38 +02:00
|
|
|
function getColliderBounds(collider: RuntimeSceneCollider): GeneratedColliderBounds {
|
|
|
|
|
if (collider.source === "brush") {
|
|
|
|
|
return {
|
2026-04-04 19:27:06 +02:00
|
|
|
min: cloneVec3(collider.worldBounds.min),
|
|
|
|
|
max: cloneVec3(collider.worldBounds.max)
|
2026-04-04 07:51:38 +02:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
min: cloneVec3(collider.worldBounds.min),
|
|
|
|
|
max: cloneVec3(collider.worldBounds.max)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function combineColliderBounds(colliders: RuntimeSceneCollider[]): RuntimeSceneBounds | null {
|
2026-03-31 03:04:15 +02:00
|
|
|
if (colliders.length === 0) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 07:51:38 +02:00
|
|
|
const firstBounds = getColliderBounds(colliders[0]);
|
|
|
|
|
const min = cloneVec3(firstBounds.min);
|
|
|
|
|
const max = cloneVec3(firstBounds.max);
|
2026-03-31 03:04:15 +02:00
|
|
|
|
|
|
|
|
for (const collider of colliders.slice(1)) {
|
2026-04-04 07:51:38 +02:00
|
|
|
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);
|
2026-03-31 03:04:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 20:04:49 +02:00
|
|
|
interface RuntimeSceneCollections {
|
|
|
|
|
entities: RuntimeEntityCollection;
|
|
|
|
|
localLights: RuntimeLocalLightCollection;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildRuntimeSceneCollections(document: SceneDocument): RuntimeSceneCollections {
|
2026-03-31 05:52:01 +02:00
|
|
|
const runtimeEntities: RuntimeEntityCollection = {
|
|
|
|
|
playerStarts: [],
|
2026-04-11 04:34:01 +02:00
|
|
|
sceneEntries: [],
|
2026-03-31 05:52:01 +02:00
|
|
|
soundEmitters: [],
|
|
|
|
|
triggerVolumes: [],
|
|
|
|
|
teleportTargets: [],
|
2026-04-11 04:34:01 +02:00
|
|
|
interactables: [],
|
|
|
|
|
sceneExits: []
|
2026-03-31 05:52:01 +02:00
|
|
|
};
|
2026-03-31 20:04:49 +02:00
|
|
|
const localLights: RuntimeLocalLightCollection = {
|
|
|
|
|
pointLights: [],
|
|
|
|
|
spotLights: []
|
|
|
|
|
};
|
2026-03-31 05:52:01 +02:00
|
|
|
|
|
|
|
|
for (const entity of getEntityInstances(document.entities)) {
|
|
|
|
|
switch (entity.kind) {
|
2026-03-31 20:04:49 +02:00
|
|
|
case "pointLight":
|
|
|
|
|
localLights.pointLights.push({
|
|
|
|
|
entityId: entity.id,
|
|
|
|
|
position: cloneVec3(entity.position),
|
|
|
|
|
colorHex: entity.colorHex,
|
|
|
|
|
intensity: entity.intensity,
|
|
|
|
|
distance: entity.distance
|
|
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
case "spotLight":
|
|
|
|
|
localLights.spotLights.push({
|
|
|
|
|
entityId: entity.id,
|
|
|
|
|
position: cloneVec3(entity.position),
|
|
|
|
|
direction: cloneVec3(entity.direction),
|
|
|
|
|
colorHex: entity.colorHex,
|
|
|
|
|
intensity: entity.intensity,
|
|
|
|
|
distance: entity.distance,
|
|
|
|
|
angleDegrees: entity.angleDegrees
|
|
|
|
|
});
|
|
|
|
|
break;
|
2026-03-31 05:52:01 +02:00
|
|
|
case "playerStart":
|
|
|
|
|
runtimeEntities.playerStarts.push({
|
|
|
|
|
entityId: entity.id,
|
|
|
|
|
position: cloneVec3(entity.position),
|
2026-04-04 15:53:00 +02:00
|
|
|
yawDegrees: entity.yawDegrees,
|
2026-04-11 11:14:43 +02:00
|
|
|
navigationMode: entity.navigationMode,
|
2026-04-11 18:05:08 +02:00
|
|
|
movement: buildRuntimePlayerMovement(entity.movementTemplate),
|
2026-04-11 12:12:48 +02:00
|
|
|
inputBindings: clonePlayerStartInputBindings(entity.inputBindings),
|
2026-04-04 15:53:00 +02:00
|
|
|
collider: buildRuntimePlayerShape(entity)
|
2026-03-31 05:52:01 +02:00
|
|
|
});
|
|
|
|
|
break;
|
2026-04-11 04:34:01 +02:00
|
|
|
case "sceneEntry":
|
|
|
|
|
runtimeEntities.sceneEntries.push({
|
|
|
|
|
entityId: entity.id,
|
|
|
|
|
position: cloneVec3(entity.position),
|
|
|
|
|
yawDegrees: entity.yawDegrees
|
|
|
|
|
});
|
|
|
|
|
break;
|
2026-03-31 05:52:01 +02:00
|
|
|
case "soundEmitter":
|
|
|
|
|
runtimeEntities.soundEmitters.push({
|
|
|
|
|
entityId: entity.id,
|
|
|
|
|
position: cloneVec3(entity.position),
|
2026-04-02 19:38:16 +02:00
|
|
|
audioAssetId: entity.audioAssetId,
|
|
|
|
|
volume: entity.volume,
|
|
|
|
|
refDistance: entity.refDistance,
|
|
|
|
|
maxDistance: entity.maxDistance,
|
2026-03-31 05:52:01 +02:00
|
|
|
autoplay: entity.autoplay,
|
|
|
|
|
loop: entity.loop
|
|
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
case "triggerVolume":
|
|
|
|
|
runtimeEntities.triggerVolumes.push({
|
|
|
|
|
entityId: entity.id,
|
|
|
|
|
position: cloneVec3(entity.position),
|
|
|
|
|
size: cloneVec3(entity.size),
|
2026-04-01 04:20:55 +02:00
|
|
|
// 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"
|
|
|
|
|
)
|
2026-03-31 05:52:01 +02:00
|
|
|
});
|
|
|
|
|
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,
|
|
|
|
|
enabled: entity.enabled
|
|
|
|
|
});
|
|
|
|
|
break;
|
2026-04-11 04:34:01 +02:00
|
|
|
case "sceneExit":
|
|
|
|
|
runtimeEntities.sceneExits.push({
|
|
|
|
|
entityId: entity.id,
|
|
|
|
|
position: cloneVec3(entity.position),
|
|
|
|
|
radius: entity.radius,
|
|
|
|
|
prompt: entity.prompt,
|
|
|
|
|
enabled: entity.enabled,
|
|
|
|
|
targetSceneId: entity.targetSceneId,
|
|
|
|
|
targetEntryEntityId: entity.targetEntryEntityId
|
|
|
|
|
});
|
|
|
|
|
break;
|
2026-03-31 05:52:01 +02:00
|
|
|
default:
|
|
|
|
|
assertNever(entity);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 20:04:49 +02:00
|
|
|
return {
|
|
|
|
|
entities: runtimeEntities,
|
|
|
|
|
localLights
|
|
|
|
|
};
|
2026-03-31 05:52:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function assertNever(value: never): never {
|
|
|
|
|
throw new Error(`Unsupported runtime entity: ${String((value as EntityInstance).kind)}`);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 15:52:49 +02:00
|
|
|
function buildRuntimePlayerShape(
|
|
|
|
|
playerStartEntity: ReturnType<typeof getPrimaryPlayerStartEntity>
|
|
|
|
|
): FirstPersonPlayerShape {
|
|
|
|
|
if (playerStartEntity === null) {
|
|
|
|
|
return FIRST_PERSON_PLAYER_SHAPE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (playerStartEntity.collider.mode) {
|
|
|
|
|
case "capsule":
|
|
|
|
|
return {
|
|
|
|
|
mode: "capsule",
|
|
|
|
|
radius: playerStartEntity.collider.capsuleRadius,
|
|
|
|
|
height: playerStartEntity.collider.capsuleHeight,
|
|
|
|
|
eyeHeight: playerStartEntity.collider.eyeHeight
|
|
|
|
|
};
|
|
|
|
|
case "box":
|
|
|
|
|
return {
|
|
|
|
|
mode: "box",
|
|
|
|
|
size: cloneVec3(playerStartEntity.collider.boxSize),
|
|
|
|
|
eyeHeight: playerStartEntity.collider.eyeHeight
|
|
|
|
|
};
|
|
|
|
|
case "none":
|
|
|
|
|
return {
|
|
|
|
|
mode: "none",
|
|
|
|
|
eyeHeight: playerStartEntity.collider.eyeHeight
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 04:34:01 +02:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 03:41:13 +02:00
|
|
|
export function buildRuntimeSceneFromDocument(document: SceneDocument, options: BuildRuntimeSceneOptions = {}): RuntimeSceneDefinition {
|
2026-04-11 11:14:35 +02:00
|
|
|
const playerStartEntity = getPrimaryPlayerStartEntity(document.entities);
|
|
|
|
|
const navigationMode = resolveRuntimeNavigationMode(
|
|
|
|
|
playerStartEntity,
|
|
|
|
|
options.navigationMode
|
|
|
|
|
);
|
|
|
|
|
|
2026-04-04 07:51:38 +02:00
|
|
|
assertRuntimeSceneBuildable(document, {
|
2026-04-11 11:14:35 +02:00
|
|
|
navigationMode,
|
2026-04-04 07:51:38 +02:00
|
|
|
loadedModelAssets: options.loadedModelAssets
|
|
|
|
|
});
|
2026-03-31 03:41:13 +02:00
|
|
|
|
2026-03-31 03:04:15 +02:00
|
|
|
const brushes = Object.values(document.brushes).map((brush) => buildRuntimeBrush(brush, document));
|
2026-04-06 08:20:15 +02:00
|
|
|
const colliders: RuntimeSceneCollider[] = [];
|
|
|
|
|
const volumes: RuntimeBoxVolumeCollection = {
|
|
|
|
|
fog: [],
|
|
|
|
|
water: []
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (const brush of Object.values(document.brushes)) {
|
|
|
|
|
if (brush.volume.mode === "none") {
|
|
|
|
|
colliders.push(buildRuntimeCollider(brush));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (brush.volume.mode === "fog") {
|
|
|
|
|
volumes.fog.push(buildRuntimeFogVolume(brush));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
volumes.water.push(buildRuntimeWaterVolume(brush));
|
|
|
|
|
}
|
2026-03-31 17:32:26 +02:00
|
|
|
const modelInstances = getModelInstances(document.modelInstances).map(buildRuntimeModelInstance);
|
2026-03-31 20:04:49 +02:00
|
|
|
const collections = buildRuntimeSceneCollections(document);
|
2026-03-31 06:16:59 +02:00
|
|
|
const interactionLinks = getInteractionLinks(document.interactionLinks).map((link) => cloneInteractionLink(link));
|
2026-04-04 15:52:49 +02:00
|
|
|
const playerCollider = buildRuntimePlayerShape(playerStartEntity);
|
2026-04-11 18:05:08 +02:00
|
|
|
const playerMovement = buildRuntimePlayerMovement(
|
|
|
|
|
playerStartEntity?.movementTemplate
|
|
|
|
|
);
|
2026-04-11 12:12:48 +02:00
|
|
|
const playerInputBindings = createPlayerStartInputBindings(
|
|
|
|
|
playerStartEntity?.inputBindings
|
|
|
|
|
);
|
2026-04-04 07:51:38 +02:00
|
|
|
|
|
|
|
|
for (const modelInstance of getModelInstances(document.modelInstances)) {
|
|
|
|
|
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) {
|
|
|
|
|
colliders.push(generatedCollider);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const combinedSceneBounds = combineColliderBounds(colliders);
|
2026-03-31 03:04:15 +02:00
|
|
|
const playerStart =
|
|
|
|
|
playerStartEntity === null
|
|
|
|
|
? null
|
|
|
|
|
: {
|
|
|
|
|
entityId: playerStartEntity.id,
|
|
|
|
|
position: cloneVec3(playerStartEntity.position),
|
2026-04-04 15:52:49 +02:00
|
|
|
yawDegrees: playerStartEntity.yawDegrees,
|
2026-04-11 11:14:35 +02:00
|
|
|
navigationMode,
|
2026-04-11 18:05:08 +02:00
|
|
|
movement: cloneRuntimePlayerMovement(playerMovement),
|
2026-04-11 12:12:48 +02:00
|
|
|
inputBindings: clonePlayerStartInputBindings(playerInputBindings),
|
2026-04-04 15:52:49 +02:00
|
|
|
collider: playerCollider
|
2026-03-31 03:04:15 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return {
|
2026-03-31 05:10:46 +02:00
|
|
|
world: cloneWorldSettings(document.world),
|
2026-03-31 20:04:49 +02:00
|
|
|
localLights: collections.localLights,
|
2026-03-31 03:04:15 +02:00
|
|
|
brushes,
|
2026-04-06 08:20:15 +02:00
|
|
|
volumes,
|
2026-03-31 03:04:15 +02:00
|
|
|
colliders,
|
2026-04-04 07:51:38 +02:00
|
|
|
sceneBounds: combinedSceneBounds,
|
2026-03-31 17:32:26 +02:00
|
|
|
modelInstances,
|
2026-03-31 20:04:49 +02:00
|
|
|
entities: collections.entities,
|
2026-03-31 06:16:59 +02:00
|
|
|
interactionLinks,
|
2026-03-31 03:04:15 +02:00
|
|
|
playerStart,
|
2026-04-04 15:52:49 +02:00
|
|
|
playerCollider,
|
2026-04-11 18:05:08 +02:00
|
|
|
playerMovement,
|
2026-04-11 12:12:48 +02:00
|
|
|
playerInputBindings,
|
2026-04-11 11:14:35 +02:00
|
|
|
navigationMode,
|
2026-04-11 04:34:01 +02:00
|
|
|
spawn: resolveRuntimeSpawn(
|
|
|
|
|
playerStart,
|
|
|
|
|
collections.entities.sceneEntries,
|
|
|
|
|
combinedSceneBounds,
|
|
|
|
|
options.sceneEntryId
|
|
|
|
|
)
|
2026-03-31 03:04:15 +02:00
|
|
|
};
|
|
|
|
|
}
|