From fa705f18bbfb6554c3ddd0de98d720cbee8fc320 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Sat, 11 Apr 2026 04:34:01 +0200 Subject: [PATCH] Add scene entry and exit interfaces and update spawn logic --- src/runtime-three/runtime-scene-build.ts | 96 +++++++++++++++++++++--- 1 file changed, 84 insertions(+), 12 deletions(-) diff --git a/src/runtime-three/runtime-scene-build.ts b/src/runtime-three/runtime-scene-build.ts index 88194bbd..20dc9a2a 100644 --- a/src/runtime-three/runtime-scene-build.ts +++ b/src/runtime-three/runtime-scene-build.ts @@ -96,6 +96,12 @@ export interface RuntimePlayerStart { collider: FirstPersonPlayerShape; } +export interface RuntimeSceneEntry { + entityId: string; + position: Vec3; + yawDegrees: number; +} + export interface RuntimeSoundEmitter { entityId: string; position: Vec3; @@ -129,6 +135,16 @@ export interface RuntimeInteractable { enabled: boolean; } +export interface RuntimeSceneExit { + entityId: string; + position: Vec3; + radius: number; + prompt: string; + enabled: boolean; + targetSceneId: string; + targetEntryEntityId: string; +} + export interface RuntimePointLight { entityId: string; position: Vec3; @@ -165,14 +181,16 @@ export interface RuntimeModelInstance { export interface RuntimeEntityCollection { playerStarts: RuntimePlayerStart[]; + sceneEntries: RuntimeSceneEntry[]; soundEmitters: RuntimeSoundEmitter[]; triggerVolumes: RuntimeTriggerVolume[]; teleportTargets: RuntimeTeleportTarget[]; interactables: RuntimeInteractable[]; + sceneExits: RuntimeSceneExit[]; } export interface RuntimeSpawnPoint { - source: "playerStart" | "fallback"; + source: "playerStart" | "sceneEntry" | "fallback"; entityId: string | null; position: Vec3; yawDegrees: number; @@ -193,9 +211,10 @@ export interface RuntimeSceneDefinition { spawn: RuntimeSpawnPoint; } -interface BuildRuntimeSceneOptions { +export interface BuildRuntimeSceneOptions { navigationMode?: RuntimeNavigationMode; loadedModelAssets?: Record; + sceneEntryId?: string | null; } function cloneVec3(vector: Vec3): Vec3 { @@ -411,10 +430,12 @@ interface RuntimeSceneCollections { function buildRuntimeSceneCollections(document: SceneDocument): RuntimeSceneCollections { const runtimeEntities: RuntimeEntityCollection = { playerStarts: [], + sceneEntries: [], soundEmitters: [], triggerVolumes: [], teleportTargets: [], - interactables: [] + interactables: [], + sceneExits: [] }; const localLights: RuntimeLocalLightCollection = { pointLights: [], @@ -451,6 +472,13 @@ function buildRuntimeSceneCollections(document: SceneDocument): RuntimeSceneColl collider: buildRuntimePlayerShape(entity) }); break; + case "sceneEntry": + runtimeEntities.sceneEntries.push({ + entityId: entity.id, + position: cloneVec3(entity.position), + yawDegrees: entity.yawDegrees + }); + break; case "soundEmitter": runtimeEntities.soundEmitters.push({ entityId: entity.id, @@ -493,6 +521,17 @@ function buildRuntimeSceneCollections(document: SceneDocument): RuntimeSceneColl enabled: entity.enabled }); break; + 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; default: assertNever(entity); } @@ -537,6 +576,42 @@ function buildRuntimePlayerShape( } } +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 { assertRuntimeSceneBuildable(document, { navigationMode: options.navigationMode ?? "orbitVisitor", @@ -606,14 +681,11 @@ export function buildRuntimeSceneFromDocument(document: SceneDocument, options: interactionLinks, playerStart, playerCollider, - spawn: - playerStart === null - ? buildFallbackSpawn(combinedSceneBounds) - : { - source: "playerStart", - entityId: playerStart.entityId, - position: cloneVec3(playerStart.position), - yawDegrees: playerStart.yawDegrees - } + spawn: resolveRuntimeSpawn( + playerStart, + collections.entities.sceneEntries, + combinedSceneBounds, + options.sceneEntryId + ) }; }