feat: P.0.lib.interaction proximity-trigger + action-dispatch
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
build/
|
||||||
|
*.log
|
||||||
58
README.md
Normal file
58
README.md
Normal file
@@ -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
|
||||||
|
```
|
||||||
124
init.lua
Normal file
124
init.lua
Normal file
@@ -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
|
||||||
1
manifest.lib
Normal file
1
manifest.lib
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"id":"lib-core.interaction","version":"0.1.0","api_min":"0.1"}
|
||||||
Reference in New Issue
Block a user