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_PLAYER_FOCUS_HEIGHT_FACTOR = 0.82;
|
||||||
const DIALOGUE_ATTENTION_NPC_FOCUS_HEIGHT_FACTOR = 0.88;
|
const DIALOGUE_ATTENTION_NPC_FOCUS_HEIGHT_FACTOR = 0.88;
|
||||||
const DIALOGUE_PARTICIPANT_MIN_SURFACE_DISTANCE = 0.5;
|
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_YAW_BLEND_RATE = 8;
|
||||||
const DIALOGUE_PARTICIPANT_RESTORE_EPSILON_DEGREES = 0.5;
|
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));
|
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) {
|
function normalizeDegrees(value: number) {
|
||||||
const wrapped = ((value + 180) % 360 + 360) % 360 - 180;
|
const wrapped = ((value + 180) % 360 + 360) % 360 - 180;
|
||||||
|
|
||||||
@@ -409,7 +420,10 @@ interface RuntimeDialogueParticipantState {
|
|||||||
npcCurrentYawDegrees: number;
|
npcCurrentYawDegrees: number;
|
||||||
npcTargetYawDegrees: number;
|
npcTargetYawDegrees: number;
|
||||||
npcRestoreYawDegrees: number;
|
npcRestoreYawDegrees: number;
|
||||||
|
playerStartFeetPosition: RuntimeTeleportTarget["position"];
|
||||||
playerTargetFeetPosition: RuntimeTeleportTarget["position"];
|
playerTargetFeetPosition: RuntimeTeleportTarget["position"];
|
||||||
|
playerPositionBlendElapsedSeconds: number;
|
||||||
|
playerPositionBlendDurationSeconds: number;
|
||||||
playerCurrentYawDegrees: number;
|
playerCurrentYawDegrees: number;
|
||||||
playerTargetYawDegrees: number;
|
playerTargetYawDegrees: number;
|
||||||
}
|
}
|
||||||
@@ -1428,6 +1442,51 @@ export class RuntimeHost {
|
|||||||
return (Math.atan2(to.x - from.x, to.z - from.z) * 180) / Math.PI;
|
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(
|
private resolveDialogueParticipantState(
|
||||||
npc: RuntimeNpc
|
npc: RuntimeNpc
|
||||||
): RuntimeDialogueParticipantState | null {
|
): RuntimeDialogueParticipantState | null {
|
||||||
@@ -1526,7 +1585,15 @@ export class RuntimeHost {
|
|||||||
npcCurrentYawDegrees: npc.yawDegrees,
|
npcCurrentYawDegrees: npc.yawDegrees,
|
||||||
npcTargetYawDegrees,
|
npcTargetYawDegrees,
|
||||||
npcRestoreYawDegrees: npc.yawDegrees,
|
npcRestoreYawDegrees: npc.yawDegrees,
|
||||||
|
playerStartFeetPosition: {
|
||||||
|
...playerFeetPosition
|
||||||
|
},
|
||||||
playerTargetFeetPosition: targetFeetPosition,
|
playerTargetFeetPosition: targetFeetPosition,
|
||||||
|
playerPositionBlendElapsedSeconds: 0,
|
||||||
|
playerPositionBlendDurationSeconds:
|
||||||
|
currentHorizontalDistance < desiredHorizontalDistance - 1e-4
|
||||||
|
? DIALOGUE_PARTICIPANT_PUSHBACK_DURATION_SECONDS
|
||||||
|
: 0,
|
||||||
playerCurrentYawDegrees: currentPlayerYawDegrees,
|
playerCurrentYawDegrees: currentPlayerYawDegrees,
|
||||||
playerTargetYawDegrees
|
playerTargetYawDegrees
|
||||||
};
|
};
|
||||||
@@ -1597,10 +1664,16 @@ export class RuntimeHost {
|
|||||||
z: state.playerTargetFeetPosition.z
|
z: state.playerTargetFeetPosition.z
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
state.playerPositionBlendElapsedSeconds = Math.min(
|
||||||
|
state.playerPositionBlendDurationSeconds,
|
||||||
|
state.playerPositionBlendElapsedSeconds + dt
|
||||||
|
);
|
||||||
|
const playerFeetPosition =
|
||||||
|
this.resolveDialogueParticipantPlayerFeetPosition(state);
|
||||||
state.playerTargetYawDegrees = this.resolveYawDegreesTowards(
|
state.playerTargetYawDegrees = this.resolveYawDegreesTowards(
|
||||||
{
|
{
|
||||||
x: state.playerTargetFeetPosition.x,
|
x: playerFeetPosition.x,
|
||||||
z: state.playerTargetFeetPosition.z
|
z: playerFeetPosition.z
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
x: npc.position.x,
|
x: npc.position.x,
|
||||||
@@ -1620,7 +1693,7 @@ export class RuntimeHost {
|
|||||||
dt
|
dt
|
||||||
);
|
);
|
||||||
this.applyTeleportPlayerAction({
|
this.applyTeleportPlayerAction({
|
||||||
position: state.playerTargetFeetPosition,
|
position: playerFeetPosition,
|
||||||
yawDegrees: state.playerCurrentYawDegrees
|
yawDegrees: state.playerCurrentYawDegrees
|
||||||
});
|
});
|
||||||
this.setRuntimeNpcYawDegrees(state.npcEntityId, state.npcCurrentYawDegrees);
|
this.setRuntimeNpcYawDegrees(state.npcEntityId, state.npcCurrentYawDegrees);
|
||||||
@@ -2057,6 +2130,12 @@ export class RuntimeHost {
|
|||||||
const dialogueNpc = this.resolveDialogueAttentionNpc();
|
const dialogueNpc = this.resolveDialogueAttentionNpc();
|
||||||
|
|
||||||
if (dialogueNpc !== null) {
|
if (dialogueNpc !== null) {
|
||||||
|
if (!this.isDialogueAttentionCameraReady(dialogueNpc.entityId)) {
|
||||||
|
return {
|
||||||
|
kind: "gameplay"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
kind: "dialogue",
|
kind: "dialogue",
|
||||||
state:
|
state:
|
||||||
|
|||||||
@@ -707,7 +707,9 @@ describe("RuntimeHost", () => {
|
|||||||
|
|
||||||
const hostInternals = host as unknown as {
|
const hostInternals = host as unknown as {
|
||||||
sceneReady: boolean;
|
sceneReady: boolean;
|
||||||
|
camera: PerspectiveCamera;
|
||||||
collisionWorld: RapierCollisionWorld | null;
|
collisionWorld: RapierCollisionWorld | null;
|
||||||
|
activeCameraSourceKey: string | null;
|
||||||
currentPlayerControllerTelemetry:
|
currentPlayerControllerTelemetry:
|
||||||
| {
|
| {
|
||||||
feetPosition: { x: number; y: number; z: number };
|
feetPosition: { x: number; y: number; z: number };
|
||||||
@@ -718,6 +720,13 @@ describe("RuntimeHost", () => {
|
|||||||
runtimeScene: ReturnType<typeof buildRuntimeSceneFromDocument> | null;
|
runtimeScene: ReturnType<typeof buildRuntimeSceneFromDocument> | null;
|
||||||
activateDesiredNavigationController(): void;
|
activateDesiredNavigationController(): void;
|
||||||
updateRuntimeDialogueParticipants(dt: number): void;
|
updateRuntimeDialogueParticipants(dt: number): void;
|
||||||
|
applyActiveCameraRig(
|
||||||
|
dt: number,
|
||||||
|
previousCameraPose?: {
|
||||||
|
position: Vector3;
|
||||||
|
lookTarget: Vector3;
|
||||||
|
}
|
||||||
|
): { entityId: string } | null;
|
||||||
createInteractionDispatcher(): {
|
createInteractionDispatcher(): {
|
||||||
startNpcDialogue(
|
startNpcDialogue(
|
||||||
npcEntityId: string,
|
npcEntityId: string,
|
||||||
@@ -758,6 +767,10 @@ describe("RuntimeHost", () => {
|
|||||||
|
|
||||||
hostInternals.updateRuntimeDialogueParticipants(0.05);
|
hostInternals.updateRuntimeDialogueParticipants(0.05);
|
||||||
hostInternals.updateRuntimeDialogueParticipants(0.05);
|
hostInternals.updateRuntimeDialogueParticipants(0.05);
|
||||||
|
hostInternals.applyActiveCameraRig(
|
||||||
|
0.1,
|
||||||
|
captureCameraPose(hostInternals.camera)
|
||||||
|
);
|
||||||
|
|
||||||
const playerTelemetry = hostInternals.currentPlayerControllerTelemetry;
|
const playerTelemetry = hostInternals.currentPlayerControllerTelemetry;
|
||||||
const runtimeNpc =
|
const runtimeNpc =
|
||||||
@@ -783,7 +796,9 @@ describe("RuntimeHost", () => {
|
|||||||
180) /
|
180) /
|
||||||
Math.PI;
|
Math.PI;
|
||||||
|
|
||||||
expect(playerDistanceFromNpc).toBeGreaterThanOrEqual(1.09);
|
expect(playerDistanceFromNpc).toBeGreaterThan(0.1);
|
||||||
|
expect(playerDistanceFromNpc).toBeLessThan(1.09);
|
||||||
|
expect(hostInternals.activeCameraSourceKey).toBe("gameplay");
|
||||||
expect(
|
expect(
|
||||||
Math.abs(
|
Math.abs(
|
||||||
resolveShortestAngleDeltaDegrees(
|
resolveShortestAngleDeltaDegrees(
|
||||||
@@ -798,6 +813,22 @@ describe("RuntimeHost", () => {
|
|||||||
)
|
)
|
||||||
).toBeGreaterThan(10);
|
).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();
|
host.closeRuntimeDialogue();
|
||||||
hostInternals.updateRuntimeDialogueParticipants(0.05);
|
hostInternals.updateRuntimeDialogueParticipants(0.05);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user