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 3f34c2b..4509eaf 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,111 @@ # lib-core.player_control -P.0 player movement lib. Single player with top-left position + size + -speed. Reads input direction via `lib-core.input`, applies 4-corner-AABB -collision against `lib-core.maps.is_walkable`, all-or-nothing block -(no wall-sliding). +Single-player movement lib. Top-left position + size + speed, reads input-direction via `lib-core.input`, applies 4-corner-AABB collision against `lib-core.maps.is_walkable`. All-or-nothing block (no wall-sliding in v0.1.0). -- Lib-ID: `lib-core.player_control` -- Version: `0.1.0` -- Spec: `meta/docs/superpowers/specs/2026-05-09-p0-lib-player_control-design.md` +**Version:** 0.1.0 +**Lib-ID:** lib-core.player_control +**Requires:** lib-core.maps v>=0.1.1, lib-core.input v>=0.4.0 +**Tags:** player, movement, aabb, collision -Forward-compat stubs (DEPRECATED-MVP) for wall-sliding, actor-integration, -sprite-rendering, click-to-move, drag-select, animation, velocity/momentum, -walls-collision, multi-player. +## Topology + + +```mermaid +graph LR + this["lib-core.player_control"] + lib_core_maps["lib-core.maps"] + this --> lib_core_maps + lib_core_input["lib-core.input"] + this --> lib_core_input + engine["engine.*"] + this --> engine +``` + ## API -- `player.set_position(x, y)` — top-left, pixel-coords -- `player.position() → {x, y}` — top-left -- `player.set_size(w, h)` — bounding box (default 24x24) -- `player.size() → {w, h}` -- `player.set_speed(px_per_sec)` — default 200 -- `player.speed() → number` -- `player.bind_movement(left_action, right_action, up_action, down_action)` -- `player.update(dt)` — reads bound actions, predicts next pos, AABB-collision-check, mutates position + +### `player.set_position(x, y)` +**Syntax:** `player.set_position(x: number, y: number) -> void` + +**Example:** +```lua +player.set_position(244, 244) +``` + +**Description:** Sets the player's top-left position in pixel-coords. Lua `error(...)` on non-number args. + +### `player.position()` +**Syntax:** `player.position() -> {x: number, y: number}` + +**Description:** Returns the current top-left position. Compute center as `p.x + size.w/2`, `p.y + size.h/2`. + +### `player.set_size(w, h)` +**Syntax:** `player.set_size(w: number, h: number) -> void` + +**Description:** Sets the AABB bounding-box size in pixels. Default 24x24. + +### `player.size()` +**Syntax:** `player.size() -> {w: number, h: number}` + +**Description:** Returns current bounding-box size. + +### `player.set_speed(s)` +**Syntax:** `player.set_speed(s: number) -> void` + +**Description:** Sets move-speed in px/sec. Default 200. + +### `player.speed()` +**Syntax:** `player.speed() -> number` + +**Description:** Returns current speed. + +### `player.bind_movement(left, right, up, down)` +**Syntax:** `player.bind_movement(left: string, right: string, up: string, down: string) -> void` + +**Example:** +```lua +input.bind("move_left", { "a", "left" }) +input.bind("move_right", { "d", "right" }) +input.bind("move_up", { "w", "up" }) +input.bind("move_down", { "s", "down" }) +player.bind_movement("move_left", "move_right", "move_up", "move_down") +``` + +**Description:** Registers 4 input-action names for the player's WASD-style direction. Until called, `update(dt)` is a silent no-op. + +### `player.update(dt)` +**Syntax:** `player.update(dt: number) -> void` + +**Example:** +```lua +function update(ctx, dt) + player.update(dt) +end +``` + +**Description:** Reads bound actions, computes direction-vector via `lib-core.input.direction`, predicts next position, runs 4-corner-AABB walkability check against the current map, and mutates position only if all corners walkable (all-or-nothing block; no wall-sliding in v0.1.0). Silent no-op until `bind_movement` is called. ## Conventions -- Position is top-left corner of player AABB (matches engine.render.draw_rect). + +- Position is top-left of the AABB (matches `engine.render.draw_rect` origin). - Center calculation lives in caller: `cx = position.x + size.w/2`. -- All-or-nothing block on collision (no wall-sliding in P.0). -- Module owns rendering (lib has no draw fn). Module composes camera-follow. +- All-or-nothing block on collision (no wall-sliding in v0.1.0). +- Module owns rendering (lib has no `draw` fn). Module composes camera-follow. - Silent no-op for `update()` before `bind_movement` (debug-friendly). - Lua `error(...)` for setter misuse (caller-bug fast-fail). +- Y-down-positive per ADR-0031. ## Consumer pattern + ```lua local player = require("lib-core.player_control") local input = require("lib-core.input") +local camera = require("lib-core.camera") -input.bind("move_left", { engine.input.KEY_A, engine.input.KEY_LEFT }) -input.bind("move_right", { engine.input.KEY_D, engine.input.KEY_RIGHT }) -input.bind("move_up", { engine.input.KEY_W, engine.input.KEY_UP }) -input.bind("move_down", { engine.input.KEY_S, engine.input.KEY_DOWN }) +input.bind("move_left", { "a", "left" }) +input.bind("move_right", { "d", "right" }) +input.bind("move_up", { "w", "up" }) +input.bind("move_down", { "s", "down" }) player.set_position(244, 244) player.bind_movement("move_left", "move_right", "move_up", "move_down") @@ -48,13 +114,24 @@ function update(ctx, dt) player.update(dt) local p = player.position() local s = player.size() - camera.set_target(p.x + s.w/2, p.y + s.h/2) -- camera follows player center + camera.set_target(p.x + s.w/2, p.y + s.h/2) end function render(ctx) - -- draw map first, then player on top local p = player.position() local s = player.size() engine.render.draw_rect(p.x, p.y, s.w, s.h, engine.render.rgb(200, 60, 80)) end ``` + +## CHANGELOG + +### v0.1.0 (P.0) +- Initial release: position/size/speed + bind_movement + per-frame update with 4-corner-AABB collision. + +## References + +- Spec v0.1.0 (P.0): `meta/docs/superpowers/specs/2026-05-09-p0-lib-player_control-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."