auto-git:
[change] src/runtime-three/runtime-host.ts [change] tests/unit/runtime-host.test.ts
This commit is contained in:
@@ -313,6 +313,7 @@ const DIALOGUE_ATTENTION_CAMERA_TRANSITION_DURATION_SECONDS = 0.35;
|
||||
const DIALOGUE_ATTENTION_PLAYER_FOCUS_HEIGHT_FACTOR = 0.82;
|
||||
const DIALOGUE_ATTENTION_NPC_FOCUS_HEIGHT_FACTOR = 0.88;
|
||||
const DIALOGUE_PARTICIPANT_MIN_SURFACE_DISTANCE = 0.5;
|
||||
const DIALOGUE_PARTICIPANT_PUSHBACK_DURATION_SECONDS = 0.3;
|
||||
const DIALOGUE_PARTICIPANT_YAW_BLEND_RATE = 8;
|
||||
const DIALOGUE_PARTICIPANT_RESTORE_EPSILON_DEGREES = 0.5;
|
||||
|
||||
@@ -324,6 +325,16 @@ function clampScalar(value: number, min: number, max: number) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
function lerpScalar(start: number, end: number, t: number) {
|
||||
return start + (end - start) * t;
|
||||
}
|
||||
|
||||
function smoothStep01(value: number) {
|
||||
const t = clampScalar(value, 0, 1);
|
||||
|
||||
return t * t * (3 - 2 * t);
|
||||
}
|
||||
|
||||
function normalizeDegrees(value: number) {
|
||||
const wrapped = ((value + 180) % 360 + 360) % 360 - 180;
|
||||
|
||||
@@ -409,7 +420,10 @@ interface RuntimeDialogueParticipantState {
|
||||
npcCurrentYawDegrees: number;
|
||||
npcTargetYawDegrees: number;
|
||||
npcRestoreYawDegrees: number;
|
||||
playerStartFeetPosition: RuntimeTeleportTarget["position"];
|
||||
playerTargetFeetPosition: RuntimeTeleportTarget["position"];
|
||||
playerPositionBlendElapsedSeconds: number;
|
||||
playerPositionBlendDurationSeconds: number;
|
||||
playerCurrentYawDegrees: number;
|
||||
playerTargetYawDegrees: number;
|
||||
}
|
||||
@@ -1428,6 +1442,51 @@ export class RuntimeHost {
|
||||
return (Math.atan2(to.x - from.x, to.z - from.z) * 180) / Math.PI;
|
||||
}
|
||||
|
||||
private resolveDialogueParticipantPlayerFeetPosition(
|
||||
state: RuntimeDialogueParticipantState
|
||||
) {
|
||||
if (state.playerPositionBlendDurationSeconds <= 0) {
|
||||
return state.playerTargetFeetPosition;
|
||||
}
|
||||
|
||||
const blendT = smoothStep01(
|
||||
state.playerPositionBlendElapsedSeconds /
|
||||
state.playerPositionBlendDurationSeconds
|
||||
);
|
||||
|
||||
return {
|
||||
x: lerpScalar(
|
||||
state.playerStartFeetPosition.x,
|
||||
state.playerTargetFeetPosition.x,
|
||||
blendT
|
||||
),
|
||||
y: lerpScalar(
|
||||
state.playerStartFeetPosition.y,
|
||||
state.playerTargetFeetPosition.y,
|
||||
blendT
|
||||
),
|
||||
z: lerpScalar(
|
||||
state.playerStartFeetPosition.z,
|
||||
state.playerTargetFeetPosition.z,
|
||||
blendT
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
private isDialogueAttentionCameraReady(npcEntityId: string) {
|
||||
const state = this.dialogueParticipantState;
|
||||
|
||||
if (state === null || state.npcEntityId !== npcEntityId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
state.playerPositionBlendDurationSeconds <= 0 ||
|
||||
state.playerPositionBlendElapsedSeconds >=
|
||||
state.playerPositionBlendDurationSeconds - 1e-4
|
||||
);
|
||||
}
|
||||
|
||||
private resolveDialogueParticipantState(
|
||||
npc: RuntimeNpc
|
||||
): RuntimeDialogueParticipantState | null {
|
||||
@@ -1526,7 +1585,15 @@ export class RuntimeHost {
|
||||
npcCurrentYawDegrees: npc.yawDegrees,
|
||||
npcTargetYawDegrees,
|
||||
npcRestoreYawDegrees: npc.yawDegrees,
|
||||
playerStartFeetPosition: {
|
||||
...playerFeetPosition
|
||||
},
|
||||
playerTargetFeetPosition: targetFeetPosition,
|
||||
playerPositionBlendElapsedSeconds: 0,
|
||||
playerPositionBlendDurationSeconds:
|
||||
currentHorizontalDistance < desiredHorizontalDistance - 1e-4
|
||||
? DIALOGUE_PARTICIPANT_PUSHBACK_DURATION_SECONDS
|
||||
: 0,
|
||||
playerCurrentYawDegrees: currentPlayerYawDegrees,
|
||||
playerTargetYawDegrees
|
||||
};
|
||||
@@ -1597,10 +1664,16 @@ export class RuntimeHost {
|
||||
z: state.playerTargetFeetPosition.z
|
||||
}
|
||||
);
|
||||
state.playerPositionBlendElapsedSeconds = Math.min(
|
||||
state.playerPositionBlendDurationSeconds,
|
||||
state.playerPositionBlendElapsedSeconds + dt
|
||||
);
|
||||
const playerFeetPosition =
|
||||
this.resolveDialogueParticipantPlayerFeetPosition(state);
|
||||
state.playerTargetYawDegrees = this.resolveYawDegreesTowards(
|
||||
{
|
||||
x: state.playerTargetFeetPosition.x,
|
||||
z: state.playerTargetFeetPosition.z
|
||||
x: playerFeetPosition.x,
|
||||
z: playerFeetPosition.z
|
||||
},
|
||||
{
|
||||
x: npc.position.x,
|
||||
@@ -1620,7 +1693,7 @@ export class RuntimeHost {
|
||||
dt
|
||||
);
|
||||
this.applyTeleportPlayerAction({
|
||||
position: state.playerTargetFeetPosition,
|
||||
position: playerFeetPosition,
|
||||
yawDegrees: state.playerCurrentYawDegrees
|
||||
});
|
||||
this.setRuntimeNpcYawDegrees(state.npcEntityId, state.npcCurrentYawDegrees);
|
||||
@@ -2057,6 +2130,12 @@ export class RuntimeHost {
|
||||
const dialogueNpc = this.resolveDialogueAttentionNpc();
|
||||
|
||||
if (dialogueNpc !== null) {
|
||||
if (!this.isDialogueAttentionCameraReady(dialogueNpc.entityId)) {
|
||||
return {
|
||||
kind: "gameplay"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
kind: "dialogue",
|
||||
state:
|
||||
|
||||
@@ -707,7 +707,9 @@ describe("RuntimeHost", () => {
|
||||
|
||||
const hostInternals = host as unknown as {
|
||||
sceneReady: boolean;
|
||||
camera: PerspectiveCamera;
|
||||
collisionWorld: RapierCollisionWorld | null;
|
||||
activeCameraSourceKey: string | null;
|
||||
currentPlayerControllerTelemetry:
|
||||
| {
|
||||
feetPosition: { x: number; y: number; z: number };
|
||||
@@ -718,6 +720,13 @@ describe("RuntimeHost", () => {
|
||||
runtimeScene: ReturnType<typeof buildRuntimeSceneFromDocument> | null;
|
||||
activateDesiredNavigationController(): void;
|
||||
updateRuntimeDialogueParticipants(dt: number): void;
|
||||
applyActiveCameraRig(
|
||||
dt: number,
|
||||
previousCameraPose?: {
|
||||
position: Vector3;
|
||||
lookTarget: Vector3;
|
||||
}
|
||||
): { entityId: string } | null;
|
||||
createInteractionDispatcher(): {
|
||||
startNpcDialogue(
|
||||
npcEntityId: string,
|
||||
@@ -758,6 +767,10 @@ describe("RuntimeHost", () => {
|
||||
|
||||
hostInternals.updateRuntimeDialogueParticipants(0.05);
|
||||
hostInternals.updateRuntimeDialogueParticipants(0.05);
|
||||
hostInternals.applyActiveCameraRig(
|
||||
0.1,
|
||||
captureCameraPose(hostInternals.camera)
|
||||
);
|
||||
|
||||
const playerTelemetry = hostInternals.currentPlayerControllerTelemetry;
|
||||
const runtimeNpc =
|
||||
@@ -783,7 +796,9 @@ describe("RuntimeHost", () => {
|
||||
180) /
|
||||
Math.PI;
|
||||
|
||||
expect(playerDistanceFromNpc).toBeGreaterThanOrEqual(1.09);
|
||||
expect(playerDistanceFromNpc).toBeGreaterThan(0.1);
|
||||
expect(playerDistanceFromNpc).toBeLessThan(1.09);
|
||||
expect(hostInternals.activeCameraSourceKey).toBe("gameplay");
|
||||
expect(
|
||||
Math.abs(
|
||||
resolveShortestAngleDeltaDegrees(
|
||||
@@ -798,6 +813,22 @@ describe("RuntimeHost", () => {
|
||||
)
|
||||
).toBeGreaterThan(10);
|
||||
|
||||
hostInternals.updateRuntimeDialogueParticipants(0.1);
|
||||
hostInternals.updateRuntimeDialogueParticipants(0.1);
|
||||
hostInternals.applyActiveCameraRig(
|
||||
0.1,
|
||||
captureCameraPose(hostInternals.camera)
|
||||
);
|
||||
|
||||
const stagedPlayerTelemetry = hostInternals.currentPlayerControllerTelemetry;
|
||||
const stagedPlayerDistanceFromNpc = Math.hypot(
|
||||
(stagedPlayerTelemetry?.feetPosition.x ?? 0) - npc.position.x,
|
||||
(stagedPlayerTelemetry?.feetPosition.z ?? 0) - npc.position.z
|
||||
);
|
||||
|
||||
expect(stagedPlayerDistanceFromNpc).toBeGreaterThanOrEqual(1.09);
|
||||
expect(hostInternals.activeCameraSourceKey).toBe(`dialogue:${npc.id}`);
|
||||
|
||||
host.closeRuntimeDialogue();
|
||||
hostInternals.updateRuntimeDialogueParticipants(0.05);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user