feat(0.2.0): BREAKING — Actor-Entity statt Lua-locals (Phase A.6)

Refactor: position / size / speed leben jetzt auf einer Actor-Entity
(lib-core.actor + lib-core.composition); player_control hält nur noch
die Action-Bindings und das per-frame Update.

Removed surface (use actor.* / composition.* instead):
  M.set_position / M.position / M.set_size / M.size /
  M.set_speed / M.speed

Neuer Flow:
  composition.define_template{id="player", properties={position,
      movement_speed, sprite_w, sprite_h, ...}, ...}
  local p = actor.create{template="player", properties={...}}
  player_control.bind_movement(...)
  player_control.update(p, dt)  -- liest position/movement_speed/sprite_w/h
                                -- aus dem actor; AABB-Collision um center,
                                -- commit via actor.move oder no-op

position.x/.y wird als visual CENTER behandelt (Phase-A Konvention).
4-corner-AABB-Check verschiebt sich entsprechend.

DEPRECATED-MVP-Marker auf :99 entfernt (actor-integration realisiert).

Bumps: 0.1.0 → 0.2.0 (BREAKING, pre-1.0 minor); +actor v0.1.0 dep.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Axel Meyer
2026-06-09 14:12:56 +00:00
parent 42905a45ce
commit b8e745e452
3 changed files with 152 additions and 134 deletions

155
README.md
View File

@@ -1,11 +1,14 @@
# lib-core.player_control
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).
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.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
**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
@@ -17,50 +20,30 @@ graph LR
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
```
<!-- topology:end -->
## 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.set_position(x, y)`
**Syntax:** `player.set_position(x: number, y: number) -> void`
### `player_control.bind_movement(left, right, up, down)`
**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`
**Syntax:** `player_control.bind_movement(left: string, right: string, up: string, down: string) -> void`
**Example:**
```lua
@@ -68,70 +51,110 @@ 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")
player_control.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.
**Description:** Registers 4 input-action names for WASD-style
direction. Until called, `update(actor, dt)` is a silent no-op (debug-
friendly).
### `player.update(dt)`
**Syntax:** `player.update(dt: number) -> void`
### `player_control.update(actor_handle, dt)`
**Syntax:** `player_control.update(actor_handle, dt: number) -> void`
**Example:**
```lua
function update(ctx, dt)
player.update(dt)
player_control.update(state.player, 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.
**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 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 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).
- **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 = require("lib-core.player_control")
local input = require("lib-core.input")
local camera = require("lib-core.camera")
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" })
player.set_position(244, 244)
player.bind_movement("move_left", "move_right", "move_up", "move_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.update(dt)
local p = player.position()
local s = player.size()
camera.set_target(p.x + s.w/2, p.y + s.h/2)
player_control.update(p, dt)
camera.set_target(actor.position(p))
end
function render(ctx)
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))
-- 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.
- 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)