# lib-core.player_control Input-driven movement for a single Actor-Entity. Reads input-direction via `lib-core.input`, applies 4-corner-AABB collision against `lib-core.maps.is_walkable` around the actor's visual center, then commits via `actor.move`. All-or-nothing block (no wall-sliding). **Version:** 0.2.0 **Lib-ID:** lib-core.player_control **Requires:** lib-core.maps v>=0.5.7, lib-core.input v>=0.4.0, lib-core.actor v>=0.1.0 **Tags:** player, movement, aabb, collision, actor ## 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 lib_core_actor["lib-core.actor"] this --> lib_core_actor engine["engine.*"] this --> engine ``` ## Scope (v0.2.0 — Phase A.6 refactor) **Breaking change vs v0.1.0:** position / size / speed are no longer stored in player_control; they live on the actor-entity (read via `actor.position` / `actor.movement_speed` / entity properties). The lib retains only the per-frame update plus the action-bindings. This decouples player-control from any single-player assumption: each call passes its own actor-handle. Multi-actor controllers (e.g. squad- demos, RTS-style click-to-move) compose by holding multiple actor-handles externally. ## API ### `player_control.bind_movement(left, right, up, down)` **Syntax:** `player_control.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_control.bind_movement("move_left", "move_right", "move_up", "move_down") ``` **Description:** Registers 4 input-action names for WASD-style direction. Until called, `update(actor, dt)` is a silent no-op (debug- friendly). ### `player_control.update(actor_handle, dt)` **Syntax:** `player_control.update(actor_handle, dt: number) -> void` **Example:** ```lua function update(ctx, dt) player_control.update(state.player, dt) end ``` **Description:** Reads bound actions; computes direction-vector via `lib-core.input.direction`; reads the actor's `position` (center), `movement_speed`, and `sprite_w`/`sprite_h` properties; predicts a new center position; runs 4-corner-AABB walkability check around the new center; commits via `actor.move(handle, ncx-cx, ncy-cy)` on pass, no-op on blocked (all-or-nothing). Loud-Error if `actor_handle` is nil. Silent no-op before `bind_movement` is called. ## Conventions - **Position is visual CENTER** of the actor (matches `render.draw_entities` Phase-A convention). - **AABB-Collision around the center**: the 4 corners are `(cx ± w/2, cy ± h/2)`. - All-or-nothing block on collision (wall-sliding deferred). - Caller owns rendering (lib has no `draw` fn). Pair with `lib-core.render.draw_entities{tag = "renderable"}`. - Module composes camera-follow via `camera.set_target(actor.position(handle))`. - Y-down-positive per ADR-0031. ## Consumer pattern ```lua local player_control = require("lib-core.player_control") local actor = require("lib-core.actor") local composition = require("lib-core.composition") local input = require("lib-core.input") local camera = require("lib-core.camera") composition.define_template{ id = "player", properties = { position = {x = 0, y = 0}, movement_speed = 200, sprite_color = engine.render.rgb(200, 60, 80), sprite_w = 24, sprite_h = 24, }, tags = {"renderable"}, } input.bind("move_left", { "a", "left" }) input.bind("move_right", { "d", "right" }) input.bind("move_up", { "w", "up" }) input.bind("move_down", { "s", "down" }) local p = actor.create{ template = "player", properties = { position = {x = 244, y = 244}, movement_speed = 200, }, } player_control.bind_movement("move_left", "move_right", "move_up", "move_down") function update(ctx, dt) player_control.update(p, dt) camera.set_target(actor.position(p)) end function render(ctx) -- Player gets drawn together with all renderable entities. render.draw_entities{ tag = "renderable" } end ``` ## CHANGELOG ### v0.2.0 (Phase A.6 — 2026-06-09) - BREAKING: removed `set_position` / `position` / `set_size` / `size` / `set_speed` / `speed`. Use `actor.position` / `actor.movement_speed` / `actor_handle:get_property("sprite_w")` etc. - `update` now takes `(actor_handle, dt)` instead of just `(dt)`. - Position is now visual-CENTER (was top-left in v0.1). - New dep: `lib-core.actor` v0.1.0. ### 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` - Spec v0.2.0 (Phase A): `meta/docs/superpowers/specs/2026-06-09-phase-A-inactive-entities-composition-actor-reentry-design.md` - ADR-0001 (engine knows verbs, libs bring nouns) - ADR-0031 (pixel-convention: Y-down-positive) - ADR-0038 (API-Doc-Convention)