auto-git:

[change] src/app/App.tsx
 [change] src/controls/control-surface.ts
 [change] src/document/migrate-scene-document.ts
 [change] src/document/scene-document-validation.ts
 [change] src/document/scene-document.ts
 [change] src/interactions/interaction-links.ts
 [change] src/runtime-three/runtime-audio-system.ts
 [change] src/runtime-three/runtime-host.ts
 [change] src/runtime-three/runtime-interaction-system.ts
 [change] src/runtime-three/runtime-scene-build.ts
 [change] tests/domain/runtime-control-foundation.test.ts
 [change] tests/domain/runtime-interaction-system.test.ts
 [change] tests/serialization/control-interaction-links.test.ts
 [change] tests/unit/runtime-host.test.ts
This commit is contained in:
2026-04-14 01:40:15 +02:00
parent 3ae63c83d1
commit c1f1ff7488
14 changed files with 1354 additions and 810 deletions

View File

@@ -1,10 +1,20 @@
import { AudioListener, Group, PositionalAudio, Scene, Vector3, type PerspectiveCamera } from "three";
import {
AudioListener,
Group,
PositionalAudio,
Scene,
Vector3,
type PerspectiveCamera
} from "three";
import type { LoadedAudioAsset } from "../assets/audio-assets";
import type { ProjectAssetRecord } from "../assets/project-assets";
import type { InteractionLink } from "../interactions/interaction-links";
import type { RuntimePlayerAudioHookState } from "./navigation-controller";
import type { RuntimeSceneDefinition, RuntimeSoundEmitter } from "./runtime-scene-build";
import type {
RuntimeSceneDefinition,
RuntimeSoundEmitter
} from "./runtime-scene-build";
interface RuntimeSoundEmitterState {
entity: RuntimeSoundEmitter;
@@ -24,12 +34,23 @@ function getErrorDetail(error: unknown): string {
return "Unknown error.";
}
function formatSoundEmitterLabel(entityId: string, link: InteractionLink | null): string {
function formatSoundEmitterLabel(
entityId: string,
link: InteractionLink | null
): string {
return link === null ? entityId : `${entityId} (${link.id})`;
}
export function computeSoundEmitterDistanceGain(distance: number, refDistance: number, maxDistance: number): number {
if (!Number.isFinite(distance) || !Number.isFinite(refDistance) || !Number.isFinite(maxDistance)) {
export function computeSoundEmitterDistanceGain(
distance: number,
refDistance: number,
maxDistance: number
): number {
if (
!Number.isFinite(distance) ||
!Number.isFinite(refDistance) ||
!Number.isFinite(maxDistance)
) {
return 0;
}
@@ -45,7 +66,8 @@ export function computeSoundEmitterDistanceGain(distance: number, refDistance: n
return 0;
}
const normalizedDistance = (distance - refDistance) / (maxDistance - refDistance);
const normalizedDistance =
(distance - refDistance) / (maxDistance - refDistance);
const clampedDistance = Math.min(1, Math.max(0, normalizedDistance));
const proximity = 1 - clampedDistance;
const easedProximity = proximity * proximity * proximity * proximity;
@@ -84,7 +106,9 @@ export class RuntimeAudioSystem {
listener = new AudioListener();
this.camera.add(listener);
} catch (error) {
console.warn(`Audio is unavailable in this browser environment: ${getErrorDetail(error)}`);
console.warn(
`Audio is unavailable in this browser environment: ${getErrorDetail(error)}`
);
}
this.listener = listener;
@@ -100,7 +124,10 @@ export class RuntimeAudioSystem {
this.queueAutoplayEmitters();
}
updateAssets(projectAssets: Record<string, ProjectAssetRecord>, loadedAudioAssets: Record<string, LoadedAudioAsset>) {
updateAssets(
projectAssets: Record<string, ProjectAssetRecord>,
loadedAudioAssets: Record<string, LoadedAudioAsset>
) {
this.projectAssets = projectAssets;
this.loadedAudioAssets = loadedAudioAssets;
this.rebuildSoundEmitters();
@@ -151,25 +178,37 @@ export class RuntimeAudioSystem {
const soundEmitter = this.soundEmitters.get(soundEmitterId);
if (soundEmitter === undefined) {
this.setRuntimeMessage(`Sound emitter ${formatSoundEmitterLabel(soundEmitterId, link)} could not be found.`);
this.setRuntimeMessage(
`Sound emitter ${formatSoundEmitterLabel(soundEmitterId, link)} could not be found.`
);
return;
}
if (this.listener === null) {
this.setRuntimeMessage("Audio is unavailable in this browser environment.");
this.setRuntimeMessage(
"Audio is unavailable in this browser environment."
);
return;
}
if (soundEmitter.buffer === null) {
const assetLabel = this.describeAudioAssetAvailability(soundEmitter.entity.audioAssetId);
this.setRuntimeMessage(`Sound emitter ${formatSoundEmitterLabel(soundEmitterId, link)} cannot play because ${assetLabel}.`);
console.warn(`playSound: ${soundEmitterId} has no playable audio buffer.`);
const assetLabel = this.describeAudioAssetAvailability(
soundEmitter.entity.audioAssetId
);
this.setRuntimeMessage(
`Sound emitter ${formatSoundEmitterLabel(soundEmitterId, link)} cannot play because ${assetLabel}.`
);
console.warn(
`playSound: ${soundEmitterId} has no playable audio buffer.`
);
return;
}
if (this.listener.context.state !== "running") {
this.pendingPlayEmitterIds.add(soundEmitterId);
this.setRuntimeMessage("Audio is locked. Click the runner to enable sound.");
this.setRuntimeMessage(
"Audio is locked. Click the runner to enable sound."
);
return;
}
@@ -188,7 +227,9 @@ export class RuntimeAudioSystem {
try {
soundEmitter.audio.stop();
} catch (error) {
console.warn(`stopSound: ${soundEmitterId} could not be stopped: ${getErrorDetail(error)}`);
console.warn(
`stopSound: ${soundEmitterId} could not be stopped: ${getErrorDetail(error)}`
);
}
}
@@ -240,7 +281,11 @@ export class RuntimeAudioSystem {
for (const entity of this.runtimeScene.entities.soundEmitters) {
const group = new Group();
group.position.set(entity.position.x, entity.position.y, entity.position.z);
group.position.set(
entity.position.x,
entity.position.y,
entity.position.z
);
let audio: PositionalAudio | null = null;
@@ -339,7 +384,11 @@ export class RuntimeAudioSystem {
private playBufferedSound(soundEmitterId: string) {
const soundEmitter = this.soundEmitters.get(soundEmitterId);
if (soundEmitter === undefined || soundEmitter.audio === null || soundEmitter.buffer === null) {
if (
soundEmitter === undefined ||
soundEmitter.audio === null ||
soundEmitter.buffer === null
) {
return;
}
@@ -355,7 +404,10 @@ export class RuntimeAudioSystem {
soundEmitter.audio.play();
}
private configurePositionalAudio(audio: PositionalAudio, entity: RuntimeSoundEmitter) {
private configurePositionalAudio(
audio: PositionalAudio,
entity: RuntimeSoundEmitter
) {
audio.setLoop(entity.loop);
audio.setRefDistance(entity.refDistance);
audio.setMaxDistance(entity.maxDistance);
@@ -381,8 +433,13 @@ export class RuntimeAudioSystem {
this.camera.getWorldPosition(_listenerPosition);
soundEmitter.group.getWorldPosition(_emitterPosition);
const distance = _listenerPosition.distanceTo(_emitterPosition);
const attenuation = computeSoundEmitterDistanceGain(distance, soundEmitter.entity.refDistance, soundEmitter.entity.maxDistance);
const underwaterDuck = 1 - (this.playerAudioHooks?.underwaterAmount ?? 0) * 0.4;
const attenuation = computeSoundEmitterDistanceGain(
distance,
soundEmitter.entity.refDistance,
soundEmitter.entity.maxDistance
);
const underwaterDuck =
1 - (this.playerAudioHooks?.underwaterAmount ?? 0) * 0.4;
soundEmitter.audio.setVolume(
soundEmitter.entity.volume * attenuation * underwaterDuck

View File

@@ -213,7 +213,10 @@ export class RuntimeHost {
private readonly volumeAnimatedUniforms: Array<{ value: number }> = [];
private readonly runtimeWaterContactUniforms: RuntimeWaterContactUniformBinding[] =
[];
private readonly localLightObjects = new Map<string, LocalLightRenderObjects>();
private readonly localLightObjects = new Map<
string,
LocalLightRenderObjects
>();
private readonly modelRenderObjects = new Map<string, Group>();
private readonly materialTextureCache = new Map<
string,
@@ -568,9 +571,7 @@ export class RuntimeHost {
}
setSceneExitHandler(
handler:
| ((request: RuntimeSceneExitTransitionRequest) => void)
| null
handler: ((request: RuntimeSceneExitTransitionRequest) => void) | null
) {
this.sceneExitHandler = handler;
}
@@ -763,7 +764,10 @@ export class RuntimeHost {
const resolvedTime =
this.currentClockState === null
? null
: resolveRuntimeTimeState(this.runtimeScene.time, this.currentClockState);
: resolveRuntimeTimeState(
this.runtimeScene.time,
this.currentClockState
);
const resolvedWorld = resolveRuntimeDayNightWorldState(
this.currentWorld,
@@ -773,8 +777,8 @@ export class RuntimeHost {
);
const backgroundTexture =
resolvedWorld.background.mode === "image"
? this.loadedImageAssets[resolvedWorld.background.assetId]?.texture ??
null
? (this.loadedImageAssets[resolvedWorld.background.assetId]?.texture ??
null)
: null;
const nightBackgroundOverlay = resolvedWorld.nightBackgroundOverlay;
const backgroundOverlayState =
@@ -785,8 +789,7 @@ export class RuntimeHost {
this.loadedImageAssets[nightBackgroundOverlay.assetId]?.texture ??
null,
opacity: nightBackgroundOverlay.opacity,
environmentIntensity:
nightBackgroundOverlay.environmentIntensity
environmentIntensity: nightBackgroundOverlay.environmentIntensity
};
const environmentState = resolveWorldEnvironmentState(
resolvedWorld.background,
@@ -1380,28 +1383,28 @@ export class RuntimeHost {
const facingGroup = new Group();
facingGroup.rotation.y = (npc.yawDegrees * Math.PI) / 180;
group.add(facingGroup);
const colliderTop = getNpcColliderHeight({
mode: npc.collider.mode,
eyeHeight: npc.collider.eyeHeight,
capsuleRadius: npc.collider.mode === "capsule" ? npc.collider.radius : 0.35,
capsuleHeight: npc.collider.mode === "capsule" ? npc.collider.height : 1.8,
boxSize:
npc.collider.mode === "box"
? npc.collider.size
: {
x: 0.7,
y: 1.8,
z: 0.7
}
}) ?? 0.18;
const colliderTop =
getNpcColliderHeight({
mode: npc.collider.mode,
eyeHeight: npc.collider.eyeHeight,
capsuleRadius:
npc.collider.mode === "capsule" ? npc.collider.radius : 0.35,
capsuleHeight:
npc.collider.mode === "capsule" ? npc.collider.height : 1.8,
boxSize:
npc.collider.mode === "box"
? npc.collider.size
: {
x: 0.7,
y: 1.8,
z: 0.7
}
}) ?? 0.18;
const body = new Mesh(new BoxGeometry(0.08, 0.08, 0.34), facingMaterial);
body.position.set(0, colliderTop + 0.12, 0.06);
const arrowHead = new Mesh(
new ConeGeometry(0.1, 0.22, 14),
facingMaterial
);
const arrowHead = new Mesh(new ConeGeometry(0.1, 0.22, 14), facingMaterial);
arrowHead.rotation.x = Math.PI * 0.5;
arrowHead.position.set(0, colliderTop + 0.12, 0.28);
@@ -1471,7 +1474,7 @@ export class RuntimeHost {
const asset =
npc.modelAssetId === null
? null
: this.projectAssets[npc.modelAssetId] ?? null;
: (this.projectAssets[npc.modelAssetId] ?? null);
const renderGroup =
npc.modelAssetId === null || asset?.kind !== "model"
? this.createNpcColliderFallbackRenderGroup(npc)
@@ -2209,7 +2212,9 @@ export class RuntimeHost {
this.applyDayNightLighting();
this.clockPublishAccumulator += dt;
if (this.clockPublishAccumulator >= RUNTIME_CLOCK_PUBLISH_INTERVAL_SECONDS) {
if (
this.clockPublishAccumulator >= RUNTIME_CLOCK_PUBLISH_INTERVAL_SECONDS
) {
this.clockPublishAccumulator = 0;
this.publishRuntimeClockState();
}
@@ -2349,9 +2354,9 @@ export class RuntimeHost {
return;
}
this.runtimeScene.entities.npcs = this.runtimeScene.npcDefinitions.filter(
(npc) => npc.active
).map((npc) => createRuntimeNpcFromDefinition(npc));
this.runtimeScene.entities.npcs = this.runtimeScene.npcDefinitions
.filter((npc) => npc.active)
.map((npc) => createRuntimeNpcFromDefinition(npc));
this.runtimeScene.colliders = [
...this.runtimeScene.staticColliders,
...this.runtimeScene.entities.npcs

View File

@@ -18,8 +18,17 @@ const DEFAULT_INTERACTABLE_TARGET_RADIUS = 0.75;
export interface RuntimeInteractionDispatcher {
teleportPlayer(target: RuntimeTeleportTarget, link: InteractionLink): void;
activateSceneExit(sceneExit: RuntimeSceneExit): void;
toggleBrushVisibility(brushId: string, visible: boolean | undefined, link: InteractionLink): void;
playAnimation(instanceId: string, clipName: string, loop: boolean | undefined, link: InteractionLink): void;
toggleBrushVisibility(
brushId: string,
visible: boolean | undefined,
link: InteractionLink
): void;
playAnimation(
instanceId: string,
clipName: string,
loop: boolean | undefined,
link: InteractionLink
): void;
stopAnimation(instanceId: string, link: InteractionLink): void;
playSound(soundEmitterId: string, link: InteractionLink): void;
stopSound(soundEmitterId: string, link: InteractionLink): void;
@@ -71,7 +80,10 @@ function normalizeVec3(vector: Vec3): Vec3 | null {
return scaleVec3(vector, 1 / Math.sqrt(lengthSquared));
}
function isPointInsideTriggerVolume(position: Vec3, triggerVolume: RuntimeTriggerVolume): boolean {
function isPointInsideTriggerVolume(
position: Vec3,
triggerVolume: RuntimeTriggerVolume
): boolean {
const halfSize = {
x: triggerVolume.size.x * 0.5,
y: triggerVolume.size.y * 0.5,
@@ -88,7 +100,12 @@ function isPointInsideTriggerVolume(position: Vec3, triggerVolume: RuntimeTrigge
);
}
function raySphereHitDistance(origin: Vec3, direction: Vec3, center: Vec3, radius: number): number | null {
function raySphereHitDistance(
origin: Vec3,
direction: Vec3,
center: Vec3,
radius: number
): number | null {
const offset = subtractVec3(origin, center);
const halfB = dotVec3(offset, direction);
const c = dotVec3(offset, offset) - radius * radius;
@@ -109,15 +126,30 @@ function raySphereHitDistance(origin: Vec3, direction: Vec3, center: Vec3, radiu
return farHit >= 0 ? 0 : null;
}
function resolveTeleportTarget(runtimeScene: RuntimeSceneDefinition, entityId: string): RuntimeTeleportTarget | null {
return runtimeScene.entities.teleportTargets.find((teleportTarget) => teleportTarget.entityId === entityId) ?? null;
function resolveTeleportTarget(
runtimeScene: RuntimeSceneDefinition,
entityId: string
): RuntimeTeleportTarget | null {
return (
runtimeScene.entities.teleportTargets.find(
(teleportTarget) => teleportTarget.entityId === entityId
) ?? null
);
}
function hasTriggerLinks(runtimeScene: RuntimeSceneDefinition, sourceEntityId: string, trigger: InteractionLink["trigger"]): boolean {
return runtimeScene.interactionLinks.some((link) => link.sourceEntityId === sourceEntityId && link.trigger === trigger);
function hasTriggerLinks(
runtimeScene: RuntimeSceneDefinition,
sourceEntityId: string,
trigger: InteractionLink["trigger"]
): boolean {
return runtimeScene.interactionLinks.some(
(link) => link.sourceEntityId === sourceEntityId && link.trigger === trigger
);
}
function getInteractableTargetRadius(interactable: RuntimeInteractable): number {
function getInteractableTargetRadius(
interactable: RuntimeInteractable
): number {
return Math.min(DEFAULT_INTERACTABLE_TARGET_RADIUS, interactable.radius);
}
@@ -143,7 +175,8 @@ function updateBestPrompt(
(currentBestPrompt === null ||
candidateDistance < currentBestPrompt.distance ||
(candidateDistance === currentBestPrompt.distance &&
candidateEntityId.localeCompare(currentBestPrompt.sourceEntityId) < 0)))
candidateEntityId.localeCompare(currentBestPrompt.sourceEntityId) <
0)))
) {
return {
prompt: nextPrompt,
@@ -164,15 +197,42 @@ export class RuntimeInteractionSystem {
this.occupiedTriggerVolumes.clear();
}
updatePlayerPosition(feetPosition: Vec3, runtimeScene: RuntimeSceneDefinition, dispatcher: RuntimeInteractionDispatcher) {
updatePlayerPosition(
feetPosition: Vec3,
runtimeScene: RuntimeSceneDefinition,
dispatcher: RuntimeInteractionDispatcher
) {
for (const triggerVolume of runtimeScene.entities.triggerVolumes) {
const containsPlayer = isPointInsideTriggerVolume(feetPosition, triggerVolume);
const wasOccupied = this.occupiedTriggerVolumes.has(triggerVolume.entityId);
const containsPlayer = isPointInsideTriggerVolume(
feetPosition,
triggerVolume
);
const wasOccupied = this.occupiedTriggerVolumes.has(
triggerVolume.entityId
);
if (!wasOccupied && containsPlayer && hasTriggerLinks(runtimeScene, triggerVolume.entityId, "enter")) {
this.dispatchLinks(triggerVolume.entityId, "enter", runtimeScene, dispatcher);
} else if (wasOccupied && !containsPlayer && hasTriggerLinks(runtimeScene, triggerVolume.entityId, "exit")) {
this.dispatchLinks(triggerVolume.entityId, "exit", runtimeScene, dispatcher);
if (
!wasOccupied &&
containsPlayer &&
hasTriggerLinks(runtimeScene, triggerVolume.entityId, "enter")
) {
this.dispatchLinks(
triggerVolume.entityId,
"enter",
runtimeScene,
dispatcher
);
} else if (
wasOccupied &&
!containsPlayer &&
hasTriggerLinks(runtimeScene, triggerVolume.entityId, "exit")
) {
this.dispatchLinks(
triggerVolume.entityId,
"exit",
runtimeScene,
dispatcher
);
}
if (containsPlayer) {
@@ -199,7 +259,10 @@ export class RuntimeInteractionSystem {
let bestHitDistance = Number.POSITIVE_INFINITY;
for (const interactable of runtimeScene.entities.interactables) {
if (!interactable.interactionEnabled || !hasTriggerLinks(runtimeScene, interactable.entityId, "click")) {
if (
!interactable.interactionEnabled ||
!hasTriggerLinks(runtimeScene, interactable.entityId, "click")
) {
continue;
}
@@ -240,7 +303,10 @@ export class RuntimeInteractionSystem {
continue;
}
const distance = distanceBetweenVec3(interactionOrigin, sceneExit.position);
const distance = distanceBetweenVec3(
interactionOrigin,
sceneExit.position
);
if (distance > sceneExit.radius) {
continue;
@@ -273,7 +339,11 @@ export class RuntimeInteractionSystem {
return bestPrompt;
}
dispatchClickInteraction(sourceEntityId: string, runtimeScene: RuntimeSceneDefinition, dispatcher: RuntimeInteractionDispatcher) {
dispatchClickInteraction(
sourceEntityId: string,
runtimeScene: RuntimeSceneDefinition,
dispatcher: RuntimeInteractionDispatcher
) {
const sceneExit =
runtimeScene.entities.sceneExits.find(
(candidate) => candidate.entityId === sourceEntityId
@@ -300,14 +370,20 @@ export class RuntimeInteractionSystem {
const controlEffect = getInteractionActionControlEffect(link.action);
if (controlEffect !== null && dispatcher.dispatchControlEffect !== undefined) {
if (
controlEffect !== null &&
dispatcher.dispatchControlEffect !== undefined
) {
dispatcher.dispatchControlEffect(controlEffect, link);
continue;
}
switch (link.action.type) {
case "teleportPlayer": {
const teleportTarget = resolveTeleportTarget(runtimeScene, link.action.targetEntityId);
const teleportTarget = resolveTeleportTarget(
runtimeScene,
link.action.targetEntityId
);
if (teleportTarget !== null) {
dispatcher.teleportPlayer(teleportTarget, link);
@@ -315,10 +391,19 @@ export class RuntimeInteractionSystem {
break;
}
case "toggleVisibility":
dispatcher.toggleBrushVisibility(link.action.targetBrushId, link.action.visible, link);
dispatcher.toggleBrushVisibility(
link.action.targetBrushId,
link.action.visible,
link
);
break;
case "playAnimation":
dispatcher.playAnimation(link.action.targetModelInstanceId, link.action.clipName, link.action.loop, link);
dispatcher.playAnimation(
link.action.targetModelInstanceId,
link.action.clipName,
link.action.loop,
link
);
break;
case "stopAnimation":
dispatcher.stopAnimation(link.action.targetModelInstanceId, link);

View File

@@ -37,7 +37,10 @@ import {
type ScenePath,
type ScenePathPoint
} from "../document/paths";
import { cloneWorldSettings, type WorldSettings } from "../document/world-settings";
import {
cloneWorldSettings,
type WorldSettings
} from "../document/world-settings";
import {
type CharacterColliderSettings,
cloneNpcPresence,
@@ -57,13 +60,27 @@ import {
} from "../entities/entity-instances";
import { getBoxBrushBounds } from "../geometry/box-brush";
import { buildBoxBrushDerivedMeshData } from "../geometry/box-brush-mesh";
import { buildGeneratedModelCollider, type GeneratedColliderBounds, type GeneratedModelCollider } from "../geometry/model-instance-collider-generation";
import { cloneInteractionLink, getInteractionLinks, type InteractionLink } from "../interactions/interaction-links";
import { cloneMaterialDef, type MaterialDef } from "../materials/starter-material-library";
import {
buildGeneratedModelCollider,
type GeneratedColliderBounds,
type GeneratedModelCollider
} from "../geometry/model-instance-collider-generation";
import {
cloneInteractionLink,
getInteractionLinks,
type InteractionLink
} from "../interactions/interaction-links";
import {
cloneMaterialDef,
type MaterialDef
} from "../materials/starter-material-library";
import { assertRuntimeSceneBuildable } from "./runtime-scene-validation";
import { resolveNpcPresenceActive } from "./runtime-npc-presence";
import type { RuntimeClockState } from "./runtime-project-time";
import { FIRST_PERSON_PLAYER_SHAPE, type FirstPersonPlayerShape } from "./player-collision";
import {
FIRST_PERSON_PLAYER_SHAPE,
type FirstPersonPlayerShape
} from "./player-collision";
export type RuntimeNavigationMode = "firstPerson" | "thirdPerson";
@@ -507,7 +524,10 @@ function buildRuntimePlayerMovement(
};
}
function resolveRuntimeMaterial(document: SceneDocument, materialId: string | null): MaterialDef | null {
function resolveRuntimeMaterial(
document: SceneDocument,
materialId: string | null
): MaterialDef | null {
if (materialId === null) {
return null;
}
@@ -521,7 +541,10 @@ function resolveRuntimeMaterial(document: SceneDocument, materialId: string | nu
return cloneMaterialDef(material);
}
function buildRuntimeBrush(brush: BoxBrush, document: SceneDocument): RuntimeBoxBrushInstance {
function buildRuntimeBrush(
brush: BoxBrush,
document: SceneDocument
): RuntimeBoxBrushInstance {
return {
id: brush.id,
kind: "box",
@@ -584,7 +607,9 @@ function buildRuntimeFogVolume(brush: BoxBrush): RuntimeFogVolume {
function buildRuntimeWaterVolume(brush: BoxBrush): RuntimeWaterVolume {
if (brush.volume.mode !== "water") {
throw new Error(`Cannot build water volume from non-water brush ${brush.id}.`);
throw new Error(
`Cannot build water volume from non-water brush ${brush.id}.`
);
}
return {
@@ -617,7 +642,9 @@ function buildRuntimeCollider(brush: BoxBrush): RuntimeBrushTriMeshCollider {
};
}
function buildRuntimeModelInstance(modelInstance: SceneDocument["modelInstances"][string]): RuntimeModelInstance {
function buildRuntimeModelInstance(
modelInstance: SceneDocument["modelInstances"][string]
): RuntimeModelInstance {
return {
instanceId: modelInstance.id,
assetId: modelInstance.assetId,
@@ -663,7 +690,9 @@ function buildRuntimePath(path: ScenePath): RuntimePath {
};
}
function getColliderBounds(collider: RuntimeSceneCollider): GeneratedColliderBounds {
function getColliderBounds(
collider: RuntimeSceneCollider
): GeneratedColliderBounds {
return {
min: cloneVec3(collider.worldBounds.min),
max: cloneVec3(collider.worldBounds.max)
@@ -751,7 +780,9 @@ export function buildRuntimeNpcCollider(
}
}
function combineColliderBounds(colliders: RuntimeSceneCollider[]): RuntimeSceneBounds | null {
function combineColliderBounds(
colliders: RuntimeSceneCollider[]
): RuntimeSceneBounds | null {
if (colliders.length === 0) {
return null;
}
@@ -786,7 +817,9 @@ function combineColliderBounds(colliders: RuntimeSceneCollider[]): RuntimeSceneB
};
}
function buildFallbackSpawn(sceneBounds: RuntimeSceneBounds | null): RuntimeSpawnPoint {
function buildFallbackSpawn(
sceneBounds: RuntimeSceneBounds | null
): RuntimeSpawnPoint {
if (sceneBounds === null) {
return {
source: "fallback",
@@ -829,7 +862,10 @@ function buildRuntimeControlSurface(
const defaultSource = createDefaultResolvedControlSource();
for (const pointLight of collections.localLights.pointLights) {
const target = createLightControlTargetRef("pointLight", pointLight.entityId);
const target = createLightControlTargetRef(
"pointLight",
pointLight.entityId
);
const descriptor = createLightIntensityControlChannelDescriptor({
target,
defaultValue: pointLight.intensity
@@ -843,7 +879,8 @@ function buildRuntimeControlSurface(
resolved.discrete.push(
createResolvedLightEnabledState({
target,
value: sourceEntity?.kind === "pointLight" ? sourceEntity.visible : true,
value:
sourceEntity?.kind === "pointLight" ? sourceEntity.visible : true,
source: defaultSource
})
);
@@ -934,7 +971,8 @@ function buildRuntimeControlSurface(
}
for (const modelInstance of modelInstances) {
const authoredModelInstance = document.modelInstances[modelInstance.instanceId];
const authoredModelInstance =
document.modelInstances[modelInstance.instanceId];
const asset =
authoredModelInstance === undefined
? undefined
@@ -1112,7 +1150,9 @@ function buildRuntimeSceneCollections(
}
function assertNever(value: never): never {
throw new Error(`Unsupported runtime entity: ${String((value as EntityInstance).kind)}`);
throw new Error(
`Unsupported runtime entity: ${String((value as EntityInstance).kind)}`
);
}
function buildRuntimePlayerShape(
@@ -1161,8 +1201,13 @@ function resolveRuntimeSpawn(
return buildFallbackSpawn(sceneBounds);
}
export function buildRuntimeSceneFromDocument(document: SceneDocument, options: BuildRuntimeSceneOptions = {}): RuntimeSceneDefinition {
const playerStartEntity = getPrimaryEnabledPlayerStartEntity(document.entities);
export function buildRuntimeSceneFromDocument(
document: SceneDocument,
options: BuildRuntimeSceneOptions = {}
): RuntimeSceneDefinition {
const playerStartEntity = getPrimaryEnabledPlayerStartEntity(
document.entities
);
const navigationMode = resolveRuntimeNavigationMode(
playerStartEntity,
options.navigationMode
@@ -1173,8 +1218,12 @@ export function buildRuntimeSceneFromDocument(document: SceneDocument, options:
loadedModelAssets: options.loadedModelAssets
});
const enabledBrushes = Object.values(document.brushes).filter((brush) => brush.enabled);
const brushes = enabledBrushes.map((brush) => buildRuntimeBrush(brush, document));
const enabledBrushes = Object.values(document.brushes).filter(
(brush) => brush.enabled
);
const brushes = enabledBrushes.map((brush) =>
buildRuntimeBrush(brush, document)
);
const staticColliders: RuntimeSceneCollider[] = [];
const volumes: RuntimeBoxVolumeCollection = {
fog: [],
@@ -1194,7 +1243,9 @@ export function buildRuntimeSceneFromDocument(document: SceneDocument, options:
volumes.water.push(buildRuntimeWaterVolume(brush));
}
const enabledModelInstances = getModelInstances(document.modelInstances).filter((modelInstance) => modelInstance.enabled);
const enabledModelInstances = getModelInstances(
document.modelInstances
).filter((modelInstance) => modelInstance.enabled);
const modelInstances = enabledModelInstances.map(buildRuntimeModelInstance);
const paths = getScenePaths(document.paths)
.filter((path) => path.enabled)
@@ -1205,9 +1256,15 @@ export function buildRuntimeSceneFromDocument(document: SceneDocument, options:
document,
runtimeTimeOfDayHours
);
const control = buildRuntimeControlSurface(document, collections, modelInstances);
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 enabledModelInstanceIds = new Set(
enabledModelInstances.map((modelInstance) => modelInstance.id)
);
const enabledEntityIds = new Set(
getEntityInstances(document.entities)
.filter((entity) => entity.enabled)
@@ -1267,7 +1324,11 @@ export function buildRuntimeSceneFromDocument(document: SceneDocument, options:
continue;
}
const generatedCollider = buildGeneratedModelCollider(modelInstance, asset, options.loadedModelAssets?.[modelInstance.assetId]);
const generatedCollider = buildGeneratedModelCollider(
modelInstance,
asset,
options.loadedModelAssets?.[modelInstance.assetId]
);
if (generatedCollider !== null) {
staticColliders.push(generatedCollider);