Editing while playing: play runs on a runtime copy, new Game tab (Scene | Game | Graph)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
cb8885359f
commit
c3a533c4e5
14 files changed
+408
-118
No files matched your search
@@ -0,0 +1,96 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { CreateAssetCommand, SetAssetPropertyCommand } from '../../src/core/commands/assetCommands';
|
||||
import { CommandStack } from '../../src/core/commands/CommandStack';
|
||||
import { ClearOverrideCommand, PlaceAssetCommand, RemoveSceneObjectCommand, SetOverrideCommand, SetParentCommand } from '../../src/core/commands/sceneObjectCommands';
|
||||
import { createEmptyProject, findScene } from '../../src/core/model/Project';
|
||||
import { deserializeProject, serializeProject } from '../../src/core/store/serialize';
|
||||
import { World } from '../../src/runtime/World';
|
||||
|
||||
/** A scene with a 100×100 box asset and two placed boxes. */
|
||||
function setup() {
|
||||
const project = createEmptyProject('p', 'Sync');
|
||||
const stack = new CommandStack(project);
|
||||
const scene = new CreateAssetCommand('scene', 'Level');
|
||||
const sprite = new CreateAssetCommand('sprite', 'Box');
|
||||
stack.execute(scene);
|
||||
stack.execute(sprite);
|
||||
const a = new PlaceAssetCommand(scene.asset.id, sprite.asset.id, { x: 0, y: 0 });
|
||||
const b = new PlaceAssetCommand(scene.asset.id, sprite.asset.id, { x: 300, y: 0 });
|
||||
stack.execute(a);
|
||||
stack.execute(b);
|
||||
const sceneOf = () => findScene(project, scene.asset.id)!;
|
||||
const object = (id: string) => sceneOf().objects.find((o) => o.id === id)!;
|
||||
return { project, stack, sceneId: scene.asset.id, spriteId: sprite.asset.id, a: a.objectId, b: b.objectId, sceneOf, object };
|
||||
}
|
||||
|
||||
describe('world ↔ model synchronisation (editing while playing)', () => {
|
||||
it('syncFromModel picks up placed, moved, edited, re-parented and removed objects', () => {
|
||||
const { project, stack, sceneId, spriteId, a, b, object } = setup();
|
||||
const world = World.instantiate(project, sceneId);
|
||||
expect(world.entities.map((e) => e.objectId)).toEqual([a, b]);
|
||||
|
||||
// Placed while playing → new entity, existing ones keep their identity.
|
||||
const entityA = world.findByObjectId(a)!;
|
||||
const c = new PlaceAssetCommand(sceneId, spriteId, { x: 50, y: 50 });
|
||||
stack.execute(c);
|
||||
world.syncFromModel();
|
||||
expect(world.entities).toHaveLength(3);
|
||||
expect(world.findByObjectId(a)).toBe(entityA);
|
||||
|
||||
// Property edits (override, asset value) and parenting are reflected; props bag keeps identity.
|
||||
const propsA = entityA.props;
|
||||
stack.execute(new SetOverrideCommand(sceneId, a, 'width', 40));
|
||||
stack.execute(new SetAssetPropertyCommand(spriteId, 'height', 70));
|
||||
stack.execute(new SetParentCommand(sceneId, b, a));
|
||||
world.syncFromModel();
|
||||
expect(entityA.props).toBe(propsA);
|
||||
expect(entityA.props.width).toBe(40);
|
||||
expect(entityA.props.height).toBe(70);
|
||||
expect(world.findByObjectId(b)!.parent).toBe(entityA);
|
||||
expect(world.findByObjectId(b)!.x).toBe(object(b).position.x);
|
||||
|
||||
// Removed while playing → entity gone.
|
||||
stack.execute(new RemoveSceneObjectCommand(sceneId, c.objectId));
|
||||
world.syncFromModel();
|
||||
expect(world.entities.map((e) => e.objectId)).toEqual([a, b]);
|
||||
});
|
||||
|
||||
it('syncToModel writes positions, changed values as overrides and destroyed entities back', () => {
|
||||
const { project, sceneId, a, b, object, sceneOf } = setup();
|
||||
const world = World.instantiate(project, sceneId);
|
||||
const entityA = world.findByObjectId(a)!;
|
||||
entityA.x = 123;
|
||||
entityA.props.width = 999; // what a Set property node would do
|
||||
entityA.props.height = 100; // unchanged → no override
|
||||
expect(world.syncToModel()).toBe(false);
|
||||
expect(object(a).position).toEqual({ x: 123, y: 0, z: 0 });
|
||||
expect(object(a).overrides).toEqual({ width: 999 });
|
||||
|
||||
// Round trip: the model now resolves to the runtime values, so a sync back changes nothing.
|
||||
world.syncFromModel();
|
||||
expect(entityA.x).toBe(123);
|
||||
expect(entityA.props.width).toBe(999);
|
||||
|
||||
// Resetting the override in the editor returns the entity to the asset value.
|
||||
new ClearOverrideCommand(sceneId, a, 'width').execute(project);
|
||||
world.syncFromModel();
|
||||
expect(entityA.props.width).toBe(100);
|
||||
|
||||
// Destroy (node) removes the entity; the sync drops its object – a structural change.
|
||||
world.remove(world.findByObjectId(b)!);
|
||||
expect(world.syncToModel()).toBe(true);
|
||||
expect(sceneOf().objects.map((o) => o.id)).toEqual([a]);
|
||||
});
|
||||
|
||||
it('a play-mode copy leaves the original model untouched', () => {
|
||||
const { project, sceneId, a, object } = setup();
|
||||
const copy = deserializeProject(serializeProject(project), project.id);
|
||||
const world = World.instantiate(copy, sceneId);
|
||||
world.findByObjectId(a)!.x = 500;
|
||||
world.remove(world.findByObjectId(a)!);
|
||||
world.syncToModel();
|
||||
expect(findScene(copy, sceneId)!.objects).toHaveLength(1);
|
||||
expect(object(a).position.x).toBe(0);
|
||||
expect(findScene(project, sceneId)!.objects).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user