Implement look-directional target switching and refine active runtime target state management

This commit is contained in:
2026-04-25 15:31:13 +02:00
parent 601a1e8ecd
commit dd0f6868ac

View File

@@ -5445,6 +5445,16 @@ export class RuntimeHost {
); );
} }
private setActiveRuntimeTargetReference(reference: RuntimeTargetReference | null) {
const previousEntityId = this.activeRuntimeTargetReference?.entityId ?? null;
const nextEntityId = reference?.entityId ?? null;
this.activeRuntimeTargetReference = reference;
if (previousEntityId !== nextEntityId) {
this.runtimeTargetLookYawDeltaRadians = 0;
}
}
private refreshRuntimeTargetingState() { private refreshRuntimeTargetingState() {
if ( if (
this.runtimeScene === null || this.runtimeScene === null ||
@@ -5490,7 +5500,7 @@ export class RuntimeHost {
this.activeRuntimeTargetReference !== null && this.activeRuntimeTargetReference !== null &&
this.resolveActiveRuntimeTarget() === null this.resolveActiveRuntimeTarget() === null
) { ) {
this.activeRuntimeTargetReference = null; this.setActiveRuntimeTargetReference(null);
} }
this.proposedRuntimeTarget = resolveStableRuntimeTargetProposal( this.proposedRuntimeTarget = resolveStableRuntimeTargetProposal(
@@ -5517,10 +5527,10 @@ export class RuntimeHost {
this.proposedRuntimeTarget ?? this.runtimeTargetCandidates[0] ?? null; this.proposedRuntimeTarget ?? this.runtimeTargetCandidates[0] ?? null;
if (nextTarget !== null) { if (nextTarget !== null) {
this.activeRuntimeTargetReference = { this.setActiveRuntimeTargetReference({
kind: nextTarget.kind, kind: nextTarget.kind,
entityId: nextTarget.entityId entityId: nextTarget.entityId
}; });
} }
return; return;
} }
@@ -5538,15 +5548,158 @@ export class RuntimeHost {
? 0 ? 0
: (activeIndex + 1) % this.runtimeTargetCandidates.length; : (activeIndex + 1) % this.runtimeTargetCandidates.length;
const nextTarget = this.runtimeTargetCandidates[nextIndex]!; const nextTarget = this.runtimeTargetCandidates[nextIndex]!;
this.activeRuntimeTargetReference = { this.setActiveRuntimeTargetReference({
kind: nextTarget.kind, kind: nextTarget.kind,
entityId: nextTarget.entityId entityId: nextTarget.entityId
}; });
this.proposedRuntimeTarget = nextTarget; this.proposedRuntimeTarget = nextTarget;
} }
private clearActiveRuntimeTarget() { private clearActiveRuntimeTarget() {
this.activeRuntimeTargetReference = null; this.setActiveRuntimeTargetReference(null);
}
private reportThirdPersonCameraLookIntent(yawDeltaRadians: number) {
if (
this.activeRuntimeTargetReference === null ||
!Number.isFinite(yawDeltaRadians) ||
Math.abs(yawDeltaRadians) <= Number.EPSILON
) {
return;
}
this.runtimeTargetLookYawDeltaRadians += yawDeltaRadians;
}
private resolveRuntimeTargetCandidateOnLookSide(
activeTarget: RuntimeResolvedTarget,
lookSide: -1 | 1
): RuntimeTargetCandidate | null {
const origin = this.currentPlayerControllerTelemetry?.eyePosition ?? null;
if (origin === null) {
return null;
}
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 null;
}
let bestCandidate: RuntimeTargetCandidate | null = null;
let bestAngle = Number.POSITIVE_INFINITY;
const activeX = activeDirection.x / activeLength;
const activeZ = activeDirection.z / activeLength;
for (const candidate of this.runtimeTargetCandidates) {
if (candidate.entityId === activeTarget.entityId) {
continue;
}
const candidateDirection = {
x: candidate.center.x - origin.x,
z: candidate.center.z - origin.z
};
const candidateLength = Math.hypot(
candidateDirection.x,
candidateDirection.z
);
if (candidateLength <= Number.EPSILON) {
continue;
}
const candidateX = candidateDirection.x / candidateLength;
const candidateZ = candidateDirection.z / candidateLength;
const signedAngle = Math.atan2(
activeZ * candidateX - activeX * candidateZ,
activeX * candidateX + activeZ * candidateZ
);
if (signedAngle * lookSide <= TARGETING_SIDE_SWITCH_YAW_THRESHOLD_RADIANS * 0.5) {
continue;
}
const absAngle = Math.abs(signedAngle);
if (
bestCandidate === null ||
absAngle < bestAngle ||
(absAngle === bestAngle && candidate.score > bestCandidate.score)
) {
bestCandidate = candidate;
bestAngle = absAngle;
}
}
return bestCandidate;
}
private updateActiveRuntimeTargetLockState() {
if (
this.activeRuntimeTargetReference === null ||
this.currentPlayerControllerTelemetry === null ||
this.activeController !== this.thirdPersonController
) {
this.runtimeTargetLookYawDeltaRadians = 0;
return;
}
const activeTarget = this.resolveActiveRuntimeTarget();
if (activeTarget === null) {
this.setActiveRuntimeTargetReference(null);
return;
}
if (
distanceBetweenPoints(
this.currentPlayerControllerTelemetry.eyePosition,
activeTarget.center
) > TARGETING_MAX_ACTIVE_TARGET_DISTANCE
) {
this.setActiveRuntimeTargetReference(null);
return;
}
const otherTargetsAvailable = this.runtimeTargetCandidates.some(
(candidate) => candidate.entityId !== activeTarget.entityId
);
const yawDelta = this.runtimeTargetLookYawDeltaRadians;
const absYawDelta = Math.abs(yawDelta);
if (
otherTargetsAvailable &&
absYawDelta >= TARGETING_CANCEL_YAW_THRESHOLD_RADIANS
) {
this.setActiveRuntimeTargetReference(null);
return;
}
if (absYawDelta < TARGETING_SIDE_SWITCH_YAW_THRESHOLD_RADIANS) {
return;
}
const lookSide = yawDelta > 0 ? 1 : -1;
const sideTarget = this.resolveRuntimeTargetCandidateOnLookSide(
activeTarget,
lookSide
);
if (sideTarget === null) {
return;
}
this.setActiveRuntimeTargetReference({
kind: sideTarget.kind,
entityId: sideTarget.entityId
});
this.proposedRuntimeTarget = sideTarget;
} }
private updateRuntimeTargetingInputState() { private updateRuntimeTargetingInputState() {