feat: P.0.lib.maps single-tile-grid implementation (replaces trivial-stub)
This commit is contained in:
236
init.lua
236
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" -> "<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
|
||||
|
||||
Reference in New Issue
Block a user