import { describe, expect, it } from "vitest";
import { AnimationClip, BoxGeometry, PlaneGeometry } from "three";

import {
  createActorControlTargetRef,
  createFollowActorPathControlEffect,
  createPlayActorAnimationControlEffect,
  createSetActorPresenceControlEffect
} from "../../src/controls/control-surface";
import { createBoxBrush } from "../../src/document/brushes";
import { createScenePath } from "../../src/document/paths";
import { createDefaultProjectTimeSettings } from "../../src/document/project-time-settings";
import { createTerrain } from "../../src/document/terrains";
import { createEmptySceneDocument } from "../../src/document/scene-document";
import {
  createCameraRigEntity,
  createCameraRigEntityTargetRef,
  DEFAULT_PLAYER_START_MOVE_SPEED,
  DEFAULT_PLAYER_START_MOVEMENT_CAPABILITIES,
  createNpcEntity,
  createPointLightEntity,
  createInteractableEntity,
  createPlayerStartMovementTemplate,
  createPlayerStartInputBindings,
  createPlayerStartEntity,
  createSceneEntryEntity,
  createSoundEmitterEntity,
  createSpotLightEntity,
  createTeleportTargetEntity,
  createTriggerVolumeEntity
} from "../../src/entities/entity-instances";
import {
  createRunSequenceInteractionLink,
  createTeleportPlayerInteractionLink,
  createToggleVisibilityInteractionLink
} from "../../src/interactions/interaction-links";
import { createProjectScheduleRoutine } from "../../src/scheduler/project-scheduler";
import { createProjectSequence } from "../../src/sequencer/project-sequences";
import { createModelInstance } from "../../src/assets/model-instances";
import { createProjectAssetStorageKey, type AudioAssetRecord } from "../../src/assets/project-assets";
import { buildRuntimeSceneFromDocument } from "../../src/runtime-three/runtime-scene-build";
import { createFixtureLoadedModelAssetFromGeometry } from "../helpers/model-collider-fixtures";

const defaultMovementTemplate = createPlayerStartMovementTemplate();

