Files
sporel-lib-core.render/init.lua
Axel Meyer e4f9d42170 feat(v0.2.0): draw_entities tag-driven entity-render
Phase A.2: draw_entities(filter) iterates composition.list_by_tag and
renders each entity per its sprite-properties. Three paths:

1. sprite_atlas + sprite_uv.{x,y,w,h} → atlas sub-rect via
   engine.render.draw_sprite_transform (with texture-handle cache)
2. sprite_color + sprite_w + sprite_h → engine.render.draw_rect
3. magenta 16x16 fallback rect for entities without sprite-properties

position.x/position.y is treated as visual-CENTER for all paths.

Filter shape: {tag = "..."} — anything else is loud-error
(template-id / property-predicate filters deferred per A-Q3).

New dep: lib-core.composition v0.1.0.
draw_map (v0.1) unverändert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 13:39:58 +00:00

163 lines
6.5 KiB
Lua

-- =====================================================================
-- 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 = "<string>" } — 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