import { edge, node } from '../lib/builders.mjs'; import { LANES, scaffoldBehavior } from './lib/scaffold.mjs'; /** * Pausing freezes world and clock and drops input instead of replaying it on resume. Brings its own * self-drifting box, since the fixture's Mover only moves on arrow keys and a pause would look like idling. */ const DRIFT_PER_SECOND = 100; const KEY_LOG = 'arrow down'; /** * Two independent things in one graph: `On update → Move` drifts the owner right without any * input, and `On key down → Log` leaves a mark for every *edge* the world saw – which is what a * pause has to swallow. */ const pauseGraph = { nodes: [ node('p-update', 'OnUpdate', 0, 0), node('p-owner', 'Owner', 0, 120), node('p-move', 'Move', 340, 0, { dx: 1, scale: DRIFT_PER_SECOND }), node('p-key', 'OnKeyDown', 0, 260, { key: 'ArrowRight' }), node('p-log', 'Log', 340, 260, { message: KEY_LOG }), ], edges: [ edge('p-e1', 'p-update', 'exec', 'p-move', 'exec'), edge('p-e2', 'p-update', 'dt', 'p-move', 'dt'), edge('p-e3', 'p-owner', 'object', 'p-move', 'object'), edge('p-e4', 'p-key', 'exec', 'p-log', 'exec'), ], }; export async function pause(t) { const { check, sleep, json, has, shot, clickText, press, entity, world, stop } = t; const playInfo = () => json('__gm.playInfo()'); const boxX = async () => (await entity('p-box')).x; const keyHits = async () => (await world()).log.filter((line) => line.includes(KEY_LOG)).length; // 19a. A box with a drifting behavior below it, written into the model directly: what is under // test is the pause, not another way of building a graph. const rig = await scaffoldBehavior(t, { assetId: 'pauser', name: 'Pauser', graph: pauseGraph, boxId: 'p-box', boxName: 'Pause box', logicId: 'p-logic', y: LANES.pause, }); await t.play(); const running = await playInfo(); const beforeDrift = await boxX(); await sleep(500); const drifted = await boxX(); // The precondition of everything below: without movement, "it stood still" is trivially true. check( 'the box drifts on its own while playing, with nothing held down', running.playing === true && running.paused === false && drifted > beforeDrift + 20, `x ${beforeDrift} → ${drifted}, ${JSON.stringify(running)}`, ); // 19b. The toolbar button holds it. Both the clock and the box have to stop – a world that keeps // ticking while the picture happens to look the same would pass a position check alone. await clickText('❚❚ Pause'); await sleep(200); const held = await playInfo(); const heldX = await boxX(); await sleep(1000); const stillHeld = await playInfo(); check( 'pausing stops the world where it is: the box and the clock both stand still', held.paused === true && stillHeld.playing === true && Math.abs((await boxX()) - heldX) < 0.001 && Math.abs(stillHeld.time - held.time) < 0.05, `x ${heldX} → ${await boxX()}, clock ${held.time.toFixed(2)} → ${stillHeld.time.toFixed(2)} over 1 s`, ); // `locate` throws on a missing label, so the button that must be *gone* is asked for through // the hook, which answers with null. check( 'the button offers to resume while it is held, and no longer to pause', (await has('▶ Resume')) === true && (await has('❚❚ Pause')) === false, `resume=${await has('▶ Resume')} pause=${await has('❚❚ Pause')}`, ); await shot('36-paused'); // 19c. Keys pressed while it is held are dropped, not queued: resuming must not fire an edge for // each of them. The count before proves the graph reacts at all. const hitsBefore = await keyHits(); await press('ArrowRight'); await sleep(1000); const hitsWhilePaused = await keyHits(); await press('ArrowRight'); await press('ArrowRight'); await sleep(200); // 19d. Ctrl+Enter resumes, the same shortcut that started play. await press('Enter', { ctrl: true }); await sleep(400); const resumed = await playInfo(); const resumedX = await boxX(); check( 'resuming lets it move on from where it stopped, without catching up the held time', resumed.paused === false && resumedX > heldX && // A second of pause plus the pressing above: catching up would show as a jump far beyond // what the resumed run itself had time for. resumedX < heldX + DRIFT_PER_SECOND * 1.2, `x ${heldX} → ${resumedX}, clock ${stillHeld.time.toFixed(2)} → ${resumed.time.toFixed(2)}`, ); const hitsAfter = await keyHits(); check( 'a key pressed while paused is swallowed instead of firing on resume', hitsWhilePaused === hitsBefore && hitsAfter === hitsBefore, `${hitsBefore} hits before, ${hitsWhilePaused} while held, ${hitsAfter} after resuming`, ); // Without this the check above would also pass on a graph that never logs anything at all. await press('ArrowRight'); await sleep(300); check( 'the same key does fire once the game is running again', (await keyHits()) === hitsBefore + 1, `${hitsBefore} → ${await keyHits()}`, ); // 19e. A hold must never outlive its play: Esc stops from a paused game, and the next run starts // moving. A pause left standing would leave the editor unable to run anything. await press('Enter', { ctrl: true }); await sleep(200); check('Ctrl+Enter holds it again', (await playInfo()).paused === true); await stop(); const afterStop = await playInfo(); await t.play(); await sleep(500); const restartedX = await boxX(); await sleep(400); check( 'stopping clears the hold, so the next play runs', afterStop.playing === false && afterStop.paused === false && (await playInfo()).paused === false && (await boxX()) > restartedX, `after stop ${JSON.stringify(afterStop)}, x ${restartedX} → ${await boxX()}`, ); await stop(); await sleep(200); // 19f. Play discarded its copy, so the box is back at the position the model holds. const left = await rig.remove(); check( 'the step leaves nothing of its own behind', left.objects === 0 && left.assets === 0, JSON.stringify(left), ); }