diff --git a/src/runtime-three/runtime-interaction-system.ts b/src/runtime-three/runtime-interaction-system.ts index f0e37217..cbff8fac 100644 --- a/src/runtime-three/runtime-interaction-system.ts +++ b/src/runtime-three/runtime-interaction-system.ts @@ -51,6 +51,11 @@ export interface RuntimeInteractionPrompt { range: number; } +export interface RuntimePlayerTriggerProbe { + feetPosition: Vec3; + eyePosition: Vec3; +} + function subtractVec3(left: Vec3, right: Vec3): Vec3 { return { x: left.x - right.x, @@ -109,6 +114,94 @@ function isPointInsideTriggerVolume( ); } +function isVec3(value: Vec3 | RuntimePlayerTriggerProbe): value is Vec3 { + return "x" in value; +} + +function rayAxisAlignedBoxHitDistance( + origin: Vec3, + direction: Vec3, + bounds: { min: Vec3; max: Vec3 } +): number | null { + let near = Number.NEGATIVE_INFINITY; + let far = Number.POSITIVE_INFINITY; + + for (const axis of ["x", "y", "z"] as const) { + const axisOrigin = origin[axis]; + const axisDirection = direction[axis]; + const axisMin = bounds.min[axis]; + const axisMax = bounds.max[axis]; + + if (Math.abs(axisDirection) <= Number.EPSILON) { + if (axisOrigin < axisMin || axisOrigin > axisMax) { + return null; + } + continue; + } + + const inverseDirection = 1 / axisDirection; + let entry = (axisMin - axisOrigin) * inverseDirection; + let exit = (axisMax - axisOrigin) * inverseDirection; + + if (entry > exit) { + const swap = entry; + entry = exit; + exit = swap; + } + + near = Math.max(near, entry); + far = Math.min(far, exit); + + if (near > far) { + return null; + } + } + + if (far < 0) { + return null; + } + + return near >= 0 ? near : 0; +} + +function isPlayerInsideTriggerVolume( + feetPosition: Vec3, + eyePosition: Vec3, + triggerVolume: RuntimeTriggerVolume +): boolean { + if ( + isPointInsideTriggerVolume(feetPosition, triggerVolume) || + isPointInsideTriggerVolume(eyePosition, triggerVolume) + ) { + return true; + } + + const halfSize = { + x: triggerVolume.size.x * 0.5, + y: triggerVolume.size.y * 0.5, + z: triggerVolume.size.z * 0.5 + }; + + return ( + rayAxisAlignedBoxHitDistance( + feetPosition, + subtractVec3(eyePosition, feetPosition), + { + min: { + x: triggerVolume.position.x - halfSize.x, + y: triggerVolume.position.y - halfSize.y, + z: triggerVolume.position.z - halfSize.z + }, + max: { + x: triggerVolume.position.x + halfSize.x, + y: triggerVolume.position.y + halfSize.y, + z: triggerVolume.position.z + halfSize.z + } + } + ) !== null + ); +} + function raySphereHitDistance( origin: Vec3, direction: Vec3, @@ -196,6 +289,85 @@ function getNpcDialoguePrompt( : "Talk"; } +function getNpcDialogueTargetBounds(npc: RuntimeNpc): { + min: Vec3; + max: Vec3; + center: Vec3; + range: number; +} { + switch (npc.collider.mode) { + case "capsule": { + return { + min: { + x: npc.position.x - npc.collider.radius, + y: npc.position.y, + z: npc.position.z - npc.collider.radius + }, + max: { + x: npc.position.x + npc.collider.radius, + y: npc.position.y + npc.collider.height, + z: npc.position.z + npc.collider.radius + }, + center: { + x: npc.position.x, + y: npc.position.y + npc.collider.height * 0.5, + z: npc.position.z + }, + range: Math.max( + DEFAULT_NPC_DIALOGUE_TARGET_RADIUS, + npc.collider.height * 0.5 + ) + }; + } + case "box": { + return { + min: { + x: npc.position.x - npc.collider.size.x * 0.5, + y: npc.position.y, + z: npc.position.z - npc.collider.size.z * 0.5 + }, + max: { + x: npc.position.x + npc.collider.size.x * 0.5, + y: npc.position.y + npc.collider.size.y, + z: npc.position.z + npc.collider.size.z * 0.5 + }, + center: { + x: npc.position.x, + y: npc.position.y + npc.collider.size.y * 0.5, + z: npc.position.z + }, + range: Math.max( + DEFAULT_NPC_DIALOGUE_TARGET_RADIUS, + Math.max( + npc.collider.size.x, + npc.collider.size.y, + npc.collider.size.z + ) * 0.5 + ) + }; + } + case "none": + return { + min: { + x: npc.position.x - DEFAULT_NPC_DIALOGUE_TARGET_RADIUS * 0.5, + y: npc.position.y, + z: npc.position.z - DEFAULT_NPC_DIALOGUE_TARGET_RADIUS * 0.5 + }, + max: { + x: npc.position.x + DEFAULT_NPC_DIALOGUE_TARGET_RADIUS * 0.5, + y: npc.position.y + 1.8, + z: npc.position.z + DEFAULT_NPC_DIALOGUE_TARGET_RADIUS * 0.5 + }, + center: { + x: npc.position.x, + y: npc.position.y + 0.9, + z: npc.position.z + }, + range: DEFAULT_NPC_DIALOGUE_TARGET_RADIUS + }; + } +} + function updateBestPrompt( currentBestPrompt: RuntimeInteractionPrompt | null, currentBestHitDistance: number,