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>
This commit is contained in:
1 parent
4b0919a224
commit
3ab8d88ad6
13 files changed
+552
-3
No files matched your search
@@ -119,6 +119,117 @@ describe('key events', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// The node's settings are milliseconds, the frame lengths seconds. Those lengths are exact in
|
||||
// binary (0.125, 0.25, 0.5), so a repeat due at a round hold time is never lost to a rounding error.
|
||||
describe('On key down repeat', () => {
|
||||
it('fires again after the delay, then on its own interval', () => {
|
||||
const f = scene();
|
||||
logOn(f, 'OnKeyDown', { key: 'a', repeatAfter: 500, repeatInterval: 250 }, 'down');
|
||||
const { world, input, frame } = startWorld(f, { behaviors: true });
|
||||
|
||||
input.press('a');
|
||||
frame(0.125);
|
||||
expect(world.log).toEqual(['down']);
|
||||
|
||||
for (let n = 0; n < 3; n++) frame(0.125); // held 0.375 s: still short of the delay
|
||||
expect(world.log).toEqual(['down']);
|
||||
|
||||
frame(0.125); // held 0.5 s
|
||||
expect(world.log).toEqual(['down', 'down']);
|
||||
|
||||
frame(0.125);
|
||||
expect(world.log).toHaveLength(2); // 0.625 s: half an interval on
|
||||
frame(0.125);
|
||||
expect(world.log).toHaveLength(3); // 0.75 s
|
||||
|
||||
frame(0.25);
|
||||
expect(world.log).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('leaves a key alone while the delay is zero', () => {
|
||||
const f = scene();
|
||||
logOn(f, 'OnKeyDown', { key: 'a', repeatAfter: 0, repeatInterval: 50 }, 'down');
|
||||
const { world, input, frame } = startWorld(f, { behaviors: true });
|
||||
|
||||
input.press('a');
|
||||
for (let n = 0; n < 30; n++) frame(0.125);
|
||||
expect(world.log).toEqual(['down']);
|
||||
});
|
||||
|
||||
it('fires every frame when the interval is zero', () => {
|
||||
const f = scene();
|
||||
logOn(f, 'OnKeyDown', { key: 'a', repeatAfter: 250, repeatInterval: 0 }, 'down');
|
||||
const { world, input, frame } = startWorld(f, { behaviors: true });
|
||||
|
||||
input.press('a');
|
||||
frame(0.125);
|
||||
frame(0.125); // held 0.125 s
|
||||
expect(world.log).toEqual(['down']);
|
||||
|
||||
frame(0.125); // held 0.25 s: the delay is up
|
||||
expect(world.log).toHaveLength(2);
|
||||
|
||||
frame(0.0625);
|
||||
frame(0.0625);
|
||||
expect(world.log).toHaveLength(4); // a short frame counts as much as a long one
|
||||
});
|
||||
|
||||
it('starts the delay over on the next press', () => {
|
||||
const f = scene();
|
||||
logOn(f, 'OnKeyDown', { key: 'a', repeatAfter: 250, repeatInterval: 250 }, 'down');
|
||||
const { world, input, frame } = startWorld(f, { behaviors: true });
|
||||
|
||||
input.press('a');
|
||||
frame(0.125);
|
||||
input.release('a');
|
||||
frame(0.5); // well past the delay, but the key is gone: no repeat
|
||||
expect(world.log).toEqual(['down']);
|
||||
|
||||
input.press('a');
|
||||
frame(0.125);
|
||||
expect(world.log).toEqual(['down', 'down']); // the press itself, not a repeat
|
||||
|
||||
frame(0.125); // held 0.125 s: the clock restarted, so nothing is due yet
|
||||
expect(world.log).toHaveLength(2);
|
||||
|
||||
frame(0.125); // held 0.25 s
|
||||
expect(world.log).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('catches up when one frame is longer than the interval', () => {
|
||||
const f = scene();
|
||||
logOn(f, 'OnKeyDown', { key: 'a', repeatAfter: 500, repeatInterval: 500 }, 'down');
|
||||
const { world, input, frame } = startWorld(f, { behaviors: true });
|
||||
|
||||
input.press('a');
|
||||
frame(0.125);
|
||||
frame(2); // repeats due at 0.5, 1.0, 1.5 and 2.0
|
||||
expect(world.log).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('gives two nodes on the same key their own clocks', () => {
|
||||
const f = scene();
|
||||
logOn(f, 'OnKeyDown', { key: 'a', repeatAfter: 250, repeatInterval: 250 }, 'fast');
|
||||
logOn(f, 'OnKeyDown', { key: 'a', repeatAfter: 750, repeatInterval: 750 }, 'slow');
|
||||
const { world, input, frame } = startWorld(f, { behaviors: true });
|
||||
|
||||
input.press('a');
|
||||
frame(0.125);
|
||||
expect(world.log).toEqual(['fast', 'slow']); // the press reaches both
|
||||
|
||||
frame(0.125);
|
||||
frame(0.125); // held 0.25 s
|
||||
expect(world.log).toEqual(['fast', 'slow', 'fast']);
|
||||
|
||||
frame(0.125);
|
||||
frame(0.125); // held 0.5 s
|
||||
expect(world.log).toEqual(['fast', 'slow', 'fast', 'fast']);
|
||||
|
||||
frame(0.25); // held 0.75 s: the fast one a third time, the slow one its first
|
||||
expect(world.log).toEqual(['fast', 'slow', 'fast', 'fast', 'fast', 'slow']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('On timer', () => {
|
||||
it('fires on its own clock, independent of the frame rate', () => {
|
||||
const f = scene();
|
||||
|
||||
Reference in new issue
Block a user