feat: P.0.lib.player_control movement + AABB-wall-collision
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
build/
|
||||
*.log
|
||||
60
README.md
Normal file
60
README.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# 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).
|
||||
|
||||
- Lib-ID: `lib-core.player_control`
|
||||
- Version: `0.1.0`
|
||||
- Spec: `meta/docs/superpowers/specs/2026-05-09-p0-lib-player_control-design.md`
|
||||
|
||||
Forward-compat stubs (DEPRECATED-MVP) for wall-sliding, actor-integration,
|
||||
sprite-rendering, click-to-move, drag-select, animation, velocity/momentum,
|
||||
walls-collision, multi-player.
|
||||
|
||||
## 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
|
||||
|
||||
## Conventions
|
||||
- Position is top-left corner of player AABB (matches engine.render.draw_rect).
|
||||
- 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.
|
||||
- Silent no-op for `update()` before `bind_movement` (debug-friendly).
|
||||
- Lua `error(...)` for setter misuse (caller-bug fast-fail).
|
||||
|
||||
## Consumer pattern
|
||||
```lua
|
||||
local player = require("lib-core.player_control")
|
||||
local input = require("lib-core.input")
|
||||
|
||||
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 })
|
||||
|
||||
player.set_position(244, 244)
|
||||
player.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) -- camera follows player center
|
||||
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
|
||||
```
|
||||
107
init.lua
Normal file
107
init.lua
Normal file
@@ -0,0 +1,107 @@
|
||||
-- =====================================================================
|
||||
-- lib-core.player_control — Player Movement + AABB-Wall-Collision (P.0)
|
||||
-- See: meta/docs/superpowers/specs/2026-05-09-p0-lib-player_control-design.md
|
||||
--
|
||||
-- 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.
|
||||
-- =====================================================================
|
||||
|
||||
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 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
|
||||
actions.up = up
|
||||
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)
|
||||
local ts = maps.tile_size()
|
||||
local corners = {
|
||||
{x, y},
|
||||
{x + player.w - 1, y},
|
||||
{x, y + player.h - 1},
|
||||
{x + player.w - 1, y + player.h - 1},
|
||||
}
|
||||
for _, c in ipairs(corners) do
|
||||
local tx = math.floor(c[1] / ts)
|
||||
local ty = math.floor(c[2] / ts)
|
||||
if not maps.is_walkable(tx, ty) then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function M.update(dt)
|
||||
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
|
||||
|
||||
if can_occupy(nx, ny) then
|
||||
player.x = nx
|
||||
player.y = ny
|
||||
end
|
||||
-- else: blocked, no movement (no wall-sliding in P.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
|
||||
1
manifest.lib
Normal file
1
manifest.lib
Normal file
@@ -0,0 +1 @@
|
||||
{"id":"lib-core.player_control","version":"0.1.0","api_min":"0.1"}
|
||||
Reference in New Issue
Block a user