Add nodes that ask whether an animation still runs (#233)
A graph could start, pause and stop an animation and hear it finish, but it could not ask in between whether one runs or how far it is. Two data nodes answer that now. - Animation playing takes an object and an animation and gives playing, progress and time left (s). It answers at the moment it is read: right after Play animation in the same pass it says on, and in the frame On animation finished fires it says off. - With Animation at None it answers for the running animation started last on the object. - Animation length gives the seconds of one pass, from the timeline and the frame rate alone. - The rule is a pure function in src/core/model/animation.ts (playerStatus, leadingPlayer). The nodes reach an animation's length through a new field animation on GraphLookup. - animationPort is exported so both nodes show the same picker as Play animation. Decided in the issue: a Once (reset) counts as running while it blends out, and progress and time left also hold for Loop and Ping-pong, at the place of the playhead. On Ping-pong progress falls again on the way back and time left counts to the next turn. Decided here, the issue says nothing on it: - A paused animation does not count as running. Progress and time left stand still. - One that Stop animation fades out does not count as running, and its time left is 0. - Time left of a Once (reset) includes its blend out. While it blends out progress is 1 and time left is the rest of the fade. Finding, not fixed: on the node the name of the chosen animation overlaps the output label progress in the same row. That comes from the fixed node width in NodeView and was seen in the Fit view only. Tests: - Gate: 198 files, 2903 tests (2891 + 12), exit 0 - e2e 1-6: 282 checks passed, 06 has +2 for block 6z-ix - Nine unit counter-probes fail, 1 to 4 tests each - The browser counter-probe fails 6z-ix with ["false","false"] - e2e total 1615 is computed, not measured in a full run. The smokes were not run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
26cc6324af
commit
c1cc80bb45
11 files changed
+437
-4
No files matched your search
@@ -1106,6 +1106,7 @@ export async function graph(t) {
|
||||
await splitOutputs();
|
||||
await pluralLookups();
|
||||
await eventOrder();
|
||||
await animationPlaying();
|
||||
|
||||
await mutate(`__gm.state.select({ kind: 'asset', assetId: 'mover' });`);
|
||||
|
||||
@@ -1726,4 +1727,108 @@ export async function graph(t) {
|
||||
scene.objects = scene.objects.filter((o) => !o.id.startsWith('ord-'));
|
||||
`);
|
||||
}
|
||||
|
||||
// 6z-ix. Animation playing (#233): typed into the palette it arrives with its ports. In a run it
|
||||
// says true in the very pulse that plays a once animation, and false when that one finished.
|
||||
async function animationPlaying() {
|
||||
await mutate(`
|
||||
const p = __gm.state.project;
|
||||
const sprite = p.assets.find((a) => a.type === 'sprite');
|
||||
p.assets.push(
|
||||
{
|
||||
id: 'ap-swing',
|
||||
type: 'animation',
|
||||
name: 'Swing 6',
|
||||
props: { fps: 10, playback: 'once' },
|
||||
root: sprite.id,
|
||||
tracks: [{ id: 'ap-row', target: '', property: 'x', keys: [{ frame: 0, value: 0 }, { frame: 2, value: 20 }] }],
|
||||
},
|
||||
{
|
||||
id: 'apy',
|
||||
type: 'behavior',
|
||||
name: 'Asker',
|
||||
props: { enabled: true, runInEditor: false },
|
||||
variables: [],
|
||||
graph: { nodes: [], edges: [] },
|
||||
},
|
||||
);
|
||||
__gm.state.select({ kind: 'asset', assetId: 'apy' });
|
||||
`);
|
||||
await showTab('graph');
|
||||
const spot = await json('__gm.graph.emptySpot()');
|
||||
await click(spot.x, spot.y, 'right');
|
||||
await typeText('animation playing');
|
||||
await press('Enter');
|
||||
const asker = (await asset('apy')).graph.nodes[0];
|
||||
const types = asker ? await json(`__gm.graph.portTypes(${JSON.stringify(asker.id)})`) : null;
|
||||
const inputs = asker ? await json(`__gm.graph.nodeInputs(${JSON.stringify(asker.id)})`) : null;
|
||||
check(
|
||||
'typing "animation playing" into the palette adds the node: object and animation in, three answers out',
|
||||
asker?.type === 'AnimationPlaying' &&
|
||||
JSON.stringify(Object.keys(types?.inputs ?? {})) === JSON.stringify(['object', 'animation']) &&
|
||||
JSON.stringify(types?.outputs) ===
|
||||
JSON.stringify({ playing: 'boolean', progress: 'number', timeLeft: 'number' }) &&
|
||||
inputs?.[1] === 'animation None',
|
||||
JSON.stringify({ asker, types, inputs }),
|
||||
);
|
||||
if (!asker) return;
|
||||
const id = JSON.stringify(asker.id);
|
||||
await mutate(`
|
||||
const p = __gm.state.project;
|
||||
const behavior = p.assets.find((a) => a.id === 'apy');
|
||||
behavior.graph.nodes.find((n) => n.id === ${id}).params.animation = 'ap-swing';
|
||||
const node = (id, type, x, y, params = {}) => ({ id, type, x, y, params });
|
||||
const edge = (id, from, fromPort, to, toPort) =>
|
||||
({ id, from: { node: from, port: fromPort }, to: { node: to, port: toPort } });
|
||||
behavior.graph.nodes.push(
|
||||
node('ap-start', 'OnStart', -300, -200),
|
||||
node('ap-owner', 'Owner', -560, 0),
|
||||
node('ap-play', 'PlayAnimation', -40, -200, { animation: 'ap-swing', blend: 0, blendOut: 0, restart: false }),
|
||||
node('ap-said', 'Log', 420, -200),
|
||||
node('ap-finished', 'OnAnimationFinished', 0, 260, { animation: 'ap-swing', order: 0 }),
|
||||
node('ap-after', 'Log', 420, 260),
|
||||
);
|
||||
behavior.graph.edges.push(
|
||||
edge('ap-e1', 'ap-start', 'exec', 'ap-play', 'exec'),
|
||||
edge('ap-e2', 'ap-owner', 'object', 'ap-play', 'object'),
|
||||
edge('ap-e3', 'ap-owner', 'object', ${id}, 'object'),
|
||||
edge('ap-e4', 'ap-play', 'exec', 'ap-said', 'exec'),
|
||||
edge('ap-e5', ${id}, 'playing', 'ap-said', 'message'),
|
||||
edge('ap-e6', 'ap-finished', 'exec', 'ap-after', 'exec'),
|
||||
edge('ap-e7', ${id}, 'playing', 'ap-after', 'message'),
|
||||
);
|
||||
const scene = __gm.state.activeScene;
|
||||
const sprite = p.assets.find((a) => a.type === 'sprite');
|
||||
for (const [id, assetId, parentId, name] of [
|
||||
['ap-target', sprite.id, '', 'Swinger'],
|
||||
['ap-holder', 'apy', 'ap-target', 'Ask holder'],
|
||||
]) {
|
||||
const o = structuredClone(scene.objects[0]);
|
||||
Object.assign(o, { id, assetId, parentId, name, position: { x: 0, y: 0, z: 0 }, overrides: {}, instanceEdits: {} });
|
||||
delete o.groupId;
|
||||
scene.objects.push(o);
|
||||
}
|
||||
`);
|
||||
await clickText('Fit');
|
||||
await shot('28-animation-playing');
|
||||
const logFrom = t.consoleLog.length;
|
||||
await t.play();
|
||||
await sleep(900);
|
||||
await stop();
|
||||
const logged = t.consoleLog
|
||||
.slice(logFrom)
|
||||
.filter((line) => line.startsWith('[behavior Ask holder]'))
|
||||
.map((line) => line.split(']').pop().trim());
|
||||
check(
|
||||
'a run hears true right after Play animation, and false once the animation finished',
|
||||
JSON.stringify(logged) === JSON.stringify(['true', 'false']),
|
||||
JSON.stringify(logged),
|
||||
);
|
||||
await mutate(`
|
||||
const p = __gm.state.project;
|
||||
p.assets = p.assets.filter((a) => a.id !== 'apy' && a.id !== 'ap-swing');
|
||||
const scene = __gm.state.activeScene;
|
||||
scene.objects = scene.objects.filter((o) => !o.id.startsWith('ap-'));
|
||||
`);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user