diff --git a/src/entities/entity-instances.ts b/src/entities/entity-instances.ts index 18bc36aa..86fc8fda 100644 --- a/src/entities/entity-instances.ts +++ b/src/entities/entity-instances.ts @@ -40,11 +40,24 @@ export interface SceneEntryEntity extends PositionedEntity { yawDegrees: number; } +export interface CharacterColliderSettings { + mode: PlayerStartColliderMode; + eyeHeight: number; + capsuleRadius: number; + capsuleHeight: number; + boxSize: Vec3; +} + +export interface PlayerStartColliderSettings extends CharacterColliderSettings {} + +export interface NpcColliderSettings extends CharacterColliderSettings {} + export interface NpcEntity extends PositionedEntity { kind: "npc"; actorId: string; yawDegrees: number; modelAssetId: string | null; + collider: NpcColliderSettings; } export const PLAYER_START_COLLIDER_MODES = ["capsule", "box", "none"] as const; @@ -193,14 +206,6 @@ export interface PlayerStartMovementTemplateOverrides { crouch?: Partial; } -export interface PlayerStartColliderSettings { - mode: PlayerStartColliderMode; - eyeHeight: number; - capsuleRadius: number; - capsuleHeight: number; - boxSize: Vec3; -} - export interface SoundEmitterEntity extends PositionedEntity { kind: "soundEmitter"; audioAssetId: string | null; @@ -388,6 +393,7 @@ export const DEFAULT_PLAYER_START_GAMEPAD_BINDINGS: PlayerStartGamepadBindings = export const DEFAULT_SCENE_ENTRY_YAW_DEGREES = 0; export const DEFAULT_NPC_YAW_DEGREES = 0; export const DEFAULT_NPC_MODEL_ASSET_ID: string | null = null; +export const DEFAULT_NPC_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_CAPSULE_RADIUS = 0.3; @@ -512,7 +518,9 @@ export function isPlayerStartGamepadCameraLookBinding( ); } -export function clonePlayerStartColliderSettings(settings: PlayerStartColliderSettings): PlayerStartColliderSettings { +function cloneCharacterColliderSettings( + settings: CharacterColliderSettings +): CharacterColliderSettings { return { mode: settings.mode, eyeHeight: settings.eyeHeight, @@ -522,6 +530,18 @@ export function clonePlayerStartColliderSettings(settings: PlayerStartColliderSe }; } +export function clonePlayerStartColliderSettings( + settings: PlayerStartColliderSettings +): PlayerStartColliderSettings { + return cloneCharacterColliderSettings(settings); +} + +export function cloneNpcColliderSettings( + settings: NpcColliderSettings +): NpcColliderSettings { + return cloneCharacterColliderSettings(settings); +} + function clonePlayerStartMovementCapabilities( capabilities: PlayerStartMovementCapabilities ): PlayerStartMovementCapabilities { @@ -988,7 +1008,9 @@ export function arePlayerStartMovementTemplatesEqual( ); } -export function getPlayerStartColliderHeight(settings: PlayerStartColliderSettings): number | null { +function getCharacterColliderHeight( + settings: CharacterColliderSettings +): number | null { switch (settings.mode) { case "capsule": return settings.capsuleHeight; @@ -999,34 +1021,63 @@ export function getPlayerStartColliderHeight(settings: PlayerStartColliderSettin } } -export function createPlayerStartColliderSettings( - overrides: Partial = {} -): PlayerStartColliderSettings { - const mode = overrides.mode ?? DEFAULT_PLAYER_START_COLLIDER_MODE; - const eyeHeight = overrides.eyeHeight ?? DEFAULT_PLAYER_START_EYE_HEIGHT; - const capsuleRadius = overrides.capsuleRadius ?? DEFAULT_PLAYER_START_CAPSULE_RADIUS; - const capsuleHeight = overrides.capsuleHeight ?? DEFAULT_PLAYER_START_CAPSULE_HEIGHT; - const boxSize = cloneVec3(overrides.boxSize ?? DEFAULT_PLAYER_START_BOX_SIZE); +export function getPlayerStartColliderHeight( + settings: PlayerStartColliderSettings +): number | null { + return getCharacterColliderHeight(settings); +} + +export function getNpcColliderHeight( + settings: NpcColliderSettings +): number | null { + return getCharacterColliderHeight(settings); +} + +function createCharacterColliderSettings( + label: string, + overrides: Partial = {}, + defaults: Partial = {} +): CharacterColliderSettings { + const mode = overrides.mode ?? defaults.mode ?? DEFAULT_PLAYER_START_COLLIDER_MODE; + const eyeHeight = + overrides.eyeHeight ?? defaults.eyeHeight ?? DEFAULT_PLAYER_START_EYE_HEIGHT; + const capsuleRadius = + overrides.capsuleRadius ?? + defaults.capsuleRadius ?? + DEFAULT_PLAYER_START_CAPSULE_RADIUS; + const capsuleHeight = + overrides.capsuleHeight ?? + defaults.capsuleHeight ?? + DEFAULT_PLAYER_START_CAPSULE_HEIGHT; + const boxSize = cloneVec3( + overrides.boxSize ?? defaults.boxSize ?? DEFAULT_PLAYER_START_BOX_SIZE + ); if (!isPlayerStartColliderMode(mode)) { - throw new Error("Player Start collider mode must be capsule, box, or none."); + throw new Error(`${label} collider mode must be capsule, box, or none.`); } - assertPositiveFiniteNumber(eyeHeight, "Player Start eye height"); - assertPositiveFiniteNumber(capsuleRadius, "Player Start capsule radius"); - assertPositiveFiniteNumber(capsuleHeight, "Player Start capsule height"); - assertPositiveFiniteVec3(boxSize, "Player Start box size"); + assertPositiveFiniteNumber(eyeHeight, `${label} eye height`); + assertPositiveFiniteNumber(capsuleRadius, `${label} capsule radius`); + assertPositiveFiniteNumber(capsuleHeight, `${label} capsule height`); + assertPositiveFiniteVec3(boxSize, `${label} box size`); if (capsuleHeight < capsuleRadius * 2) { - throw new Error("Player Start capsule height must be at least twice the capsule radius."); + throw new Error( + `${label} capsule height must be at least twice the capsule radius.` + ); } if (mode === "capsule" && eyeHeight > capsuleHeight) { - throw new Error("Player Start eye height must be less than or equal to the capsule height."); + throw new Error( + `${label} eye height must be less than or equal to the capsule height.` + ); } if (mode === "box" && eyeHeight > boxSize.y) { - throw new Error("Player Start eye height must be less than or equal to the box height."); + throw new Error( + `${label} eye height must be less than or equal to the box height.` + ); } return { @@ -1038,6 +1089,24 @@ export function createPlayerStartColliderSettings( }; } +export function createPlayerStartColliderSettings( + overrides: Partial = {} +): PlayerStartColliderSettings { + return createCharacterColliderSettings("Player Start", overrides); +} + +export function createNpcColliderSettings( + overrides: Partial = {} +): NpcColliderSettings { + return createCharacterColliderSettings("NPC", overrides, { + mode: DEFAULT_NPC_COLLIDER_MODE, + eyeHeight: DEFAULT_PLAYER_START_EYE_HEIGHT, + capsuleRadius: DEFAULT_PLAYER_START_CAPSULE_RADIUS, + capsuleHeight: DEFAULT_PLAYER_START_CAPSULE_HEIGHT, + boxSize: DEFAULT_PLAYER_START_BOX_SIZE + }); +} + function normalizeSoundEmitterAudioAssetId(audioAssetId: string | null | undefined): string | null { if (audioAssetId === undefined || audioAssetId === null) { return null; @@ -1279,6 +1348,7 @@ export function createNpcEntity( | "actorId" | "yawDegrees" | "modelAssetId" + | "collider" > > = {} ): NpcEntity { @@ -1288,6 +1358,7 @@ export function createNpcEntity( const modelAssetId = normalizeNpcModelAssetId( overrides.modelAssetId ?? DEFAULT_NPC_MODEL_ASSET_ID ); + const collider = createNpcColliderSettings(overrides.collider); assertFiniteVec3(position, "NPC position"); @@ -1304,7 +1375,8 @@ export function createNpcEntity( position, actorId, yawDegrees: normalizeYawDegrees(yawDegrees), - modelAssetId + modelAssetId, + collider }; } @@ -1676,7 +1748,12 @@ export function areEntityInstancesEqual(left: EntityInstance, right: EntityInsta return ( left.actorId === typedRight.actorId && left.yawDegrees === typedRight.yawDegrees && - left.modelAssetId === typedRight.modelAssetId + left.modelAssetId === typedRight.modelAssetId && + left.collider.mode === typedRight.collider.mode && + left.collider.eyeHeight === typedRight.collider.eyeHeight && + left.collider.capsuleRadius === typedRight.collider.capsuleRadius && + left.collider.capsuleHeight === typedRight.collider.capsuleHeight && + areVec3Equal(left.collider.boxSize, typedRight.collider.boxSize) ); } case "soundEmitter": {