Files
feuerwerk/tests/core/key-repeat.test.ts
T
rootandClaude Opus 5 3ab8d88ad6 Let On key down repeat while the key stays held
A held key fired once and then nothing, so sliding a Tetris piece sideways or
walking a menu meant building a timer next to every key node. Two settings on
the node do it now: Repeat after and Repeat interval, both in milliseconds so
the usual values stay whole numbers (300, 50) instead of 0.3 and 0.05. A delay
of 0 keeps the node exactly as it was, an interval of 0 means every frame.

The browser's own auto-repeat is deliberately not used - Input.press has always
dropped it - because its delay and rate are an operating-system setting of the
player's, not a game's. The repeat is built from the measured hold time instead,
per node, so two nodes on one key keep their own clocks. The rule itself is a
pure function in keyRepeat.ts and is checked without a world.

Both rows stay off the node body while the delay is 0: two lines of "0" on every
key node in every graph would be noise, and the screenshot said so. NodeDef gains
hiddenParams for that; the inspector still shows them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 22:44:00 +02:00

74 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* The repeat clock behind `On key down`, checked without a world: the cases that are awkward to
* stage in one (a key already held when tracking starts, a hostile interval) live here.
*/
import { describe, expect, it } from 'vitest';
import { nodeDef } from '../../src/core/graph';
import { MIN_REPEAT_INTERVAL, stepKeyRepeat } from '../../src/core/graph/keyRepeat';
import type { PropertyBag } from '../../src/core/model/Properties';
const held = { pressed: false, down: true, dt: 0.1, after: 0.2, interval: 0.2 };
describe('key repeat', () => {
it('starts the clock on the press, whatever the frame was worth', () => {
const step = stepKeyRepeat(null, { ...held, pressed: true, dt: 5 });
expect(step.fires).toBe(1);
expect(step.state).toEqual({ held: 0, repeats: 0 });
});
it('forgets the clock when the key comes up', () => {
expect(stepKeyRepeat({ held: 9, repeats: 3 }, { ...held, down: false })).toEqual({
state: null,
fires: 0,
});
});
it('stays quiet for a key that was already held when it started watching', () => {
// No state means the press was never seen – repeating without a first fire would be a lie.
expect(stepKeyRepeat(null, held)).toEqual({ state: null, fires: 0 });
});
it('never repeats while the delay is zero, however long the key is held', () => {
let state = { held: 0, repeats: 0 };
for (let n = 0; n < 100; n++) {
const step = stepKeyRepeat(state, { ...held, after: 0, dt: 1 });
expect(step.fires).toBe(0);
state = step.state!;
}
expect(state.held).toBe(100);
});
it('treats a negative delay as no delay at all', () => {
expect(stepKeyRepeat({ held: 5, repeats: 0 }, { ...held, after: -1 }).fires).toBe(0);
});
it('holds an absurdly small interval to the floor instead of freezing the frame', () => {
// Without the floor a 1 ns interval over a 1 s frame would ask for a billion fires.
const step = stepKeyRepeat({ held: 0.2, repeats: 0 }, { ...held, dt: 1, interval: 1e-9 });
expect(step.fires).toBeLessThanOrEqual(1 / MIN_REPEAT_INTERVAL + 1);
expect(step.fires).toBeGreaterThan(1 / MIN_REPEAT_INTERVAL - 2);
});
it('counts every due repeat when one frame swallows several', () => {
const step = stepKeyRepeat({ held: 0, repeats: 0 }, { ...held, dt: 1, after: 0.5, interval: 0.5 });
expect(step).toEqual({ state: { held: 1, repeats: 2 }, fires: 2 });
});
});
describe('the repeat rows on the node body', () => {
const hidden = (params: PropertyBag) => nodeDef('OnKeyDown')?.hiddenParams?.(params);
it('leaves both off while the repeat is switched off', () => {
expect(hidden({ key: 'a' })).toEqual(['repeatAfter', 'repeatInterval']);
expect(hidden({ key: 'a', repeatAfter: 0, repeatInterval: 50 })).toEqual([
'repeatAfter',
'repeatInterval',
]);
});
it('shows both once a delay is set, an interval of zero included', () => {
// Zero is a setting here ("every frame"), not an absence – hiding that row would lie.
expect(hidden({ key: 'a', repeatAfter: 300, repeatInterval: 0 })).toEqual([]);
});
});