draw_entities path-1 reads an optional sprite_scale (number, default 1.0) and applies it as uniform scaleX/scaleY in draw_sprite_transform around the visual center. Lets templates whose native sprite resolution (e.g. TC_Basics 300×300 bed) doesn't match the project tile-scale (~/sporel_tile_scale.md: 64 px = 0.5 m via the puppet shoulder anchor) render at credible real-world sizes without re-baking the atlas. Consumers compute their per-atlas scale from atlas_meta.pixels_per_meter (atlas-baker v0.4.0+ output) divided into their project canonical px/m — one line in M.init, no per-entity constants in the template. Unset sprite_scale preserves v0.2.0 behavior — spine-prototype, map-editor, vagrant's pre-TC_Basics items unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
177 lines
7.3 KiB
Lua
177 lines
7.3 KiB
Lua
-- =====================================================================
|
||
-- lib-core.render v0.3.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.3.0 additions:
|
||
-- - draw_entities path (1) reads optional `sprite_scale` (number,
|
||
-- default 1.0); applied uniformly as scaleX/scaleY in
|
||
-- draw_sprite_transform. Lets templates whose native sprite size
|
||
-- doesn't match the project tile-scale (e.g. TC_Basics 600×300
|
||
-- bed at 64 px = 0.5 m) scale down without per-entity baker
|
||
-- re-runs or render-time hacks. Default 1.0 preserves v0.2.0
|
||
-- behaviour for unset consumers.
|
||
--
|
||
-- 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)
|
||
-- v0.3.0: optional sprite_scale; default 1.0 keeps
|
||
-- v0.2.0 behaviour for entities that don't set it.
|
||
local s = get_num(e, "sprite_scale") or 1.0
|
||
-- position is visual-center: origin = uv center.
|
||
-- Origin stays in source-uv coords (pre-scale), so
|
||
-- draw_sprite_transform applies scale around it.
|
||
engine.render.draw_sprite_transform(
|
||
tex, px, py, 0, s, s, 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
|