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:
1 parent
147f8ffbea
commit
fae827c087
4 files changed
+75
-4
No files matched your search
@@ -43,7 +43,7 @@ npm run build # writes dist/ and regenerates index.html; the folder is wh
|
||||
The browser suite and the three example games need a machine with a GPU:
|
||||
|
||||
```bash
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1396 checks (GM_STEPS=20 for one step)
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1397 checks (GM_STEPS=20 for one step)
|
||||
# every tool signs in first – see the Accounts page
|
||||
node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs
|
||||
node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs
|
||||
|
||||
@@ -25,8 +25,11 @@ export type Playback = (typeof PLAYBACKS)[number];
|
||||
/** What an animation can be built over: something placed, or a whole scene. */
|
||||
export const ANIMATION_ROOT_TYPES: readonly AssetType[] = ['sprite', 'text', 'container', 'scene'];
|
||||
|
||||
/** Position lives on the object, not in its property bag, so the timeline offers it by name. */
|
||||
const POSITION_KEYS = ['x', 'y'] as const;
|
||||
/**
|
||||
* Position lives on the object, not in its property bag, so the timeline offers it by name. `z` is
|
||||
* the draw order, which is sorted on every render, so a keyed `z` moves the object in front or behind.
|
||||
*/
|
||||
const POSITION_KEYS = ['x', 'y', 'z'] as const;
|
||||
|
||||
function isPositionKey(key: string): key is (typeof POSITION_KEYS)[number] {
|
||||
return (POSITION_KEYS as readonly string[]).includes(key);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -56,7 +56,13 @@ export async function animation(t) {
|
||||
);
|
||||
await evaluate("__gm.animation.rowDialogPick('an-arm', 'x')");
|
||||
await settle();
|
||||
const armProperties = (await json('__gm.animation.rowDialog()')).properties.length;
|
||||
const armOffered = (await json('__gm.animation.rowDialog()')).properties;
|
||||
const armProperties = armOffered.length;
|
||||
check(
|
||||
'the position rows come first, Z (the draw order) after X and Y',
|
||||
JSON.stringify(armOffered.slice(0, 3)) === JSON.stringify(['X', 'Y', 'Z']),
|
||||
JSON.stringify(armOffered),
|
||||
);
|
||||
const addButton = await find('Add row', true);
|
||||
await click(addButton.x, addButton.y);
|
||||
const row = (await walk()).tracks;
|
||||
|
||||
Reference in new issue
Block a user