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>
Engine resolver does exact strcmp on dep versions, so consumer manifests
must pin the new lib-core.panel 0.4.0 to keep load-time resolution green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
D.2-revisit closes the Workbench Gate-2 walkthrough: right-click
on Workbench now opens workbench_crafting (left-half layout-slot)
+ workbench_inv (right-half) simultaneously, replacing the v0.12.0
auto-switch workaround. Both panels visible at the same time.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Track lib-core.panel v0.2.0 (multi-active panels with layout-slots).
No behavioral change in this consumer — the v0.1.1 call sites remain
compatible via the panel framework's bw-compat shim.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Drop "craft" tag from Inspect-Recipe payload; multi-line detail
posts now route only to the inspect/detail channel, not the toast
events channel.
- Wire rock_pick + rock_pile templates to blob_rect_stone atlas
slots (slot_05_tee_open + slot_13_solid) so crafted items render
in inventory + on drop instead of magenta fallback.
- Drop dead "crafting" tag from events filter (no post emitted it).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously ESC unconditionally switched to the launcher, leaving no way
to dismiss an open inspect-detail (or any panel-widget opened via
notify.post without a toggle key). Now ESC closes the active panel
first; only when no panel is open does it quit to the launcher.
Rock template now declares name="Rock", a description, weight (kg),
volume (L), and composition.stone=1.0. The module-side label_resolver
override is removed — display lib's default reads name now.
Inspect action prints a structured block (name, description, weight,
volume, composition.<material>, reg_id) to the console. Future
notification system will route this to an in-game text panel; for now
the console output makes Inspect useful instead of silent.
- lib-core.panel and lib-core.inventory-list-display added to deps (v0.1.0 each)
- backpack-widget created in M.init with label-resolver returning "Rock"
- Drop action registered: removes from backpack, relocates to player position,
re-registers pickup trigger
- Inspect action registered: prints reg_id via engine.print
- M.update: panel.update(dt) + is_pausing() early-return added after
quit_to_launcher check
- interaction.update wrapped in panel.is_open() guard to block world triggers
while panel is visible
- M.draw: panel.render() added at z-top after CI traces
- Q-drop (LIFO quick-drop via Q key) unchanged and parallel to panel Drop action
- Add container={kind="list"} to the backpack template so inventory.add
accepts it as a valid list-container
- Remove old backpack proximity-trigger (print-only callback, pre-B.5);
the rock's register_pickup is now the sole interaction trigger
- Emit unconditional inventory inspection trace after every add/remove:
backpack count + item reg-ids for user-facing demo feedback
- Add CI-gated tree-check traces: tree_check_after_pickup=ok and
tree_check_after_drop=ok verify composition-tree child placement
- Bump version 0.7.3 -> 0.8.0 across init.lua header, manifest.module,
and README
- README: rewrite Controls section into full demo walkthrough; add Demo
Walkthrough section; update Demonstrates + Interactions sections;
add v0.8.0 CHANGELOG entry
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rock template (stack_mode=individual, tags=renderable+item) defined
alongside backpack and vagrant_player templates.
- state.rock entity created in M.init using slot_00_isolated UV from
the blob_rect_stone atlas (lib-asset.prototype-blob-geom); position
(270, 400) — 80px left of the backpack.
- register_pickup(state.rock) wires an interaction trigger; existing
render.draw_entities{tag="renderable"} draws the rock with no
special-case render code.
- CI trace: engine.print("vagrant: rock_spawned") emitted once on init.
- Version bump 0.7.2 -> 0.7.3.
- Add lib-core.inventory-list v0.1.0 to manifest deps; bump module
version 0.7.1 -> 0.7.2
- Import inventory = require("lib-core.inventory-list") in init.lua
- Add Item-Template-Convention comment block after vagrant_player
template: documents stack_mode/tags/sprite/position contract that
B.4 (rock) and B.5 (backpack-as-container) will fulfill
- Add register_pickup(item) factory: reads item position, registers
interaction trigger; callback calls inventory.add + unregisters
trigger on pickup
- Bind "drop" action to Q; drop handler in M.update pops last item
from backpack via inventory.remove (renderable tag restored by
library), repositions item at player pos +30px, re-registers pickup
- Update README Controls section: document E (pickup) and Q (drop)
with note that end-to-end use requires B.4 + B.5
Refactor: Player gesplittet in zwei Handles. state.actor (composition-
Entity) ist canonical position-owner. state.player (puppet handle)
bleibt für Animation + Render zuständig und syncs each frame via
puppet.move_to(state.player, actor.position(state.actor)).
vagrant_player-Template ist UNTAGGED — render.draw_entities sieht ihn
nicht, da puppet.render(state.player) das Sprite-Rendering schon macht.
Tag-Konflikt vermieden.
Movement-Refactor in M.update:
- CI_DRIVE-Branch: actor.move(state.actor, dx*speed*dt, 0)
- Normal-Movement: actor.move(state.actor, mx*speed*dt, my*speed*dt)
- Sync-Punkt nach beiden Branches: puppet.move_to(state.player,
actor.position(state.actor))
- interaction.update + camera.set_target lesen actor.position
Sprint-Logic unverändert (eigene WALK_SPEED constants; movement_speed
auf actor ist informational/120).
Bumps: 0.6.0 → 0.7.0; +actor v0.1.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refactor: Backpack ist composition-Entity mit Atlas-UV-Render-Pfad.
Template at top-level declares sprite_atlas + sprite_uv.{x,y,w,h} +
position. create() in M.init nach atlas-load setzt reale UV-Werte aus
state.furniture_uvs["backpack_001"].
render.draw_entities{tag="renderable"} ersetzt den direkten
draw_sprite_transform-Call für Backpack. interaction.register liest
position aus entity-properties.
Bed + Bench bleiben hardcoded Atlas-Furniture — kein
Interaction-Trigger, kein Demo-Pflicht zur Entity-Promotion in
Phase A. Phase B (container-on-bed) oder Phase D wäre der Trigger.
Atlas-Texture wird doppelt geladen (state.world_tex für bed/bench,
render-internal-cache für backpack) — ineffizient aber nicht
kaputt; single-handle-merge ist späterer Slice.
Bumps: render 0.1.0 → 0.2.0; +composition v0.1.0; module 0.4.4 → 0.6.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The launcher migrated from lib-sporel.launcher to lib-management.launcher
per ADR-0045 (three-tier library naming). Update the quit_to_launcher
handler to match the new module id.
manifest.module
+ new dep on lib-asset.prototype-blob-geom v0.1.0 (companion lib
for the design paper's reference asset set + Python atlas-packer)
+ asset_aliases entry for blob_testbench (mixed 3-material atlas:
grass/stone/wood x 14 slots = 42 tiles) and blob_rect_stone
(single-material atlas for vertex-painted autotile)
- removed subterrain_tiles asset_alias (no longer used)
bumped lib-core.maps dep 0.4.0 -> 0.5.0
vagrant_test.map.json rewritten as native schema-v3 with three non-
empty terrain layers, exercising all renderer paths in a single map:
- surface (tiles[] path, multi-material via blob_testbench):
grass-solid floor across all 400 cells
14-wide x 3-tall showcase strip on rows 10-12 columns 2-15
displays every slot of every material the atlas ships (slot 0
through 13 x grass/stone/wood, ids 1..42 visible at a glance)
- topsurface (tiles[] path): 2x4 wood overlay decoration on rows
3-4 columns 2-5
- wall (vertex-painted autotile with blob_rect_stone material):
21x21 vertex grid; 80 vertices painted in a border-ring pattern
so the renderer's bitmask + 47->14 slot lookup produces the
visually-correct stone wall around the 18x18 interior; plus
3 interior pillars (cells 9:7, 7:15, 13:15) authored as
overrides with slot 0 (isolated), demonstrating that the
override sublayer can place truly-1-cell-isolated material
which the any-corner vertex rule alone cannot express
assets/tiles/vagrant_test.tilemap.json removed; under schema v3 the
engine resolves the palette directly from the atlas's tiles.atlas.json,
the standalone tilemap.json was unused (atlas-bootstrap stub path
took over since M.2).
Closure verifies SPOREL_CI=1 ./Sporel.exe --module=vagrant-skeleton
rc=0 with 60 render_frame_ok, both atlases load (no magenta-
placeholders), walkability rule (surface present + wall absent)
correctly treats border + pillars as non-walkable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Asset-aliases now reference the three baked subterrain atlas-sets
(subterrain_player, subterrain_tiles, subterrain_world) rather than
the legacy indexed pack labels. The rig asset_pack updates to
subterrain_player and each bone gains a calibrated anchor offset.
Furniture-draw loads the subterrain_world atlas once and renders
via source-rect draw_sprite_transform. The map tilemap field updates
to subterrain_tiles to match the new atlas-id. Dep-bumps for
lib-core.maps 0.3.0, lib-core.puppet 0.5.0, and
lib-asset.prototype-subterrain 0.2.0. Replace r_lib.draw_map with
draw_map_pre/post_entities.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sprint: holding shift while WASD doubles player_speed (120 → 240) and
walk-anim play-speed (0.45 → 0.9). Mid-walk sprint toggle uses puppet
v0.4.4 set_play_speed → no cycle restart. Releasing shift or stopping
WASD reverts to normal speed in next frame.
Interaction: 'interact' bound to E. Proximity trigger registered on
the backpack sprite at (350, 400) with range 40px. Pressing E within
range fires callback that prints a log message with the actor distance.
Uses existing lib-core.interaction (registered at init, polled by
interaction.update(player.x, player.y) each frame).
New dep: lib-core.interaction ^0.1.0. Module version 0.4.0 → 0.4.4
(syncs with puppet bump).
Updates README to describe the Subterrain-style puppet control model
implemented across v0.3.1–v0.4.3. Previous README described v0.2.0
state and predated the multi-look-target, slerp, inherit_scale,
procedural orientation, and tactical-twist foot behavior.
Demonstrates section updated: multi-look-target (soft/aim), scale-
deformation walk cycle, procedural leg orientation, foot-body-orientation
(tactical twist), inherit_scale=false on feet, per-channel conflict guard,
camera.begin/finish world-space rendering.
Interactions section updated to describe the v0.4.3 visual behavior.
Full CHANGELOG from v0.2.0 through v0.4.3 with per-version summaries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Note re user observation 'hat sich die animationslänge durch negative
skalierung verändert': no, anim duration (0.8s) and play speed (the
dt multiplier) are independent of bone-state values. The full cycle
visually traverses scl=+1 → 0 → -1 → 0 → +1 over 0.8s × 1/0.45 ≈
1.78s effective. Previous 0.4 gave 2.0s — slightly slower than
desired walking cadence.
User feedback after iteration-4: legs SHOULD cycle through +1/0/-1
(substrate-style — leg renders forward, under body, then behind body).
But feet must NOT Y-flip when leg.scl_y goes negative, and must NOT
collapse when leg.scl_y=0. They should follow leg-end position only.
Three changes:
1. walk_fwd_lower keyframes restored to substrate-style (leg.scl_y
sign-flip on both halves: leg_l +1↔-1, leg_r -1↔+1). foot.pos
animations REMOVED — cascade through leg.world.scl now correctly
moves foot to leg-end position automatically (foot.local.pos
stays at rest 25 in y).
2. foot.rest.scl: [1, -1] → [1, 1] (no Y-flip in rest, natural
sprite orientation). inherit_scale=false → foot.world.scl stays
at foot.local.scl=[1,1] regardless of leg.scl_y. Foot sprite
never flips, never collapses.
3. Procedural extended: writes foot.local.rot = -leg.local.rot so
foot.world.rot = body.world.rot (feet stay body-oriented even
when legs rotate to walk-direction).
Speed slowed further from 0.5 → 0.4 (user: still too fast at 0.5).
User manual-test feedback after iteration-3 hip-stable + leg-rotation
fixes:
1. Animation too fast for actual map traversal → play walk_fwd_lower
and walk_upper with speed=0.5 (effective 1.6s cycle, was 0.8s).
2. Second half visually broken → was substrate's sign-flip trick where
leg.scl_y goes +1 (opposite of rest -1) and foot.pos.y flips
negative. Cascade foot.world.scl_y = +1 * -1 = -1 → foot rendered
Y-flipped. User: 'die füße ... darf hier die skalierung nicht negativ
werden'.
New cycle: each half extends ONE leg (scl_y=-1 matching rest) while
the other stays at scl_y=0 (collapsed/invisible). foot.world.scl_y
never goes negative — only [0, +1]. Alternating-leg gait. Substrate
first-half preserved verbatim; second-half mirrors with role swap.
User feedback: 'anchorpoints sind hüftgelenke und können sich nicht
frei drehen. hüfte bleibt zum körper statisch'.
Architecture:
- legs.parent = body (was lower_control). Hip position cascades from
body.world.rot, so hips stay at body-frame left/right regardless of
body rotation. lower_control virtual bone removed (no longer needed).
- Procedural 'leg_orientation' sets leg.local.rot = walk_dir - body.rot
each frame, so leg.world.rot = walk_dir (independent of body cascade).
Requires puppet v0.4.2 per-channel write_bone (walk_anim writes leg.scl
while procedural writes leg.rot — disjoint channels).
Result: legs visually attach at body's anatomical left/right hips no
matter where body faces; stride oscillation aligns with WASD walk
direction. Hip position no longer drifts with walk direction.
User feedback: 'Beine sollten in die richtung oszillieren, in die
gegangen wird, nicht nach norden/süden relativ zum fenster.'
Old: lower_control.rot = wedge_deg (substrate's body-relative ±30°
sub-angle), legs always oscillated near world-up axis.
New: lower_control.rot = atan(move_x, -move_y) = walk direction in
sprite-top-up convention. Legs visually rotate to align with WASD
direction → stride animation cycles along the walk axis.
Side-effect: 4-direction-anim picker collapsed to single
walk_fwd_lower (the rotation handles directional differentiation;
4 anims kept on disk for later gait-style work). Bumps puppet dep
to 0.4.1 (head south-flick fix).
Two fixes from manual interactive verification after iteration-3:
1. Head 90° CCW off → removed sprite_rot=-90 from head bone. The
v0.3.3 sprite_rot fix was based on misobservation: at that point
the persistent-state-rate-limit bug was active and head was
wiggling within ±10° of an arbitrary initial direction, perceived
as 90° CW off. With v0.3.2+ persistent-state fix and v0.4.0 slerp,
head rotates correctly when sprite-natural-face is -Y (matching
body-sprite convention).
2. Legs walked in inverted sector → signed_angle_2d cross-component
was using Y-up math convention but Sporel screen-coords are Y-down.
With Y-down, what's CCW in math is CW visually. Flipping the cross
sign aligns sector selection: body up + player walks right → CW
visually → walk_right_lower (was wrongly returning walk_left).
Front/back leg-anchor issue (#3 from user report) deferred — likely an
optical-overlap artifact from rotated body sprite + world-frame leg
positions, will reassess after #1+#2 fixes are visually verified.
Iteration-3 implemented Subterrain control model but forgot to bump
the consumer manifest. Engine lib-resolver does exact strcmp so
'0.3.3' fails against published 0.4.0 → ENGINE_ERR_LIB_DEP_MISSING
FATAL at module load.
Recurring pattern (cf. feedback_sporel_dep_versions memory) — every
lib-bump must update all consumer manifests in the same commit-set.
Add lower_control virtual bone (reparents legs for independent orientation).
Update look_at fields: body uses "soft" target with slerp=6, head uses
"aim" target with instant snap and ±60° clamp. Rename walk.anim.json to
walk_fwd_lower.anim.json and add walk_back_lower, walk_left_lower,
walk_right_lower anims ported from substrate. Port HandleAnimationLower:
signed-angle sector selection (±45°/±135°) drives 4-directional walk anim
choice; wedge clamp sets lower_control rotation. Separate aim/soft look
targets both track raw mouse for now.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
head_001.png is drawn with face pointing +X (right) at rot=0, but the
look-at math assumes sprite-top (-Y) is forward. Without compensation,
head visually appears 90° clockwise off when math says 'point at mouse'.
sprite_rot=-90 on head bone aligns sprite-top with bone-forward at
render time. Body, shoulder, and arm sprites already match the
sprite-top=forward convention so no sprite_rot needed there.
Also bumps puppet dep to 0.3.3 and removes the temporary debug print.
Map + furniture + puppet now render via the 2D camera so the camera
target (= puppet position) appears at screen-center. Without the wrap,
world rendering happened in raw window-coords: puppet at world (320,240)
appeared at screen upper-left, mouse-screen-to-world math was correct
but mouse cursor over the visible puppet computed dx/dy from puppet's
world position to mouse_world which was OFF-by-(w/2,h/2). Visual result:
head pointed in wrong direction relative to actual mouse position.
Spine-prototype already used this pattern (init.lua:128). Aligning
vagrant-skeleton with that convention.
Iteration 2 bumped lib-core.puppet to v0.3.2 (persistent look_at_state
fix) but the vagrant-skeleton manifest dep was not updated. Engine
lib-resolver does exact strcmp → module fails to load with
ENGINE_ERR_LIB_DEP_MISSING at launcher select.
- humanoid.rig.json: removed locomotion-block (apply_locomotion no-ops
now). head: removed look_at_speed (instant snap). body: unchanged
(look_at_speed=180 lazy).
- init.lua: removed set_procedural("facing", ...) — root stays at
rest.rot=0. walk.anim + idle_lower.anim no longer loaded/played
(files retained on disk for revert). New set_procedural("walk_cycle",
...) drives legs per-frame: walk_phase advances at 1.25Hz, sin-cycle
on leg.scl, walk_dir applied to leg.rot. Idle: legs scl=0.
- Lazy body + instant head + head-leading: head responds immediately
to mouse, body lags via look_at_speed=180.
This is an experiment. If procedural feels worse, revert by re-enabling
walk.anim + idle_lower.anim + locomotion-block.
- humanoid.rig.json: root removed from upper track; body look_at + slow speed
(180 deg/s, lazy); head fast speed (480 deg/s) + +-90 deg limits; puppet
dep bumped to 0.3.1
- animations/idle_lower.anim.json (NEW): leg+foot scl=[mirror, 0] keeps legs
invisible at idle (locomotion skips animated bones; foot cascade zeros out)
- init.lua: set_procedural 'facing' callback writes root.rot from
last_walk_rot_deg each frame; WASD-vector updates last_walk_rot_deg
via atan(dx, -dy); idle_lower played parallel to idle, stopped during walk
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>