311 lines
9.7 KiB
TypeScript
311 lines
9.7 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
createActorControlTargetRef,
|
|
createProjectGlobalControlTargetRef,
|
|
createLightControlTargetRef,
|
|
createFollowActorPathControlEffect,
|
|
createPlayActorAnimationControlEffect,
|
|
createSetActorPresenceControlEffect,
|
|
createSetLightIntensityControlEffect
|
|
} from "../../src/controls/control-surface";
|
|
import { createScenePath } from "../../src/document/paths";
|
|
import { createEmptySceneDocument } from "../../src/document/scene-document";
|
|
import {
|
|
createNpcEntity,
|
|
createPointLightEntity
|
|
} from "../../src/entities/entity-instances";
|
|
import { createProjectScheduleRoutine } from "../../src/scheduler/project-scheduler";
|
|
import { createProjectSequence } from "../../src/sequencer/project-sequences";
|
|
import {
|
|
applyRuntimeProjectScheduleToControlState,
|
|
resolveRuntimeProjectScheduleState
|
|
} from "../../src/runtime-three/runtime-project-scheduler";
|
|
import { buildRuntimeSceneFromDocument } from "../../src/runtime-three/runtime-scene-build";
|
|
import {
|
|
commitRuntimeScheduleSyncResult,
|
|
createRuntimeScheduleSyncContext,
|
|
syncRuntimeSceneScheduleToClock
|
|
} from "../../src/runtime-three/runtime-schedule-sync";
|
|
import { BoxGeometry } from "three";
|
|
import { createFixtureLoadedModelAssetFromGeometry } from "../helpers/model-collider-fixtures";
|
|
|
|
describe("runtime schedule sync", () => {
|
|
it("matches direct scheduler resolution while applying actor path and animation state", () => {
|
|
const actorTarget = createActorControlTargetRef("actor-patroller");
|
|
const { asset, loadedAsset } = createFixtureLoadedModelAssetFromGeometry(
|
|
"asset-npc-patroller",
|
|
new BoxGeometry(0.8, 1.8, 0.6)
|
|
);
|
|
asset.metadata.animationNames = ["Walk"];
|
|
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 context = createRuntimeScheduleSyncContext(runtimeScene);
|
|
const clock = {
|
|
timeOfDayHours: 11,
|
|
dayCount: 0,
|
|
dayLengthMinutes: 24
|
|
};
|
|
const directResolvedScheduler = resolveRuntimeProjectScheduleState({
|
|
scheduler: runtimeScene.scheduler.document,
|
|
sequences: runtimeScene.sequences,
|
|
actorIds: runtimeScene.npcDefinitions.map(
|
|
(definition) => definition.actorId
|
|
),
|
|
dayNumber: clock.dayCount + 1,
|
|
timeOfDayHours: clock.timeOfDayHours,
|
|
pathsById: context.pathsById
|
|
});
|
|
const directResolvedControl = applyRuntimeProjectScheduleToControlState(
|
|
runtimeScene.control.resolved,
|
|
directResolvedScheduler,
|
|
runtimeScene.control.baselineResolved
|
|
);
|
|
|
|
const result = syncRuntimeSceneScheduleToClock({
|
|
runtimeScene,
|
|
clock,
|
|
context
|
|
});
|
|
commitRuntimeScheduleSyncResult(runtimeScene, result);
|
|
|
|
expect(result.resolvedScheduler).toEqual(directResolvedScheduler);
|
|
expect(result.resolvedControl).toEqual(directResolvedControl);
|
|
expect(result.npcChanges).toHaveLength(1);
|
|
expect(result.npcEntityCollectionChanged).toBe(true);
|
|
expect(runtimeScene.npcDefinitions[0]).toEqual(
|
|
expect.objectContaining({
|
|
entityId: npc.id,
|
|
activeRoutineTitle: "Patrolling",
|
|
animationClipName: "Walk",
|
|
yawDegrees: 90,
|
|
position: {
|
|
x: 4,
|
|
y: 0,
|
|
z: 0
|
|
},
|
|
resolvedPath: expect.objectContaining({
|
|
pathId: path.id,
|
|
progress: 0.5
|
|
})
|
|
})
|
|
);
|
|
expect(runtimeScene.entities.npcs).toEqual([
|
|
expect.objectContaining({
|
|
entityId: npc.id,
|
|
activeRoutineTitle: "Patrolling",
|
|
animationClipName: "Walk",
|
|
position: {
|
|
x: 4,
|
|
y: 0,
|
|
z: 0
|
|
}
|
|
})
|
|
]);
|
|
});
|
|
|
|
it("reuses scene-stable scheduler inputs and leaves NPC collections untouched when output is unchanged", () => {
|
|
const npc = createNpcEntity({
|
|
id: "entity-npc-stable",
|
|
actorId: "actor-stable"
|
|
});
|
|
const document = createEmptySceneDocument();
|
|
document.entities[npc.id] = npc;
|
|
const runtimeScene = buildRuntimeSceneFromDocument(document);
|
|
const context = createRuntimeScheduleSyncContext(runtimeScene);
|
|
const clock = {
|
|
timeOfDayHours: 6,
|
|
dayCount: 0,
|
|
dayLengthMinutes: 24
|
|
};
|
|
|
|
const firstResult = syncRuntimeSceneScheduleToClock({
|
|
runtimeScene,
|
|
clock,
|
|
context
|
|
});
|
|
const npcEntities = runtimeScene.entities.npcs;
|
|
const colliders = runtimeScene.colliders;
|
|
const pathsById = context.pathsById;
|
|
const actorStatesByActorId = context.actorStatesByActorId;
|
|
const actorIds = context.actorIds;
|
|
const secondResult = syncRuntimeSceneScheduleToClock({
|
|
runtimeScene,
|
|
clock,
|
|
context
|
|
});
|
|
|
|
expect(firstResult.npcChanges).toHaveLength(0);
|
|
expect(secondResult.npcChanges).toHaveLength(0);
|
|
expect(context.pathsById).toBe(pathsById);
|
|
expect(context.actorIds).toBe(actorIds);
|
|
expect(context.actorStatesByActorId).toBe(actorStatesByActorId);
|
|
expect(runtimeScene.entities.npcs).toBe(npcEntities);
|
|
expect(runtimeScene.colliders).toBe(colliders);
|
|
});
|
|
|
|
it("resolves scheduled controls and impulse routines through the shared helper", () => {
|
|
const pointLight = createPointLightEntity({
|
|
id: "entity-point-light-night-lamp",
|
|
intensity: 1.25
|
|
});
|
|
const document = createEmptySceneDocument();
|
|
document.entities[pointLight.id] = pointLight;
|
|
document.sequences.sequences["sequence-transition"] = createProjectSequence({
|
|
id: "sequence-transition",
|
|
title: "Transition",
|
|
effects: [
|
|
{
|
|
stepClass: "impulse",
|
|
type: "startSceneTransition",
|
|
targetSceneId: "scene-house",
|
|
targetEntryEntityId: "entry-house"
|
|
}
|
|
]
|
|
});
|
|
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
|
|
})
|
|
});
|
|
document.scheduler.routines["routine-transition"] =
|
|
createProjectScheduleRoutine({
|
|
id: "routine-transition",
|
|
title: "Transition Window",
|
|
target: createProjectGlobalControlTargetRef(),
|
|
startHour: 20,
|
|
endHour: 22,
|
|
sequenceId: "sequence-transition",
|
|
effects: []
|
|
});
|
|
const runtimeScene = buildRuntimeSceneFromDocument(document);
|
|
const context = createRuntimeScheduleSyncContext(runtimeScene);
|
|
const clock = {
|
|
timeOfDayHours: 21,
|
|
dayCount: 0,
|
|
dayLengthMinutes: 24
|
|
};
|
|
const directResolvedScheduler = resolveRuntimeProjectScheduleState({
|
|
scheduler: runtimeScene.scheduler.document,
|
|
sequences: runtimeScene.sequences,
|
|
actorIds: context.actorIds,
|
|
dayNumber: clock.dayCount + 1,
|
|
timeOfDayHours: clock.timeOfDayHours,
|
|
pathsById: context.pathsById
|
|
});
|
|
const directResolvedControl = applyRuntimeProjectScheduleToControlState(
|
|
runtimeScene.control.resolved,
|
|
directResolvedScheduler,
|
|
runtimeScene.control.baselineResolved
|
|
);
|
|
|
|
const result = syncRuntimeSceneScheduleToClock({
|
|
runtimeScene,
|
|
clock,
|
|
context
|
|
});
|
|
|
|
expect(result.resolvedScheduler).toEqual(directResolvedScheduler);
|
|
expect(result.resolvedControl).toEqual(directResolvedControl);
|
|
expect(result.resolvedScheduler.impulses).toEqual([
|
|
expect.objectContaining({
|
|
routineId: "routine-transition",
|
|
effects: [
|
|
expect.objectContaining({
|
|
type: "startSceneTransition",
|
|
targetSceneId: "scene-house",
|
|
targetEntryEntityId: "entry-house"
|
|
})
|
|
]
|
|
})
|
|
]);
|
|
expect(result.nextActiveImpulseRoutineIds).toEqual(
|
|
new Set(["routine-transition"])
|
|
);
|
|
expect(result.resolvedControl.channels).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
type: "lightIntensity",
|
|
value: 3.5,
|
|
source: {
|
|
kind: "scheduler",
|
|
scheduleId: "routine-night-lamp"
|
|
}
|
|
})
|
|
])
|
|
);
|
|
});
|
|
});
|