-- ===================================================================== -- lib-core.render v0.2.0 — Tile-Grid + Entity Rendering -- See: meta/docs/superpowers/specs/2026-05-09-p0-lib-render-design.md -- meta/docs/superpowers/specs/2026-06-09-phase-A-... -- -- v0.2.0 additions: -- - draw_entities(filter) — tag-based entity-render via composition. -- Supports two sprite-property paths and a fallback: -- (1) sprite_atlas + sprite_uv.{x,y,w,h} → atlas-sub-rect render -- via engine.render.draw_sprite_transform -- (2) sprite_color + sprite_w + sprite_h → colored rect -- via engine.render.draw_rect -- (3) none of above → magenta fallback rect (debug-visible) -- `position.x` and `position.y` are visual CENTER for all modes. -- Texture-handles for sprite_atlas paths are cached internally. -- -- v0.1.0 API unverändert: draw_map. -- ===================================================================== local maps = require("lib-core.maps") local composition = require("lib-core.composition") local FALLBACK_COLOR = {255, 0, 255} -- magenta — "missing color" debug-marker -- Texture-handle cache for sprite_atlas paths. lazy-loaded on first -- draw_entities call that references the path. Lives for the engine -- process; no eviction in v0.2.0. local atlas_cache = {} -- Fallback-rect size (px) for entities without any sprite-property. -- Magenta is the debug-marker; size is small enough to be intrusive -- without dominating the screen. local FALLBACK_RECT_SIZE = 16 local function load_atlas(path) local h = atlas_cache[path] if h ~= nil then return h end h = engine.asset.load_texture(path) atlas_cache[path] = h return h end -- Reads tile-color from TileType-table; fallback to magenta if missing or malformed. local function tile_color(tile_type) local c = tile_type.color if type(c) ~= "table" or #c < 3 then return FALLBACK_COLOR end return c end local M = {} -- Iterates current (or specified) map's tiles, draws a colored rect per tile. -- Must be called from inside a render hook (engine.render.* enforces this). -- Returns the number of tiles drawn (for smoke verification + debug). function M.draw_map(map_id) local size = maps.size(map_id) local tile_size = maps.tile_size(map_id) local count = 0 for ty = 0, size.h - 1 do for tx = 0, size.w - 1 do local t = maps.tile_at(map_id, tx, ty) if t ~= nil then local c = tile_color(t) local color = engine.render.rgb(c[1], c[2], c[3]) engine.render.draw_rect( tx * tile_size, ty * tile_size, tile_size, tile_size, color) count = count + 1 end end end return count end -- --------------------------------------------------------------------- -- v0.2.0 — Entity Rendering -- --------------------------------------------------------------------- -- Try to read an entity-property as a number; returns nil if absent -- (instead of the engine's typed default), so callers can branch on -- presence without conflating with default-zero. -- -- engine.entity:get_property currently returns nil only if the property -- isn't declared at all; declared-with-default-value yields the default. -- v0.1 composition declares whatever's in the template — any property -- not declared is genuinely absent and yields the engine fallback. local function get_num(entity, key) local v = entity:get_property(key) if type(v) == "number" then return v end return nil end local function get_str(entity, key) local v = entity:get_property(key) if type(v) == "string" and v ~= "" then return v end return nil end -- Draws all entities tagged with `filter.tag`. -- Filter shape: { tag = "" } — anything else is loud-error. -- Returns the number of entities drawn (for smoke verification). function M.draw_entities(filter) if type(filter) ~= "table" then error("render.draw_entities: filter must be a table with field 'tag'") end if type(filter.tag) ~= "string" or filter.tag == "" then error("render.draw_entities: filter.tag must be a non-empty string " .. "(other filter-shapes deferred — see Phase-A Spec A-Q3)") end local list = composition.list_by_tag(filter.tag) local count = 0 for _, e in ipairs(list) do local px = get_num(e, "position.x") local py = get_num(e, "position.y") if px ~= nil and py ~= nil then local atlas = get_str(e, "sprite_atlas") if atlas ~= nil then -- Path (1): atlas sub-rect. local uvx = get_num(e, "sprite_uv.x") local uvy = get_num(e, "sprite_uv.y") local uvw = get_num(e, "sprite_uv.w") local uvh = get_num(e, "sprite_uv.h") if uvx and uvy and uvw and uvh then local tex = load_atlas(atlas) -- position is visual-center: origin = uv center engine.render.draw_sprite_transform( tex, px, py, 0, 1, 1, uvw / 2, uvh / 2, 0xFFFFFFFF, uvx, uvy, uvw, uvh) count = count + 1 end else local sc = e:get_property("sprite_color") local sw = get_num(e, "sprite_w") local sh = get_num(e, "sprite_h") if type(sc) == "number" and sw and sh then -- Path (2): colored rect, position = visual-center engine.render.draw_rect( px - sw / 2, py - sh / 2, sw, sh, sc) count = count + 1 else -- Path (3): magenta fallback local fbcol = engine.render.rgb( FALLBACK_COLOR[1], FALLBACK_COLOR[2], FALLBACK_COLOR[3]) local s = FALLBACK_RECT_SIZE engine.render.draw_rect(px - s / 2, py - s / 2, s, s, fbcol) count = count + 1 end end end end return count end -- Forward-compat stubs for later slices (kept as comments to avoid table-pollution): -- DEPRECATED-MVP: function M.set_camera(camera) -- lib-core.camera slice -- DEPRECATED-MVP: function M.push_layer(name, order) -- multi-layer slice -- DEPRECATED-MVP: function M.register_pass(...) -- post-processing slice (spec §6) -- DEPRECATED-MVP: function M.set_clear_color(rgb) -- per-layer clear-color (spec §4) return M