-- ===================================================================== -- 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