From 10bee5ef4c79f36c6b2636699e6d9c1020bcea18 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Mon, 27 Apr 2026 17:23:43 +0200 Subject: [PATCH] Enhance scene document validation with project-level checks and add mouse invert setting --- src/document/migrate-scene-document.ts | 1 + .../domain/scene-document-validation.test.ts | 131 +++++++++++++++++- tests/unit/runtime-host.test.ts | 5 +- 3 files changed, 134 insertions(+), 3 deletions(-) diff --git a/src/document/migrate-scene-document.ts b/src/document/migrate-scene-document.ts index 91c5e328..5bb59a53 100644 --- a/src/document/migrate-scene-document.ts +++ b/src/document/migrate-scene-document.ts @@ -198,6 +198,7 @@ import { PLAYER_START_GAMEPAD_CAMERA_LOOK_SCENE_DOCUMENT_VERSION, PLAYER_START_INTERACT_BINDINGS_SCENE_DOCUMENT_VERSION, PLAYER_START_INPUT_BINDINGS_SCENE_DOCUMENT_VERSION, + PLAYER_START_MOUSE_INVERT_SCENE_DOCUMENT_VERSION, PLAYER_START_TARGETING_SETTINGS_SCENE_DOCUMENT_VERSION, PLAYER_START_NAVIGATION_MODE_SCENE_DOCUMENT_VERSION, PLAYER_START_PAUSE_BINDINGS_SCENE_DOCUMENT_VERSION, diff --git a/tests/domain/scene-document-validation.test.ts b/tests/domain/scene-document-validation.test.ts index 2397b647..583739a1 100644 --- a/tests/domain/scene-document-validation.test.ts +++ b/tests/domain/scene-document-validation.test.ts @@ -16,8 +16,17 @@ import { createSoundEmitterControlTargetRef } from "../../src/controls/control-surface"; import { createScenePath } from "../../src/document/paths"; -import { createEmptySceneDocument } from "../../src/document/scene-document"; -import { validateSceneDocument } from "../../src/document/scene-document-validation"; +import { + createEmptyProjectDocument, + createEmptyProjectScene, + createEmptySceneDocument, + createSceneDocumentFromProject +} from "../../src/document/scene-document"; +import { + validateProjectDocument, + validateSceneDocument, + validateSceneDocumentLocalBuildContent +} from "../../src/document/scene-document-validation"; import { createCameraRigActorTargetRef, createCameraRigEntity, @@ -69,6 +78,124 @@ describe("validateSceneDocument", () => { expect(validation.warnings).toEqual([]); }); + it("validates project-global actor schedule targets against all project scenes", () => { + const sceneA = createEmptyProjectScene({ + id: "scene-a", + name: "Scene A" + }); + const sceneB = createEmptyProjectScene({ + id: "scene-b", + name: "Scene B" + }); + const ana = createNpcEntity({ + id: "entity-npc-ana-nanto", + actorId: "Ana Nanto" + }); + const actorTarget = createActorControlTargetRef(ana.actorId); + const project = createEmptyProjectDocument({ + sceneId: sceneA.id, + sceneName: sceneA.name + }); + + project.scenes = { + [sceneA.id]: { + ...sceneA, + entities: { + [ana.id]: ana + } + }, + [sceneB.id]: sceneB + }; + project.sequences.sequences["sequence-ana-presence"] = + createProjectSequence({ + id: "sequence-ana-presence", + title: "Ana Presence", + effects: [ + { + stepClass: "held", + type: "controlEffect", + effect: createSetActorPresenceControlEffect({ + target: actorTarget, + active: true + }) + } + ] + }); + project.scheduler.routines["routine-ana-presence"] = + createProjectScheduleRoutine({ + id: "routine-ana-presence", + title: "Ana Presence", + target: actorTarget, + sequenceId: "sequence-ana-presence", + effects: [] + }); + + const projectValidation = validateProjectDocument(project); + const sceneBValidation = validateSceneDocumentLocalBuildContent( + createSceneDocumentFromProject(project, sceneB.id) + ); + + expect(projectValidation.errors).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + code: "missing-control-actor-target" + }) + ]) + ); + expect(sceneBValidation.errors).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + code: "missing-control-actor-target" + }) + ]) + ); + }); + + it("keeps project validation strict for truly missing project-global actor targets", () => { + const project = createEmptyProjectDocument(); + const missingActorTarget = createActorControlTargetRef("actor-missing"); + + project.sequences.sequences["sequence-missing-actor"] = + createProjectSequence({ + id: "sequence-missing-actor", + title: "Missing Actor", + effects: [ + { + stepClass: "held", + type: "controlEffect", + effect: createSetActorPresenceControlEffect({ + target: missingActorTarget, + active: true + }) + } + ] + }); + project.scheduler.routines["routine-missing-actor"] = + createProjectScheduleRoutine({ + id: "routine-missing-actor", + title: "Missing Actor", + target: missingActorTarget, + sequenceId: "sequence-missing-actor", + effects: [] + }); + + const validation = validateProjectDocument(project); + + expect(validation.errors).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + code: "missing-control-actor-target", + path: + "sequences.sequences.sequence-missing-actor.effects.0.effect.target.actorId" + }), + expect.objectContaining({ + code: "missing-control-actor-target", + path: "scheduler.routines.routine-missing-actor.target.actorId" + }) + ]) + ); + }); + it("detects duplicate authored ids across collections", () => { const brush = createBoxBrush({ id: "shared-room-id" diff --git a/tests/unit/runtime-host.test.ts b/tests/unit/runtime-host.test.ts index 1a2a343b..f1f07916 100644 --- a/tests/unit/runtime-host.test.ts +++ b/tests/unit/runtime-host.test.ts @@ -3795,11 +3795,13 @@ describe("RuntimeHost", () => { thirdPersonController: { id: "thirdPerson"; activate: ReturnType; + deactivate: ReturnType; }; activateDesiredNavigationController(): void; }; const deactivate = vi.fn(); const activate = vi.fn(); + const nextControllerDeactivate = vi.fn(); const domElement = ( host as unknown as { domElement: HTMLCanvasElement; @@ -3815,7 +3817,8 @@ describe("RuntimeHost", () => { hostInternals.desiredNavigationMode = "thirdPerson"; hostInternals.thirdPersonController = { id: "thirdPerson", - activate + activate, + deactivate: nextControllerDeactivate }; Object.defineProperty(document, "pointerLockElement", { configurable: true,