Let a container animation reach alike-named objects in another one
game-maker / gate (push) Successful in 3m10s
game-maker / suite (push) Successful in 15m43s

An animation over a container aims its rows at template ids. Played on
another container, those ids are not there, so every row below the
root was skipped without a word. "Enemy - Hit" built over the Slime
flashed the slime's Sprite but never the skeleton's.

- When the template id is not found below the object, the runtime
  takes the names on the way to the row's target in the root and
  walks the same names below the object, level by level.
- The id still comes first, so the root's own instances bind as
  before.
- A name missing at some level, or taken twice, skips the row, as
  a gone target always did.
- The rule is the pure function targetNamePath in animation.ts. The
  AnimationSystem only walks the children.

Tests:
- animation.test.ts: 84 tests, all green (+2). One plays over a
  second container built alike, renamed and with the name twice.
  The other names a row two containers deep.
- Counter-check: with the name fallback off, the new runtime test
  fails (1 of 84).
- Project cbbf160bb7e8 (revision 14407), Demo scene: "Enemy - Hit"
  and "Enemy - Die" change Flash and scale on the skeleton's Sprite.
  Without the fallback the skeleton stays unchanged.
- The gate did not run before this commit. CI covers it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-05 11:16:42 +02:00
1 parent 2da07599d8
commit 7b1796baac
3 files changed
+86 -2

No files matched your search

+19 -2
View File
@@ -10,6 +10,7 @@ import {
isPositionRow,
playbackOf,
startersOf,
targetNamePath,
trackWriteAt,
} from '../../core/model/animation';
import type { AnimationAsset, AnimationTrack } from '../../core/model/Asset';
@@ -136,7 +137,8 @@ function drop(entity: Entity, player: AnimationPlayer): void {
/**
* The rows that reach something on this object. A scene's rows aim at the scene itself or at its
* objects by id, and only while that scene runs; everything else aims at the object itself or at
* what its template placed inside it, however deep. A row whose target is gone, or lacks the
* what its template placed inside it, however deep – by template id, or else by the names on the way
* there, so one animation fits containers built alike. A row whose target is gone, or lacks the
* property, is skipped.
*/
function bind(world: World, asset: AnimationAsset, owner: Entity): Binding[] {
@@ -163,7 +165,10 @@ function bind(world: World, asset: AnimationAsset, owner: Entity): Binding[] {
if (root.type === 'scene')
entity = world.sceneId === root.id ? world.findByObjectId(track.target) : undefined;
else if (track.target === '') entity = owner;
else entity = world.findByObjectId(childId(owner.objectId, track.target));
else
entity =
world.findByObjectId(childId(owner.objectId, track.target)) ??
byNames(world, owner, targetNamePath(world.project, root, track.target));
const def = entity && defOn(world, entity, key);
if (entity && def) {
const aimed = entity;
@@ -197,6 +202,18 @@ function childId(ownerId: string, path: string): string {
return split ? instanceChildId(split.instanceId, `${split.path}/${path}`) : instanceChildId(ownerId, path);
}
/** The object below `owner` with these names, level by level; none when a name is missing or taken twice. */
function byNames(world: World, owner: Entity, names: string[] | undefined): Entity | undefined {
if (!names) return undefined;
let at: Entity | undefined = owner;
for (const name of names) {
const found: Entity[] = world.childrenOf(at).filter((e) => e.name === name);
at = found.length === 1 ? found[0] : undefined;
if (!at) return undefined;
}
return at;
}
function read(entity: Entity, key: string): PropertyValue {
if (isPositionRow(entity.type, key)) return entity[key];
return entity.props[key] ?? 0;