From bf3dda0ace3b68bede34741420465dab38952f66 Mon Sep 17 00:00:00 2001 From: Axel Meyer Date: Sat, 9 May 2026 23:22:25 +0200 Subject: [PATCH] feat: P.0.lib.input action-mapping + direction-vector helper --- .gitignore | 2 ++ README.md | 44 +++++++++++++++++++++++++++++++++ init.lua | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++ manifest.lib | 1 + 4 files changed, 117 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 init.lua create mode 100644 manifest.lib diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..41ae811 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build/ +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..f9148ed --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# lib-core.input + +P.0 action-mapping lib. Decouples physical keys from logical actions. +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` + +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. + +## API +- `input.bind(action_name, {key1, key2, ...})` — bind multi-key array +- `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 + +## 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). + +## 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 }) + +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") + -- d.x, d.y ∈ {-1, 0, +1} +end +``` diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..a7f560e --- /dev/null +++ b/init.lua @@ -0,0 +1,70 @@ +-- ===================================================================== +-- lib-core.input — Action-Mapping + Direction-Vector (P.0) +-- See: meta/docs/superpowers/specs/2026-05-09-p0-lib-input-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. +-- ===================================================================== + +local bindings = {} -- action_name -> array of key-codes + +local M = {} + +function M.bind(action_name, keys) + if type(action_name) ~= "string" then + 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')", + action_name)) + end + bindings[action_name] = keys +end + +function M.unbind(action_name) + bindings[action_name] = nil +end + +function M.is_action_down(action_name) + local keys = bindings[action_name] + if not keys then return false end + for _, k in ipairs(keys) do + if engine.input.is_key_down(k) then return true end + end + return false +end + +function M.was_action_pressed(action_name) + local keys = bindings[action_name] + if not keys then return false end + for _, k in ipairs(keys) do + if engine.input.was_pressed(k) then return true end + end + return false +end + +-- Direction-vector helper: returns {x, y} in {-1, 0, +1} each axis. +-- Y-down-positive per Sporel pixel-convention (ADR-0031). +function M.direction(left_action, right_action, up_action, down_action) + local x = (M.is_action_down(right_action) and 1 or 0) + - (M.is_action_down(left_action) and 1 or 0) + local y = (M.is_action_down(down_action) and 1 or 0) + - (M.is_action_down(up_action) and 1 or 0) + return { x = x, y = y } +end + +function M.action_count() + local n = 0 + for _ in pairs(bindings) do n = n + 1 end + return n +end + +-- DEPRECATED-MVP: bind_mouse(action, button) -- mouse-button slice +-- DEPRECATED-MVP: bind_gamepad(action, ...) -- gamepad slice +-- 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 + +return M diff --git a/manifest.lib b/manifest.lib new file mode 100644 index 0000000..45ea304 --- /dev/null +++ b/manifest.lib @@ -0,0 +1 @@ +{"id":"lib-core.input","version":"0.1.0","api_min":"0.1"}