Two findings from #269. A reference from a container into a placement nested in it was dead from the start, and deleting an asset left references to the objects it took along showing "(missing)". - A reference is stored the way its holder shows the object. In a container tab that is q~x for content of a nested placement q. - referenceCandidates translates it when the container is placed: in placement r, q~x becomes r~q/x. pathOfId and idOfPath in instance.ts do the mapping, objectChoices offers the same spelling the other way. The rule holds at any depth. - Storing q/x as proposed in #285 was dropped. The container tab would then have had to translate its own ids. - No migration. All projects and snapshots hold three references, none of them with ~ or /. - Create container lets a reference to content of a member placement follow it. It was left dead before. - Paste lets such a reference point into the copy. It kept pointing at the original before. Both go through the new renamedReference. - Delete asset records what every holder shows before the cascade and clears every reference to an object that is gone afterwards, with dropReferencesToGone from #269. The row shows None, undo restores the references (#286). Tests: - Gate: 198 files, 2882 tests (2873 + 9), exit 0 - Unit object-references: 26 passed (17 + 9) - e2e 1-17,57: 566 checks passed, 57 has +57i and +57j - Six unit counter-probes fail between 1 and 3 tests each - The browser counter-probe fails 57i and 57j as described in the issues - e2e total 1609 is computed, not measured in a full run Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
490 lines
23 KiB
TypeScript
490 lines
23 KiB
TypeScript
/**
|
||
* An object's place names another object of the same scene (#258): picked on a placed object,
|
||
* stored as its id, the object itself once the game runs – and inside a container, each placement's
|
||
* own copy of it.
|
||
*/
|
||
import { describe, expect, it } from 'vitest';
|
||
import { CreateAssetCommand, DeleteAssetCommand } from '../../src/core/commands/assetCommands';
|
||
import { CreateContainerCommand, UnpackContainerCommand } from '../../src/core/commands/containerCommands';
|
||
import { PasteObjectsCommand } from '../../src/core/commands/pasteCommands';
|
||
import { AddPortsCommand } from '../../src/core/commands/portCommands';
|
||
import {
|
||
PlaceAssetCommand,
|
||
RemoveSceneObjectsCommand,
|
||
RenameSceneObjectCommand,
|
||
SetOverrideCommand,
|
||
SetParentCommand,
|
||
} from '../../src/core/commands/sceneObjectCommands';
|
||
import { AddStructFieldCommand, AddVariableCommand } from '../../src/core/commands/variableCommands';
|
||
import type { BehaviorAsset, ContainerAsset } from '../../src/core/model/Asset';
|
||
import { copyObjects } from '../../src/core/model/clipboard';
|
||
import { instanceChildId, resolvedHolder } from '../../src/core/model/instance';
|
||
import {
|
||
objectChoices,
|
||
objectReference,
|
||
referenceCandidates,
|
||
referencedId,
|
||
} from '../../src/core/model/objectRefs';
|
||
import { NO_OBJECT } from '../../src/core/model/Properties';
|
||
import { findAsset, findObjectHolder } from '../../src/core/model/Project';
|
||
import { makeItem } from '../../src/core/properties/listItems';
|
||
import { coerceValue, variableDef } from '../../src/core/properties/PropertySchema';
|
||
import {
|
||
graphEditor,
|
||
makeBehavior,
|
||
makeSpriteScene,
|
||
place,
|
||
roundTrip,
|
||
type SceneFixture,
|
||
startWorld,
|
||
} from '../helpers';
|
||
|
||
/** A behavior whose object variable `target` is logged on every update. */
|
||
function watcher(f: SceneFixture): BehaviorAsset {
|
||
const behavior = makeBehavior(f, 'Watcher');
|
||
f.stack.execute(new AddVariableCommand(behavior.id, 'target', 'object'));
|
||
const edit = graphEditor(f, behavior);
|
||
const log = edit.addNode('Log');
|
||
edit.wire([
|
||
[edit.addNode('OnUpdate'), 'exec', log, 'exec'],
|
||
[edit.variables(['target']), 'target', log, 'message'],
|
||
]);
|
||
return behavior;
|
||
}
|
||
|
||
/** A Watcher and a box called "Crate" in the scene, the Watcher pointed at the box. */
|
||
function watching() {
|
||
const f = makeSpriteScene();
|
||
const behavior = watcher(f);
|
||
const watcherId = place(f, behavior.id, { x: 0, y: 0 });
|
||
const crate = place(f, f.spriteId, { x: 10, y: 0 });
|
||
f.stack.execute(new RenameSceneObjectCommand(f.sceneId, crate, 'Crate'));
|
||
f.stack.execute(new SetOverrideCommand(f.sceneId, watcherId, 'target', objectReference(crate)));
|
||
return { ...f, behavior, watcherId, crate };
|
||
}
|
||
|
||
describe('a picked object at rest', () => {
|
||
it('is kept by the override, the file, a struct field and a list item', () => {
|
||
const f = watching();
|
||
expect(f.object(f.watcherId).overrides.target).toEqual({ $object: f.crate });
|
||
const loaded = roundTrip(f.project);
|
||
const scene = loaded.assets.find((a) => a.id === f.sceneId) as {
|
||
objects: { id: string; overrides: object }[];
|
||
};
|
||
expect(scene.objects.find((o) => o.id === f.watcherId)!.overrides).toEqual({
|
||
target: { $object: f.crate },
|
||
});
|
||
const def = variableDef('held', 'struct', undefined, [{ key: 'who', kind: 'object' }]);
|
||
expect(coerceValue(def, { who: objectReference('x') })).toEqual({ who: { $object: 'x' } });
|
||
expect(makeItem('object', objectReference('x'))).toEqual({ $object: 'x' });
|
||
});
|
||
|
||
it('is nothing for anything that names no object', () => {
|
||
const def = variableDef('target', 'object');
|
||
expect(coerceValue(def, 'Crate')).toEqual(NO_OBJECT);
|
||
expect(coerceValue(def, objectReference(''))).toEqual(NO_OBJECT);
|
||
expect(referencedId('Crate')).toBe('');
|
||
});
|
||
});
|
||
|
||
describe('a picked object in a running game', () => {
|
||
it('is the object it names, and nothing once that is destroyed', () => {
|
||
const f = watching();
|
||
const run = startWorld(f, { behaviors: true });
|
||
run.frame();
|
||
expect(run.world.log.at(-1)).toBe('Crate');
|
||
run.world.remove(run.world.findByObjectId(f.crate)!);
|
||
run.frame();
|
||
expect(run.world.log.at(-1)).toBe('');
|
||
});
|
||
|
||
it('is the object inside a struct field and a list item too', () => {
|
||
const f = makeSpriteScene();
|
||
const behavior = makeBehavior(f, 'Holder');
|
||
f.stack.execute(new AddVariableCommand(behavior.id, 'held', 'struct'));
|
||
f.stack.execute(new AddStructFieldCommand(behavior.id, 'held', { key: 'who', kind: 'object' }));
|
||
f.stack.execute(new AddVariableCommand(behavior.id, 'crowd', 'list'));
|
||
const edit = graphEditor(f, behavior);
|
||
const expand = edit.addNode('ExpandStruct');
|
||
f.stack.execute(new AddPortsCommand(behavior.id, expand.id, [{ key: 'who', kind: 'object' }]));
|
||
const first = edit.addNode('ItemAt', { index: 0 });
|
||
const join = edit.addNode('Join');
|
||
const log = edit.addNode('Log');
|
||
const get = edit.variables(['held', 'crowd']);
|
||
edit.wire([
|
||
[edit.addNode('OnUpdate'), 'exec', log, 'exec'],
|
||
[get, 'held', expand, 'value'],
|
||
[get, 'crowd', first, 'list'],
|
||
[expand, 'who', join, 'a'],
|
||
[first, 'item', join, 'b'],
|
||
[join, 'text', log, 'message'],
|
||
]);
|
||
const holder = place(f, behavior.id, { x: 0, y: 0 });
|
||
const crate = place(f, f.spriteId, { x: 0, y: 0 });
|
||
const barrel = place(f, f.spriteId, { x: 0, y: 0 });
|
||
f.stack.execute(new RenameSceneObjectCommand(f.sceneId, crate, 'Crate'));
|
||
f.stack.execute(new RenameSceneObjectCommand(f.sceneId, barrel, 'Barrel'));
|
||
f.stack.execute(new SetOverrideCommand(f.sceneId, holder, 'held', { who: objectReference(crate) }));
|
||
f.stack.execute(new SetOverrideCommand(f.sceneId, holder, 'crowd', [objectReference(barrel)]));
|
||
const run = startWorld(f, { behaviors: true });
|
||
run.frame();
|
||
expect(run.world.log.at(-1)).toBe('CrateBarrel');
|
||
});
|
||
|
||
it('stays in the scene when a graph puts another object into the place (editor run)', () => {
|
||
const f = watching();
|
||
const edit = graphEditor(f, f.behavior);
|
||
const spawn = edit.addNode('Spawn', { asset: f.spriteId });
|
||
const set = edit.variables(['target'], { write: true });
|
||
edit.wire([
|
||
[edit.addNode('OnStart'), 'exec', spawn, 'exec'],
|
||
[spawn, 'exec', set, 'exec'],
|
||
[spawn, 'object', set, 'target'],
|
||
]);
|
||
const run = startWorld(f, { behaviors: true });
|
||
run.frame();
|
||
run.world.syncToModel();
|
||
run.world.syncFromModel();
|
||
run.frame();
|
||
expect(run.world.log.at(-1)).toBe('Box');
|
||
expect(f.object(f.watcherId).overrides.target).toEqual({ $object: f.crate });
|
||
});
|
||
|
||
it('follows a pick made while the game runs', () => {
|
||
const f = watching();
|
||
const barrel = place(f, f.spriteId, { x: 0, y: 0 });
|
||
f.stack.execute(new RenameSceneObjectCommand(f.sceneId, barrel, 'Barrel'));
|
||
const run = startWorld(f, { behaviors: true });
|
||
run.frame();
|
||
f.stack.execute(new SetOverrideCommand(f.sceneId, f.watcherId, 'target', objectReference(barrel)));
|
||
run.world.syncFromModel();
|
||
run.frame();
|
||
expect(run.world.log.slice(-2)).toEqual(['Crate', 'Barrel']);
|
||
});
|
||
});
|
||
|
||
describe('a picked object inside a container', () => {
|
||
/** The Watcher and its Crate made into a container, placed a second time beside the first. */
|
||
function contained() {
|
||
const f = watching();
|
||
const create = new CreateContainerCommand(f.sceneId, [f.watcherId, f.crate]);
|
||
f.stack.execute(create);
|
||
const second = new PlaceAssetCommand(f.sceneId, create.assetId, { x: 500, y: 0 });
|
||
f.stack.execute(second);
|
||
const asset = findAsset(f.project, create.assetId) as ContainerAsset;
|
||
const [tWatcher, tCrate] = asset.objects.map((o) => o.id) as [string, string];
|
||
return { ...f, create, asset, tWatcher, tCrate, first: create.objectId, second: second.objectId };
|
||
}
|
||
|
||
it('names the template copy, and each placement reaches its own', () => {
|
||
const f = contained();
|
||
expect(f.asset.objects[0]!.overrides.target).toEqual({ $object: f.tCrate });
|
||
const run = startWorld(f);
|
||
for (const placed of [f.first, f.second]) {
|
||
const watcher = run.world.findByObjectId(instanceChildId(placed, f.tWatcher))!;
|
||
const found = run.world.findReferenced(watcher, f.tCrate);
|
||
expect(found?.objectId).toBe(instanceChildId(placed, f.tCrate));
|
||
}
|
||
});
|
||
|
||
it('offers the content of its own placement, named as the template names it', () => {
|
||
const f = contained();
|
||
const choices = objectChoices(f.project, f.sceneOf(), instanceChildId(f.second, f.tWatcher));
|
||
expect(choices.map((c) => [c.ref, c.depth])).toEqual([
|
||
[f.tWatcher, 0],
|
||
[f.tCrate, 0],
|
||
]);
|
||
});
|
||
|
||
it('points a reference from outside at what the placed container shows', () => {
|
||
const f = watching();
|
||
const other = place(f, f.behavior.id, { x: 0, y: 0 });
|
||
f.stack.execute(new SetOverrideCommand(f.sceneId, other, 'target', objectReference(f.crate)));
|
||
const create = new CreateContainerCommand(f.sceneId, [f.crate]);
|
||
f.stack.execute(create);
|
||
const tCrate = (findAsset(f.project, create.assetId) as ContainerAsset).objects[0]!.id;
|
||
expect(f.object(other).overrides.target).toEqual({ $object: instanceChildId(create.objectId, tCrate) });
|
||
f.stack.undo();
|
||
expect(f.object(other).overrides.target).toEqual({ $object: f.crate });
|
||
});
|
||
|
||
it('is spelled out as the copy it names when the container is unpacked', () => {
|
||
const f = contained();
|
||
f.stack.execute(new UnpackContainerCommand(f.sceneId, f.second));
|
||
const watcher = f.object(instanceChildId(f.second, f.tWatcher));
|
||
expect(watcher.overrides.target).toEqual({ $object: instanceChildId(f.second, f.tCrate) });
|
||
});
|
||
});
|
||
|
||
describe('the objects of a scene', () => {
|
||
it('are offered in hierarchy order, the object itself included', () => {
|
||
const f = watching();
|
||
const lid = place(f, f.spriteId, { x: 0, y: 0 });
|
||
f.stack.execute(new SetParentCommand(f.sceneId, lid, f.crate));
|
||
f.stack.execute(new RenameSceneObjectCommand(f.sceneId, lid, 'Lid'));
|
||
const choices = objectChoices(f.project, f.sceneOf(), f.watcherId);
|
||
expect(choices.map((c) => [c.label, c.depth])).toEqual([
|
||
['Watcher', 0],
|
||
['Crate', 0],
|
||
['Lid', 1],
|
||
]);
|
||
expect(choices[1]!.ref).toBe(f.crate);
|
||
});
|
||
});
|
||
|
||
describe('removing and pasting', () => {
|
||
it('drops a reference to a removed object, and undo brings it back', () => {
|
||
const f = watching();
|
||
f.stack.execute(new RemoveSceneObjectsCommand(f.sceneId, [f.crate]));
|
||
expect(f.object(f.watcherId).overrides.target).toEqual(NO_OBJECT);
|
||
f.stack.undo();
|
||
expect(f.object(f.watcherId).overrides.target).toEqual({ $object: f.crate });
|
||
});
|
||
|
||
it('points a pasted copy at the copy of what it named, and leaves one out of the copy alone', () => {
|
||
const f = watching();
|
||
const both = copyObjects(f.project, f.sceneOf(), [f.watcherId, f.crate])!;
|
||
const pasteBoth = new PasteObjectsCommand(f.sceneId, both, {}, { x: 5, y: 0 });
|
||
f.stack.execute(pasteBoth);
|
||
const [watcherCopy, crateCopy] = pasteBoth.rootIds as [string, string];
|
||
expect(f.object(watcherCopy).overrides.target).toEqual({ $object: crateCopy });
|
||
|
||
const alone = copyObjects(f.project, f.sceneOf(), [f.watcherId])!;
|
||
const pasteAlone = new PasteObjectsCommand(f.sceneId, alone, {}, { x: 5, y: 0 });
|
||
f.stack.execute(pasteAlone);
|
||
expect(f.object(pasteAlone.rootIds[0]!).overrides.target).toEqual({ $object: f.crate });
|
||
});
|
||
});
|
||
|
||
describe('removing an object from a container template (#269)', () => {
|
||
/**
|
||
* Watcher, Crate and Barrel made into a container whose Watcher points at nothing, placed in
|
||
* "Level" and in "Level 2", each placement's Watcher pointed at its own Crate.
|
||
*/
|
||
function placedTwice() {
|
||
const f = watching();
|
||
const barrel = place(f, f.spriteId, { x: 20, y: 0 });
|
||
const create = new CreateContainerCommand(f.sceneId, [f.watcherId, f.crate, barrel]);
|
||
f.stack.execute(create);
|
||
const asset = findAsset(f.project, create.assetId) as ContainerAsset;
|
||
const [tWatcher, tCrate, tBarrel] = asset.objects.map((o) => o.id) as [string, string, string];
|
||
f.stack.execute(new SetOverrideCommand(asset.id, tWatcher, 'target', NO_OBJECT));
|
||
const level2 = new CreateAssetCommand('scene', 'Level 2');
|
||
f.stack.execute(level2);
|
||
const other = new PlaceAssetCommand(level2.asset.id, asset.id, { x: 0, y: 0 });
|
||
f.stack.execute(other);
|
||
const placements = [
|
||
[f.sceneId, create.objectId],
|
||
[level2.asset.id, other.objectId],
|
||
] as const;
|
||
for (const [sceneId, placed] of placements)
|
||
f.stack.execute(
|
||
new SetOverrideCommand(sceneId, instanceChildId(placed, tWatcher), 'target', objectReference(tCrate)),
|
||
);
|
||
return { ...f, asset, tWatcher, tCrate, tBarrel, level2: level2.asset.id, placements };
|
||
}
|
||
|
||
/** What the inspector shows as `target` on the object, container content resolved. */
|
||
function targetOf(f: SceneFixture, holderId: string, objectId: string) {
|
||
const holder = resolvedHolder(f.project, findObjectHolder(f.project, holderId)!);
|
||
return holder.objects.find((o) => o.id === objectId)!.overrides.target;
|
||
}
|
||
|
||
/** The edit a placement holds for its Watcher. */
|
||
function editOf(f: ReturnType<typeof placedTwice>, sceneId: string, placed: string) {
|
||
const holder = findObjectHolder(f.project, sceneId)!;
|
||
return holder.objects.find((o) => o.id === placed)!.instanceEdits[f.tWatcher];
|
||
}
|
||
|
||
it('empties the placements pointing at it in every scene, and undo brings them back', () => {
|
||
const f = placedTwice();
|
||
f.stack.execute(new RemoveSceneObjectsCommand(f.asset.id, [f.tCrate]));
|
||
for (const [sceneId, placed] of f.placements) {
|
||
expect(targetOf(f, sceneId, instanceChildId(placed, f.tWatcher))).toEqual(NO_OBJECT);
|
||
// None is what the template says too, so the placement follows it again.
|
||
expect(editOf(f, sceneId, placed)).toBeUndefined();
|
||
}
|
||
f.stack.undo();
|
||
for (const [sceneId, placed] of f.placements) {
|
||
expect(targetOf(f, sceneId, instanceChildId(placed, f.tWatcher))).toEqual({ $object: f.tCrate });
|
||
expect(editOf(f, sceneId, placed)?.overrides.target).toEqual({ $object: f.tCrate });
|
||
}
|
||
});
|
||
|
||
it('leaves a placement at None where the template names something else', () => {
|
||
const f = placedTwice();
|
||
f.stack.execute(new SetOverrideCommand(f.asset.id, f.tWatcher, 'target', objectReference(f.tBarrel)));
|
||
f.stack.execute(new RemoveSceneObjectsCommand(f.asset.id, [f.tCrate]));
|
||
const [sceneId, placed] = f.placements[0];
|
||
expect(targetOf(f, sceneId, instanceChildId(placed, f.tWatcher))).toEqual(NO_OBJECT);
|
||
expect(editOf(f, sceneId, placed)?.overrides.target).toEqual(NO_OBJECT);
|
||
});
|
||
|
||
it('empties a reference from outside the placement, and leaves one to what stays alone', () => {
|
||
const f = placedTwice();
|
||
const [, placed] = f.placements[0];
|
||
const toCrate = place(f, f.behavior.id, { x: 0, y: 0 });
|
||
const toBarrel = place(f, f.behavior.id, { x: 0, y: 0 });
|
||
const crate = objectReference(instanceChildId(placed, f.tCrate));
|
||
const barrel = objectReference(instanceChildId(placed, f.tBarrel));
|
||
f.stack.execute(new SetOverrideCommand(f.sceneId, toCrate, 'target', crate));
|
||
f.stack.execute(new SetOverrideCommand(f.sceneId, toBarrel, 'target', barrel));
|
||
f.stack.execute(new RemoveSceneObjectsCommand(f.asset.id, [f.tCrate]));
|
||
expect(f.object(toCrate).overrides.target).toEqual(NO_OBJECT);
|
||
expect(f.object(toBarrel).overrides.target).toEqual(barrel);
|
||
f.stack.undo();
|
||
expect(f.object(toCrate).overrides.target).toEqual(crate);
|
||
});
|
||
|
||
it('reaches placements nested in another container, template and scene alike', () => {
|
||
const f = placedTwice();
|
||
const [, placed] = f.placements[0];
|
||
// The placement in "Level" becomes the content of Outer and takes its edit into Outer's template.
|
||
const outer = new CreateContainerCommand(f.sceneId, [placed]);
|
||
f.stack.execute(outer);
|
||
const outerAsset = findAsset(f.project, outer.assetId) as ContainerAsset;
|
||
const inner = outerAsset.objects[0]!.id;
|
||
const outerPlaced = new PlaceAssetCommand(f.level2, outerAsset.id, { x: 0, y: 0 });
|
||
f.stack.execute(outerPlaced);
|
||
const nestedWatcher = instanceChildId(outerPlaced.objectId, `${inner}/${f.tWatcher}`);
|
||
f.stack.execute(new SetOverrideCommand(f.level2, nestedWatcher, 'target', objectReference(f.tBarrel)));
|
||
|
||
f.stack.execute(new RemoveSceneObjectsCommand(f.asset.id, [f.tCrate]));
|
||
expect(targetOf(f, outerAsset.id, instanceChildId(inner, f.tWatcher))).toEqual(NO_OBJECT);
|
||
expect(targetOf(f, f.level2, nestedWatcher)).toEqual({ $object: f.tBarrel });
|
||
f.stack.execute(new RemoveSceneObjectsCommand(f.asset.id, [f.tBarrel]));
|
||
expect(targetOf(f, f.level2, nestedWatcher)).toEqual(NO_OBJECT);
|
||
|
||
f.stack.undo();
|
||
f.stack.undo();
|
||
expect(targetOf(f, outerAsset.id, instanceChildId(inner, f.tWatcher))).toEqual({ $object: f.tCrate });
|
||
expect(targetOf(f, f.level2, nestedWatcher)).toEqual({ $object: f.tBarrel });
|
||
});
|
||
|
||
it('does the same when the asset of the object is deleted (#286)', () => {
|
||
const f = placedTwice();
|
||
f.stack.execute(new DeleteAssetCommand(f.spriteId));
|
||
for (const [sceneId, placed] of f.placements) {
|
||
expect(targetOf(f, sceneId, instanceChildId(placed, f.tWatcher))).toEqual(NO_OBJECT);
|
||
expect(editOf(f, sceneId, placed)).toBeUndefined();
|
||
}
|
||
f.stack.undo();
|
||
for (const [sceneId, placed] of f.placements)
|
||
expect(editOf(f, sceneId, placed)?.overrides.target).toEqual({ $object: f.tCrate });
|
||
});
|
||
});
|
||
|
||
describe('a reference into a placement nested in a container (#285)', () => {
|
||
/**
|
||
* The Crate made into "Inner", then the Watcher and that placement into "Outer", placed a second
|
||
* time. A Lookout outside points at the Crate as well.
|
||
*/
|
||
function nested() {
|
||
const f = watching();
|
||
const lookout = place(f, f.behavior.id, { x: 0, y: 0 });
|
||
f.stack.execute(new SetOverrideCommand(f.sceneId, lookout, 'target', objectReference(f.crate)));
|
||
const inner = new CreateContainerCommand(f.sceneId, [f.crate]);
|
||
f.stack.execute(inner);
|
||
const innerPlaced = inner.objectId;
|
||
const outer = new CreateContainerCommand(f.sceneId, [f.watcherId, innerPlaced]);
|
||
f.stack.execute(outer);
|
||
const second = new PlaceAssetCommand(f.sceneId, outer.assetId, { x: 500, y: 0 });
|
||
f.stack.execute(second);
|
||
const asset = findAsset(f.project, outer.assetId) as ContainerAsset;
|
||
const tWatcher = asset.objects.find((o) => o.assetId === f.behavior.id)!.id;
|
||
const tInner = asset.objects.find((o) => o.assetId === inner.assetId)!.id;
|
||
const tCrate = (findAsset(f.project, inner.assetId) as ContainerAsset).objects[0]!.id;
|
||
/** The Crate as Outer's own view names it. */
|
||
const crate = instanceChildId(tInner, tCrate);
|
||
/** The Crate's copy in a placement of Outer. */
|
||
const copyIn = (placed: string) => instanceChildId(placed, `${tInner}/${tCrate}`);
|
||
const placements = [outer.objectId, second.objectId] as const;
|
||
return { ...f, lookout, inner, innerPlaced, asset, tWatcher, crate, copyIn, placements };
|
||
}
|
||
|
||
it('is picked in the container as its view names it, and every placement reaches its own copy', () => {
|
||
const f = nested();
|
||
f.stack.execute(new SetOverrideCommand(f.asset.id, f.tWatcher, 'target', NO_OBJECT));
|
||
const choice = objectChoices(f.project, f.asset, f.tWatcher).find((c) => c.label === 'Crate')!;
|
||
expect(choice).toMatchObject({ ref: f.crate, objectId: f.crate });
|
||
f.stack.execute(new SetOverrideCommand(f.asset.id, f.tWatcher, 'target', objectReference(choice.ref)));
|
||
const run = startWorld(f);
|
||
for (const placed of f.placements) {
|
||
const watcher = run.world.findByObjectId(instanceChildId(placed, f.tWatcher))!;
|
||
expect(run.world.findReferenced(watcher, f.crate)?.objectId).toBe(f.copyIn(placed));
|
||
}
|
||
});
|
||
|
||
it('is shown as the Crate on a placement, and offered there with the same name', () => {
|
||
const f = nested();
|
||
const [, placed] = f.placements;
|
||
const choices = objectChoices(f.project, f.sceneOf(), instanceChildId(placed, f.tWatcher));
|
||
expect(choices.find((c) => c.ref === f.crate)).toMatchObject({
|
||
objectId: f.copyIn(placed),
|
||
label: 'Crate',
|
||
});
|
||
});
|
||
|
||
it('follows its object when the placement around it becomes container content', () => {
|
||
const f = nested();
|
||
expect(f.asset.objects.find((o) => o.id === f.tWatcher)!.overrides.target).toEqual({ $object: f.crate });
|
||
expect(f.object(f.lookout).overrides.target).toEqual({ $object: f.copyIn(f.placements[0]) });
|
||
const run = startWorld(f);
|
||
const lookout = run.world.findByObjectId(f.lookout)!;
|
||
expect(run.world.findReferenced(lookout, f.copyIn(f.placements[0]))?.objectId).toBe(
|
||
f.copyIn(f.placements[0]),
|
||
);
|
||
});
|
||
|
||
it('is spelled out as the copy it names when the container is unpacked', () => {
|
||
const f = nested();
|
||
const [, placed] = f.placements;
|
||
f.stack.execute(new UnpackContainerCommand(f.sceneId, placed));
|
||
expect(f.object(instanceChildId(placed, f.tWatcher)).overrides.target).toEqual({
|
||
$object: f.copyIn(placed),
|
||
});
|
||
});
|
||
|
||
it('reaches content nested deeper as well, from content of a placement', () => {
|
||
expect(referenceCandidates('r~q/x', 'p~z')).toEqual(['r~q/p/z', 'p~z']);
|
||
expect(referenceCandidates('r~x', 'p~z/y')).toEqual(['r~p/z/y', 'p~z/y']);
|
||
});
|
||
|
||
it('points a pasted copy at the content of the copied placement', () => {
|
||
const f = watching();
|
||
const inner = new CreateContainerCommand(f.sceneId, [f.crate]);
|
||
f.stack.execute(inner);
|
||
const tCrate = (findAsset(f.project, inner.assetId) as ContainerAsset).objects[0]!.id;
|
||
const both = copyObjects(f.project, f.sceneOf(), [f.watcherId, inner.objectId])!;
|
||
const paste = new PasteObjectsCommand(f.sceneId, both, {}, { x: 5, y: 0 });
|
||
f.stack.execute(paste);
|
||
const copies = paste.rootIds.map((id) => f.object(id));
|
||
const watcherCopy = copies.find((o) => o.assetId === f.behavior.id)!;
|
||
const innerCopy = copies.find((o) => o.assetId === inner.assetId)!;
|
||
expect(watcherCopy.overrides.target).toEqual({ $object: instanceChildId(innerCopy.id, tCrate) });
|
||
});
|
||
});
|
||
|
||
describe('deleting an asset (#286)', () => {
|
||
it('empties a reference to an object placed from it, and undo brings both back', () => {
|
||
const f = watching();
|
||
f.stack.execute(new DeleteAssetCommand(f.spriteId));
|
||
expect(f.object(f.watcherId).overrides.target).toEqual(NO_OBJECT);
|
||
f.stack.undo();
|
||
expect(f.object(f.watcherId).overrides.target).toEqual({ $object: f.crate });
|
||
expect(f.object(f.crate).name).toBe('Crate');
|
||
});
|
||
|
||
it('empties a reference into a placement of a deleted container', () => {
|
||
const f = watching();
|
||
const create = new CreateContainerCommand(f.sceneId, [f.crate]);
|
||
f.stack.execute(create);
|
||
const named = f.object(f.watcherId).overrides.target;
|
||
expect(referencedId(named)).toContain(create.objectId);
|
||
f.stack.execute(new DeleteAssetCommand(create.assetId));
|
||
expect(f.object(f.watcherId).overrides.target).toEqual(NO_OBJECT);
|
||
f.stack.undo();
|
||
expect(f.object(f.watcherId).overrides.target).toEqual(named);
|
||
});
|
||
});
|