feat(P.3.7): lib-core.command v0.1.0 — initial release

Handle-Registry (get_pos + set_pos + speed callbacks) + lib-driven
auto-wire (bind_move_action + bind_target_provider) + per-frame
movement with arrive-snap. Move-only execution; context-dispatch
deferred to P.4.

See: meta/docs/superpowers/specs/2026-05-14-p3-7-lib-command-design.md
This commit is contained in:
Axel Meyer
2026-05-14 17:00:59 +02:00
commit 7a5bc36daa
3 changed files with 289 additions and 0 deletions

40
README.md Normal file
View File

@@ -0,0 +1,40 @@
# lib-core.command — v0.1.0
Move-Commands für registrierte Units. RTS-Style Click-to-Move (RMB → bewegt selected Units).
## Quick Setup
```lua
local command = require("lib-core.command")
local input = require("lib-core.input")
local selection = require("lib-core.selection") -- target-provider source
input.bind("rmb", { "mouse_right" })
command.bind_move_action("rmb")
command.bind_target_provider(function() return selection.list() end)
-- Register each movable unit (read+write callbacks + speed):
local handle = command.register(
function() return { x = unit.x, y = unit.y } end,
function(nx, ny) unit.x = nx; unit.y = ny end,
100 -- px/s
)
-- per-frame:
function update(ctx, dt)
command.update(dt)
end
```
## API
- Registry: `register(get_pos_fn, set_pos_fn, speed) → handle`, `unregister(h)`, `count_registered()`
- Speed: `set_speed(h, n)` / `speed(h) → n`
- Issue (programmatic): `move_to(handles, x, y)`, `cancel(h)`, `cancel_all()`
- State-Query: `current(h) → {type, target_x, target_y}|nil`, `is_moving(h) → bool`
- Bindings: `bind_move_action(name)`, `bind_target_provider(fn)`
- Settings: `set_arrive_threshold(n)` / `arrive_threshold()`, `set_enabled(b)` / `enabled()`
- Per-Frame: `update(dt)`
See `meta/docs/superpowers/specs/2026-05-14-p3-7-lib-command-design.md` for full design.

248
init.lua Normal file
View File

