game-maker / suite (push) Successful in 6m48s
The suite spent 88% of its step time asleep: 564 s of steps, 496 s of
them in sleep(n) - fixed spans estimated by hand and then copied from
step to step. That is why it was slow, and why it flickered.
The signal is afterFrames(count, timeout) in ui/render.ts, exposed as
__gm.afterFrames(). It wakes the loop itself, because an editor that
draws nothing would never redeem a promise of a frame, and it gives up
after a timeout instead of hanging the run. settle() in the harness
waits two drawn frames (~32 ms) instead of 150-300 ms and falls back to
sleep(150) when the hook is missing (an old build): silently not
waiting would make every later check a coin toss.
Every action goes through it now - click, doubleClick, wheel, drag,
dragAsset, press, typeText, select, mutate, undo, redo - and with that
about 550 sleep() calls behind those actions became redundant, helpers
that are nothing but a mutate in a coat included. What a frame cannot
answer got its own condition instead: waitUntil(expr), play()/stop()
waiting on __gm.state.mode rather than on 300 ms, saved() for the
autosave (the two hand-written polling loops sampled at 300 ms for
something that arrives after a second), and showTab() as a real tool -
47 places called evaluate("__gm.showTab(...)") raw, and 24 of them
waited for nothing at all and read the old panel.
564 s -> 243 s of step time, the waiting share 88% -> 76%. Three kinds
of wait were kept, and each one bit when it was taken away: loading
bytes (textures, fonts), game time that a repaint cannot stand in for
(time scale, shader and particle clocks, the live values a running
graph carries), and the inertia the scroll step measures on purpose.
Two things the new speed uncovered, both real: 25-animation read the
frame menu unfiltered and had frozen an incomplete list (the CI failure
on c990dbc), and 33-render-on-change measured idleness while the
autosave of the step before was still painting - it waits for
saveStatus to leave dirty/saving now, so it measures that nothing
happens any more instead of that it waited long enough.
519 lines
21 KiB
JavaScript
519 lines
21 KiB
JavaScript
import { colourDistance } from '../lib/harness.mjs';
|
||
import { edge, node, propertiesOf, variable, written } from '../lib/builders.mjs';
|
||
import { MAKE_OBJECT, openTab } from './lib/scaffold.mjs';
|
||
|
||
/**
|
||
* Animations: made from a texture, edited in their own tab, worn by a sprite through `texture`,
|
||
* run in play only, and heard by `On animation finished`. The step pastes a strip of four colour
|
||
* cells, so which frame is on screen is a `pixelAt` question. It removes everything it made.
|
||
*/
|
||
|
||
const CELLS = ['#e63c3c', '#3cc85a', '#466ef0', '#f5f5f5'];
|
||
const NAMES = ['red', 'green', 'blue', 'white'];
|
||
|
||
const PASTE_STRIP = `(async () => {
|
||
const canvas = document.createElement('canvas');
|
||
canvas.width = 64;
|
||
canvas.height = 16;
|
||
const g = canvas.getContext('2d');
|
||
${JSON.stringify(CELLS)}.forEach((colour, i) => { g.fillStyle = colour; g.fillRect(i * 16, 0, 16, 16); });
|
||
const blob = await new Promise((resolve) => canvas.toBlob(resolve, 'image/png'));
|
||
const transfer = new DataTransfer();
|
||
transfer.items.add(new File([blob], 'strip.png', { type: 'image/png' }));
|
||
window.dispatchEvent(new ClipboardEvent('paste', { clipboardData: transfer, bubbles: true }));
|
||
})()`;
|
||
|
||
const FRAME_IDS = ['an-f0', 'an-f1', 'an-f2', 'an-f3'];
|
||
|
||
function nearestCell(sample) {
|
||
let best = -1;
|
||
let bestDistance = Infinity;
|
||
CELLS.forEach((hex, i) => {
|
||
const distance = colourDistance(sample, hex);
|
||
if (distance < bestDistance) {
|
||
bestDistance = distance;
|
||
best = i;
|
||
}
|
||
});
|
||
return bestDistance < 60 ? NAMES[best] : `other ${sample}`;
|
||
}
|
||
|
||
/** `On animation finished (walk) → done += 1, and the owner wears Run`. */
|
||
const logicGraph = (walkId) => ({
|
||
nodes: [
|
||
node('an-done', 'OnAnimationFinished', 0, 0, { animation: walkId }),
|
||
node('an-count', 'Variables', 300, 0, {}, [written('done', 'number')]),
|
||
node('an-add', 'Add', 300, 200, { b: 1 }),
|
||
node('an-owner', 'Owner', 300, 360),
|
||
node('an-wear', 'Properties', 600, 0, { ...propertiesOf('sprite'), texture: 'an-run' }, [
|
||
written('texture', 'texture'),
|
||
]),
|
||
],
|
||
edges: [
|
||
edge('an-e1', 'an-done', 'exec', 'an-count', 'exec'),
|
||
edge('an-e2', 'an-count', 'done', 'an-add', 'a'),
|
||
edge('an-e3', 'an-add', 'result', 'an-count', 'done'),
|
||
edge('an-e4', 'an-count', 'exec', 'an-wear', 'exec'),
|
||
edge('an-e5', 'an-owner', 'object', 'an-wear', 'object'),
|
||
],
|
||
});
|
||
|
||
export async function animation(t) {
|
||
const { check, sleep, json, evaluate, shot, mutate, press, click, clickText, drag, undo, redo, showTab } =
|
||
t;
|
||
const { mouse, typeText } = t;
|
||
const { entity, stop } = t;
|
||
const info = () => json('__gm.animation.current()');
|
||
const frames = async (id) =>
|
||
await json(`__gm.state.project.assets.find((a) => a.id === ${JSON.stringify(id)})?.frames ?? null`);
|
||
const previewColour = async () => {
|
||
const rect = (await info()).preview;
|
||
return rect
|
||
? nearestCell(
|
||
await json(`__gm.pixelAt(${Math.round(rect.x + rect.w / 2)}, ${Math.round(rect.y + rect.h / 2)})`),
|
||
)
|
||
: 'none';
|
||
};
|
||
|
||
await press('Escape');
|
||
await evaluate('document.activeElement instanceof HTMLElement && document.activeElement.blur()');
|
||
await evaluate('__gm.state.select(null)');
|
||
|
||
// 25a. A strip of four cells, cut into four frames and moved to the head of the asset list.
|
||
await evaluate(PASTE_STRIP);
|
||
await sleep(2500);
|
||
const strip = await json(
|
||
"(__gm.state.project.assets.find((a) => a.type === 'texture' && a.name === 'strip') ?? {}).id ?? ''",
|
||
);
|
||
check('the strip is imported as a texture', strip !== '', strip);
|
||
await mutate(`
|
||
const p = __gm.state.project;
|
||
const index = p.assets.findIndex((a) => a.id === ${JSON.stringify(strip)});
|
||
const texture = p.assets.splice(index, 1)[0];
|
||
texture.props.filter = 'nearest';
|
||
texture.frames = ${JSON.stringify(FRAME_IDS)}.map((id, i) => ({ id, name: 'Cell ' + (i + 1), x: i * 16, y: 0, w: 16, h: 16 }));
|
||
p.assets.unshift(texture);
|
||
`);
|
||
|
||
// 25b. The texture's context menu makes an animation of its frames, as one undo step.
|
||
const row = await json(`__gm.assetRow(${JSON.stringify(strip)})`);
|
||
await click(row.x, row.y, 'right');
|
||
await clickText('Create animation from texture');
|
||
const made = await json(
|
||
"__gm.state.project.assets.filter((a) => a.type === 'animation').map((a) => ({ id: a.id, name: a.name, frames: a.frames, props: a.props }))",
|
||
);
|
||
const walk = made[0] ?? {};
|
||
check(
|
||
'Create animation from texture plays its frames in order',
|
||
made.length === 1 && JSON.stringify(walk.frames) === JSON.stringify(FRAME_IDS),
|
||
JSON.stringify(made),
|
||
);
|
||
check(
|
||
'and is selected, named after the texture, at 10 fps looping',
|
||
(await json('__gm.state.selection?.assetId')) === walk.id &&
|
||
walk.name === 'strip 2' &&
|
||
walk.props?.fps === 10 &&
|
||
walk.props?.playback === 'loop',
|
||
JSON.stringify({ selection: await json('__gm.state.selection'), walk }),
|
||
);
|
||
await undo();
|
||
const undone = await json("__gm.state.project.assets.filter((a) => a.type === 'animation').length");
|
||
await redo();
|
||
check(
|
||
'undo takes asset and frames back in one step, redo brings both',
|
||
undone === 0 && JSON.stringify(await frames(walk.id)) === JSON.stringify(FRAME_IDS),
|
||
JSON.stringify({ undone, after: await frames(walk.id) }),
|
||
);
|
||
|
||
// 25c. The Animation tab shows it and plays it; the inspector keeps its two properties.
|
||
await evaluate(`__gm.state.select({ kind: 'asset', assetId: ${JSON.stringify(walk.id)} })`);
|
||
await openTab(t, 'animation');
|
||
check(
|
||
'the inspector edits frames per second and playback',
|
||
(await t.has('Frames per second')) && (await t.has('Playback')) && (await t.has('Loop')),
|
||
);
|
||
const opened = await info();
|
||
check(
|
||
'the tab button has room for its bold label',
|
||
(await json("__gm.shownLabel('Animation')")) === 'Animation',
|
||
String(await json("__gm.shownLabel('Animation')")),
|
||
);
|
||
check(
|
||
'the Animation tab shows the selected animation with its four frames, playing',
|
||
(await json('__gm.tab()')) === 'animation' &&
|
||
opened.assetId === walk.id &&
|
||
opened.frames === 4 &&
|
||
opened.playing,
|
||
JSON.stringify(opened),
|
||
);
|
||
const shown = new Set();
|
||
for (let i = 0; i < 8; i++) {
|
||
shown.add((await info()).shown);
|
||
await sleep(70);
|
||
}
|
||
check('the preview runs through the frames', shown.size >= 3, JSON.stringify([...shown]));
|
||
await shot('01-playing');
|
||
|
||
// 25c-ii. (#90) The preview runs on the global clock like everything else that moves by itself,
|
||
// so a scale of 0 holds the frame on screen while the preview is still *playing*.
|
||
await evaluate('__gm.setTimeScale(0)');
|
||
await sleep(300);
|
||
const atZero = await info();
|
||
await sleep(500);
|
||
const stillZero = await info();
|
||
await evaluate('__gm.setTimeScale(1)');
|
||
const moved = new Set();
|
||
for (let i = 0; i < 8; i++) {
|
||
moved.add((await info()).shown);
|
||
await sleep(70);
|
||
}
|
||
check(
|
||
'a global time scale of 0 holds the frame without stopping the play, and 1 runs it on',
|
||
atZero.playing === true &&
|
||
stillZero.playing === true &&
|
||
stillZero.shown === atZero.shown &&
|
||
moved.size >= 3,
|
||
JSON.stringify({ atZero: atZero.shown, stillZero: stillZero.shown, afterwards: [...moved] }),
|
||
);
|
||
|
||
// 25d. Pausing holds the frame on screen, and the preview shows that frame's picture.
|
||
await click(...Object.values(await json('__gm.animation.playButton()')));
|
||
const paused = await info();
|
||
await sleep(350);
|
||
check(
|
||
'the play button pauses, and the frame on screen stays',
|
||
!paused.playing &&
|
||
(await json('__gm.animation.playLabel()')) === '▶' &&
|
||
(await info()).shown === paused.shown,
|
||
JSON.stringify({ paused, later: await info() }),
|
||
);
|
||
check(
|
||
'the preview draws the frame it says it shows',
|
||
(await previewColour()) === NAMES[paused.shown],
|
||
JSON.stringify({ shown: paused.shown, colour: await previewColour() }),
|
||
);
|
||
|
||
// 25e. A click on a tile selects it and shows it.
|
||
const tile = async (i) => json(`__gm.animation.tileCentre(${i})`);
|
||
const third = await tile(2);
|
||
await click(third.x, third.y);
|
||
const picked = await info();
|
||
check(
|
||
'clicking a tile selects it and the preview shows it',
|
||
picked.selected === 2 && picked.shown === 2 && (await previewColour()) === 'blue',
|
||
JSON.stringify({ picked, colour: await previewColour() }),
|
||
);
|
||
await shot('02-tile-picked');
|
||
|
||
// 25e'. The wheel zooms the preview about the pointer, the middle button pans, Fit undoes both.
|
||
const fitted = picked.preview;
|
||
const pointer = { x: Math.round(fitted.x + fitted.w * 0.25), y: Math.round(fitted.y + fitted.h * 0.5) };
|
||
const wheel = async (x, y, deltaY) => {
|
||
await mouse('mouseMoved', x, y);
|
||
await mouse('mouseWheel', x, y, 'none', { deltaX: 0, deltaY });
|
||
await sleep(250);
|
||
};
|
||
await wheel(pointer.x, pointer.y, -240);
|
||
const zoomed = (await info()).preview;
|
||
const share = (rect) => ((pointer.x - rect.x) / rect.w).toFixed(3);
|
||
check(
|
||
'the wheel zooms the preview in about the pointer',
|
||
zoomed.w > fitted.w * 1.3 && share(zoomed) === share(fitted),
|
||
JSON.stringify({ fitted, zoomed }),
|
||
);
|
||
check(
|
||
'the pixel under the pointer is still the frame shown',
|
||
nearestCell(await json(`__gm.pixelAt(${pointer.x}, ${pointer.y})`)) === 'blue',
|
||
);
|
||
await drag(pointer.x, pointer.y, pointer.x + 40, pointer.y - 30, { button: 'middle' });
|
||
const panned = (await info()).preview;
|
||
check(
|
||
'the middle button pans it',
|
||
Math.round(panned.x - zoomed.x) === 40 &&
|
||
Math.round(panned.y - zoomed.y) === -30 &&
|
||
panned.w === zoomed.w,
|
||
JSON.stringify({ zoomed, panned }),
|
||
);
|
||
await wheel(third.x, third.y, -240);
|
||
check(
|
||
'the wheel over the strip leaves the zoom alone',
|
||
JSON.stringify((await info()).preview) === JSON.stringify(panned),
|
||
JSON.stringify((await info()).preview),
|
||
);
|
||
await shot('02b-zoomed');
|
||
await clickText('Fit');
|
||
check(
|
||
'Fit puts the preview back',
|
||
JSON.stringify((await info()).preview) === JSON.stringify(fitted),
|
||
JSON.stringify((await info()).preview),
|
||
);
|
||
|
||
// 25f. Dragging a tile into another gap reorders the frames, as one undo step.
|
||
const first = await tile(0);
|
||
await drag(first.x, first.y, third.x + 39, third.y, 10);
|
||
const reordered = await frames(walk.id);
|
||
check(
|
||
'dragging the first tile behind the third moves that frame there',
|
||
JSON.stringify(reordered) === JSON.stringify(['an-f1', 'an-f2', 'an-f0', 'an-f3']) &&
|
||
(await info()).selected === 2,
|
||
JSON.stringify({ reordered, info: await info() }),
|
||
);
|
||
await undo();
|
||
check('undo puts it back', JSON.stringify(await frames(walk.id)) === JSON.stringify(FRAME_IDS));
|
||
|
||
// 25g. Delete removes the selected tile.
|
||
const second = await tile(1);
|
||
await click(second.x, second.y);
|
||
await press('Delete');
|
||
const deleted = await frames(walk.id);
|
||
check(
|
||
'Delete removes the selected frame and nothing else',
|
||
JSON.stringify(deleted) === JSON.stringify(['an-f0', 'an-f2', 'an-f3']) &&
|
||
(await json(`__gm.state.project.assets.some((a) => a.id === ${JSON.stringify(walk.id)})`)),
|
||
JSON.stringify(deleted),
|
||
);
|
||
await undo();
|
||
|
||
// 25h. + Frames offers a sheet's frames at once and inserts them after the selection.
|
||
await clickText('+ Frames');
|
||
// Search first: in the full suite a 17-frame sheet sorts ahead of this one and pushes the later
|
||
// cells out of the list's view, and a row outside its list is not on the screen to be read.
|
||
// The filter also sharpens the check, because the animation is called "strip 2" and would match.
|
||
await typeText('strip');
|
||
const offered = await json('__gm.overlayTexts()');
|
||
check(
|
||
'+ Frames offers the whole strip and each cell, but no animation',
|
||
offered.includes('strip – all 4 frames') &&
|
||
offered.includes('strip / Cell 3') &&
|
||
offered.includes('strip / Cell 4') &&
|
||
!offered.some((s) => s.startsWith('strip 2')),
|
||
JSON.stringify(offered),
|
||
);
|
||
await clickText('strip – all 4 frames');
|
||
const grown = await frames(walk.id);
|
||
check(
|
||
'all four go in right after the selected frame',
|
||
JSON.stringify(grown) === JSON.stringify(['an-f0', 'an-f1', ...FRAME_IDS, 'an-f2', 'an-f3']),
|
||
JSON.stringify(grown),
|
||
);
|
||
await undo();
|
||
|
||
// 25i. A sprite wears the animation through its Texture picker.
|
||
await mutate(`
|
||
const p = __gm.state.project;
|
||
const sprite = structuredClone(p.assets.find((a) => a.type === 'sprite'));
|
||
Object.assign(sprite, { id: 'an-sprite', name: 'Animated' });
|
||
sprite.props = { ...sprite.props, width: 80, height: 80, color: 0xffffff, texture: '' };
|
||
p.assets.unshift(sprite);
|
||
const scene = __gm.state.activeScene;
|
||
${MAKE_OBJECT}
|
||
const box = make('an-box', 'an-sprite', '', 120, 'Animated');
|
||
box.position.z = 950;
|
||
scene.objects.push(box);
|
||
__gm.state.select({ kind: 'asset', assetId: 'an-sprite' });
|
||
`);
|
||
await sleep(500);
|
||
const picker = await json("__gm.inspectorField('Texture', true)");
|
||
await click(picker.x, picker.y);
|
||
const choices = await json('__gm.overlayTexts()');
|
||
check(
|
||
'the Texture picker offers the animation first, marked as one',
|
||
choices.indexOf('strip 2 (animation)') === choices.indexOf('● None') + 1,
|
||
JSON.stringify(choices),
|
||
);
|
||
await clickText('strip 2 (animation)');
|
||
check(
|
||
'picking it makes the sprite wear the animation',
|
||
(await json("__gm.state.project.assets.find((a) => a.id === 'an-sprite').props.texture")) === walk.id,
|
||
);
|
||
|
||
// 25j. The scene shows the first frame and does not play.
|
||
await showTab('scene');
|
||
await sleep(700);
|
||
const box = await json("__gm.objectRect('an-box')");
|
||
const sceneColour = async () =>
|
||
nearestCell(await json(`__gm.pixelAt(${Math.round(box.x)}, ${Math.round(box.y)})`));
|
||
const before = await sceneColour();
|
||
await sleep(450);
|
||
check(
|
||
'the Scene tab shows the first frame and stays on it',
|
||
before === 'red' && (await sceneColour()) === 'red',
|
||
JSON.stringify({ box, before, after: await sceneColour() }),
|
||
);
|
||
await shot('03-scene-first-frame');
|
||
|
||
// 25j'. A frame is a texture of its own. Drawn from the sheet, an edge that landed inside a screen
|
||
// pixel showed the neighbouring cell: the partly covered pixel is evaluated at its centre,
|
||
// past the edge. Measured in play, where the background is flat.
|
||
await mutate(`__gm.state.project.assets.find((a) => a.id === 'an-sprite').props.texture = 'an-f1'`);
|
||
const rgb = (hex) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));
|
||
const fraction = (v) => v - Math.floor(v);
|
||
const edgesOf = async () => {
|
||
const r = await json("__gm.gameRect('an-box')");
|
||
return { y: Math.round(r.y), left: r.x - r.w / 2, right: r.x + r.w / 2, scale: r.w / 80 };
|
||
};
|
||
// Moves the box so the edge's fraction lands where the pixel is partly covered but its centre
|
||
// lies outside; `outward` says which way the neighbouring cell would be.
|
||
const edgePixels = async (side, target, outward) => {
|
||
await t.play();
|
||
const first = await edgesOf();
|
||
await stop();
|
||
const shift = (target - fraction(first[side]) + 1) % 1;
|
||
await mutate(
|
||
`__gm.state.activeScene.objects.find((o) => o.id === 'an-box').position.x += ${shift / first.scale}`,
|
||
);
|
||
await t.play();
|
||
const e = await edgesOf();
|
||
const x = Math.floor(e[side]);
|
||
const edge = rgb(await json(`__gm.pixelAt(${x}, ${e.y})`));
|
||
const outside = rgb(await json(`__gm.pixelAt(${x + outward * 2}, ${e.y})`));
|
||
await stop();
|
||
return { fraction: fraction(e[side]).toFixed(2), edge, outside };
|
||
};
|
||
// The edge pixel is the cell's green over the background and nothing else: red and blue rise
|
||
// above the background only in the cell's own proportion to the rise in green (plus rounding),
|
||
// and green rises enough to prove it is the partly covered pixel, not plain background.
|
||
const cell = rgb(CELLS[1]);
|
||
const onlyOwnColour = ({ edge, outside }) => {
|
||
const green = edge[1] - outside[1];
|
||
return (
|
||
green > 10 &&
|
||
[0, 2].every(
|
||
(c) => edge[c] - outside[c] <= (green * (cell[c] - outside[c])) / (cell[1] - outside[1]) + 3,
|
||
)
|
||
);
|
||
};
|
||
const leftEdge = await edgePixels('left', 0.75, -1);
|
||
check(
|
||
'the left edge of a frame shows nothing of the cell to its left',
|
||
onlyOwnColour(leftEdge),
|
||
JSON.stringify(leftEdge),
|
||
);
|
||
const rightEdge = await edgePixels('right', 0.25, 1);
|
||
check(
|
||
'nor the right edge anything of the cell to its right',
|
||
onlyOwnColour(rightEdge),
|
||
JSON.stringify(rightEdge),
|
||
);
|
||
await mutate(
|
||
`__gm.state.project.assets.find((a) => a.id === 'an-sprite').props.texture = ${JSON.stringify(walk.id)}`,
|
||
);
|
||
|
||
// 25k. Play runs it: the pixels the game draws change colour over time.
|
||
await t.play();
|
||
const colours = new Set();
|
||
for (let i = 0; i < 10; i++) {
|
||
const at = await json("__gm.gameScreen('an-box')");
|
||
if (at) colours.add(nearestCell(await json(`__gm.pixelAt(${Math.round(at.x)}, ${Math.round(at.y)})`)));
|
||
await sleep(60);
|
||
}
|
||
const clock = (await entity('an-box'))?.animation;
|
||
await shot('04-playing');
|
||
await stop();
|
||
check(
|
||
'in play the sprite runs through the frames',
|
||
[...colours].filter((c) => NAMES.includes(c)).length >= 3 && clock?.id === walk.id && clock.time > 0,
|
||
JSON.stringify({ colours: [...colours], clock }),
|
||
);
|
||
|
||
// 25l. Once: the end is an event, and a Properties node puts another animation on.
|
||
await mutate(`
|
||
const p = __gm.state.project;
|
||
p.assets.find((a) => a.id === ${JSON.stringify(walk.id)}).props.playback = 'once';
|
||
p.assets.unshift({ id: 'an-run', type: 'animation', name: 'Run', props: { fps: 10, playback: 'loop' }, frames: ['an-f3'] });
|
||
const logic = structuredClone(p.assets.find((a) => a.id === 'mover'));
|
||
Object.assign(logic, {
|
||
id: 'an-logic',
|
||
name: 'Animation logic',
|
||
props: { enabled: true, runInEditor: false, done: 0 },
|
||
variables: ${JSON.stringify([variable('done', 'number', 0)])},
|
||
graph: ${JSON.stringify(logicGraph(walk.id))},
|
||
});
|
||
p.assets.unshift(logic);
|
||
const scene = __gm.state.activeScene;
|
||
${MAKE_OBJECT}
|
||
scene.objects.push(make('an-brain', 'an-logic', 'an-box', 0, 'Animation logic'));
|
||
__gm.state.select(null);
|
||
`);
|
||
await t.play();
|
||
await sleep(900);
|
||
const brain = (await entity('an-brain'))?.props;
|
||
const worn = await entity('an-box');
|
||
const at = await json("__gm.gameScreen('an-box')");
|
||
const after = at
|
||
? nearestCell(await json(`__gm.pixelAt(${Math.round(at.x)}, ${Math.round(at.y)})`))
|
||
: 'none';
|
||
await stop();
|
||
check(
|
||
'On animation finished fires once when a once animation ends',
|
||
brain?.done === 1,
|
||
JSON.stringify({ brain, log: await json('__gm.state.notice') }),
|
||
);
|
||
check(
|
||
'and the Properties node it runs puts the other animation on',
|
||
worn?.props.texture === 'an-run' && worn?.animation?.id === 'an-run' && after === 'white',
|
||
JSON.stringify({ texture: worn?.props.texture, clock: worn?.animation, after }),
|
||
);
|
||
|
||
// 25l'. A picture is a kind of its own: the Texture node hands one into the graph, and a
|
||
// variable may be one. Both are picked, never typed.
|
||
const pickPicture = async (label, name) => {
|
||
const field = await json(`__gm.inspectorField(${JSON.stringify(label)}, true)`);
|
||
await click(field.x, field.y);
|
||
await typeText(name);
|
||
await press('Enter');
|
||
};
|
||
await mutate(`
|
||
const logic = __gm.state.project.assets.find((a) => a.id === 'an-logic');
|
||
logic.graph.nodes.push(${JSON.stringify(node('an-pic', 'Texture', 600, 300))});
|
||
logic.variables.push(${JSON.stringify(variable('pic', 'texture', ''))});
|
||
logic.props.pic = '';
|
||
__gm.state.select({ kind: 'node', assetId: 'an-logic', nodeId: 'an-pic', nodeIds: ['an-pic'] });
|
||
`);
|
||
await pickPicture('Value', walk.name);
|
||
const picNode = (await t.asset('an-logic')).graph.nodes.find((n) => n.id === 'an-pic');
|
||
const picRows = (await json("__gm.graph.nodeSettings('an-pic')")) ?? [];
|
||
check(
|
||
'the Texture node picks its picture and shows it by name',
|
||
picNode?.params.value === walk.id && picRows.some((r) => r.includes(walk.name)),
|
||
JSON.stringify({ params: picNode?.params, rows: picRows }),
|
||
);
|
||
await evaluate("__gm.state.select({ kind: 'asset', assetId: 'an-logic' })");
|
||
await pickPicture('pic', walk.name);
|
||
check(
|
||
'a texture variable is picked with the same picker',
|
||
(await t.asset('an-logic')).props.pic === walk.id,
|
||
JSON.stringify((await t.asset('an-logic')).props),
|
||
);
|
||
await shot('04b-texture-kind');
|
||
|
||
// 25m. Deleting the texture counts the animations that play its frames.
|
||
const again = await json(`__gm.assetRow(${JSON.stringify(strip)})`);
|
||
await click(again.x, again.y, 'right');
|
||
await clickText('Delete…');
|
||
const dialog = await json('__gm.overlayTexts()');
|
||
await shot('05-delete-dialog');
|
||
check(
|
||
'deleting the texture names the animations that use it',
|
||
dialog.some((s) => s.includes('2 animation(s)')),
|
||
JSON.stringify(dialog),
|
||
);
|
||
check('and its bold Delete button is not cut short', dialog.includes('Delete'), JSON.stringify(dialog));
|
||
const cancel = await t.locate('Cancel', true);
|
||
await click(cancel.x, cancel.y);
|
||
|
||
await mutate(`
|
||
const p = __gm.state.project;
|
||
const mine = new Set(['an-sprite', 'an-run', 'an-logic', ${JSON.stringify(walk.id)}, ${JSON.stringify(strip)}]);
|
||
p.assets = p.assets.filter((a) => !mine.has(a.id));
|
||
const scene = __gm.state.activeScene;
|
||
scene.objects = scene.objects.filter((o) => o.id !== 'an-box' && o.id !== 'an-brain');
|
||
__gm.state.select(null);
|
||
__gm.state.preview();
|
||
`);
|
||
const left = await json(
|
||
"__gm.state.project.assets.filter((a) => a.type === 'animation' || a.id === 'an-sprite' || a.id === 'an-logic').length",
|
||
);
|
||
check('the step leaves the fixture as it found it', left === 0, String(left));
|
||
}
|