describe("buildRuntimeSceneFromDocument", () => {
  it("builds enabled fixed camera rigs into runtime entities", () => {
    const interactable = createInteractableEntity({
      id: "entity-interactable-camera-anchor",
      position: {
        x: 4,
        y: 1,
        z: -2
      },
      prompt: "Anchor"
    });
    const cameraRig = createCameraRigEntity({
      id: "entity-camera-rig-overlook",
      position: {
        x: 8,
        y: 3,
        z: -6
      },
      priority: 7,
      defaultActive: false,
      target: createCameraRigEntityTargetRef(interactable.id),
      targetOffset: {
        x: 0,
        y: 1.5,
        z: 0
      },
      transitionMode: "blend",
      transitionDurationSeconds: 0.6,
      lookAround: {
        enabled: true,
        yawLimitDegrees: 10,
        pitchLimitDegrees: 6,
        recenterSpeed: 5
      }
    });
    const disabledCameraRig = createCameraRigEntity({
      id: "entity-camera-rig-disabled",
      enabled: false
    });

    const runtimeScene = buildRuntimeSceneFromDocument({
      ...createEmptySceneDocument({ name: "Camera Rig Runtime Scene" }),
      entities: {
        [interactable.id]: interactable,
        [cameraRig.id]: cameraRig,
        [disabledCameraRig.id]: disabledCameraRig
      }
    });

    expect(runtimeScene.entities.cameraRigs).toEqual([
      {
        entityId: cameraRig.id,
        rigType: "fixed",
        priority: 7,
        defaultActive: false,
        position: {
          x: 8,
          y: 3,
          z: -6
        },
        target: {
          kind: "entity",
          entityId: interactable.id
        },
        targetOffset: {
          x: 0,
          y: 1.5,
          z: 0
        },
        transitionMode: "blend",
        transitionDurationSeconds: 0.6,
        lookAround: {
          enabled: true,
          yawLimitDegrees: 10,
          pitchLimitDegrees: 6,
          recenterSpeed: 5
        }
      }
    ]);
  });

  it("builds enabled rail camera rigs into runtime entities", () => {
    const interactable = createInteractableEntity({
      id: "entity-interactable-rail-anchor",
      position: {
        x: 3,
        y: 1,
        z: 2
      },
      prompt: "Anchor"
    });
    const path = createScenePath({
      id: "path-camera-rail",
      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",
      rigType: "rail",
      pathId: path.id,
      priority: 9,
      defaultActive: true,
      target: createCameraRigEntityTargetRef(interactable.id),
      targetOffset: {
        x: 0,
        y: 1.25,
        z: 0
      },
      transitionMode: "cut"
    });

    const runtimeScene = buildRuntimeSceneFromDocument({
      ...createEmptySceneDocument({ name: "Rail Camera Rig Runtime Scene" }),
      paths: {
        [path.id]: path
      },
      entities: {
        [interactable.id]: interactable,
        [cameraRig.id]: cameraRig
      }
    });

    expect(runtimeScene.entities.cameraRigs).toEqual([
      {
        entityId: cameraRig.id,
        rigType: "rail",
        priority: 9,
        defaultActive: true,
        pathId: path.id,
        railPlacementMode: "nearestToTarget",
        target: {
          kind: "entity",
          entityId: interactable.id
        },
        targetOffset: {
          x: 0,
          y: 1.25,
          z: 0
        },
        transitionMode: "cut",
        transitionDurationSeconds: 0.35,
        lookAround: {
          enabled: true,
          yawLimitDegrees: 12,
          pitchLimitDegrees: 8,
          recenterSpeed: 3.5
        }
      }
    ]);
  });

  it("builds mapped rail camera rigs into runtime entities", () => {
    const interactable = createInteractableEntity({
      id: "entity-interactable-mapped-rail-anchor",
      position: {
        x: 2,
        y: 1,
        z: 2
      },
      prompt: "Anchor"
    });
    const path = createScenePath({
      id: "path-camera-rail-mapped",
      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-mapped",
      rigType: "rail",
      pathId: path.id,
      railPlacementMode: "mapTargetBetweenPoints",
      trackStartPoint: {
        x: 0,
        y: 1,
        z: 2
      },
      trackEndPoint: {
        x: 10,
        y: 1,
        z: 2
      },
      railStartProgress: 0.2,
      railEndProgress: 0.8,
      target: createCameraRigEntityTargetRef(interactable.id),
      transitionMode: "blend",
      transitionDurationSeconds: 0.5
    });

    const runtimeScene = buildRuntimeSceneFromDocument({
      ...createEmptySceneDocument({ name: "Mapped Rail Camera Rig Runtime Scene" }),
      paths: {
        [path.id]: path
      },
      entities: {
        [interactable.id]: interactable,
        [cameraRig.id]: cameraRig
      }
    });

    expect(runtimeScene.entities.cameraRigs).toEqual([
      {
        entityId: cameraRig.id,
        rigType: "rail",
        priority: 0,
        defaultActive: true,
        pathId: path.id,
        railPlacementMode: "mapTargetBetweenPoints",
        trackStartPoint: {
          x: 0,
          y: 1,
          z: 2
        },
        trackEndPoint: {
          x: 10,
          y: 1,
          z: 2
        },
        railStartProgress: 0.2,
        railEndProgress: 0.8,
        target: {
          kind: "entity",
          entityId: interactable.id
        },
        targetOffset: {
          x: 0,
          y: 1.4,
          z: 0
        },
        transitionMode: "blend",
        transitionDurationSeconds: 0.5,
        lookAround: {
          enabled: true,
          yawLimitDegrees: 12,
          pitchLimitDegrees: 8,
          recenterSpeed: 3.5
        }
      }
    ]);
  });

  it("builds runtime brush data, colliders, and an authored player spawn from the document", () => {
    const brush = createBoxBrush({
      id: "brush-room-floor",
      center: {
        x: 0,
        y: -0.5,
        z: 0
      },
      size: {
        x: 8,
        y: 1,
        z: 8
      }
    });
    brush.faces.posY.materialId = "starter-concrete-checker";

    const playerStart = createPlayerStartEntity({
      id: "entity-player-start-main",
      position: {
        x: 2,
        y: 0,
        z: -1
      },
      yawDegrees: 90,
      collider: {
        mode: "box",
        eyeHeight: 1.4,
        capsuleRadius: 0.3,
        capsuleHeight: 1.8,
        boxSize: {
          x: 0.8,
          y: 1.6,
          z: 0.7
        }
      }
    });
    const soundEmitter = createSoundEmitterEntity({
      id: "entity-sound-lobby",
      position: {
        x: -1,
        y: 1,
        z: 0
      },
      audioAssetId: "asset-audio-lobby",
      volume: 0.75,
      refDistance: 8,
      maxDistance: 24,
      autoplay: true,
      loop: false
    });
    const triggerVolume = createTriggerVolumeEntity({
      id: "entity-trigger-door",
      position: {
        x: 0,
        y: 1,
        z: 2
      },
      size: {
        x: 2,
        y: 2,
        z: 1
      },
      triggerOnEnter: true,
      triggerOnExit: false
    });
    const teleportTarget = createTeleportTargetEntity({
      id: "entity-teleport-target-main",
      position: {
        x: 6,
        y: 0,
        z: -3
      },
      yawDegrees: 270
    });
    const interactable = createInteractableEntity({
      id: "entity-interactable-console",
      position: {
        x: 1,
        y: 1,
        z: 1
      },
      radius: 1.5,
      prompt: "Use Console",
      interactionEnabled: true
    });
    const sceneEntry = createSceneEntryEntity({
      id: "entity-scene-entry-house-front",
      position: {
        x: -3,
        y: 0,
        z: 2
      },
      yawDegrees: 135
    });
    const npc = createNpcEntity({
      id: "entity-npc-guide",
      actorId: "actor-house-guide",
      position: {
        x: -1,
        y: 0,
        z: -2
      },
      yawDegrees: 45,
      modelAssetId: "asset-model-triangle",
      dialogues: [
        {
          id: "dialogue-warning",
          title: "Warning",
          lines: [
            {
              id: "dialogue-line-warning-1",
              text: "The generator is unstable."
            }
          ]
        }
      ],
      defaultDialogueId: "dialogue-warning"
    });
    const pointLight = createPointLightEntity({
      id: "entity-point-light-main",
      position: {
        x: 2,
        y: 3,
        z: 1
      }
    });
    const spotLight = createSpotLightEntity({
      id: "entity-spot-light-main",
      position: {
        x: -2,
        y: 4,
        z: 0
      },
      direction: {
        x: 0.2,
        y: -1,
        z: 0.1
      }
    });
    const imageAsset = {
      id: "asset-background-panorama",
      kind: "image" as const,
      sourceName: "skybox-panorama.svg",
      mimeType: "image/svg+xml",
      storageKey: createProjectAssetStorageKey("asset-background-panorama"),
      byteLength: 2048,
      metadata: {
        kind: "image" as const,
        width: 512,
        height: 256,
        hasAlpha: false,
        warnings: []
      }
    };
    const audioAsset = {
      id: "asset-audio-lobby",
      kind: "audio" as const,
      sourceName: "lobby-loop.ogg",
      mimeType: "audio/ogg",
      storageKey: createProjectAssetStorageKey("asset-audio-lobby"),
      byteLength: 4096,
      metadata: {
        kind: "audio" as const,
        durationSeconds: 3.25,
        channelCount: 2,
        sampleRateHz: 48000,
        warnings: []
      }
    } satisfies AudioAssetRecord;
    const modelAsset = {
      id: "asset-model-triangle",
      kind: "model" as const,
      sourceName: "tiny-triangle.gltf",
      mimeType: "model/gltf+json",
      storageKey: createProjectAssetStorageKey("asset-model-triangle"),
      byteLength: 36,
      metadata: {
        kind: "model" as const,
        format: "gltf" as const,
        sceneName: "Fixture Triangle Scene",
        nodeCount: 2,
        meshCount: 1,
        materialNames: ["Fixture Material"],
        textureNames: [],
        animationNames: [],
        boundingBox: {
          min: {
            x: 0,
            y: 0,
            z: 0
          },
          max: {
            x: 1,
            y: 1,
            z: 0
          },
          size: {
            x: 1,
            y: 1,
            z: 0
          }
        },
        warnings: []
      }
    };
    const modelInstance = createModelInstance({
      id: "model-instance-triangle",
      assetId: modelAsset.id,
      position: {
        x: -2,
        y: 0,
        z: 4
      },
      rotationDegrees: {
        x: 0,
        y: 90,
        z: 0
      },
      scale: {
        x: 2,
        y: 2,
        z: 2
      }
    });
    const path = createScenePath({
      id: "path-lobby-route",
      name: "Lobby Route",
      points: [
        {
          id: "path-point-a",
          position: {
            x: -2,
            y: 0,
            z: -2
          }
        },
        {
          id: "path-point-b",
          position: {
            x: -2,
            y: 0,
            z: 1
          }
        },
        {
          id: "path-point-c",
          position: {
            x: 2,
            y: 0,
            z: 1
          }
        }
      ]
    });

    const document = {
      ...createEmptySceneDocument({ name: "Runtime Slice" }),
      brushes: {
        [brush.id]: brush
      },
      paths: {
        [path.id]: path
      },
      assets: {
        [audioAsset.id]: audioAsset,
        [modelAsset.id]: modelAsset,
        [imageAsset.id]: imageAsset
      },
      modelInstances: {
        [modelInstance.id]: modelInstance
      },
      entities: {
        [playerStart.id]: playerStart,
        [npc.id]: npc,
        [soundEmitter.id]: soundEmitter,
        [triggerVolume.id]: triggerVolume,
        [teleportTarget.id]: teleportTarget,
        [interactable.id]: interactable,
        [sceneEntry.id]: sceneEntry,
        [pointLight.id]: pointLight,
        [spotLight.id]: spotLight
      },
      interactionLinks: {
        "link-teleport": createTeleportPlayerInteractionLink({
          id: "link-teleport",
          sourceEntityId: triggerVolume.id,
          trigger: "enter",
          targetEntityId: teleportTarget.id
        }),
        "link-hide-brush": createToggleVisibilityInteractionLink({
          id: "link-hide-brush",
          sourceEntityId: triggerVolume.id,
          trigger: "exit",
          targetBrushId: brush.id,
          visible: false
        }),
        "link-click-teleport": createTeleportPlayerInteractionLink({
          id: "link-click-teleport",
          sourceEntityId: interactable.id,
          trigger: "click",
          targetEntityId: teleportTarget.id
        }),
        "link-interactable-dialogue": createRunSequenceInteractionLink({
          id: "link-interactable-dialogue",
          sourceEntityId: interactable.id,
          trigger: "click",
          sequenceId: "sequence-warning-dialogue"
        }),
        "link-trigger-dialogue": createRunSequenceInteractionLink({
          id: "link-trigger-dialogue",
          sourceEntityId: triggerVolume.id,
          trigger: "enter",
          sequenceId: "sequence-warning-dialogue"
        })
      }
    };
    document.sequences.sequences["sequence-warning-dialogue"] =
      createProjectSequence({
        id: "sequence-warning-dialogue",
        title: "Warning Dialogue",
        effects: [
          {
            stepClass: "impulse",
            type: "makeNpcTalk",
            npcEntityId: npc.id,
            dialogueId: "dialogue-warning"
          }
        ]
      });
    document.world.background = {
      mode: "image",
      assetId: imageAsset.id,
      environmentIntensity: 0.75
    };
    document.world.ambientLight.intensity = 0.55;
    document.world.sunLight.direction = {
      x: -0.8,
      y: 1.2,
      z: 0.1
    };
    document.time = {
      ...createDefaultProjectTimeSettings(),
      startTimeOfDayHours: 18.5,
      dayLengthMinutes: 16
    };

    const runtimeScene = buildRuntimeSceneFromDocument(document);

    expect(runtimeScene.time).toEqual(document.time);
    expect(runtimeScene.time).not.toBe(document.time);
    expect(runtimeScene.world).toEqual(document.world);
    expect(runtimeScene.world).not.toBe(document.world);
    expect(runtimeScene.world.sunLight.direction).not.toBe(document.world.sunLight.direction);
    expect(runtimeScene.brushes).toHaveLength(1);
    expect(runtimeScene.modelInstances).toEqual([
      {
        instanceId: "model-instance-triangle",
        assetId: "asset-model-triangle",
        name: undefined,
        visible: true,
        position: {
          x: -2,
          y: 0,
          z: 4
        },
        rotationDegrees: {
          x: 0,
          y: 90,
          z: 0
        },
        scale: {
          x: 2,
          y: 2,
          z: 2
        }
      }
    ]);
    expect(runtimeScene.paths).toEqual([
      {
        id: "path-lobby-route",
        name: "Lobby Route",
        visible: true,
        enabled: true,
        loop: false,
        points: [
          {
            pointId: "path-point-a",
            position: {
              x: -2,
              y: 0,
              z: -2
            }
          },
          {
            pointId: "path-point-b",
            position: {
              x: -2,
              y: 0,
              z: 1
            }
          },
          {
            pointId: "path-point-c",
            position: {
              x: 2,
              y: 0,
              z: 1
            }
          }
        ],
        segments: [
          {
            index: 0,
            startPointId: "path-point-a",
            endPointId: "path-point-b",
            start: {
              x: -2,
              y: 0,
              z: -2
            },
            end: {
              x: -2,
              y: 0,
              z: 1
            },
            length: 3,
            distanceStart: 0,
            distanceEnd: 3,
            tangent: {
              x: 0,
              y: 0,
              z: 1
            }
          },
          {
            index: 1,
            startPointId: "path-point-b",
            endPointId: "path-point-c",
            start: {
              x: -2,
              y: 0,
              z: 1
            },
            end: {
              x: 2,
              y: 0,
              z: 1
            },
            length: 4,
            distanceStart: 3,
            distanceEnd: 7,
            tangent: {
              x: 1,
              y: 0,
              z: 0
            }
          }
        ],
        totalLength: 7
      }
    ]);
    expect(runtimeScene.brushes[0].rotationDegrees).toEqual({
      x: 0,
      y: 0,
      z: 0
    });
    expect(runtimeScene.brushes[0].faces.posY.material?.id).toBe("starter-concrete-checker");
    expect(runtimeScene.colliders).toHaveLength(2);
    expect(runtimeScene.colliders[0]).toMatchObject({
      kind: "trimesh",
      source: "brush",
      brushId: "brush-room-floor",
      center: {
        x: 0,
        y: -0.5,
        z: 0
      },
      rotationDegrees: {
        x: 0,
        y: 0,
        z: 0
      },
      worldBounds: {
        min: {
          x: -4,
          y: -1,
          z: -4
        },
        max: {
          x: 4,
          y: 0,
          z: 4
        }
      }
    });
    const brushCollider = runtimeScene.colliders[0];
    if (brushCollider.kind !== "trimesh") {
      throw new Error(`Expected a trimesh brush collider, received ${brushCollider.kind}.`);
    }
    expect(Array.from(brushCollider.vertices)).toHaveLength(24);
    expect(Array.from(brushCollider.indices)).toHaveLength(36);
    expect(runtimeScene.sceneBounds).toEqual({
      min: {
        x: -4,
        y: -1,
        z: -4
      },
      max: {
        x: 4,
        y: 1.8,
        z: 4
      },
      center: {
        x: 0,
        y: 0.4,
        z: 0
      },
      size: {
        x: 8,
        y: 2.8,
        z: 8
      }
    });
    expect(runtimeScene.entities).toEqual({
      playerStarts: [
        {
          entityId: "entity-player-start-main",
          position: {
            x: 2,
            y: 0,
            z: -1
          },
          yawDegrees: 90,
          navigationMode: "firstPerson",
          movement: {
            templateKind: "default",
            moveSpeed: DEFAULT_PLAYER_START_MOVE_SPEED,
            maxSpeed: defaultMovementTemplate.maxSpeed,
            maxStepHeight: defaultMovementTemplate.maxStepHeight,
            capabilities: DEFAULT_PLAYER_START_MOVEMENT_CAPABILITIES,
            jump: defaultMovementTemplate.jump,
            sprint: defaultMovementTemplate.sprint,
            crouch: defaultMovementTemplate.crouch
          },
          inputBindings: playerStart.inputBindings,
          collider: {
            mode: "box",
            eyeHeight: 1.4,
            size: {
              x: 0.8,
              y: 1.6,
              z: 0.7
            }
          }
        }
      ],
      cameraRigs: [],
      sceneEntries: [
        {
          entityId: "entity-scene-entry-house-front",
          position: {
            x: -3,
            y: 0,
            z: 2
          },
          yawDegrees: 135
        }
      ],
      npcs: [
        {
          entityId: "entity-npc-guide",
          actorId: "actor-house-guide",
          name: undefined,
          visible: true,
          position: {
            x: -1,
            y: 0,
            z: -2
          },
          yawDegrees: 45,
          modelAssetId: modelAsset.id,
          dialogues: [
            {
              id: "dialogue-warning",
              title: "Warning",
              lines: [
                {
                  id: "dialogue-line-warning-1",
                  text: "The generator is unstable."
                }
              ]
            }
          ],
          defaultDialogueId: "dialogue-warning",
          activeRoutineTitle: null,
          animationClipName: null,
          animationLoop: undefined,
          resolvedPath: null,
          collider: {
            mode: "capsule",
            radius: 0.3,
            height: 1.8,
            eyeHeight: 1.6
          }
        }
      ],
      soundEmitters: [
        {
          entityId: "entity-sound-lobby",
          position: {
            x: -1,
            y: 1,
            z: 0
          },
          audioAssetId: audioAsset.id,
          volume: 0.75,
          refDistance: 8,
          maxDistance: 24,
          autoplay: true,
          loop: false
        }
      ],
      triggerVolumes: [
        {
          entityId: "entity-trigger-door",
          position: {
            x: 0,
            y: 1,
            z: 2
          },
          size: {
            x: 2,
            y: 2,
            z: 1
          },
          triggerOnEnter: true,
          triggerOnExit: true
        }
      ],
      teleportTargets: [
        {
          entityId: "entity-teleport-target-main",
          position: {
            x: 6,
            y: 0,
            z: -3
          },
          yawDegrees: 270
        }
      ],
      interactables: [
        {
          entityId: "entity-interactable-console",
          position: {
            x: 1,
            y: 1,
            z: 1
          },
          radius: 1.5,
          prompt: "Use Console",
          interactionEnabled: true
        }
      ]
    });
    expect(runtimeScene.localLights).toEqual({
      pointLights: [
        {
          entityId: "entity-point-light-main",
          enabled: true,
          position: {
            x: 2,
            y: 3,
            z: 1
          },
          colorHex: "#ffffff",
          intensity: 1.25,
          distance: 8
        }
      ],
      spotLights: [
        {
          entityId: "entity-spot-light-main",
          enabled: true,
          position: {
            x: -2,
            y: 4,
            z: 0
          },
          direction: {
            x: 0.2,
            y: -1,
            z: 0.1
          },
          colorHex: "#ffffff",
          intensity: 1.5,
          distance: 12,
          angleDegrees: 35
        }
      ]
    });
    expect(runtimeScene.interactionLinks).toEqual([
      {
        id: "link-click-teleport",
        sourceEntityId: "entity-interactable-console",
        trigger: "click",
        action: {
          type: "teleportPlayer",
          targetEntityId: "entity-teleport-target-main"
        }
      },
      {
        id: "link-interactable-dialogue",
        sourceEntityId: "entity-interactable-console",
        trigger: "click",
        action: {
          type: "runSequence",
          sequenceId: "sequence-warning-dialogue"
        }
      },
      {
        id: "link-teleport",
        sourceEntityId: "entity-trigger-door",
        trigger: "enter",
        action: {
          type: "teleportPlayer",
          targetEntityId: "entity-teleport-target-main"
        }
      },
      {
        id: "link-trigger-dialogue",
        sourceEntityId: "entity-trigger-door",
        trigger: "enter",
        action: {
          type: "runSequence",
          sequenceId: "sequence-warning-dialogue"
        }
      },
      {
        id: "link-hide-brush",
        sourceEntityId: "entity-trigger-door",
        trigger: "exit",
        action: {
          type: "toggleVisibility",
          targetBrushId: "brush-room-floor",
          visible: false
        }
      }
    ]);
    expect(runtimeScene.playerStart).toEqual({
      entityId: "entity-player-start-main",
      position: {
        x: 2,
        y: 0,
        z: -1
      },
      yawDegrees: 90,
      navigationMode: "firstPerson",
      movement: {
        templateKind: "default",
        moveSpeed: DEFAULT_PLAYER_START_MOVE_SPEED,
        maxSpeed: defaultMovementTemplate.maxSpeed,
        maxStepHeight: defaultMovementTemplate.maxStepHeight,
        capabilities: DEFAULT_PLAYER_START_MOVEMENT_CAPABILITIES,
        jump: defaultMovementTemplate.jump,
        sprint: defaultMovementTemplate.sprint,
        crouch: defaultMovementTemplate.crouch
      },
      inputBindings: playerStart.inputBindings,
      collider: {
        mode: "box",
        eyeHeight: 1.4,
        size: {
          x: 0.8,
          y: 1.6,
          z: 0.7
        }
      }
    });
    expect(runtimeScene.colliders[1]).toMatchObject({
      kind: "character",
      source: "npc",
      entityId: "entity-npc-guide",
      position: {
        x: -1,
        y: 0,
        z: -2
      },
      rotationDegrees: {
        x: 0,
        y: 45,
        z: 0
      },
      shape: {
        mode: "capsule",
        radius: 0.3,
        height: 1.8,
        eyeHeight: 1.6
      },
      worldBounds: {
        min: {
          x: -1.3,
          y: 0,
          z: -2.3
        },
        max: {
          x: -0.7,
          y: 1.8,
          z: -1.7
        }
      }
    });
    expect(runtimeScene.playerCollider).toEqual({
      mode: "box",
      eyeHeight: 1.4,
      size: {
        x: 0.8,
        y: 1.6,
        z: 0.7
      }
    });
    expect(runtimeScene.playerMovement).toEqual({
      templateKind: "default",
      moveSpeed: DEFAULT_PLAYER_START_MOVE_SPEED,
      maxSpeed: defaultMovementTemplate.maxSpeed,
      maxStepHeight: defaultMovementTemplate.maxStepHeight,
      capabilities: DEFAULT_PLAYER_START_MOVEMENT_CAPABILITIES,
      jump: defaultMovementTemplate.jump,
      sprint: defaultMovementTemplate.sprint,
      crouch: defaultMovementTemplate.crouch
    });
    expect(runtimeScene.playerInputBindings).toEqual(playerStart.inputBindings);
    expect(runtimeScene.navigationMode).toBe("firstPerson");
    expect(runtimeScene.spawn).toEqual({
      source: "playerStart",
      entityId: "entity-player-start-main",
      position: {
        x: 2,
        y: 0,
        z: -1
      },
      yawDegrees: 90
    });
  });

  it("filters active NPCs from the project scheduler against the current runtime clock", () => {
    const alwaysNpc = createNpcEntity({
      id: "entity-npc-always",
      actorId: "actor-town-always"
    });
    const daytimeNpc = createNpcEntity({
      id: "entity-npc-daytime",
      actorId: "actor-town-daytime"
    });
    const overnightNpc = createNpcEntity({
      id: "entity-npc-overnight",
      actorId: "actor-town-overnight"
    });
    const document = {
      ...createEmptySceneDocument({ name: "NPC Presence Scene" }),
      entities: {
        [alwaysNpc.id]: alwaysNpc,
        [daytimeNpc.id]: daytimeNpc,
        [overnightNpc.id]: overnightNpc
      }
    };
    document.scheduler.routines["routine-daytime"] = createProjectScheduleRoutine({
      id: "routine-daytime",
      title: "Day Shift",
      target: createActorControlTargetRef(daytimeNpc.actorId),
      startHour: 9,
      endHour: 17,
      effect: createSetActorPresenceControlEffect({
        target: createActorControlTargetRef(daytimeNpc.actorId),
        active: true
      })
    });
    document.scheduler.routines["routine-overnight"] = createProjectScheduleRoutine({
      id: "routine-overnight",
      title: "Night Shift",
      target: createActorControlTargetRef(overnightNpc.actorId),
      startHour: 22,
      endHour: 2,
      effect: createSetActorPresenceControlEffect({
        target: createActorControlTargetRef(overnightNpc.actorId),
        active: true
      })
    });

    const daytimeRuntimeScene = buildRuntimeSceneFromDocument(document, {
      runtimeClock: {
        timeOfDayHours: 10,
        dayCount: 2,
        dayLengthMinutes: 24
      }
    });
    const overnightRuntimeScene = buildRuntimeSceneFromDocument(document, {
      runtimeClock: {
        timeOfDayHours: 23.5,
        dayCount: 2,
        dayLengthMinutes: 24
      }
    });

    expect(
      daytimeRuntimeScene.entities.npcs.map((npc) => npc.entityId)
    ).toEqual([
      "entity-npc-always",
      "entity-npc-daytime",
      "entity-npc-overnight"
    ]);
    expect(
      daytimeRuntimeScene.npcDefinitions.map((npc) => ({
        entityId: npc.entityId,
        active: npc.active
      }))
    ).toEqual([
      {
        entityId: "entity-npc-always",
        active: true
      },
      {
        entityId: "entity-npc-daytime",
        active: true
      },
      {
        entityId: "entity-npc-overnight",
        active: true
      }
    ]);
    expect(
      daytimeRuntimeScene.colliders
        .filter((collider) => collider.source === "npc")
        .map((collider) => collider.entityId)
    ).toEqual([
      "entity-npc-always",
      "entity-npc-daytime",
      "entity-npc-overnight"
    ]);

    expect(
      overnightRuntimeScene.entities.npcs.map((npc) => npc.entityId)
    ).toEqual([
      "entity-npc-always",
      "entity-npc-daytime",
      "entity-npc-overnight"
    ]);
    expect(
      overnightRuntimeScene.colliders
        .filter((collider) => collider.source === "npc")
        .map((collider) => collider.entityId)
    ).toEqual([
      "entity-npc-always",
      "entity-npc-daytime",
      "entity-npc-overnight"
    ]);
    expect(
      overnightRuntimeScene.entities.npcs.find(
        (npc) => npc.entityId === overnightNpc.id
      )
    ).toEqual(
      expect.objectContaining({
        activeRoutineTitle: "Night Shift"
      })
    );
  });

  it("builds a deterministic fallback spawn when no PlayerStart is authored", () => {
    const brush = createBoxBrush({
      id: "brush-room-wall",
      center: {
        x: 0,
        y: 1,
        z: 0
      },
      size: {
        x: 6,
        y: 2,
        z: 6
      }
    });

    const runtimeScene = buildRuntimeSceneFromDocument({
      ...createEmptySceneDocument({ name: "Fallback Runtime Scene" }),
      brushes: {
        [brush.id]: brush
      }
    });

    expect(runtimeScene.playerStart).toBeNull();
    expect(runtimeScene.playerCollider).toEqual({
      mode: "capsule",
      radius: 0.3,
      height: 1.8,
      eyeHeight: 1.6
    });
    expect(runtimeScene.playerMovement).toEqual({
      templateKind: "default",
      moveSpeed: DEFAULT_PLAYER_START_MOVE_SPEED,
      maxSpeed: defaultMovementTemplate.maxSpeed,
      maxStepHeight: defaultMovementTemplate.maxStepHeight,
      capabilities: DEFAULT_PLAYER_START_MOVEMENT_CAPABILITIES,
      jump: defaultMovementTemplate.jump,
      sprint: defaultMovementTemplate.sprint,
      crouch: defaultMovementTemplate.crouch
    });
    expect(runtimeScene.playerInputBindings).toEqual(
      createPlayerStartInputBindings()
    );
    expect(runtimeScene.navigationMode).toBe("thirdPerson");
    expect(runtimeScene.entities).toEqual({
      playerStarts: [],
      cameraRigs: [],
      sceneEntries: [],
      npcs: [],
      soundEmitters: [],
      triggerVolumes: [],
      teleportTargets: [],
      interactables: []
    });
    expect(runtimeScene.interactionLinks).toEqual([]);
    expect(runtimeScene.spawn).toEqual({
      source: "fallback",
      entityId: null,
      position: {
        x: 0,
        y: 2.1,
        z: 6
      },
      yawDegrees: 180
    });
  });

  it("blocks first-person runtime builds when PlayerStart is missing", () => {
    expect(() =>
      buildRuntimeSceneFromDocument(createEmptySceneDocument({ name: "Missing Player Start" }), {
        navigationMode: "firstPerson"
      })
    ).toThrow("First-person run requires an authored Player Start");
  });

  it("uses the authored Player Start navigation mode for third-person scenes", () => {
    const playerStart = createPlayerStartEntity({
      id: "entity-player-start-third-person",
      navigationMode: "thirdPerson"
    });

    const runtimeScene = buildRuntimeSceneFromDocument({
      ...createEmptySceneDocument({ name: "Authored Third Person Scene" }),
      entities: {
        [playerStart.id]: playerStart
      }
    });

    expect(runtimeScene.navigationMode).toBe("thirdPerson");
    expect(runtimeScene.playerStart?.navigationMode).toBe("thirdPerson");
    expect(runtimeScene.entities.playerStarts[0]?.navigationMode).toBe(
      "thirdPerson"
    );
  });

  it("keeps hidden authored objects active but excludes disabled authored objects from the runtime build", () => {
    const hiddenBrush = createBoxBrush({
      id: "brush-hidden-runtime",
      visible: false
    });
    const disabledBrush = createBoxBrush({
      id: "brush-disabled-runtime",
      enabled: false
    });
    const { asset, loadedAsset } = createFixtureLoadedModelAssetFromGeometry(
      "asset-authored-state-runtime",
      new BoxGeometry(1, 1, 1)
    );
    const hiddenModelInstance = createModelInstance({
      id: "model-instance-hidden-runtime",
      assetId: asset.id,
      visible: false,
      collision: {
        mode: "static",
        visible: false
      }
    });
    const disabledModelInstance = createModelInstance({
      id: "model-instance-disabled-runtime",
      assetId: asset.id,
      enabled: false,
      collision: {
        mode: "static",
        visible: false
      }
    });
    const disabledPlayerStart = createPlayerStartEntity({
      id: "entity-player-start-disabled-runtime",
      enabled: false,
      position: {
        x: -4,
        y: 0,
        z: 0
      }
    });
    const enabledPlayerStart = createPlayerStartEntity({
      id: "entity-player-start-enabled-runtime",
      position: {
        x: 4,
        y: 0,
        z: 0
      }
    });
    const hiddenInteractable = createInteractableEntity({
      id: "entity-interactable-hidden-runtime",
      visible: false,
      interactionEnabled: true,
      prompt: "Hidden but active"
    });
    const disabledInteractable = createInteractableEntity({
      id: "entity-interactable-disabled-runtime",
      enabled: false,
      interactionEnabled: true,
      prompt: "Disabled"
    });
    const teleportTarget = createTeleportTargetEntity({
      id: "entity-teleport-target-runtime",
      position: {
        x: 12,
        y: 0,
        z: 0
      }
    });

    const runtimeScene = buildRuntimeSceneFromDocument(
      {
        ...createEmptySceneDocument({ name: "Authored State Runtime Scene" }),
        assets: {
          [asset.id]: asset
        },
        brushes: {
          [hiddenBrush.id]: hiddenBrush,
          [disabledBrush.id]: disabledBrush
        },
        modelInstances: {
          [hiddenModelInstance.id]: hiddenModelInstance,
          [disabledModelInstance.id]: disabledModelInstance
        },
        entities: {
          [disabledPlayerStart.id]: disabledPlayerStart,
          [enabledPlayerStart.id]: enabledPlayerStart,
          [hiddenInteractable.id]: hiddenInteractable,
          [disabledInteractable.id]: disabledInteractable,
          [teleportTarget.id]: teleportTarget
        },
        interactionLinks: {
          "link-hidden-click": createTeleportPlayerInteractionLink({
            id: "link-hidden-click",
            sourceEntityId: hiddenInteractable.id,
            trigger: "click",
            targetEntityId: teleportTarget.id
          }),
          "link-disabled-click": createTeleportPlayerInteractionLink({
            id: "link-disabled-click",
            sourceEntityId: disabledInteractable.id,
            trigger: "click",
            targetEntityId: teleportTarget.id
          })
        }
      },
      {
        navigationMode: "firstPerson",
        loadedModelAssets: {
          [asset.id]: loadedAsset
        }
      }
    );

    expect(runtimeScene.playerStart?.entityId).toBe(enabledPlayerStart.id);
    expect(runtimeScene.brushes.map((brush) => brush.id)).toEqual([hiddenBrush.id]);
    expect(runtimeScene.brushes[0]?.visible).toBe(false);
    expect(runtimeScene.modelInstances).toEqual([
      expect.objectContaining({
        instanceId: hiddenModelInstance.id,
        assetId: asset.id,
        visible: false
      })
    ]);
    expect(
      runtimeScene.entities.interactables.map((entity) => entity.entityId)
    ).toEqual([hiddenInteractable.id]);
    expect(runtimeScene.colliders).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          source: "brush",
          brushId: hiddenBrush.id
        }),
        expect.objectContaining({
          source: "modelInstance",
          instanceId: hiddenModelInstance.id,
          assetId: asset.id
        })
      ])
    );
    expect(runtimeScene.interactionLinks.map((link) => link.id)).toEqual([
      "link-hidden-click"
    ]);
  });

  it("uses a requested Scene Entry as the runtime spawn without replacing the Player Start collider", () => {
    const playerStart = createPlayerStartEntity({
      id: "entity-player-start-main",
      position: {
        x: 0,
        y: 0,
        z: 0
      },
      yawDegrees: 90
    });
    const sceneEntry = createSceneEntryEntity({
      id: "entity-scene-entry-basement-door",
      position: {
        x: 4,
        y: 0,
        z: -2
      },
      yawDegrees: 270
    });

    const runtimeScene = buildRuntimeSceneFromDocument(
      {
        ...createEmptySceneDocument({ name: "Scene Entry Spawn" }),
        entities: {
          [playerStart.id]: playerStart,
          [sceneEntry.id]: sceneEntry
        }
      },
      {
        navigationMode: "firstPerson",
        sceneEntryId: sceneEntry.id
      }
    );

    expect(runtimeScene.playerStart?.entityId).toBe(playerStart.id);
    expect(runtimeScene.spawn).toEqual({
      source: "sceneEntry",
      entityId: sceneEntry.id,
      position: {
        x: 4,
        y: 0,
        z: -2
      },
      yawDegrees: 270
    });
  });

  it("adds generated imported-model colliders to the runtime scene build", () => {
    const floorBrush = createBoxBrush({
      id: "brush-runtime-floor",
      center: {
        x: 0,
        y: -0.5,
        z: 0
      },
      size: {
        x: 8,
        y: 1,
        z: 8
      }
    });
    const { asset, loadedAsset } = createFixtureLoadedModelAssetFromGeometry("asset-runtime-collider", new BoxGeometry(1, 2, 1));
    const modelInstance = createModelInstance({
      id: "model-instance-runtime-collider",
      assetId: asset.id,
      position: {
        x: 2,
        y: 1,
        z: 0
      },
      collision: {
        mode: "static",
        visible: true
      }
    });

    const runtimeScene = buildRuntimeSceneFromDocument(
      {
        ...createEmptySceneDocument({ name: "Imported Collider Scene" }),
        assets: {
          [asset.id]: asset
        },
        brushes: {
          [floorBrush.id]: floorBrush
        },
        modelInstances: {
          [modelInstance.id]: modelInstance
        }
      },
      {
        loadedModelAssets: {
          [asset.id]: loadedAsset
        }
      }
    );

    expect(runtimeScene.colliders).toHaveLength(2);
    expect(runtimeScene.colliders[1]).toMatchObject({
      source: "modelInstance",
      instanceId: modelInstance.id,
      assetId: asset.id,
      kind: "trimesh",
      mode: "static",
      visible: true
    });
    expect(runtimeScene.sceneBounds?.max.y).toBeGreaterThanOrEqual(2);
  });

  it("adds authored terrain heightfield colliders to the runtime scene build", () => {
    const terrain = createTerrain({
      id: "terrain-runtime-heightfield",
      position: {
        x: -4,
        y: 0,
        z: -4
      },
      sampleCountX: 3,
      sampleCountZ: 3,
      cellSize: 4,
      heights: [0, 1, 0, 0, 2, 0, 0, 1, 0]
    });

    const runtimeScene = buildRuntimeSceneFromDocument({
      ...createEmptySceneDocument({ name: "Authored Terrain Collider Scene" }),
      terrains: {
        [terrain.id]: terrain
      }
    });

    expect(runtimeScene.terrains[0]).toMatchObject({
      id: terrain.id,
      collisionEnabled: true
    });
    expect(runtimeScene.colliders[0]).toMatchObject({
      source: "terrain",
      terrainId: terrain.id,
      kind: "heightfield",
      rows: 3,
      cols: 3,
      minX: 0,
      maxX: 8,
      minZ: 0,
      maxZ: 8
    });

    if (
      runtimeScene.colliders[0] === undefined ||
      runtimeScene.colliders[0].source !== "terrain"
    ) {
      throw new Error("Expected the runtime collider to be an authored terrain heightfield.");
    }

    expect(Array.from(runtimeScene.colliders[0].heights)).toEqual(
      terrain.heights
    );
    expect(runtimeScene.sceneBounds?.max.y).toBe(2);
  });

  it("adds static-simple imported-model colliders as compound box pieces", () => {
    const wallGeometry = new PlaneGeometry(4, 4, 4, 4);
    wallGeometry.rotateY(Math.PI * 0.5);
    const { asset, loadedAsset } = createFixtureLoadedModelAssetFromGeometry("asset-runtime-static-simple", wallGeometry);
    const modelInstance = createModelInstance({
      id: "model-instance-runtime-static-simple",
      assetId: asset.id,
      position: {
        x: 2,
        y: 2,
        z: 0
      },
      collision: {
        mode: "static-simple",
        visible: true
      }
    });

    const runtimeScene = buildRuntimeSceneFromDocument(
      {
        ...createEmptySceneDocument({ name: "Imported Static Simple Collider Scene" }),
        assets: {
          [asset.id]: asset
        },
        modelInstances: {
          [modelInstance.id]: modelInstance
        }
      },
      {
        loadedModelAssets: {
          [asset.id]: loadedAsset
        }
      }
    );

    expect(runtimeScene.colliders).toHaveLength(1);
    expect(runtimeScene.colliders[0]).toMatchObject({
      source: "modelInstance",
      instanceId: modelInstance.id,
      assetId: asset.id,
      kind: "compound",
      mode: "static-simple",
      visible: true,
      decomposition: "surface-voxel-boxes"
    });

    if (runtimeScene.colliders[0].source !== "modelInstance" || runtimeScene.colliders[0].kind !== "compound") {
      throw new Error("Expected the runtime collider to be a generated compound model collider.");
    }

    expect(runtimeScene.colliders[0].pieces.every((piece) => piece.kind === "box")).toBe(true);
  });

  it("preserves rotated whitebox box transforms for runner rendering and collision bounds", () => {
    const brush = createBoxBrush({
      id: "brush-rotated-room",
      center: {
        x: 1.25,
        y: 1.5,
        z: -0.75
      },
      rotationDegrees: {
        x: 0,
        y: 45,
        z: 0
      },
      size: {
        x: 2,
        y: 2,
        z: 4
      }
    });
    const runtimeScene = buildRuntimeSceneFromDocument({
      ...createEmptySceneDocument({ name: "Rotated Whitebox Scene" }),
      brushes: {
        [brush.id]: brush
      }
    });

    expect(runtimeScene.brushes[0]).toMatchObject({
      center: brush.center,
      rotationDegrees: brush.rotationDegrees,
      size: brush.size
    });
    expect(runtimeScene.colliders[0]).toMatchObject({
      kind: "trimesh",
      source: "brush",
      brushId: brush.id,
      center: brush.center,
      rotationDegrees: brush.rotationDegrees
    });
    expect(runtimeScene.sceneBounds?.min.x).toBeCloseTo(-0.8713203436);
    expect(runtimeScene.sceneBounds?.max.x).toBeCloseTo(3.3713203436);
    expect(runtimeScene.sceneBounds?.min.z).toBeCloseTo(-2.8713203436);
    expect(runtimeScene.sceneBounds?.max.z).toBeCloseTo(1.3713203436);
  });

  it("builds non-blocking water, fog, and light volumes from whitebox boxes", () => {
    const solidBrush = createBoxBrush({
      id: "brush-solid",
      center: {
        x: 0,
        y: 0,
        z: 0
      },
      size: {
        x: 6,
        y: 1,
        z: 6
      }
    });
    const waterBrush = createBoxBrush({
      id: "brush-water",
      center: {
        x: 2,
        y: 0.5,
        z: 1
      },
      size: {
        x: 3,
        y: 2,
        z: 3
      },
      volume: {
        mode: "water",
        water: {
          colorHex: "#2f79c4",
          surfaceOpacity: 0.7,
          waveStrength: 0.35,
          foamContactLimit: 6,
          surfaceDisplacementEnabled: false
        }
      }
    });
    const fogBrush = createBoxBrush({
      id: "brush-fog",
      center: {
        x: -2,
        y: 1,
        z: -1
      },
      size: {
        x: 4,
        y: 3,
        z: 2
      },
      volume: {
        mode: "fog",
        fog: {
          colorHex: "#99aac4",
          density: 0.55,
          padding: 0.25
        }
      }
    });
    const lightBrush = createBoxBrush({
      id: "brush-light",
      center: {
        x: 0,
        y: 2,
        z: -3
      },
      size: {
        x: 5,
        y: 4,
        z: 3
      },
      volume: {
        mode: "light",
        light: {
          colorHex: "#ffe0b6",
          intensity: 2.25,
          padding: 0.5,
          falloff: "smoothstep"
        }
      }
    });

    const runtimeScene = buildRuntimeSceneFromDocument({
      ...createEmptySceneDocument({ name: "Volume Runtime Scene" }),
      brushes: {
        [solidBrush.id]: solidBrush,
        [waterBrush.id]: waterBrush,
        [fogBrush.id]: fogBrush,
        [lightBrush.id]: lightBrush
      }
    });

    expect(runtimeScene.brushes).toHaveLength(4);
    expect(runtimeScene.colliders).toHaveLength(1);
    expect(runtimeScene.colliders[0]).toMatchObject({
      source: "brush",
      brushId: solidBrush.id
    });
    expect(runtimeScene.volumes.water).toEqual([
      {
        brushId: waterBrush.id,
        center: waterBrush.center,
        rotationDegrees: waterBrush.rotationDegrees,
        size: waterBrush.size,
        colorHex: "#2f79c4",
        surfaceOpacity: 0.7,
        waveStrength: 0.35
      }
    ]);
    expect(runtimeScene.volumes.fog).toEqual([
      {
        brushId: fogBrush.id,
        center: fogBrush.center,
        rotationDegrees: fogBrush.rotationDegrees,
        size: fogBrush.size,
        colorHex: "#99aac4",
        density: 0.55,
        padding: 0.25
      }
    ]);
    expect(runtimeScene.volumes.light).toEqual([
      expect.objectContaining({
        brushId: lightBrush.id,
        enabled: true,
        center: lightBrush.center,
        rotationDegrees: lightBrush.rotationDegrees,
        size: lightBrush.size,
        colorHex: "#ffe0b6",
        intensity: 2.25,
        padding: 0.5,
        falloff: "smoothstep",
        lights: expect.arrayContaining([
          expect.objectContaining({
            intensity: expect.any(Number),
            distance: expect.any(Number),
            decay: 2
          })
        ])
      })
    ]);
    expect(runtimeScene.volumes.light[0]?.lights).toHaveLength(4);
  });

  it("resolves active actor routines into NPC animation and deterministic follow-path pose", () => {
    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,
      dialogues: [
        {
          id: "dialogue-patrol",
          title: "Patrol",
          lines: [
            {
              id: "dialogue-line-patrol-1",
              text: "All clear."
            }
          ]
        }
      ],
      defaultDialogueId: "dialogue-patrol",
      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: 11,
        dayCount: 0,
        dayLengthMinutes: 24
      },
      loadedModelAssets: {
        [asset.id]: loadedAsset
      }
    });

    expect(runtimeScene.npcDefinitions[0]).toEqual(
      expect.objectContaining({
        entityId: npc.id,
        active: true,
        activeRoutineTitle: "Patrolling",
        animationClipName: "Walk",
        yawDegrees: 90,
        position: {
          x: 4,
          y: 0,
          z: 0
        },
        resolvedPath: expect.objectContaining({
          pathId: path.id,
          progress: 0.5,
          elapsedHours: 2,
          yawDegrees: 90
        })
      })
    );
    expect(runtimeScene.entities.npcs).toEqual([
      expect.objectContaining({
        entityId: npc.id,
        animationClipName: "Walk",
        defaultDialogueId: "dialogue-patrol",
        dialogues: expect.arrayContaining([
          expect.objectContaining({
            id: "dialogue-patrol"
          })
        ]),
        position: {
          x: 4,
          y: 0,
          z: 0
        },
        activeRoutineTitle: "Patrolling",
        resolvedPath: expect.objectContaining({
          pathId: path.id,
          progress: 0.5
        })
      })
    ]);
  });
});
