Add player trigger probe and collision detection functions

This commit is contained in:
2026-04-14 21:12:06 +02:00
parent df6f1b50da
commit 7449dccd61

View File

@@ -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,