feat: P.0.lib.maps single-tile-grid implementation (replaces trivial-stub)

This commit is contained in:
Axel Meyer
2026-05-09 18:13:59 +02:00
parent 5cf3d0c2fb
commit 94c2197d46
3 changed files with 251 additions and 3 deletions

View File

@@ -1,6 +1,20 @@
# lib-core.maps # lib-core.maps
P.0.module.skeleton trivial-stub. Will be properly implemented in P.0.lib.maps slice. P.0 single-tile-grid-map implementation. Loads JSON map + tilemap files,
provides tile-fetch + walkability queries.
- Lib-ID: `lib-core.maps` - Lib-ID: `lib-core.maps`
- Version: `0.1.0` - Version: `0.1.0`
- Spec: `meta/docs/superpowers/specs/2026-05-09-p0-lib-maps-design.md`
Forward-compat stubs (DEPRECATED-MVP) for multi-map graph, lifecycle states,
walls, sprite-fields, generators, save-integration. See
`meta/docs/architecture/map-topology.md` for the full target spec.
## API
- `maps.load(path)` — load + register a map from JSON
- `maps.create(table)` — programmatic creation (tests, procedural)
- `maps.tile_at(map_id, tx, ty)` / `maps.tile_at(tx, ty)` — current-map sugar
- `maps.is_walkable(...)`, `maps.size(...)`, `maps.tile_size(...)`
- `maps.current()`, `maps.set_current(id)`, `maps.list()`
- Stubs (deferred): `maps.state()`, `maps.pin()`

236
init.lua
View File

