import { waitFor } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";

import {
  createActiveSceneControlTargetRef,
  createActivateCameraRigOverrideControlEffect,
  createActorControlTargetRef,
  createCameraRigControlTargetRef,
  createClearCameraRigOverrideControlEffect,
  createFollowActorPathControlEffect,
  createLightControlTargetRef,
  createModelInstanceControlTargetRef,
  createPlayActorAnimationControlEffect,
  createPlayModelAnimationControlEffect,
  createProjectGlobalControlTargetRef,
  createPlaySoundControlEffect,
  createSetActorPresenceControlEffect,
  createSetProjectTimePausedControlEffect,
  type ControlEffect,
  createSetAmbientLightColorControlEffect,
  createSetAmbientLightIntensityControlEffect,
  createSetLightEnabledControlEffect,
  createSetLightColorControlEffect,
  createSetLightIntensityControlEffect,
  createSetModelInstanceVisibleControlEffect,
  createSetSoundVolumeControlEffect,
  createSetSunLightColorControlEffect,
  createSetSunLightIntensityControlEffect,
  createSoundEmitterControlTargetRef,
  createStopModelAnimationControlEffect,
  createStopSoundControlEffect
} from "../../src/controls/control-surface";
import { createBoxBrush } from "../../src/document/brushes";
import { createScenePath } from "../../src/document/paths";
import { createEmptySceneDocument } from "../../src/document/scene-document";
import {
  createCameraRigEntity,
  createCameraRigEntityTargetRef,
  createCameraRigPlayerTargetRef,
  createCameraRigWorldPointTargetRef,
  createInteractableEntity,
  createNpcEntity,
  createPointLightEntity,
  createPlayerStartEntity,
  createSoundEmitterEntity,
  createTriggerVolumeEntity
} from "../../src/entities/entity-instances";
import {
  createControlInteractionLink,
  type InteractionLink
} from "../../src/interactions/interaction-links";
import { createProjectScheduleRoutine } from "../../src/scheduler/project-scheduler";
import { createProjectSequence } from "../../src/sequencer/project-sequences";
import {
  createProjectAssetStorageKey,
  type AudioAssetRecord,
  type ModelAssetRecord
} from "../../src/assets/project-assets";
import { createModelInstance } from "../../src/assets/model-instances";
import { RapierCollisionWorld } from "../../src/runtime-three/rapier-collision-world";
import {
  RuntimeHost,
  resolveRuntimeTargetVisualPlacement,
  type RuntimeDialogueState,
  type RuntimePauseState,
  type RuntimeSceneLoadState
} from "../../src/runtime-three/runtime-host";
import { buildRuntimeSceneFromDocument } from "../../src/runtime-three/runtime-scene-build";
import {
  AnimationClip,
  BoxGeometry,
  PerspectiveCamera,
  Quaternion,
  Vector3,
  type AnimationMixer
} from "three";
import { createFixtureLoadedModelAssetFromGeometry } from "../helpers/model-collider-fixtures";

function createDeferred<T>() {
  let resolve: ((value: T) => void) | null = null;
  let reject: ((error: unknown) => void) | null = null;
  const promise = new Promise<T>((innerResolve, innerReject) => {
    resolve = innerResolve;
    reject = innerReject;
  });

  return {
    promise,
    resolve(value: T) {
      resolve?.(value);
    },
    reject(error: unknown) {
      reject?.(error);
    }
  };
}

function resolveYawPitchRadians(direction: Vector3) {
  return {
    yawRadians: Math.atan2(direction.x, direction.z),
    pitchRadians: Math.asin(Math.max(-1, Math.min(1, direction.y)))
  };
}

function captureCameraPose(camera: PerspectiveCamera) {
  const position = camera.position.clone();
  const lookTarget = position
    .clone()
    .add(camera.getWorldDirection(new Vector3()));

  return {
    position,
    lookTarget
  };
}

function createRuntimeKeyEvent(
  code: string,
  overrides: Partial<KeyboardEvent> = {}
): KeyboardEvent {
  return {
    code,
    defaultPrevented: false,
    repeat: false,
    altKey: false,
    ctrlKey: false,
    metaKey: false,
    target: null,
    preventDefault: vi.fn(),
    stopImmediatePropagation: vi.fn(),
    ...overrides
  } as unknown as KeyboardEvent;
}

function resolveShortestAngleDeltaDegrees(
  fromDegrees: number,
  toDegrees: number
) {
  return ((toDegrees - fromDegrees + 540) % 360) - 180;
}

