-- ===================================================================== -- lib-core.input — Action-Mapping + Direction-Vector (v0.4.0) -- See: meta/docs/superpowers/specs/2026-05-12-p3-3-mouse-input-design.md -- -- v0.4.0 (P.3.10 2026-05-15): was_action_released added — release-edge -- detection parallel to was_action_pressed. Engine-Surface: -- engine.input.was_released + was_mouse_released. Selection-Lib's -- prev_click_down workaround promoted. -- -- v0.3.0 (P.3.3 2026-05-12): bind() now accepts unified key+mouse -- strings via BIND_MAP with kind-tagged entries ({kind="key"|"mouse", -- code=}). Mixed bindings supported: e.g. -- input.bind("interact", {"e", "mouse_left"}) -- Backward-compatible with v0.2.0 callsites — existing key-only binds -- work unchanged. -- -- v0.2.0 (P.2.6 2026-05-11): bind() accepts string-key-names only. -- Integer engine.input.KEY_* args are rejected. -- -- Scope v0.4.0: action-mapping (string-key+mouse-arrays per action) + -- press-edge + release-edge detection + direction-vector helper. -- DEPRECATED-MVP: mouse-wheel, mouse-delta helper, side-buttons, -- gamepad bindings, action-context-stack, key-rebinding config. -- ===================================================================== -- Unified string → {kind, code} mapping. Lowercase convention. -- "mouse_*" namespaces avoid collision with arrow-key "left"/"right". local BIND_MAP = { -- Letters (26) — kind="key" a = {kind="key", code=engine.input.KEY_A}, b = {kind="key", code=engine.input.KEY_B}, c = {kind="key", code=engine.input.KEY_C}, d = {kind="key", code=engine.input.KEY_D}, e = {kind="key", code=engine.input.KEY_E}, f = {kind="key", code=engine.input.KEY_F}, g = {kind="key", code=engine.input.KEY_G}, h = {kind="key", code=engine.input.KEY_H}, i = {kind="key", code=engine.input.KEY_I}, j = {kind="key", code=engine.input.KEY_J}, k = {kind="key", code=engine.input.KEY_K}, l = {kind="key", code=engine.input.KEY_L}, m = {kind="key", code=engine.input.KEY_M}, n = {kind="key", code=engine.input.KEY_N}, o = {kind="key", code=engine.input.KEY_O}, p = {kind="key", code=engine.input.KEY_P}, q = {kind="key", code=engine.input.KEY_Q}, r = {kind="key", code=engine.input.KEY_R}, s = {kind="key", code=engine.input.KEY_S}, t = {kind="key", code=engine.input.KEY_T}, u = {kind="key", code=engine.input.KEY_U}, v = {kind="key", code=engine.input.KEY_V}, w = {kind="key", code=engine.input.KEY_W}, x = {kind="key", code=engine.input.KEY_X}, y = {kind="key", code=engine.input.KEY_Y}, z = {kind="key", code=engine.input.KEY_Z}, -- Numbers (10) — kind="key" ["0"] = {kind="key", code=engine.input.KEY_ZERO}, ["1"] = {kind="key", code=engine.input.KEY_ONE}, ["2"] = {kind="key", code=engine.input.KEY_TWO}, ["3"] = {kind="key", code=engine.input.KEY_THREE}, ["4"] = {kind="key", code=engine.input.KEY_FOUR}, ["5"] = {kind="key", code=engine.input.KEY_FIVE}, ["6"] = {kind="key", code=engine.input.KEY_SIX}, ["7"] = {kind="key", code=engine.input.KEY_SEVEN}, ["8"] = {kind="key", code=engine.input.KEY_EIGHT}, ["9"] = {kind="key", code=engine.input.KEY_NINE}, -- Arrows (4) — kind="key" left = {kind="key", code=engine.input.KEY_LEFT}, right = {kind="key", code=engine.input.KEY_RIGHT}, up = {kind="key", code=engine.input.KEY_UP}, down = {kind="key", code=engine.input.KEY_DOWN}, -- Whitespace + control (5) — kind="key" space = {kind="key", code=engine.input.KEY_SPACE}, tab = {kind="key", code=engine.input.KEY_TAB}, enter = {kind="key", code=engine.input.KEY_ENTER}, backspace = {kind="key", code=engine.input.KEY_BACKSPACE}, escape = {kind="key", code=engine.input.KEY_ESCAPE}, -- Modifiers (left-side; aliases for ergonomics) — kind="key" shift = {kind="key", code=engine.input.KEY_LEFT_SHIFT}, lshift = {kind="key", code=engine.input.KEY_LEFT_SHIFT}, ctrl = {kind="key", code=engine.input.KEY_LEFT_CONTROL}, lctrl = {kind="key", code=engine.input.KEY_LEFT_CONTROL}, alt = {kind="key", code=engine.input.KEY_LEFT_ALT}, lalt = {kind="key", code=engine.input.KEY_LEFT_ALT}, -- Console-toggle (DE: ^, US: `) — kind="key" ["^"] = {kind="key", code=engine.input.KEY_GRAVE}, -- Mouse buttons (3) — kind="mouse" (P.3.3) mouse_left = {kind="mouse", code=engine.input.MOUSE_LEFT}, mouse_right = {kind="mouse", code=engine.input.MOUSE_RIGHT}, mouse_middle = {kind="mouse", code=engine.input.MOUSE_MIDDLE}, } local bindings = {} -- action_name -> array of {kind, code} entries 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 {kind, code} entry via BIND_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', 'left', 'space', 'mouse_left', etc. — see BIND_MAP in lib-core.input/init.lua", i, action_name, type(k))) end local entry = BIND_MAP[k] if entry == nil then error(string.format("input.bind: unknown key-name '%s' for action '%s' — must be lowercase key ('a'..'z', 'left', 'space', ...) or mouse ('mouse_left', 'mouse_right', 'mouse_middle')", k, action_name)) end resolved[i] = entry end bindings[action_name] = resolved end function M.unbind(action_name) bindings[action_name] = nil end function M.is_action_down(action_name) local entries = bindings[action_name] if not entries then return false end for _, e in ipairs(entries) do if e.kind == "key" and engine.input.is_key_down(e.code) then return true end if e.kind == "mouse" and engine.input.is_mouse_down(e.code) then return true end end return false end function M.was_action_pressed(action_name) local entries = bindings[action_name] if not entries then return false end for _, e in ipairs(entries) do if e.kind == "key" and engine.input.was_pressed(e.code) then return true end if e.kind == "mouse" and engine.input.was_mouse_pressed(e.code) then return true end end return false end function M.was_action_released(action_name) local entries = bindings[action_name] if not entries then return false end for _, e in ipairs(entries) do if e.kind == "key" and engine.input.was_released(e.code) then return true end if e.kind == "mouse" and engine.input.was_mouse_released(e.code) 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 (P.3.3-introduced): -- mouse-wheel binding (wheel_up/wheel_down) — P.3-future-slice (post-P.3.4); continuous-vs-edge brainstorming needed -- mouse-delta helper (engine.input.get_mouse_delta) — P.3.5/6 drag-pan / drag-box -- mouse-button side-buttons (mouse_4 / mouse_5) — additive extension -- mouse-position lib-wrapper (input.mouse_pos() → {x,y}) — convenience layer -- -- DEPRECATED-MVP (existing from v0.2.0): -- bind_gamepad(action, ...) — gamepad slice -- push_context(name) / pop_context() — action-context-stack -- load_bindings_from_config(path) — key-rebinding slice -- right-modifier-keys (rshift/rctrl/ralt) — engine.input.KEY_RIGHT_* required -- function-keys (f1..f12) + special-keys (delete/insert/home/end/pageup/pagedown) — engine.input extension required return M