From 9dd7f7c070ebe43e2a8280f6f086bb5a09c4dffd Mon Sep 17 00:00:00 2001 From: Axel Meyer Date: Sun, 10 May 2026 00:39:01 +0200 Subject: [PATCH] feat: P.0.lib.player_control movement + AABB-wall-collision --- .gitignore | 2 + README.md | 60 +++++++++++++++++++++++++++++ init.lua | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++ manifest.lib | 1 + 4 files changed, 170 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 init.lua create mode 100644 manifest.lib diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..41ae811 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build/ +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..3f34c2b --- /dev/null +++ b/README.md @@ -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 +``` diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..85dcef6 --- /dev/null +++ b/init.lua @@ -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 diff --git a/manifest.lib b/manifest.lib new file mode 100644 index 0000000..b053e1d --- /dev/null +++ b/manifest.lib @@ -0,0 +1 @@ +{"id":"lib-core.player_control","version":"0.1.0","api_min":"0.1"}