feat(P.2.6): v0.2.0 string-keys API (BREAKING)
bind() accepts lowercase string-keys only. Integer engine.input.KEY_*
args are rejected. Internal KEY_MAP translates strings to keycodes.
Coverage: 26 letters (a-z), 10 numbers ('0'-'9'), 4 arrows (left/
right/up/down), 5 whitespace+control (space/tab/enter/backspace/
escape), 3 modifiers (shift/ctrl/alt with l-prefix aliases for
explicit left-side), and '^' (console-toggle, DE-keyboard
Zirkumflex / US-keyboard backtick).
Right-modifier-aliases (rshift/rctrl/ralt) + function-keys + special-
keys forthcoming when engine.input exposes the corresponding KEY_*
constants.
Atomic-bind: invalid key fails entire bind without partial state
mutation. Loud Lua-errors on misuse with hint to README.
Adressiert audit-I3 (engine-internal-namespace leak).
Spec: meta/docs/superpowers/specs/2026-05-11-engine-input-symbolic-keys-design.md
This commit is contained in:
60
README.md
60
README.md
@@ -5,40 +5,72 @@ Polling-based (`is_action_down`) + edge-based (`was_action_pressed`).
|
||||
Direction-vector helper composes 4 actions into `{x, y}` ∈ {-1, 0, +1}².
|
||||
|
||||
- Lib-ID: `lib-core.input`
|
||||
- Version: `0.1.0`
|
||||
- Spec: `meta/docs/superpowers/specs/2026-05-09-p0-lib-input-design.md`
|
||||
- Version: `0.2.0` (breaking change from v0.1.0 — string-keys only, see §Migration)
|
||||
- Spec v0.2.0 (P.2.6): `meta/docs/superpowers/specs/2026-05-11-engine-input-symbolic-keys-design.md`
|
||||
- Spec v0.1.0 (P.0): `meta/docs/superpowers/specs/2026-05-09-p0-lib-input-design.md`
|
||||
|
||||
Forward-compat stubs (DEPRECATED-MVP) for mouse-button actions, gamepad
|
||||
bindings, action-context-stack, key-rebinding-config, modifier-combos,
|
||||
analog-axis, was_action_released edge.
|
||||
analog-axis, was_action_released edge, right-modifier-keys, function-keys,
|
||||
special-keys (delete/insert/home/end/pageup/pagedown).
|
||||
|
||||
## API
|
||||
- `input.bind(action_name, {key1, key2, ...})` — bind multi-key array
|
||||
|
||||
- `input.bind(action_name, {"keyname1", "keyname2", ...})` — bind multi-key array (string-keys only, see §Key-Names)
|
||||
- `input.unbind(action_name)` — remove binding
|
||||
- `input.is_action_down(action) → bool` — any bound key currently down
|
||||
- `input.was_action_pressed(action) → bool` — any bound key just-pressed
|
||||
- `input.direction(left, right, up, down) → {x, y}` — vec in {-1,0,+1}², Y-down-positive
|
||||
- `input.action_count() → number` — bound-actions count
|
||||
|
||||
## Key-Names (v0.2.0)
|
||||
|
||||
Strict-lowercase. Engine-internal `engine.input.KEY_*` integer-args are rejected.
|
||||
|
||||
| Category | Names |
|
||||
|---|---|
|
||||
| Letters | `a`, `b`, …, `z` |
|
||||
| Numbers | `0`, `1`, …, `9` |
|
||||
| Arrows | `left`, `right`, `up`, `down` |
|
||||
| Whitespace + control | `space`, `tab`, `enter`, `backspace`, `escape` |
|
||||
| Modifiers (left-side) | `shift` (= `lshift`), `ctrl` (= `lctrl`), `alt` (= `lalt`) |
|
||||
| Console-toggle | `^` (DE: Zirkumflex / US: backtick — Quake/Source-Tradition) |
|
||||
|
||||
Right-modifier-aliases (`rshift`, `rctrl`, `ralt`) + function-keys (`f1`..`f12`) + special-keys (`delete`, `insert`, …) are **forthcoming** — additiv wenn `engine.input` die entsprechenden `KEY_*`-Konstanten exposed.
|
||||
|
||||
Unknown key-names raise a Lua-error with hint to this README.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Y-down-positive per Sporel pixel-convention (ADR-0031).
|
||||
- Multi-key per action (e.g., `{KEY_A, KEY_LEFT}` for both A and Left-Arrow).
|
||||
- Silent-false for undefined actions (debug-friendly; lookup-misuse won't
|
||||
drown the game-loop in errors).
|
||||
- Lua `error(...)` for `bind()` misuse (caller-bug fast-fail).
|
||||
- Multi-key per action (e.g., `{"a", "left"}` for both A and Left-Arrow).
|
||||
- Silent-false for undefined actions (debug-friendly; lookup-misuse won't drown the game-loop in errors).
|
||||
- Lua `error(...)` for `bind()` misuse (caller-bug fast-fail). Atomic: invalid key fails the entire bind without partial-state mutation.
|
||||
|
||||
## Consumer pattern
|
||||
|
||||
```lua
|
||||
local input = require("lib-core.input")
|
||||
input.bind("quit", { engine.input.KEY_ESCAPE })
|
||||
input.bind("pan_left", { engine.input.KEY_A, engine.input.KEY_LEFT })
|
||||
input.bind("pan_right", { engine.input.KEY_D, engine.input.KEY_RIGHT })
|
||||
input.bind("pan_up", { engine.input.KEY_W, engine.input.KEY_UP })
|
||||
input.bind("pan_down", { engine.input.KEY_S, engine.input.KEY_DOWN })
|
||||
input.bind("quit", { "escape" })
|
||||
input.bind("move_left", { "a", "left" })
|
||||
input.bind("move_right", { "d", "right" })
|
||||
input.bind("move_up", { "w", "up" })
|
||||
input.bind("move_down", { "s", "down" })
|
||||
|
||||
function update(ctx, dt)
|
||||
if input.was_action_pressed("quit") then engine.exit(0) end
|
||||
local d = input.direction("pan_left", "pan_right", "pan_up", "pan_down")
|
||||
local d = input.direction("move_left", "move_right", "move_up", "move_down")
|
||||
-- d.x, d.y ∈ {-1, 0, +1}
|
||||
end
|
||||
```
|
||||
|
||||
## Migration v0.1.0 → v0.2.0
|
||||
|
||||
`bind()` switched from integer `engine.input.KEY_*` arguments to lowercase string-keys (per P.2.6). Reason: stop leaking engine-internal namespace into modules; align with ADR-0001 „engine knows verbs, libs bring nouns".
|
||||
|
||||
| v0.1.0 | v0.2.0 |
|
||||
|---|---|
|
||||
| `bind("quit", { engine.input.KEY_ESCAPE })` | `bind("quit", { "escape" })` |
|
||||
| `bind("move", { engine.input.KEY_A, engine.input.KEY_LEFT })` | `bind("move", { "a", "left" })` |
|
||||
|
||||
`engine.input.KEY_*` constants remain available for direct engine-input use (e.g. `engine.input.is_key_down(engine.input.KEY_F1)` for edge-cases without bind), but `lib-core.input.bind` strict-rejects them.
|
||||
|
||||
77
init.lua
77
init.lua
@@ -1,14 +1,56 @@
|
||||
-- =====================================================================
|
||||
-- lib-core.input — Action-Mapping + Direction-Vector (P.0)
|
||||
-- See: meta/docs/superpowers/specs/2026-05-09-p0-lib-input-design.md
|
||||
-- lib-core.input — Action-Mapping + Direction-Vector (v0.2.0)
|
||||
-- See: meta/docs/superpowers/specs/2026-05-11-engine-input-symbolic-keys-design.md
|
||||
--
|
||||
-- Scope: action-mapping (key-arrays per action) + direction-vector helper.
|
||||
-- DEPRECATED-MVP for: mouse-button actions, gamepad bindings, action-
|
||||
-- context-stack, key-rebinding config, modifier-combos, analog-axis,
|
||||
-- was_action_released edge.
|
||||
-- v0.2.0 (P.2.6 2026-05-11): bind() accepts string-key-names only.
|
||||
-- Integer engine.input.KEY_* args are rejected (modules go through
|
||||
-- string-API; KEY_* stays as low-level engine-primitive).
|
||||
--
|
||||
-- Scope: action-mapping (string-key-name-arrays per action) +
|
||||
-- direction-vector helper. DEPRECATED-MVP for: mouse-button actions,
|
||||
-- gamepad bindings, action-context-stack, key-rebinding config,
|
||||
-- modifier-combos, analog-axis, was_action_released edge.
|
||||
-- =====================================================================
|
||||
|
||||
local bindings = {} -- action_name -> array of key-codes
|
||||
-- string → engine.input.KEY_* mapping. Lowercase-only convention.
|
||||
-- Right-modifier-Aliases (rshift/rctrl/ralt) + function-keys + special-keys
|
||||
-- (delete/insert/home/end/pageup/pagedown) sind forthcoming, additiv wenn
|
||||
-- engine.input rechte-Modifier + F-keys + Special-Keys exposed.
|
||||
local KEY_MAP = {
|
||||
-- Letters (26)
|
||||
a = engine.input.KEY_A, b = engine.input.KEY_B, c = engine.input.KEY_C,
|
||||
d = engine.input.KEY_D, e = engine.input.KEY_E, f = engine.input.KEY_F,
|
||||
g = engine.input.KEY_G, h = engine.input.KEY_H, i = engine.input.KEY_I,
|
||||
j = engine.input.KEY_J, k = engine.input.KEY_K, l = engine.input.KEY_L,
|
||||
m = engine.input.KEY_M, n = engine.input.KEY_N, o = engine.input.KEY_O,
|
||||
p = engine.input.KEY_P, q = engine.input.KEY_Q, r = engine.input.KEY_R,
|
||||
s = engine.input.KEY_S, t = engine.input.KEY_T, u = engine.input.KEY_U,
|
||||
v = engine.input.KEY_V, w = engine.input.KEY_W, x = engine.input.KEY_X,
|
||||
y = engine.input.KEY_Y, z = engine.input.KEY_Z,
|
||||
-- Numbers (10)
|
||||
["0"] = engine.input.KEY_ZERO, ["1"] = engine.input.KEY_ONE,
|
||||
["2"] = engine.input.KEY_TWO, ["3"] = engine.input.KEY_THREE,
|
||||
["4"] = engine.input.KEY_FOUR, ["5"] = engine.input.KEY_FIVE,
|
||||
["6"] = engine.input.KEY_SIX, ["7"] = engine.input.KEY_SEVEN,
|
||||
["8"] = engine.input.KEY_EIGHT, ["9"] = engine.input.KEY_NINE,
|
||||
-- Arrows (4)
|
||||
left = engine.input.KEY_LEFT, right = engine.input.KEY_RIGHT,
|
||||
up = engine.input.KEY_UP, down = engine.input.KEY_DOWN,
|
||||
-- Whitespace + control (5)
|
||||
space = engine.input.KEY_SPACE,
|
||||
tab = engine.input.KEY_TAB,
|
||||
enter = engine.input.KEY_ENTER,
|
||||
backspace = engine.input.KEY_BACKSPACE,
|
||||
escape = engine.input.KEY_ESCAPE,
|
||||
-- Modifiers (left-side; aliases for ergonomics)
|
||||
shift = engine.input.KEY_LEFT_SHIFT, lshift = engine.input.KEY_LEFT_SHIFT,
|
||||
ctrl = engine.input.KEY_LEFT_CONTROL, lctrl = engine.input.KEY_LEFT_CONTROL,
|
||||
alt = engine.input.KEY_LEFT_ALT, lalt = engine.input.KEY_LEFT_ALT,
|
||||
-- Console-toggle (DE: ^, US: `)
|
||||
["^"] = engine.input.KEY_GRAVE,
|
||||
}
|
||||
|
||||
local bindings = {} -- action_name -> array of key-codes (post-resolution)
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -17,10 +59,25 @@ function M.bind(action_name, keys)
|
||||
error("input.bind: action_name must be a string")
|
||||
end
|
||||
if type(keys) ~= "table" or #keys == 0 then
|
||||
error(string.format("input.bind: keys must be a non-empty array of key-codes (action '%s')",
|
||||
error(string.format("input.bind: keys must be a non-empty array of key-name strings (action '%s')",
|
||||
action_name))
|
||||
end
|
||||
bindings[action_name] = keys
|
||||
-- Resolve each string-key to engine keycode via KEY_MAP. Atomic: if any
|
||||
-- key invalid, the entire bind fails without partial-state mutation.
|
||||
local resolved = {}
|
||||
for i, k in ipairs(keys) do
|
||||
if type(k) ~= "string" then
|
||||
error(string.format("input.bind: key #%d for action '%s' must be a lowercase string (got %s); use 'a'..'z', '0'..'9', 'left', 'space', 'ctrl', 'escape', etc. — see lib-core.input README for full table",
|
||||
i, action_name, type(k)))
|
||||
end
|
||||
local code = KEY_MAP[k]
|
||||
if code == nil then
|
||||
error(string.format("input.bind: unknown key-name '%s' for action '%s' — must be lowercase, see KEY_MAP in lib-core.input/init.lua for valid keys",
|
||||
k, action_name))
|
||||
end
|
||||
resolved[i] = code
|
||||
end
|
||||
bindings[action_name] = resolved
|
||||
end
|
||||
|
||||
function M.unbind(action_name)
|
||||
@@ -66,5 +123,7 @@ end
|
||||
-- DEPRECATED-MVP: push_context(name) / pop_context() -- action-context-stack
|
||||
-- DEPRECATED-MVP: load_bindings_from_config(path) -- key-rebinding slice
|
||||
-- DEPRECATED-MVP: was_action_released(action) -- lift-detection slice
|
||||
-- DEPRECATED-MVP: right-modifier-keys (rshift/rctrl/ralt) -- engine.input.KEY_RIGHT_* erforderlich
|
||||
-- DEPRECATED-MVP: function-keys (f1..f12) + special-keys (delete/insert/home/end/pageup/pagedown) -- engine.input-Erweiterung erforderlich
|
||||
|
||||
return M
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"id":"lib-core.input","version":"0.1.0","api_min":"0.1"}
|
||||
{"id":"lib-core.input","version":"0.2.0","api_min":"0.1"}
|
||||
|
||||
Reference in New Issue
Block a user