/** * The throwaway rig steps 17–20 share in the fixture scene: a behavior asset cloned off `mover` with the * step's graph, a crate to hold it, and a logic child that runs it. */ /** * Where each step parks its box. The fixture scene is shared, so two steps at the same y would * overlap and each would fail for reasons belonging to the other. */ export const LANES = { editorRun: 400, editorRunContainer: 800, editorRunEdits: 1200, pause: 300, nested: 700, keyRepeat: -300, stateMachine: -700, console: -1100, transform: -1500, font: -1900, particles: -2300, sound: -2700, }; /** * Source for a `make(id, assetId, parentId, y, name)` factory, to paste inside a `mutate()` * string. It clones `scene.objects[0]`, so a new object carries whatever shape the fixture has. */ export const MAKE_OBJECT = ` const make = (id, assetId, parentId, y, name = '') => { const o = structuredClone(scene.objects[0]); Object.assign(o, { id, assetId, name, position: { x: 0, y, z: 0 }, parentId, overrides: {}, instanceEdits: {}, }); return o; };`; /** * Returns the created ids plus `remove()`, so a step's cleanup matches what it made. `front` puts the * asset at the head of the list: a row appended at the end scrolls out of `dragAsset`'s reach. */ export async function scaffoldBehavior(t, options) { const { assetId, name, graph, boxId, boxName = 'Box', logicId, y, front = false, props = { enabled: true, runInEditor: false }, } = options; await t.mutate(` const p = __gm.state.project; const scene = __gm.state.activeScene; const asset = structuredClone(p.assets.find((a) => a.id === 'mover')); Object.assign(asset, { id: ${JSON.stringify(assetId)}, name: ${JSON.stringify(name)}, props: ${JSON.stringify(props)}, variables: [], graph: ${JSON.stringify(graph)}, }); p.assets.${front ? 'unshift' : 'push'}(asset); ${MAKE_OBJECT} scene.objects.push( make(${JSON.stringify(boxId)}, 'crate', '', ${y}, ${JSON.stringify(boxName)}), make(${JSON.stringify(logicId)}, ${JSON.stringify(assetId)}, ${JSON.stringify(boxId)}, 0, ${JSON.stringify(name)}), ); `); return { assetId, boxId, logicId, /** Removes everything this rig added and reports what survived (should be all zeroes). */ async remove() { await t.evaluate('__gm.state.select(null)'); await t.mutate(` const p = __gm.state.project; const scene = __gm.state.activeScene; const objects = ${JSON.stringify([boxId, logicId])}; const assets = ${JSON.stringify([assetId])}; scene.objects = scene.objects.filter((o) => !objects.includes(o.id)); p.assets = p.assets.filter((a) => !assets.includes(a.id)); `); return t.json(`{ objects: __gm.sceneObjects().filter((o) => ${JSON.stringify([boxId, logicId])}.includes(o.id)).length, assets: __gm.state.project.assets.filter((a) => a.id === ${JSON.stringify(assetId)}).length, }`); }, }; } /** What each tab button reads – the text a check has to click to open it. */ export const TAB_LABELS = { scene: 'Scene', game: 'Game', graph: 'Behavior', statemachine: 'State machine', shader: 'Shader', texture: 'Texture', animation: 'Animation', container: 'Container', list: 'List', particles: 'Particles', }; /** * Opens a tab by clicking it, through the `»` menu when the row had to hide it. Since the tenth tab * (#73) that is the normal case at the test window's width: a plain `clickText('Container')` found * nothing and took the whole suite down with it. */ export async function openTab(t, tab) { const row = await t.json('__gm.tabRow()'); if (row.hidden.includes(tab)) { await t.clickText('»'); } await t.clickText(TAB_LABELS[tab]); } /** * Picks an asset type from the already-open `+` menu. Since #43 the menu may grow to the height of * the canvas, so on this window every type is usually on screen at once; the scroll stays in as * insurance for a shorter window, where entries below the mask edge are still found by `locate` * but a click there lands on nothing. That is what the fourteenth type (`Variables`, #91) did to * `Font` and to step 30. */ export async function pickAssetType(t, label) { const box = await t.json('__gm.overlayRect()'); let p = await t.locate(label, true); let scrolled = false; if (box && (!p || p.y > box.y + box.h - 4)) { await t.wheel(box.x + box.w / 2, box.y + box.h / 2, 300); p = await t.locate(label, true); scrolled = true; } await t.click(p.x, p.y); return { box, p, scrolled }; }