feat(input): #83 Input Manager + auto-commit the device layer in the frame loop
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 22s
ci / build-and-test (push) Successful in 2m18s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 28s

Fixes the papercut: the generated frame loop called rt_poll() (feeding only
Input.key) but never input_poll(), so Input.active/key_down/mouse/pad read
empty unless the game called Input.poll() by hand. The loop now calls
input_poll() when the game uses any Input runtime method — committing the
held-key/mouse/gamepad state, and record/replay — and stores its return as the
frame key so Input.key still works. A game using no Input runtime keeps the
plain rt_poll path, byte-identical.

Adds the Input-Manager API: Input.action(name,key) ships a default binding
(kept if already bound, so a rebind/loaded map isn't clobbered),
Input.bind_pad(name,button) makes an action device-agnostic (keyboard OR pad),
and Input.active/just_pressed/just_released read the multi-key device layer
with clean on-press/on-release edges (deterministic, dispatch-free — a handler
polls the edge; a replay fires identically).

Examples input_manager + input_auto, 5 docs pages. Full suite 107/0, goldens
byte-identical, fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-02 07:04:10 +03:00
parent 6a83c28e05
commit bccd26fb29
14 changed files with 31773 additions and 31127 deletions

View file

@ -0,0 +1,23 @@
---
id: input-action
name: Input.action
category: input
kind: namespace-method
tokens: Input.action
sig: Input.action(action: str, key: int) -> void
tip: Register a default key binding for an action (kept if already bound).
order: 31
ns: Input
member: action
---
Registers a <em>default</em> keyboard binding: it binds <code>key</code> to the action only if the action has no key bound yet. Ship your defaults with <code>Input.action</code> in a Boot handler; a player's later <a href="input-rebind.html"><code>Input.rebind</code></a> (or a loaded key-map) is not clobbered, and re-running the defaults is idempotent. Combine with <a href="input-bind_pad.html"><code>Input.bind_pad</code></a> for a device-agnostic action, and read it with <a href="input-active.html"><code>Input.active</code></a>.
```ludic
program Defaults {
entry {
Input.action("jump", ' ')
if Input.active("jump") { print(1) }
}
}
```

View file

@ -0,0 +1,23 @@
---
id: input-active
name: Input.active
category: input
kind: namespace-method
tokens: Input.active
sig: Input.active(action: str) -> bool
tip: Is a named action active right now (any bound key held, or pad button down)?
order: 33
ns: Input
member: active
---
Returns whether a named action is active this frame, reading the whole <a href="input-key_down.html">device layer</a>: true if any of the action's bound keyboard keys is in the multi-key held-set <em>or</em> any bound gamepad button is down on pad 0. Unlike <a href="input-down.html"><code>Input.down</code></a> (which reads only the single per-frame key), this sees simultaneous holds (move <em>and</em> jump) and is device-agnostic. The frame loop commits the device layer automatically, so it reads live without a manual <a href="input-poll.html"><code>Input.poll</code></a>.
```ludic
program Active {
entry {
Input.action("left", 'a')
if Input.active("left") { print(1) }
}
}
```

View file

@ -0,0 +1,24 @@
---
id: input-bind_pad
name: Input.bind_pad
category: input
kind: namespace-method
tokens: Input.bind_pad
sig: Input.bind_pad(action: str, button: int) -> void
tip: Also fire a named action from a gamepad button (device-agnostic).
order: 32
ns: Input
member: bind_pad
---
Maps a gamepad <code>button</code> (on pad 0) to a named action, so the action fires from the keyboard <em>or</em> the pad. The same action can carry both keyboard keys (<a href="input-bind.html"><code>Input.bind</code></a> / <a href="input-action.html"><code>Input.action</code></a>) and pad buttons; <a href="input-active.html"><code>Input.active</code></a> and its edges fire on either device — one action, any input.
```ludic
program Pad {
entry {
Input.action("fire", 'j')
Input.bind_pad("fire", 0)
if Input.active("fire") { print(1) }
}
}
```

View file

@ -0,0 +1,23 @@
---
id: input-just_pressed
name: Input.just_pressed
category: input
kind: namespace-method
tokens: Input.just_pressed
sig: Input.just_pressed(action: str) -> bool
tip: Did a named action go active this frame (the deterministic on-press)?
order: 34
ns: Input
member: just_pressed
---
Returns whether a named action went active this frame — active now, not active last frame. This is the deterministic on-press edge over the device layer (keyboard or pad), the event-style "just pressed" without a callback: a handler polls it each frame and reacts. Pair with <a href="input-just_released.html"><code>Input.just_released</code></a> for the release edge and <a href="input-active.html"><code>Input.active</code></a> for the held state.
```ludic
program Press {
entry {
Input.action("jump", ' ')
if Input.just_pressed("jump") { print(1) }
}
}
```

View file

@ -0,0 +1,23 @@
---
id: input-just_released
name: Input.just_released
category: input
kind: namespace-method
tokens: Input.just_released
sig: Input.just_released(action: str) -> bool
tip: Did a named action go inactive this frame (the on-release edge)?
order: 35
ns: Input
member: just_released
---
Returns whether a named action went inactive this frame — not active now, active last frame. The on-release edge over the device layer (keyboard or pad), symmetric with <a href="input-just_pressed.html"><code>Input.just_pressed</code></a>. Use it for release-triggered actions (variable-height jump cut-off, charge-and-release). Deterministic, so a replay fires the same edges.
```ludic
program Release {
entry {
Input.action("charge", ' ')
if Input.just_released("charge") { print(1) }
}
}
```