/** * The auto-repeat of `On key down`, kept apart from the runtime so it can be tested without a world. * The browser's own repeat is dropped (`Input.press`): its delay and rate are an OS setting, not tunable. */ /** 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 }; }