feat(vagrant): v0.14.0 — TC_Basics first-consumer + UI item icons

ADR-0054 Item-Visual-Identity validated end-to-end.

World-sprite migration off the proprietary Subterrain world-atlas:
  - Bed + Bench promoted to composition-entities with renderable tag;
    rendered via the existing draw_entities path (the hardcoded
    draw_sprite_transform calls for furniture are gone).
  - Backpack sprite source: Subterrain backpack_001 → TC_Basics sack1.
  - Stick visual: blob_rect_stone slot_04_straight → TC_Basics log_pile1.
  - rock_pick crafted-output visual: blob_rect_stone slot_05_tee_open →
    TC_Basics spade.
  - rock + rock_pile + workbench stay on blob_rect_stone (no TC_Basics
    equivalent yet).

Scale resolution is now atlas-level (no more module-side constants):
M.init reads tc_meta.atlas_meta.pixels_per_meter and divides into
PROJECT_PIXELS_PER_METER (128 per puppet shoulder anchor) to derive
tc_scale, applied per-entity via lib-core.render v0.3.0's sprite_
scale property.

UI item icons via ADR-0054 §3 Konsum:
  - Backpack + Workbench-Inventory rows now show item icons via the
    inventory-list-display v0.2.0 default resolver (fallback chain
    icon_atlas → sprite_atlas), fit-to-bounds.
  - Backpack-Crafting + Workbench-Crafting panels get an icon column
    via crafting-display v0.3.0; vagrant supplies a small recipe-
    output → {atlas, uv} lookup (craft_icons) and wires it as
    icon_resolver on both widgets.

Deps cascaded: lib-core.render 0.2.0 → 0.3.0, lib-core.inventory-
list-display 0.1.1 → 0.2.0, lib-core.crafting-display 0.2.0 → 0.3.0,
lib-asset.prototype-tc-basics 0.1.0 → 0.3.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
calic
2026-06-21 11:45:24 +02:00
parent 088e14aeb2
commit c3290058c1
3 changed files with 161 additions and 61 deletions

196
init.lua
View File

