An animated or set Z of -1 on a weapon put it behind everything on Z 0, not just behind the player holding it. A child can now opt in to being drawn at its parent's place, so its own Z only orders it against the parent and its siblings. - New property "Sort with parent" on sprites, texts and particle emitters, shown in the inspector right under Z. It is a normal property, set on the asset and overridable per object. - With it on, the rest of the scene sees only the parent's Z and, where the layer sorts by Y, the parent's feet. Below 0 the child is drawn behind the parent, from 0 on in front. - It works across levels: a grandchild with the switch hangs on the top object of the chain. - It has no effect under a parent that is not drawn itself (container, behavior, camera, collider) or under a tilemap. - New assets start with it on, saved ones read as off, so old projects draw unchanged. For that a schema row now has "initial" next to "default". The gold "changed" mark and reset follow "initial", so a fresh sprite is not marked as changed. No load migration is needed. - parentGroups() in drawOrder.ts gives a slot only to the top of a chain and inserts the children there. Editor, play mode and picking all use it, so they agree. Tests: - 10 new unit tests in tests/viewport/sort-with-parent.test.ts. - 8 existing tests list the sprite rows or defaults and got the row. - New e2e check 5b-ii clicks the switch and checks order and undo. - Counter-checks, all originals restored by cmp: switch ignored fails 7, children not sorted by Z fails 6, no chain fails 1, "initial" ignored fails 9, row missing in the inspector fails e2e 5. - ./check.sh: 166 files, 2426 tests, CHECK EXIT 0. - e2e 5: ALL PASSED, 13 checks. Check total 1398, computed only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
146 lines
5.2 KiB
TypeScript
146 lines
5.2 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import { buildAsset } from '../../src/core/commands/assetCommands';
|
||
import { createEmptyProject } from '../../src/core/model/Project';
|
||
import { createSceneObject } from '../../src/core/model/SceneObject';
|
||
import { coerceValue, defaultsFor, schemaFor } from '../../src/core/properties/PropertySchema';
|
||
import {
|
||
deviatesFromAsset,
|
||
deviatesFromTemplate,
|
||
inheritedValue,
|
||
isModified,
|
||
resolveAssetProps,
|
||
resolveSceneObjectProps,
|
||
} from '../../src/core/properties/resolve';
|
||
|
||
describe('PropertySchema', () => {
|
||
it('has no position on any asset type', () => {
|
||
for (const type of ['scene', 'camera', 'sprite'] as const) {
|
||
const keys = schemaFor(type).map((d) => d.key);
|
||
expect(keys).not.toContain('x');
|
||
expect(keys).not.toContain('y');
|
||
expect(keys).not.toContain('position');
|
||
}
|
||
});
|
||
|
||
it('provides defaults for every key', () => {
|
||
expect(defaultsFor('sprite')).toEqual({
|
||
enabled: true,
|
||
scale: 1,
|
||
rotation: 0,
|
||
mirrorX: false,
|
||
mirrorY: false,
|
||
sortWithParent: false,
|
||
width: 100,
|
||
height: 100,
|
||
color: 0xffffff,
|
||
texture: '',
|
||
shader: '',
|
||
});
|
||
});
|
||
|
||
it('coerces values against the definition', () => {
|
||
const width = schemaFor('sprite').find((d) => d.key === 'width')!;
|
||
expect(coerceValue(width, -5)).toBe(1);
|
||
expect(coerceValue(width, '42')).toBe(42);
|
||
expect(coerceValue(width, 'abc')).toBe(100);
|
||
const color = schemaFor('sprite').find((d) => d.key === 'color')!;
|
||
expect(coerceValue(color, '#ff0000')).toBe(0xff0000);
|
||
expect(coerceValue(color, 0x1ffffff)).toBe(0xffffff);
|
||
});
|
||
});
|
||
|
||
describe('resolve', () => {
|
||
const project = createEmptyProject('p', 'P');
|
||
it('fills asset props with defaults and ignores unknown keys', () => {
|
||
const sprite = buildAsset('sprite', 'Box');
|
||
sprite.props = { width: 50, bogus: 1 };
|
||
expect(resolveAssetProps(project, sprite)).toEqual({
|
||
enabled: true,
|
||
scale: 1,
|
||
rotation: 0,
|
||
mirrorX: false,
|
||
mirrorY: false,
|
||
sortWithParent: false,
|
||
width: 50,
|
||
height: 100,
|
||
color: 0xffffff,
|
||
texture: '',
|
||
shader: '',
|
||
});
|
||
});
|
||
|
||
it('applies sparse overrides on top of asset values', () => {
|
||
const sprite = buildAsset('sprite', 'Box');
|
||
sprite.props.color = 0x00ff00;
|
||
const object = createSceneObject('o1', sprite.id, { x: 10, y: 20 });
|
||
expect(resolveSceneObjectProps(project, sprite, object)).toEqual({
|
||
enabled: true,
|
||
scale: 1,
|
||
rotation: 0,
|
||
mirrorX: false,
|
||
mirrorY: false,
|
||
sortWithParent: true,
|
||
width: 100,
|
||
height: 100,
|
||
color: 0x00ff00,
|
||
texture: '',
|
||
shader: '',
|
||
});
|
||
|
||
object.overrides.color = 0x0000ff;
|
||
expect(resolveSceneObjectProps(project, sprite, object).color).toBe(0x0000ff);
|
||
expect(inheritedValue(project, sprite, 'color')).toBe(0x00ff00);
|
||
});
|
||
|
||
it('detects asset values that differ from the schema default', () => {
|
||
const sprite = buildAsset('sprite', 'Box');
|
||
expect(isModified(project, sprite, 'width')).toBe(false); // explicit default counts as unmodified
|
||
sprite.props.width = 250;
|
||
expect(isModified(project, sprite, 'width')).toBe(true);
|
||
delete sprite.props.height; // missing → resolved from default → unmodified
|
||
expect(isModified(project, sprite, 'height')).toBe(false);
|
||
expect(isModified(project, sprite, 'bogus')).toBe(false);
|
||
});
|
||
|
||
it('ignores an override that carries the asset value – nothing to reset or apply', () => {
|
||
const sprite = buildAsset('sprite', 'Box');
|
||
sprite.props.width = 250;
|
||
const object = createSceneObject('o1', sprite.id, { x: 0, y: 0 });
|
||
expect(deviatesFromAsset(project, object, sprite, 'width')).toBe(false); // no override at all
|
||
|
||
object.overrides.width = 250; // same value as the asset → not a deviation
|
||
expect(deviatesFromAsset(project, object, sprite, 'width')).toBe(false);
|
||
|
||
object.overrides.width = 300;
|
||
expect(deviatesFromAsset(project, object, sprite, 'width')).toBe(true);
|
||
|
||
// The asset moving to the object's value settles the difference again.
|
||
sprite.props.width = 300;
|
||
expect(deviatesFromAsset(project, object, sprite, 'width')).toBe(false);
|
||
});
|
||
|
||
it('compares an instance against its template as overrides, not as values', () => {
|
||
const sprite = buildAsset('sprite', 'Box');
|
||
const template = createSceneObject('t1', sprite.id, { x: 0, y: 0 });
|
||
const object = createSceneObject('o1', sprite.id, { x: 0, y: 0 });
|
||
expect(deviatesFromTemplate(object, template, 'width')).toBe(false); // neither overrides
|
||
|
||
template.overrides.width = 140;
|
||
expect(deviatesFromTemplate(object, template, 'width')).toBe(true);
|
||
|
||
object.overrides.width = 140;
|
||
expect(deviatesFromTemplate(object, template, 'width')).toBe(false);
|
||
|
||
object.overrides.width = 77;
|
||
expect(deviatesFromTemplate(object, template, 'width')).toBe(true);
|
||
});
|
||
|
||
it('keeps position on the scene object only', () => {
|
||
const sprite = buildAsset('sprite', 'Box');
|
||
const object = createSceneObject('o1', sprite.id, { x: 10, y: 20 });
|
||
expect(object.position).toEqual({ x: 10, y: 20, z: 0 });
|
||
expect('position' in sprite).toBe(false);
|
||
expect(resolveSceneObjectProps(project, sprite, object)).not.toHaveProperty('x');
|
||
});
|
||
});
|