import { edge, node } from '../lib/builders.mjs'; /** * The auto-repeat of `On key down`: a held key fires again after a delay and then on its own * interval, which is what lets a Tetris piece slide sideways on one press. * * The step brings its own box and behavior with *two* `On key down` nodes on the same key – one * plain, one repeating. One hold then measures both at once, so "it repeated" cannot be confused * with "the key arrived more often than expected". Everything it adds is removed again at the end. */ const PLAIN = 'plain hit'; const REPEATING = 'repeating hit'; const DELAY = 300; const INTERVAL = 50; const repeatGraph = { nodes: [ node('r-plain', 'OnKeyDown', 0, 0, { key: 'ArrowRight' }), node('r-plain-log', 'Log', 340, 0, { message: PLAIN }), node('r-repeat', 'OnKeyDown', 0, 220, { key: 'ArrowRight', repeatAfter: DELAY, repeatInterval: INTERVAL, }), node('r-repeat-log', 'Log', 340, 220, { message: REPEATING }), ], edges: [ edge('r-e1', 'r-plain', 'exec', 'r-plain-log', 'exec'), edge('r-e2', 'r-repeat', 'exec', 'r-repeat-log', 'exec'), ], }; export async function keyRepeat(t) { const { check, sleep, json, shot, click, holdKey, world, mutate, stop, select, typeText } = t; /** Both counts out of one read of the world, so the two are always of the same moment. */ const hits = async () => { const log = (await world()).log; const count = (message) => log.filter((line) => line.includes(message)).length; return { plain: count(PLAIN), repeating: count(REPEATING) }; }; // 20a. A box with the two-node behavior below it, written into the model directly: what is under // test is the repeat, not another way of building a graph. await mutate(` const p = __gm.state.project; const scene = __gm.state.activeScene; const proto = scene.objects[0]; const a = structuredClone(p.assets.find((x) => x.id === 'mover')); Object.assign(a, { id: 'repeater', name: 'Repeater', props: { enabled: true, runInEditor: false }, variables: [], graph: ${JSON.stringify(repeatGraph)}, }); p.assets.push(a); const make = (id, assetId, parentId, y) => { const o = structuredClone(proto); Object.assign(o, { id, assetId, name: id === 'r-box' ? 'Repeat box' : 'Repeater', position: { x: 0, y, z: 0 }, parentId, overrides: {}, instanceEdits: {}, }); return o; }; scene.objects.push(make('r-box', 'crate', '', -300), make('r-logic', 'repeater', 'r-box', 0)); `); await sleep(400); await t.play(); await sleep(300); // 20b. A hold shorter than the delay is still just a press. Without this the check below would // also pass on a node that simply fires every frame the key is down. const beforeShort = await hits(); await holdKey('ArrowRight', 120); await sleep(200); const afterShort = await hits(); check( 'a hold shorter than the delay fires once, repeat or not', afterShort.plain === beforeShort.plain + 1 && afterShort.repeating === beforeShort.repeating + 1, `plain ${beforeShort.plain} → ${afterShort.plain}, repeating ${beforeShort.repeating} → ${afterShort.repeating} over 120 ms with a ${DELAY} ms delay`, ); // 20c. Hold it past the delay: the plain node still counts one press, the repeating one runs. await holdKey('ArrowRight', 800); await sleep(200); const afterLong = await hits(); const gained = afterLong.repeating - afterShort.repeating; check( 'holding past the delay keeps the repeating node firing, while the plain one counts one press', afterLong.plain === afterShort.plain + 1 && gained > 4 && gained < 25, `plain +${afterLong.plain - afterShort.plain}, repeating +${gained} over 800 ms (press + ~${Math.round((800 - DELAY) / INTERVAL)} repeats expected)`, ); await stop(); await sleep(300); // 20d. Both settings are on the node itself, so a graph can be tuned where it is built. await select({ kind: 'node', assetId: 'repeater', nodeId: 'r-repeat' }); await sleep(300); const afterField = await json(`__gm.inspectorField('Repeat after (ms)')`); const intervalField = await json(`__gm.inspectorField('Repeat interval (ms)')`); check( 'the node inspector offers the delay and the interval', afterField !== null && intervalField !== null, JSON.stringify({ afterField, intervalField }), ); // A switched-off repeat is worth no rows: two lines of "0" on every key node would make each // one taller for nothing. The repeating node next to it shows both, so this is not just hiding. const plainRows = await json(`__gm.graph.nodeSettings('r-plain')`); const repeatRows = await json(`__gm.graph.nodeSettings('r-repeat')`); check( 'a node with no repeat set carries no repeat rows, the repeating one carries both', plainRows.length === 1 && repeatRows.length === 3 && repeatRows.some((r) => r.startsWith('Repeat after')) && repeatRows.some((r) => r.startsWith('Repeat interval')), JSON.stringify({ plainRows, repeatRows }), ); await shot('40-key-repeat-node'); await click(afterField.x, afterField.y); await typeText('750'); await click(intervalField.x, intervalField.y); await sleep(200); const params = await json( `__gm.state.project.assets.find((a) => a.id === 'repeater').graph.nodes.find((n) => n.id === 'r-repeat').params`, ); check( 'typing a new delay lands on the node', params.repeatAfter === 750 && params.repeatInterval === INTERVAL, JSON.stringify(params), ); // 20e. The typed value is what the next run obeys: 750 ms is longer than this hold, so the // repeating node falls silent although nothing else about the graph changed. await t.play(); await sleep(300); const beforeTuned = await hits(); await holdKey('ArrowRight', 500); await sleep(200); const afterTuned = await hits(); check( 'the delay typed into the inspector is the one the run uses', afterTuned.repeating === beforeTuned.repeating + 1, `repeating ${beforeTuned.repeating} → ${afterTuned.repeating} over 500 ms with the delay raised to 750 ms`, ); await stop(); await sleep(300); await mutate(` const p = __gm.state.project; const scene = __gm.state.activeScene; scene.objects = scene.objects.filter((o) => o.id !== 'r-box' && o.id !== 'r-logic'); p.assets = p.assets.filter((a) => a.id !== 'repeater'); `); await sleep(300); check( 'the step leaves nothing of its own behind', (await json(`__gm.sceneObjects().filter((o) => o.id.startsWith('r-')).length`)) === 0, ); }