A composition can now sit in a scene like a container and play there. Its items become real objects while they run, so behaviors, colliders and events work on them as on any other object. - A composition is placeable with its own symbol, transform and render layer. The scene view shows its picture at frame 0. A click on that picture selects the placement, not a single item. - CompositionSystem spawns each item as an object under the placement when it begins, with the id <placement>~<item>. On start and the other events run. The object is destroyed when the item ends. - The top track lies in front no matter which item started first. World.arrange() orders the entity list and the scene objects. - Clips and driven videos follow the same rules as compositionAt(). Loop, restart, stop and the game pause work. - New nodes: Play composition (with restart), Stop composition and the event On composition finished. They were planned for step 4 but come now, because without autoplay nothing could play otherwise. - The editor run ignores Play composition and says so in the log. It writes into the saved project, and the items would stay behind. - contentId() moved to instance.ts. The animation system uses it for nested ids too. - Compositions get no "Sort with parent" for now. Tests: the gate passed with 301 files and 4643 tests, 18 more than step 1. 13 of them are in tests/core/composition-play.test.ts. The first gate run failed in event-order.test.ts, which counts the event nodes and now finds 19. Five counter-checks each turned exactly their test red. Two order tests stayed green without the fix at first and were rebuilt. e2e 68 is new and passed with 5 own checks. Its counter-check without arrange() failed as expected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
125 lines
4.9 KiB
TypeScript
125 lines
4.9 KiB
TypeScript
/**
|
||
* What the scene view shows of each placeable type – symbol, name, a collider's outline. It lives
|
||
* in the project like the game scale: saved, but never an undo step.
|
||
*/
|
||
import { describe, expect, it } from 'vitest';
|
||
import { createEmptyProject } from '../../src/core/model/Project';
|
||
import {
|
||
defaultSceneVisibility,
|
||
sceneVisibilityOf,
|
||
shows,
|
||
VISIBILITY_TYPES,
|
||
visibilityParts,
|
||
} from '../../src/core/model/sceneVisibility';
|
||
import { deserializeProject, serializeProject } from '../../src/core/store/serialize';
|
||
import { EditorState } from '../../src/editor/state';
|
||
|
||
function loaded() {
|
||
const state = new EditorState();
|
||
state.loadProject(createEmptyProject('p', 'P'), 1);
|
||
return state;
|
||
}
|
||
|
||
describe('scene visibility', () => {
|
||
it('lists every placeable type, Behavior to Tilemap', () => {
|
||
expect(VISIBILITY_TYPES).toEqual([
|
||
'behavior',
|
||
'camera',
|
||
'clip',
|
||
'collider',
|
||
'composition',
|
||
'container',
|
||
'joint',
|
||
'particles',
|
||
'rigidbody',
|
||
'sprite',
|
||
'text',
|
||
'tilemap',
|
||
]);
|
||
});
|
||
|
||
it('gives a symbol switch only to types with a symbol, and an outline switch to what collides', () => {
|
||
expect(visibilityParts('behavior')).toEqual(['symbol', 'label']);
|
||
expect(visibilityParts('collider')).toEqual(['symbol', 'label', 'shape']);
|
||
expect(visibilityParts('sprite')).toEqual(['label']);
|
||
expect(visibilityParts('tilemap')).toEqual(['label', 'shape']);
|
||
});
|
||
|
||
it('gives only a camera a switch for its shader, off until switched on (#299)', () => {
|
||
expect(visibilityParts('camera')).toEqual(['symbol', 'label', 'shader']);
|
||
expect(VISIBILITY_TYPES.filter((type) => visibilityParts(type).includes('shader'))).toEqual(['camera']);
|
||
const visibility = defaultSceneVisibility();
|
||
expect(shows(visibility, 'camera', 'shader')).toBe(false);
|
||
visibility.camera.shader = true;
|
||
expect(shows(visibility, 'camera', 'shader')).toBe(true);
|
||
visibility.sprite.shader = true;
|
||
expect(shows(visibility, 'sprite', 'shader')).toBe(false);
|
||
});
|
||
|
||
it('shows what it always showed in a new project and in one written before there was any', () => {
|
||
for (const visibility of [
|
||
createEmptyProject('p', 'P').sceneVisibility,
|
||
deserializeProject({ id: 'p', name: 'P', assets: [] }).sceneVisibility,
|
||
]) {
|
||
expect(shows(visibility, 'behavior', 'symbol')).toBe(true);
|
||
expect(shows(visibility, 'behavior', 'label')).toBe(true);
|
||
expect(shows(visibility, 'collider', 'shape')).toBe(true);
|
||
expect(shows(visibility, 'tilemap', 'shape')).toBe(true);
|
||
expect(shows(visibility, 'sprite', 'label')).toBe(false);
|
||
expect(shows(visibility, 'sprite', 'shape')).toBe(true);
|
||
}
|
||
});
|
||
|
||
it('survives a save and a load', () => {
|
||
const project = createEmptyProject('p', 'P');
|
||
project.sceneVisibility.collider.shape = false;
|
||
project.sceneVisibility.tilemap.shape = false;
|
||
project.sceneVisibility.rigidbody.label = false;
|
||
project.sceneVisibility.sprite.label = true;
|
||
project.sceneVisibility.camera.shader = true;
|
||
const back = deserializeProject(JSON.parse(JSON.stringify(serializeProject(project)))).sceneVisibility;
|
||
expect(back.camera.shader).toBe(true);
|
||
expect(back.collider.shape).toBe(false);
|
||
expect(back.tilemap.shape).toBe(false);
|
||
expect(back.rigidbody.label).toBe(false);
|
||
expect(back.sprite.label).toBe(true);
|
||
expect(back.behavior).toEqual(defaultSceneVisibility().behavior);
|
||
});
|
||
|
||
it('reads untrusted JSON part by part, the rest as the defaults', () => {
|
||
const read = sceneVisibilityOf({
|
||
sceneVisibility: { camera: { symbol: false, label: 'no' }, texture: { label: true }, text: 3 },
|
||
});
|
||
expect(read.camera).toEqual({ symbol: false, label: true, shape: true, shader: false });
|
||
expect(read.text).toEqual(defaultSceneVisibility().text);
|
||
expect('texture' in read).toBe(false);
|
||
expect(sceneVisibilityOf(null)).toEqual(defaultSceneVisibility());
|
||
});
|
||
|
||
it('never hides what a type does not have to hide', () => {
|
||
const visibility = defaultSceneVisibility();
|
||
visibility.sprite.symbol = false;
|
||
visibility.sprite.shape = false;
|
||
expect(shows(visibility, 'sprite', 'shape')).toBe(true);
|
||
visibility.behavior.shape = false;
|
||
expect(shows(visibility, 'behavior', 'shape')).toBe(true);
|
||
});
|
||
|
||
it('saves a switch without an undo step', () => {
|
||
const state = loaded();
|
||
state.setSceneVisibility('collider', 'shape', false);
|
||
expect(state.project.sceneVisibility.collider.shape).toBe(false);
|
||
expect(state.saveStatus).toBe('dirty');
|
||
expect(state.commands.canUndo).toBe(false);
|
||
});
|
||
|
||
it('keeps a switch flipped while playing after Stop', () => {
|
||
const state = loaded();
|
||
state.enterPlay();
|
||
state.setSceneVisibility('behavior', 'label', false);
|
||
expect(state.project.sceneVisibility.behavior.label).toBe(false);
|
||
state.exitPlay();
|
||
expect(state.project.sceneVisibility.behavior.label).toBe(false);
|
||
});
|
||
});
|