@@ -1,6 +1,6 @@
-- sporel-module-vagrant-skeleton v0.13.0
-- sporel-module-vagrant-skeleton v0.14.0
-- Sprite-mode test-chamber: 13-bone humanoid puppet + sprite-tilemap +
-- hardcoded furniture. Subterrain-style puppet control model:
-- TC_Basics furniture (bed/bench/backpack). Subterrain-style puppet control model:
-- multi-look-target (aim/soft), slerp body rotation, hip-static legs,
-- foot-body-orientation. Sprint (shift+wasd) + interaction (E near
-- pickup-item) added v0.4.4.
@@ -63,6 +63,17 @@
-- Inventory with layout="right-half"; right-click opens both panels
-- simultaneously side-by-side. Replaces the v0.12.0 single-active
-- caveat where workbench_inv covered workbench_crafting.
--
-- v0.14.0: Vagrant migrates Bed + Bench + Backpack visuals off the
-- proprietary Subterrain world-atlas onto TC_Basics sprites
-- (single_bed1, bench1, sack1) — first concrete consumer of the
-- ADR-0054 Item-Visual-Identity weiche. Bed + Bench promoted to
-- composition-templates with `renderable` tag, rendered via the
-- existing draw_entities path; no more hardcoded draw_sprite_transform
-- calls for furniture. lib-asset.prototype-subterrain dep retained
-- for the puppet sprite atlas (subterrain_player); the world-atlas
-- (subterrain_world) drop closes the last furniture dependency on
-- the Subterrain world-tiles.
local input = require("lib-core.input")
local camera = require("lib-core.camera")
@@ -89,19 +100,30 @@ local WALK_SPEED_SPRINT = 240 -- 2x
local ANIM_SPEED_NORMAL = 0.45
local ANIM_SPEED_SPRINT = 0.9 -- 2x
-- Furniture positions (Bed/Bench remain hardcoded for v0.1; Backpack
-- promoted to Inactive-Entity via composition. Bed/Bench have no
-- interaction-trigger and no demo-pflicht to be entities — they stay
-- atlas-furniture until a container-on-bed feature or similar triggers).
-- World-furniture positions (all three now composition-entities with
-- renderable tag; rendered via draw_entities). Positions are visual
-- CENTER per lib-core.render v0.2.0 anchor convention.
local BACKPACK_X, BACKPACK_Y = 350, 400
local BED_X, BED_Y = 200, 150
local BENCH_X, BENCH_Y = 400, 300
local INTERACTION_RANGE = 40 -- pixels
-- World-overlay float colors (pickup confirmation)
local COLOR_PICKUP_FLOAT = 0xFFFF80FF
-- Project canonical pixels-per-meter, anchored to the puppet shoulder
-- width (64 px = 0.5 m per ~/sporel_tile_scale.md). All sprite atlases
-- declare their own `atlas_meta.pixels_per_meter` in their sprite-mode
-- JSON output (atlas-baker v0.4.0+); the per-entity scale-factor is
-- derived as PROJECT_PIXELS_PER_METER / atlas_ppm at template-load time
-- and applied via lib-core.render v0.3.0's `sprite_scale` property.
-- Atlases authored at canonical ppm get scale 1.0 → no-op.
local PROJECT_PIXELS_PER_METER = 128
-- Backpack-Template (Phase A.4 — Atlas-UV render-path).
-- v0.8.0: container={kind="list"} declared so the backpack receives
-- items via inventory.add (capability-by-declaration).
-- v0.14.0: sprite source migrated to TC_Basics `sack1`.
-- Real sprite_atlas + sprite_uv values are filled in M.init after the
-- atlas-JSON is parsed; defaults here are placeholders so the inert-
-- declaration types are correct (string + numbers).
@@ -110,12 +132,41 @@ composition.define_template{
properties = {
sprite_atlas = "",
sprite_uv = {x = 0, y = 0, w = 1, h = 1},
sprite_scale = 1.0,
position = {x = 0, y = 0},
},
tags = {"renderable"},
container = { kind = "list" }, -- receives items via inventory.add
}
-- Bed + Bench-Templates (v0.14.0 — ADR-0054 first-consumer slice).
-- Static world-furniture, no interaction-trigger, no container-block:
-- just sprite + position + `renderable` tag so draw_entities renders
-- them via the Atlas-UV path. Migrates off the proprietary Subterrain
-- world-atlas onto TC_Basics `single_bed1` + `bench1`. Real
-- sprite_atlas + sprite_uv values are filled in M.init after the
-- TC_Basics sprite-mode atlas JSON is parsed.
composition.define_template{
id = "bed",
properties = {
sprite_atlas = "",
sprite_uv = {x = 0, y = 0, w = 1, h = 1},
sprite_scale = 1.0,
position = {x = 0, y = 0},
},
tags = {"renderable"},
}
composition.define_template{
id = "bench",
properties = {
sprite_atlas = "",
sprite_uv = {x = 0, y = 0, w = 1, h = 1},
sprite_scale = 1.0,
position = {x = 0, y = 0},
},
tags = {"renderable"},
}
-- Rock-Template: an item sitting in the world that the player can pick up.
-- sprite_atlas + sprite_uv are filled in M.init after the blob_rect_stone atlas
-- is loaded; placeholder defaults here keep the inert-declaration types correct.
@@ -205,9 +256,9 @@ local state = {
player = nil, -- puppet handle (state-bearing for animation)
actor = nil, -- composition-entity owning position
map = nil,
world_tex = nil, -- subterrain_world atlas diffuse handle (used for bed/bench)
furniture_uvs = nil, -- name -> { x, y, w, h } from atlas JSON
backpack = nil, -- composition-entity, created in M.init after atlas load
bed = nil, -- composition-entity (v0.14.0 — TC_Basics single_bed1)
bench = nil, -- composition-entity (v0.14.0 — TC_Basics bench1)
rock = nil, -- composition-entity (item), created in M.init after blob_rect_stone load
stick = nil, -- composition-entity (item), placeholder for crafting demo
workbench = nil, -- composition-entity (container), Phase D Workbench
@@ -491,34 +542,61 @@ function M.init(ctx)
puppet.write_bone(handle, "foot_r", { rot = foot_local_deg })
end)
-- Furniture atlas (subterrain_world): load JSON + diffuse texture once.
local world_lib = aliases.subterrain_world
local world_base = world_lib .. "/assets/atlases/subterrain_world"
local world_atlas_png = world_base .. "/tiles.diffuse.atlas.png"
local world_meta = engine.asset.load_json(world_base .. "/tiles.atlas.json")
state.world_tex = engine.asset.load_texture(world_atlas_png)
state.furniture_uvs = {}
for _, t in ipairs(world_meta.tiles) do
state.furniture_uvs[t.name] = { x = t.uv[1], y = t.uv[2], w = t.uv[3], h = t.uv[4] }
end
-- Furniture atlas (TC_Basics sprite-mode): load JSON + diffuse atlas.
-- atlas-baker v0.4.0 sprite-mode output layout — sprites table is
-- a map of alias → {x, y, w, h, source_file}; rendering reads
-- x/y/w/h via render.draw_entities' Atlas-UV path. atlas_meta.
-- pixels_per_meter declares the atlas's native resolution; scale
-- against the project canonical to get the per-entity sprite_scale.
local tc_lib = aliases.tc_basics
local tc_base = tc_lib .. "/assets/atlases/tcbasics"
local tc_atlas_png = tc_base .. "/sprites.diffuse.atlas.png"
local tc_meta = engine.asset.load_json(tc_base .. "/sprites.uv.json")
local tc_sprites = tc_meta.sprites
local tc_atlas_ppm = (tc_meta.atlas_meta
and tc_meta.atlas_meta.pixels_per_meter)
or PROJECT_PIXELS_PER_METER
local tc_scale = PROJECT_PIXELS_PER_METER / tc_atlas_ppm
-- Spawn Backpack-Entity with real Atlas-UV values now that the
-- atlas-json is parsed. lib-core.render's atlas-cache will load
-- the texture lazy-on-first-draw (separate from state.world_tex
-- which serves bed/bench's direct draw_sprite_transform call;
-- ineffizient aber kein-Schaden, single-texture-handle-merge ist
-- ein späterer Slice).
local backpack_uv = state.furniture_uvs["backpack_001"]
-- Spawn Backpack-Entity with TC_Basics `sack1` sprite. lib-core.render's
-- atlas-cache lazy-loads the texture on first draw_entities call.
local sack_uv = tc_sprites.sack1
state.backpack = composition.create{
template = "backpack",
properties = {
sprite_atlas = world_atlas_png,
sprite_uv = {x = backpack_uv.x, y = backpack_uv.y,
w = backpack_uv.w, h = backpack_uv.h},
sprite_atlas = tc_atlas_png,
sprite_uv = {x = sack_uv.x, y = sack_uv.y,
w = sack_uv.w, h = sack_uv.h},
sprite_scale = tc_scale,
position = {x = BACKPACK_X, y = BACKPACK_Y},
},
}
-- Spawn Bed + Bench entities (v0.14.0). Static world-furniture
-- rendered via the same Atlas-UV path as the backpack.
local bed_uv = tc_sprites.single_bed1
state.bed = composition.create{
template = "bed",
properties = {
sprite_atlas = tc_atlas_png,
sprite_uv = {x = bed_uv.x, y = bed_uv.y,
w = bed_uv.w, h = bed_uv.h},
sprite_scale = tc_scale,
position = {x = BED_X, y = BED_Y},
},
}
local bench_uv = tc_sprites.bench1
state.bench = composition.create{
template = "bench",
properties = {
sprite_atlas = tc_atlas_png,
sprite_uv = {x = bench_uv.x, y = bench_uv.y,
w = bench_uv.w, h = bench_uv.h},
sprite_scale = tc_scale,
position = {x = BENCH_X, y = BENCH_Y},
},
}
-- Blob-rect-stone atlas: load JSON + diffuse texture for the rock sprite.
-- blob_rect_stone alias resolves to lib-asset.prototype-blob-geom. The
-- "slot_00_isolated" tile is the standalone isolated stone shape — reads as
@@ -534,15 +612,15 @@ function M.init(ctx)
end
-- Crafted-output templates: defined here (not top-level) so their
-- sprite_atlas defaults can carry the resolved blob_rect_stone PNG path.
-- sprite_atlas defaults can carry the resolved atlas PNG path.
-- crafting.craft calls composition.create with no overrides, so the
-- template defaults are the only carrier for sprite_atlas / sprite_uv
-- on the crafted instance. Without this, crafted items render as a
-- "?" placeholder in the inventory and a magenta-fallback square on
-- drop (lib-core.render path-3).
-- slot_05_tee_open reads as a tool-shaped tile (rock_pick).
-- slot_13_solid reads as a dense cluster tile (rock_pile).
local rock_pick_uv = stone_uvs["slot_05_tee_open"]
-- v0.14.0: rock_pick visual switched to TC_Basics `spade`; rock_pile
-- stays on blob_rect_stone (no TC_Basics rock-stack equivalent).
local spade_uv = tc_sprites.spade
composition.define_template{
id = "rock_pick",
properties = {
@@ -553,9 +631,10 @@ function M.init(ctx)
volume = 0.0015,
["composition.stone"] = 0.9,
["composition.wood"] = 0.1,
sprite_atlas = stone_atlas_png,
sprite_uv = {x = rock_pick_uv.x, y = rock_pick_uv.y,
w = rock_pick_uv.w, h = rock_pick_uv.h},
sprite_atlas = tc_atlas_png,
sprite_uv = {x = spade_uv.x, y = spade_uv.y,
w = spade_uv.w, h = spade_uv.h},
sprite_scale = tc_scale,
position = {x = 0, y = 0},
},
tags = {"renderable", "item"},
@@ -627,14 +706,18 @@ function M.init(ctx)
-- on the same Y as rock + backpack). Placeholder visual: slot_04_straight
-- from blob_rect_stone reads as a long horizontal bar (Phase C-Q10 atlas
-- re-use; echtes Stick-Sprite folgt mit atlas-baker-decals).
-- v0.14.0: stick visual switched to TC_Basics `log_pile1`. Sprite_scale
-- via property-override since the stick template default (1.0) was set
-- before tc_scale was known.
local STICK_X, STICK_Y = BACKPACK_X - 160, BACKPACK_Y
local stick_uv = stone_uvs["slot_04_straight"]
local log_pile_uv = tc_sprites.log_pile1
state.stick = composition.create{
template = "stick",
properties = {
sprite_atlas = stone_atlas_png,
sprite_uv = {x = stick_uv.x, y = stick_uv.y,
w = stick_uv.w, h = stick_uv.h},
sprite_atlas = tc_atlas_png,
sprite_uv = {x = log_pile_uv.x, y = log_pile_uv.y,
w = log_pile_uv.w, h = log_pile_uv.h},
sprite_scale = tc_scale,
position = {x = STICK_X, y = STICK_Y},
},
}
@@ -674,6 +757,23 @@ function M.init(ctx)
description = "Stack rocks for storage.",
}
-- v0.14.0: per-recipe icon resolver for the Crafting + Workbench-Crafting
-- widgets. Maps recipe.output.template → {atlas, uv} for the icon
-- column added in crafting-display v0.3.0. composition has no
-- template-property-getter today (ADR-0001 keeps the read-side
-- domain-free), so the module-side that DEFINES the templates is the
-- canonical place to know each output's sprite.
local craft_icons = {
rock_pick = { atlas = tc_atlas_png, uv = tc_sprites.spade },
rock_pile = { atlas = stone_atlas_png, uv = stone_uvs.slot_13_solid },
}
local function craft_icon_for(recipe)
if recipe and recipe.output and recipe.output.template then
return craft_icons[recipe.output.template]
end
return nil
end
-- Inventory-UI Setup
panel.set_theme{ panel_width_frac = 0.4, font_size_title = 20 }
@@ -737,6 +837,7 @@ function M.init(ctx)
ctx_factory = function()
return { actor = state.actor }
end,
icon_resolver = craft_icon_for,
})
-- Craft action: invokes crafting.craft on the backpack, posts a
@@ -818,6 +919,7 @@ function M.init(ctx)
ctx_factory = function()
return { actor = state.actor, at_workbench = true }
end,
icon_resolver = craft_icon_for,
})
-- Craft action: invokes crafting.craft on the resolved locale (multi-
@@ -1148,19 +1250,9 @@ end
function M.draw(ctx)
camera.begin()
maps.draw_map_pre_entities()
-- Bed + Bench remain hardcoded furniture (no interaction-trigger →
-- no entity-Pflicht in Phase A; see Backpack-Template comment).
local bed_uv = state.furniture_uvs["bed_001"]
local bench_uv = state.furniture_uvs["bench_001"]
engine.render.draw_sprite_transform(state.world_tex, 200, 150, 0, 1, 1,
bed_uv.w / 2, bed_uv.h / 2, 0xFFFFFFFF,
bed_uv.x, bed_uv.y, bed_uv.w, bed_uv.h)
engine.render.draw_sprite_transform(state.world_tex, 400, 300, 0, 1, 1,
bench_uv.w / 2, bench_uv.h / 2, 0xFFFFFFFF,
bench_uv.x, bench_uv.y, bench_uv.w, bench_uv.h)
-- Backpack (and any other renderable entity) via Phase-A.2 tag-based
-- render. The atlas-cache loads tiles.diffuse.atlas.png lazy-on-first
-- call (separate handle from state.world_tex — see M.init note).
-- All renderable furniture + items via Phase-A.2 tag-based render
-- (v0.14.0: Bed + Bench joined Backpack as composition-entities).
-- lib-core.render's atlas-cache lazy-loads each sprite_atlas path.
r_lib.draw_entities{ tag = "renderable" }
-- Puppet (sprite-mode via texture_handles populated by load_rig+load_textures).
puppet.render(state.player)