Add test for applying expanded typed control effects for model, sound, and scene lighting
This commit is contained in:
@@ -225,6 +225,364 @@ describe("RuntimeHost", () => {
|
|||||||
host.dispose();
|
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 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,
|
||||||
|
{} 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).toBeCloseTo(0.6);
|
||||||
|
expect(hostInternals.ambientLight.color.getHexString()).toBe("112233");
|
||||||
|
expect(hostInternals.sunLight.intensity).toBeCloseTo(0.75);
|
||||||
|
expect(hostInternals.sunLight.color.getHexString()).toBe("ffeeaa");
|
||||||
|
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", () => {
|
it("re-resolves NPC activity from the project scheduler when the runtime clock advances", () => {
|
||||||
vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
||||||
vi.spyOn(RapierCollisionWorld, "create").mockResolvedValue({
|
vi.spyOn(RapierCollisionWorld, "create").mockResolvedValue({
|
||||||
|
|||||||
Reference in New Issue
Block a user