@@ -1 +1,235 @@
return { hello = function() return "maps loaded" end } -- =====================================================================
-- lib-core.maps — Single Tile-Grid Map Lib (P.0)
-- See: meta/docs/superpowers/specs/2026-05-09-p0-lib-maps-design.md
--
-- Forward-compat stubs (DEPRECATED-MVP) for: multi-map-graph,
-- lifecycle-states, walls, sprite-fields, generators, save-integration,
-- cross-lib tilemap resolution, override-field-merge, sub-require.
-- =====================================================================
-- Module-private state
local map_registry = {} -- map_id -> Map
local tilemap_registry = {} -- full_tilemap_id -> Tilemap
local current_map_id = nil
-- =====================================================================
-- Internal helpers
-- =====================================================================
-- Splits "lib-x.foo" -> "lib-x", "foo" (split at FIRST dot only).
-- Returns (nil, full_id) if no dot present.
local function split_namespaced_id(full_id)
local dot = string.find(full_id, ".", 1, true)
if not dot then return nil, full_id end
return string.sub(full_id, 1, dot-1), string.sub(full_id, dot+1)
end
-- "demo_tilemap" -> "<current-module-id>.demo_tilemap"
-- "lib-x.foo" -> "lib-x.foo" (cross-lib path; checked at load)
local function resolve_tilemap_id(ref)
if string.find(ref, ".", 1, true) then
return ref
end
return engine.module.id() .. "." .. ref
end
-- Schema-validation helper: reads required field with type-check.
local function require_field(t, key, expected_type, source)
local v = t[key]
if v == nil then
error(string.format("maps.load: schema violation in %s: missing required field '%s'",
source, key))
end
if type(v) ~= expected_type then
error(string.format("maps.load: schema violation in %s: field '%s' must be %s, got %s",
source, key, expected_type, type(v)))
end
return v
end
local function validate_map_table(t, source)
require_field(t, "id", "string", source)
require_field(t, "tilemap", "string", source)
local size = require_field(t, "size", "table", source)
require_field(size, "w", "number", source .. ".size")
require_field(size, "h", "number", source .. ".size")
local tiles = require_field(t, "tiles", "table", source)
local expected = size.w * size.h
if #tiles ~= expected then
error(string.format("maps.load: schema violation in %s: tiles array length %d != size.w * size.h (%d)",
source, #tiles, expected))
end
end
local function validate_tilemap_table(t, source, expected_local_name)
require_field(t, "id", "string", source)
if t.id ~= expected_local_name then
error(string.format("maps.load: tilemap manifest id '%s' mismatches filename-stem '%s' in %s",
t.id, expected_local_name, source))
end
require_field(t, "tile_size", "number", source)
local tiles = require_field(t, "tiles", "table", source)
for i, entry in ipairs(tiles) do
if type(entry) ~= "table" then
error(string.format("maps.load: tilemap %s tiles[%d] must be table", source, i))
end
require_field(entry, "id", "string", source .. ".tiles[" .. i .. "]")
require_field(entry, "walkable", "boolean", source .. ".tiles[" .. i .. "]")
end
end
local function load_tilemap(full_id)
if tilemap_registry[full_id] then
return tilemap_registry[full_id]
end
local lib_id, local_name = split_namespaced_id(full_id)
local current_lib = engine.module.id()
if lib_id ~= current_lib then
-- DEPRECATED-MVP: cross-lib tilemap resolution deferred to render-slice
error(string.format("maps.load: cross-lib tilemap resolution deferred [DEPRECATED-MVP]; tilemap '%s' not from current module '%s'",
full_id, current_lib))
end
local path = "assets/tiles/" .. local_name .. ".tilemap.json"
local raw = engine.asset.load_json(path)
validate_tilemap_table(raw, path, local_name)
local tilemap = {
id = full_id,
tile_size = raw.tile_size,
tiles = raw.tiles,
}
tilemap_registry[full_id] = tilemap
return tilemap
end
local function build_map(t_map, tilemap)
-- Verify each tile-id is in palette range
for i, tid in ipairs(t_map.tiles) do
if type(tid) ~= "number" or tid < 1 or tid > #tilemap.tiles then
error(string.format("maps.load: tile-id %s at index %d exceeds palette size %d (in map '%s')",
tostring(tid), i, #tilemap.tiles, t_map.id))
end
end
return {
id = t_map.id,
size = t_map.size,
tile_size = t_map.tile_size or tilemap.tile_size,
tiles = t_map.tiles, -- shallow-ref
tilemap = tilemap, -- shallow-ref
-- DEPRECATED-MVP: forward-compat stubs (multi-map slice fills)
walls = {},
regions = {},
edges = {},
state = "Active",
pinned = false,
}
end
-- =====================================================================
-- Public API
-- =====================================================================
local M = {}
function M.load(path)
local raw = engine.asset.load_json(path)
validate_map_table(raw, path)
local full_id = resolve_tilemap_id(raw.tilemap)
local tilemap = load_tilemap(full_id)
local map = build_map(raw, tilemap)
if map_registry[map.id] then
error(string.format("maps.load: map-id '%s' already registered", map.id))
end
map_registry[map.id] = map
return map.id
end
-- Programmatic creation (tests, future procedural-map generators).
-- Caller must provide a fully-built tilemap-table (not a path/id ref).
function M.create(t)
if type(t.tilemap_table) ~= "table" then
error("maps.create: tilemap_table required (use maps.load for JSON path)")
end
local map = build_map(t, t.tilemap_table)
if map_registry[map.id] then
error(string.format("maps.create: map-id '%s' already registered", map.id))
end
map_registry[map.id] = map
return map.id
end
function M.size(map_id)
local id = map_id or current_map_id
if not id then error("maps.size: no current map") end
return map_registry[id].size
end
function M.tile_size(map_id)
local id = map_id or current_map_id
if not id then error("maps.tile_size: no current map") end
return map_registry[id].tile_size
end
-- Arity-flex sugar: tile_at(tx, ty) uses current_map_id; tile_at(map_id, tx, ty) is explicit.
function M.tile_at(a, b, c)
local map_id, tx, ty
if c == nil then
map_id, tx, ty = current_map_id, a, b
if not map_id then
error("maps.tile_at: no current map; call set_current() first or pass map_id")
end
else
map_id, tx, ty = a, b, c
end
local m = map_registry[map_id]
if not m then
error(string.format("maps.tile_at: unknown map-id '%s'", tostring(map_id)))
end
if tx < 0 or tx >= m.size.w or ty < 0 or ty >= m.size.h then
return nil
end
local palette_id = m.tiles[ty * m.size.w + tx + 1]
return m.tilemap.tiles[palette_id]
end
function M.is_walkable(a, b, c)
local t = M.tile_at(a, b, c)
if t == nil then return false end
return t.walkable == true
end
function M.tilemap_id(map_id)
local id = map_id or current_map_id
return map_registry[id].tilemap.id
end
function M.current()
return current_map_id
end
function M.set_current(map_id)
if not map_registry[map_id] then
error(string.format("maps.set_current: unknown map-id '%s'", tostring(map_id)))
end
current_map_id = map_id
end
function M.list()
local out = {}
for id, _ in pairs(map_registry) do
out[#out+1] = id
end
return out
end
-- Forward-compat stubs (DEPRECATED-MVP — implemented in later slices)
function M.state(map_id)
-- DEPRECATED-MVP: lifecycle states (Virgin/Inert/Passive/Active/Pinned) — multi-map slice
return "Active"
end
function M.pin(map_id, reason)
-- DEPRECATED-MVP: world.pin_map mechanic — multi-map slice
engine.warn("maps.pin: deferred to map-topology lifecycle slice")
end
return M

View File

@@ -1 +1 @@
{"id":"lib-core.maps","version":"0.1.0"} {"id":"lib-core.maps","version":"0.1.0","api_min":"0.1"}