@@ -0,0 +1,248 @@
-- =====================================================================
-- lib-core.command v0.1.0 — Move-Commands + Per-Frame-Execution
-- See: meta/docs/superpowers/specs/2026-05-14-p3-7-lib-command-design.md
--
-- Handle-Registry mit get_pos + set_pos + speed (extending selection-pattern
-- mit write-callback). Lib-driven auto-wire via action-binding (press-edge,
-- analog selection). Per-frame movement: direction-normalize + step-vs-distance
-- → arrive-snap. Move-only in v0.1.0; context-sensitive dispatch deferred.
--
-- DEPRECATED-MVPs siehe Spec §7 + inline-comments unten.
-- =====================================================================
local input = require("lib-core.input")
local camera = require("lib-core.camera")
-- Registry: array of {handle, get_pos, set_pos, speed}.
local units = {}
local next_handle = 1
-- Active commands: handle → {type, target_x, target_y}.
local active_commands = {}
-- Bindings.
local move_action = nil
local target_provider = nil
-- Settings.
local arrive_threshold = 2 -- px
local enabled_flag = true
-- ---------- internal helpers ----------
local function check_function(arg_name, value)
if type(value) ~= "function" then
error(string.format("command.%s: must be a function", arg_name))
end
end
local function check_positive(arg_name, value)
if type(value) ~= "number" or value <= 0 then
error(string.format("command.%s: must be positive number", arg_name))
end
end
local function check_string(arg_name, value)
if type(value) ~= "string" or value == "" then
error(string.format("command.%s: must be non-empty string", arg_name))
end
end
local function find_unit_index(handle)
for i, u in ipairs(units) do
if u.handle == handle then return i end
end
return nil
end
local function find_unit(handle)
for _, u in ipairs(units) do
if u.handle == handle then return u end
end
return nil
end
-- ---------- public API ----------
local M = {}
-- Registry
function M.register(get_pos_fn, set_pos_fn, speed)
check_function("register.get_pos_fn", get_pos_fn)
check_function("register.set_pos_fn", set_pos_fn)
check_positive("register.speed", speed)
local h = next_handle
next_handle = next_handle + 1
units[#units + 1] = {
handle = h,
get_pos = get_pos_fn,
set_pos = set_pos_fn,
speed = speed,
}
return h
end
function M.unregister(handle)
local idx = find_unit_index(handle)
if idx then
table.remove(units, idx)
active_commands[handle] = nil -- clear dangling command
end
-- silent no-op on unknown handle
end
function M.count_registered()
return #units
end
-- Per-Unit Speed
function M.set_speed(handle, n)
local u = find_unit(handle)
if u == nil then
error("command.set_speed: handle " .. tostring(handle) .. " not registered")
end
check_positive("set_speed.n", n)
u.speed = n
end
function M.speed(handle)
local u = find_unit(handle)
if u == nil then
error("command.speed: handle " .. tostring(handle) .. " not registered")
end
return u.speed
end
-- Command-Issue (Programmatic)
function M.move_to(handles, target_x, target_y)
if type(handles) ~= "table" then
error("command.move_to: handles must be a table (array of handles)")
end
if type(target_x) ~= "number" or type(target_y) ~= "number" then
error("command.move_to: target_x and target_y must be numbers")
end
-- Validate ALL handles BEFORE mutating any state.
for _, h in ipairs(handles) do
if find_unit(h) == nil then
error("command.move_to: handle " .. tostring(h) .. " not registered")
end
end
for _, h in ipairs(handles) do
active_commands[h] = { type = "move", target_x = target_x, target_y = target_y }
end
end
function M.cancel(handle)
active_commands[handle] = nil -- silent no-op
end
function M.cancel_all()
active_commands = {}
end
-- Command-State-Query
function M.current(handle)
local cmd = active_commands[handle]
if cmd == nil then return nil end
return { type = cmd.type, target_x = cmd.target_x, target_y = cmd.target_y }
end
function M.is_moving(handle)
return active_commands[handle] ~= nil
end
-- Bindings
function M.bind_move_action(action_name)
check_string("bind_move_action", action_name)
move_action = action_name
end
function M.bind_target_provider(fn)
check_function("bind_target_provider", fn)
target_provider = fn
end
-- Settings
function M.set_arrive_threshold(n)
check_positive("set_arrive_threshold", n)
arrive_threshold = n
end
function M.arrive_threshold()
return arrive_threshold
end
function M.set_enabled(b)
if type(b) ~= "boolean" then
error("command.set_enabled: must be boolean")
end
enabled_flag = b
end
function M.enabled()
return enabled_flag
end
-- Per-Frame
function M.update(dt)
if not enabled_flag then return end
-- A) Input-Polling für Auto-Wire (press-edge):
if move_action ~= nil and target_provider ~= nil then
if input.was_action_pressed(move_action) then
local handles = target_provider()
if type(handles) == "table" and #handles > 0 then
local mx, my = engine.input.get_mouse_pos()
local wx, wy = camera.screen_to_world(mx, my)
M.move_to(handles, wx, wy)
end
end
end
-- B) Movement-Tick: iterate active commands:
for handle, cmd in pairs(active_commands) do
local unit = find_unit(handle)
if unit == nil then
active_commands[handle] = nil -- handle unregistered
elseif cmd.type == "move" then
local pos = unit.get_pos()
if type(pos) == "table"
and type(pos.x) == "number" and type(pos.y) == "number" then
local dx = cmd.target_x - pos.x
local dy = cmd.target_y - pos.y
local dist = math.sqrt(dx * dx + dy * dy)
local step = unit.speed * dt
if dist <= arrive_threshold or step >= dist then
unit.set_pos(cmd.target_x, cmd.target_y)
active_commands[handle] = nil
else
local ratio = step / dist
unit.set_pos(pos.x + dx * ratio, pos.y + dy * ratio)
end
end
-- invalid pos return: skip frame (best-effort)
end
end
end
-- DEPRECATED-MVP: context-sensitive RMB dispatch (attack/harvest/follow per
-- target-type) — own slice when 2nd command-type exists.
-- DEPRECATED-MVP: pathfinding (A* / nav-mesh) — own slice, P.4
-- DEPRECATED-MVP: stop-command via dedicated action — comes with attack slice
-- DEPRECATED-MVP: formation-move (offset keeping) — P.4 RTS-polish
-- DEPRECATED-MVP: command-queue (Shift+RMB waypoints) — own slice
-- DEPRECATED-MVP: path-move (waypoint-list as single target) — precursor to queue
-- DEPRECATED-MVP: per-command speed-multiplier (sprint, walk) — gameplay-driven
-- DEPRECATED-MVP: faction/ownership system (own vs enemy command-eligibility) — P.4+
-- DEPRECATED-MVP: observer pattern (on_command_complete callback) — UI-refresh
-- DEPRECATED-MVP: direction-smoothing (turn-rate, acceleration) — P.4 RTS-polish
-- DEPRECATED-MVP: collision-avoidance (units pushing each other) — P.4+
return M

1
manifest.lib Normal file
View File

@@ -0,0 +1 @@
{"id":"lib-core.command","version":"0.1.0","api_min":"0.1","deps":[{"id":"lib-core.input","version":"0.3.0"},{"id":"lib-core.camera","version":"0.3.0"}]}