commit 7a5bc36daa5fcc9e3ccde6844d9ea57c75018db1 Author: Axel Meyer Date: Thu May 14 17:00:59 2026 +0200 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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..241dbbd --- /dev/null +++ b/README.md @@ -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. diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..1e706a5 --- /dev/null +++ b/init.lua @@ -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 diff --git a/manifest.lib b/manifest.lib new file mode 100644 index 0000000..536abb4 --- /dev/null +++ b/manifest.lib @@ -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"}]} \ No newline at end of file