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:
40
README.md
Normal file
40
README.md
Normal 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
248
init.lua
Normal 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
1
manifest.lib
Normal 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"}]}
|
||||
Reference in New Issue
Block a user