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:
rootandClaude Opus 5 committed 2026-09-09 22:44:00 +02:00
1 parent 4b0919a224
commit 3ab8d88ad6
13 files changed
+552 -3

No files matched your search

+5
View File
@@ -51,6 +51,11 @@ export interface NodeDef {
outputs: PortDef[];
/** Node settings that are not ports (key name, operator, variable). */
params: PropertyDef[];
/**
* Settings that say nothing at this value set, so the node body leaves them off; the inspector
* still offers them. Without it a switched-off repeat is two rows of "0" on every key node.
*/
hiddenParams?(params: PropertyBag): string[];
/**
* Pure data nodes are evaluated on demand when a downstream input needs their value; exec nodes
* run when the flow reaches them. Event nodes have no `run` – the interpreter seeds their outputs.
+56
View File
@@ -0,0 +1,56 @@
/**
* The auto-repeat of `On key down`, kept apart from the runtime so it can be checked without a
* world: given how long the key has been held, how often does the node fire this frame?
*
* The browser's own key auto-repeat is deliberately not used (`Input.press` drops it): its delay
* and rate are an operating-system setting, not something a game can tune.
*/
/** Smallest repeat gap, mirroring `On timer`; 0 keeps its own meaning of "every frame". */
export const MIN_REPEAT_INTERVAL = 0.01;
export interface KeyRepeatState {
/** Seconds since the key went down. */
held: number;
/** Repeats so far in this press; the next is due at `after + repeats × interval`. */
repeats: number;
}
export interface KeyRepeatStep {
/** The key went down *this frame*, as opposed to `down`, which is the whole hold. */
pressed: boolean;
down: boolean;
dt: number;
/** Seconds of holding before the first repeat; 0 switches repeating off. */
after: number;
/** Seconds between repeats; 0 fires every frame. */
interval: number;
}
/**
* `state` is null while the key is not being tracked. A key that is already held when tracking
* starts stays untracked until it is pressed again, so a repeat never runs without its first fire.
*/
export function stepKeyRepeat(
state: KeyRepeatState | null,
step: KeyRepeatStep,
): { state: KeyRepeatState | null; fires: number } {
if (step.pressed) return { state: { held: 0, repeats: 0 }, fires: 1 };
if (!step.down) return { state: null, fires: 0 };
if (!state) return { state: null, fires: 0 };
const after = Math.max(0, step.after);
const held = state.held + Math.max(0, step.dt);
if (after <= 0 || held < after) return { state: { ...state, held }, fires: 0 };
if (step.interval <= 0) return { state: { ...state, held }, fires: 1 };
const interval = Math.max(MIN_REPEAT_INTERVAL, step.interval);
let repeats = state.repeats;
let fires = 0;
while (held >= after + repeats * interval) {
repeats += 1;
fires += 1;
}
return { state: { held, repeats }, fires };
}
+9 -2
View File
@@ -1,12 +1,19 @@
import type { NodeDef } from '../../NodeRegistry';
import { asNumber } from '../../values';
import { exec, keyParam } from '../shared';
export const OnKeyDown: NodeDef = {
type: 'OnKeyDown',
label: 'On key down',
category: 'event',
description: 'Runs once when the key is pressed.',
description: 'Runs when the key is pressed, and again while it stays held if a repeat is set.',
inputs: [],
outputs: [exec()],
params: [keyParam],
params: [
keyParam,
{ key: 'repeatAfter', label: 'Repeat after (ms)', kind: 'number', default: 0, min: 0, step: 50 },
{ key: 'repeatInterval', label: 'Repeat interval (ms)', kind: 'number', default: 0, min: 0, step: 10 },
],
// Both rows go together: with no delay there is no repeat, so the interval is not in play either.
hiddenParams: (params) => (asNumber(params.repeatAfter ?? 0) > 0 ? [] : ['repeatAfter', 'repeatInterval']),
};