Let Destroy keep the children and hand them back as a list
game-maker / gate (push) Successful in 3m15s
game-maker / suite (push) Successful in 9m41s

Destroying a container used to take everything below it along. Now
Destroy can remove just the object itself and pass its direct children
on, so a behavior can keep working with what was inside.

- New input keep children (bool, default off). When on, only the
  object goes. Its direct children move up to the object's parent, or
  to the top level if there is none. Grandchildren stay where they are.
- New output children (object list) with the direct children that were
  kept. It is empty when keep children is off.
- The children keep their place on screen. carriedTransform folds the
  destroyed object's position, rotation and scale into their own, the
  same as reparenting in the editor.
- Behaviors and the rigidbody of the object are always destroyed with
  it and are not in the list. Moved up they would steer or simulate
  the parent.
- World remembers which children it reparented, and syncToModel writes
  their new parent into the scene. A reload of the model keeps it.
- Limit, read from the code only: in the editor run the contents of a
  placed container are not baked. Moved children there last for the
  run only.

Tests:
- ./check.sh: 170 files, 2478 tests, CHECK EXIT 0
- e2e 34 (the module with a Destroy node): ALL PASSED, 21 checks
- world-sync: world matrix of child and grandchild unchanged under a
  rotated and scaled parent, parent survives syncFromModel, top level
  case, already destroyed object gives []
- destroy-keep-children: real graph Destroy(Owner) -> Log(children)
  logs Lid, Label without the grandchild Nail
- 3 counter-probes, all caught: no world transform (1 test fails), no
  parent in the scene (1), behaviors and rigidbody moved up (2)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-26 21:45:27 +02:00
1 parent 7979b8ff19
commit 483c34bb5c
8 files changed
+196 -17

No files matched your search

+46 -1
View File
@@ -11,6 +11,9 @@ import type { Cell } from '../core/model/tilemap';
import { descendantsBelow } from '../core/model/walk';
import { findAsset, findAssetOf, findScene, type Project } from '../core/model/Project';
import { createSceneObject, type SceneObject } from '../core/model/SceneObject';
import { apply, IDENTITY } from '../core/model/matrix';
import { hasTransformProps } from '../core/properties/PropertySchema';
import { carriedTransform, localMatrixOf, transformOf } from '../core/properties/transform';
import { footprintContains } from '../core/properties/bounds';
import { colliderFrame, colliderShape, shapeContains } from '../core/properties/collider';
import { resolveSceneObjectProps } from '../core/properties/resolve';
@@ -94,6 +97,8 @@ export class World {
private elapsed = 0;
/** Set when `spawn` added an object; reported (and cleared) by the next `syncToModel`. */
private structureChanged = false;
/** Children a Destroy moved up to their grandparent; written into the scene by `syncToModel`. */
private readonly movedUp = new Set<Entity>();
private tilesChanged = false;
private constructor(
@@ -292,6 +297,14 @@ export class World {
let values = false;
const spawned = this.structureChanged;
this.structureChanged = false;
// Moved up by a Destroy that kept them. Only a stored object can say so: a container's content
// has its place in the template, which is why the editor run cannot destroy it for good either.
for (const entity of this.movedUp) {
const object = scene.objects.find((o) => o.id === entity.objectId);
if (object && this.entities.includes(entity)) object.parentId = entity.parent?.objectId ?? '';
}
const reparented = this.movedUp.size > 0;
this.movedUp.clear();
const alive = new Set(this.entities.map((e) => e.objectId));
const before = scene.objects.length;
for (let i = scene.objects.length - 1; i >= 0; i--) {
@@ -322,7 +335,7 @@ export class World {
for (const key of drifted) ref.setOverride(key, withoutObjects(entity.props[key]));
values = true;
}
return { structure: spawned || scene.objects.length !== before, values };
return { structure: spawned || reparented || scene.objects.length !== before, values };
}
/** Registers a system; it starts immediately and is ticked in registration order. */
@@ -454,4 +467,36 @@ export class World {
const index = this.entities.indexOf(entity);
if (index >= 0) this.entities.splice(index, 1);
}
/**
* Removes only the entity: its children move up to its parent (top level when it had none) and
* stay where they are in the world. Its behaviors and its rigidbody are part of it rather than
* things it carries, so they go with it – moved up they would drive the parent instead.
* Answers the children that moved up.
*/
removeKeepingChildren(entity: Entity): Entity[] {
if (!this.entities.includes(entity)) return [];
const own = localMatrixOf(entity, transformOf(entity.props));
const kept: Entity[] = [];
for (const child of this.childrenOf(entity)) {
if (child.type === 'behavior' || child.type === 'rigidbody') continue;
const at = apply(own, child);
child.x = at.x;
child.y = at.y;
child.z += entity.z;
const before = transformOf(child.props);
const carried = carriedTransform(before, own, IDENTITY);
if (hasTransformProps(child.type) && carried !== before) {
child.props.rotation = carried.rotation;
child.props.scale = carried.scale;
child.props.mirrorX = carried.mirrorX;
child.props.mirrorY = carried.mirrorY;
}
child.parent = entity.parent;
this.movedUp.add(child);
kept.push(child);
}
this.remove(entity);
return kept;
}
}