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

129
init.lua
View File

@@ -1,59 +1,46 @@
-- =====================================================================
-- lib-core.player_control — Player Movement + AABB-Wall-Collision (P.0)
-- lib-core.player_control v0.2.0 — Input → Actor Movement + AABB-Collision
-- See: meta/docs/superpowers/specs/2026-05-09-p0-lib-player_control-design.md
-- meta/docs/superpowers/specs/2026-06-09-phase-A-...
--
-- Scope: 1 player, top-left position + size + speed, 4-corner-AABB
-- collision against maps.is_walkable, action-bindings via lib-core.input.
-- DEPRECATED-MVP for: wall-sliding, actor-integration, sprite-rendering,
-- click-to-move, drag-select, animation, velocity/momentum, multi-player.
-- v0.2.0 (Phase A.6) — BREAKING 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.
--
-- Caller sets up:
-- composition.define_template{id="player", properties={position,
-- movement_speed, sprite_w, sprite_h, sprite_color, ...}, ...}
-- local p = actor.create{template="player", properties={...}}
-- player_control.bind_movement("move_left", ...)
-- player_control.update(p, dt) -- jeden Frame
--
-- position.x/.y on the actor is treated as visual CENTER (per Phase-A
-- render-convention). The collision-check uses sprite_w/sprite_h
-- read from the actor's properties.
--
-- Removed in v0.2.0 (use actor.* / composition.* instead):
-- M.set_position / M.position / M.set_size / M.size /
-- M.set_speed / M.speed — alle obsolete; lese via
-- actor.position(handle) / handle:get_property("sprite_w") /
-- actor.movement_speed(handle).
--
-- DEPRECATED-MVP (unverändert):
-- - separate-axis wall-sliding (fluidity slice)
-- - sprite-rendering (lebt jetzt in render.draw_entities)
-- - click-to-move + drag-select (P.3 RTS slice)
-- - velocity/momentum (juice slice)
-- - collision against maps.walls field (when walls populated)
-- =====================================================================
local maps = require("lib-core.maps")
local input = require("lib-core.input")
local player = {
x = 0, y = 0, -- top-left in pixel-coords
w = 24, h = 24,
speed = 200, -- px/sec
}
local actor = require("lib-core.actor")
local actions = { left = nil, right = nil, up = nil, down = nil }
local M = {}
function M.set_position(x, y)
if type(x) ~= "number" or type(y) ~= "number" then
error("player_control.set_position: x and y must be numbers")
end
player.x = x; player.y = y
end
function M.position()
return { x = player.x, y = player.y }
end
function M.set_size(w, h)
if type(w) ~= "number" or type(h) ~= "number" or w <= 0 or h <= 0 then
error("player_control.set_size: w and h must be positive numbers")
end
player.w = w; player.h = h
end
function M.size()
return { w = player.w, h = player.h }
end
function M.set_speed(s)
if type(s) ~= "number" or s < 0 then
error("player_control.set_speed: speed must be a non-negative number")
end
player.speed = s
end
function M.speed()
return player.speed
end
function M.bind_movement(left, right, up, down)
actions.left = left
actions.right = right
@@ -61,15 +48,18 @@ function M.bind_movement(left, right, up, down)
actions.down = down
end
-- 4-corner-AABB-check: returns true iff all 4 corners of (x, y, w, h)
-- sit on walkable tiles per maps.is_walkable.
local function can_occupy(x, y)
-- 4-corner-AABB-check: returns true iff all 4 corners of the rect
-- centered at (cx, cy) with size (w, h) sit on walkable tiles per
-- maps.is_walkable. cx, cy is the visual center (Phase-A convention).
local function can_occupy_centered(cx, cy, w, h)
local ts = maps.tile_size()
local x0 = cx - w / 2
local y0 = cy - h / 2
local corners = {
{x, y},
{x + player.w - 1, y},
{x, y + player.h - 1},
{x + player.w - 1, y + player.h - 1},
{x0, y0},
{x0 + w - 1, y0},
{x0, y0 + h - 1},
{x0 + w - 1, y0 + h - 1},
}
for _, c in ipairs(corners) do
local tx, ty = engine.spatial.pixel_to_tile(c[1], c[2], ts)
@@ -78,29 +68,34 @@ local function can_occupy(x, y)
return true
end
function M.update(dt)
-- Per-frame input-driven movement. Reads movement-speed + sprite-size
-- from the actor's own properties; applies 4-corner collision check
-- against the current map; commits via actor.move on pass, no-op on
-- blocked (no wall-sliding in v0.2.0).
--
-- Caller is expected to have invoked actor.create + bind_movement
-- before the first update.
function M.update(actor_handle, dt)
if actor_handle == nil then
error("player_control.update: actor_handle must not be nil")
end
if not actions.left then return end -- not bound, silent no-op
local d = input.direction(actions.left, actions.right, actions.up, actions.down)
if d.x == 0 and d.y == 0 then return end
local nx = player.x + d.x * player.speed * dt
local ny = player.y + d.y * player.speed * dt
local cx, cy = actor.position(actor_handle)
local speed = actor.movement_speed(actor_handle)
local w = actor_handle:get_property("sprite_w")
local h = actor_handle:get_property("sprite_h")
if can_occupy(nx, ny) then
player.x = nx
player.y = ny
local ncx = cx + d.x * speed * dt
local ncy = cy + d.y * speed * dt
if can_occupy_centered(ncx, ncy, w, h) then
actor.move(actor_handle, ncx - cx, ncy - cy)
end
-- else: blocked, no movement (no wall-sliding in P.0)
-- else: blocked, no movement (no wall-sliding in P.0/v0.2.0)
end
-- DEPRECATED-MVP: separate-axis wall-sliding -- fluidity slice
-- DEPRECATED-MVP: actor-integration (player as Actor) -- lib-core.actor slice
-- DEPRECATED-MVP: sprite-rendering -- texture-render slice
-- DEPRECATED-MVP: click-to-move + drag-select -- P.3 RTS slice
-- DEPRECATED-MVP: animation-state-machine -- animation slice
-- DEPRECATED-MVP: velocity/momentum -- juice slice
-- DEPRECATED-MVP: collision against maps.walls field -- when walls populated
-- DEPRECATED-MVP: multi-player (multi-instance) -- multiplayer slice
return M