From 0c8576be05eb0404b498f0d45829ce9cfae97f9c Mon Sep 17 00:00:00 2001 From: Axel Meyer Date: Sun, 10 May 2026 13:11:13 +0200 Subject: [PATCH] feat: P.0.lib.interaction proximity-trigger + action-dispatch --- .gitignore | 2 + README.md | 58 ++++++++++++++++++++++++ init.lua | 124 +++++++++++++++++++++++++++++++++++++++++++++++++++ manifest.lib | 1 + 4 files changed, 185 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..0c514bf --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# lib-core.interaction + +P.0 proximity-trigger + action-dispatch lib. Register triggers with +`(target_x, target_y, range, action_name, callback)`; per-frame +`update(actor_x, actor_y)` checks distance + `was_action_pressed` and +fires the nearest matching trigger per `action_name`. + +- Lib-ID: `lib-core.interaction` +- Version: `0.1.0` +- Spec: `meta/docs/superpowers/specs/2026-05-10-p0-lib-interaction-design.md` + +Forward-compat stubs (DEPRECATED-MVP) for AABB-targets, action-effect- +pipeline integration, visual-feedback / moodlet, action-cycling, +hold-vs-press, per-action-resolution-override, callback pcall-wrap, +target-entity-binding, range-shape variants. + +## API +- `interaction.register(target_x, target_y, range, action_name, callback) → id` +- `interaction.unregister(id)` — silent no-op if id unknown +- `interaction.update(actor_x, actor_y)` — proximity-check + dispatch +- `interaction.list() → array of { id, target_x, target_y, range, action_name }` +- `interaction.trigger_count() → integer` + +## Conventions +- Targets are points (not AABBs in P.0). `range` is scalar pixel-radius. +- Distance: `engine.spatial.distance(actor, target) ≤ range`. +- Multi-match resolution: per `action_name`, only NEAREST in-range trigger + fires per update. Different `action_name`s are independent (all may fire + the same frame). +- Was-pressed semantics: `input.was_action_pressed` (single-frame edge). +- Callback receives `info = { actor_x, actor_y, target_x, target_y, + distance, trigger_id, action_name }`. Closures may ignore the arg + (`function() ... end` is legal). +- Loud Lua-errors on API misuse (non-number coords, non-positive range, + non-string action_name, non-function callback). +- Silent-accept for `action_name` that hasn't been registered with + `lib-core.input` — the trigger simply never fires (matches input-lib's + silent-false convention). +- Callback errors bubble up (no pcall-wrap in P.0). Resilience is + E.9 refactor target. + +## Consumer pattern +```lua +local interaction = require("lib-core.interaction") +local input = require("lib-core.input") + +input.bind("interact", { engine.input.KEY_E }) + +local sign = { x = 144, y = 144, text = "Hier steht: ..." } +interaction.register(sign.x, sign.y, 40, "interact", function(info) + engine.print(sign.text) +end) + +function update(ctx, dt) + -- (after player.update; reads player center) + interaction.update(player_center_x, player_center_y) +end +``` diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..cace700 --- /dev/null +++ b/init.lua @@ -0,0 +1,124 @@ +-- ===================================================================== +-- lib-core.interaction — Proximity-Trigger + Action-Dispatch (P.0) +-- See: meta/docs/superpowers/specs/2026-05-10-p0-lib-interaction-design.md +-- +-- Scope: register proximity-triggers (target-point + range + action-name + +-- callback), dispatch nearest matching trigger per action_name when action +-- fires. Was-pressed semantics (single-frame edge, no hold-repeat). +-- DEPRECATED-MVP for: AABB-targets, action-effect-pipeline, visual-feedback, +-- action-cycling, hold-charging, per-action-resolution-override, +-- callback-pcall-wrap, target-entity-binding, range-shape-variants. +-- ===================================================================== +local input = require("lib-core.input") +local spatial = engine.spatial + +local M = {} + +-- module-local state +local triggers = {} -- array of trigger records +local next_id = 1 + +local function find_index_by_id(id) + for i, t in ipairs(triggers) do + if t.id == id then return i end + end + return nil +end + +function M.register(target_x, target_y, range, action_name, callback) + if type(target_x) ~= "number" or type(target_y) ~= "number" then + error("interaction.register: target_x and target_y must be numbers") + end + if type(range) ~= "number" or range <= 0 then + error("interaction.register: range must be a positive number") + end + if type(action_name) ~= "string" or action_name == "" then + error("interaction.register: action_name must be a non-empty string") + end + if type(callback) ~= "function" then + error("interaction.register: callback must be a function") + end + local id = next_id + next_id = next_id + 1 + triggers[#triggers + 1] = { + id = id, + target_x = target_x, + target_y = target_y, + range = range, + action_name = action_name, + callback = callback, + } + return id +end + +function M.unregister(id) + local idx = find_index_by_id(id) + if idx then + table.remove(triggers, idx) + end + -- silent no-op if id not found +end + +function M.update(actor_x, actor_y) + if type(actor_x) ~= "number" or type(actor_y) ~= "number" then + error("interaction.update: actor_x and actor_y must be numbers") + end + + -- group-by-action_name: { action_name -> { nearest_trigger, nearest_dist } } + local groups = {} + for _, t in ipairs(triggers) do + local d = spatial.distance(actor_x, actor_y, t.target_x, t.target_y) + if d <= t.range then + local g = groups[t.action_name] + if not g or d < g.dist then + groups[t.action_name] = { trigger = t, dist = d } + end + end + end + + -- fire nearest per action_name if action was pressed this frame + for action_name, g in pairs(groups) do + if input.was_action_pressed(action_name) then + local t = g.trigger + t.callback({ + actor_x = actor_x, + actor_y = actor_y, + target_x = t.target_x, + target_y = t.target_y, + distance = g.dist, + trigger_id = t.id, + action_name = action_name, + }) + end + end +end + +function M.list() + local out = {} + for i, t in ipairs(triggers) do + out[i] = { + id = t.id, + target_x = t.target_x, + target_y = t.target_y, + range = t.range, + action_name = t.action_name, + } + end + return out +end + +function M.trigger_count() + return #triggers +end + +-- DEPRECATED-MVP: AABB-targets (range as rect) -- footprint-items slice +-- DEPRECATED-MVP: action-effect-pipeline integration -- post-P.0 action-pipeline activation +-- DEPRECATED-MVP: visual-feedback / moodlet UI hint -- UI slice +-- DEPRECATED-MVP: action-cycling (multi-action UX) -- UI + multi-action-UX slice +-- DEPRECATED-MVP: hold-vs-press semantics (charge, hold) -- charge-action slice +-- DEPRECATED-MVP: per-action-resolution-override -- multi-pickup slice +-- DEPRECATED-MVP: callback pcall-wrap (resilience) -- E.9 resilience refactor +-- DEPRECATED-MVP: target-entity-binding (entity-handle) -- actor / composition slice +-- DEPRECATED-MVP: range-shape variants (cone, line, AABB) -- combat / vision-cone slice + +return M diff --git a/manifest.lib b/manifest.lib new file mode 100644 index 0000000..aef01cc --- /dev/null +++ b/manifest.lib @@ -0,0 +1 @@ +{"id":"lib-core.interaction","version":"0.1.0","api_min":"0.1"}