diff --git a/README.md b/README.md index f9148ed..e9c66dc 100644 --- a/README.md +++ b/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. diff --git a/init.lua b/init.lua index a7f560e..cd0bed4 100644 --- a/init.lua +++ b/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 diff --git a/manifest.lib b/manifest.lib index 45ea304..a9f9190 100644 --- a/manifest.lib +++ b/manifest.lib @@ -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"}