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