Refactor: Improve type safety and readability across entity and player start logic

This commit is contained in:
2026-04-27 15:54:04 +02:00
parent 1d53f03fec
commit d09c550ffe
4 changed files with 436 additions and 171 deletions

View File

@@ -2747,10 +2747,14 @@ export function App({ store, initialStatusMessage }: AppProps) {
const [playerStartYawDraft, setPlayerStartYawDraft] = useState("0"); const [playerStartYawDraft, setPlayerStartYawDraft] = useState("0");
const [playerStartNavigationModeDraft, setPlayerStartNavigationModeDraft] = const [playerStartNavigationModeDraft, setPlayerStartNavigationModeDraft] =
useState<PlayerStartNavigationMode>(DEFAULT_PLAYER_START_NAVIGATION_MODE); useState<PlayerStartNavigationMode>(DEFAULT_PLAYER_START_NAVIGATION_MODE);
const [playerStartInteractionReachDraft, setPlayerStartInteractionReachDraft] = const [
useState(String(DEFAULT_PLAYER_START_INTERACTION_REACH_METERS)); playerStartInteractionReachDraft,
const [playerStartInteractionAngleDraft, setPlayerStartInteractionAngleDraft] = setPlayerStartInteractionReachDraft
useState(String(DEFAULT_PLAYER_START_INTERACTION_ANGLE_DEGREES)); ] = useState(String(DEFAULT_PLAYER_START_INTERACTION_REACH_METERS));
const [
playerStartInteractionAngleDraft,
setPlayerStartInteractionAngleDraft
] = useState(String(DEFAULT_PLAYER_START_INTERACTION_ANGLE_DEGREES));
const [ const [
playerStartMovementTemplateDraft, playerStartMovementTemplateDraft,
setPlayerStartMovementTemplateDraft setPlayerStartMovementTemplateDraft
@@ -13433,10 +13437,10 @@ export function App({ store, initialStatusMessage }: AppProps) {
{ {
<div <div
className="info-banner" className="info-banner"
data-testid="runner-interaction-help" data-testid="runner-interaction-help"
> >
Interact binding: {runtimeInteractInstruction}. Interact binding: {runtimeInteractInstruction}.
</div> </div>
} }
</Panel> </Panel>
</aside> </aside>
@@ -21679,7 +21683,8 @@ export function App({ store, initialStatusMessage }: AppProps) {
> >
<label className="form-field"> <label className="form-field">
<span className="label"> <span className="label">
{getPlayerStartInputActionLabel(action)} Key / Mouse {getPlayerStartInputActionLabel(action)} Key /
Mouse
</span> </span>
<button <button
type="button" type="button"
@@ -21740,7 +21745,8 @@ export function App({ store, initialStatusMessage }: AppProps) {
> >
<label className="form-field"> <label className="form-field">
<span className="label"> <span className="label">
{getPlayerStartInputActionLabel(action)} Key / Mouse {getPlayerStartInputActionLabel(action)} Key /
Mouse
</span> </span>
<button <button
type="button" type="button"
@@ -21801,7 +21807,8 @@ export function App({ store, initialStatusMessage }: AppProps) {
> >
<label className="form-field"> <label className="form-field">
<span className="label"> <span className="label">
{getPlayerStartInputActionLabel(action)} Key / Mouse {getPlayerStartInputActionLabel(action)} Key /
Mouse
</span> </span>
<button <button
type="button" type="button"

View File

@@ -123,8 +123,7 @@ export interface CameraRigBaseEntity extends AuthoredEntityState {
} }
export interface FixedCameraRigEntity export interface FixedCameraRigEntity
extends PositionedEntity, extends PositionedEntity, CameraRigBaseEntity {
CameraRigBaseEntity {
rigType: "fixed"; rigType: "fixed";
} }
@@ -191,7 +190,8 @@ export interface NpcEntity extends PositionedEntity {
} }
export const PLAYER_START_COLLIDER_MODES = ["capsule", "box", "none"] as const; export const PLAYER_START_COLLIDER_MODES = ["capsule", "box", "none"] as const;
export type PlayerStartColliderMode = (typeof PLAYER_START_COLLIDER_MODES)[number]; export type PlayerStartColliderMode =
(typeof PLAYER_START_COLLIDER_MODES)[number];
export const PLAYER_START_NAVIGATION_MODES = [ export const PLAYER_START_NAVIGATION_MODES = [
"firstPerson", "firstPerson",
"thirdPerson" "thirdPerson"
@@ -403,7 +403,9 @@ export type EntityInstance =
export type EntityKind = EntityInstance["kind"]; export type EntityKind = EntityInstance["kind"];
export interface EntityRegistryEntry<T extends EntityInstance = EntityInstance> { export interface EntityRegistryEntry<
T extends EntityInstance = EntityInstance
> {
kind: T["kind"]; kind: T["kind"];
label: string; label: string;
description: string; description: string;
@@ -573,7 +575,8 @@ export const DEFAULT_NPC_DIALOGUE_ID: string | null = null;
export const DEFAULT_NPC_COLLIDER_MODE: PlayerStartColliderMode = "capsule"; export const DEFAULT_NPC_COLLIDER_MODE: PlayerStartColliderMode = "capsule";
export const DEFAULT_NPC_TIME_WINDOW_START_HOUR = 9; export const DEFAULT_NPC_TIME_WINDOW_START_HOUR = 9;
export const DEFAULT_NPC_TIME_WINDOW_END_HOUR = 17; export const DEFAULT_NPC_TIME_WINDOW_END_HOUR = 17;
export const DEFAULT_PLAYER_START_COLLIDER_MODE: PlayerStartColliderMode = "capsule"; export const DEFAULT_PLAYER_START_COLLIDER_MODE: PlayerStartColliderMode =
"capsule";
export const DEFAULT_PLAYER_START_EYE_HEIGHT = 1.6; export const DEFAULT_PLAYER_START_EYE_HEIGHT = 1.6;
export const DEFAULT_PLAYER_START_CAPSULE_RADIUS = 0.3; export const DEFAULT_PLAYER_START_CAPSULE_RADIUS = 0.3;
export const DEFAULT_PLAYER_START_CAPSULE_HEIGHT = 1.8; export const DEFAULT_PLAYER_START_CAPSULE_HEIGHT = 1.8;
@@ -613,7 +616,11 @@ function areVec3Equal(left: Vec3, right: Vec3): boolean {
} }
function assertFiniteVec3(vector: Vec3, label: string) { function assertFiniteVec3(vector: Vec3, label: string) {
if (!Number.isFinite(vector.x) || !Number.isFinite(vector.y) || !Number.isFinite(vector.z)) { if (
!Number.isFinite(vector.x) ||
!Number.isFinite(vector.y) ||
!Number.isFinite(vector.z)
) {
throw new Error(`${label} must be finite on every axis.`); throw new Error(`${label} must be finite on every axis.`);
} }
} }
@@ -634,7 +641,9 @@ function assertPositiveFiniteVec3(vector: Vec3, label: string) {
function assertNonNegativeFiniteNumber(value: number, label: string) { function assertNonNegativeFiniteNumber(value: number, label: string) {
if (!Number.isFinite(value) || value < 0) { 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.`
);
} }
} }
@@ -656,7 +665,9 @@ function assertBoolean(value: boolean, label: string) {
} }
} }
export function isPlayerStartColliderMode(value: string): value is PlayerStartColliderMode { export function isPlayerStartColliderMode(
value: string
): value is PlayerStartColliderMode {
return PLAYER_START_COLLIDER_MODES.includes(value as PlayerStartColliderMode); return PLAYER_START_COLLIDER_MODES.includes(value as PlayerStartColliderMode);
} }
@@ -783,7 +794,9 @@ function normalizeCameraRigTargetActorId(actorId: string): string {
const normalizedActorId = actorId.trim(); const normalizedActorId = actorId.trim();
if (normalizedActorId.length === 0) { if (normalizedActorId.length === 0) {
throw new Error("Camera Rig actor targets must reference a non-empty actor id."); throw new Error(
"Camera Rig actor targets must reference a non-empty actor id."
);
} }
return normalizedActorId; return normalizedActorId;
@@ -793,7 +806,9 @@ function normalizeCameraRigTargetEntityId(entityId: string): string {
const normalizedEntityId = entityId.trim(); const normalizedEntityId = entityId.trim();
if (normalizedEntityId.length === 0) { if (normalizedEntityId.length === 0) {
throw new Error("Camera Rig entity targets must reference a non-empty entity id."); throw new Error(
"Camera Rig entity targets must reference a non-empty entity id."
);
} }
return normalizedEntityId; return normalizedEntityId;
@@ -843,7 +858,9 @@ export function createCameraRigWorldPointTargetRef(
}; };
} }
export function cloneCameraRigTargetRef(target: CameraRigTargetRef): CameraRigTargetRef { export function cloneCameraRigTargetRef(
target: CameraRigTargetRef
): CameraRigTargetRef {
switch (target.kind) { switch (target.kind) {
case "player": case "player":
return createCameraRigPlayerTargetRef(); return createCameraRigPlayerTargetRef();
@@ -882,15 +899,16 @@ export function areCameraRigTargetRefsEqual(
case "entity": case "entity":
return right.kind === "entity" && left.entityId === right.entityId; return right.kind === "entity" && left.entityId === right.entityId;
case "worldPoint": case "worldPoint":
return right.kind === "worldPoint" && areVec3Equal(left.point, right.point); return (
right.kind === "worldPoint" && areVec3Equal(left.point, right.point)
);
} }
} }
export function createCameraRigLookAroundSettings( export function createCameraRigLookAroundSettings(
overrides: Partial<CameraRigLookAroundSettings> = {} overrides: Partial<CameraRigLookAroundSettings> = {}
): CameraRigLookAroundSettings { ): CameraRigLookAroundSettings {
const enabled = const enabled = overrides.enabled ?? DEFAULT_CAMERA_RIG_LOOK_AROUND_ENABLED;
overrides.enabled ?? DEFAULT_CAMERA_RIG_LOOK_AROUND_ENABLED;
const yawLimitDegrees = const yawLimitDegrees =
overrides.yawLimitDegrees ?? overrides.yawLimitDegrees ??
DEFAULT_CAMERA_RIG_LOOK_AROUND_YAW_LIMIT_DEGREES; DEFAULT_CAMERA_RIG_LOOK_AROUND_YAW_LIMIT_DEGREES;
@@ -943,7 +961,10 @@ export function areCameraRigLookAroundSettingsEqual(
function getPrimaryCameraRigDocumentPlayerTarget( function getPrimaryCameraRigDocumentPlayerTarget(
entities: Record<string, EntityInstance> entities: Record<string, EntityInstance>
): PlayerStartEntity | null { ): PlayerStartEntity | null {
return getPrimaryEnabledPlayerStartEntity(entities) ?? getPrimaryPlayerStartEntity(entities); return (
getPrimaryEnabledPlayerStartEntity(entities) ??
getPrimaryPlayerStartEntity(entities)
);
} }
export function resolveCameraRigDocumentTargetPosition( export function resolveCameraRigDocumentTargetPosition(
@@ -952,7 +973,9 @@ export function resolveCameraRigDocumentTargetPosition(
): Vec3 | null { ): Vec3 | null {
switch (target.kind) { switch (target.kind) {
case "player": case "player":
return getPrimaryCameraRigDocumentPlayerTarget(entities)?.position ?? null; return (
getPrimaryCameraRigDocumentPlayerTarget(entities)?.position ?? null
);
case "actor": { case "actor": {
const enabledNpc = const enabledNpc =
getEntityInstances(entities).find( getEntityInstances(entities).find(
@@ -1012,10 +1035,7 @@ export function resolveCameraRigDocumentPosition(
} }
return rig.railPlacementMode === "mapTargetBetweenPoints" return rig.railPlacementMode === "mapTargetBetweenPoints"
? sampleResolvedScenePathPosition( ? sampleResolvedScenePathPosition(resolvedPath, rig.railStartProgress)
resolvedPath,
rig.railStartProgress
)
: resolvedPath.points.length > 0 : resolvedPath.points.length > 0
? cloneVec3(resolvedPath.points[0]!.position) ? cloneVec3(resolvedPath.points[0]!.position)
: null; : null;
@@ -1046,7 +1066,10 @@ export function resolveCameraRigDocumentLookTarget(
rig: Pick<CameraRigBaseEntity, "target" | "targetOffset">, rig: Pick<CameraRigBaseEntity, "target" | "targetOffset">,
entities: Record<string, EntityInstance> entities: Record<string, EntityInstance>
): Vec3 | null { ): Vec3 | null {
const baseTarget = resolveCameraRigDocumentTargetPosition(rig.target, entities); const baseTarget = resolveCameraRigDocumentTargetPosition(
rig.target,
entities
);
if (baseTarget === null) { if (baseTarget === null) {
return null; return null;
@@ -1196,14 +1219,11 @@ export function createPlayerStartInputBindings(
moveRight: moveRight:
overrides.gamepad?.moveRight ?? overrides.gamepad?.moveRight ??
DEFAULT_PLAYER_START_GAMEPAD_BINDINGS.moveRight, DEFAULT_PLAYER_START_GAMEPAD_BINDINGS.moveRight,
jump: jump: overrides.gamepad?.jump ?? DEFAULT_PLAYER_START_GAMEPAD_BINDINGS.jump,
overrides.gamepad?.jump ?? DEFAULT_PLAYER_START_GAMEPAD_BINDINGS.jump,
sprint: sprint:
overrides.gamepad?.sprint ?? overrides.gamepad?.sprint ?? DEFAULT_PLAYER_START_GAMEPAD_BINDINGS.sprint,
DEFAULT_PLAYER_START_GAMEPAD_BINDINGS.sprint,
crouch: crouch:
overrides.gamepad?.crouch ?? overrides.gamepad?.crouch ?? DEFAULT_PLAYER_START_GAMEPAD_BINDINGS.crouch,
DEFAULT_PLAYER_START_GAMEPAD_BINDINGS.crouch,
interact: interact:
overrides.gamepad?.interact ?? overrides.gamepad?.interact ??
DEFAULT_PLAYER_START_GAMEPAD_BINDINGS.interact, DEFAULT_PLAYER_START_GAMEPAD_BINDINGS.interact,
@@ -1219,19 +1239,27 @@ export function createPlayerStartInputBindings(
}; };
if (!isPlayerStartKeyboardBindingCode(keyboard.moveForward)) { if (!isPlayerStartKeyboardBindingCode(keyboard.moveForward)) {
throw new Error("Player Start move-forward keyboard binding must be supported."); throw new Error(
"Player Start move-forward keyboard binding must be supported."
);
} }
if (!isPlayerStartKeyboardBindingCode(keyboard.moveBackward)) { if (!isPlayerStartKeyboardBindingCode(keyboard.moveBackward)) {
throw new Error("Player Start move-backward keyboard binding must be supported."); throw new Error(
"Player Start move-backward keyboard binding must be supported."
);
} }
if (!isPlayerStartKeyboardBindingCode(keyboard.moveLeft)) { if (!isPlayerStartKeyboardBindingCode(keyboard.moveLeft)) {
throw new Error("Player Start move-left keyboard binding must be supported."); throw new Error(
"Player Start move-left keyboard binding must be supported."
);
} }
if (!isPlayerStartKeyboardBindingCode(keyboard.moveRight)) { if (!isPlayerStartKeyboardBindingCode(keyboard.moveRight)) {
throw new Error("Player Start move-right keyboard binding must be supported."); throw new Error(
"Player Start move-right keyboard binding must be supported."
);
} }
if (!isPlayerStartKeyboardBindingCode(keyboard.jump)) { if (!isPlayerStartKeyboardBindingCode(keyboard.jump)) {
@@ -1247,11 +1275,15 @@ export function createPlayerStartInputBindings(
} }
if (!isPlayerStartKeyboardBindingCode(keyboard.interact)) { if (!isPlayerStartKeyboardBindingCode(keyboard.interact)) {
throw new Error("Player Start interact keyboard binding must be supported."); throw new Error(
"Player Start interact keyboard binding must be supported."
);
} }
if (!isPlayerStartKeyboardBindingCode(keyboard.clearTarget)) { if (!isPlayerStartKeyboardBindingCode(keyboard.clearTarget)) {
throw new Error("Player Start clear-target keyboard binding must be supported."); throw new Error(
"Player Start clear-target keyboard binding must be supported."
);
} }
if (!isPlayerStartKeyboardBindingCode(keyboard.pauseTime)) { if (!isPlayerStartKeyboardBindingCode(keyboard.pauseTime)) {
@@ -1259,19 +1291,27 @@ export function createPlayerStartInputBindings(
} }
if (!isPlayerStartGamepadBinding(gamepad.moveForward)) { if (!isPlayerStartGamepadBinding(gamepad.moveForward)) {
throw new Error("Player Start move-forward gamepad binding must be supported."); throw new Error(
"Player Start move-forward gamepad binding must be supported."
);
} }
if (!isPlayerStartGamepadBinding(gamepad.moveBackward)) { if (!isPlayerStartGamepadBinding(gamepad.moveBackward)) {
throw new Error("Player Start move-backward gamepad binding must be supported."); throw new Error(
"Player Start move-backward gamepad binding must be supported."
);
} }
if (!isPlayerStartGamepadBinding(gamepad.moveLeft)) { if (!isPlayerStartGamepadBinding(gamepad.moveLeft)) {
throw new Error("Player Start move-left gamepad binding must be supported."); throw new Error(
"Player Start move-left gamepad binding must be supported."
);
} }
if (!isPlayerStartGamepadBinding(gamepad.moveRight)) { if (!isPlayerStartGamepadBinding(gamepad.moveRight)) {
throw new Error("Player Start move-right gamepad binding must be supported."); throw new Error(
"Player Start move-right gamepad binding must be supported."
);
} }
if (!isPlayerStartGamepadActionBinding(gamepad.jump)) { if (!isPlayerStartGamepadActionBinding(gamepad.jump)) {
@@ -1291,7 +1331,9 @@ export function createPlayerStartInputBindings(
} }
if (!isPlayerStartGamepadActionBinding(gamepad.clearTarget)) { if (!isPlayerStartGamepadActionBinding(gamepad.clearTarget)) {
throw new Error("Player Start clear-target gamepad binding must be supported."); throw new Error(
"Player Start clear-target gamepad binding must be supported."
);
} }
if (!isPlayerStartGamepadActionBinding(gamepad.pauseTime)) { if (!isPlayerStartGamepadActionBinding(gamepad.pauseTime)) {
@@ -1299,7 +1341,9 @@ export function createPlayerStartInputBindings(
} }
if (!isPlayerStartGamepadCameraLookBinding(gamepad.cameraLook)) { if (!isPlayerStartGamepadCameraLookBinding(gamepad.cameraLook)) {
throw new Error("Player Start camera-look gamepad binding must be supported."); throw new Error(
"Player Start camera-look gamepad binding must be supported."
);
} }
return { return {
@@ -1319,8 +1363,7 @@ export function isPlayerStartMovementTemplateKind(
export function createPlayerStartMovementTemplate( export function createPlayerStartMovementTemplate(
overrides: PlayerStartMovementTemplateOverrides = {} overrides: PlayerStartMovementTemplateOverrides = {}
): PlayerStartMovementTemplate { ): PlayerStartMovementTemplate {
const kind = const kind = overrides.kind ?? DEFAULT_PLAYER_START_MOVEMENT_TEMPLATE_KIND;
overrides.kind ?? DEFAULT_PLAYER_START_MOVEMENT_TEMPLATE_KIND;
if (!isPlayerStartMovementTemplateKind(kind)) { if (!isPlayerStartMovementTemplateKind(kind)) {
throw new Error( throw new Error(
@@ -1352,18 +1395,14 @@ export function createPlayerStartMovementTemplate(
const maxSpeed = overrides.maxSpeed ?? preset.maxSpeed; const maxSpeed = overrides.maxSpeed ?? preset.maxSpeed;
const maxStepHeight = overrides.maxStepHeight ?? preset.maxStepHeight; const maxStepHeight = overrides.maxStepHeight ?? preset.maxStepHeight;
const capabilities: PlayerStartMovementCapabilities = { const capabilities: PlayerStartMovementCapabilities = {
jump: jump: overrides.capabilities?.jump ?? preset.capabilities.jump,
overrides.capabilities?.jump ?? preset.capabilities.jump, sprint: overrides.capabilities?.sprint ?? preset.capabilities.sprint,
sprint: crouch: overrides.capabilities?.crouch ?? preset.capabilities.crouch
overrides.capabilities?.sprint ?? preset.capabilities.sprint,
crouch:
overrides.capabilities?.crouch ?? preset.capabilities.crouch
}; };
const jump: PlayerStartJumpSettings = { const jump: PlayerStartJumpSettings = {
speed: overrides.jump?.speed ?? preset.jump.speed, speed: overrides.jump?.speed ?? preset.jump.speed,
bufferMs: overrides.jump?.bufferMs ?? preset.jump.bufferMs, bufferMs: overrides.jump?.bufferMs ?? preset.jump.bufferMs,
coyoteTimeMs: coyoteTimeMs: overrides.jump?.coyoteTimeMs ?? preset.jump.coyoteTimeMs,
overrides.jump?.coyoteTimeMs ?? preset.jump.coyoteTimeMs,
variableHeight: variableHeight:
overrides.jump?.variableHeight ?? preset.jump.variableHeight, overrides.jump?.variableHeight ?? preset.jump.variableHeight,
maxHoldMs: overrides.jump?.maxHoldMs ?? preset.jump.maxHoldMs, maxHoldMs: overrides.jump?.maxHoldMs ?? preset.jump.maxHoldMs,
@@ -1371,11 +1410,9 @@ export function createPlayerStartMovementTemplate(
overrides.jump?.moveWhileJumping ?? preset.jump.moveWhileJumping, overrides.jump?.moveWhileJumping ?? preset.jump.moveWhileJumping,
moveWhileFalling: moveWhileFalling:
overrides.jump?.moveWhileFalling ?? preset.jump.moveWhileFalling, overrides.jump?.moveWhileFalling ?? preset.jump.moveWhileFalling,
directionOnly: directionOnly: overrides.jump?.directionOnly ?? preset.jump.directionOnly,
overrides.jump?.directionOnly ?? preset.jump.directionOnly,
bunnyHop: overrides.jump?.bunnyHop ?? preset.jump.bunnyHop, bunnyHop: overrides.jump?.bunnyHop ?? preset.jump.bunnyHop,
bunnyHopBoost: bunnyHopBoost: overrides.jump?.bunnyHopBoost ?? preset.jump.bunnyHopBoost
overrides.jump?.bunnyHopBoost ?? preset.jump.bunnyHopBoost
}; };
const sprint: PlayerStartSprintSettings = { const sprint: PlayerStartSprintSettings = {
speedMultiplier: speedMultiplier:
@@ -1388,10 +1425,7 @@ export function createPlayerStartMovementTemplate(
assertPositiveFiniteNumber(moveSpeed, "Player Start move speed"); assertPositiveFiniteNumber(moveSpeed, "Player Start move speed");
assertNonNegativeFiniteNumber(maxSpeed, "Player Start max speed"); assertNonNegativeFiniteNumber(maxSpeed, "Player Start max speed");
assertNonNegativeFiniteNumber( assertNonNegativeFiniteNumber(maxStepHeight, "Player Start max step height");
maxStepHeight,
"Player Start max step height"
);
assertBoolean( assertBoolean(
capabilities.jump, capabilities.jump,
"Player Start movement template jump capability" "Player Start movement template jump capability"
@@ -1429,10 +1463,7 @@ export function createPlayerStartMovementTemplate(
jump.moveWhileFalling, jump.moveWhileFalling,
"Player Start move while falling setting" "Player Start move while falling setting"
); );
assertBoolean( assertBoolean(jump.directionOnly, "Player Start air direction only setting");
jump.directionOnly,
"Player Start air direction only setting"
);
assertBoolean(jump.bunnyHop, "Player Start bunny hop setting"); assertBoolean(jump.bunnyHop, "Player Start bunny hop setting");
assertNonNegativeFiniteNumber( assertNonNegativeFiniteNumber(
jump.bunnyHopBoost, jump.bunnyHopBoost,
@@ -1460,7 +1491,9 @@ export function createPlayerStartMovementTemplate(
} }
export function inferPlayerStartMovementTemplateKind( export function inferPlayerStartMovementTemplateKind(
template: Omit<PlayerStartMovementTemplate, "kind"> | PlayerStartMovementTemplate template:
| Omit<PlayerStartMovementTemplate, "kind">
| PlayerStartMovementTemplate
): PlayerStartMovementTemplateKind { ): PlayerStartMovementTemplateKind {
const candidate = createPlayerStartMovementTemplate({ const candidate = createPlayerStartMovementTemplate({
kind: "custom", kind: "custom",
@@ -1486,35 +1519,47 @@ export function inferPlayerStartMovementTemplateKind(
candidate.maxStepHeight === candidate.maxStepHeight ===
createPlayerStartMovementTemplate({ kind: presetKind }).maxStepHeight && createPlayerStartMovementTemplate({ kind: presetKind }).maxStepHeight &&
candidate.capabilities.jump === candidate.capabilities.jump ===
createPlayerStartMovementTemplate({ kind: presetKind }).capabilities.jump && createPlayerStartMovementTemplate({ kind: presetKind }).capabilities
.jump &&
candidate.capabilities.sprint === candidate.capabilities.sprint ===
createPlayerStartMovementTemplate({ kind: presetKind }).capabilities.sprint && createPlayerStartMovementTemplate({ kind: presetKind }).capabilities
.sprint &&
candidate.capabilities.crouch === candidate.capabilities.crouch ===
createPlayerStartMovementTemplate({ kind: presetKind }).capabilities.crouch && createPlayerStartMovementTemplate({ kind: presetKind }).capabilities
.crouch &&
candidate.jump.speed === candidate.jump.speed ===
createPlayerStartMovementTemplate({ kind: presetKind }).jump.speed && createPlayerStartMovementTemplate({ kind: presetKind }).jump.speed &&
candidate.jump.bufferMs === candidate.jump.bufferMs ===
createPlayerStartMovementTemplate({ kind: presetKind }).jump.bufferMs && createPlayerStartMovementTemplate({ kind: presetKind }).jump.bufferMs &&
candidate.jump.coyoteTimeMs === candidate.jump.coyoteTimeMs ===
createPlayerStartMovementTemplate({ kind: presetKind }).jump.coyoteTimeMs && createPlayerStartMovementTemplate({ kind: presetKind }).jump
.coyoteTimeMs &&
candidate.jump.variableHeight === candidate.jump.variableHeight ===
createPlayerStartMovementTemplate({ kind: presetKind }).jump.variableHeight && createPlayerStartMovementTemplate({ kind: presetKind }).jump
.variableHeight &&
candidate.jump.maxHoldMs === candidate.jump.maxHoldMs ===
createPlayerStartMovementTemplate({ kind: presetKind }).jump.maxHoldMs && createPlayerStartMovementTemplate({ kind: presetKind }).jump
.maxHoldMs &&
candidate.jump.moveWhileJumping === candidate.jump.moveWhileJumping ===
createPlayerStartMovementTemplate({ kind: presetKind }).jump.moveWhileJumping && createPlayerStartMovementTemplate({ kind: presetKind }).jump
.moveWhileJumping &&
candidate.jump.moveWhileFalling === candidate.jump.moveWhileFalling ===
createPlayerStartMovementTemplate({ kind: presetKind }).jump.moveWhileFalling && createPlayerStartMovementTemplate({ kind: presetKind }).jump
.moveWhileFalling &&
candidate.jump.directionOnly === candidate.jump.directionOnly ===
createPlayerStartMovementTemplate({ kind: presetKind }).jump.directionOnly && createPlayerStartMovementTemplate({ kind: presetKind }).jump
.directionOnly &&
candidate.jump.bunnyHop === candidate.jump.bunnyHop ===
createPlayerStartMovementTemplate({ kind: presetKind }).jump.bunnyHop && createPlayerStartMovementTemplate({ kind: presetKind }).jump.bunnyHop &&
candidate.jump.bunnyHopBoost === candidate.jump.bunnyHopBoost ===
createPlayerStartMovementTemplate({ kind: presetKind }).jump.bunnyHopBoost && createPlayerStartMovementTemplate({ kind: presetKind }).jump
.bunnyHopBoost &&
candidate.sprint.speedMultiplier === candidate.sprint.speedMultiplier ===
createPlayerStartMovementTemplate({ kind: presetKind }).sprint.speedMultiplier && createPlayerStartMovementTemplate({ kind: presetKind }).sprint
.speedMultiplier &&
candidate.crouch.speedMultiplier === candidate.crouch.speedMultiplier ===
createPlayerStartMovementTemplate({ kind: presetKind }).crouch.speedMultiplier createPlayerStartMovementTemplate({ kind: presetKind }).crouch
.speedMultiplier
) { ) {
return presetKind; return presetKind;
} }
@@ -1609,9 +1654,12 @@ function createCharacterColliderSettings(
overrides: Partial<CharacterColliderSettings> = {}, overrides: Partial<CharacterColliderSettings> = {},
defaults: Partial<CharacterColliderSettings> = {} defaults: Partial<CharacterColliderSettings> = {}
): CharacterColliderSettings { ): CharacterColliderSettings {
const mode = overrides.mode ?? defaults.mode ?? DEFAULT_PLAYER_START_COLLIDER_MODE; const mode =
overrides.mode ?? defaults.mode ?? DEFAULT_PLAYER_START_COLLIDER_MODE;
const eyeHeight = const eyeHeight =
overrides.eyeHeight ?? defaults.eyeHeight ?? DEFAULT_PLAYER_START_EYE_HEIGHT; overrides.eyeHeight ??
defaults.eyeHeight ??
DEFAULT_PLAYER_START_EYE_HEIGHT;
const capsuleRadius = const capsuleRadius =
overrides.capsuleRadius ?? overrides.capsuleRadius ??
defaults.capsuleRadius ?? defaults.capsuleRadius ??
@@ -1678,7 +1726,9 @@ export function createNpcColliderSettings(
}); });
} }
function normalizeSoundEmitterAudioAssetId(audioAssetId: string | null | undefined): string | null { function normalizeSoundEmitterAudioAssetId(
audioAssetId: string | null | undefined
): string | null {
if (audioAssetId === undefined || audioAssetId === null) { if (audioAssetId === undefined || audioAssetId === null) {
return null; return null;
} }
@@ -1686,13 +1736,17 @@ function normalizeSoundEmitterAudioAssetId(audioAssetId: string | null | undefin
const trimmedAudioAssetId = audioAssetId.trim(); const trimmedAudioAssetId = audioAssetId.trim();
if (trimmedAudioAssetId.length === 0) { if (trimmedAudioAssetId.length === 0) {
throw new Error("Sound Emitter audio asset id must be non-empty when authored."); throw new Error(
"Sound Emitter audio asset id must be non-empty when authored."
);
} }
return trimmedAudioAssetId; return trimmedAudioAssetId;
} }
export function normalizeEntityName(name: string | null | undefined): string | undefined { export function normalizeEntityName(
name: string | null | undefined
): string | undefined {
if (name === undefined || name === null) { if (name === undefined || name === null) {
return undefined; return undefined;
} }
@@ -1701,7 +1755,9 @@ export function normalizeEntityName(name: string | null | undefined): string | u
return trimmedName.length === 0 ? undefined : trimmedName; return trimmedName.length === 0 ? undefined : trimmedName;
} }
function resolveAuthoredEntityVisibility(visible: boolean | undefined): boolean { function resolveAuthoredEntityVisibility(
visible: boolean | undefined
): boolean {
const resolvedVisible = visible ?? DEFAULT_ENTITY_VISIBLE; const resolvedVisible = visible ?? DEFAULT_ENTITY_VISIBLE;
assertBoolean(resolvedVisible, "Entity visible"); assertBoolean(resolvedVisible, "Entity visible");
@@ -1725,7 +1781,10 @@ export function createNpcActorId(): string {
} }
export function isNpcPresenceMode(value: unknown): value is NpcPresenceMode { export function isNpcPresenceMode(value: unknown): value is NpcPresenceMode {
return typeof value === "string" && NPC_PRESENCE_MODES.includes(value as NpcPresenceMode); return (
typeof value === "string" &&
NPC_PRESENCE_MODES.includes(value as NpcPresenceMode)
);
} }
export function createNpcAlwaysPresence(): NpcAlwaysPresence { export function createNpcAlwaysPresence(): NpcAlwaysPresence {
@@ -1787,9 +1846,7 @@ export function areNpcPresencesEqual(
); );
} }
function normalizeNpcPresence( function normalizeNpcPresence(presence: NpcPresence | undefined): NpcPresence {
presence: NpcPresence | undefined
): NpcPresence {
if (presence === undefined) { if (presence === undefined) {
return createNpcAlwaysPresence(); return createNpcAlwaysPresence();
} }
@@ -1848,7 +1905,9 @@ function normalizeNpcDefaultDialogueId(
return null; return null;
} }
return dialogues.some((dialogue) => dialogue.id === normalizedDefaultDialogueId) return dialogues.some(
(dialogue) => dialogue.id === normalizedDefaultDialogueId
)
? normalizedDefaultDialogueId ? normalizedDefaultDialogueId
: null; : null;
} }
@@ -1864,9 +1923,23 @@ export function normalizeInteractablePrompt(prompt: string): string {
} }
export function createPointLightEntity( export function createPointLightEntity(
overrides: Partial<Pick<PointLightEntity, "id" | "name" | "visible" | "enabled" | "position" | "colorHex" | "intensity" | "distance">> = {} overrides: Partial<
Pick<
PointLightEntity,
| "id"
| "name"
| "visible"
| "enabled"
| "position"
| "colorHex"
| "intensity"
| "distance"
>
> = {}
): PointLightEntity { ): PointLightEntity {
const position = cloneVec3(overrides.position ?? DEFAULT_POINT_LIGHT_POSITION); const position = cloneVec3(
overrides.position ?? DEFAULT_POINT_LIGHT_POSITION
);
const colorHex = overrides.colorHex ?? DEFAULT_POINT_LIGHT_COLOR_HEX; const colorHex = overrides.colorHex ?? DEFAULT_POINT_LIGHT_COLOR_HEX;
const intensity = overrides.intensity ?? DEFAULT_POINT_LIGHT_INTENSITY; const intensity = overrides.intensity ?? DEFAULT_POINT_LIGHT_INTENSITY;
const distance = overrides.distance ?? DEFAULT_POINT_LIGHT_DISTANCE; const distance = overrides.distance ?? DEFAULT_POINT_LIGHT_DISTANCE;
@@ -1890,14 +1963,31 @@ export function createPointLightEntity(
} }
export function createSpotLightEntity( export function createSpotLightEntity(
overrides: Partial<Pick<SpotLightEntity, "id" | "name" | "visible" | "enabled" | "position" | "direction" | "colorHex" | "intensity" | "distance" | "angleDegrees">> = {} overrides: Partial<
Pick<
SpotLightEntity,
| "id"
| "name"
| "visible"
| "enabled"
| "position"
| "direction"
| "colorHex"
| "intensity"
| "distance"
| "angleDegrees"
>
> = {}
): SpotLightEntity { ): SpotLightEntity {
const position = cloneVec3(overrides.position ?? DEFAULT_SPOT_LIGHT_POSITION); const position = cloneVec3(overrides.position ?? DEFAULT_SPOT_LIGHT_POSITION);
const direction = cloneVec3(overrides.direction ?? DEFAULT_SPOT_LIGHT_DIRECTION); const direction = cloneVec3(
overrides.direction ?? DEFAULT_SPOT_LIGHT_DIRECTION
);
const colorHex = overrides.colorHex ?? DEFAULT_SPOT_LIGHT_COLOR_HEX; const colorHex = overrides.colorHex ?? DEFAULT_SPOT_LIGHT_COLOR_HEX;
const intensity = overrides.intensity ?? DEFAULT_SPOT_LIGHT_INTENSITY; const intensity = overrides.intensity ?? DEFAULT_SPOT_LIGHT_INTENSITY;
const distance = overrides.distance ?? DEFAULT_SPOT_LIGHT_DISTANCE; const distance = overrides.distance ?? DEFAULT_SPOT_LIGHT_DISTANCE;
const angleDegrees = overrides.angleDegrees ?? DEFAULT_SPOT_LIGHT_ANGLE_DEGREES; const angleDegrees =
overrides.angleDegrees ?? DEFAULT_SPOT_LIGHT_ANGLE_DEGREES;
assertFiniteVec3(position, "Spot Light position"); assertFiniteVec3(position, "Spot Light position");
assertFiniteVec3(direction, "Spot Light direction"); assertFiniteVec3(direction, "Spot Light direction");
@@ -1906,8 +1996,14 @@ export function createSpotLightEntity(
assertNonNegativeFiniteNumber(intensity, "Spot Light intensity"); assertNonNegativeFiniteNumber(intensity, "Spot Light intensity");
assertPositiveFiniteNumber(distance, "Spot Light distance"); assertPositiveFiniteNumber(distance, "Spot Light distance");
if (!Number.isFinite(angleDegrees) || angleDegrees <= 0 || angleDegrees >= 180) { if (
throw new Error("Spot Light angle must be a finite degree value between 0 and 180."); !Number.isFinite(angleDegrees) ||
angleDegrees <= 0 ||
angleDegrees >= 180
) {
throw new Error(
"Spot Light angle must be a finite degree value between 0 and 180."
);
} }
return { return {
@@ -1947,7 +2043,9 @@ export function createPlayerStartEntity(
collider?: Partial<PlayerStartColliderSettings>; collider?: Partial<PlayerStartColliderSettings>;
} = {} } = {}
): PlayerStartEntity { ): PlayerStartEntity {
const position = cloneVec3(overrides.position ?? DEFAULT_PLAYER_START_POSITION); const position = cloneVec3(
overrides.position ?? DEFAULT_PLAYER_START_POSITION
);
const yawDegrees = overrides.yawDegrees ?? DEFAULT_PLAYER_START_YAW_DEGREES; const yawDegrees = overrides.yawDegrees ?? DEFAULT_PLAYER_START_YAW_DEGREES;
const navigationMode = const navigationMode =
overrides.navigationMode ?? DEFAULT_PLAYER_START_NAVIGATION_MODE; overrides.navigationMode ?? DEFAULT_PLAYER_START_NAVIGATION_MODE;
@@ -2024,7 +2122,12 @@ export function createPlayerStartEntity(
} }
export function createSceneEntryEntity( export function createSceneEntryEntity(
overrides: Partial<Pick<SceneEntryEntity, "id" | "name" | "visible" | "enabled" | "position" | "yawDegrees">> = {} overrides: Partial<
Pick<
SceneEntryEntity,
"id" | "name" | "visible" | "enabled" | "position" | "yawDegrees"
>
> = {}
): SceneEntryEntity { ): SceneEntryEntity {
const position = cloneVec3(overrides.position ?? DEFAULT_ENTITY_POSITION); const position = cloneVec3(overrides.position ?? DEFAULT_ENTITY_POSITION);
const yawDegrees = overrides.yawDegrees ?? DEFAULT_SCENE_ENTRY_YAW_DEGREES; const yawDegrees = overrides.yawDegrees ?? DEFAULT_SCENE_ENTRY_YAW_DEGREES;
@@ -2204,8 +2307,7 @@ export function createCameraRigEntity(
DEFAULT_CAMERA_RIG_TRACK_START_POINT DEFAULT_CAMERA_RIG_TRACK_START_POINT
); );
const trackEndPoint = cloneVec3( const trackEndPoint = cloneVec3(
mappedRailOverrides.trackEndPoint ?? mappedRailOverrides.trackEndPoint ?? DEFAULT_CAMERA_RIG_TRACK_END_POINT
DEFAULT_CAMERA_RIG_TRACK_END_POINT
); );
const railStartProgress = const railStartProgress =
mappedRailOverrides.railStartProgress ?? mappedRailOverrides.railStartProgress ??
@@ -2325,15 +2427,29 @@ export function createSoundEmitterEntity(
overrides: Partial< overrides: Partial<
Pick< Pick<
SoundEmitterEntity, SoundEmitterEntity,
"id" | "name" | "visible" | "enabled" | "position" | "audioAssetId" | "volume" | "refDistance" | "maxDistance" | "autoplay" | "loop" | "id"
| "name"
| "visible"
| "enabled"
| "position"
| "audioAssetId"
| "volume"
| "refDistance"
| "maxDistance"
| "autoplay"
| "loop"
> >
> = {} > = {}
): SoundEmitterEntity { ): SoundEmitterEntity {
const position = cloneVec3(overrides.position ?? DEFAULT_ENTITY_POSITION); const position = cloneVec3(overrides.position ?? DEFAULT_ENTITY_POSITION);
const audioAssetId = normalizeSoundEmitterAudioAssetId(overrides.audioAssetId ?? DEFAULT_SOUND_EMITTER_AUDIO_ASSET_ID); const audioAssetId = normalizeSoundEmitterAudioAssetId(
overrides.audioAssetId ?? DEFAULT_SOUND_EMITTER_AUDIO_ASSET_ID
);
const volume = overrides.volume ?? DEFAULT_SOUND_EMITTER_VOLUME; const volume = overrides.volume ?? DEFAULT_SOUND_EMITTER_VOLUME;
const refDistance = overrides.refDistance ?? DEFAULT_SOUND_EMITTER_REF_DISTANCE; const refDistance =
const maxDistance = overrides.maxDistance ?? DEFAULT_SOUND_EMITTER_MAX_DISTANCE; overrides.refDistance ?? DEFAULT_SOUND_EMITTER_REF_DISTANCE;
const maxDistance =
overrides.maxDistance ?? DEFAULT_SOUND_EMITTER_MAX_DISTANCE;
const autoplay = overrides.autoplay ?? false; const autoplay = overrides.autoplay ?? false;
const loop = overrides.loop ?? false; const loop = overrides.loop ?? false;
@@ -2343,7 +2459,9 @@ export function createSoundEmitterEntity(
assertPositiveFiniteNumber(maxDistance, "Sound Emitter max distance"); assertPositiveFiniteNumber(maxDistance, "Sound Emitter max distance");
if (maxDistance < refDistance) { if (maxDistance < refDistance) {
throw new Error("Sound Emitter max distance must be greater than or equal to ref distance."); throw new Error(
"Sound Emitter max distance must be greater than or equal to ref distance."
);
} }
assertBoolean(autoplay, "Sound Emitter autoplay"); assertBoolean(autoplay, "Sound Emitter autoplay");
@@ -2366,7 +2484,19 @@ export function createSoundEmitterEntity(
} }
export function createTriggerVolumeEntity( export function createTriggerVolumeEntity(
overrides: Partial<Pick<TriggerVolumeEntity, "id" | "name" | "visible" | "enabled" | "position" | "size" | "triggerOnEnter" | "triggerOnExit">> = {} overrides: Partial<
Pick<
TriggerVolumeEntity,
| "id"
| "name"
| "visible"
| "enabled"
| "position"
| "size"
| "triggerOnEnter"
| "triggerOnExit"
>
> = {}
): TriggerVolumeEntity { ): TriggerVolumeEntity {
const position = cloneVec3(overrides.position ?? DEFAULT_ENTITY_POSITION); const position = cloneVec3(overrides.position ?? DEFAULT_ENTITY_POSITION);
const size = cloneVec3(overrides.size ?? DEFAULT_TRIGGER_VOLUME_SIZE); const size = cloneVec3(overrides.size ?? DEFAULT_TRIGGER_VOLUME_SIZE);
@@ -2392,10 +2522,16 @@ export function createTriggerVolumeEntity(
} }
export function createTeleportTargetEntity( export function createTeleportTargetEntity(
overrides: Partial<Pick<TeleportTargetEntity, "id" | "name" | "visible" | "enabled" | "position" | "yawDegrees">> = {} overrides: Partial<
Pick<
TeleportTargetEntity,
"id" | "name" | "visible" | "enabled" | "position" | "yawDegrees"
>
> = {}
): TeleportTargetEntity { ): TeleportTargetEntity {
const position = cloneVec3(overrides.position ?? DEFAULT_ENTITY_POSITION); const position = cloneVec3(overrides.position ?? DEFAULT_ENTITY_POSITION);
const yawDegrees = overrides.yawDegrees ?? DEFAULT_TELEPORT_TARGET_YAW_DEGREES; const yawDegrees =
overrides.yawDegrees ?? DEFAULT_TELEPORT_TARGET_YAW_DEGREES;
assertFiniteVec3(position, "Teleport Target position"); assertFiniteVec3(position, "Teleport Target position");
@@ -2415,11 +2551,25 @@ export function createTeleportTargetEntity(
} }
export function createInteractableEntity( export function createInteractableEntity(
overrides: Partial<Pick<InteractableEntity, "id" | "name" | "visible" | "enabled" | "position" | "radius" | "prompt" | "interactionEnabled">> = {} overrides: Partial<
Pick<
InteractableEntity,
| "id"
| "name"
| "visible"
| "enabled"
| "position"
| "radius"
| "prompt"
| "interactionEnabled"
>
> = {}
): InteractableEntity { ): InteractableEntity {
const position = cloneVec3(overrides.position ?? DEFAULT_ENTITY_POSITION); const position = cloneVec3(overrides.position ?? DEFAULT_ENTITY_POSITION);
const radius = overrides.radius ?? DEFAULT_INTERACTABLE_RADIUS; const radius = overrides.radius ?? DEFAULT_INTERACTABLE_RADIUS;
const prompt = normalizeInteractablePrompt(overrides.prompt ?? DEFAULT_INTERACTABLE_PROMPT); const prompt = normalizeInteractablePrompt(
overrides.prompt ?? DEFAULT_INTERACTABLE_PROMPT
);
const interactionEnabled = overrides.interactionEnabled ?? true; const interactionEnabled = overrides.interactionEnabled ?? true;
assertFiniteVec3(position, "Interactable position"); assertFiniteVec3(position, "Interactable position");
@@ -2439,17 +2589,21 @@ export function createInteractableEntity(
}; };
} }
export const ENTITY_REGISTRY: { [K in EntityKind]: EntityRegistryEntry<Extract<EntityInstance, { kind: K }>> } = { export const ENTITY_REGISTRY: {
[K in EntityKind]: EntityRegistryEntry<Extract<EntityInstance, { kind: K }>>;
} = {
pointLight: { pointLight: {
kind: "pointLight", kind: "pointLight",
label: "Point Light", label: "Point Light",
description: "Authored local point light that illuminates nearby geometry in a spherical radius.", description:
"Authored local point light that illuminates nearby geometry in a spherical radius.",
createDefaultEntity: createPointLightEntity createDefaultEntity: createPointLightEntity
}, },
spotLight: { spotLight: {
kind: "spotLight", kind: "spotLight",
label: "Spot Light", label: "Spot Light",
description: "Authored local spotlight with an explicit direction and cone angle.", description:
"Authored local spotlight with an explicit direction and cone angle.",
createDefaultEntity: createSpotLightEntity createDefaultEntity: createSpotLightEntity
}, },
playerStart: { playerStart: {
@@ -2484,48 +2638,90 @@ export const ENTITY_REGISTRY: { [K in EntityKind]: EntityRegistryEntry<Extract<E
soundEmitter: { soundEmitter: {
kind: "soundEmitter", kind: "soundEmitter",
label: "Sound Emitter", label: "Sound Emitter",
description: "Authored positional audio source wired to an audio asset and configurable for looping, volume, and distance falloff.", description:
"Authored positional audio source wired to an audio asset and configurable for looping, volume, and distance falloff.",
createDefaultEntity: createSoundEmitterEntity createDefaultEntity: createSoundEmitterEntity
}, },
triggerVolume: { triggerVolume: {
kind: "triggerVolume", kind: "triggerVolume",
label: "Trigger Volume", label: "Trigger Volume",
description: "Axis-aligned authored trigger volume for enter and exit events.", description:
"Axis-aligned authored trigger volume for enter and exit events.",
createDefaultEntity: createTriggerVolumeEntity createDefaultEntity: createTriggerVolumeEntity
}, },
teleportTarget: { teleportTarget: {
kind: "teleportTarget", kind: "teleportTarget",
label: "Teleport Target", label: "Teleport Target",
description: "Explicit authored teleport destination with a facing direction.", description:
"Explicit authored teleport destination with a facing direction.",
createDefaultEntity: createTeleportTargetEntity createDefaultEntity: createTeleportTargetEntity
}, },
interactable: { interactable: {
kind: "interactable", kind: "interactable",
label: "Interactable", label: "Interactable",
description: "Explicit authored interaction point for later click and use behavior.", description:
"Explicit authored interaction point for later click and use behavior.",
createDefaultEntity: createInteractableEntity createDefaultEntity: createInteractableEntity
} }
}; };
export function isEntityKind(value: unknown): value is EntityKind { export function isEntityKind(value: unknown): value is EntityKind {
return typeof value === "string" && Object.prototype.hasOwnProperty.call(ENTITY_REGISTRY, value); return (
typeof value === "string" &&
Object.prototype.hasOwnProperty.call(ENTITY_REGISTRY, value)
);
} }
export function getEntityRegistryEntry<K extends EntityKind>(kind: K): EntityRegistryEntry<Extract<EntityInstance, { kind: K }>> { export function getEntityRegistryEntry<K extends EntityKind>(
kind: K
): EntityRegistryEntry<Extract<EntityInstance, { kind: K }>> {
return ENTITY_REGISTRY[kind]; return ENTITY_REGISTRY[kind];
} }
export function createDefaultEntityInstance(kind: "playerStart", overrides?: Partial<PlayerStartEntity>): PlayerStartEntity; export function createDefaultEntityInstance(
export function createDefaultEntityInstance(kind: "cameraRig", overrides?: Partial<CameraRigEntity>): CameraRigEntity; kind: "playerStart",
export function createDefaultEntityInstance(kind: "sceneEntry", overrides?: Partial<SceneEntryEntity>): SceneEntryEntity; overrides?: Partial<PlayerStartEntity>
export function createDefaultEntityInstance(kind: "npc", overrides?: Partial<NpcEntity>): NpcEntity; ): PlayerStartEntity;
export function createDefaultEntityInstance(kind: "pointLight", overrides?: Partial<PointLightEntity>): PointLightEntity; export function createDefaultEntityInstance(
export function createDefaultEntityInstance(kind: "spotLight", overrides?: Partial<SpotLightEntity>): SpotLightEntity; kind: "cameraRig",
export function createDefaultEntityInstance(kind: "soundEmitter", overrides?: Partial<SoundEmitterEntity>): SoundEmitterEntity; overrides?: Partial<CameraRigEntity>
export function createDefaultEntityInstance(kind: "triggerVolume", overrides?: Partial<TriggerVolumeEntity>): TriggerVolumeEntity; ): CameraRigEntity;
export function createDefaultEntityInstance(kind: "teleportTarget", overrides?: Partial<TeleportTargetEntity>): TeleportTargetEntity; export function createDefaultEntityInstance(
export function createDefaultEntityInstance(kind: "interactable", overrides?: Partial<InteractableEntity>): InteractableEntity; kind: "sceneEntry",
export function createDefaultEntityInstance(kind: EntityKind, overrides: Partial<EntityInstance> = {}): EntityInstance { overrides?: Partial<SceneEntryEntity>
): SceneEntryEntity;
export function createDefaultEntityInstance(
kind: "npc",
overrides?: Partial<NpcEntity>
): NpcEntity;
export function createDefaultEntityInstance(
kind: "pointLight",
overrides?: Partial<PointLightEntity>
): PointLightEntity;
export function createDefaultEntityInstance(
kind: "spotLight",
overrides?: Partial<SpotLightEntity>
): SpotLightEntity;
export function createDefaultEntityInstance(
kind: "soundEmitter",
overrides?: Partial<SoundEmitterEntity>
): SoundEmitterEntity;
export function createDefaultEntityInstance(
kind: "triggerVolume",
overrides?: Partial<TriggerVolumeEntity>
): TriggerVolumeEntity;
export function createDefaultEntityInstance(
kind: "teleportTarget",
overrides?: Partial<TeleportTargetEntity>
): TeleportTargetEntity;
export function createDefaultEntityInstance(
kind: "interactable",
overrides?: Partial<InteractableEntity>
): InteractableEntity;
export function createDefaultEntityInstance(
kind: EntityKind,
overrides: Partial<EntityInstance> = {}
): EntityInstance {
switch (kind) { switch (kind) {
case "pointLight": case "pointLight":
return createPointLightEntity(overrides); return createPointLightEntity(overrides);
@@ -2575,11 +2771,21 @@ export function cloneEntityInstance(entity: EntityInstance): EntityInstance {
} }
} }
export function cloneEntityRegistry(entities: Record<string, EntityInstance>): Record<string, EntityInstance> { export function cloneEntityRegistry(
return Object.fromEntries(Object.entries(entities).map(([entityId, entity]) => [entityId, cloneEntityInstance(entity)])); entities: Record<string, EntityInstance>
): Record<string, EntityInstance> {
return Object.fromEntries(
Object.entries(entities).map(([entityId, entity]) => [
entityId,
cloneEntityInstance(entity)
])
);
} }
export function areEntityInstancesEqual(left: EntityInstance, right: EntityInstance): boolean { export function areEntityInstancesEqual(
left: EntityInstance,
right: EntityInstance
): boolean {
if ( if (
left.kind !== right.kind || left.kind !== right.kind ||
left.id !== right.id || left.id !== right.id ||
@@ -2650,7 +2856,10 @@ export function areEntityInstancesEqual(left: EntityInstance, right: EntityInsta
left.railPlacementMode === typedRight.railPlacementMode && left.railPlacementMode === typedRight.railPlacementMode &&
(left.railPlacementMode === "mapTargetBetweenPoints" (left.railPlacementMode === "mapTargetBetweenPoints"
? typedRight.railPlacementMode === "mapTargetBetweenPoints" && ? typedRight.railPlacementMode === "mapTargetBetweenPoints" &&
areVec3Equal(left.trackStartPoint, typedRight.trackStartPoint) && areVec3Equal(
left.trackStartPoint,
typedRight.trackStartPoint
) &&
areVec3Equal(left.trackEndPoint, typedRight.trackEndPoint) && areVec3Equal(left.trackEndPoint, typedRight.trackEndPoint) &&
left.railStartProgress === typedRight.railStartProgress && left.railStartProgress === typedRight.railStartProgress &&
left.railEndProgress === typedRight.railEndProgress left.railEndProgress === typedRight.railEndProgress
@@ -2735,7 +2944,10 @@ export function areEntityInstancesEqual(left: EntityInstance, right: EntityInsta
} }
} }
export function compareEntityInstances(left: EntityInstance, right: EntityInstance): number { export function compareEntityInstances(
left: EntityInstance,
right: EntityInstance
): number {
const leftOrder = ENTITY_KIND_ORDER.indexOf(left.kind); const leftOrder = ENTITY_KIND_ORDER.indexOf(left.kind);
const rightOrder = ENTITY_KIND_ORDER.indexOf(right.kind); const rightOrder = ENTITY_KIND_ORDER.indexOf(right.kind);
@@ -2746,7 +2958,9 @@ export function compareEntityInstances(left: EntityInstance, right: EntityInstan
return left.id.localeCompare(right.id); return left.id.localeCompare(right.id);
} }
export function getEntityInstances(entities: Record<string, EntityInstance>): EntityInstance[] { export function getEntityInstances(
entities: Record<string, EntityInstance>
): EntityInstance[] {
return Object.values(entities).sort(compareEntityInstances); return Object.values(entities).sort(compareEntityInstances);
} }
@@ -2754,10 +2968,15 @@ export function getEntitiesOfKind<K extends EntityKind>(
entities: Record<string, EntityInstance>, entities: Record<string, EntityInstance>,
kind: K kind: K
): Extract<EntityInstance, { kind: K }>[] { ): Extract<EntityInstance, { kind: K }>[] {
return getEntityInstances(entities).filter((entity): entity is Extract<EntityInstance, { kind: K }> => entity.kind === kind); return getEntityInstances(entities).filter(
(entity): entity is Extract<EntityInstance, { kind: K }> =>
entity.kind === kind
);
} }
export function getPlayerStartEntities(entities: Record<string, EntityInstance>): PlayerStartEntity[] { export function getPlayerStartEntities(
entities: Record<string, EntityInstance>
): PlayerStartEntity[] {
return getEntitiesOfKind(entities, "playerStart"); return getEntitiesOfKind(entities, "playerStart");
} }
@@ -2767,12 +2986,18 @@ export function getCameraRigEntities(
return getEntitiesOfKind(entities, "cameraRig"); return getEntitiesOfKind(entities, "cameraRig");
} }
export function getPrimaryPlayerStartEntity(entities: Record<string, EntityInstance>): PlayerStartEntity | null { export function getPrimaryPlayerStartEntity(
entities: Record<string, EntityInstance>
): PlayerStartEntity | null {
return getPlayerStartEntities(entities)[0] ?? null; return getPlayerStartEntities(entities)[0] ?? null;
} }
export function getPrimaryEnabledPlayerStartEntity(entities: Record<string, EntityInstance>): PlayerStartEntity | null { export function getPrimaryEnabledPlayerStartEntity(
return getPlayerStartEntities(entities).find((entity) => entity.enabled) ?? null; entities: Record<string, EntityInstance>
): PlayerStartEntity | null {
return (
getPlayerStartEntities(entities).find((entity) => entity.enabled) ?? null
);
} }
export function getEntityKindLabel(kind: EntityKind): string { export function getEntityKindLabel(kind: EntityKind): string {

View File

@@ -14,8 +14,7 @@ export interface PlayerStartMovementActionState {
moveRight: number; moveRight: number;
} }
export interface PlayerStartActionInputState export interface PlayerStartActionInputState extends PlayerStartMovementActionState {
extends PlayerStartMovementActionState {
jump: number; jump: number;
sprint: number; sprint: number;
crouch: number; crouch: number;
@@ -38,7 +37,9 @@ function readGamepadButtonStrength(button: GamepadButton | undefined): number {
return 0; return 0;
} }
return clampUnitInterval(button.pressed ? Math.max(button.value, 1) : button.value); return clampUnitInterval(
button.pressed ? Math.max(button.value, 1) : button.value
);
} }
function readCenteredAxisValue(value: number | undefined): number { function readCenteredAxisValue(value: number | undefined): number {
@@ -116,7 +117,11 @@ function readGamepadBindingStrength(
for (let index = 0; index < gamepads.length; index += 1) { for (let index = 0; index < gamepads.length; index += 1) {
const gamepad = gamepads[index]; const gamepad = gamepads[index];
if (gamepad === null || gamepad === undefined || gamepad.connected === false) { if (
gamepad === null ||
gamepad === undefined ||
gamepad.connected === false
) {
continue; continue;
} }
@@ -169,11 +174,18 @@ function readGamepadActionBindingStrength(
for (let index = 0; index < gamepads.length; index += 1) { for (let index = 0; index < gamepads.length; index += 1) {
const gamepad = gamepads[index]; const gamepad = gamepads[index];
if (gamepad === null || gamepad === undefined || gamepad.connected === false) { if (
gamepad === null ||
gamepad === undefined ||
gamepad.connected === false
) {
continue; continue;
} }
strength = Math.max(strength, readSingleGamepadActionBinding(gamepad, binding)); strength = Math.max(
strength,
readSingleGamepadActionBinding(gamepad, binding)
);
} }
return strength; return strength;
@@ -209,7 +221,11 @@ function readGamepadCameraLook(
for (let index = 0; index < gamepads.length; index += 1) { for (let index = 0; index < gamepads.length; index += 1) {
const gamepad = gamepads[index]; const gamepad = gamepads[index];
if (gamepad === null || gamepad === undefined || gamepad.connected === false) { if (
gamepad === null ||
gamepad === undefined ||
gamepad.connected === false
) {
continue; continue;
} }
@@ -244,7 +260,10 @@ export function getAvailableGamepads(): ArrayLike<Gamepad | null> | undefined {
export function resolvePlayerStartMovementActions( export function resolvePlayerStartMovementActions(
pressedKeys: ReadonlySet<string>, pressedKeys: ReadonlySet<string>,
bindings: PlayerStartInputBindings, bindings: PlayerStartInputBindings,
gamepads: ArrayLike<Gamepad | null> | null | undefined = getAvailableGamepads() gamepads:
| ArrayLike<Gamepad | null>
| null
| undefined = getAvailableGamepads()
): PlayerStartMovementActionState { ): PlayerStartMovementActionState {
const actionInputs = resolvePlayerStartActionInputs( const actionInputs = resolvePlayerStartActionInputs(
pressedKeys, pressedKeys,
@@ -263,7 +282,10 @@ export function resolvePlayerStartMovementActions(
export function resolvePlayerStartActionInputs( export function resolvePlayerStartActionInputs(
pressedKeys: ReadonlySet<string>, pressedKeys: ReadonlySet<string>,
bindings: PlayerStartInputBindings, bindings: PlayerStartInputBindings,
gamepads: ArrayLike<Gamepad | null> | null | undefined = getAvailableGamepads() gamepads:
| ArrayLike<Gamepad | null>
| null
| undefined = getAvailableGamepads()
): PlayerStartActionInputState { ): PlayerStartActionInputState {
return { return {
moveForward: Math.max( moveForward: Math.max(
@@ -312,7 +334,10 @@ export function resolvePlayerStartActionInputs(
export function resolvePlayerStartPauseInput( export function resolvePlayerStartPauseInput(
pressedKeys: ReadonlySet<string>, pressedKeys: ReadonlySet<string>,
bindings: PlayerStartInputBindings, bindings: PlayerStartInputBindings,
gamepads: ArrayLike<Gamepad | null> | null | undefined = getAvailableGamepads() gamepads:
| ArrayLike<Gamepad | null>
| null
| undefined = getAvailableGamepads()
): number { ): number {
return resolvePlayerStartActionInputs(pressedKeys, bindings, gamepads) return resolvePlayerStartActionInputs(pressedKeys, bindings, gamepads)
.pauseTime; .pauseTime;
@@ -321,7 +346,10 @@ export function resolvePlayerStartPauseInput(
export function resolvePlayerStartInteractInput( export function resolvePlayerStartInteractInput(
pressedKeys: ReadonlySet<string>, pressedKeys: ReadonlySet<string>,
bindings: PlayerStartInputBindings, bindings: PlayerStartInputBindings,
gamepads: ArrayLike<Gamepad | null> | null | undefined = getAvailableGamepads() gamepads:
| ArrayLike<Gamepad | null>
| null
| undefined = getAvailableGamepads()
): number { ): number {
return resolvePlayerStartActionInputs(pressedKeys, bindings, gamepads) return resolvePlayerStartActionInputs(pressedKeys, bindings, gamepads)
.interact; .interact;
@@ -330,21 +358,30 @@ export function resolvePlayerStartInteractInput(
export function resolvePlayerStartClearTargetInput( export function resolvePlayerStartClearTargetInput(
pressedKeys: ReadonlySet<string>, pressedKeys: ReadonlySet<string>,
bindings: PlayerStartInputBindings, bindings: PlayerStartInputBindings,
gamepads: ArrayLike<Gamepad | null> | null | undefined = getAvailableGamepads() gamepads:
| ArrayLike<Gamepad | null>
| null
| undefined = getAvailableGamepads()
): number { ): number {
return resolvePlayerStartActionInputs(pressedKeys, bindings, gamepads) return resolvePlayerStartActionInputs(pressedKeys, bindings, gamepads)
.clearTarget; .clearTarget;
} }
export function resolveDefaultTargetCycleInput( export function resolveDefaultTargetCycleInput(
gamepads: ArrayLike<Gamepad | null> | null | undefined = getAvailableGamepads() gamepads:
| ArrayLike<Gamepad | null>
| null
| undefined = getAvailableGamepads()
): number { ): number {
return readGamepadActionBindingStrength(gamepads, "rightStickPress"); return readGamepadActionBindingStrength(gamepads, "rightStickPress");
} }
export function resolvePlayerStartLookInput( export function resolvePlayerStartLookInput(
bindings: PlayerStartInputBindings, bindings: PlayerStartInputBindings,
gamepads: ArrayLike<Gamepad | null> | null | undefined = getAvailableGamepads() gamepads:
| ArrayLike<Gamepad | null>
| null
| undefined = getAvailableGamepads()
): PlayerStartLookInputState { ): PlayerStartLookInputState {
return readGamepadCameraLook(gamepads, bindings.gamepad.cameraLook); return readGamepadCameraLook(gamepads, bindings.gamepad.cameraLook);
} }

View File

@@ -5662,17 +5662,13 @@ export class RuntimeHost {
z: this.cameraForward.z z: this.cameraForward.z
} }
: { : {
x: x: Math.sin(
Math.sin( (this.currentPlayerControllerTelemetry.yawDegrees * Math.PI) / 180
(this.currentPlayerControllerTelemetry.yawDegrees * Math.PI) / ),
180
),
y: 0, y: 0,
z: z: Math.cos(
Math.cos( (this.currentPlayerControllerTelemetry.yawDegrees * Math.PI) / 180
(this.currentPlayerControllerTelemetry.yawDegrees * Math.PI) / )
180
)
}; };
return this.interactionSystem.resolveClickInteractionPrompt( return this.interactionSystem.resolveClickInteractionPrompt(