diff --git a/README.md b/README.md index b457f7b..3095524 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,20 @@ # 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` - 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()` diff --git a/init.lua b/init.lua index 1326eed..8249604 100644 --- a/init.lua +++ b/init.lua @@ -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" -> ".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 diff --git a/manifest.lib b/manifest.lib index d4330ea..fb2d819 100644 --- a/manifest.lib +++ b/manifest.lib @@ -1 +1 @@ -{"id":"lib-core.maps","version":"0.1.0"} +{"id":"lib-core.maps","version":"0.1.0","api_min":"0.1"}