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>
5.1 KiB
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
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:
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:
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_entitiesPhase-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
drawfn). Pair withlib-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
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. Useactor.position/actor.movement_speed/actor_handle:get_property("sprite_w")etc. updatenow takes(actor_handle, dt)instead of just(dt).- Position is now visual-CENTER (was top-left in v0.1).
- New dep:
lib-core.actorv0.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)