Refactor runtime target look input to support directional side targeting and update target state management

This commit is contained in:
2026-04-25 15:57:12 +02:00
parent d7822b9741
commit ed94970088
2 changed files with 72 additions and 94 deletions

View File

@@ -5531,43 +5531,83 @@ export class RuntimeHost {
return;
}
if (this.activeRuntimeTargetReference === null) {
const nextTarget =
this.proposedRuntimeTarget ?? this.runtimeTargetCandidates[0] ?? null;
const nextTarget =
this.proposedRuntimeTarget ?? this.runtimeTargetCandidates[0] ?? null;
if (nextTarget !== null) {
this.setActiveRuntimeTargetReference({
kind: nextTarget.kind,
entityId: nextTarget.entityId
});
}
return;
if (nextTarget !== null) {
this.setActiveRuntimeTargetReference({
kind: nextTarget.kind,
entityId: nextTarget.entityId
});
}
if (this.runtimeTargetCandidates.length === 0) {
return;
}
const activeEntityId = this.activeRuntimeTargetReference.entityId;
const activeIndex = this.runtimeTargetCandidates.findIndex(
(candidate) => candidate.entityId === activeEntityId
);
const nextIndex =
activeIndex < 0
? 0
: (activeIndex + 1) % this.runtimeTargetCandidates.length;
const nextTarget = this.runtimeTargetCandidates[nextIndex]!;
this.setActiveRuntimeTargetReference({
kind: nextTarget.kind,
entityId: nextTarget.entityId
});
this.proposedRuntimeTarget = nextTarget;
}
private clearActiveRuntimeTarget() {
this.setActiveRuntimeTargetReference(null);
}
private handleRuntimeTargetLookInput(horizontalIntent: -1 | 0 | 1): boolean {
const activeTarget = this.resolveActiveRuntimeTarget();
if (activeTarget === null) {
if (this.activeRuntimeTargetReference !== null) {
this.setActiveRuntimeTargetReference(null);
}
this.runtimeTargetLookInputHeldDirection = null;
return false;
}
if (horizontalIntent === 0) {
this.runtimeTargetLookInputHeldDirection = null;
return true;
}
if (this.runtimeTargetLookInputHeldDirection === horizontalIntent) {
return true;
}
this.runtimeTargetLookInputHeldDirection = horizontalIntent;
const origin = this.currentPlayerControllerTelemetry?.eyePosition ?? null;
if (origin === null) {
return true;
}
this.camera.getWorldDirection(this.cameraForward);
const cameraLength = Math.hypot(this.cameraForward.x, this.cameraForward.z);
const fallbackDirection = {
x: activeTarget.center.x - origin.x,
z: activeTarget.center.z - origin.z
};
const fallbackLength = Math.hypot(fallbackDirection.x, fallbackDirection.z);
const cameraDirection =
cameraLength <= Number.EPSILON && fallbackLength > Number.EPSILON
? {
x: fallbackDirection.x / fallbackLength,
z: fallbackDirection.z / fallbackLength
}
: {
x: this.cameraForward.x / Math.max(cameraLength, Number.EPSILON),
z: this.cameraForward.z / Math.max(cameraLength, Number.EPSILON)
};
const sideTarget = this.resolveRuntimeTargetCandidateOnLookSide(
activeTarget,
horizontalIntent,
cameraDirection
);
if (sideTarget !== null) {
this.setActiveRuntimeTargetReference({
kind: sideTarget.kind,
entityId: sideTarget.entityId
});
this.runtimeTargetLookInputHeldDirection = horizontalIntent;
this.proposedRuntimeTarget = sideTarget;
}
return true;
}
private resolveRuntimeTargetCandidateOnLookSide(
activeTarget: RuntimeResolvedTarget,
lookSide: -1 | 1,
@@ -5619,7 +5659,7 @@ export class RuntimeHost {
activeX * candidateX + activeZ * candidateZ
);
if (signedAngle * lookSide <= TARGETING_SIDE_SWITCH_YAW_THRESHOLD_RADIANS * 0.5) {
if (signedAngle * lookSide <= TARGETING_SIDE_SWITCH_EPSILON_RADIANS) {
continue;
}
@@ -5670,65 +5710,6 @@ export class RuntimeHost {
return;
}
const otherTargetsAvailable = this.runtimeTargetCandidates.some(
(candidate) => candidate.entityId !== activeTarget.entityId
);
const origin = this.currentPlayerControllerTelemetry.eyePosition;
const activeDirection = {
x: activeTarget.center.x - origin.x,
z: activeTarget.center.z - origin.z
};
const activeLength = Math.hypot(activeDirection.x, activeDirection.z);
if (activeLength <= Number.EPSILON) {
return;
}
this.camera.getWorldDirection(this.cameraForward);
const cameraLength = Math.hypot(this.cameraForward.x, this.cameraForward.z);
if (cameraLength <= Number.EPSILON) {
return;
}
const activeX = activeDirection.x / activeLength;
const activeZ = activeDirection.z / activeLength;
const cameraDirection = {
x: this.cameraForward.x / cameraLength,
z: this.cameraForward.z / cameraLength
};
const cameraAngleFromTarget = Math.atan2(
activeZ * cameraDirection.x - activeX * cameraDirection.z,
activeX * cameraDirection.x + activeZ * cameraDirection.z
);
const absCameraAngleFromTarget = Math.abs(cameraAngleFromTarget);
if (absCameraAngleFromTarget < TARGETING_SIDE_SWITCH_YAW_THRESHOLD_RADIANS) {
return;
}
const lookSide = cameraAngleFromTarget > 0 ? 1 : -1;
const sideTarget = this.resolveRuntimeTargetCandidateOnLookSide(
activeTarget,
lookSide,
cameraDirection
);
if (sideTarget !== null) {
this.setActiveRuntimeTargetReference({
kind: sideTarget.kind,
entityId: sideTarget.entityId
});
this.proposedRuntimeTarget = sideTarget;
return;
}
if (
otherTargetsAvailable &&
absCameraAngleFromTarget >= TARGETING_CANCEL_YAW_THRESHOLD_RADIANS
) {
this.setActiveRuntimeTargetReference(null);
}
}
private updateRuntimeTargetingInputState() {

View File

@@ -3043,7 +3043,7 @@ describe("RuntimeHost", () => {
host.dispose();
});
it("activates the proposed runtime target and cycles visible target candidates", () => {
it("toggles the proposed runtime target instead of cycling candidates", () => {
const host = new RuntimeHost({
enableRendering: false
});
@@ -3101,10 +3101,7 @@ describe("RuntimeHost", () => {
hostInternals.activateOrCycleRuntimeTarget();
expect(hostInternals.activeRuntimeTargetReference).toEqual({
kind: "interactable",
entityId: "switch-two"
});
expect(hostInternals.activeRuntimeTargetReference).toBeNull();
hostInternals.clearActiveRuntimeTarget();