Files
sporel-lib-core.interaction/init.lua

125 lines
4.3 KiB
Lua

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