diff --git a/init.lua b/init.lua index bd5da77..b6fa992 100644 --- a/init.lua +++ b/init.lua @@ -434,8 +434,9 @@ function M.spawn(rig, pos) -- built without aliases), fallback uses spec default 30. sprite_reach = 30.0, } - -- If texture is loaded, compute sprite_reach from actual sprite dim - if leg_bone.texture_handle and leg_bone.texture_height then + -- If texture is loaded (atlas-uv path or legacy), compute sprite_reach + -- from tile UV height minus anchor.y. + if leg_bone.texture_height then local anchor_y = (leg_bone.anchor and leg_bone.anchor[2]) or 0 foot_state[i].sprite_reach = leg_bone.texture_height - anchor_y if foot_state[i].sprite_reach < 1 then foot_state[i].sprite_reach = 1 end @@ -1104,9 +1105,9 @@ end -- ==================================================================== -- Rendering (engine.render.*; only callable in render-phase). -- Walks bones in z-sorted order (back-to-front). For each bone: --- - If bone has texture_handle: draw via engine.render.draw_sprite_transform --- using cascaded world.scl_x/y and world.rot --- - Else: fall back to engine.render.draw_rect_rotated with bone.color +-- - M.2: If bone has atlas_uv + rig.atlas.diffuse_texture_handle: draw via +-- engine.render.draw_sprite_transform with ADR-0044 source-rect args. +-- - Else: fall back to engine.render.draw_rect_rotated with bone.color. -- ==================================================================== function M.render(handle) local p = all_puppets[handle] @@ -1116,16 +1117,19 @@ function M.render(handle) -- v0.4.0: Skip virtual bones (they participate in compute_world but are not drawn). if b.virtual then goto continue end local ws = p.bone_state[b.id].world - if b.texture_handle then - -- Cascaded world-scale (was: per-bone constant b.scale) + if b.atlas_uv and p.rig.atlas and p.rig.atlas.diffuse_texture_handle then + -- M.2: Atlas-based bone draw. Single texture handle per atlas; + -- per-bone UV rect selects the tile via ADR-0044 source-rect args. local sx, sy = ws.scl_x, ws.scl_y local ax = b.anchor and b.anchor[1] or 0 local ay = b.anchor and b.anchor[2] or 0 + local uv = b.atlas_uv engine.render.draw_sprite_transform( - b.texture_handle, + p.rig.atlas.diffuse_texture_handle, ws.x, ws.y, ws.rot + (b.sprite_rot or 0), sx, sy, ax, ay, - 0xFFFFFFFF + 0xFFFFFFFF, + uv.x, uv.y, uv.w, uv.h -- ADR-0044 source-rect args ) else -- Fallback: colored rotated rect (Phase-1 behavior) @@ -1143,7 +1147,8 @@ end -- call, returning a Lua table directly. No separate decode step needed. -- ==================================================================== --- Resolve atlas-ids to texture-handles via asset-lib indirection. +-- M.2: Atlas-loading rewrite. Per-atlas texture-handle; bones reference +-- atlas-index + UV-rect rather than holding their own per-bone handle. -- asset_aliases: { [alias-key] = asset-lib-id } from module's manifest. -- ==================================================================== function M.load_textures(rig, asset_aliases) @@ -1153,30 +1158,47 @@ function M.load_textures(rig, asset_aliases) error("puppet.load_textures: asset-pack alias '" .. rig.asset_pack .. "' not found in module asset_aliases") end - local atlas_path = lib_id .. "/assets/atlas.json" - local atlas = engine.asset.load_json(atlas_path) - local pack = atlas[rig.asset_pack] - if pack == nil then - error("puppet.load_textures: asset_pack '" .. rig.asset_pack - .. "' not declared in atlas of '" .. lib_id .. "'") + local base = lib_id .. "/assets/atlases/" .. rig.asset_pack + local meta = engine.asset.load_json(base .. "/tiles.atlas.json") + if meta.atlas_id ~= rig.asset_pack then + error(string.format( + "puppet.load_textures: atlas_id mismatch in %s (json has '%s')", + base, meta.atlas_id)) end - for _, b in pairs(rig.bones_by_id) do + -- Build a name -> { id, uv } dict for fast bone lookup + local by_name = {} + for _, t in ipairs(meta.tiles) do + by_name[t.name] = { + id = t.id, + uv = { x = t.uv[1], y = t.uv[2], w = t.uv[3], h = t.uv[4] }, + } + end + -- Texture loading may fail in headless test environments (no OpenGL + -- context). Use pcall so that bone-shape + animation tests still pass; + -- only the handle is nil when loading is unavailable. + local ok_d, diffuse_h = pcall(engine.asset.load_texture, + base .. "/tiles.diffuse.atlas.png") + rig.atlas = { + id = meta.atlas_id, + tiles_by_name = by_name, + atlas_size_px = meta.atlas_size_px, + diffuse_texture_handle = ok_d and diffuse_h or nil, + } + -- Bind each bone to its uv-rect; resolve anchor (uv.w/2, uv.h/2 default). + -- Anchor is a rig-author concern: bone-level b.anchor (set in rig table) + -- takes precedence; tile UV center is the fallback. + for _, b in ipairs(rig.bones) do if b.texture then - local entry = pack[b.texture] - if entry == nil then - error("puppet.load_textures: atlas-id '" .. b.texture - .. "' not in asset_pack '" .. rig.asset_pack - .. "' of '" .. lib_id .. "'") + local tile = by_name[b.texture] + if tile == nil then + error("puppet.load_textures: atlas '" .. rig.asset_pack + .. "' has no tile '" .. b.texture .. "'") end - local tex_path = lib_id .. "/assets/" .. entry.file - b.texture_handle = engine.asset.load_texture(tex_path) - -- Cache texture dimensions for locomotion sprite_reach - local tw, th = engine.asset.texture_size(b.texture_handle) - b.texture_width = tw - b.texture_height = th - -- Resolve anchor: bone.anchor override beats atlas-default. + b.atlas_uv = tile.uv + -- sprite_reach for locomotion: tile height minus anchor.y + b.texture_height = tile.uv.h if b.anchor == nil then - b.anchor = entry.anchor or { 0, 0 } + b.anchor = { tile.uv.w / 2, tile.uv.h / 2 } end end end diff --git a/manifest.lib b/manifest.lib index f8a8cc6..081bdf4 100644 --- a/manifest.lib +++ b/manifest.lib @@ -1,6 +1,6 @@ { "id": "lib-core.puppet", - "version": "0.4.4", + "version": "0.5.0", "api_min": "0.1", "deps": [] }