auto-git:
[change] src/runtime-three/runtime-host.ts [change] tests/unit/runtime-interaction-system.test.ts
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
AdditiveBlending,
|
||||
AmbientLight,
|
||||
AnimationClip,
|
||||
AnimationMixer,
|
||||
@@ -326,9 +327,34 @@ const DIALOGUE_PARTICIPANT_PUSHBACK_DURATION_SECONDS = 0.3;
|
||||
const DIALOGUE_PARTICIPANT_YAW_BLEND_RATE = 8;
|
||||
const DIALOGUE_PARTICIPANT_RESTORE_EPSILON_DEGREES = 0.5;
|
||||
const TARGETING_LUX_FOLLOW_RATE = 8;
|
||||
const TARGETING_LUX_BOB_RATE = 4.2;
|
||||
const TARGETING_LUX_PULSE_RATE = 6.5;
|
||||
const PROPOSED_TARGET_CAMERA_ASSIST_STRENGTH = 0.28;
|
||||
const ACTIVE_TARGET_CAMERA_ASSIST_STRENGTH = 0.55;
|
||||
|
||||
export function resolveRuntimeTargetVisualPlacement(target: {
|
||||
center: { x: number; y: number; z: number };
|
||||
range: number;
|
||||
}) {
|
||||
const luxLift = clampScalar(target.range * 0.42, 0.78, 1.35);
|
||||
const activeMarkerLift = clampScalar(target.range * 0.62, 0.95, 1.7);
|
||||
const activeMarkerScale = clampScalar(target.range * 0.82, 0.9, 2);
|
||||
|
||||
return {
|
||||
luxPosition: {
|
||||
x: target.center.x,
|
||||
y: target.center.y + luxLift,
|
||||
z: target.center.z
|
||||
},
|
||||
activeMarkerPosition: {
|
||||
x: target.center.x,
|
||||
y: target.center.y + activeMarkerLift,
|
||||
z: target.center.z
|
||||
},
|
||||
activeMarkerScale
|
||||
};
|
||||
}
|
||||
|
||||
function dampScalar(current: number, target: number, rate: number, dt: number) {
|
||||
return current + (target - current) * Math.min(1, dt * rate);
|
||||
}
|
||||
@@ -477,30 +503,49 @@ export class RuntimeHost {
|
||||
private readonly targetingLuxGroup = new Group();
|
||||
private readonly targetingActiveGroup = new Group();
|
||||
private readonly targetingLuxMesh = new Mesh(
|
||||
new SphereGeometry(0.065, 16, 8),
|
||||
new SphereGeometry(0.13, 24, 12),
|
||||
new MeshBasicMaterial({
|
||||
color: 0x8df7ff,
|
||||
depthTest: false,
|
||||
depthWrite: false,
|
||||
transparent: true,
|
||||
opacity: 0.95
|
||||
})
|
||||
);
|
||||
private readonly targetingLuxGlowMesh = new Mesh(
|
||||
new SphereGeometry(0.34, 24, 12),
|
||||
new MeshBasicMaterial({
|
||||
blending: AdditiveBlending,
|
||||
color: 0x8df7ff,
|
||||
depthTest: false,
|
||||
depthWrite: false,
|
||||
transparent: true,
|
||||
opacity: 0.28
|
||||
})
|
||||
);
|
||||
private readonly targetingLuxLight = new PointLight(0x8df7ff, 1.25, 3.2, 2);
|
||||
private readonly targetingActiveRing = new Mesh(
|
||||
new TorusGeometry(0.42, 0.018, 8, 36),
|
||||
new TorusGeometry(0.52, 0.026, 8, 48),
|
||||
new MeshBasicMaterial({
|
||||
color: 0xfff2a2,
|
||||
depthTest: false,
|
||||
depthWrite: false,
|
||||
transparent: true,
|
||||
opacity: 0.95
|
||||
})
|
||||
);
|
||||
private readonly targetingActiveArrow = new Mesh(
|
||||
new ConeGeometry(0.12, 0.24, 12),
|
||||
new ConeGeometry(0.16, 0.32, 16),
|
||||
new MeshBasicMaterial({
|
||||
color: 0xfff2a2,
|
||||
depthTest: false,
|
||||
depthWrite: false,
|
||||
transparent: true,
|
||||
opacity: 0.95
|
||||
})
|
||||
);
|
||||
private targetingLuxInitialized = false;
|
||||
private targetingVisualTime = 0;
|
||||
private readonly firstPersonController =
|
||||
new FirstPersonNavigationController();
|
||||
private readonly thirdPersonController =
|
||||
@@ -644,8 +689,15 @@ export class RuntimeHost {
|
||||
this.scene.add(this.brushGroup);
|
||||
this.scene.add(this.terrainGroup);
|
||||
this.scene.add(this.modelGroup);
|
||||
this.targetingLuxMesh.renderOrder = 10000;
|
||||
this.targetingLuxGlowMesh.renderOrder = 9999;
|
||||
this.targetingLuxLight.castShadow = false;
|
||||
this.targetingLuxGroup.add(this.targetingLuxGlowMesh);
|
||||
this.targetingLuxGroup.add(this.targetingLuxMesh);
|
||||
this.targetingActiveArrow.position.y = 0.58;
|
||||
this.targetingLuxGroup.add(this.targetingLuxLight);
|
||||
this.targetingActiveRing.renderOrder = 10001;
|
||||
this.targetingActiveArrow.renderOrder = 10002;
|
||||
this.targetingActiveArrow.position.y = 0.68;
|
||||
this.targetingActiveArrow.rotation.x = Math.PI;
|
||||
this.targetingActiveGroup.add(this.targetingActiveRing);
|
||||
this.targetingActiveGroup.add(this.targetingActiveArrow);
|
||||
@@ -1152,6 +1204,8 @@ export class RuntimeHost {
|
||||
this.shaderSkyEnvironmentCache?.dispose();
|
||||
this.targetingLuxMesh.geometry.dispose();
|
||||
this.targetingLuxMesh.material.dispose();
|
||||
this.targetingLuxGlowMesh.geometry.dispose();
|
||||
this.targetingLuxGlowMesh.material.dispose();
|
||||
this.targetingActiveRing.geometry.dispose();
|
||||
this.targetingActiveRing.material.dispose();
|
||||
this.targetingActiveArrow.geometry.dispose();
|
||||
@@ -5363,6 +5417,7 @@ export class RuntimeHost {
|
||||
this.activeRuntimeTargetReference = null;
|
||||
this.previousTargetCycleInputActive = false;
|
||||
this.targetingLuxInitialized = false;
|
||||
this.targetingVisualTime = 0;
|
||||
this.targetingVisualGroup.visible = false;
|
||||
this.targetingLuxGroup.visible = false;
|
||||
this.targetingActiveGroup.visible = false;
|
||||
@@ -5552,11 +5607,15 @@ export class RuntimeHost {
|
||||
return;
|
||||
}
|
||||
|
||||
const lift = clampScalar(visualTarget.range * 0.18, 0.25, 0.55);
|
||||
this.targetingVisualTime += Math.max(0, dt);
|
||||
const visualPlacement = resolveRuntimeTargetVisualPlacement(visualTarget);
|
||||
const bob = Math.sin(this.targetingVisualTime * TARGETING_LUX_BOB_RATE) * 0.08;
|
||||
const pulse =
|
||||
1 + Math.sin(this.targetingVisualTime * TARGETING_LUX_PULSE_RATE) * 0.12;
|
||||
const targetLuxPosition = {
|
||||
x: visualTarget.center.x,
|
||||
y: visualTarget.center.y + lift,
|
||||
z: visualTarget.center.z
|
||||
x: visualPlacement.luxPosition.x,
|
||||
y: visualPlacement.luxPosition.y + bob,
|
||||
z: visualPlacement.luxPosition.z
|
||||
};
|
||||
|
||||
if (!this.targetingLuxInitialized) {
|
||||
@@ -5578,18 +5637,22 @@ export class RuntimeHost {
|
||||
);
|
||||
}
|
||||
|
||||
this.targetingLuxMesh.scale.setScalar(pulse);
|
||||
this.targetingLuxGlowMesh.scale.setScalar(1.05 + (pulse - 1) * 1.8);
|
||||
this.targetingLuxLight.intensity = activeTarget === null ? 1.15 : 1.45;
|
||||
this.targetingLuxLight.distance = activeTarget === null ? 3 : 3.6;
|
||||
this.targetingLuxGroup.lookAt(this.camera.position);
|
||||
this.targetingVisualGroup.visible = true;
|
||||
this.targetingLuxGroup.visible = true;
|
||||
this.targetingActiveGroup.visible = activeTarget !== null;
|
||||
|
||||
if (activeTarget !== null) {
|
||||
this.targetingActiveGroup.position.set(
|
||||
activeTarget.center.x,
|
||||
activeTarget.center.y,
|
||||
activeTarget.center.z
|
||||
visualPlacement.activeMarkerPosition.x,
|
||||
visualPlacement.activeMarkerPosition.y,
|
||||
visualPlacement.activeMarkerPosition.z
|
||||
);
|
||||
const markerScale = clampScalar(activeTarget.range * 1.05, 0.85, 2.4);
|
||||
this.targetingActiveGroup.scale.setScalar(markerScale);
|
||||
this.targetingActiveGroup.scale.setScalar(visualPlacement.activeMarkerScale);
|
||||
this.targetingActiveGroup.lookAt(this.camera.position);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +146,48 @@ describe("runtime interaction targeting", () => {
|
||||
).toBe("previous");
|
||||
});
|
||||
|
||||
it("proposes farther in-view targets without broadening click prompt range", () => {
|
||||
const distantNpc = createNpc({
|
||||
entityId: "npc-distant",
|
||||
name: "Far Guard",
|
||||
position: { x: 0, y: 0, z: 6.2 }
|
||||
});
|
||||
const distantInteractable = createInteractable({
|
||||
entityId: "interactable-distant",
|
||||
position: { x: 1.2, y: 1, z: 4.5 },
|
||||
radius: 1,
|
||||
prompt: "Use"
|
||||
});
|
||||
const scene = createRuntimeSceneFixture({
|
||||
npcs: [distantNpc],
|
||||
interactables: [distantInteractable],
|
||||
links: [
|
||||
createClickLink(distantNpc.entityId),
|
||||
createClickLink(distantInteractable.entityId)
|
||||
]
|
||||
});
|
||||
const candidates = resolveRuntimeTargetCandidates({
|
||||
interactionOrigin: { x: 0, y: 1, z: 0 },
|
||||
cameraPosition: { x: 0, y: 1.6, z: -1 },
|
||||
cameraForward: { x: 0, y: 0, z: 1 },
|
||||
runtimeScene: scene
|
||||
});
|
||||
const system = new RuntimeInteractionSystem();
|
||||
|
||||
expect(candidates.map((candidate) => candidate.entityId)).toEqual([
|
||||
"interactable-distant",
|
||||
"npc-distant"
|
||||
]);
|
||||
expect(
|
||||
system.resolveClickInteractionPrompt(
|
||||
{ x: 0, y: 1, z: 0 },
|
||||
{ x: 0, y: 1.6, z: -1 },
|
||||
{ x: 0, y: 0, z: 1 },
|
||||
scene
|
||||
)
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps click prompt resolution coherent with the shared target sources", () => {
|
||||
const npc = createNpc({
|
||||
entityId: "npc-talk",
|
||||
|
||||
Reference in New Issue
Block a user