Let a child sort with its parent so its Z stays local (#232)
game-maker / gate (push) Successful in 3m7s
game-maker / suite (push) Failing after 8m51s

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>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-25 23:08:09 +02:00
1 parent fae827c087
commit 8b44f97c35
13 files changed
+351 -15

No files matched your search

+3 -3
View File
@@ -25,14 +25,14 @@ import {
} from '../model/Scope';
import type { SceneObject } from '../model/SceneObject';
import { isTextureFile } from '../model/texture';
import { coerceValue, defaultsFor } from '../properties/PropertySchema';
import { coerceValue, defaultsFor, initialOf, initialPropsFor } from '../properties/PropertySchema';
import { type Removed, removeById, removeFirst, restoreAt } from '../util/list';
import { type Command, CommandError } from './Command';
import { requireAsset, requirePropDef, requireStateMachine } from './require';
import { SetBagValueCommand } from './SetBagValueCommand';
export function buildAsset(type: AssetType, name: string, id = newId()): Asset {
const base = { id, name, props: defaultsFor(type) };
const base = { id, name, props: initialPropsFor(type) };
switch (type) {
case 'texture':
return buildTextureAsset(name, { file: '', width: 0, height: 0 }, id);
@@ -436,7 +436,7 @@ export class ResetAssetPropertyCommand extends AssetPropertyCommand {
}
protected override computeNext(asset: Asset, project: Project): PropertyValue {
return requirePropDef(project, asset, this.key).default;
return initialOf(requirePropDef(project, asset, this.key));
}
}
+50 -4
View File
@@ -91,9 +91,22 @@ type Extras<K extends FlatKind> = K extends 'number'
* (see `isRowShown`), everything else – graph, defaults, files – always has it.
*/
export type FlatDef<K extends FlatKind = FlatKind> = {
[P in K]: { key: string; label: string; kind: P; default: ValueOf<P>; shownWith?: string } & Extras<P>;
[P in K]: {
key: string;
label: string;
kind: P;
default: ValueOf<P>;
/** What a new asset starts with when it differs from `default`, the value a missing key reads as. */
initial?: ValueOf<P>;
shownWith?: string;
} & Extras<P>;
}[K];
/** The value a new asset starts with, which is also what Reset goes back to. */
export function initialOf(def: PropertyDef): PropertyValue {
return 'initial' in def && def.initial !== undefined ? def.initial : def.default;
}
/** Whether the inspector shows the row, given the values of the bag it is on. */
export function isRowShown(def: PropertyDef, values: PropertyBag): boolean {
return !('shownWith' in def) || def.shownWith === undefined || values[def.shownWith] === true;
@@ -233,9 +246,30 @@ export function isTransformProp(key: string): boolean {
return TRANSFORM_KEYS.has(key);
}
const COMMON_KEYS = new Set([...COMMON_PROPS, ...TRANSFORM_PROPS].map((d) => d.key));
export const SORT_WITH_PARENT_KEY = 'sortWithParent';
/** The switch and the transform rows: reserved names, shown apart from the type's own properties. */
/**
* Whether a child is drawn in its parent's place instead of on its own (#232, see `drawSlots`).
* A project stored before it reads as off, so it draws as it did; a new asset starts with it on.
*/
export const SORT_PROPS: PropertySchema = [
{ key: SORT_WITH_PARENT_KEY, label: 'Sort with parent', kind: 'boolean', default: false, initial: true },
];
/** A tilemap has no single place in the draw order, and the other types are not drawn at all. */
const SORT_TYPES: ReadonlySet<AssetType> = new Set(['sprite', 'text', 'particles']);
export function hasSortProps(type: AssetType): boolean {
return SORT_TYPES.has(type);
}
export function isSortProp(key: string): boolean {
return key === SORT_WITH_PARENT_KEY;
}
const COMMON_KEYS = new Set([...COMMON_PROPS, ...TRANSFORM_PROPS, ...SORT_PROPS].map((d) => d.key));
/** The switches, the transform and sort rows: reserved names, shown apart from the type's own properties. */
export function isCommonProp(key: string): boolean {
return COMMON_KEYS.has(key);
}
@@ -585,7 +619,12 @@ const FULL_SCHEMAS: Record<AssetType, PropertySchema> = Object.fromEntries(
(Object.keys(ASSET_SCHEMAS) as AssetType[]).map((type) => [
type,
isPlaceableType(type)
? [...COMMON_PROPS, ...(hasTransformProps(type) ? TRANSFORM_PROPS : []), ...ASSET_SCHEMAS[type]]
? [
...COMMON_PROPS,
...(hasTransformProps(type) ? TRANSFORM_PROPS : []),
...(hasSortProps(type) ? SORT_PROPS : []),
...ASSET_SCHEMAS[type],
]
: ASSET_SCHEMAS[type],
]),
) as Record<AssetType, PropertySchema>;
@@ -686,6 +725,13 @@ export function defaultsOf(schema: PropertySchema): PropertyBag {
return bag;
}
/** The bag a new asset of the type is created with. */
export function initialPropsFor(type: AssetType): PropertyBag {
const bag: PropertyBag = {};
for (const def of FULL_SCHEMAS[type]) bag[def.key] = initialOf(def);
return bag;
}
export function variableDef(
key: string,
kind: VariableKind,
+3 -3
View File
@@ -2,7 +2,7 @@ import type { Asset } from '../model/Asset';
import { type PropertyBag, type PropertyValue, sameValue } from '../model/Properties';
import type { Project } from '../model/Project';
import type { SceneObject } from '../model/SceneObject';
import { propDef, schemaForAsset, schemaForObject } from './PropertySchema';
import { initialOf, propDef, schemaForAsset, schemaForObject } from './PropertySchema';
/**
* Schema defaults filled with the asset's own values. Keys unknown to the schema are ignored, so
@@ -38,12 +38,12 @@ function valuesOver(
return bag;
}
/** Whether the asset's own value differs from the schema default – the asset-level override. */
/** Whether the asset's own value differs from what a new asset starts with – the asset-level override. */
export function isModified(project: Project, asset: Asset, key: string): boolean {
const def = propDef(project, asset, key);
if (!def) return false;
const own = asset.props[key];
return own !== undefined && !sameValue(own, def.default);
return own !== undefined && !sameValue(own, initialOf(def));
}
/** The value the object would have without its override for `key`. */