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
130 lines
5.8 KiB
Lua
130 lines
5.8 KiB
Lua
-- =====================================================================
|
|
-- lib-core.input — Action-Mapping + Direction-Vector (v0.2.0)
|
|
-- See: meta/docs/superpowers/specs/2026-05-11-engine-input-symbolic-keys-design.md
|
|
--
|
|
-- 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.
|
|
-- =====================================================================
|
|
|
|
-- 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 = {}
|
|
|
|
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-name strings (action '%s')",
|
|
action_name))
|
|
end
|
|
-- 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)
|
|
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
|
|
-- 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
|