An animation reaches the scene's own background and objects inside placed containers (#109)
game-maker / gate (push) Successful in 1m33s
game-maker / suite (push) Successful in 5m37s

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-17 06:48:21 +02:00
1 parent 353b348c3c
commit f68941fa3a
7 files changed
+334 -128

No files matched your search

+85
View File
@@ -10,6 +10,8 @@ import {
SetTrackRelativeCommand,
} from '../../src/core/commands/animationCommands';
import { CreateAssetCommand } from '../../src/core/commands/assetCommands';
import { CommandError } from '../../src/core/commands/Command';
import { animatableDefs, animationTargets, targetAssetOf, trackLabel } from '../../src/core/model/animation';
import { CreateContainerCommand } from '../../src/core/commands/containerCommands';
import { RemoveSceneObjectsCommand } from '../../src/core/commands/sceneObjectCommands';
import { PREVIEW_ROOT_ID, previewHolder, previewObjectId } from '../../src/core/model/animationPreview';
@@ -183,3 +185,86 @@ describe('the preview of an animation over a scene', () => {
expect(f.sceneOf().objects.find((o) => o.id === box)!.position.y).toBe(2);
});
});
/** A sprite packed into `Inner`, `Inner` packed into `Outer`, and `Outer` placed in the scene. */
function nested() {
const f = makeSpriteScene();
const arm = place(f, f.spriteId, { x: 10, y: 20 });
const inner = new CreateContainerCommand(f.sceneId, [arm]);
f.stack.execute(inner);
const outer = new CreateContainerCommand(f.sceneId, [inner.objectId]);
f.stack.execute(outer);
const placing = (findAsset(f.project, outer.assetId) as ContainerAsset).objects[0]!;
const armTemplate = (findAsset(f.project, inner.assetId) as ContainerAsset).objects[0]!;
return { f, inner, outer, placing, armTemplate, path: `${placing.id}/${armTemplate.id}` };
}
describe('the reach of a row (#109)', () => {
it('offers what a container placed inside the root holds, as a path of template ids', () => {
const { f, outer, placing, path } = nested();
const targets = animationTargets(f.project, findAsset(f.project, outer.assetId));
expect(targets.map((t) => [t.id, t.depth])).toEqual([
[placing.id, 0],
[path, 1],
]);
});
it('offers a scene the content of its placed containers under the ids the running scene uses', () => {
const { f, outer, placing, path } = nested();
const targets = animationTargets(f.project, findAsset(f.project, f.sceneId));
expect(targets.map((t) => t.id)).toEqual([
outer.objectId,
instanceChildId(outer.objectId, placing.id),
instanceChildId(outer.objectId, path),
]);
});
it('draws a row aimed into a nested container on the child, through the instance edits of a copy', () => {
const { f, outer, path, armTemplate } = nested();
const walk = addAnimation(f, outer.assetId);
addRow(f, walk, 'x', [[0, 55]], path);
const before = JSON.stringify(f.project);
const child = drawn(f, current(f, walk.id), 0).get(instanceChildId(PREVIEW_ROOT_ID, path))!;
expect(child.position).toEqual({ ...armTemplate.position, x: 55 });
expect(JSON.stringify(f.project)).toBe(before);
});
it('draws a scene row aimed into a placed container without writing the scene', () => {
const { f, outer, path } = nested();
const id = instanceChildId(outer.objectId, path);
const walk = addAnimation(f, f.sceneId);
addRow(f, walk, 'y', [[0, 300]], id);
const before = JSON.stringify(f.project);
const holder = previewHolder(f.project, current(f, walk.id), 0)!;
expect(resolveObjects(f.project, holder).find((o) => o.id === id)!.position.y).toBe(300);
expect(JSON.stringify(f.project)).toBe(before);
});
it("keys the scene's own properties on the scene itself", () => {
const f = makeSpriteScene();
const walk = addAnimation(f, f.sceneId);
expect(animatableDefs('scene').map((d) => d.key)).toEqual(['backgroundColor']);
expect(() => f.stack.execute(new AddAnimationTrackCommand(walk.id, '', 'x'))).toThrow(CommandError);
addRow(f, walk, 'backgroundColor', [
[0, 0x000000],
[10, 0xff0000],
]);
const before = f.sceneOf().props.backgroundColor;
const holder = previewHolder(f.project, current(f, walk.id), 5)!;
expect(holder.props.backgroundColor).toBe(0x800000);
expect(collectHolderItems(f.project, holder, 0).background).toBe(0x800000);
expect(f.sceneOf().props.backgroundColor).toBe(before);
});
it('reads a row as missing once the template loses its target, and keeps its keys', () => {
const { f, outer, inner, path, armTemplate } = nested();
const walk = addAnimation(f, outer.assetId);
const row = addRow(f, walk, 'x', [[0, 55]], path);
f.stack.execute(new RemoveSceneObjectsCommand(inner.assetId, [armTemplate.id]));
const now = current(f, walk.id);
const track = now.tracks.find((t) => t.id === row)!;
expect(targetAssetOf(f.project, now, path)).toBeUndefined();
expect(trackLabel(f.project, now, track)).toBe('(missing) · x');
expect(track.keys).toEqual([{ frame: 0, value: 55 }]);
});
});
+69
View File
@@ -414,6 +414,75 @@ describe('the AnimationSystem', () => {
});
});
describe('the reach of a played row (#109)', () => {
/** A sprite packed into `Inner`, `Inner` packed into `Outer`, and `Outer` placed in the scene. */
function nested() {
const f = makeSpriteScene();
const arm = place(f, f.spriteId, { x: 0, y: 0 });
const inner = new CreateContainerCommand(f.sceneId, [arm]);
f.stack.execute(inner);
const outer = new CreateContainerCommand(f.sceneId, [inner.objectId]);
f.stack.execute(outer);
const placing = (findAsset(f.project, outer.assetId) as ContainerAsset).objects[0]!;
const armTemplate = (findAsset(f.project, inner.assetId) as ContainerAsset).objects[0]!;
return { f, outer, path: `${placing.id}/${armTemplate.id}` };
}
const slideX: [number, AnimationKey['value']][] = [
[0, 0],
[10, 100],
];
it('reaches a child two containers deep on each instance of the root on its own', () => {
const { f, outer, path } = nested();
const second = place(f, outer.assetId, { x: 500, y: 0 });
const walk = addAnimation(f, outer.assetId, { fps: 10 });
addRow(f, walk, 'x', slideX, path);
const { world, frame } = startWorld(f, { animations: true });
playAnimation(world.findByObjectId(outer.objectId)!, walk.id);
frame(0.1);
frame(0.5);
const x = (instance: string) => world.findByObjectId(instanceChildId(instance, path))!.x;
expect(x(outer.objectId)).toBeCloseTo(50);
expect(x(second)).toBe(0);
});
it('reaches into a container placed in a scene, baked for play or resolved for the editor run', () => {
for (const bakeContainers of [true, false]) {
const { f, outer, path } = nested();
const box = place(f, f.spriteId, { x: 0, y: 0 });
const id = instanceChildId(outer.objectId, path);
const walk = addAnimation(f, f.sceneId, { fps: 10 });
addRow(f, walk, 'x', slideX, id);
const world = World.instantiate(f.project, f.sceneId, { bakeContainers });
world.addSystem(new AnimationSystem());
playAnimation(world.findByObjectId(box)!, walk.id);
world.tick(0.1);
world.tick(0.5);
expect(world.findByObjectId(id)!.x).toBeCloseTo(50);
}
});
it("plays the scene's own background, and a once-reset pass puts it back", () => {
const f = makeSpriteScene();
const box = place(f, f.spriteId, { x: 0, y: 0 });
f.sceneOf().props.backgroundColor = 0x123456;
const walk = addAnimation(f, f.sceneId, { fps: 10, playback: 'once-reset' });
addRow(f, walk, 'backgroundColor', [
[0, 0x000000],
[10, 0xff0000],
]);
const { world, frame } = startWorld(f, { animations: true });
playAnimation(world.findByObjectId(box)!, walk.id);
frame(0.1);
frame(0.5);
expect(world.backgroundColor).toBe(0x800000);
for (let i = 0; i < 10; i++) frame(0.1);
expect(world.backgroundColor).toBe(0x123456);
expect(f.sceneOf().props.backgroundColor).toBe(0x123456);
});
});
describe('the animation nodes', () => {
/** A behavior below the sprite plays `walk` on its owner at start and counts `On animation finished`. */
function listening(filter: 'walk' | 'other') {