describe("RuntimeHost", () => {
  afterEach(() => {
    vi.restoreAllMocks();
  });

  it("delays controller activation until collision setup reports the scene as ready", async () => {
    const runtimeScene = buildRuntimeSceneFromDocument(
      createEmptySceneDocument()
    );
    vi.spyOn(console, "warn").mockImplementation(() => undefined);
    const collisionWorld = {
      dispose: vi.fn(),
      resolveThirdPersonCameraCollision: vi.fn(
        (_pivot, desiredCameraPosition) => desiredCameraPosition
      )
    } as unknown as RapierCollisionWorld;
    const deferredCollisionWorld = createDeferred<RapierCollisionWorld>();
    vi.spyOn(RapierCollisionWorld, "create").mockReturnValue(
      deferredCollisionWorld.promise
    );

    const runtimeMessages: Array<string | null> = [];
    const sceneLoadStates: RuntimeSceneLoadState[] = [];
    const host = new RuntimeHost({
      enableRendering: false
    });
    host.setRuntimeMessageHandler((message) => {
      runtimeMessages.push(message);
    });
    host.setSceneLoadStateHandler((state) => {
      sceneLoadStates.push(state);
    });

    host.loadScene(runtimeScene);
    host.setNavigationMode("thirdPerson");

    expect(sceneLoadStates).toEqual([
      {
        status: "loading",
        message: null
      }
    ]);
    expect(runtimeMessages).toEqual([null]);

    deferredCollisionWorld.resolve(collisionWorld);

    await waitFor(() => {
      expect(sceneLoadStates).toContainEqual({
        status: "ready",
        message: null
      });
      expect(runtimeMessages).toContain(
        "Third Person active. Drag to orbit the camera, use the right stick for gamepad camera look, move with your authored bindings, and scroll to zoom."
      );
    });

    host.dispose();
    expect(collisionWorld.dispose).toHaveBeenCalledTimes(1);
  });

  it("uses the authored interact binding instead of always dispatching left mouse clicks", () => {
    const playerStart = createPlayerStartEntity({
      id: "entity-player-start-authored-interact",
      inputBindings: {
        keyboard: {
          interact: "KeyE"
        },
        gamepad: {
          interact: "buttonWest"
        }
      }
    });
    const interactable = createInteractableEntity({
      id: "entity-authored-interact-target"
    });
    const runtimeScene = buildRuntimeSceneFromDocument({
      ...createEmptySceneDocument({ name: "Authored Interact Scene" }),
      entities: {
        [playerStart.id]: playerStart,
        [interactable.id]: interactable
      }
    });
    const host = new RuntimeHost({
      enableRendering: false
    });
    host.loadScene(runtimeScene);

    const hostInternals = host as unknown as {
      sceneReady: boolean;
      activeController: unknown;
      thirdPersonController: unknown;
      currentInteractionPrompt: {
        sourceEntityId: string;
        prompt: string;
        distance: number;
        range: number;
      } | null;
      interactionSystem: {
        dispatchClickInteraction: ReturnType<typeof vi.fn>;
      };
      handleRuntimePointerDown(event: {
        button: number;
        clientX: number;
        clientY: number;
        preventDefault(): void;
        stopImmediatePropagation(): void;
      }): void;
      handleRuntimeKeyDown(event: KeyboardEvent): void;
    };
    const dispatchClickInteraction = vi.fn();

    hostInternals.sceneReady = true;
    hostInternals.activeController = hostInternals.thirdPersonController;
    hostInternals.currentInteractionPrompt = {
      sourceEntityId: interactable.id,
      prompt: interactable.prompt,
      distance: 0,
      range: interactable.radius
    };
    hostInternals.interactionSystem.dispatchClickInteraction =
      dispatchClickInteraction;

    hostInternals.handleRuntimePointerDown({
      button: 0,
      clientX: 0,
      clientY: 0,
      preventDefault: vi.fn(),
      stopImmediatePropagation: vi.fn()
    });

    expect(dispatchClickInteraction).not.toHaveBeenCalled();

    hostInternals.handleRuntimeKeyDown(createRuntimeKeyEvent("KeyE"));

    expect(dispatchClickInteraction).toHaveBeenCalledTimes(1);
    expect(dispatchClickInteraction).toHaveBeenCalledWith(
      interactable.id,
      runtimeScene,
      expect.any(Object)
    );
  });

  it("starts default-active rigs in place and blends rig-to-rig overrides", () => {
    vi.spyOn(console, "warn").mockImplementation(() => undefined);
    vi.spyOn(RapierCollisionWorld, "create").mockResolvedValue({
      dispose: vi.fn(),
      resolveThirdPersonCameraCollision: vi.fn(
        (_pivot, desiredCameraPosition) => desiredCameraPosition
      )
    } as unknown as RapierCollisionWorld);

    const defaultRig = createCameraRigEntity({
      id: "entity-camera-rig-default",
      position: {
        x: 0,
        y: 5,
        z: 10
      },
      priority: 10,
      defaultActive: true,
      target: createCameraRigWorldPointTargetRef({
        x: 0,
        y: 1,
        z: 0
      }),
      transitionMode: "blend",
      transitionDurationSeconds: 0.75
    });
    const overrideRig = createCameraRigEntity({
      id: "entity-camera-rig-override",
      position: {
        x: 10,
        y: 4,
        z: -6
      },
      priority: 0,
      defaultActive: false,
      target: createCameraRigWorldPointTargetRef({
        x: 2,
        y: 2,
        z: -1
      }),
      transitionMode: "blend",
      transitionDurationSeconds: 0.5
    });
    const runtimeScene = buildRuntimeSceneFromDocument({
      ...createEmptySceneDocument({ name: "Camera Rig Priority Scene" }),
      entities: {
        [defaultRig.id]: defaultRig,
        [overrideRig.id]: overrideRig
      }
    });
    const host = new RuntimeHost({
      enableRendering: false
    });
    host.loadScene(runtimeScene);

    const hostInternals = host as unknown as {
      sceneReady: boolean;
      camera: PerspectiveCamera;
      activeRuntimeCameraRig: { entityId: string } | null;
      cameraTransitionState: { elapsedSeconds: number } | null;
      applyActiveCameraRig(dt: number): { entityId: string } | null;
    };

    hostInternals.sceneReady = true;
    hostInternals.camera.position.set(-12, 3, 14);
    hostInternals.camera.lookAt(0, 1.6, 0);

    expect(hostInternals.applyActiveCameraRig(0.1)?.entityId).toBe(
      defaultRig.id
    );
    expect(hostInternals.camera.position).toMatchObject(defaultRig.position);
    expect(hostInternals.cameraTransitionState).toBeNull();

    host.setActiveCameraRigOverride(overrideRig.id);

    expect(hostInternals.applyActiveCameraRig(0.25)?.entityId).toBe(
      overrideRig.id
    );
    expect(hostInternals.activeRuntimeCameraRig?.entityId).toBe(overrideRig.id);
    expect(hostInternals.camera.position.x).toBeCloseTo(5, 4);
    expect(hostInternals.camera.position.y).toBeCloseTo(4.5, 4);
    expect(hostInternals.camera.position.z).toBeCloseTo(2, 4);

    hostInternals.applyActiveCameraRig(0.25);

    expect(hostInternals.camera.position).toMatchObject(overrideRig.position);

    host.dispose();
  });

  it("blends from gameplay camera into an active rig override", () => {
    vi.spyOn(console, "warn").mockImplementation(() => undefined);
    vi.spyOn(RapierCollisionWorld, "create").mockResolvedValue({
      dispose: vi.fn(),
      resolveThirdPersonCameraCollision: vi.fn(
        (_pivot, desiredCameraPosition) => desiredCameraPosition
      )
    } as unknown as RapierCollisionWorld);

    const cameraRig = createCameraRigEntity({
      id: "entity-camera-rig-gameplay-entry",
      defaultActive: false,
      position: {
        x: 8,
        y: 4,
        z: -6
      },
      target: createCameraRigWorldPointTargetRef({
        x: 0,
        y: 1.5,
        z: 0
      }),
      transitionMode: "blend",
      transitionDurationSeconds: 0.5
    });
    const runtimeScene = buildRuntimeSceneFromDocument({
      ...createEmptySceneDocument({ name: "Camera Rig Gameplay Entry Scene" }),
      entities: {
        [cameraRig.id]: cameraRig
      }
    });
    const host = new RuntimeHost({
      enableRendering: false
    });
    host.loadScene(runtimeScene);

    const hostInternals = host as unknown as {
      sceneReady: boolean;
      camera: PerspectiveCamera;
      cameraTransitionState: { elapsedSeconds: number } | null;
      applyActiveCameraRig(dt: number): { entityId: string } | null;
    };

    hostInternals.sceneReady = true;
    hostInternals.applyActiveCameraRig(0);
    hostInternals.camera.position.set(0, 2, 12);
    hostInternals.camera.lookAt(0, 1.5, 0);

    host.setActiveCameraRigOverride(cameraRig.id);

    expect(hostInternals.applyActiveCameraRig(0.25)?.entityId).toBe(
      cameraRig.id
    );
    expect(hostInternals.cameraTransitionState).not.toBeNull();
    expect(hostInternals.camera.position.x).toBeCloseTo(4, 4);
    expect(hostInternals.camera.position.y).toBeCloseTo(3, 4);
    expect(hostInternals.camera.position.z).toBeCloseTo(3, 4);

    hostInternals.applyActiveCameraRig(0.25);

    expect(hostInternals.camera.position).toMatchObject(cameraRig.position);

    host.dispose();
  });

  it("blends from a rig back to the gameplay camera", () => {
    vi.spyOn(console, "warn").mockImplementation(() => undefined);
    vi.spyOn(RapierCollisionWorld, "create").mockResolvedValue({
      dispose: vi.fn(),
      resolveThirdPersonCameraCollision: vi.fn(
        (_pivot, desiredCameraPosition) => desiredCameraPosition
      )
    } as unknown as RapierCollisionWorld);

    const cameraRig = createCameraRigEntity({
      id: "entity-camera-rig-gameplay-exit",
      defaultActive: false,
      position: {
        x: 8,
        y: 4,
        z: -6
      },
      target: createCameraRigWorldPointTargetRef({
        x: 0,
        y: 1.5,
        z: 0
      }),
      transitionMode: "blend",
      transitionDurationSeconds: 0.5
    });
    const runtimeScene = buildRuntimeSceneFromDocument({
      ...createEmptySceneDocument({ name: "Camera Rig Gameplay Exit Scene" }),
      entities: {
        [cameraRig.id]: cameraRig
      }
    });
    const host = new RuntimeHost({
      enableRendering: false
    });
    host.loadScene(runtimeScene);

    const hostInternals = host as unknown as {
      sceneReady: boolean;
      camera: PerspectiveCamera;
      cameraTransitionState: { elapsedSeconds: number } | null;
      applyActiveCameraRig(
        dt: number,
        previousCameraPose?: {
          position: Vector3;
          lookTarget: Vector3;
        }
      ): { entityId: string } | null;
    };

    hostInternals.sceneReady = true;
    hostInternals.applyActiveCameraRig(0);
    hostInternals.camera.position.set(0, 2, 12);
    hostInternals.camera.lookAt(0, 1.5, 0);
    host.setActiveCameraRigOverride(cameraRig.id);
    hostInternals.applyActiveCameraRig(0.5);

    const previousRigPose = captureCameraPose(hostInternals.camera);

    host.setActiveCameraRigOverride(null);
    hostInternals.camera.position.set(-6, 3, 8);
    hostInternals.camera.lookAt(0, 1.5, 0);

    expect(
      hostInternals.applyActiveCameraRig(0.25, previousRigPose)
    ).toBeNull();
    expect(hostInternals.cameraTransitionState).not.toBeNull();
    expect(hostInternals.camera.position.x).toBeCloseTo(1, 4);
    expect(hostInternals.camera.position.y).toBeCloseTo(3.5, 4);
    expect(hostInternals.camera.position.z).toBeCloseTo(1, 4);

    hostInternals.camera.position.set(-6, 3, 8);
    hostInternals.camera.lookAt(0, 1.5, 0);
    hostInternals.applyActiveCameraRig(0.25, previousRigPose);

    expect(hostInternals.camera.position).toMatchObject({
      x: -6,
      y: 3,
      z: 8
    });

    host.dispose();
  });

  it("activates the dialogue attention camera and pauses runtime when dialogue opens", () => {
    vi.spyOn(console, "warn").mockImplementation(() => undefined);
    vi.spyOn(RapierCollisionWorld, "create").mockResolvedValue({
      dispose: vi.fn(),
      resolveThirdPersonCameraCollision: vi.fn(
        (_pivot, desiredCameraPosition) => desiredCameraPosition
      )
    } as unknown as RapierCollisionWorld);

    const playerStart = createPlayerStartEntity({
      id: "entity-player-start-dialogue-camera",
      position: {
        x: 0,
        y: 0,
        z: 0
      }
    });
    const npc = createNpcEntity({
      id: "entity-npc-dialogue-camera",
      position: {
        x: 2,
        y: 0,
        z: 2
      },
      dialogues: [
        {
          id: "dialogue-attention",
          title: "Attention",
          lines: [
            {
              id: "dialogue-attention-line-1",
              text: "Look this way."
            }
          ]
        }
      ],
      defaultDialogueId: "dialogue-attention"
    });
    const runtimeScene = buildRuntimeSceneFromDocument({
      ...createEmptySceneDocument({ name: "Dialogue Attention Scene" }),
      entities: {
        [playerStart.id]: playerStart,
        [npc.id]: npc
      }
    });
    const host = new RuntimeHost({
      enableRendering: false
    });
    host.loadScene(runtimeScene);

    const hostInternals = host as unknown as {
      sceneReady: boolean;
      camera: PerspectiveCamera;
      currentPauseState: RuntimePauseState;
      activeCameraSourceKey: string | null;
      activeRuntimeCameraRig: { entityId: string } | null;
      cameraTransitionState: { elapsedSeconds: number } | null;
      applyActiveCameraRig(
        dt: number,
        previousCameraPose?: {
          position: Vector3;
          lookTarget: Vector3;
        }
      ): { entityId: string } | null;
      createInteractionDispatcher(): {
        startNpcDialogue(
          npcEntityId: string,
          dialogueId: string | null,
          source?: {
            kind: "interactionLink" | "npc" | "direct";
            sourceEntityId: string | null;
            linkId: string | null;
            trigger: "enter" | "exit" | "click" | null;
          }
        ): void;
      };
    };
    const dispatcher = hostInternals.createInteractionDispatcher();

    hostInternals.sceneReady = true;
    hostInternals.camera.position.set(0, 2.6, 6);
    hostInternals.camera.lookAt(0, 1.6, 0);
    hostInternals.applyActiveCameraRig(
      0,
      captureCameraPose(hostInternals.camera)
    );

    dispatcher.startNpcDialogue(npc.id, null, {
      kind: "npc",
      sourceEntityId: npc.id,
      linkId: null,
      trigger: "click"
    });

    const gameplayPose = captureCameraPose(hostInternals.camera);
    hostInternals.applyActiveCameraRig(0.175, gameplayPose);

    expect(hostInternals.currentPauseState).toEqual({
      paused: true,
      source: "dialogue"
    });
    expect(hostInternals.activeCameraSourceKey).toBe(`dialogue:${npc.id}`);
    expect(hostInternals.activeRuntimeCameraRig).toBeNull();
    expect(hostInternals.cameraTransitionState).not.toBeNull();
    expect(hostInternals.camera.position.z).toBeLessThan(6);

    hostInternals.applyActiveCameraRig(0.175, gameplayPose);

    const cameraForward = hostInternals.camera.getWorldDirection(new Vector3());
    const playerFocusDirection = new Vector3(
      -hostInternals.camera.position.x,
      1.312 - hostInternals.camera.position.y,
      -hostInternals.camera.position.z
    ).normalize();
    const npcFocusDirection = new Vector3(
      2 - hostInternals.camera.position.x,
      1.408 - hostInternals.camera.position.y,
      2 - hostInternals.camera.position.z
    ).normalize();

    expect(cameraForward.dot(playerFocusDirection)).toBeGreaterThan(0.6);
    expect(cameraForward.dot(npcFocusDirection)).toBeGreaterThan(0.6);

    host.dispose();
  });

  it("resolves dialogue attention camera collision from the conversation midpoint", () => {
    vi.spyOn(console, "warn").mockImplementation(() => undefined);
    const resolveThirdPersonCameraCollision = vi.fn(
      (
        pivot: { x: number; y: number; z: number },
        desiredCameraPosition: { x: number; y: number; z: number },
        radius: number
      ) => {
        expect(radius).toBeGreaterThan(0);

        return {
          x: pivot.x + (desiredCameraPosition.x - pivot.x) * 0.55,
          y: pivot.y + (desiredCameraPosition.y - pivot.y) * 0.55,
          z: pivot.z + (desiredCameraPosition.z - pivot.z) * 0.55
        };
      }
    );
    vi.spyOn(RapierCollisionWorld, "create").mockResolvedValue({
      dispose: vi.fn(),
      resolveThirdPersonCameraCollision
    } as unknown as RapierCollisionWorld);

    const playerStart = createPlayerStartEntity({
      id: "entity-player-start-dialogue-collision",
      position: {
        x: 0,
        y: 0,
        z: 0
      }
    });
    const npc = createNpcEntity({
      id: "entity-npc-dialogue-collision",
      position: {
        x: 2,
        y: 0,
        z: 2
      },
      dialogues: [
        {
          id: "dialogue-collision",
          title: "Collision",
          lines: [
            {
              id: "dialogue-collision-line-1",
              text: "Avoid the wall."
            }
          ]
        }
      ],
      defaultDialogueId: "dialogue-collision"
    });
    const runtimeScene = buildRuntimeSceneFromDocument({
      ...createEmptySceneDocument({ name: "Dialogue Collision Scene" }),
      entities: {
        [playerStart.id]: playerStart,
        [npc.id]: npc
      }
    });
    const host = new RuntimeHost({
      enableRendering: false
    });
    host.loadScene(runtimeScene);

    const hostInternals = host as unknown as {
      sceneReady: boolean;
      camera: PerspectiveCamera;
      collisionWorld: RapierCollisionWorld | null;
      applyActiveCameraRig(
        dt: number,
        previousCameraPose?: {
          position: Vector3;
          lookTarget: Vector3;
        }
      ): { entityId: string } | null;
      createInteractionDispatcher(): {
        startNpcDialogue(
          npcEntityId: string,
          dialogueId: string | null,
          source?: {
            kind: "interactionLink" | "npc" | "direct";
            sourceEntityId: string | null;
            linkId: string | null;
            trigger: "enter" | "exit" | "click" | null;
          }
        ): void;
      };
    };
    const dispatcher = hostInternals.createInteractionDispatcher();

    hostInternals.sceneReady = true;
    hostInternals.collisionWorld = {
      dispose: vi.fn(),
      resolveThirdPersonCameraCollision
    } as unknown as RapierCollisionWorld;
    hostInternals.camera.position.set(0, 2.6, 6);
    hostInternals.camera.lookAt(0, 1.6, 0);

    dispatcher.startNpcDialogue(npc.id, null, {
      kind: "npc",
      sourceEntityId: npc.id,
      linkId: null,
      trigger: "click"
    });

    const gameplayPose = captureCameraPose(hostInternals.camera);
    hostInternals.applyActiveCameraRig(0.175, gameplayPose);

    expect(resolveThirdPersonCameraCollision).toHaveBeenCalled();
    const lastCollisionCall =
      resolveThirdPersonCameraCollision.mock.calls.at(-1);

    expect(lastCollisionCall).toBeDefined();

    const [pivot, desiredCameraPosition, radius] = lastCollisionCall as [
      { x: number; y: number; z: number },
      { x: number; y: number; z: number },
      number
    ];
    expect(pivot).toMatchObject({
      x: 1,
      z: 1
    });
    expect(pivot?.y).toBeCloseTo(1.36, 5);
    expect(radius).toBe(0.2);
    expect(hostInternals.camera.position.x).toBeCloseTo(
      pivot.x + (desiredCameraPosition.x - pivot.x) * 0.55
    );
    expect(hostInternals.camera.position.y).toBeCloseTo(
      pivot.y + (desiredCameraPosition.y - pivot.y) * 0.55
    );
    expect(hostInternals.camera.position.z).toBeCloseTo(
      pivot.z + (desiredCameraPosition.z - pivot.z) * 0.55
    );

    host.dispose();
  });

  it("smooths runtime camera collision recovery when an obstruction clears", () => {
    const host = new RuntimeHost({
      enableRendering: false
    });
    const hostInternals = host as unknown as {
      camera: PerspectiveCamera;
      collisionWorld: RapierCollisionWorld | null;
      applyCameraPose(
        pose: {
          position: Vector3;
          lookTarget: Vector3;
          collisionPivot: Vector3;
          collisionRadius: number;
        },
        dt?: number
      ): void;
    };
    const pivot = new Vector3(0, 1, 0);
    const desiredPosition = new Vector3(0, 2, -4);
    const pose = {
      position: desiredPosition,
      lookTarget: new Vector3(0, 1, 0),
      collisionPivot: pivot,
      collisionRadius: 0.2
    };
    let collisionScale = 0.25;
    const distanceFromPivot = () =>
      hostInternals.camera.position.distanceTo(pivot);
    const desiredDistance = desiredPosition.distanceTo(pivot);

    hostInternals.collisionWorld = {
      dispose: vi.fn(),
      resolveThirdPersonCameraCollision: vi.fn(
        (
          collisionPivot: { x: number; y: number; z: number },
          desiredCameraPosition: { x: number; y: number; z: number }
        ) => ({
          x:
            collisionPivot.x +
            (desiredCameraPosition.x - collisionPivot.x) * collisionScale,
          y:
            collisionPivot.y +
            (desiredCameraPosition.y - collisionPivot.y) * collisionScale,
          z:
            collisionPivot.z +
            (desiredCameraPosition.z - collisionPivot.z) * collisionScale
        })
      )
    } as unknown as RapierCollisionWorld;

    hostInternals.applyCameraPose(pose, 0);
    const blockedDistance = distanceFromPivot();

    collisionScale = 1;
    hostInternals.applyCameraPose(pose, 0.1);
    const recoveringDistance = distanceFromPivot();

    expect(recoveringDistance).toBeGreaterThan(blockedDistance);
    expect(recoveringDistance).toBeLessThan(desiredDistance);

    hostInternals.applyCameraPose(pose, 1);

    expect(distanceFromPivot()).toBeCloseTo(desiredDistance);
    host.dispose();
  });

  it("stages dialogue participants with minimum spacing and restores npc yaw after dialogue", () => {
    vi.spyOn(console, "warn").mockImplementation(() => undefined);
    const canOccupyPlayerShape = vi.fn(() => true);
    vi.spyOn(RapierCollisionWorld, "create").mockResolvedValue({
      dispose: vi.fn(),
      canOccupyPlayerShape,
      resolveThirdPersonCameraCollision: vi.fn(
        (_pivot, desiredCameraPosition) => desiredCameraPosition
      )
    } as unknown as RapierCollisionWorld);

    const playerStart = createPlayerStartEntity({
      id: "entity-player-start-dialogue-spacing",
      position: {
        x: 1.9,
        y: 0,
        z: 2
      },
      yawDegrees: 0
    });
    const npc = createNpcEntity({
      id: "entity-npc-dialogue-spacing",
      position: {
        x: 2,
        y: 0,
        z: 2
      },
      yawDegrees: 0,
      dialogues: [
        {
          id: "dialogue-spacing",
          title: "Spacing",
          lines: [
            {
              id: "dialogue-spacing-line-1",
              text: "Take a step back."
            }
          ]
        }
      ],
      defaultDialogueId: "dialogue-spacing"
    });
    const runtimeScene = buildRuntimeSceneFromDocument({
      ...createEmptySceneDocument({ name: "Dialogue Spacing Scene" }),
      entities: {
        [playerStart.id]: playerStart,
        [npc.id]: npc
      }
    });
    const host = new RuntimeHost({
      enableRendering: false
    });
    host.loadScene(runtimeScene);

    const hostInternals = host as unknown as {
      sceneReady: boolean;
      camera: PerspectiveCamera;
      collisionWorld: RapierCollisionWorld | null;
      activeCameraSourceKey: string | null;
      currentPlayerControllerTelemetry: {
        feetPosition: { x: number; y: number; z: number };
        yawDegrees: number;
      } | null;
      dialogueParticipantState: { npcEntityId: string } | null;
      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,
          dialogueId: string | null,
          source?: {
            kind: "interactionLink" | "npc" | "direct";
            sourceEntityId: string | null;
            linkId: string | null;
            trigger: "enter" | "exit" | "click" | null;
          }
        ): void;
      };
    };
    const dispatcher = hostInternals.createInteractionDispatcher();

    hostInternals.sceneReady = true;
    hostInternals.collisionWorld = {
      dispose: vi.fn(),
      canOccupyPlayerShape,
      resolveThirdPersonCameraCollision: vi.fn(
        (_pivot, desiredCameraPosition) => desiredCameraPosition
      )
    } as unknown as RapierCollisionWorld;
    hostInternals.activateDesiredNavigationController();

    expect(
      hostInternals.currentPlayerControllerTelemetry?.feetPosition
    ).toEqual(playerStart.position);

    dispatcher.startNpcDialogue(npc.id, null, {
      kind: "npc",
      sourceEntityId: npc.id,
      linkId: null,
      trigger: "click"
    });

    expect(hostInternals.dialogueParticipantState?.npcEntityId).toBe(npc.id);

    hostInternals.updateRuntimeDialogueParticipants(0.05);
    hostInternals.updateRuntimeDialogueParticipants(0.05);
    hostInternals.applyActiveCameraRig(
      0.1,
      captureCameraPose(hostInternals.camera)
    );

    const playerTelemetry = hostInternals.currentPlayerControllerTelemetry;
    const runtimeNpc =
      hostInternals.runtimeScene?.entities.npcs.find(
        (candidate) => candidate.entityId === npc.id
      ) ?? null;

    expect(canOccupyPlayerShape).toHaveBeenCalled();
    expect(playerTelemetry).not.toBeNull();
    expect(runtimeNpc).not.toBeNull();

    const playerFeetPosition = playerTelemetry?.feetPosition ?? {
      x: 0,
      y: 0,
      z: 0
    };
    const playerDistanceFromNpc = Math.hypot(
      playerFeetPosition.x - npc.position.x,
      playerFeetPosition.z - npc.position.z
    );
    const playerTargetYawDegrees =
      (Math.atan2(
        npc.position.x - playerFeetPosition.x,
        npc.position.z - playerFeetPosition.z
      ) *
        180) /
      Math.PI;

    expect(playerDistanceFromNpc).toBeGreaterThan(0.1);
    expect(playerDistanceFromNpc).toBeLessThan(1.09);
    expect(hostInternals.activeCameraSourceKey).toBe("gameplay");
    expect(
      Math.abs(
        resolveShortestAngleDeltaDegrees(
          playerTelemetry?.yawDegrees ?? 0,
          playerTargetYawDegrees
        )
      )
    ).toBeLessThan(35);
    expect(
      Math.abs(resolveShortestAngleDeltaDegrees(runtimeNpc?.yawDegrees ?? 0, 0))
    ).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);

    expect(runtimeNpc?.yawDegrees).not.toBeCloseTo(0, 3);

    for (let step = 0; step < 10; step += 1) {
      hostInternals.updateRuntimeDialogueParticipants(0.05);
    }

    expect(
      Math.abs(runtimeNpc?.yawDegrees ?? Number.POSITIVE_INFINITY)
    ).toBeLessThan(1);
    expect(hostInternals.dialogueParticipantState).toBeNull();

    host.dispose();
  });

  it("keeps explicit camera rig overrides above dialogue attention", () => {
    vi.spyOn(console, "warn").mockImplementation(() => undefined);
    vi.spyOn(RapierCollisionWorld, "create").mockResolvedValue({
      dispose: vi.fn(),
      resolveThirdPersonCameraCollision: vi.fn(
        (_pivot, desiredCameraPosition) => desiredCameraPosition
      )
    } as unknown as RapierCollisionWorld);

    const npc = createNpcEntity({
      id: "entity-npc-dialogue-rig-priority",
      position: {
        x: 2,
        y: 0,
        z: 2
      },
      dialogues: [
        {
          id: "dialogue-priority",
          title: "Priority",
          lines: [
            {
              id: "dialogue-priority-line-1",
              text: "Rig wins."
            }
          ]
        }
      ],
      defaultDialogueId: "dialogue-priority"
    });
    const rig = createCameraRigEntity({
      id: "entity-camera-rig-dialogue-override",
      position: {
        x: 8,
        y: 4,
        z: -6
      },
      target: createCameraRigEntityTargetRef(npc.id),
      transitionMode: "cut"
    });
    const runtimeScene = buildRuntimeSceneFromDocument({
      ...createEmptySceneDocument({ name: "Dialogue Rig Priority Scene" }),
      entities: {
        [npc.id]: npc,
        [rig.id]: rig
      }
    });
    const host = new RuntimeHost({
      enableRendering: false
    });
    host.loadScene(runtimeScene);

    const hostInternals = host as unknown as {
      sceneReady: boolean;
      camera: PerspectiveCamera;
      currentPauseState: RuntimePauseState;
      activeCameraSourceKey: string | null;
      activeRuntimeCameraRig: { entityId: string } | null;
      applyActiveCameraRig(
        dt: number,
        previousCameraPose?: {
          position: Vector3;
          lookTarget: Vector3;
        }
      ): { entityId: string } | null;
      createInteractionDispatcher(): {
        startNpcDialogue(
          npcEntityId: string,
          dialogueId: string | null,
          source?: {
            kind: "interactionLink" | "npc" | "direct";
            sourceEntityId: string | null;
            linkId: string | null;
            trigger: "enter" | "exit" | "click" | null;
          }
        ): void;
      };
    };
    const dispatcher = hostInternals.createInteractionDispatcher();

    hostInternals.sceneReady = true;
    dispatcher.startNpcDialogue(npc.id, null, {
      kind: "npc",
      sourceEntityId: npc.id,
      linkId: null,
      trigger: "click"
    });
    host.setActiveCameraRigOverride(rig.id);

    expect(hostInternals.applyActiveCameraRig(0)?.entityId).toBe(rig.id);
    expect(hostInternals.currentPauseState).toEqual({
      paused: true,
      source: "dialogue"
    });
    expect(hostInternals.activeCameraSourceKey).toBe(`rig:${rig.id}`);
    expect(hostInternals.activeRuntimeCameraRig?.entityId).toBe(rig.id);
    expect(hostInternals.camera.position).toMatchObject(rig.position);

    host.dispose();
  });

  it("blends back to gameplay camera when dialogue closes", () => {
    vi.spyOn(console, "warn").mockImplementation(() => undefined);
    vi.spyOn(RapierCollisionWorld, "create").mockResolvedValue({
      dispose: vi.fn(),
      resolveThirdPersonCameraCollision: vi.fn(
        (_pivot, desiredCameraPosition) => desiredCameraPosition
      )
    } as unknown as RapierCollisionWorld);

    const playerStart = createPlayerStartEntity({
      id: "entity-player-start-dialogue-exit",
      position: {
        x: 0,
        y: 0,
        z: 0
      }
    });
    const npc = createNpcEntity({
      id: "entity-npc-dialogue-exit",
      position: {
        x: 2,
        y: 0,
        z: 2
      },
      dialogues: [
        {
          id: "dialogue-exit",
          title: "Exit",
          lines: [
            {
              id: "dialogue-exit-line-1",
              text: "Back to play."
            }
          ]
        }
      ],
      defaultDialogueId: "dialogue-exit"
    });
    const runtimeScene = buildRuntimeSceneFromDocument({
      ...createEmptySceneDocument({ name: "Dialogue Exit Scene" }),
      entities: {
        [playerStart.id]: playerStart,
        [npc.id]: npc
      }
    });
    const host = new RuntimeHost({
      enableRendering: false
    });
    host.loadScene(runtimeScene);

    const hostInternals = host as unknown as {
      sceneReady: boolean;
      camera: PerspectiveCamera;
      currentPauseState: RuntimePauseState;
      activeCameraSourceKey: string | null;
      cameraTransitionState: { elapsedSeconds: number } | null;
      applyActiveCameraRig(
        dt: number,
        previousCameraPose?: {
          position: Vector3;
          lookTarget: Vector3;
        }
      ): { entityId: string } | null;
      createInteractionDispatcher(): {
        startNpcDialogue(
          npcEntityId: string,
          dialogueId: string | null,
          source?: {
            kind: "interactionLink" | "npc" | "direct";
            sourceEntityId: string | null;
            linkId: string | null;
            trigger: "enter" | "exit" | "click" | null;
          }
        ): void;
      };
    };
    const dispatcher = hostInternals.createInteractionDispatcher();

    hostInternals.sceneReady = true;
    hostInternals.camera.position.set(0, 2.6, 6);
    hostInternals.camera.lookAt(0, 1.6, 0);
    dispatcher.startNpcDialogue(npc.id, null, {
      kind: "npc",
      sourceEntityId: npc.id,
      linkId: null,
      trigger: "click"
    });
    hostInternals.applyActiveCameraRig(
      0.35,
      captureCameraPose(hostInternals.camera)
    );
    const dialoguePose = captureCameraPose(hostInternals.camera);

    host.closeRuntimeDialogue();
    hostInternals.camera.position.set(0, 2.6, 6);
    hostInternals.camera.lookAt(0, 1.6, 0);

    expect(hostInternals.applyActiveCameraRig(0.175, dialoguePose)).toBeNull();
    expect(hostInternals.currentPauseState).toEqual({
      paused: false,
      source: null
    });
    expect(hostInternals.activeCameraSourceKey).toBe("gameplay");
    expect(hostInternals.cameraTransitionState).not.toBeNull();
    expect(hostInternals.camera.position.z).toBeGreaterThan(
      dialoguePose.position.z
    );
    expect(hostInternals.camera.position.z).toBeLessThan(6);

    hostInternals.camera.position.set(0, 2.6, 6);
    hostInternals.camera.lookAt(0, 1.6, 0);
    hostInternals.applyActiveCameraRig(0.175, dialoguePose);

    expect(hostInternals.camera.position).toMatchObject({
      x: 0,
      y: 2.6,
      z: 6
    });

    host.dispose();
  });

  it("locks a fixed camera rig to its target and clamps authored look-around input", () => {
    vi.spyOn(console, "warn").mockImplementation(() => undefined);
    vi.spyOn(RapierCollisionWorld, "create").mockResolvedValue({
      dispose: vi.fn(),
      resolveThirdPersonCameraCollision: vi.fn(
        (_pivot, desiredCameraPosition) => desiredCameraPosition
      )
    } as unknown as RapierCollisionWorld);

    const playerStart = createPlayerStartEntity({
      id: "entity-player-start-camera-rig",
      position: {
        x: 1,
        y: 0,
        z: -2
      }
    });
    const cameraRig = createCameraRigEntity({
      id: "entity-camera-rig-lookaround",
      position: {
        x: -4,
        y: 3,
        z: -8
      },
      target: createCameraRigPlayerTargetRef(),
      targetOffset: {
        x: 0,
        y: 1.6,
        z: 0
      },
      transitionMode: "cut",
      lookAround: {
        enabled: true,
        yawLimitDegrees: 10,
        pitchLimitDegrees: 5,
        recenterSpeed: 12
      }
    });
    const runtimeScene = buildRuntimeSceneFromDocument({
      ...createEmptySceneDocument({ name: "Camera Rig Look Scene" }),
      entities: {
        [playerStart.id]: playerStart,
        [cameraRig.id]: cameraRig
      }
    });
    const host = new RuntimeHost({
      enableRendering: false
    });
    host.loadScene(runtimeScene);

    const hostInternals = host as unknown as {
      sceneReady: boolean;
      camera: PerspectiveCamera;
      applyActiveCameraRig(dt: number): { entityId: string } | null;
      handleRuntimePointerDown(event: {
        button: number;
        clientX: number;
        clientY: number;
        preventDefault(): void;
        stopImmediatePropagation(): void;
      }): void;
      handleRuntimePointerMove(event: {
        clientX: number;
        clientY: number;
        preventDefault(): void;
        stopImmediatePropagation(): void;
      }): void;
      handleRuntimePointerUp(event: { stopImmediatePropagation(): void }): void;
    };

    hostInternals.sceneReady = true;

    expect(hostInternals.applyActiveCameraRig(0)?.entityId).toBe(cameraRig.id);

    const expectedBaseDirection = new Vector3(
      playerStart.position.x - cameraRig.position.x,
      playerStart.position.y + 1.6 - cameraRig.position.y,
      playerStart.position.z - cameraRig.position.z
    ).normalize();
    const baseDirection = hostInternals.camera.getWorldDirection(new Vector3());

    expect(baseDirection.angleTo(expectedBaseDirection)).toBeLessThan(1e-4);

    hostInternals.handleRuntimePointerDown({
      button: 0,
      clientX: 0,
      clientY: 0,
      preventDefault: vi.fn(),
      stopImmediatePropagation: vi.fn()
    });
    hostInternals.handleRuntimePointerMove({
      clientX: -10000,
      clientY: 10000,
      preventDefault: vi.fn(),
      stopImmediatePropagation: vi.fn()
    });
    hostInternals.applyActiveCameraRig(0);

    const lookedDirection = hostInternals.camera.getWorldDirection(
      new Vector3()
    );
    const baseAngles = resolveYawPitchRadians(expectedBaseDirection);
    const lookedAngles = resolveYawPitchRadians(lookedDirection);

    expect(
      ((lookedAngles.yawRadians - baseAngles.yawRadians) * 180) / Math.PI
    ).toBeCloseTo(10, 1);
    expect(
      ((lookedAngles.pitchRadians - baseAngles.pitchRadians) * 180) / Math.PI
    ).toBeCloseTo(-5, 1);

    hostInternals.handleRuntimePointerUp({
      stopImmediatePropagation: vi.fn()
    });
    hostInternals.applyActiveCameraRig(0.5);

    const recenteredAngles = resolveYawPitchRadians(
      hostInternals.camera.getWorldDirection(new Vector3())
    );

    expect(
      Math.abs(recenteredAngles.yawRadians - baseAngles.yawRadians)
    ).toBeLessThan(Math.abs(lookedAngles.yawRadians - baseAngles.yawRadians));
    expect(
      Math.abs(recenteredAngles.pitchRadians - baseAngles.pitchRadians)
    ).toBeLessThan(
      Math.abs(lookedAngles.pitchRadians - baseAngles.pitchRadians)
    );

    host.dispose();
  });

  it("routes camera rig control effects through the runtime override path", () => {
    vi.spyOn(console, "warn").mockImplementation(() => undefined);
    vi.spyOn(RapierCollisionWorld, "create").mockResolvedValue({
      dispose: vi.fn(),
      resolveThirdPersonCameraCollision: vi.fn(
        (_pivot, desiredCameraPosition) => desiredCameraPosition
      )
    } as unknown as RapierCollisionWorld);

    const defaultRig = createCameraRigEntity({
      id: "entity-camera-rig-default-control",
      position: {
        x: 0,
        y: 3,
        z: 6
      },
      defaultActive: true,
      target: createCameraRigWorldPointTargetRef({
        x: 0,
        y: 1.5,
        z: 0
      })
    });
    const overrideRig = createCameraRigEntity({
      id: "entity-camera-rig-override-control",
      position: {
        x: 10,
        y: 5,
        z: -4
      },
      defaultActive: false,
      target: createCameraRigWorldPointTargetRef({
        x: 1,
        y: 2,
        z: 0
      })
    });
    const triggerVolume = createTriggerVolumeEntity({
      id: "entity-trigger-camera-control"
    });
    const runtimeScene = buildRuntimeSceneFromDocument({
      ...createEmptySceneDocument({ name: "Camera Control Runtime Scene" }),
      entities: {
        [defaultRig.id]: defaultRig,
        [overrideRig.id]: overrideRig,
        [triggerVolume.id]: triggerVolume
      }
    });
    const host = new RuntimeHost({
      enableRendering: false
    });
    host.loadScene(runtimeScene);

    const activateEffect = createActivateCameraRigOverrideControlEffect({
      target: createCameraRigControlTargetRef(overrideRig.id)
    });
    const clearEffect = createClearCameraRigOverrideControlEffect({
      target: createCameraRigControlTargetRef(overrideRig.id)
    });
    const activateLink = createControlInteractionLink({
      id: "link-camera-activate",
      sourceEntityId: triggerVolume.id,
      effect: activateEffect
    });
    const clearLink = createControlInteractionLink({
      id: "link-camera-clear",
      sourceEntityId: triggerVolume.id,
      effect: clearEffect
    });
    const hostInternals = host as unknown as {
      sceneReady: boolean;
      activeCameraRigOverrideEntityId: string | null;
      activeRuntimeCameraRig: { entityId: string } | null;
      applyActiveCameraRig(dt: number): { entityId: string } | null;
      createInteractionDispatcher(): {
        dispatchControlEffect(
          effect: ControlEffect,
          link: InteractionLink
        ): void;
      };
    };
    const dispatcher = hostInternals.createInteractionDispatcher();

    hostInternals.sceneReady = true;
    expect(hostInternals.applyActiveCameraRig(0)?.entityId).toBe(defaultRig.id);

    dispatcher.dispatchControlEffect(activateEffect, activateLink);

    expect(hostInternals.activeCameraRigOverrideEntityId).toBe(overrideRig.id);
    expect(hostInternals.applyActiveCameraRig(0)?.entityId).toBe(
      overrideRig.id
    );
    expect(runtimeScene.control.resolved.discrete).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          type: "cameraRigOverride",
          entityId: overrideRig.id,
          source: {
            kind: "interactionLink",
            linkId: activateLink.id
          }
        })
      ])
    );

    dispatcher.dispatchControlEffect(clearEffect, clearLink);

    expect(hostInternals.activeCameraRigOverrideEntityId).toBeNull();
    expect(hostInternals.applyActiveCameraRig(0)?.entityId).toBe(defaultRig.id);
    expect(runtimeScene.control.resolved.discrete).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          type: "cameraRigOverride",
          entityId: null,
          source: {
            kind: "interactionLink",
            linkId: clearLink.id
          }
        })
      ])
    );

    host.dispose();
  });

  it("resolves rail camera rigs from the target's nearest path progress and preserves look-around", () => {
    vi.spyOn(console, "warn").mockImplementation(() => undefined);
    vi.spyOn(RapierCollisionWorld, "create").mockResolvedValue({
      dispose: vi.fn(),
      resolveThirdPersonCameraCollision: vi.fn(
        (_pivot, desiredCameraPosition) => desiredCameraPosition
      )
    } as unknown as RapierCollisionWorld);

    const target = createInteractableEntity({
      id: "entity-camera-rail-target",
      position: {
        x: 3,
        y: 1,
        z: 2
      },
      prompt: "Anchor"
    });
    const path = createScenePath({
      id: "path-camera-rail-runtime",
      points: [
        {
          id: "point-a",
          position: {
            x: 0,
            y: 3,
            z: 0
          }
        },
        {
          id: "point-b",
          position: {
            x: 10,
            y: 3,
            z: 0
          }
        }
      ]
    });
    const cameraRig = createCameraRigEntity({
      id: "entity-camera-rig-rail-runtime",
      rigType: "rail",
      pathId: path.id,
      target: createCameraRigEntityTargetRef(target.id),
      targetOffset: {
        x: 0,
        y: 1.5,
        z: 0
      },
      transitionMode: "cut",
      lookAround: {
        enabled: true,
        yawLimitDegrees: 12,
        pitchLimitDegrees: 6,
        recenterSpeed: 10
      }
    });
    const runtimeScene = buildRuntimeSceneFromDocument({
      ...createEmptySceneDocument({ name: "Rail Camera Rig Runtime Scene" }),
      paths: {
        [path.id]: path
      },
      entities: {
        [target.id]: target,
        [cameraRig.id]: cameraRig
      }
    });
    const host = new RuntimeHost({
      enableRendering: false
    });
    host.loadScene(runtimeScene);

    const hostInternals = host as unknown as {
      sceneReady: boolean;
      camera: PerspectiveCamera;
      runtimeScene: typeof runtimeScene;
      applyActiveCameraRig(dt: number): { entityId: string } | null;
      handleRuntimePointerDown(event: {
        button: number;
        clientX: number;
        clientY: number;
        preventDefault(): void;
        stopImmediatePropagation(): void;
      }): void;
      handleRuntimePointerMove(event: {
        clientX: number;
        clientY: number;
        preventDefault(): void;
        stopImmediatePropagation(): void;
      }): void;
      handleRuntimePointerUp(event: { stopImmediatePropagation(): void }): void;
    };

    hostInternals.sceneReady = true;

    expect(hostInternals.applyActiveCameraRig(0)?.entityId).toBe(cameraRig.id);
    expect(hostInternals.camera.position).toMatchObject({
      x: 3,
      y: 3,
      z: 0
    });

    const initialDirection = hostInternals.camera.getWorldDirection(
      new Vector3()
    );

    hostInternals.handleRuntimePointerDown({
      button: 0,
      clientX: 0,
      clientY: 0,
      preventDefault: vi.fn(),
      stopImmediatePropagation: vi.fn()
    });
    hostInternals.handleRuntimePointerMove({
      clientX: -1000,
      clientY: 0,
      preventDefault: vi.fn(),
      stopImmediatePropagation: vi.fn()
    });
    hostInternals.applyActiveCameraRig(0);

    expect(hostInternals.camera.position).toMatchObject({
      x: 3,
      y: 3,
      z: 0
    });
    expect(
      hostInternals.camera
        .getWorldDirection(new Vector3())
        .angleTo(initialDirection)
    ).toBeGreaterThan(0.05);

    hostInternals.handleRuntimePointerUp({
      stopImmediatePropagation: vi.fn()
    });
    hostInternals.runtimeScene.entities.interactables[0]!.position = {
      x: 8,
      y: 1,
      z: 2
    };
    hostInternals.applyActiveCameraRig(0);

    expect(hostInternals.camera.position).toMatchObject({
      x: 8,
      y: 3,
      z: 0
    });

    host.dispose();
  });

  it("maps rail camera rig progress between authored world points", () => {
    vi.spyOn(console, "warn").mockImplementation(() => undefined);
    vi.spyOn(RapierCollisionWorld, "create").mockResolvedValue({
      dispose: vi.fn(),
      resolveThirdPersonCameraCollision: vi.fn(
        (_pivot, desiredCameraPosition) => desiredCameraPosition
      )
    } as unknown as RapierCollisionWorld);

    const target = createInteractableEntity({
      id: "entity-camera-mapped-rail-target",
      position: {
        x: 2,
        y: 1,
        z: 2
      },
      prompt: "Anchor"
    });
    const path = createScenePath({
      id: "path-camera-mapped-rail-runtime",
      points: [
        {
          id: "point-a",
          position: {
            x: 0,
            y: 3,
            z: 0
          }
        },
        {
          id: "point-b",
          position: {
            x: 10,
            y: 3,
            z: 0
          }
        }
      ]
    });
    const cameraRig = createCameraRigEntity({
      id: "entity-camera-rig-mapped-rail-runtime",
      rigType: "rail",
      pathId: path.id,
      railPlacementMode: "mapTargetBetweenPoints",
      trackStartPoint: {
        x: 0,
        y: 1,
        z: 2
      },
      trackEndPoint: {
        x: 10,
        y: 1,
        z: 2
      },
      railStartProgress: 0.25,
      railEndProgress: 0.75,
      target: createCameraRigEntityTargetRef(target.id),
      transitionMode: "cut"
    });
    const runtimeScene = buildRuntimeSceneFromDocument({
      ...createEmptySceneDocument({
        name: "Mapped Rail Camera Rig Runtime Scene"
      }),
      paths: {
        [path.id]: path
      },
      entities: {
        [target.id]: target,
        [cameraRig.id]: cameraRig
      }
    });
    const host = new RuntimeHost({
      enableRendering: false
    });
    host.loadScene(runtimeScene);

    const hostInternals = host as unknown as {
      sceneReady: boolean;
      camera: PerspectiveCamera;
      runtimeScene: typeof runtimeScene;
      applyActiveCameraRig(dt: number): { entityId: string } | null;
    };

    hostInternals.sceneReady = true;

    expect(hostInternals.applyActiveCameraRig(0)?.entityId).toBe(cameraRig.id);
    expect(hostInternals.camera.position).toMatchObject({
      x: 3.5,
      y: 3,
      z: 0
    });

    hostInternals.runtimeScene.entities.interactables[0]!.position = {
      x: 10,
      y: 1,
      z: 2
    };
    hostInternals.applyActiveCameraRig(0);

    expect(hostInternals.camera.position).toMatchObject({
      x: 7.5,
      y: 3,
      z: 0
    });

    host.dispose();
  });

  it("applies typed light control effects through the runtime dispatcher", () => {
    vi.spyOn(console, "warn").mockImplementation(() => undefined);
    vi.spyOn(RapierCollisionWorld, "create").mockResolvedValue({
      dispose: vi.fn(),
      resolveThirdPersonCameraCollision: vi.fn(
        (_pivot, desiredCameraPosition) => desiredCameraPosition
      )
    } as unknown as RapierCollisionWorld);

    const pointLight = createPointLightEntity({
      id: "entity-point-light-main",
      intensity: 1.25
    });
    const runtimeScene = buildRuntimeSceneFromDocument({
      ...createEmptySceneDocument(),
      entities: {
        [pointLight.id]: pointLight
      }
    });
    const host = new RuntimeHost({
      enableRendering: false
    });
    host.loadScene(runtimeScene);

    const disableEffect = createSetLightEnabledControlEffect({
      target: createLightControlTargetRef("pointLight", pointLight.id),
      enabled: false
    });
    const intensityEffect = createSetLightIntensityControlEffect({
      target: createLightControlTargetRef("pointLight", pointLight.id),
      intensity: 3.5
    });
    const disableLink = createControlInteractionLink({
      id: "link-light-disable",
      sourceEntityId: "entity-trigger-main",
      effect: disableEffect
    });
    const intensityLink = createControlInteractionLink({
      id: "link-light-intensity",
      sourceEntityId: "entity-trigger-main",
      effect: intensityEffect
    });
    const hostInternals = host as unknown as {
      createInteractionDispatcher(): {
        dispatchControlEffect(
          effect: ControlEffect,
          link: InteractionLink
        ): void;
      };
      localLightObjects: Map<
        string,
        {
          group: { visible: boolean };
          light: { intensity: number };
        }
      >;
    };
    const dispatcher = hostInternals.createInteractionDispatcher();
    const renderObjects = hostInternals.localLightObjects.get(pointLight.id);

    expect(renderObjects).toBeDefined();
    expect(renderObjects?.group.visible).toBe(true);
    expect(renderObjects?.light.intensity).toBe(1.25);

    dispatcher.dispatchControlEffect(disableEffect, disableLink);
    dispatcher.dispatchControlEffect(intensityEffect, intensityLink);

    expect(renderObjects?.group.visible).toBe(false);
    expect(renderObjects?.light.intensity).toBe(3.5);
    expect(runtimeScene.control.resolved.discrete).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          type: "lightEnabled",
          value: false,
          source: {
            kind: "interactionLink",
            linkId: disableLink.id
          }
        })
      ])
    );
    expect(runtimeScene.control.resolved.channels).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          type: "lightIntensity",
          value: 3.5,
          source: {
            kind: "interactionLink",
            linkId: intensityLink.id
          }
        })
      ])
    );

    host.dispose();
  });

  it("creates derived runtime point lights for authored light volumes", () => {
    vi.spyOn(console, "warn").mockImplementation(() => undefined);
    vi.spyOn(RapierCollisionWorld, "create").mockResolvedValue({
      dispose: vi.fn(),
      resolveThirdPersonCameraCollision: vi.fn(
        (_pivot, desiredCameraPosition) => desiredCameraPosition
      )
    } as unknown as RapierCollisionWorld);

    const lightBrush = createBoxBrush({
      id: "brush-runtime-light-volume",
      size: {
        x: 6,
        y: 5,
        z: 3
      },
      volume: {
        mode: "light",
        light: {
          colorHex: "#ffe0b6",
          intensity: 2,
          padding: 0.4,
          falloff: "smoothstep"
        }
      }
    });
    const runtimeScene = buildRuntimeSceneFromDocument({
      ...createEmptySceneDocument(),
      brushes: {
        [lightBrush.id]: lightBrush
      }
    });
    const host = new RuntimeHost({
      enableRendering: false
    });

    host.loadScene(runtimeScene);

    const hostInternals = host as unknown as {
      lightVolumeObjects: Map<
        string,
        {
          group: { visible: boolean };
          lights: Array<{
            intensity: number;
            distance: number;
            castShadow: boolean;
          }>;
        }
      >;
    };
    const renderObjects = hostInternals.lightVolumeObjects.get(lightBrush.id);

    expect(runtimeScene.volumes.light).toHaveLength(1);
    expect(runtimeScene.volumes.light[0]?.lights).toHaveLength(4);
    expect(renderObjects).toBeDefined();
    expect(renderObjects?.group.visible).toBe(true);
    expect(renderObjects?.lights).toHaveLength(4);
    expect(
      renderObjects?.lights.every(
        (light) =>
          light.intensity > 0 &&
          light.distance > 0 &&
          light.castShadow === false
      )
    ).toBe(true);

    host.dispose();
  });

  it("applies project time pause control effects through the runtime dispatcher", () => {
    vi.spyOn(console, "warn").mockImplementation(() => undefined);
    vi.spyOn(RapierCollisionWorld, "create").mockResolvedValue({
      dispose: vi.fn(),
      resolveThirdPersonCameraCollision: vi.fn(
        (_pivot, desiredCameraPosition) => desiredCameraPosition
      )
    } as unknown as RapierCollisionWorld);

    const runtimeScene = buildRuntimeSceneFromDocument(
      createEmptySceneDocument()
    );
    const host = new RuntimeHost({
      enableRendering: false
    });
    const pauseStates: RuntimePauseState[] = [];
    host.setRuntimePauseStateHandler((state) => {
      pauseStates.push(state);
    });
    host.loadScene(runtimeScene);

    const pauseEffect = createSetProjectTimePausedControlEffect({
      target: createProjectGlobalControlTargetRef(),
      paused: true
    });
    const resumeEffect = createSetProjectTimePausedControlEffect({
      target: createProjectGlobalControlTargetRef(),
      paused: false
    });
    const pauseLink = createControlInteractionLink({
      id: "link-pause-time",
      sourceEntityId: "entity-trigger-main",
      effect: pauseEffect
    });
    const resumeLink = createControlInteractionLink({
      id: "link-resume-time",
      sourceEntityId: "entity-trigger-main",
      effect: resumeEffect
    });
    const hostInternals = host as unknown as {
      createInteractionDispatcher(): {
        dispatchControlEffect(
          effect: ControlEffect,
          link: InteractionLink
        ): void;
      };
    };
    const dispatcher = hostInternals.createInteractionDispatcher();

    dispatcher.dispatchControlEffect(pauseEffect, pauseLink);

    expect(pauseStates).toContainEqual({
      paused: true,
      source: "control"
    });
    expect(runtimeScene.control.resolved.discrete).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          type: "projectTimePaused",
          target: {
            kind: "global",
            scope: "project"
          },
          value: true,
          source: {
            kind: "interactionLink",
            linkId: pauseLink.id
          }
        })
      ])
    );

    dispatcher.dispatchControlEffect(resumeEffect, resumeLink);

    expect(pauseStates).toContainEqual({
      paused: false,
      source: null
    });

    host.dispose();
  });

  it("opens, advances, and closes NPC dialogues through the runtime host", () => {
    vi.spyOn(console, "warn").mockImplementation(() => undefined);
    vi.spyOn(RapierCollisionWorld, "create").mockResolvedValue({
      dispose: vi.fn(),
      resolveThirdPersonCameraCollision: vi.fn(
        (_pivot, desiredCameraPosition) => desiredCameraPosition
      )
    } as unknown as RapierCollisionWorld);

    const document = createEmptySceneDocument();
    const npc = createNpcEntity({
      id: "entity-npc-operator",
      dialogues: [
        {
          id: "dialogue-warning",
          title: "Generator Warning",
          lines: [
            {
              id: "dialogue-line-warning-1",
              text: "The generator is unstable."
            },
            {
              id: "dialogue-line-warning-2",
              text: "A low hum fills the room."
            }
          ]
        }
      ],
      defaultDialogueId: "dialogue-warning"
    });
    document.entities[npc.id] = npc;

    const runtimeScene = buildRuntimeSceneFromDocument(document);
    const host = new RuntimeHost({
      enableRendering: false
    });
    const dialogueStates: Array<RuntimeDialogueState | null> = [];
    host.setRuntimeDialogueHandler((dialogue) => {
      dialogueStates.push(dialogue);
    });
    host.loadScene(runtimeScene);

    const hostInternals = host as unknown as {
      createInteractionDispatcher(): {
        startNpcDialogue(
          npcEntityId: string,
          dialogueId: string | null,
          source?: {
            kind: "interactionLink" | "npc" | "direct";
            sourceEntityId: string | null;
            linkId: string | null;
            trigger: "enter" | "exit" | "click" | null;
          }
        ): void;
      };
    };
    const dispatcher = hostInternals.createInteractionDispatcher();

    dispatcher.startNpcDialogue(npc.id, "dialogue-warning", {
      kind: "npc",
      sourceEntityId: npc.id,
      linkId: null,
      trigger: "click"
    });
    host.advanceRuntimeDialogue();
    host.advanceRuntimeDialogue();

    expect(dialogueStates).toEqual([
      expect.objectContaining({
        dialogueId: "dialogue-warning",
        npcEntityId: npc.id,
        lineIndex: 0,
        speakerName: npc.actorId,
        text: "The generator is unstable.",
        source: {
          kind: "npc",
          sourceEntityId: npc.id,
          linkId: null,
          trigger: "click"
        }
      }),
      expect.objectContaining({
        dialogueId: "dialogue-warning",
        npcEntityId: npc.id,
        lineIndex: 1,
        speakerName: npc.actorId,
        text: "A low hum fills the room.",
        source: {
          kind: "npc",
          sourceEntityId: npc.id,
          linkId: null,
          trigger: "click"
        }
      }),
      null
    ]);

    host.dispose();
  });

  it("keeps dialogue pause active for runtime progression while camera blends and dialogue advance still work", () => {
    vi.spyOn(console, "warn").mockImplementation(() => undefined);
    vi.spyOn(RapierCollisionWorld, "create").mockResolvedValue({
      dispose: vi.fn(),
      resolveThirdPersonCameraCollision: vi.fn(
        (_pivot, desiredCameraPosition) => desiredCameraPosition
      )
    } as unknown as RapierCollisionWorld);
    vi.spyOn(window, "requestAnimationFrame").mockReturnValue(1);
    vi.spyOn(performance, "now").mockReturnValue(1100);

    const playerStart = createPlayerStartEntity({
      id: "entity-player-start-dialogue-pause",
      position: {
        x: 0,
        y: 0,
        z: 0
      }
    });
    const npc = createNpcEntity({
      id: "entity-npc-dialogue-pause",
      position: {
        x: 2,
        y: 0,
        z: 2
      },
      dialogues: [
        {
          id: "dialogue-pause",
          title: "Pause",
          lines: [
            {
              id: "dialogue-pause-line-1",
              text: "Time should stop."
            },
            {
              id: "dialogue-pause-line-2",
              text: "But dialogue should keep going."
            }
          ]
        }
      ],
      defaultDialogueId: "dialogue-pause"
    });
    const runtimeScene = buildRuntimeSceneFromDocument({
      ...createEmptySceneDocument({ name: "Dialogue Pause Scene" }),
      entities: {
        [playerStart.id]: playerStart,
        [npc.id]: npc
      }
    });
    const host = new RuntimeHost({
      enableRendering: false
    });
    host.loadScene(runtimeScene);

    const hostInternals = host as unknown as {
      sceneReady: boolean;
      camera: PerspectiveCamera;
      currentClockState: {
        timeOfDayHours: number;
        dayCount: number;
        dayLengthMinutes: number;
      } | null;
      currentDialogue: RuntimeDialogueState | null;
      currentPauseState: RuntimePauseState;
      activeCameraSourceKey: string | null;
      cameraTransitionState: { elapsedSeconds: number } | null;
      previousFrameTime: number;
      applyActiveCameraRig(
        dt: number,
        previousCameraPose?: {
          position: Vector3;
          lookTarget: Vector3;
        }
      ): { entityId: string } | null;
      render(): void;
      createInteractionDispatcher(): {
        startNpcDialogue(
          npcEntityId: string,
          dialogueId: string | null,
          source?: {
            kind: "interactionLink" | "npc" | "direct";
            sourceEntityId: string | null;
            linkId: string | null;
            trigger: "enter" | "exit" | "click" | null;
          }
        ): void;
      };
    };
    const dispatcher = hostInternals.createInteractionDispatcher();

    hostInternals.sceneReady = true;
    hostInternals.previousFrameTime = 1000;
    hostInternals.camera.position.set(0, 2.6, 6);
    hostInternals.camera.lookAt(0, 1.6, 0);
    hostInternals.applyActiveCameraRig(
      0,
      captureCameraPose(hostInternals.camera)
    );
    const clockBefore = {
      ...hostInternals.currentClockState!
    };

    dispatcher.startNpcDialogue(npc.id, null, {
      kind: "npc",
      sourceEntityId: npc.id,
      linkId: null,
      trigger: "click"
    });
    hostInternals.render();

    expect(hostInternals.currentPauseState).toEqual({
      paused: true,
      source: "dialogue"
    });
    expect(hostInternals.activeCameraSourceKey).toBe(`dialogue:${npc.id}`);
    expect(hostInternals.cameraTransitionState?.elapsedSeconds).toBeGreaterThan(
      0
    );
    expect(hostInternals.currentClockState).toEqual(clockBefore);

    host.advanceRuntimeDialogue();

    expect(hostInternals.currentDialogue?.lineIndex).toBe(1);
    expect(hostInternals.currentDialogue?.text).toBe(
      "But dialogue should keep going."
    );

    host.dispose();
  });

  it("publishes late dialogue handlers, ignores repeated same-NPC dialogue starts, and replaces with a different NPC dialogue", () => {
    vi.spyOn(console, "warn").mockImplementation(() => undefined);
    vi.spyOn(RapierCollisionWorld, "create").mockResolvedValue({
      dispose: vi.fn(),
      resolveThirdPersonCameraCollision: vi.fn(
        (_pivot, desiredCameraPosition) => desiredCameraPosition
      )
    } as unknown as RapierCollisionWorld);

    const document = createEmptySceneDocument();
    const npcA = createNpcEntity({
      id: "entity-npc-a",
      dialogues: [
        {
          id: "dialogue-a",
          title: "A",
          lines: [
            {
              id: "dialogue-line-a-1",
              text: "First dialogue."
            }
          ]
        }
      ],
      defaultDialogueId: "dialogue-a"
    });
    const npcB = createNpcEntity({
      id: "entity-npc-b",
      dialogues: [
        {
          id: "dialogue-b",
          title: "B",
          lines: [
            {
              id: "dialogue-line-b-1",
              text: "Second dialogue."
            }
          ]
        }
      ],
      defaultDialogueId: "dialogue-b"
    });
    document.entities[npcA.id] = npcA;
    document.entities[npcB.id] = npcB;

    const host = new RuntimeHost({
      enableRendering: false
    });
    host.loadScene(buildRuntimeSceneFromDocument(document));

    const hostInternals = host as unknown as {
      createInteractionDispatcher(): {
        startNpcDialogue(
          npcEntityId: string,
          dialogueId: string | null,
          source?: {
            kind: "interactionLink" | "npc" | "direct";
            sourceEntityId: string | null;
            linkId: string | null;
            trigger: "enter" | "exit" | "click" | null;
          }
        ): void;
      };
    };
    const dispatcher = hostInternals.createInteractionDispatcher();

    dispatcher.startNpcDialogue(npcA.id, "dialogue-a", {
      kind: "npc",
      sourceEntityId: npcA.id,
      linkId: null,
      trigger: "click"
    });

    const dialogueStates: Array<RuntimeDialogueState | null> = [];
    host.setRuntimeDialogueHandler((dialogue) => {
      dialogueStates.push(dialogue);
    });

    dispatcher.startNpcDialogue(npcA.id, "dialogue-a", {
      kind: "npc",
      sourceEntityId: npcA.id,
      linkId: null,
      trigger: "click"
    });
    dispatcher.startNpcDialogue(npcB.id, "dialogue-b", {
      kind: "npc",
      sourceEntityId: npcB.id,
      linkId: null,
      trigger: "click"
    });

    expect(dialogueStates).toEqual([
      expect.objectContaining({
        dialogueId: "dialogue-a",
        npcEntityId: npcA.id,
        text: "First dialogue.",
        source: {
          kind: "npc",
          sourceEntityId: npcA.id,
          linkId: null,
          trigger: "click"
        }
      }),
      expect.objectContaining({
        dialogueId: "dialogue-b",
        npcEntityId: npcB.id,
        text: "Second dialogue.",
        source: {
          kind: "npc",
          sourceEntityId: npcB.id,
          linkId: null,
          trigger: "click"
        }
      })
    ]);

    host.dispose();
  });

  it("applies expanded typed control effects for model, sound, and scene lighting", () => {
    vi.spyOn(console, "warn").mockImplementation(() => undefined);
    vi.spyOn(RapierCollisionWorld, "create").mockResolvedValue({
      dispose: vi.fn(),
      resolveThirdPersonCameraCollision: vi.fn(
        (_pivot, desiredCameraPosition) => desiredCameraPosition
      )
    } as unknown as RapierCollisionWorld);

    const triggerVolume = createTriggerVolumeEntity({
      id: "entity-trigger-main"
    });
    const pointLight = createPointLightEntity({
      id: "entity-point-light-main",
      colorHex: "#ff8800",
      intensity: 1.25
    });
    const soundEmitter = createSoundEmitterEntity({
      id: "entity-sound-main",
      audioAssetId: "asset-audio-main",
      volume: 0.8
    });
    const modelAsset = {
      id: "asset-model-animated",
      kind: "model" as const,
      sourceName: "animated.glb",
      mimeType: "model/gltf-binary",
      storageKey: createProjectAssetStorageKey("asset-model-animated"),
      byteLength: 1024,
      metadata: {
        kind: "model" as const,
        format: "glb" as const,
        sceneName: null,
        nodeCount: 1,
        meshCount: 1,
        materialNames: [],
        textureNames: [],
        animationNames: ["Idle"],
        boundingBox: null,
        warnings: []
      }
    } satisfies ModelAssetRecord;
    const audioAsset = {
      id: "asset-audio-main",
      kind: "audio" as const,
      sourceName: "loop.ogg",
      mimeType: "audio/ogg",
      storageKey: createProjectAssetStorageKey("asset-audio-main"),
      byteLength: 512,
      metadata: {
        kind: "audio" as const,
        durationSeconds: 2,
        channelCount: 2,
        sampleRateHz: 48000,
        warnings: []
      }
    } satisfies AudioAssetRecord;
    const modelInstance = createModelInstance({
      id: "model-instance-animated",
      assetId: modelAsset.id
    });
    const runtimeScene = buildRuntimeSceneFromDocument({
      ...createEmptySceneDocument(),
      assets: {
        [modelAsset.id]: modelAsset,
        [audioAsset.id]: audioAsset
      },
      modelInstances: {
        [modelInstance.id]: modelInstance
      },
      entities: {
        [triggerVolume.id]: triggerVolume,
        [pointLight.id]: pointLight,
        [soundEmitter.id]: soundEmitter
      }
    });
    const host = new RuntimeHost({
      enableRendering: false
    });
    host.loadScene(runtimeScene);

    const hostInternals = host as unknown as {
      createInteractionDispatcher(): {
        dispatchControlEffect(
          effect: ControlEffect,
          link: InteractionLink
        ): void;
      };
      localLightObjects: Map<
        string,
        {
          group: { visible: boolean };
          light: { intensity: number; color: { getHexString(): string } };
        }
      >;
      modelRenderObjects: Map<string, { visible: boolean }>;
      ambientLight: {
        intensity: number;
        color: { getHexString(): string };
      };
      sunLight: {
        intensity: number;
        color: { getHexString(): string };
      };
      audioSystem: {
        hasSoundEmitter(soundEmitterId: string): boolean;
        playSound(soundEmitterId: string, link: InteractionLink | null): void;
        stopSound(soundEmitterId: string): void;
        setSoundEmitterVolume(soundEmitterId: string, volume: number): void;
      };
      animationMixers: Map<string, AnimationMixer>;
      applyPlayAnimationAction(
        instanceId: string,
        clipName: string,
        loop: boolean | undefined
      ): void;
      applyStopAnimationAction(instanceId: string): void;
    };

    const dispatcher = hostInternals.createInteractionDispatcher();
    const lightRenderObjects = hostInternals.localLightObjects.get(
      pointLight.id
    );
    const modelRenderGroup = hostInternals.modelRenderObjects.get(
      modelInstance.id
    );
    const initialAmbientIntensity = hostInternals.ambientLight.intensity;
    const initialAmbientColor = hostInternals.ambientLight.color.getHexString();
    const initialSunIntensity = hostInternals.sunLight.intensity;
    const initialSunColor = hostInternals.sunLight.color.getHexString();

    const hasSoundEmitterSpy = vi
      .spyOn(hostInternals.audioSystem, "hasSoundEmitter")
      .mockReturnValue(true);
    const playSoundSpy = vi
      .spyOn(hostInternals.audioSystem, "playSound")
      .mockImplementation(() => undefined);
    const stopSoundSpy = vi
      .spyOn(hostInternals.audioSystem, "stopSound")
      .mockImplementation(() => undefined);
    const setSoundEmitterVolumeSpy = vi
      .spyOn(hostInternals.audioSystem, "setSoundEmitterVolume")
      .mockImplementation(() => undefined);
    hostInternals.animationMixers.set(modelInstance.id, {
      stopAllAction: vi.fn()
    } as unknown as AnimationMixer);
    const playAnimationSpy = vi
      .spyOn(hostInternals, "applyPlayAnimationAction")
      .mockImplementation(() => undefined);
    const stopAnimationSpy = vi
      .spyOn(hostInternals, "applyStopAnimationAction")
      .mockImplementation(() => undefined);

    const hideModelEffect = createSetModelInstanceVisibleControlEffect({
      target: createModelInstanceControlTargetRef(modelInstance.id),
      visible: false
    });
    const playAnimationEffect = createPlayModelAnimationControlEffect({
      target: createModelInstanceControlTargetRef(modelInstance.id),
      clipName: "Idle",
      loop: false
    });
    const stopAnimationEffect = createStopModelAnimationControlEffect({
      target: createModelInstanceControlTargetRef(modelInstance.id)
    });
    const playSoundEffect = createPlaySoundControlEffect({
      target: createSoundEmitterControlTargetRef(soundEmitter.id)
    });
    const stopSoundEffect = createStopSoundControlEffect({
      target: createSoundEmitterControlTargetRef(soundEmitter.id)
    });
    const setSoundVolumeEffect = createSetSoundVolumeControlEffect({
      target: createSoundEmitterControlTargetRef(soundEmitter.id),
      volume: 0.2
    });
    const lightColorEffect = createSetLightColorControlEffect({
      target: createLightControlTargetRef("pointLight", pointLight.id),
      colorHex: "#00ffaa"
    });
    const ambientIntensityEffect = createSetAmbientLightIntensityControlEffect({
      target: createActiveSceneControlTargetRef(),
      intensity: 0.6
    });
    const ambientColorEffect = createSetAmbientLightColorControlEffect({
      target: createActiveSceneControlTargetRef(),
      colorHex: "#112233"
    });
    const sunIntensityEffect = createSetSunLightIntensityControlEffect({
      target: createActiveSceneControlTargetRef(),
      intensity: 0.75
    });
    const sunColorEffect = createSetSunLightColorControlEffect({
      target: createActiveSceneControlTargetRef(),
      colorHex: "#ffeeaa"
    });

    const links = {
      hideModel: createControlInteractionLink({
        id: "link-hide-model",
        sourceEntityId: triggerVolume.id,
        effect: hideModelEffect
      }),
      playAnimation: createControlInteractionLink({
        id: "link-play-animation",
        sourceEntityId: triggerVolume.id,
        effect: playAnimationEffect
      }),
      stopAnimation: createControlInteractionLink({
        id: "link-stop-animation",
        sourceEntityId: triggerVolume.id,
        effect: stopAnimationEffect
      }),
      playSound: createControlInteractionLink({
        id: "link-play-sound",
        sourceEntityId: triggerVolume.id,
        effect: playSoundEffect
      }),
      stopSound: createControlInteractionLink({
        id: "link-stop-sound",
        sourceEntityId: triggerVolume.id,
        effect: stopSoundEffect
      }),
      setSoundVolume: createControlInteractionLink({
        id: "link-set-sound-volume",
        sourceEntityId: triggerVolume.id,
        effect: setSoundVolumeEffect
      }),
      lightColor: createControlInteractionLink({
        id: "link-light-color",
        sourceEntityId: triggerVolume.id,
        effect: lightColorEffect
      }),
      ambientIntensity: createControlInteractionLink({
        id: "link-ambient-intensity",
        sourceEntityId: triggerVolume.id,
        effect: ambientIntensityEffect
      }),
      ambientColor: createControlInteractionLink({
        id: "link-ambient-color",
        sourceEntityId: triggerVolume.id,
        effect: ambientColorEffect
      }),
      sunIntensity: createControlInteractionLink({
        id: "link-sun-intensity",
        sourceEntityId: triggerVolume.id,
        effect: sunIntensityEffect
      }),
      sunColor: createControlInteractionLink({
        id: "link-sun-color",
        sourceEntityId: triggerVolume.id,
        effect: sunColorEffect
      })
    };

    dispatcher.dispatchControlEffect(hideModelEffect, links.hideModel);
    dispatcher.dispatchControlEffect(playAnimationEffect, links.playAnimation);
    dispatcher.dispatchControlEffect(stopAnimationEffect, links.stopAnimation);
    dispatcher.dispatchControlEffect(playSoundEffect, links.playSound);
    dispatcher.dispatchControlEffect(stopSoundEffect, links.stopSound);
    dispatcher.dispatchControlEffect(
      setSoundVolumeEffect,
      links.setSoundVolume
    );
    dispatcher.dispatchControlEffect(lightColorEffect, links.lightColor);
    dispatcher.dispatchControlEffect(
      ambientIntensityEffect,
      links.ambientIntensity
    );
    dispatcher.dispatchControlEffect(ambientColorEffect, links.ambientColor);
    dispatcher.dispatchControlEffect(sunIntensityEffect, links.sunIntensity);
    dispatcher.dispatchControlEffect(sunColorEffect, links.sunColor);

    expect(modelRenderGroup?.visible).toBe(false);
    expect(runtimeScene.modelInstances[0]).toEqual(
      expect.objectContaining({
        visible: false,
        animationClipName: undefined,
        animationAutoplay: false
      })
    );
    expect(playAnimationSpy).toHaveBeenCalledWith(
      modelInstance.id,
      "Idle",
      false
    );
    expect(stopAnimationSpy).toHaveBeenCalledWith(modelInstance.id);
    expect(hasSoundEmitterSpy).toHaveBeenCalledWith(soundEmitter.id);
    expect(playSoundSpy).toHaveBeenCalledWith(soundEmitter.id, links.playSound);
    expect(stopSoundSpy).toHaveBeenCalledWith(soundEmitter.id);
    expect(setSoundEmitterVolumeSpy).toHaveBeenCalledWith(soundEmitter.id, 0.2);
    expect(runtimeScene.entities.soundEmitters[0]).toEqual(
      expect.objectContaining({
        autoplay: false,
        volume: 0.2
      })
    );
    expect(lightRenderObjects?.light.color.getHexString()).toBe("00ffaa");
    expect(runtimeScene.localLights.pointLights[0]).toEqual(
      expect.objectContaining({
        colorHex: "#00ffaa"
      })
    );
    expect(hostInternals.ambientLight.intensity).not.toBeCloseTo(
      initialAmbientIntensity
    );
    expect(hostInternals.ambientLight.color.getHexString()).not.toBe(
      initialAmbientColor
    );
    expect(hostInternals.sunLight.intensity).not.toBeCloseTo(
      initialSunIntensity
    );
    expect(hostInternals.sunLight.color.getHexString()).not.toBe(
      initialSunColor
    );
    expect(runtimeScene.world.ambientLight).toEqual(
      expect.objectContaining({
        intensity: 0.6,
        colorHex: "#112233"
      })
    );
    expect(runtimeScene.world.sunLight).toEqual(
      expect.objectContaining({
        intensity: 0.75,
        colorHex: "#ffeeaa"
      })
    );
    expect(runtimeScene.control.resolved.discrete).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          type: "modelVisibility",
          value: false
        }),
        expect.objectContaining({
          type: "modelAnimationPlayback",
          clipName: null
        }),
        expect.objectContaining({
          type: "soundPlayback",
          value: false
        }),
        expect.objectContaining({
          type: "lightColor",
          value: "#00ffaa"
        }),
        expect.objectContaining({
          type: "ambientLightColor",
          value: "#112233"
        }),
        expect.objectContaining({
          type: "sunLightColor",
          value: "#ffeeaa"
        })
      ])
    );
    expect(runtimeScene.control.resolved.channels).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          type: "soundVolume",
          value: 0.2
        }),
        expect.objectContaining({
          type: "ambientLightIntensity",
          value: 0.6
        }),
        expect.objectContaining({
          type: "sunLightIntensity",
          value: 0.75
        })
      ])
    );

    host.dispose();
  });

  it("re-resolves NPC activity from the project scheduler when the runtime clock advances", () => {
    vi.spyOn(console, "warn").mockImplementation(() => undefined);
    vi.spyOn(RapierCollisionWorld, "create").mockResolvedValue({
      dispose: vi.fn(),
      resolveThirdPersonCameraCollision: vi.fn(
        (_pivot, desiredCameraPosition) => desiredCameraPosition
      )
    } as unknown as RapierCollisionWorld);

    const npc = createNpcEntity({
      id: "entity-npc-night-guard",
      actorId: "actor-night-guard"
    });
    const document = createEmptySceneDocument();
    document.entities[npc.id] = npc;
    document.scheduler.routines["routine-night-guard"] =
      createProjectScheduleRoutine({
        id: "routine-night-guard",
        title: "Night Shift",
        target: createActorControlTargetRef(npc.actorId),
        startHour: 20,
        endHour: 4,
        effect: createSetActorPresenceControlEffect({
          target: createActorControlTargetRef(npc.actorId),
          active: true
        })
      });

    const runtimeScene = buildRuntimeSceneFromDocument(document);
    const host = new RuntimeHost({
      enableRendering: false
    });
    host.loadScene(runtimeScene);

    const hostInternals = host as unknown as {
      currentClockState: {
        timeOfDayHours: number;
        dayCount: number;
        dayLengthMinutes: number;
      } | null;
      sceneReady: boolean;
      runtimeScene: typeof runtimeScene | null;
      syncRuntimeScheduleToCurrentClock(): void;
    };

    expect(runtimeScene.entities.npcs).toEqual([
      expect.objectContaining({
        entityId: npc.id,
        activeRoutineTitle: null
      })
    ]);

    hostInternals.sceneReady = true;
    hostInternals.currentClockState = {
      timeOfDayHours: 21,
      dayCount: 0,
      dayLengthMinutes: 24
    };
    hostInternals.syncRuntimeScheduleToCurrentClock();

    expect(hostInternals.runtimeScene?.entities.npcs).toEqual([
      expect.objectContaining({
        entityId: npc.id,
        actorId: npc.actorId,
        activeRoutineTitle: "Night Shift"
      })
    ]);

    hostInternals.currentClockState = {
      timeOfDayHours: 6,
      dayCount: 1,
      dayLengthMinutes: 24
    };
    hostInternals.syncRuntimeScheduleToCurrentClock();

    expect(hostInternals.runtimeScene?.entities.npcs).toEqual([
      expect.objectContaining({
        entityId: npc.id,
        activeRoutineTitle: null
      })
    ]);
    expect(hostInternals.runtimeScene?.npcDefinitions[0]).toEqual(
      expect.objectContaining({
        entityId: npc.id,
        active: true,
        activeRoutineTitle: null
      })
    );

    host.dispose();
  });

  it("re-resolves NPC animation and follow-path pose from the project scheduler", () => {
    vi.spyOn(console, "warn").mockImplementation(() => undefined);
    vi.spyOn(RapierCollisionWorld, "create").mockResolvedValue({
      dispose: vi.fn(),
      resolveThirdPersonCameraCollision: vi.fn(
        (_pivot, desiredCameraPosition) => desiredCameraPosition
      )
    } as unknown as RapierCollisionWorld);

    const actorTarget = createActorControlTargetRef("actor-patroller");
    const { asset, loadedAsset } = createFixtureLoadedModelAssetFromGeometry(
      "asset-npc-patroller",
      new BoxGeometry(0.8, 1.8, 0.6)
    );
    asset.metadata.animationNames = ["Walk"];
    loadedAsset.animations = [new AnimationClip("Walk", 1, [])];
    const npc = createNpcEntity({
      id: "entity-npc-patroller",
      actorId: actorTarget.actorId,
      modelAssetId: asset.id,
      yawDegrees: 15
    });
    const path = createScenePath({
      id: "path-patrol",
      points: [
        {
          id: "path-point-start",
          position: {
            x: 0,
            y: 0,
            z: 0
          }
        },
        {
          id: "path-point-end",
          position: {
            x: 8,
            y: 0,
            z: 0
          }
        }
      ]
    });
    const document = createEmptySceneDocument();
    document.assets[asset.id] = asset;
    document.entities[npc.id] = npc;
    document.paths[path.id] = path;
    document.scheduler.routines["routine-patrol"] =
      createProjectScheduleRoutine({
        id: "routine-patrol",
        title: "Patrolling",
        target: actorTarget,
        startHour: 9,
        endHour: 13,
        effects: [
          createSetActorPresenceControlEffect({
            target: actorTarget,
            active: true
          }),
          createPlayActorAnimationControlEffect({
            target: actorTarget,
            clipName: "Walk",
            loop: true
          }),
          createFollowActorPathControlEffect({
            target: actorTarget,
            pathId: path.id,
            speed: 2,
            loop: false,
            progressMode: "deriveFromTime"
          })
        ]
      });

    const runtimeScene = buildRuntimeSceneFromDocument(document, {
      runtimeClock: {
        timeOfDayHours: 6,
        dayCount: 0,
        dayLengthMinutes: 24
      },
      loadedModelAssets: {
        [asset.id]: loadedAsset
      }
    });
    const host = new RuntimeHost({
      enableRendering: false
    });
    host.updateAssets(
      document.assets,
      {
        [asset.id]: loadedAsset
      },
      {},
      {}
    );
    host.loadScene(runtimeScene);

    const hostInternals = host as unknown as {
      currentClockState: {
        timeOfDayHours: number;
        dayCount: number;
        dayLengthMinutes: number;
      } | null;
      sceneReady: boolean;
      runtimeScene: typeof runtimeScene | null;
      modelRenderObjects: Map<
        string,
        {
          visible: boolean;
          position: { x: number; y: number; z: number };
          rotation: { y: number };
        }
      >;
      applyPlayAnimationAction(
        instanceId: string,
        clipName: string,
        loop: boolean | undefined
      ): void;
      syncRuntimeScheduleToCurrentClock(): void;
    };
    const playAnimationSpy = vi
      .spyOn(hostInternals, "applyPlayAnimationAction")
      .mockImplementation(() => undefined);

    hostInternals.sceneReady = true;
    hostInternals.currentClockState = {
      timeOfDayHours: 11,
      dayCount: 0,
      dayLengthMinutes: 24
    };
    hostInternals.syncRuntimeScheduleToCurrentClock();

    expect(playAnimationSpy).toHaveBeenCalledWith(npc.id, "Walk", true);
    expect(hostInternals.runtimeScene?.entities.npcs).toEqual([
      expect.objectContaining({
        entityId: npc.id,
        activeRoutineTitle: "Patrolling",
        animationClipName: "Walk",
        position: {
          x: 4,
          y: 0,
          z: 0
        }
      })
    ]);
    expect(hostInternals.runtimeScene?.npcDefinitions[0]).toEqual(
      expect.objectContaining({
        entityId: npc.id,
        animationClipName: "Walk",
        yawDegrees: 90,
        resolvedPath: expect.objectContaining({
          pathId: path.id,
          progress: 0.5
        })
      })
    );
    expect(hostInternals.modelRenderObjects.get(npc.id)).toEqual(
      expect.objectContaining({
        visible: true,
        position: expect.objectContaining({
          x: 4,
          y: 0,
          z: 0
        }),
        rotation: expect.objectContaining({
          y: Math.PI / 2
        })
      })
    );

    hostInternals.currentClockState = {
      timeOfDayHours: 14,
      dayCount: 0,
      dayLengthMinutes: 24
    };
    hostInternals.syncRuntimeScheduleToCurrentClock();

    expect(hostInternals.runtimeScene?.entities.npcs).toEqual([
      expect.objectContaining({
        entityId: npc.id,
        activeRoutineTitle: null,
        animationClipName: null,
        position: {
          x: 8,
          y: 0,
          z: 0
        },
        resolvedPath: expect.objectContaining({
          pathId: path.id,
          progress: 1
        })
      })
    ]);
    expect(hostInternals.runtimeScene?.npcDefinitions[0]).toEqual(
      expect.objectContaining({
        entityId: npc.id,
        active: true,
        activeRoutineTitle: null,
        animationClipName: null,
        yawDegrees: 90,
        position: {
          x: 8,
          y: 0,
          z: 0
        },
        resolvedPath: expect.objectContaining({
          pathId: path.id,
          progress: 1,
          yawDegrees: 90
        })
      })
    );

    host.dispose();
  });

  it("applies scheduler-controlled light effects and restores authored defaults when the routine ends", () => {
    vi.spyOn(console, "warn").mockImplementation(() => undefined);
    vi.spyOn(RapierCollisionWorld, "create").mockResolvedValue({
      dispose: vi.fn(),
      resolveThirdPersonCameraCollision: vi.fn(
        (_pivot, desiredCameraPosition) => desiredCameraPosition
      )
    } as unknown as RapierCollisionWorld);

    const pointLight = createPointLightEntity({
      id: "entity-point-light-night-lamp",
      intensity: 1.25
    });
    const document = createEmptySceneDocument();
    document.entities[pointLight.id] = pointLight;
    document.scheduler.routines["routine-night-lamp"] =
      createProjectScheduleRoutine({
        id: "routine-night-lamp",
        title: "Night Lamp",
        target: createLightControlTargetRef("pointLight", pointLight.id),
        startHour: 20,
        endHour: 4,
        effect: createSetLightIntensityControlEffect({
          target: createLightControlTargetRef("pointLight", pointLight.id),
          intensity: 3.5
        })
      });

    const runtimeScene = buildRuntimeSceneFromDocument(document);
    const host = new RuntimeHost({
      enableRendering: false
    });
    host.loadScene(runtimeScene);

    const hostInternals = host as unknown as {
      currentClockState: {
        timeOfDayHours: number;
        dayCount: number;
        dayLengthMinutes: number;
      } | null;
      sceneReady: boolean;
      runtimeScene: typeof runtimeScene | null;
      localLightObjects: Map<
        string,
        {
          light: { intensity: number };
        }
      >;
      syncRuntimeScheduleToCurrentClock(): void;
    };

    hostInternals.sceneReady = true;
    hostInternals.currentClockState = {
      timeOfDayHours: 21,
      dayCount: 0,
      dayLengthMinutes: 24
    };
    hostInternals.syncRuntimeScheduleToCurrentClock();

    expect(
      hostInternals.localLightObjects.get(pointLight.id)?.light.intensity
    ).toBe(3.5);
    expect(hostInternals.runtimeScene?.control.resolved.channels).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          type: "lightIntensity",
          value: 3.5,
          source: {
            kind: "scheduler",
            scheduleId: "routine-night-lamp"
          }
        })
      ])
    );

    hostInternals.currentClockState = {
      timeOfDayHours: 6,
      dayCount: 1,
      dayLengthMinutes: 24
    };
    hostInternals.syncRuntimeScheduleToCurrentClock();

    expect(
      hostInternals.localLightObjects.get(pointLight.id)?.light.intensity
    ).toBe(1.25);
    expect(hostInternals.runtimeScene?.control.resolved.channels).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          type: "lightIntensity",
          value: 1.25,
          source: {
            kind: "default"
          }
        })
      ])
    );

    host.dispose();
  });

  it("fires scheduler impulse sequences only once until the runtime session is reset", () => {
    vi.spyOn(console, "warn").mockImplementation(() => undefined);
    vi.spyOn(RapierCollisionWorld, "create").mockResolvedValue({
      dispose: vi.fn(),
      resolveThirdPersonCameraCollision: vi.fn(
        (_pivot, desiredCameraPosition) => desiredCameraPosition
      )
    } as unknown as RapierCollisionWorld);

    const document = createEmptySceneDocument();
    document.sequences.sequences["sequence-scene-transition"] =
      createProjectSequence({
        id: "sequence-scene-transition",
        title: "Scene Transition",
        effects: [
          {
            stepClass: "impulse",
            type: "startSceneTransition",
            targetSceneId: "scene-house",
            targetEntryEntityId: "entry-house"
          }
        ]
      });
    document.scheduler.routines["routine-scene-transition"] =
      createProjectScheduleRoutine({
        id: "routine-scene-transition",
        title: "Scene Transition Window",
        target: createProjectGlobalControlTargetRef(),
        sequenceId: "sequence-scene-transition",
        startHour: 8,
        endHour: 12,
        priority: 0,
        effects: []
      });

    const runtimeScene = buildRuntimeSceneFromDocument(document);
    const host = new RuntimeHost({
      enableRendering: false
    });
    const transitions: Array<{
      sourceEntityId: string | null;
      targetSceneId: string;
      targetEntryEntityId: string;
    }> = [];
    host.setSceneTransitionHandler((request) => {
      transitions.push(request);
    });
    host.loadScene(runtimeScene);

    const hostInternals = host as unknown as {
      currentClockState: {
        timeOfDayHours: number;
        dayCount: number;
        dayLengthMinutes: number;
      } | null;
      sceneReady: boolean;
      syncRuntimeScheduleToCurrentClock(): void;
    };

    hostInternals.sceneReady = true;
    hostInternals.currentClockState = {
      timeOfDayHours: 9,
      dayCount: 0,
      dayLengthMinutes: 24
    };
    hostInternals.syncRuntimeScheduleToCurrentClock();

    hostInternals.currentClockState = {
      timeOfDayHours: 10,
      dayCount: 0,
      dayLengthMinutes: 24
    };
    hostInternals.syncRuntimeScheduleToCurrentClock();

    host.loadScene(runtimeScene);
    hostInternals.currentClockState = {
      timeOfDayHours: 10.5,
      dayCount: 0,
      dayLengthMinutes: 24
    };
    hostInternals.syncRuntimeScheduleToCurrentClock();

    hostInternals.currentClockState = {
      timeOfDayHours: 9,
      dayCount: 1,
      dayLengthMinutes: 24
    };
    hostInternals.syncRuntimeScheduleToCurrentClock();

    expect(transitions).toEqual([
      {
        sourceEntityId: null,
        targetSceneId: "scene-house",
        targetEntryEntityId: "entry-house"
      }
    ]);

    host.dispose();
  });

  it("toggles the proposed runtime target instead of cycling candidates", () => {
    const host = new RuntimeHost({
      enableRendering: false
    });
    const hostInternals = host as unknown as {
      runtimeScene: unknown;
      sceneReady: boolean;
      activeController: unknown;
      thirdPersonController: unknown;
      runtimeTargetCandidates: Array<{
        kind: "npc" | "interactable";
        entityId: string;
      }>;
      proposedRuntimeTarget: {
        kind: "npc" | "interactable";
        entityId: string;
      } | null;
      activeRuntimeTargetReference: {
        kind: "npc" | "interactable";
        entityId: string;
      } | null;
      activateOrCycleRuntimeTarget(): void;
      clearActiveRuntimeTarget(): void;
    };
    const firstTarget = {
      kind: "npc" as const,
      entityId: "npc-one",
      prompt: "Talk",
      position: { x: 0, y: 0, z: 2 },
      center: { x: 0, y: 1, z: 2 },
      distance: 0,
      range: 2,
      viewDot: 1,
      score: 3
    };
    const secondTarget = {
      ...firstTarget,
      kind: "interactable" as const,
      entityId: "switch-two",
      prompt: "Use",
      score: 2.5
    };

    hostInternals.runtimeScene = {} as never;
    hostInternals.sceneReady = true;
    hostInternals.activeController = hostInternals.thirdPersonController;
    hostInternals.runtimeTargetCandidates = [firstTarget, secondTarget];
    hostInternals.proposedRuntimeTarget = firstTarget;

    hostInternals.activateOrCycleRuntimeTarget();

    expect(hostInternals.activeRuntimeTargetReference).toEqual({
      kind: "npc",
      entityId: "npc-one"
    });

    hostInternals.activateOrCycleRuntimeTarget();

    expect(hostInternals.activeRuntimeTargetReference).toBeNull();

    hostInternals.clearActiveRuntimeTarget();

    expect(hostInternals.activeRuntimeTargetReference).toBeNull();
    host.dispose();
  });

  it("places targeting visuals above the target focus at readable scale", () => {
    const placement = resolveRuntimeTargetVisualPlacement({
      center: { x: 1, y: 1.1, z: -2 },
      range: 1.5
    });

    expect(placement.luxPosition).toMatchObject({
      x: 1,
      z: -2
    });
    expect(placement.luxPosition.y).toBeCloseTo(2.18);
    expect(placement.activeMarkerPosition).toMatchObject({
      x: 1,
      y: 1.1,
      z: -2
    });
    expect(placement.activeMarkerRadius).toBeGreaterThan(0.6);
    expect(placement.activeMarkerScale).toBeGreaterThan(0.75);
  });

  it("uses three active target arrows that point inward at target center", () => {
    const host = new RuntimeHost({
      enableRendering: false
    });
    const hostInternals = host as unknown as {
      runtimeScene: unknown;
      sceneReady: boolean;
      activeController: unknown;
      thirdPersonController: unknown;
      currentPlayerControllerTelemetry: unknown;
      activeRuntimeTargetReference: {
        kind: "npc";
        entityId: string;
      } | null;
      targetingActiveGroup: {
        children: unknown[];
        position: Vector3;
        visible: boolean;
      };
      targetingActiveArrows: Array<{
        position: Vector3;
        quaternion: Quaternion;
      }>;
      camera: PerspectiveCamera;
      updateRuntimeTargetingVisuals(dt: number): void;
    };

    hostInternals.runtimeScene = {
      entities: {
        npcs: [
          {
            entityId: "npc-active",
            visible: true,
            position: { x: 0, y: 0, z: 4 },
            collider: {
              mode: "capsule",
              radius: 0.35,
              height: 1.8,
              eyeHeight: 1.6
            },
            name: "Active",
            defaultDialogueId: null,
            dialogues: []
          }
        ],
        interactables: [],
        cameraRigs: []
      },
      interactionLinks: [
        {
          id: "link-active",
          sourceEntityId: "npc-active",
          trigger: "click",
          action: { type: "runSequence", sequenceId: "noop" }
        }
      ]
    } as never;
    hostInternals.sceneReady = true;
    hostInternals.activeController = hostInternals.thirdPersonController;
    hostInternals.currentPlayerControllerTelemetry = {
      feetPosition: { x: 0, y: 0, z: 0 },
      eyePosition: { x: 0, y: 1.6, z: 0 }
    };
    hostInternals.activeRuntimeTargetReference = {
      kind: "npc",
      entityId: "npc-active"
    };
    hostInternals.camera.position.set(0, 1.6, -3);
    hostInternals.camera.lookAt(0, 0.9, 4);
    hostInternals.camera.updateMatrixWorld();
    hostInternals.camera.updateProjectionMatrix();

    hostInternals.updateRuntimeTargetingVisuals(0.25);

    expect(hostInternals.targetingActiveGroup.visible).toBe(true);
    expect(hostInternals.targetingActiveGroup.children).toHaveLength(3);
    expect(hostInternals.targetingActiveGroup.position.y).toBeCloseTo(0.9);

    for (const arrow of hostInternals.targetingActiveArrows) {
      const inwardDirection = arrow.position
        .clone()
        .multiplyScalar(-1)
        .normalize();
      const arrowTipDirection = new Vector3(0, 1, 0)
        .applyQuaternion(arrow.quaternion)
        .normalize();

      expect(arrow.position.length()).toBeGreaterThan(0.7);
      expect(arrowTipDirection.dot(inwardDirection)).toBeGreaterThan(0.99);
    }

    host.dispose();
  });

  it("flies Lux out from the player center when a target is proposed", () => {
    const host = new RuntimeHost({
      enableRendering: false
    });
    const hostInternals = host as unknown as {
      runtimeScene: unknown;
      sceneReady: boolean;
      activeController: unknown;
      thirdPersonController: unknown;
      currentPlayerControllerTelemetry: unknown;
      proposedRuntimeTarget: {
        kind: "npc";
        entityId: string;
        prompt: string;
        position: { x: number; y: number; z: number };
        center: { x: number; y: number; z: number };
        distance: number;
        range: number;
        viewDot: number;
        score: number;
      } | null;
      targetingVisualGroup: { visible: boolean };
      targetingLuxGroup: { position: Vector3; visible: boolean };
      targetingLuxFlightState: string;
      camera: PerspectiveCamera;
      updateRuntimeTargetingVisuals(dt: number): void;
    };

    hostInternals.runtimeScene = {
      entities: {
        npcs: [],
        interactables: [],
        cameraRigs: []
      }
    } as never;
    hostInternals.sceneReady = true;
    hostInternals.activeController = hostInternals.thirdPersonController;
    hostInternals.currentPlayerControllerTelemetry = {
      feetPosition: { x: 0, y: 0, z: 0 },
      eyePosition: { x: 0, y: 1.6, z: 0 }
    };
    hostInternals.proposedRuntimeTarget = {
      kind: "npc",
      entityId: "npc-target",
      prompt: "Talk",
      position: { x: 0, y: 0, z: 4 },
      center: { x: 0, y: 1, z: 4 },
      distance: 4,
      range: 1.5,
      viewDot: 1,
      score: 1
    };
    hostInternals.camera.position.set(0, 1.6, -3);

    hostInternals.updateRuntimeTargetingVisuals(0);

    expect(hostInternals.targetingVisualGroup.visible).toBe(true);
    expect(hostInternals.targetingLuxGroup.visible).toBe(true);
    expect(hostInternals.targetingLuxFlightState).toBe("outbound");
    expect(hostInternals.targetingLuxGroup.position.x).toBeCloseTo(0);
    expect(hostInternals.targetingLuxGroup.position.y).toBeCloseTo(0.832);
    expect(hostInternals.targetingLuxGroup.position.z).toBeCloseTo(0);

    hostInternals.updateRuntimeTargetingVisuals(0.1);

    expect(hostInternals.targetingLuxGroup.position.x).toBeGreaterThan(0.005);
    expect(hostInternals.targetingLuxGroup.position.y).toBeGreaterThan(0.832);
    expect(hostInternals.targetingLuxGroup.position.z).toBeGreaterThan(0);
    expect(hostInternals.targetingLuxGroup.position.z).toBeLessThan(4);
    host.dispose();
  });

  it("flies Lux back to the player before hiding when the proposal disappears", () => {
    const host = new RuntimeHost({
      enableRendering: false
    });
    const hostInternals = host as unknown as {
      runtimeScene: unknown;
      sceneReady: boolean;
      activeController: unknown;
      thirdPersonController: unknown;
      currentPlayerControllerTelemetry: unknown;
      proposedRuntimeTarget: {
        kind: "npc";
        entityId: string;
        prompt: string;
        position: { x: number; y: number; z: number };
        center: { x: number; y: number; z: number };
        distance: number;
        range: number;
        viewDot: number;
        score: number;
      } | null;
      targetingVisualGroup: { visible: boolean };
      targetingLuxGroup: { position: Vector3; visible: boolean };
      targetingLuxFlightState: string;
      camera: PerspectiveCamera;
      updateRuntimeTargetingVisuals(dt: number): void;
    };

    hostInternals.runtimeScene = {
      entities: {
        npcs: [],
        interactables: [],
        cameraRigs: []
      }
    } as never;
    hostInternals.sceneReady = true;
    hostInternals.activeController = hostInternals.thirdPersonController;
    hostInternals.currentPlayerControllerTelemetry = {
      feetPosition: { x: 0, y: 0, z: 0 },
      eyePosition: { x: 0, y: 1.6, z: 0 }
    };
    hostInternals.proposedRuntimeTarget = {
      kind: "npc",
      entityId: "npc-target",
      prompt: "Talk",
      position: { x: 0, y: 0, z: 5 },
      center: { x: 0, y: 1, z: 5 },
      distance: 5,
      range: 1.5,
      viewDot: 1,
      score: 1
    };
    hostInternals.camera.position.set(0, 1.6, -3);

    hostInternals.updateRuntimeTargetingVisuals(0);
    hostInternals.updateRuntimeTargetingVisuals(1);
    const homePosition = new Vector3(0, 0.832, 0);
    const distanceBeforeReturn =
      hostInternals.targetingLuxGroup.position.distanceTo(homePosition);

    hostInternals.proposedRuntimeTarget = null;
    hostInternals.updateRuntimeTargetingVisuals(0.1);

    expect(hostInternals.targetingVisualGroup.visible).toBe(true);
    expect(hostInternals.targetingLuxGroup.visible).toBe(true);
    expect(hostInternals.targetingLuxFlightState).toBe("returning");
    expect(
      hostInternals.targetingLuxGroup.position.distanceTo(homePosition)
    ).toBeLessThan(distanceBeforeReturn);

    for (let index = 0; index < 20; index += 1) {
      hostInternals.updateRuntimeTargetingVisuals(0.1);
    }

    expect(hostInternals.targetingVisualGroup.visible).toBe(false);
    expect(hostInternals.targetingLuxGroup.visible).toBe(false);
    expect(hostInternals.targetingLuxFlightState).toBe("hidden");
    host.dispose();
  });

  it("uses Lux-only proposal feedback and consumes Escape when clearing an active target", () => {
    const host = new RuntimeHost({
      enableRendering: false
    });
    const hostInternals = host as unknown as {
      runtimeScene: unknown;
      sceneReady: boolean;
      activeController: unknown;
      thirdPersonController: unknown;
      proposedRuntimeTarget: {
        kind: "npc" | "interactable";
        entityId: string;
        center: { x: number; y: number; z: number };
      } | null;
      activeRuntimeTargetReference: {
        kind: "npc" | "interactable";
        entityId: string;
      } | null;
      resolveThirdPersonTargetAssist(): unknown;
      handleRuntimeKeyDown(event: KeyboardEvent): void;
    };
    const escapeEvent = {
      code: "Escape",
      defaultPrevented: false,
      repeat: false,
      altKey: false,
      ctrlKey: false,
      metaKey: false,
      target: null,
      preventDefault: vi.fn(),
      stopImmediatePropagation: vi.fn()
    } as unknown as KeyboardEvent;

    hostInternals.runtimeScene = {
      playerInputBindings: {
        keyboard: {
          pauseTime: "KeyP"
        }
      },
      entities: {
        cameraRigs: [],
        interactables: [],
        npcs: []
      },
      interactionLinks: []
    } as never;
    hostInternals.sceneReady = true;
    hostInternals.activeController = hostInternals.thirdPersonController;
    hostInternals.proposedRuntimeTarget = {
      kind: "npc",
      entityId: "npc-proposed",
      center: { x: 0, y: 1, z: 3 }
    };

    expect(hostInternals.resolveThirdPersonTargetAssist()).toBeNull();

    hostInternals.activeRuntimeTargetReference = {
      kind: "npc",
      entityId: "npc-active"
    };
    hostInternals.handleRuntimeKeyDown(escapeEvent);

    expect(hostInternals.activeRuntimeTargetReference).toBeNull();
    expect(escapeEvent.preventDefault).toHaveBeenCalledTimes(1);
    expect(escapeEvent.stopImmediatePropagation).toHaveBeenCalledTimes(1);
    host.dispose();
  });

  it("switches an active target once from directional screen-space look input", () => {
    const host = new RuntimeHost({
      enableRendering: false
    });
    const hostInternals = host as unknown as {
      runtimeScene: unknown;
      activeController: unknown;
      thirdPersonController: unknown;
      currentPlayerControllerTelemetry: unknown;
      runtimeTargetCandidates: Array<{
        kind: "npc";
        entityId: string;
        center: { x: number; y: number; z: number };
        score: number;
      }>;
      activeRuntimeTargetReference: {
        kind: "npc";
        entityId: string;
      } | null;
      camera: PerspectiveCamera;
      handleRuntimeTargetLookInput(input: {
        horizontal: number;
        vertical: number;
      }): {
        activeTargetLocked: boolean;
        switchedTarget: boolean;
        switchInputHeld: boolean;
      };
      updateActiveRuntimeTargetLockState(): void;
    };

    hostInternals.runtimeScene = {
      entities: {
        npcs: [
          {
            entityId: "npc-active",
            visible: true,
            position: { x: 0, y: 0, z: 5 },
            collider: {
              mode: "capsule",
              radius: 0.35,
              height: 1.8,
              eyeHeight: 1.6
            },
            name: "Active",
            defaultDialogueId: null,
            dialogues: []
          },
          {
            entityId: "npc-right",
            visible: true,
            position: { x: 2, y: 0, z: 5 },
            collider: {
              mode: "capsule",
              radius: 0.35,
              height: 1.8,
              eyeHeight: 1.6
            },
            name: "Right",
            defaultDialogueId: null,
            dialogues: []
          },
          {
            entityId: "npc-above",
            visible: true,
            position: { x: 0, y: 2, z: 5 },
            collider: {
              mode: "capsule",
              radius: 0.35,
              height: 1.8,
              eyeHeight: 1.6
            },
            name: "Above",
            defaultDialogueId: null,
            dialogues: []
          }
        ],
        interactables: [],
        cameraRigs: []
      },
      interactionLinks: [
        {
          id: "link-active",
          sourceEntityId: "npc-active",
          trigger: "click",
          action: { type: "runSequence", sequenceId: "noop" }
        },
        {
          id: "link-right",
          sourceEntityId: "npc-right",
          trigger: "click",
          action: { type: "runSequence", sequenceId: "noop" }
        },
        {
          id: "link-above",
          sourceEntityId: "npc-above",
          trigger: "click",
          action: { type: "runSequence", sequenceId: "noop" }
        }
      ]
    } as never;
    hostInternals.activeController = hostInternals.thirdPersonController;
    hostInternals.currentPlayerControllerTelemetry = {
      eyePosition: { x: 0, y: 1.6, z: 0 }
    };
    hostInternals.runtimeTargetCandidates = [
      {
        kind: "npc",
        entityId: "npc-active",
        center: { x: 0, y: 0.9, z: 5 },
        score: 3
      },
      {
        kind: "npc",
        entityId: "npc-right",
        center: { x: 2, y: 0.9, z: 5 },
        score: 2.5
      },
      {
        kind: "npc",
        entityId: "npc-above",
        center: { x: 0, y: 2.9, z: 5 },
        score: 2.4
      }
    ];
    hostInternals.activeRuntimeTargetReference = {
      kind: "npc",
      entityId: "npc-active"
    };

    hostInternals.camera.position.set(0, 1.6, 0);
    hostInternals.camera.lookAt(0, 0.9, 5);
    hostInternals.camera.updateMatrixWorld();
    hostInternals.camera.updateProjectionMatrix();

    expect(
      hostInternals.handleRuntimeTargetLookInput({
        horizontal: -1,
        vertical: 0
      })
    ).toEqual({
      activeTargetLocked: true,
      switchedTarget: true,
      switchInputHeld: true
    });

    expect(hostInternals.activeRuntimeTargetReference).toEqual({
      kind: "npc",
      entityId: "npc-right"
    });
    expect(
      hostInternals.handleRuntimeTargetLookInput({
        horizontal: -1,
        vertical: 0
      })
    ).toEqual({
      activeTargetLocked: true,
      switchedTarget: false,
      switchInputHeld: true
    });
    expect(hostInternals.activeRuntimeTargetReference).toEqual({
      kind: "npc",
      entityId: "npc-right"
    });
    expect(
      hostInternals.handleRuntimeTargetLookInput({
        horizontal: 0,
        vertical: 0
      })
    ).toEqual({
      activeTargetLocked: true,
      switchedTarget: false,
      switchInputHeld: false
    });
    hostInternals.activeRuntimeTargetReference = {
      kind: "npc",
      entityId: "npc-active"
    };
    expect(
      hostInternals.handleRuntimeTargetLookInput({
        horizontal: 0,
        vertical: 1
      })
    ).toEqual({
      activeTargetLocked: true,
      switchedTarget: true,
      switchInputHeld: true
    });
    expect(hostInternals.activeRuntimeTargetReference).toEqual({
      kind: "npc",
      entityId: "npc-above"
    });
    host.dispose();
  });

  it("does not switch active target from camera angle without look input", () => {
    const host = new RuntimeHost({
      enableRendering: false
    });
    const hostInternals = host as unknown as {
      runtimeScene: unknown;
      activeController: unknown;
      thirdPersonController: unknown;
      currentPlayerControllerTelemetry: unknown;
      runtimeTargetCandidates: Array<{
        kind: "npc";
        entityId: string;
        center: { x: number; y: number; z: number };
        score: number;
      }>;
      activeRuntimeTargetReference: {
        kind: "npc";
        entityId: string;
      } | null;
      camera: PerspectiveCamera;
      updateActiveRuntimeTargetLockState(): void;
    };

    hostInternals.runtimeScene = {
      entities: {
        npcs: [
          {
            entityId: "npc-active",
            visible: true,
            position: { x: 0, y: 0, z: 5 },
            collider: {
              mode: "capsule",
              radius: 0.35,
              height: 1.8,
              eyeHeight: 1.6
            },
            name: "Active",
            defaultDialogueId: null,
            dialogues: []
          }
        ],
        interactables: [],
        cameraRigs: []
      },
      interactionLinks: [
        {
          id: "link-active",
          sourceEntityId: "npc-active",
          trigger: "click",
          action: { type: "runSequence", sequenceId: "noop" }
        }
      ]
    } as never;
    hostInternals.activeController = hostInternals.thirdPersonController;
    hostInternals.currentPlayerControllerTelemetry = {
      eyePosition: { x: 0, y: 1.6, z: 0 }
    };
    hostInternals.runtimeTargetCandidates = [
      {
        kind: "npc",
        entityId: "npc-active",
        center: { x: 0, y: 0.9, z: 5 },
        score: 3
      },
      {
        kind: "npc",
        entityId: "npc-other",
        center: { x: -2, y: 0.9, z: 5 },
        score: 2.5
      }
    ];
    hostInternals.activeRuntimeTargetReference = {
      kind: "npc",
      entityId: "npc-active"
    };

    hostInternals.camera.position.set(0, 1.6, 0);
    hostInternals.camera.lookAt(10, 1.6, 3);
    hostInternals.updateActiveRuntimeTargetLockState();

    expect(hostInternals.activeRuntimeTargetReference).toEqual({
      kind: "npc",
      entityId: "npc-active"
    });
    host.dispose();
  });

  it("retargets to the centered on-screen candidate before clearing a distant active target", () => {
    const host = new RuntimeHost({
      enableRendering: false
    });
    const hostInternals = host as unknown as {
      runtimeScene: unknown;
      activeController: unknown;
      thirdPersonController: unknown;
      currentPlayerControllerTelemetry: unknown;
      runtimeTargetCandidates: Array<{
        kind: "npc";
        entityId: string;
        prompt: string;
        position: { x: number; y: number; z: number };
        center: { x: number; y: number; z: number };
        distance: number;
        range: number;
        viewDot: number;
        score: number;
      }>;
      proposedRuntimeTarget: {
        kind: "npc";
        entityId: string;
      } | null;
      activeRuntimeTargetReference: {
        kind: "npc";
        entityId: string;
      } | null;
      camera: PerspectiveCamera;
      updateActiveRuntimeTargetLockState(): void;
    };

    hostInternals.runtimeScene = {
      entities: {
        npcs: [
          {
            entityId: "npc-far",
            visible: true,
            position: { x: 0, y: 0, z: 16 },
            collider: {
              mode: "capsule",
              radius: 0.35,
              height: 1.8,
              eyeHeight: 1.6
            },
            name: "Far",
            defaultDialogueId: null,
            dialogues: []
          },
          {
            entityId: "npc-near",
            visible: true,
            position: { x: 0, y: 0, z: 6 },
            collider: {
              mode: "capsule",
              radius: 0.35,
              height: 1.8,
              eyeHeight: 1.6
            },
            name: "Near",
            defaultDialogueId: null,
            dialogues: []
          }
        ],
        interactables: [],
        cameraRigs: []
      },
      interactionLinks: [
        {
          id: "link-far",
          sourceEntityId: "npc-far",
          trigger: "click",
          action: { type: "runSequence", sequenceId: "noop" }
        },
        {
          id: "link-near",
          sourceEntityId: "npc-near",
          trigger: "click",
          action: { type: "runSequence", sequenceId: "noop" }
        }
      ]
    } as never;
    hostInternals.activeController = hostInternals.thirdPersonController;
    hostInternals.currentPlayerControllerTelemetry = {
      eyePosition: { x: 0, y: 1.6, z: 0 }
    };
    hostInternals.activeRuntimeTargetReference = {
      kind: "npc",
      entityId: "npc-far"
    };
    hostInternals.runtimeTargetCandidates = [
      {
        kind: "npc",
        entityId: "npc-near",
        prompt: "Talk",
        position: { x: 0, y: 0, z: 6 },
        center: { x: 0, y: 0.9, z: 6 },
        distance: 6,
        range: 1.5,
        viewDot: 1,
        score: 2
      }
    ];
    hostInternals.camera.position.set(0, 1.6, 0);
    hostInternals.camera.lookAt(0, 0.9, 6);
    hostInternals.camera.updateMatrixWorld();
    hostInternals.camera.updateProjectionMatrix();

    hostInternals.updateActiveRuntimeTargetLockState();

    expect(hostInternals.activeRuntimeTargetReference).toEqual({
      kind: "npc",
      entityId: "npc-near"
    });
    expect(hostInternals.proposedRuntimeTarget).toEqual({
      kind: "npc",
      entityId: "npc-near",
      prompt: "Talk",
      position: { x: 0, y: 0, z: 6 },
      center: { x: 0, y: 0.9, z: 6 },
      distance: 6,
      range: 1.5,
      viewDot: 1,
      score: 2
    });
    host.dispose();
  });

  it("does not auto-retarget between close border targets inside the distance hysteresis", () => {
    const host = new RuntimeHost({
      enableRendering: false
    });
    const hostInternals = host as unknown as {
      runtimeScene: unknown;
      activeController: unknown;
      thirdPersonController: unknown;
      currentPlayerControllerTelemetry: unknown;
      runtimeTargetCandidates: Array<{
        kind: "npc";
        entityId: string;
        prompt: string;
        position: { x: number; y: number; z: number };
        center: { x: number; y: number; z: number };
        distance: number;
        range: number;
        viewDot: number;
        score: number;
      }>;
      activeRuntimeTargetReference: {
        kind: "npc";
        entityId: string;
      } | null;
      camera: PerspectiveCamera;
      updateActiveRuntimeTargetLockState(): void;
    };

    hostInternals.runtimeScene = {
      entities: {
        npcs: [
          {
            entityId: "npc-border-a",
            visible: true,
            position: { x: 0, y: 0, z: 15.5 },
            collider: {
              mode: "capsule",
              radius: 0.35,
              height: 1.8,
              eyeHeight: 1.6
            },
            name: "Border A",
            defaultDialogueId: null,
            dialogues: []
          },
          {
            entityId: "npc-border-b",
            visible: true,
            position: { x: 0.3, y: 0, z: 15.2 },
            collider: {
              mode: "capsule",
              radius: 0.35,
              height: 1.8,
              eyeHeight: 1.6
            },
            name: "Border B",
            defaultDialogueId: null,
            dialogues: []
          }
        ],
        interactables: [],
        cameraRigs: []
      },
      interactionLinks: [
        {
          id: "link-border-a",
          sourceEntityId: "npc-border-a",
          trigger: "click",
          action: { type: "runSequence", sequenceId: "noop" }
        },
        {
          id: "link-border-b",
          sourceEntityId: "npc-border-b",
          trigger: "click",
          action: { type: "runSequence", sequenceId: "noop" }
        }
      ]
    } as never;
    hostInternals.activeController = hostInternals.thirdPersonController;
    hostInternals.currentPlayerControllerTelemetry = {
      eyePosition: { x: 0, y: 1.6, z: 0 }
    };
    hostInternals.activeRuntimeTargetReference = {
      kind: "npc",
      entityId: "npc-border-a"
    };
    hostInternals.runtimeTargetCandidates = [
      {
        kind: "npc",
        entityId: "npc-border-b",
        prompt: "Talk",
        position: { x: 0.3, y: 0, z: 15.2 },
        center: { x: 0.3, y: 0.9, z: 15.2 },
        distance: 14.9,
        range: 1.5,
        viewDot: 1,
        score: 2
      }
    ];
    hostInternals.camera.position.set(0, 1.6, 0);
    hostInternals.camera.lookAt(0, 0.9, 15.5);
    hostInternals.camera.updateMatrixWorld();
    hostInternals.camera.updateProjectionMatrix();

    hostInternals.updateActiveRuntimeTargetLockState();

    expect(hostInternals.activeRuntimeTargetReference).toEqual({
      kind: "npc",
      entityId: "npc-border-a"
    });
    host.dispose();
  });

  it("clears active target on manual look boundary without retargeting", () => {
    const host = new RuntimeHost({
      enableRendering: false
    });
    const hostInternals = host as unknown as {
      controllerContext: {
        handleRuntimeTargetLookBoundaryReached(): boolean;
      };
      runtimeTargetCandidates: Array<{
        kind: "npc";
        entityId: string;
        prompt: string;
        position: { x: number; y: number; z: number };
        center: { x: number; y: number; z: number };
        distance: number;
        range: number;
        viewDot: number;
        score: number;
      }>;
      proposedRuntimeTarget: {
        kind: "npc";
        entityId: string;
      } | null;
      activeRuntimeTargetReference: {
        kind: "npc";
        entityId: string;
      } | null;
    };

    hostInternals.activeRuntimeTargetReference = {
      kind: "npc",
      entityId: "npc-active"
    };
    hostInternals.proposedRuntimeTarget = null;
    hostInternals.runtimeTargetCandidates = [
      {
        kind: "npc",
        entityId: "npc-near",
        prompt: "Talk",
        position: { x: 0, y: 0, z: 6 },
        center: { x: 0, y: 0.9, z: 6 },
        distance: 6,
        range: 1.5,
        viewDot: 1,
        score: 2
      }
    ];

    expect(
      hostInternals.controllerContext.handleRuntimeTargetLookBoundaryReached()
    ).toBe(false);
    expect(hostInternals.activeRuntimeTargetReference).toBeNull();
    expect(hostInternals.proposedRuntimeTarget).toBeNull();
    host.dispose();
  });

  it("clears an active target when the player moves too far away without an on-screen fallback", () => {
    const host = new RuntimeHost({
      enableRendering: false
    });
    const hostInternals = host as unknown as {
      runtimeScene: unknown;
      activeController: unknown;
      thirdPersonController: unknown;
      currentPlayerControllerTelemetry: unknown;
      runtimeTargetCandidates: unknown[];
      activeRuntimeTargetReference: {
        kind: "npc";
        entityId: string;
      } | null;
      updateActiveRuntimeTargetLockState(): void;
    };

    hostInternals.runtimeScene = {
      entities: {
        npcs: [
          {
            entityId: "npc-far",
            visible: true,
            position: { x: 0, y: 0, z: 16 },
            collider: {
              mode: "capsule",
              radius: 0.35,
              height: 1.8,
              eyeHeight: 1.6
            },
            name: "Far",
            defaultDialogueId: null,
            dialogues: []
          }
        ],
        interactables: [],
        cameraRigs: []
      },
      interactionLinks: [
        {
          id: "link-far",
          sourceEntityId: "npc-far",
          trigger: "click",
          action: { type: "runSequence", sequenceId: "noop" }
        }
      ]
    } as never;
    hostInternals.activeController = hostInternals.thirdPersonController;
    hostInternals.currentPlayerControllerTelemetry = {
      eyePosition: { x: 0, y: 1.6, z: 0 }
    };
    hostInternals.activeRuntimeTargetReference = {
      kind: "npc",
      entityId: "npc-far"
    };
    hostInternals.runtimeTargetCandidates = [];

    hostInternals.updateActiveRuntimeTargetLockState();

    expect(hostInternals.activeRuntimeTargetReference).toBeNull();
    host.dispose();
  });

  it("proposes the target closest to screen center instead of the nearest candidate", () => {
    const host = new RuntimeHost({
      enableRendering: false
    });
    const hostInternals = host as unknown as {
      runtimeScene: unknown;
      sceneReady: boolean;
      activeController: unknown;
      thirdPersonController: unknown;
      currentPlayerControllerTelemetry: unknown;
      runtimeTargetCandidates: Array<{
        kind: "npc";
        entityId: string;
        distance: number;
      }>;
      proposedRuntimeTarget: {
        kind: "npc";
        entityId: string;
      } | null;
      camera: PerspectiveCamera;
      refreshRuntimeTargetingState(): void;
    };

    hostInternals.runtimeScene = {
      entities: {
        npcs: [
          {
            entityId: "npc-close-edge",
            visible: true,
            position: { x: 3, y: 0, z: 4 },
            collider: {
              mode: "capsule",
              radius: 0.35,
              height: 1.8,
              eyeHeight: 1.6
            },
            name: "Close Edge",
            defaultDialogueId: null,
            dialogues: []
          },
          {
            entityId: "npc-center-farther",
            visible: true,
            position: { x: 0, y: 0, z: 10 },
            collider: {
              mode: "capsule",
              radius: 0.35,
              height: 1.8,
              eyeHeight: 1.6
            },
            name: "Center Farther",
            defaultDialogueId: null,
            dialogues: []
          }
        ],
        interactables: [],
        cameraRigs: []
      },
      interactionLinks: [
        {
          id: "link-close-edge",
          sourceEntityId: "npc-close-edge",
          trigger: "click",
          action: { type: "runSequence", sequenceId: "noop" }
        },
        {
          id: "link-center-farther",
          sourceEntityId: "npc-center-farther",
          trigger: "click",
          action: { type: "runSequence", sequenceId: "noop" }
        }
      ]
    } as never;
    hostInternals.sceneReady = true;
    hostInternals.activeController = hostInternals.thirdPersonController;
    hostInternals.currentPlayerControllerTelemetry = {
      eyePosition: { x: 0, y: 1.6, z: 0 }
    };
    hostInternals.camera.position.set(0, 1.6, 0);
    hostInternals.camera.lookAt(0, 0.9, 10);
    hostInternals.camera.updateMatrixWorld();
    hostInternals.camera.updateProjectionMatrix();

    hostInternals.refreshRuntimeTargetingState();

    const closeCandidate = hostInternals.runtimeTargetCandidates.find(
      (candidate) => candidate.entityId === "npc-close-edge"
    );
    const centeredCandidate = hostInternals.runtimeTargetCandidates.find(
      (candidate) => candidate.entityId === "npc-center-farther"
    );

    expect(closeCandidate?.distance).toBeLessThan(
      centeredCandidate?.distance ?? 0
    );
    expect(hostInternals.proposedRuntimeTarget).toMatchObject({
      kind: "npc",
      entityId: "npc-center-farther"
    });
    host.dispose();
  });

  it("biases Lux proposal focus slightly above screen center", () => {
    const host = new RuntimeHost({
      enableRendering: false
    });
    const hostInternals = host as unknown as {
      runtimeTargetCandidates: Array<{
        kind: "npc";
        entityId: string;
        prompt: string;
        position: { x: number; y: number; z: number };
        center: { x: number; y: number; z: number };
        distance: number;
        range: number;
        viewDot: number;
        score: number;
      }>;
      camera: PerspectiveCamera;
      resolveRuntimeTargetCandidateNearestScreenCenter(): {
        kind: "npc";
        entityId: string;
      } | null;
    };

    hostInternals.runtimeTargetCandidates = [
      {
        kind: "npc",
        entityId: "npc-above-center",
        prompt: "Talk",
        position: { x: 0, y: 0.98, z: 8 },
        center: { x: 0, y: 1.88, z: 8 },
        distance: 8,
        range: 1.5,
        viewDot: 1,
        score: 1
      },
      {
        kind: "npc",
        entityId: "npc-below-center",
        prompt: "Talk",
        position: { x: 0, y: -0.14, z: 8 },
        center: { x: 0, y: 0.76, z: 8 },
        distance: 8,
        range: 1.5,
        viewDot: 1,
        score: 1
      }
    ];
    hostInternals.camera.position.set(0, 1.6, 0);
    hostInternals.camera.lookAt(0, 1.6, 8);
    hostInternals.camera.updateMatrixWorld();
    hostInternals.camera.updateProjectionMatrix();

    expect(
      hostInternals.resolveRuntimeTargetCandidateNearestScreenCenter()
    ).toMatchObject({
      kind: "npc",
      entityId: "npc-above-center"
    });
    host.dispose();
  });

  it("filters Lux target candidates when the camera ray is occluded", () => {
    const host = new RuntimeHost({
      enableRendering: false
    });
    const hostInternals = host as unknown as {
      runtimeScene: unknown;
      sceneReady: boolean;
      activeController: unknown;
      thirdPersonController: unknown;
      currentPlayerControllerTelemetry: unknown;
      collisionWorld: {
        isLineSegmentClear(
          start: { x: number; y: number; z: number },
          end: { x: number; y: number; z: number }
        ): boolean;
        dispose(): void;
      };
      runtimeTargetCandidates: Array<{
        kind: "npc";
        entityId: string;
      }>;
      proposedRuntimeTarget: {
        kind: "npc";
        entityId: string;
      } | null;
      camera: PerspectiveCamera;
      refreshRuntimeTargetingState(): void;
    };

    hostInternals.runtimeScene = {
      entities: {
        npcs: [
          {
            entityId: "npc-visible",
            visible: true,
            position: { x: 0, y: 0, z: 6 },
            collider: {
              mode: "capsule",
              radius: 0.35,
              height: 1.8,
              eyeHeight: 1.6
            },
            name: "Visible",
            defaultDialogueId: null,
            dialogues: []
          },
          {
            entityId: "npc-occluded",
            visible: true,
            position: { x: 0, y: 0, z: 8 },
            collider: {
              mode: "capsule",
              radius: 0.35,
              height: 1.8,
              eyeHeight: 1.6
            },
            name: "Occluded",
            defaultDialogueId: null,
            dialogues: []
          }
        ],
        interactables: [],
        cameraRigs: []
      },
      interactionLinks: [
        {
          id: "link-visible",
          sourceEntityId: "npc-visible",
          trigger: "click",
          action: { type: "runSequence", sequenceId: "noop" }
        },
        {
          id: "link-occluded",
          sourceEntityId: "npc-occluded",
          trigger: "click",
          action: { type: "runSequence", sequenceId: "noop" }
        }
      ]
    } as never;
    hostInternals.sceneReady = true;
    hostInternals.activeController = hostInternals.thirdPersonController;
    hostInternals.currentPlayerControllerTelemetry = {
      eyePosition: { x: 0, y: 1.6, z: 0 }
    };
    hostInternals.collisionWorld = {
      isLineSegmentClear: (_start, end) => end.z < 8,
      dispose: vi.fn()
    };
    hostInternals.camera.position.set(0, 1.6, 0);
    hostInternals.camera.lookAt(0, 0.9, 8);
    hostInternals.camera.updateMatrixWorld();
    hostInternals.camera.updateProjectionMatrix();

    hostInternals.refreshRuntimeTargetingState();

    expect(
      hostInternals.runtimeTargetCandidates.some(
        (candidate) => candidate.entityId === "npc-occluded"
      )
    ).toBe(false);
    expect(hostInternals.proposedRuntimeTarget).toMatchObject({
      kind: "npc",
      entityId: "npc-visible"
    });
    host.dispose();
  });

  it("requires player-eye visibility for Lux proposal even when the camera can see the target", () => {
    const host = new RuntimeHost({
      enableRendering: false
    });
    const hostInternals = host as unknown as {
      runtimeScene: unknown;
      sceneReady: boolean;
      activeController: unknown;
      thirdPersonController: unknown;
      currentPlayerControllerTelemetry: unknown;
      collisionWorld: {
        isLineSegmentClear(
          start: { x: number; y: number; z: number },
          end: { x: number; y: number; z: number }
        ): boolean;
        dispose(): void;
      };
      runtimeTargetCandidates: Array<{
        kind: "npc";
        entityId: string;
      }>;
      proposedRuntimeTarget: {
        kind: "npc";
        entityId: string;
      } | null;
      camera: PerspectiveCamera;
      refreshRuntimeTargetingState(): void;
    };

    hostInternals.runtimeScene = {
      entities: {
        npcs: [
          {
            entityId: "npc-player-occluded",
            visible: true,
            position: { x: 0, y: 0, z: 7 },
            collider: {
              mode: "capsule",
              radius: 0.35,
              height: 1.8,
              eyeHeight: 1.6
            },
            name: "Player Occluded",
            defaultDialogueId: null,
            dialogues: []
          },
          {
            entityId: "npc-player-visible",
            visible: true,
            position: { x: 0.8, y: 0, z: 8 },
            collider: {
              mode: "capsule",
              radius: 0.35,
              height: 1.8,
              eyeHeight: 1.6
            },
            name: "Player Visible",
            defaultDialogueId: null,
            dialogues: []
          }
        ],
        interactables: [],
        cameraRigs: []
      },
      interactionLinks: [
        {
          id: "link-player-occluded",
          sourceEntityId: "npc-player-occluded",
          trigger: "click",
          action: { type: "runSequence", sequenceId: "noop" }
        },
        {
          id: "link-player-visible",
          sourceEntityId: "npc-player-visible",
          trigger: "click",
          action: { type: "runSequence", sequenceId: "noop" }
        }
      ]
    } as never;
    hostInternals.sceneReady = true;
    hostInternals.activeController = hostInternals.thirdPersonController;
    hostInternals.currentPlayerControllerTelemetry = {
      eyePosition: { x: 0, y: 1.6, z: 0 }
    };
    hostInternals.collisionWorld = {
      isLineSegmentClear: (start, end) => start.x !== 0 || end.z !== 7,
      dispose: vi.fn()
    };
    hostInternals.camera.position.set(1, 1.6, 0);
    hostInternals.camera.lookAt(0, 0.9, 7);
    hostInternals.camera.updateMatrixWorld();
    hostInternals.camera.updateProjectionMatrix();

    hostInternals.refreshRuntimeTargetingState();

    expect(
      hostInternals.runtimeTargetCandidates.some(
        (candidate) => candidate.entityId === "npc-player-occluded"
      )
    ).toBe(true);
    expect(hostInternals.proposedRuntimeTarget).toMatchObject({
      kind: "npc",
      entityId: "npc-player-visible"
    });
    host.dispose();
  });

  it("keeps Lux proposed when close above-target rays need body sample clearance", () => {
    const host = new RuntimeHost({
      enableRendering: false
    });
    const hostInternals = host as unknown as {
      runtimeScene: unknown;
      sceneReady: boolean;
      activeController: unknown;
      thirdPersonController: unknown;
      currentPlayerControllerTelemetry: unknown;
      collisionWorld: {
        isLineSegmentClear(
          start: { x: number; y: number; z: number },
          end: { x: number; y: number; z: number },
          options?: { targetClearance?: number }
        ): boolean;
        dispose(): void;
      };
      runtimeTargetCandidates: Array<{
        kind: "npc";
        entityId: string;
      }>;
      proposedRuntimeTarget: {
        kind: "npc";
        entityId: string;
      } | null;
      camera: PerspectiveCamera;
      refreshRuntimeTargetingState(): void;
    };

    hostInternals.runtimeScene = {
      entities: {
        npcs: [
          {
            entityId: "npc-close-above",
            visible: true,
            position: { x: 0, y: 0, z: 1 },
            collider: {
              mode: "capsule",
              radius: 0.35,
              height: 1.8,
              eyeHeight: 1.6
            },
            name: "Close Above",
            defaultDialogueId: null,
            dialogues: []
          }
        ],
        interactables: [],
        cameraRigs: []
      },
      interactionLinks: [
        {
          id: "link-close-above",
          sourceEntityId: "npc-close-above",
          trigger: "click",
          action: { type: "runSequence", sequenceId: "noop" }
        }
      ]
    } as never;
    hostInternals.sceneReady = true;
    hostInternals.activeController = hostInternals.thirdPersonController;
    hostInternals.currentPlayerControllerTelemetry = {
      eyePosition: { x: 0, y: 2.6, z: 0.8 }
    };
    hostInternals.collisionWorld = {
      isLineSegmentClear: vi.fn((_start, end, options) => {
        const clearance = options?.targetClearance ?? 0;
        return end.y > 1.35 && clearance >= 0.5;
      }),
      dispose: vi.fn()
    };
    hostInternals.camera.position.set(0, 3, 0.2);
    hostInternals.camera.lookAt(0, 1.45, 1);
    hostInternals.camera.updateMatrixWorld();
    hostInternals.camera.updateProjectionMatrix();

    hostInternals.refreshRuntimeTargetingState();

    expect(hostInternals.runtimeTargetCandidates).toEqual([
      expect.objectContaining({
        kind: "npc",
        entityId: "npc-close-above"
      })
    ]);
    expect(hostInternals.proposedRuntimeTarget).toMatchObject({
      kind: "npc",
      entityId: "npc-close-above"
    });
    host.dispose();
  });

  it("keeps an occluded active target through a short camera visibility grace", () => {
    const host = new RuntimeHost({
      enableRendering: false
    });
    const hostInternals = host as unknown as {
      runtimeScene: unknown;
      activeController: unknown;
      thirdPersonController: unknown;
      currentPlayerControllerTelemetry: unknown;
      collisionWorld: {
        isLineSegmentClear(): boolean;
        dispose(): void;
      };
      activeRuntimeTargetReference: {
        kind: "npc";
        entityId: string;
      } | null;
      camera: PerspectiveCamera;
      updateActiveRuntimeTargetLockState(dt?: number): void;
    };

    hostInternals.runtimeScene = {
      entities: {
        npcs: [
          {
            entityId: "npc-occluded-active",
            visible: true,
            position: { x: 0, y: 0, z: 6 },
            collider: {
              mode: "capsule",
              radius: 0.35,
              height: 1.8,
              eyeHeight: 1.6
            },
            name: "Occluded Active",
            defaultDialogueId: null,
            dialogues: []
          }
        ],
        interactables: [],
        cameraRigs: []
      },
      interactionLinks: [
        {
          id: "link-occluded-active",
          sourceEntityId: "npc-occluded-active",
          trigger: "click",
          action: { type: "runSequence", sequenceId: "noop" }
        }
      ]
    } as never;
    hostInternals.activeController = hostInternals.thirdPersonController;
    hostInternals.currentPlayerControllerTelemetry = {
      eyePosition: { x: 0, y: 1.6, z: 0 }
    };
    hostInternals.collisionWorld = {
      isLineSegmentClear: () => false,
      dispose: vi.fn()
    };
    hostInternals.activeRuntimeTargetReference = {
      kind: "npc",
      entityId: "npc-occluded-active"
    };
    hostInternals.camera.position.set(0, 1.6, 0);

    hostInternals.updateActiveRuntimeTargetLockState(0.2);

    expect(hostInternals.activeRuntimeTargetReference).toEqual({
      kind: "npc",
      entityId: "npc-occluded-active"
    });

    hostInternals.updateActiveRuntimeTargetLockState(0.2);

    expect(hostInternals.activeRuntimeTargetReference).toBeNull();
    host.dispose();
  });

  it("clears runtime targeting when switching into first-person mode", () => {
    const host = new RuntimeHost({
      enableRendering: false
    });
    const hostInternals = host as unknown as {
      activeRuntimeTargetReference: {
        kind: "npc" | "interactable";
        entityId: string;
      } | null;
      proposedRuntimeTarget: unknown;
      runtimeTargetCandidates: unknown[];
    };

    hostInternals.activeRuntimeTargetReference = {
      kind: "npc",
      entityId: "npc-one"
    };
    hostInternals.proposedRuntimeTarget = {
      kind: "npc",
      entityId: "npc-one"
    };
    hostInternals.runtimeTargetCandidates = [{}];

    host.setNavigationMode("firstPerson");

    expect(hostInternals.activeRuntimeTargetReference).toBeNull();
    expect(hostInternals.proposedRuntimeTarget).toBeNull();
    expect(hostInternals.runtimeTargetCandidates).toEqual([]);
    host.dispose();
  });

  it("invalidates an active runtime target when it is no longer targetable", () => {
    const host = new RuntimeHost({
      enableRendering: false
    });
    const hostInternals = host as unknown as {
      runtimeScene: unknown;
      sceneReady: boolean;
      activeController: unknown;
      thirdPersonController: unknown;
      currentPlayerControllerTelemetry: unknown;
      activeRuntimeTargetReference: {
        kind: "npc" | "interactable";
        entityId: string;
      } | null;
      camera: PerspectiveCamera;
      refreshRuntimeTargetingState(): void;
    };

    hostInternals.runtimeScene = {
      entities: {
        interactables: [
          {
            entityId: "switch-one",
            position: { x: 0, y: 1, z: 2 },
            radius: 3,
            prompt: "Use",
            interactionEnabled: false
          }
        ],
        npcs: [],
        playerStarts: [],
        sceneEntries: [],
        cameraRigs: [],
        soundEmitters: [],
        triggerVolumes: [],
        teleportTargets: []
      },
      interactionLinks: [
        {
          id: "link-switch-one",
          sourceEntityId: "switch-one",
          trigger: "click",
          action: {
            type: "runSequence",
            sequenceId: "noop"
          }
        }
      ]
    } as never;
    hostInternals.sceneReady = true;
    hostInternals.activeController = hostInternals.thirdPersonController;
    hostInternals.currentPlayerControllerTelemetry = {
      eyePosition: { x: 0, y: 1.6, z: 0 }
    };
    hostInternals.activeRuntimeTargetReference = {
      kind: "interactable",
      entityId: "switch-one"
    };
    hostInternals.camera.position.set(0, 1.6, -2);
    hostInternals.camera.lookAt(0, 1, 2);

    hostInternals.refreshRuntimeTargetingState();

    expect(hostInternals.activeRuntimeTargetReference).toBeNull();
    host.dispose();
  });

  it("does not provide gameplay target camera assist while a camera rig is active", () => {
    const host = new RuntimeHost({
      enableRendering: false
    });
    const hostInternals = host as unknown as {
      runtimeScene: unknown;
      activeController: unknown;
      thirdPersonController: unknown;
      activeRuntimeTargetReference: {
        kind: "npc" | "interactable";
        entityId: string;
      } | null;
      resolveThirdPersonTargetAssist(): unknown;
    };

    hostInternals.runtimeScene = {
      entities: {
        cameraRigs: [
          {
            entityId: "camera-rig-default",
            defaultActive: true,
            priority: 1
          }
        ],
        interactables: [],
        npcs: [],
        playerStarts: [],
        sceneEntries: [],
        soundEmitters: [],
        triggerVolumes: [],
        teleportTargets: []
      },
      interactionLinks: []
    } as never;
    hostInternals.activeController = hostInternals.thirdPersonController;
    hostInternals.activeRuntimeTargetReference = {
      kind: "interactable",
      entityId: "switch-one"
    };

    expect(hostInternals.resolveThirdPersonTargetAssist()).toBeNull();
    host.dispose();
  });
});
