Repository navigation
Add Gamepad actions, and rebinding for keyboard and gamepad - #57
Merged
Merged
Conversation
Games had keyboard, pointer, and touch input but no controllers, and bindings were fixed when the actions were made, so players could not remap them. Gamepad follows Keyboard's shape (named actions, edges, idle/scripted/recording/parseTimeline) and adds the sticks, triggers, and every button by name for menus and settings screens. rebind() keeps the action names and swaps the keys or buttons. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015qkEJzyV5DVhoLVDMmoWJh
This was referenced Oct 21, 2026
- rebind keeps the actions the source was made with, so a rebinding object with extra keys can no longer add actions at runtime. - A key or button held across a rebind holds a newly bound action only after a fresh press, on both devices, so an action never turns held without a press. - Timelines play an entry naming a held action or button in both press and release as a release then a new press, and recordings emit it, so a release and press inside one update replays exactly. - Keyboard.presses(host) reports every key press for a settings screen waiting for the key to bind. - Keyboard.parseRebinding and Gamepad.parseRebinding validate saved bindings into a Result ready for rebind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- rebind takes exactly the actions the source was made with: an extra action fails to compile even from an object not written inline, and a Record<string, ...> is rejected since it cannot prove its keys. - InputCapture.system requires the resource's value type to be exactly the source's snapshot type, not merely assignable both ways, and the error names SnapshotOf as the fix. - Gamepad.actions takes the window and reads the pads on every animation frame until dispose, so poll() is gone and the lifecycle matches Keyboard (snapshot / rebind / dispose). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Gamepad timeline entries take an optional `connected`: a disconnect releases every held button and zeroes the sticks and triggers, as with a live pad, and recordings write disconnects and reconnects, so a replay no longer stays connected after the pad drops. - The top-down integration reads a gamepad next to the keyboard: a GamepadInput service, a PadState resource declared with InputCapture.SnapshotOf, its own capture system, the left stick at its analog length or the d-pad for walking, and south to interact, with headless tests for each. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SandroMaglione
pushed a commit
that referenced
this pull request
Oct 22, 2026
Gamepad, Keyboard.parseRebinding, and the top-down integration's gamepad
landed on main after this branch was cut, still reading { ok, value }.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GwwnHMKTTiVBpH9SivGeLk
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by Sandro · project thread
Adds controller support to
@typeonce/ritmo-browser, plus runtime rebinding so games can offer a controls settings screen. Spirit Call will build on it next.GamepadGamepad.actions(window, bindings, options)binds actions to buttons named by the standard mapping (south,west,rightTrigger,dpadUp,start, …), so a binding means the same thing on Xbox, PlayStation, and Nintendo pads.actions(with the sameActionStateasKeyboard), everybuttonsstate by name whatever the bindings (for fixed menu navigation, and for a settings screen waiting for the button to bind), theleftandrightsticks with a radial dead zone (ygrows down, as withTouch's stick), the analogleftTriggerandrightTrigger,connected, and the padid.snapshot()) untildispose(); a tap shorter than one update still counts, with nothing extra to call.idle,scripted,recording,parseTimeline, andisButton, like the other devices. Timelines name buttons, not actions, so a replay follows the bindings as live play does.GamepadHostis a structural subset ofwindow(navigator.getGamepadsplus animation frames), so the package still compiles without DOM types, and tests run in Node.rebindKeyboard.actions(...).rebind(next)andGamepad.actions(...).rebind(next)keep the same action names (the types enforce this) and swap the keys or buttons from the next snapshot. An action held only through a key it lost reportsreleased. A key held across the rebind is still tracked, so its release is seen.Keyboard.Actions: it now needsrebind() {}. This is noted in the changeset.Review fixes (second commit)
rebindkeeps the actions the source was made with: a rebinding object with extra keys (which TypeScript allows when it is not an inline literal) no longer adds actions at runtime.pressandreleaseis a release then a new press (still a tap when it was up), and recordings emit it, so a release and press inside one update replays exactly. Applies toKeyboardandGamepad.Keyboard.presses(host): every key press (bound or not) as{ key, code }, for a settings screen waiting for the key to bind.Keyboard.parseRebinding(bindings, data)andGamepad.parseRebinding(bindings, data): validate saved bindings into aResult<Rebinding<B>, InvalidRebinding>ready forrebind, without casts.Review fixes (third commit)
rebindtakes exactly the actions the source was made with: an extra action fails to compile (extra is not an action of these bindings) even from an object not written inline.InputCapture.systemrequires the resource's value type to be exactly the snapshot type (an extra optional field used to pass); the error namesInputCapture.SnapshotOf<typeof Source>as the fix. Breaking, noted in the changeset.Gamepad.actionstakeswindowand reads the pads on every animation frame;poll()is removed, so keyboard and gamepad share thesnapshot/rebind/disposelifecycle.Review fixes (fourth commit)
connected; recordings write disconnects and reconnects, so a replay drops and regains the pad on the same frames (a disconnect releases everything and zeroes the sticks and triggers, as live).integration/top-downnow plays with a gamepad next to the keyboard:GamepadInputservice,PadStatedeclared withInputCapture.SnapshotOf, its own capture system, left stick (analog) or d-pad to walk, south to interact, with headless tests.Checks
pnpm run check(typecheck, lint, type tests on both TypeScript targets, and the tests) andpnpm pack:checkpass.packages/browser/test/Gamepad.test.ts(17) and theKeyboard.rebindtests, plus the type tests indtslint/Gamepad.tst.tsanddtslint/Keyboard.tst.ts.Generated by Claude Code