From 7198b432417fc0f4d9681d2806c150ac3b14586c Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Tue, 14 Apr 2026 21:12:42 +0200 Subject: [PATCH] Update player position handling in interaction system and tests --- src/runtime-three/runtime-host.ts | 5 +- .../runtime-interaction-system.ts | 42 ++++++-------- .../domain/runtime-interaction-system.test.ts | 55 +++++++++++++++++++ 3 files changed, 75 insertions(+), 27 deletions(-) diff --git a/src/runtime-three/runtime-host.ts b/src/runtime-three/runtime-host.ts index 93fb19da..af7686a9 100644 --- a/src/runtime-three/runtime-host.ts +++ b/src/runtime-three/runtime-host.ts @@ -2734,7 +2734,10 @@ export class RuntimeHost { this.currentPlayerControllerTelemetry !== null ) { this.interactionSystem.updatePlayerPosition( - this.currentPlayerControllerTelemetry.feetPosition, + { + feetPosition: this.currentPlayerControllerTelemetry.feetPosition, + eyePosition: this.currentPlayerControllerTelemetry.eyePosition + }, this.runtimeScene, this.createInteractionDispatcher() ); diff --git a/src/runtime-three/runtime-interaction-system.ts b/src/runtime-three/runtime-interaction-system.ts index cbff8fac..abb58ed8 100644 --- a/src/runtime-three/runtime-interaction-system.ts +++ b/src/runtime-three/runtime-interaction-system.ts @@ -255,23 +255,6 @@ function getInteractableTargetRadius( return Math.min(DEFAULT_INTERACTABLE_TARGET_RADIUS, interactable.radius); } -function getNpcDialogueTargetRadius(npc: RuntimeNpc): number { - switch (npc.collider.mode) { - case "capsule": - return Math.max( - DEFAULT_NPC_DIALOGUE_TARGET_RADIUS, - npc.collider.radius * 2 - ); - case "box": - return Math.max( - DEFAULT_NPC_DIALOGUE_TARGET_RADIUS, - Math.max(npc.collider.size.x, npc.collider.size.z) * 0.75 - ); - case "none": - return DEFAULT_NPC_DIALOGUE_TARGET_RADIUS; - } -} - function getNpcDialoguePrompt( npc: RuntimeNpc, hasClickLinks: boolean @@ -413,13 +396,21 @@ export class RuntimeInteractionSystem { } updatePlayerPosition( - feetPosition: Vec3, + playerProbe: Vec3 | RuntimePlayerTriggerProbe, runtimeScene: RuntimeSceneDefinition, dispatcher: RuntimeInteractionDispatcher ) { + const feetPosition = isVec3(playerProbe) + ? playerProbe + : playerProbe.feetPosition; + const eyePosition = isVec3(playerProbe) + ? playerProbe + : playerProbe.eyePosition; + for (const triggerVolume of runtimeScene.entities.triggerVolumes) { - const containsPlayer = isPointInsideTriggerVolume( + const containsPlayer = isPlayerInsideTriggerVolume( feetPosition, + eyePosition, triggerVolume ); const wasOccupied = this.occupiedTriggerVolumes.has( @@ -562,18 +553,17 @@ export class RuntimeInteractionSystem { continue; } - const radius = getNpcDialogueTargetRadius(npc); - const distance = distanceBetweenVec3(interactionOrigin, npc.position); + const bounds = getNpcDialogueTargetBounds(npc); + const distance = distanceBetweenVec3(interactionOrigin, bounds.center); - if (distance > radius) { + if (distance > bounds.range) { continue; } - const hitDistance = raySphereHitDistance( + const hitDistance = rayAxisAlignedBoxHitDistance( rayOrigin, normalizedViewDirection, - npc.position, - radius + bounds ); if (hitDistance === null) { @@ -586,7 +576,7 @@ export class RuntimeInteractionSystem { npc.entityId, getNpcDialoguePrompt(npc, hasClickLinks), distance, - radius, + bounds.range, hitDistance ); bestPrompt = next.prompt; diff --git a/tests/domain/runtime-interaction-system.test.ts b/tests/domain/runtime-interaction-system.test.ts index aeb155b5..7cebbd53 100644 --- a/tests/domain/runtime-interaction-system.test.ts +++ b/tests/domain/runtime-interaction-system.test.ts @@ -679,6 +679,61 @@ describe("RuntimeInteractionSystem", () => { expect(dispatches).toEqual(["link-trigger-dialogue:dialogue-threshold"]); }); + it("treats the player body segment as entering a trigger volume, not just the feet point", () => { + const runtimeScene = createRuntimeSceneFixture(); + runtimeScene.entities.triggerVolumes = [ + { + entityId: "entity-trigger-chest-height", + position: { + x: 0, + y: 1.2, + z: 0 + }, + size: { + x: 2, + y: 1, + z: 2 + }, + triggerOnEnter: true, + triggerOnExit: false + } + ]; + runtimeScene.interactionLinks = [ + createStartDialogueInteractionLink({ + id: "link-body-trigger-dialogue", + sourceEntityId: "entity-trigger-chest-height", + trigger: "enter", + dialogueId: "dialogue-threshold" + }) + ]; + + const dispatches: string[] = []; + const interactionSystem = new RuntimeInteractionSystem(); + + interactionSystem.updatePlayerPosition( + { + feetPosition: { + x: 0, + y: 0, + z: 0 + }, + eyePosition: { + x: 0, + y: 1.6, + z: 0 + } + }, + runtimeScene, + createDispatcher({ + startDialogue: (dialogueId, source) => { + dispatches.push(`${source?.linkId}:${dialogueId}`); + } + }) + ); + + expect(dispatches).toEqual(["link-body-trigger-dialogue:dialogue-threshold"]); + }); + it("resolves direct NPC dialogue prompts and dispatches them through the shared start path", () => { const runtimeScene = createRuntimeSceneFixture(); runtimeScene.entities.interactables = [];