Let an animation key Z so an object can pass behind another

The timeline offered X and Y as position rows but never Z. The list
of position rows was simply written without it when animations came
in with #97. A swinging weapon could therefore not go behind its
character.

- Z is the third position row, after X and Y, on every placed root
  or object. A scene and a behavior still offer no position.
- Play and preview already read and write any position row by name,
  so the one entry in POSITION_KEYS is all the code change.
- The draw order is sorted on every render, and play writes z back
  into the scene each frame. A keyed Z therefore takes effect at
  once. A blended 4.5 sits above 4 and below 5.

Tests:
- ./check.sh: 165 files, 2416 tests, CHECK EXIT 0.
- 3 new unit tests in tests/core/animation.test.ts: Z is offered
  after X and Y and not on a behavior or scene, a running game moves
  the object from behind to in front of another one, and play,
  scene write-back and preview agree on z.
- e2e 25: ALL PASSED, 29 checks. The new check reads X, Y, Z as the
  first rows of the row dialog. Check sum 1396 -> 1397.
- Counter-check: without z in the list all 3 new unit tests fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-25 22:08:00 +02:00
1 parent 147f8ffbea
commit fae827c087
4 files changed
+75 -4

No files matched your search

+62
View File
@@ -29,8 +29,10 @@ import { AddNodeCommand } from '../../src/core/commands/graphCommands';
import { SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
import { RenameVariableCommand } from '../../src/core/commands/variableCommands';
import {
animatableDefs,
animationDuration,
frameCount,
isPositionRow,
pauseAnimation,
PLACED_ROOT_ID,
playAnimation,
@@ -55,6 +57,8 @@ import { animationsUsing } from '../../src/core/model/usages';
import { deserializeProject } from '../../src/core/store/serialize';
import { AnimationSystem } from '../../src/runtime/systems/AnimationSystem';
import { World } from '../../src/runtime/World';
import { drawSlots } from '../../src/viewport/drawOrder';
import { collectWorldItems } from '../../src/viewport/items';
import {
graphEditor,
makeBehavior,
@@ -1349,3 +1353,61 @@ describe('the animation nodes', () => {
]);
});
});
describe('a row on z, the draw order', () => {
/** The sprite placed twice: `box` at z 0 plays a row moving z 0 → 10, `wall` stands at z 5. */
function climb() {
const f = makeSpriteScene();
const box = place(f, f.spriteId, { x: 0, y: 0, z: 0 });
const wall = place(f, f.spriteId, { x: 0, y: 0, z: 5 });
const walk = addAnimation(f, f.spriteId, { fps: 10 });
addRow(f, walk, 'z', [
[0, 0],
[10, 10],
]);
const started = startWorld(f, { animations: true });
const order = () =>
drawSlots(collectWorldItems(started.world).items)
.map((slot) => (slot.id === box ? 'box' : slot.id === wall ? 'wall' : ''))
.filter(Boolean);
return { f, walk, box, sprite: started.world.findByObjectId(box)!, order, ...started };
}
it('is offered after X and Y on a placed asset, and not on a behavior', () => {
expect(animatableDefs('sprite').slice(0, 3)).toMatchObject([
{ key: 'x', label: 'X' },
{ key: 'y', label: 'Y' },
{ key: 'z', label: 'Z', kind: 'number' },
]);
expect(animatableDefs('text').some((d) => d.key === 'z')).toBe(true);
expect(animatableDefs('behavior').some((d) => d.key === 'z')).toBe(false);
expect(isPositionRow('sprite', 'z')).toBe(true);
expect(isPositionRow('scene', 'z')).toBe(false);
});
it('moves the object behind and in front of another one while the game runs', () => {
const { walk, sprite, frame, order } = climb();
expect(order()).toEqual(['box', 'wall']);
playAnimation(sprite, walk.id);
frame(0.05);
frame(0.3);
expect(sprite.z).toBeCloseTo(3);
expect(order()).toEqual(['box', 'wall']);
frame(0.4);
expect(sprite.z).toBeCloseTo(7);
expect(order()).toEqual(['wall', 'box']);
});
it('writes z into the scene as the preview shows it', () => {
const { f, walk, box, sprite, world, frame } = climb();
playAnimation(sprite, walk.id);
frame(0.05);
frame(0.45);
world.syncToModel();
const previewed = previewHolder(f.project, findAssetOf(f.project, walk.id, 'animation')!, 4.5)!;
const root = previewed.objects.find((o) => o.id === PLACED_ROOT_ID)!;
expect(sprite.z).toBeCloseTo(4.5);
expect(root.position.z).toBeCloseTo(4.5);
expect(f.sceneOf().objects.find((o) => o.id === box)!.position.z).toBeCloseTo(4.5);
});
});