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:
1348
src/app/App.tsx
1348
src/app/App.tsx
File diff suppressed because it is too large
Load Diff
@@ -199,7 +199,9 @@ function assertNonEmptyString(value: string, label: string) {
|
||||
|
||||
function assertNonNegativeFiniteNumber(value: number, label: string) {
|
||||
if (!Number.isFinite(value) || value < 0) {
|
||||
throw new Error(`${label} must be a finite number greater than or equal to zero.`);
|
||||
throw new Error(
|
||||
`${label} must be a finite number greater than or equal to zero.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,9 +214,7 @@ function assertBoolean(value: boolean, label: string) {
|
||||
export function isControlEntityTargetKind(
|
||||
value: unknown
|
||||
): value is ControlEntityTargetKind {
|
||||
return CONTROL_ENTITY_TARGET_KINDS.includes(
|
||||
value as ControlEntityTargetKind
|
||||
);
|
||||
return CONTROL_ENTITY_TARGET_KINDS.includes(value as ControlEntityTargetKind);
|
||||
}
|
||||
|
||||
export function isControlInteractionTargetKind(
|
||||
@@ -412,10 +412,7 @@ export function createLightIntensityControlChannelDescriptor(options: {
|
||||
options.defaultValue,
|
||||
"Control light intensity default"
|
||||
);
|
||||
assertNonNegativeFiniteNumber(
|
||||
minValue,
|
||||
"Control light intensity minimum"
|
||||
);
|
||||
assertNonNegativeFiniteNumber(minValue, "Control light intensity minimum");
|
||||
|
||||
return {
|
||||
channel: "light.intensity",
|
||||
@@ -751,7 +748,9 @@ export function areControlEffectsEqual(
|
||||
return false;
|
||||
}
|
||||
|
||||
if (getControlTargetRefKey(left.target) !== getControlTargetRefKey(right.target)) {
|
||||
if (
|
||||
getControlTargetRefKey(left.target) !== getControlTargetRefKey(right.target)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -767,8 +766,7 @@ export function areControlEffectsEqual(
|
||||
return true;
|
||||
case "setInteractionEnabled":
|
||||
return (
|
||||
left.enabled ===
|
||||
(right as SetInteractionEnabledControlEffect).enabled
|
||||
left.enabled === (right as SetInteractionEnabledControlEffect).enabled
|
||||
);
|
||||
case "setLightEnabled":
|
||||
return left.enabled === (right as SetLightEnabledControlEffect).enabled;
|
||||
@@ -868,7 +866,8 @@ export function applyControlEffectToResolvedState(
|
||||
});
|
||||
const stateKey = getResolvedDiscreteControlStateKey(nextState);
|
||||
const existingIndex = nextResolved.discrete.findIndex(
|
||||
(candidate) => getResolvedDiscreteControlStateKey(candidate) === stateKey
|
||||
(candidate) =>
|
||||
getResolvedDiscreteControlStateKey(candidate) === stateKey
|
||||
);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
@@ -886,7 +885,8 @@ export function applyControlEffectToResolvedState(
|
||||
});
|
||||
const stateKey = getResolvedDiscreteControlStateKey(nextState);
|
||||
const existingIndex = nextResolved.discrete.findIndex(
|
||||
(candidate) => getResolvedDiscreteControlStateKey(candidate) === stateKey
|
||||
(candidate) =>
|
||||
getResolvedDiscreteControlStateKey(candidate) === stateKey
|
||||
);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
|
||||
@@ -288,7 +288,9 @@ function readSceneEditorPanelPreferences(
|
||||
viewMode !== "front" &&
|
||||
viewMode !== "side"
|
||||
) {
|
||||
throw new Error(`${label}.viewMode must be a supported viewport view mode.`);
|
||||
throw new Error(
|
||||
`${label}.viewMode must be a supported viewport view mode.`
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -1287,7 +1289,8 @@ function readPlayerStartKeyboardBindingCode(
|
||||
label,
|
||||
fallback,
|
||||
(candidate): candidate is typeof fallback =>
|
||||
typeof candidate === "string" && isPlayerStartKeyboardBindingCode(candidate)
|
||||
typeof candidate === "string" &&
|
||||
isPlayerStartKeyboardBindingCode(candidate)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2254,7 +2257,7 @@ function readWorldTimeOfDaySettings(
|
||||
),
|
||||
ambientColorHex: expectHexColor(
|
||||
isRecord(nightValue)
|
||||
? nightValue.ambientColorHex ?? nightDefaults.ambientColorHex
|
||||
? (nightValue.ambientColorHex ?? nightDefaults.ambientColorHex)
|
||||
: nightDefaults.ambientColorHex,
|
||||
`${label}.night.ambientColorHex`
|
||||
),
|
||||
@@ -2265,7 +2268,7 @@ function readWorldTimeOfDaySettings(
|
||||
),
|
||||
lightColorHex: expectHexColor(
|
||||
isRecord(nightValue)
|
||||
? nightValue.lightColorHex ?? nightDefaults.lightColorHex
|
||||
? (nightValue.lightColorHex ?? nightDefaults.lightColorHex)
|
||||
: nightDefaults.lightColorHex,
|
||||
`${label}.night.lightColorHex`
|
||||
),
|
||||
@@ -2351,8 +2354,16 @@ function readPointLightEntity(value: unknown, label: string): EntityInstance {
|
||||
const entity = createPointLightEntity({
|
||||
id: expectString(value.id, `${label}.id`),
|
||||
name: readOptionalEntityName(value.name, `${label}.name`),
|
||||
visible: readOptionalBoolean(value.visible, `${label}.visible`, DEFAULT_ENTITY_VISIBLE),
|
||||
enabled: readOptionalBoolean(value.enabled, `${label}.enabled`, DEFAULT_ENTITY_ENABLED),
|
||||
visible: readOptionalBoolean(
|
||||
value.visible,
|
||||
`${label}.visible`,
|
||||
DEFAULT_ENTITY_VISIBLE
|
||||
),
|
||||
enabled: readOptionalBoolean(
|
||||
value.enabled,
|
||||
`${label}.enabled`,
|
||||
DEFAULT_ENTITY_ENABLED
|
||||
),
|
||||
position: readVec3(value.position, `${label}.position`),
|
||||
colorHex: expectHexColor(value.colorHex, `${label}.colorHex`),
|
||||
intensity: expectNonNegativeFiniteNumber(
|
||||
@@ -2378,8 +2389,16 @@ function readSpotLightEntity(value: unknown, label: string): EntityInstance {
|
||||
const entity = createSpotLightEntity({
|
||||
id: expectString(value.id, `${label}.id`),
|
||||
name: readOptionalEntityName(value.name, `${label}.name`),
|
||||
visible: readOptionalBoolean(value.visible, `${label}.visible`, DEFAULT_ENTITY_VISIBLE),
|
||||
enabled: readOptionalBoolean(value.enabled, `${label}.enabled`, DEFAULT_ENTITY_ENABLED),
|
||||
visible: readOptionalBoolean(
|
||||
value.visible,
|
||||
`${label}.visible`,
|
||||
DEFAULT_ENTITY_VISIBLE
|
||||
),
|
||||
enabled: readOptionalBoolean(
|
||||
value.enabled,
|
||||
`${label}.enabled`,
|
||||
DEFAULT_ENTITY_ENABLED
|
||||
),
|
||||
position: readVec3(value.position, `${label}.position`),
|
||||
direction: readVec3(value.direction, `${label}.direction`),
|
||||
colorHex: expectHexColor(value.colorHex, `${label}.colorHex`),
|
||||
@@ -2410,8 +2429,16 @@ function readPlayerStartEntity(value: unknown, label: string): EntityInstance {
|
||||
const entity = createPlayerStartEntity({
|
||||
id: expectString(value.id, `${label}.id`),
|
||||
name: readOptionalEntityName(value.name, `${label}.name`),
|
||||
visible: readOptionalBoolean(value.visible, `${label}.visible`, DEFAULT_ENTITY_VISIBLE),
|
||||
enabled: readOptionalBoolean(value.enabled, `${label}.enabled`, DEFAULT_ENTITY_ENABLED),
|
||||
visible: readOptionalBoolean(
|
||||
value.visible,
|
||||
`${label}.visible`,
|
||||
DEFAULT_ENTITY_VISIBLE
|
||||
),
|
||||
enabled: readOptionalBoolean(
|
||||
value.enabled,
|
||||
`${label}.enabled`,
|
||||
DEFAULT_ENTITY_ENABLED
|
||||
),
|
||||
position: readVec3(value.position, `${label}.position`),
|
||||
yawDegrees: expectFiniteNumber(value.yawDegrees, `${label}.yawDegrees`),
|
||||
navigationMode: readPlayerStartNavigationMode(
|
||||
@@ -2448,8 +2475,16 @@ function readSoundEmitterEntity(value: unknown, label: string): EntityInstance {
|
||||
const entity = createSoundEmitterEntity({
|
||||
id: expectString(value.id, `${label}.id`),
|
||||
name: readOptionalEntityName(value.name, `${label}.name`),
|
||||
visible: readOptionalBoolean(value.visible, `${label}.visible`, DEFAULT_ENTITY_VISIBLE),
|
||||
enabled: readOptionalBoolean(value.enabled, `${label}.enabled`, DEFAULT_ENTITY_ENABLED),
|
||||
visible: readOptionalBoolean(
|
||||
value.visible,
|
||||
`${label}.visible`,
|
||||
DEFAULT_ENTITY_VISIBLE
|
||||
),
|
||||
enabled: readOptionalBoolean(
|
||||
value.enabled,
|
||||
`${label}.enabled`,
|
||||
DEFAULT_ENTITY_ENABLED
|
||||
),
|
||||
position: readVec3(value.position, `${label}.position`),
|
||||
audioAssetId:
|
||||
value.audioAssetId === undefined || value.audioAssetId === null
|
||||
@@ -2488,8 +2523,16 @@ function readLegacySoundEmitterEntity(
|
||||
const entity = createSoundEmitterEntity({
|
||||
id: expectString(value.id, `${label}.id`),
|
||||
name: readOptionalEntityName(value.name, `${label}.name`),
|
||||
visible: readOptionalBoolean(value.visible, `${label}.visible`, DEFAULT_ENTITY_VISIBLE),
|
||||
enabled: readOptionalBoolean(value.enabled, `${label}.enabled`, DEFAULT_ENTITY_ENABLED),
|
||||
visible: readOptionalBoolean(
|
||||
value.visible,
|
||||
`${label}.visible`,
|
||||
DEFAULT_ENTITY_VISIBLE
|
||||
),
|
||||
enabled: readOptionalBoolean(
|
||||
value.enabled,
|
||||
`${label}.enabled`,
|
||||
DEFAULT_ENTITY_ENABLED
|
||||
),
|
||||
position: readVec3(value.position, `${label}.position`),
|
||||
refDistance: radius,
|
||||
maxDistance: radius,
|
||||
@@ -2527,8 +2570,16 @@ function readTriggerVolumeEntity(
|
||||
const entity = createTriggerVolumeEntity({
|
||||
id: expectString(value.id, `${label}.id`),
|
||||
name: readOptionalEntityName(value.name, `${label}.name`),
|
||||
visible: readOptionalBoolean(value.visible, `${label}.visible`, DEFAULT_ENTITY_VISIBLE),
|
||||
enabled: readOptionalBoolean(value.enabled, `${label}.enabled`, DEFAULT_ENTITY_ENABLED),
|
||||
visible: readOptionalBoolean(
|
||||
value.visible,
|
||||
`${label}.visible`,
|
||||
DEFAULT_ENTITY_VISIBLE
|
||||
),
|
||||
enabled: readOptionalBoolean(
|
||||
value.enabled,
|
||||
`${label}.enabled`,
|
||||
DEFAULT_ENTITY_ENABLED
|
||||
),
|
||||
position: readVec3(value.position, `${label}.position`),
|
||||
size,
|
||||
triggerOnEnter: expectBoolean(
|
||||
@@ -2561,8 +2612,16 @@ function readTeleportTargetEntity(
|
||||
const entity = createTeleportTargetEntity({
|
||||
id: expectString(value.id, `${label}.id`),
|
||||
name: readOptionalEntityName(value.name, `${label}.name`),
|
||||
visible: readOptionalBoolean(value.visible, `${label}.visible`, DEFAULT_ENTITY_VISIBLE),
|
||||
enabled: readOptionalBoolean(value.enabled, `${label}.enabled`, DEFAULT_ENTITY_ENABLED),
|
||||
visible: readOptionalBoolean(
|
||||
value.visible,
|
||||
`${label}.visible`,
|
||||
DEFAULT_ENTITY_VISIBLE
|
||||
),
|
||||
enabled: readOptionalBoolean(
|
||||
value.enabled,
|
||||
`${label}.enabled`,
|
||||
DEFAULT_ENTITY_ENABLED
|
||||
),
|
||||
position: readVec3(value.position, `${label}.position`),
|
||||
yawDegrees: expectFiniteNumber(value.yawDegrees, `${label}.yawDegrees`)
|
||||
});
|
||||
@@ -2587,11 +2646,19 @@ function readInteractableEntity(value: unknown, label: string): EntityInstance {
|
||||
const entity = createInteractableEntity({
|
||||
id: expectString(value.id, `${label}.id`),
|
||||
name: readOptionalEntityName(value.name, `${label}.name`),
|
||||
visible: readOptionalBoolean(value.visible, `${label}.visible`, DEFAULT_ENTITY_VISIBLE),
|
||||
visible: readOptionalBoolean(
|
||||
value.visible,
|
||||
`${label}.visible`,
|
||||
DEFAULT_ENTITY_VISIBLE
|
||||
),
|
||||
enabled:
|
||||
value.interactionEnabled === undefined
|
||||
? DEFAULT_ENTITY_ENABLED
|
||||
: readOptionalBoolean(value.enabled, `${label}.enabled`, DEFAULT_ENTITY_ENABLED),
|
||||
: readOptionalBoolean(
|
||||
value.enabled,
|
||||
`${label}.enabled`,
|
||||
DEFAULT_ENTITY_ENABLED
|
||||
),
|
||||
position: readVec3(value.position, `${label}.position`),
|
||||
radius: expectPositiveFiniteNumber(value.radius, `${label}.radius`),
|
||||
prompt: expectString(value.prompt, `${label}.prompt`),
|
||||
@@ -2614,8 +2681,16 @@ function readSceneEntryEntity(value: unknown, label: string): EntityInstance {
|
||||
const entity = createSceneEntryEntity({
|
||||
id: expectString(value.id, `${label}.id`),
|
||||
name: readOptionalEntityName(value.name, `${label}.name`),
|
||||
visible: readOptionalBoolean(value.visible, `${label}.visible`, DEFAULT_ENTITY_VISIBLE),
|
||||
enabled: readOptionalBoolean(value.enabled, `${label}.enabled`, DEFAULT_ENTITY_ENABLED),
|
||||
visible: readOptionalBoolean(
|
||||
value.visible,
|
||||
`${label}.visible`,
|
||||
DEFAULT_ENTITY_VISIBLE
|
||||
),
|
||||
enabled: readOptionalBoolean(
|
||||
value.enabled,
|
||||
`${label}.enabled`,
|
||||
DEFAULT_ENTITY_ENABLED
|
||||
),
|
||||
position: readVec3(value.position, `${label}.position`),
|
||||
yawDegrees: expectFiniteNumber(value.yawDegrees, `${label}.yawDegrees`)
|
||||
});
|
||||
@@ -2703,11 +2778,19 @@ function readSceneExitEntity(value: unknown, label: string): EntityInstance {
|
||||
const entity = createSceneExitEntity({
|
||||
id: expectString(value.id, `${label}.id`),
|
||||
name: readOptionalEntityName(value.name, `${label}.name`),
|
||||
visible: readOptionalBoolean(value.visible, `${label}.visible`, DEFAULT_ENTITY_VISIBLE),
|
||||
visible: readOptionalBoolean(
|
||||
value.visible,
|
||||
`${label}.visible`,
|
||||
DEFAULT_ENTITY_VISIBLE
|
||||
),
|
||||
enabled:
|
||||
value.interactionEnabled === undefined
|
||||
? DEFAULT_ENTITY_ENABLED
|
||||
: readOptionalBoolean(value.enabled, `${label}.enabled`, DEFAULT_ENTITY_ENABLED),
|
||||
: readOptionalBoolean(
|
||||
value.enabled,
|
||||
`${label}.enabled`,
|
||||
DEFAULT_ENTITY_ENABLED
|
||||
),
|
||||
position: readVec3(value.position, `${label}.position`),
|
||||
radius: expectPositiveFiniteNumber(value.radius, `${label}.radius`),
|
||||
prompt: expectString(value.prompt, `${label}.prompt`),
|
||||
@@ -2811,7 +2894,9 @@ function readControlTargetRef(value: unknown, label: string): ControlTargetRef {
|
||||
const entityKind = expectString(value.entityKind, `${label}.entityKind`);
|
||||
|
||||
if (!isControlEntityTargetKind(entityKind)) {
|
||||
throw new Error(`${label}.entityKind must be a supported control entity kind.`);
|
||||
throw new Error(
|
||||
`${label}.entityKind must be a supported control entity kind.`
|
||||
);
|
||||
}
|
||||
|
||||
return createEntityControlTargetRef(
|
||||
@@ -3169,7 +3254,9 @@ function readScenePathValue(value: unknown, label: string): ScenePath {
|
||||
return createScenePath({
|
||||
id: expectString(value.id, `${label}.id`),
|
||||
name:
|
||||
value.name === undefined ? undefined : expectString(value.name, `${label}.name`),
|
||||
value.name === undefined
|
||||
? undefined
|
||||
: expectString(value.name, `${label}.name`),
|
||||
visible: expectBoolean(value.visible, `${label}.visible`),
|
||||
enabled: expectBoolean(value.enabled, `${label}.enabled`),
|
||||
loop: expectBoolean(value.loop, `${label}.loop`),
|
||||
@@ -3609,13 +3696,15 @@ export function migrateSceneDocument(source: unknown): SceneDocument {
|
||||
source.version !== PROJECT_TIME_NIGHT_BACKGROUND_SCENE_DOCUMENT_VERSION &&
|
||||
source.version !== 33 &&
|
||||
source.version !== AUTHORED_OBJECT_STATE_SCENE_DOCUMENT_VERSION &&
|
||||
source.version !== PLAYER_START_AIR_DIRECTION_CONTROL_SCENE_DOCUMENT_VERSION &&
|
||||
source.version !==
|
||||
PLAYER_START_AIR_DIRECTION_CONTROL_SCENE_DOCUMENT_VERSION &&
|
||||
source.version !== PLAYER_START_AIR_CONTROL_SCENE_DOCUMENT_VERSION &&
|
||||
source.version !== PLAYER_START_MOVEMENT_TEMPLATE_SCENE_DOCUMENT_VERSION &&
|
||||
source.version !== PROJECT_NAME_SCENE_DOCUMENT_VERSION &&
|
||||
source.version !== STATIC_SIMPLE_MODEL_COLLIDERS_SCENE_DOCUMENT_VERSION &&
|
||||
source.version !== SCENE_EDITOR_PREFERENCES_SCENE_DOCUMENT_VERSION &&
|
||||
source.version !== PLAYER_START_GAMEPAD_CAMERA_LOOK_SCENE_DOCUMENT_VERSION &&
|
||||
source.version !==
|
||||
PLAYER_START_GAMEPAD_CAMERA_LOOK_SCENE_DOCUMENT_VERSION &&
|
||||
source.version !== PLAYER_START_INPUT_BINDINGS_SCENE_DOCUMENT_VERSION &&
|
||||
source.version !== PLAYER_START_NAVIGATION_MODE_SCENE_DOCUMENT_VERSION &&
|
||||
source.version !== SCENE_TRANSITION_ENTITIES_SCENE_DOCUMENT_VERSION &&
|
||||
|
||||
@@ -55,10 +55,7 @@ import {
|
||||
HOURS_PER_DAY,
|
||||
type ProjectTimeSettings
|
||||
} from "./project-time-settings";
|
||||
import {
|
||||
MIN_SCENE_PATH_POINT_COUNT,
|
||||
type ScenePath
|
||||
} from "./paths";
|
||||
import { MIN_SCENE_PATH_POINT_COUNT, type ScenePath } from "./paths";
|
||||
import {
|
||||
isAdvancedRenderingWaterReflectionMode,
|
||||
isAdvancedRenderingShadowMapSize,
|
||||
@@ -368,7 +365,9 @@ function validateWorldSettings(
|
||||
);
|
||||
}
|
||||
|
||||
if (!isNonNegativeFiniteNumber(world.timeOfDay.night.ambientIntensityFactor)) {
|
||||
if (
|
||||
!isNonNegativeFiniteNumber(world.timeOfDay.night.ambientIntensityFactor)
|
||||
) {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
"error",
|
||||
@@ -1524,7 +1523,11 @@ function validateEntityName(
|
||||
}
|
||||
}
|
||||
|
||||
function validateScenePath(pathValue: ScenePath, path: string, diagnostics: SceneDiagnostic[]) {
|
||||
function validateScenePath(
|
||||
pathValue: ScenePath,
|
||||
path: string,
|
||||
diagnostics: SceneDiagnostic[]
|
||||
) {
|
||||
if (!isBoolean(pathValue.visible)) {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
@@ -1793,9 +1796,7 @@ function validatePlayerStartEntity(
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!isNonNegativeFiniteNumber(entity.movementTemplate?.jump?.coyoteTimeMs)
|
||||
) {
|
||||
if (!isNonNegativeFiniteNumber(entity.movementTemplate?.jump?.coyoteTimeMs)) {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
"error",
|
||||
@@ -1916,7 +1917,11 @@ function validatePlayerStartEntity(
|
||||
);
|
||||
}
|
||||
|
||||
if (!isPlayerStartKeyboardBindingCode(entity.inputBindings?.keyboard.moveForward)) {
|
||||
if (
|
||||
!isPlayerStartKeyboardBindingCode(
|
||||
entity.inputBindings?.keyboard.moveForward
|
||||
)
|
||||
) {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
"error",
|
||||
@@ -1927,7 +1932,11 @@ function validatePlayerStartEntity(
|
||||
);
|
||||
}
|
||||
|
||||
if (!isPlayerStartKeyboardBindingCode(entity.inputBindings?.keyboard.moveBackward)) {
|
||||
if (
|
||||
!isPlayerStartKeyboardBindingCode(
|
||||
entity.inputBindings?.keyboard.moveBackward
|
||||
)
|
||||
) {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
"error",
|
||||
@@ -1938,7 +1947,9 @@ function validatePlayerStartEntity(
|
||||
);
|
||||
}
|
||||
|
||||
if (!isPlayerStartKeyboardBindingCode(entity.inputBindings?.keyboard.moveLeft)) {
|
||||
if (
|
||||
!isPlayerStartKeyboardBindingCode(entity.inputBindings?.keyboard.moveLeft)
|
||||
) {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
"error",
|
||||
@@ -1949,7 +1960,9 @@ function validatePlayerStartEntity(
|
||||
);
|
||||
}
|
||||
|
||||
if (!isPlayerStartKeyboardBindingCode(entity.inputBindings?.keyboard.moveRight)) {
|
||||
if (
|
||||
!isPlayerStartKeyboardBindingCode(entity.inputBindings?.keyboard.moveRight)
|
||||
) {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
"error",
|
||||
@@ -1971,7 +1984,9 @@ function validatePlayerStartEntity(
|
||||
);
|
||||
}
|
||||
|
||||
if (!isPlayerStartKeyboardBindingCode(entity.inputBindings?.keyboard.sprint)) {
|
||||
if (
|
||||
!isPlayerStartKeyboardBindingCode(entity.inputBindings?.keyboard.sprint)
|
||||
) {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
"error",
|
||||
@@ -1982,7 +1997,9 @@ function validatePlayerStartEntity(
|
||||
);
|
||||
}
|
||||
|
||||
if (!isPlayerStartKeyboardBindingCode(entity.inputBindings?.keyboard.crouch)) {
|
||||
if (
|
||||
!isPlayerStartKeyboardBindingCode(entity.inputBindings?.keyboard.crouch)
|
||||
) {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
"error",
|
||||
@@ -2004,7 +2021,9 @@ function validatePlayerStartEntity(
|
||||
);
|
||||
}
|
||||
|
||||
if (!isPlayerStartGamepadBinding(entity.inputBindings?.gamepad.moveBackward)) {
|
||||
if (
|
||||
!isPlayerStartGamepadBinding(entity.inputBindings?.gamepad.moveBackward)
|
||||
) {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
"error",
|
||||
@@ -2048,7 +2067,9 @@ function validatePlayerStartEntity(
|
||||
);
|
||||
}
|
||||
|
||||
if (!isPlayerStartGamepadActionBinding(entity.inputBindings?.gamepad.sprint)) {
|
||||
if (
|
||||
!isPlayerStartGamepadActionBinding(entity.inputBindings?.gamepad.sprint)
|
||||
) {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
"error",
|
||||
@@ -2059,7 +2080,9 @@ function validatePlayerStartEntity(
|
||||
);
|
||||
}
|
||||
|
||||
if (!isPlayerStartGamepadActionBinding(entity.inputBindings?.gamepad.crouch)) {
|
||||
if (
|
||||
!isPlayerStartGamepadActionBinding(entity.inputBindings?.gamepad.crouch)
|
||||
) {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
"error",
|
||||
@@ -2761,7 +2784,10 @@ function validateNpcEntity(
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof entity.actorId !== "string" || entity.actorId.trim().length === 0) {
|
||||
if (
|
||||
typeof entity.actorId !== "string" ||
|
||||
entity.actorId.trim().length === 0
|
||||
) {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
"error",
|
||||
@@ -2985,7 +3011,9 @@ function validateControlEffect(
|
||||
return;
|
||||
}
|
||||
case "stopModelAnimation":
|
||||
if (document.modelInstances[effect.target.modelInstanceId] === undefined) {
|
||||
if (
|
||||
document.modelInstances[effect.target.modelInstanceId] === undefined
|
||||
) {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
"error",
|
||||
@@ -3012,7 +3040,10 @@ function validateControlEffect(
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetEntity.kind !== effect.target.entityKind || targetEntity.kind !== "soundEmitter") {
|
||||
if (
|
||||
targetEntity.kind !== effect.target.entityKind ||
|
||||
targetEntity.kind !== "soundEmitter"
|
||||
) {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
"error",
|
||||
@@ -3886,9 +3917,7 @@ export function validateSceneDocument(
|
||||
}
|
||||
}
|
||||
|
||||
for (const [pathKey, pathValue] of Object.entries(
|
||||
document.paths
|
||||
)) {
|
||||
for (const [pathKey, pathValue] of Object.entries(document.paths)) {
|
||||
const path = `paths.${pathKey}`;
|
||||
|
||||
if (pathValue.id !== pathKey) {
|
||||
@@ -4133,8 +4162,7 @@ export function validateProjectDocument(
|
||||
}
|
||||
|
||||
const targetScenePath = `${scenePath}.entities.${entityId}.targetSceneId`;
|
||||
const targetEntryPath =
|
||||
`${scenePath}.entities.${entityId}.targetEntryEntityId`;
|
||||
const targetEntryPath = `${scenePath}.entities.${entityId}.targetEntryEntityId`;
|
||||
const targetScene = document.scenes[entity.targetSceneId];
|
||||
|
||||
if (targetScene === undefined) {
|
||||
|
||||
@@ -28,7 +28,8 @@ export const NPC_COLLIDER_SCENE_DOCUMENT_VERSION = 42 as const;
|
||||
export const NPC_ENTITY_FOUNDATION_SCENE_DOCUMENT_VERSION = 41 as const;
|
||||
export const WORLD_TIME_ENVIRONMENT_SCENE_DOCUMENT_VERSION = 40 as const;
|
||||
export const PROJECT_TIME_NIGHT_BACKGROUND_SCENE_DOCUMENT_VERSION = 39 as const;
|
||||
export const PROJECT_TIME_DAY_NIGHT_PROFILE_SCENE_DOCUMENT_VERSION = 38 as const;
|
||||
export const PROJECT_TIME_DAY_NIGHT_PROFILE_SCENE_DOCUMENT_VERSION =
|
||||
38 as const;
|
||||
export const PROJECT_TIME_SYSTEM_SCENE_DOCUMENT_VERSION = 37 as const;
|
||||
export const AUTHORED_OBJECT_STATE_SCENE_DOCUMENT_VERSION = 36 as const;
|
||||
export const PLAYER_START_AIR_DIRECTION_CONTROL_SCENE_DOCUMENT_VERSION =
|
||||
@@ -41,7 +42,8 @@ export const PLAYER_START_MOVEMENT_TEMPLATE_SCENE_DOCUMENT_VERSION =
|
||||
export const STATIC_SIMPLE_MODEL_COLLIDERS_SCENE_DOCUMENT_VERSION = 30 as const;
|
||||
export const SCENE_EDITOR_PREFERENCES_SCENE_DOCUMENT_VERSION = 29 as const;
|
||||
export const PROJECT_NAME_SCENE_DOCUMENT_VERSION = 28 as const;
|
||||
export const PLAYER_START_GAMEPAD_CAMERA_LOOK_SCENE_DOCUMENT_VERSION = 27 as const;
|
||||
export const PLAYER_START_GAMEPAD_CAMERA_LOOK_SCENE_DOCUMENT_VERSION =
|
||||
27 as const;
|
||||
export const PLAYER_START_INPUT_BINDINGS_SCENE_DOCUMENT_VERSION = 26 as const;
|
||||
export const PLAYER_START_NAVIGATION_MODE_SCENE_DOCUMENT_VERSION = 25 as const;
|
||||
export const SCENE_TRANSITION_ENTITIES_SCENE_DOCUMENT_VERSION = 24 as const;
|
||||
@@ -105,7 +107,10 @@ export interface SceneEditorPreferences {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
viewportPanels: Record<SceneEditorViewportPanelId, SceneEditorPanelPreferences>;
|
||||
viewportPanels: Record<
|
||||
SceneEditorViewportPanelId,
|
||||
SceneEditorPanelPreferences
|
||||
>;
|
||||
}
|
||||
|
||||
export interface SceneLoadingScreenSettings {
|
||||
@@ -206,12 +211,7 @@ export function createEmptyProjectDocument(
|
||||
overrides: Partial<
|
||||
Pick<
|
||||
ProjectDocument,
|
||||
| "name"
|
||||
| "time"
|
||||
| "activeSceneId"
|
||||
| "materials"
|
||||
| "textures"
|
||||
| "assets"
|
||||
"name" | "time" | "activeSceneId" | "materials" | "textures" | "assets"
|
||||
>
|
||||
> & {
|
||||
sceneId?: string;
|
||||
|
||||
@@ -206,7 +206,9 @@ export function createToggleVisibilityInteractionLink(
|
||||
assertNonEmptyString(options.targetBrushId, "Visibility target brush id");
|
||||
|
||||
if (options.visible !== undefined && typeof options.visible !== "boolean") {
|
||||
throw new Error("Visibility action visible must be a boolean when authored.");
|
||||
throw new Error(
|
||||
"Visibility action visible must be a boolean when authored."
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -326,13 +328,17 @@ export function getInteractionActionControlEffect(
|
||||
switch (action.type) {
|
||||
case "playAnimation":
|
||||
return createPlayModelAnimationControlEffect({
|
||||
target: createModelInstanceControlTargetRef(action.targetModelInstanceId),
|
||||
target: createModelInstanceControlTargetRef(
|
||||
action.targetModelInstanceId
|
||||
),
|
||||
clipName: action.clipName,
|
||||
loop: action.loop
|
||||
});
|
||||
case "stopAnimation":
|
||||
return createStopModelAnimationControlEffect({
|
||||
target: createModelInstanceControlTargetRef(action.targetModelInstanceId)
|
||||
target: createModelInstanceControlTargetRef(
|
||||
action.targetModelInstanceId
|
||||
)
|
||||
});
|
||||
case "playSound":
|
||||
return createPlaySoundControlEffect({
|
||||
@@ -410,7 +416,8 @@ export function areInteractionLinksEqual(
|
||||
return (
|
||||
left.action.targetModelInstanceId ===
|
||||
(right.action as PlayAnimationAction).targetModelInstanceId &&
|
||||
left.action.clipName === (right.action as PlayAnimationAction).clipName &&
|
||||
left.action.clipName ===
|
||||
(right.action as PlayAnimationAction).clipName &&
|
||||
left.action.loop === (right.action as PlayAnimationAction).loop
|
||||
);
|
||||
case "stopAnimation":
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user