diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100644 index 0000000..1ebdad0 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,13 @@ +#!/bin/sh +# Sporel API-Doc-Convention pre-commit hook. +SPOREL_EXE="${SPOREL_EXE:-$(command -v Sporel.exe 2>/dev/null || command -v sporel 2>/dev/null)}" +if [ -z "$SPOREL_EXE" ]; then + echo "INFO: Sporel.exe not on PATH. Skipping lint." >&2 + exit 0 +fi +"$SPOREL_EXE" --lint="$(pwd)" --fix +RC=$? +if git diff --cached --name-only | grep -qx 'README.md'; then + git add README.md +fi +exit $RC diff --git a/README.md b/README.md index 241dbbd..c1e8f1a 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,190 @@ -# lib-core.command — v0.1.0 +# lib-core.command -Move-Commands für registrierte Units. RTS-Style Click-to-Move (RMB → bewegt selected Units). +Move-commands for registered units. RTS-style click-to-move: press a bound action (e.g., RMB) and selected units move to the world-coord under the cursor. Lib-driven auto-wire via action-binding + target-provider; per-frame movement via direction-normalize + step-vs-distance + arrive-snap. -## Quick Setup +**Version:** 0.1.0 +**Lib-ID:** lib-core.command +**Requires:** lib-core.input v>=0.4.0, lib-core.camera v>=0.3.0 +**Tags:** command, movement, rts, click-to-move +## Topology + + +```mermaid +graph LR + this["lib-core.command"] + lib_core_input["lib-core.input"] + this --> lib_core_input + lib_core_camera["lib-core.camera"] + this --> lib_core_camera + engine["engine.*"] + this --> engine +``` + + +## API + +### `command.register(get_pos_fn, set_pos_fn, speed)` +**Syntax:** `command.register(get_pos_fn: fun() -> {x: number, y: number}, set_pos_fn: fun(x: number, y: number), speed: number) -> handle` + +**Example:** ```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: +**Description:** Registers a movable unit. Caller supplies read-callback (returns `{x, y}`) + write-callback (applies new pos) + speed in pixels per second. Returns an opaque handle used for all subsequent calls. + +### `command.unregister(handle)` +**Syntax:** `command.unregister(handle: handle) -> void` + +**Description:** Removes a unit from the registry. No-op if the handle is unknown. Cancels any active command on that handle. + +### `command.count_registered()` +**Syntax:** `command.count_registered() -> number` + +**Description:** Returns the count of currently registered units. Debug-friendly. + +### `command.set_speed(handle, n)` +**Syntax:** `command.set_speed(handle: handle, n: number) -> void` + +**Description:** Updates a unit's move-speed (px/s). Takes effect on the next `update(dt)` tick. + +### `command.speed(handle)` +**Syntax:** `command.speed(handle: handle) -> number` + +**Description:** Returns current speed for the handle. + +### `command.move_to(handles, target_x, target_y)` +**Syntax:** `command.move_to(handles: handle[], target_x: number, target_y: number) -> void` + +**Example:** +```lua +command.move_to({ h1, h2, h3 }, 512, 384) +``` + +**Description:** Programmatic move-issue for a batch of handles. All listed units start moving toward `(target_x, target_y)` (world-coords). Replaces any active command per handle. + +### `command.cancel(handle)` +**Syntax:** `command.cancel(handle: handle) -> void` + +**Description:** Cancels the active command for one handle. No-op if no command is active. + +### `command.cancel_all()` +**Syntax:** `command.cancel_all() -> void` + +**Description:** Cancels active commands for all registered handles. + +### `command.current(handle)` +**Syntax:** `command.current(handle: handle) -> {type: string, target_x: number, target_y: number} | nil` + +**Example:** +```lua +local cmd = command.current(h) +if cmd then print(cmd.type, cmd.target_x, cmd.target_y) end +``` + +**Description:** Returns the active command for a handle, or `nil` if idle. v0.1.0 always has `type == "move"`. + +### `command.is_moving(handle)` +**Syntax:** `command.is_moving(handle: handle) -> bool` + +**Description:** Convenience predicate. Equivalent to `command.current(h) ~= nil`. + +### `command.bind_move_action(action_name)` +**Syntax:** `command.bind_move_action(action_name: string) -> void` + +**Example:** +```lua +input.bind("rmb", { "mouse_right" }) +command.bind_move_action("rmb") +``` + +**Description:** Binds an input-action whose press-edge issues a move-command to all targets returned by the registered target-provider. Cursor world-coord is computed via `lib-core.camera.screen_to_world`. + +### `command.bind_target_provider(fn)` +**Syntax:** `command.bind_target_provider(fn: fun() -> handle[]) -> void` + +**Example:** +```lua +local selection = require("lib-core.selection") +command.bind_target_provider(function() return selection.list() end) +``` + +**Description:** Registers a callback that returns the current target-handle-list whenever the move-action fires. Typical wiring: forward `lib-core.selection.list()`. + +### `command.set_arrive_threshold(n)` +**Syntax:** `command.set_arrive_threshold(n: number) -> void` + +**Description:** Sets the arrival-distance in pixels. Once a unit is within `n` px of its target, the command completes (snap-to-target). + +### `command.arrive_threshold()` +**Syntax:** `command.arrive_threshold() -> number` + +**Description:** Returns the current arrive-threshold. + +### `command.set_enabled(b)` +**Syntax:** `command.set_enabled(b: bool) -> void` + +**Description:** Master enable/disable. When disabled, `update(dt)` is a no-op and the bound action is ignored. + +### `command.enabled()` +**Syntax:** `command.enabled() -> bool` + +**Description:** Returns master-enabled state. + +### `command.update(dt)` +**Syntax:** `command.update(dt: number) -> void` + +**Example:** +```lua function update(ctx, dt) command.update(dt) end ``` -## API +**Description:** Per-frame execution: for each active command, computes direction-vector + normalized step, applies `set_pos_fn(...)`. When step >= remaining distance, snaps to target and clears the command. -- 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)` +## Conventions -See `meta/docs/superpowers/specs/2026-05-14-p3-7-lib-command-design.md` for full design. +- World-coords + pixel-units throughout (per ADR-0031). +- Move-only in v0.1.0; context-sensitive dispatch (attack/use/etc.) deferred. +- Issuing a new move on an active handle replaces the old target (no queueing). +- `register` / `unregister` are O(1) amortized; handles are opaque integers. + +## Consumer pattern + +```lua +local input = require("lib-core.input") +local selection = require("lib-core.selection") +local command = require("lib-core.command") + +input.bind("rmb", { "mouse_right" }) +command.bind_move_action("rmb") +command.bind_target_provider(function() return selection.list() end) + +local handle = command.register( + function() return { x = unit.x, y = unit.y } end, + function(nx, ny) unit.x = nx; unit.y = ny end, + 100 +) + +function update(ctx, dt) + command.update(dt) +end +``` + +## CHANGELOG + +### v0.1.0 (P.3.7) +- Initial release: move-only commands + handle-registry + auto-wire (action + target-provider) + per-frame execution. + +## References + +- Spec v0.1.0 (P.3.7): `meta/docs/superpowers/specs/2026-05-14-p3-7-lib-command-design.md` +- ADR-0001 (engine knows verbs, libs bring nouns) +- ADR-0031 (pixel-convention: Y-down-positive) +- ADR-0038 (API-Doc-Convention) diff --git a/scripts/install-hooks.sh b/scripts/install-hooks.sh new file mode 100644 index 0000000..2ff6e41 --- /dev/null +++ b/scripts/install-hooks.sh @@ -0,0 +1,3 @@ +#!/bin/sh +git config core.hooksPath .githooks +echo "Hooks activated."