feat(controller): add hold + press combos to main bindings (#328)

* feat(controller): add hold + press combos to main bindings

Main Alt+C actions can now be learned as combos such as L1 + D-pad right.
A combo fires on each press while its hold button is down, and claims its
press input so token move and popup scroll ignore it until it is released.
A plain action on a hold button fires on release, unless another bound
button was pressed during the hold.

The save path needed two changes for combos to persist. The IPC validator
rebuilt bindings field by field and dropped `hold`, so Alt+C showed a combo
from its local copy until the next reload turned it back into a plain
button. Validator and config resolver now share one parser. Saves also
replace the controller section, since the deep merge in patchRawConfig kept
the old `hold` when a combo was rebound to one button.

Also fixes Learn on the (D-pad) rows, which threw after cancel() cleared
the capture target.

* test(launcher): wait for content in background child test

The test polled for the fake app's output files to exist, but the shell
creates a redirect target before writing to it. A poll landing in that gap
read an empty env file and failed with '' !== '1', as seen once in CI.
Wait until both files have content instead.

* fix(controller): keep a diagonal push from learning as a combo

Learn took the first axis of a diagonal stick push as the hold and the
second as the press, saving a combo that one axis could never trigger. This
reached main Alt+C rows when they moved to combo capture. Axes crossing the
threshold in the same poll as the first input now count as one movement. A
second axis moved in a later poll still records a combo.

* fix(controller): only treat simultaneous axes as one movement

The diagonal push guard also blocked an axis activated in the same poll as a
button, so L1 plus a stick direction could not record the stick until it was
released and moved again. Apply the guard only when the first input is an
axis, so that input pair records as a combo again.
This commit is contained in:
sudacode authored and GitHub committed 2026-11-05 23:23:54 -08:00
1 parent 79e5535823
commit b31ddce627
30 files changed
+1078 -295

No files matched your search

+7
View File
@@ -191,12 +191,16 @@ export interface ControllerNoneBinding {
export interface ControllerButtonInputBinding {
kind: 'button';
buttonIndex: number;
/** Makes this a combo: it fires when this input is pressed while `hold` is down. */
hold?: ControllerRawInput;
}
export interface ControllerAxisDirectionInputBinding {
kind: 'axis';
axisIndex: number;
direction: ControllerAxisDirection;
/** Makes this a combo: it fires when this input is pressed while `hold` is down. */
hold?: ControllerRawInput;
}
export interface ControllerAxisInputBinding {
@@ -343,6 +347,9 @@ export type ControllerReviewInput =
| { kind: 'face'; letter: ControllerFaceLetter }
| { kind: 'dpad'; direction: ControllerDpadDirection };
/** An input as Learn records it: a raw button index or a raw axis direction. */
export type ControllerRawInput = Extract<ControllerReviewInput, { kind: 'button' | 'axis' }>;
/** Press `press`, optionally while holding `hold`. */
export type ControllerReviewBindingConfig =
| { kind: 'none' }