Chords & sequences
Define simultaneous shortcuts and ordered command patterns on top of the frame-accurate action journal.
Chords & sequences
ChordAction and SequenceAction extend the named action layer for controls that need more than one button. They consume the same ordered input batches as ButtonAction, so several steps that happen between two frame boundaries are still observed in their real source-event order.
class GameScene extends Scene {
// Your own game objects - anything exposing these methods.
declare game: { save(): void };
declare player: { specialAttack(): void };
controls = new ActionMap({
save: new ChordAction('Control+S'),
special: new SequenceAction('Down>Down+Right>Right>A', {
maxGap: 250,
timeout: 1_200,
}),
});
override init(): void {
this.inputs.attach(this.controls);
}
override update(): void {
if (this.controls.save.pressed) {
this.game.save();
}
if (this.controls.special.triggered) {
this.player.specialAttack();
}
}
}+ means simultaneous channels in one step; > advances to the next step; | alternates between whole alternatives within one step — see “Alternation” below. A single atomic platform batch never invents ordering: A and B entering together does not satisfy A>B — see below.
SequenceAction also exposes progress — completedSteps / totalSteps, handy for rendering a partial-combo hint. It never reaches 1: the same update that completes the final step sets triggered and snaps progress straight back to 0, so the real range is [0, (n-1)/n] for n steps. It resets to 0 on a mismatch, or whenever the map itself is reset (a scene suspend, or losing availability under a when policy — see pause menu). A maxGap/timeout expiry resets it too, but only once the NEXT relevant input event arrives (see “Tuning restart behavior” below) — a half-completed pattern can hold a stale, non-zero progress for an arbitrarily long real gap if nothing else touches a bound channel in the meantime; there is no timer ticking it down on its own.
Not text or IME input
String patterns resolve case-insensitive Keyboard enum names only ('Control+S', 'control+s'), plus a handful of shorthand aliases (Ctrl for Control, Cmd/Command/Super for Meta, Opt for Alt, Esc for Escape) — see Modifier sides. They are not a text or IME API: they never decode typed characters, dead keys, or composed input, and reject any token that is not a known Keyboard member or alias. Use DOM text input outside the game channel system for localized characters, composition, and editable text.
Array patterns accept keyboard, pointer, gamepad-button, and gamepad-axis channels directly — mix a chord step of several channels as a nested array: new SequenceAction([Keyboard.J, Keyboard.J, [Keyboard.Control, Keyboard.K]]). A repeated single-channel step like the two Keyboard.J entries above requires a genuine release between them: holding J down after the first step is accepted does not also satisfy the second, since a step only advances on a fresh press (an inactive-to-active transition), never merely because a channel already reads as held. Gamepad channels are resolved against the pad the owning ActionMap was given, so a chord or sequence bound to GamepadButton.South follows its player’s controller without naming a slot.
Alternation: |
| separates alternatives within one step — the step is satisfied if ANY ONE of them is. Precedence, loosest to tightest: > separates steps, | separates alternatives, + joins channels within one alternative — 'Control+S|Meta+S' is “Control and S, or Meta and S”, and 'A+B|C>D' is “(A and B) or C, then D”.
This composes with the same strongest/weakest reduction ButtonAction/ChordAction already use for analog sources: an alternation reports the strongest of its alternatives, and each alternative (like any chord) reports the weakest of its own members — so new ChordAction([GamepadButton.LeftTrigger]) alternating with a South+RightTrigger chord still reports each alternative’s own least-engaged member, not a collapsed boolean.
In array form, wrap every alternative in its own array, even a single-channel one, so it is never ambiguous with a plain chord: [[Keyboard.Control, Keyboard.K], [Keyboard.Meta, Keyboard.K]] is the array form of 'Control+K|Meta+K'. A step written as [Keyboard.A, Keyboard.B] still means “A and B required together” (a chord), exactly as before — mixing a bare channel and a nested alternative within the same step is rejected.
Switching from one alternative to another within the same SequenceAction step is never treated as an unrelated, mismatching entry — only a channel belonging to NONE of the step’s alternatives resets progress (see “Tuning restart behavior” below).
Same-batch entries never invent an order
A ChannelEventBatch is one atomic platform event — every channel it wrote arrives together, in one indivisible group (see action mapping). SequenceAction only advances a step when the step’s own channels transition from inactive to active on their own, without an unrelated tracked channel entering in that same batch. So if A and B both go from released to held within one batch, a 'A>B' pattern reports no progress at all rather than guessing which one “came first” — the platform never told it. Two channels changing together CAN satisfy a single chord step, though: 'Control+K' accepts Control and K arriving in the same batch, because that is exactly what the step itself requires.
Tuning restart behavior
maxGap (default 600ms) bounds the time between two consecutive accepted steps; timeout (default 3000ms) bounds the whole pattern from its first accepted step to its last. Both are checked against the source events’ monotonic timestamps when the next relevant input event arrives — they do not schedule timers and do not wake an otherwise idle frame.
By default (resetOnMismatch: true), an unrelated tracked channel entering resets progress to the start; the very same batch can still restart the pattern on its first step if that step’s own channels are the only thing it touched. Set resetOnMismatch: false to ignore an out-of-order channel instead of discarding progress — the pattern simply keeps waiting for its actual next step.