Recipe roles now match on a single `affordance.*` axis, required by both consumed `inputs` and non-consumed `tools` (the inputs/tools split carries consumption, not a separate axis). `form` — which conflated geometry, identity and capability — is removed. Its functional members folded into affordances (blade->cutting, shaft->leverage, plus knappable/spinnable/binding); identity members became template-id (chop_tree) or material+weight (log, hammer head). vagrant-skeleton v0.20.0; no crafting/composition lib bump (they never knew `form`). Verified headless via the tool-harness: 16/16 checks pass — role-flexibility (stick OR branch hafts), weight discrimination, tool-gating (tool not consumed), and anti-recursion guards (sharp_stone not re-knappable, cordage not re-spinnable).
22 KiB
sporel-module-vagrant-skeleton
F.vagrant Tier-1 test-chamber: single player as a 13-bone humanoid puppet, WASD movement, camera-chase, multi-target look-at (body lazy-tracks mouse via slerp, head snaps to mouse with ±60° clamp). Demonstrates the Subterrain-style puppet control model: leg orientation decoupled from body via procedural callback, foot-body-orientation ("tactical twist"), scale-deformation walk cycle, and inheritance-decoupled foot sprites. End-to-end inventory pickup/drop demo: pick up a rock into a backpack container, drop it back into the world.
Version: 0.20.0 Module-ID: vagrant-skeleton Requires: lib-core.input >=0.4.0, lib-core.camera >=0.3.0, lib-core.render >=0.2.0, lib-core.maps >=0.1.2, lib-core.puppet >=0.4.4, lib-core.interaction >=0.1.0, lib-core.composition >=0.3.0, lib-core.actor >=0.1.0, lib-core.inventory-list >=0.1.0, lib-core.panel >=0.2.0, lib-core.inventory-list-display >=0.1.0, lib-core.notify >=0.1.0, lib-core.notify-display >=0.1.0, lib-core.world-overlay >=0.1.0, lib-core.crafting >=0.3.0, lib-core.crafting-display >=0.2.0, lib-asset.prototype-subterrain >=0.2.0, lib-asset.prototype-tc-basics >=0.1.0 Tags: test-chamber, puppet, walking-sim, interaction
v0.20.0:
formretired →affordanceis the one capability axis (ADR-0055 amendment). Theformrole-tag mixed geometry, identity and capability; it is gone. Its functional members folded intoaffordance.*(blade→cutting,shaft→leverage, plusknappable/spinnable/binding); identity members became template-id (chop_tree={template= "tree"}) or material+mass (log/hammer-head). The SAME affordance vocabulary is now required by both consumedinputsand non-consumedtools. Role- flexibility preserved: a haft ={["affordance.leverage"]=true}→ stick OR branch; weight still discriminates (light stick sharpens, heavy branch breaks).v0.19.0: Role-based recipes (superseded by v0.20.0's axis rename). Introduced role-matching for Stone-Age recipes so any item filling a role qualifies, not a specific item id — originally via a
formtag, since folded intoaffordance. Template-id matching stays available (rock_pick/rock_pile) — one of several modes, not the default.v0.18.0: Stone-Age round-out (content pack) —
craft_stone_hammer(rock+stick+cordage → hammering tool), which now gatesknap_sharp_stone(was bare-handed);chop_tree(tree + chopping axe → 2 logs, closing the axe→log→plank loop);break_branch(branch → 2 sticks, multi-output);sharpen_stick(stick + cutting → sharpened_stick). Tool-gating verified (knap/chop fail without the right affordance).v0.17.0: T1-modder content packs. Stone-Age items + recipes + world- spawns moved OUT of
init.luaintocontent/stone_age.lua— pure declarative data (c.T{}item,c.R{}recipe,c.S{}spawn), loaded via a content facade ininit.lua(engine.module.dir_of+dofile, the sanctioned multi-file pattern).c.Tauto-registers each craftable's crafting-panel icon from its sprite. A T1 modder now adds/edits Stone-Age content by touching only the pack file, never engine wiring. Behaviour is unchanged (regression-verified); also fixes craftedsharp_stonerendering (was a "?" placeholder before).v0.16.0: Stone-Age tech chain — a self-bootstrapping crafting graph on the v0.3 API.
knap_sharp_stone(rock → sharp_stone, bare-handed) yields a cutting tool that feeds BOTHsaw_planks(as the non-consumed tool) ANDcraft_stone_axe.twist_cordage(3× plant_fiber → cordage) + the axe haft (sharp_stone + stick + cordage → stone_axe, carriesaffordance.chopping). Three plant fibers spawn in the world. Validated headless end-to-end incl. the knap→saw and knap→haft connections.v0.15.0: First consumer of
lib-core.craftingv0.3.0 (ADR-0055). New stone-age slice: a Log (heavy wood item) + a Sharp Stone (cutting tool) spawn in the world, and thesaw_planksrecipe matches its input by property-constraint (composition.wood > 0.5ANDweight > 5) instead of template-id, requires the sharp-stone as a non-consumed tool (affordance.cutting), and yields 4 planks (multi-output). A light plank (weight 2) is correctly not re-sawable. Existingrock_pick/rock_pilerecipes are unchanged (template-id hybrid bw-compat).v0.14.0: Bed + Bench + Backpack visuals migrated from the proprietary Subterrain world-atlas to TC_Basics sprites (
single_bed1,bench1,sack1). Bed + Bench promoted to composition-entities (renderable tag) — the hardcodeddraw_sprite_transformcalls for furniture are gone. First concrete consumer of ADR-0054 Item-Visual-Identity. The Subterrain dep remains for the puppet sprite parts (subterrain_playeralias).
Topology
Controls
- W A S D — direct movement (4-way, normalized for diagonals)
- Shift + WASD — sprint (2x player speed + 2x walk-anim cadence)
- Mouse — body + head look-at targets (same position, different smoothing)
- E — interact: if within range of the rock or stick, picks it up into the backpack
- Q — quick-drop (LIFO): places the most-recently-added backpack item at player position (+30px right)
- Tab — toggle inventory panel (Backpack contents)
- C — toggle Backpack-Crafting panel (lists known recipes; availability dimmed by inventory)
- Right-Click on Workbench (within range) — opens Workbench-Crafting (left-half) + Workbench-Inventory (right-half) panels side-by-side (panel v0.2.0 multi-active)
- Right-Click on item row (inventory panel): context menu with Drop / Inspect (Backpack) or Take / Inspect (Workbench)
- Right-Click on recipe row (crafting panel): context menu with Craft / Inspect
- L — toggle game-log (chronological list of all events)
- Right-Click on item row → Inspect — opens a modal Item Details panel (instead of console-only output)
- Pickup (E) — shows "+1 " floating above the player + toast at top-right + log entry
- Drop (right-click on item → Drop, or Q for LIFO quick-drop) — toast at top-right + log entry
- Craft (right-click on recipe → Craft) — consumes inputs from current locale, adds output, posts a success/failure toast
- Take (right-click on Workbench-item → Take) — moves item from Workbench → Backpack + toast
- ESC — close any open panel; if no panel is open, return to launcher
Demo Walkthrough
The world contains five notable entities at startup:
- Player — spawns at (320, 240), controlled via WASD + mouse.
- Backpack — a visible sprite at (350, 400). Acts as a container (kind="list"). Not directly interactable; it receives items via inventory.add.
- Rock — a standalone stone tile at (270, 400), 80px to the left of the backpack. Tagged "renderable" + "item"; starts visible on the ground.
- Stick — a placeholder pickup-item at (190, 400), 160px to the left of the backpack.
Re-uses the
slot_04_straightblob_rect_stone tile for its visual (Phase C-Q10 pragmatic resolution — echtes Stick-Sprite folgt mit dem ersten atlas-baker-decals-Slice). - Workbench — a crafting bench at (350, 380), just above the backpack. Container
(kind="list"); placeholder visual re-uses the
slot_07_tee_fullblob_rect_stone tile. Right-Click within range opens Workbench-Crafting + Workbench-Inventory panels.
Pickup flow:
- Walk toward the rock (it sits to the left of the backpack).
- Press E within 40px of the rock.
- The rock moves into the backpack via
inventory.add; its "renderable" tag is removed so it no longer appears in the world. - The console prints:
vagrant: backpack count=1 items=[<reg_id>]
Drop flow:
- Press Q at any time when the backpack has items.
- The most-recently-added item is removed from the backpack (
inventory.remove) and placed at the player position +30px to the right. Its "renderable" tag is restored so it reappears in the world. - A new pickup trigger is registered at the drop position.
- The console prints:
vagrant: backpack count=0 items=[]
Multiple pickups: Each E → Q → E cycle moves the same rock in and out. Only one rock is spawned per session.
Workbench flow (Phase D v0.2 + v0.13.0 multi-panel):
- Pick up rock + stick into the backpack (E on each).
- Walk to the Workbench (just above the Backpack at (350, 380)) and right-click within 40px range.
- Both panels open simultaneously, side-by-side: Workbench-Crafting in the left-half layout-slot, Workbench-Inventory in the right-half slot (panel v0.2.0 multi-active + layout-slots).
- Right-click on a recipe row (e.g. Stone Pick) → context menu with Craft + Inspect. Click Craft → crafted item lands in Workbench-Inventory (NOT Backpack); it appears in the right-half panel immediately.
- Right-click on the crafted item in Workbench-Inventory → Take moves it to the Backpack; a "Took " toast appears.
- Crafting inputs are aggregated from
{backpack, workbench}(drain order: backpack first, then workbench).
Demonstrates
- lib-core.puppet v0.4.3 integration:
- Multi-look-target: body "soft" (slerp=6, lazy) + head "aim" (instant-snap, ±60° clamp)
- Scale-deformation walk cycle: leg.scl_y cycles +1/0/-1 via keyframe animation
- Procedural leg orientation: legs face walk direction independent of body rotation
- Foot-body-orientation ("tactical twist"): feet face body direction, not leg direction
inherit_scale: falseon feet: feet do not Y-flip when leg.scl_y goes negative- Per-channel conflict guard: walk animation (scl) + procedural (rot) on same leg bones
- Camera in
camera.begin()/finish()block for correct world-space rendering
- WASD movement → walk_fwd_lower + walk_upper animation pair (speed = 0.45x)
- Shift+WASD sprint →
puppet.set_play_speeddoubles cadence to 0.9x mid-cycle (no animation restart on toggle); player movement speed doubled (120 → 240) - Idle state → idle + idle_lower animations
- Camera-chase via lib-core.camera target-provider
- Sprite-tilemap via lib-core.maps
- lib-core.interaction proximity trigger on the rock at (270, 400), range 40px, "interact" action bound to E — fires inventory.add on dispatch
- lib-core.inventory-list: backpack is a container (kind="list"); rock is an individual-stack item; add/remove toggle renderable tag
Interactions
- WASD active → legs swing in walk direction; body slowly rotates to mouse; head snaps to mouse
- Shift held while moving → 2x player speed; walk cadence doubles (smooth mid-cycle transition)
- WASD released → idle_lower collapses legs (scl=0); upper body returns to idle sway
- Mouse moved → body lazy-rotates via slerp (look_at_slerp: 6); head snaps within ±60° of body
- Walking east while body faces north → legs perpendicular to body, hips body-relative, feet face north
- E pressed within 40px of the rock → fires
"interact"action; rock moves into backpack; console printsvagrant: backpack count=N items=[...] - Q pressed with items in backpack → most-recent item drops at player position; pickup trigger re-registered at drop location; console prints updated backpack state
CI Hooks
SPOREL_CI=1→ 60-frame perf canary. Emitsvagrant: frame_avg_ms=Nandvagrant: render_frame_ok(per-frame, from render hook), then exits. Used by smoke stages P0-S25a + P0-S25b.SPOREL_VAGRANT_PHASE1_DRIVE=1→ scripted synthetic WASD (frames 20-40 move east). Used by P0-S25 puppet-test integration.
References
superpowers/specs/2026-05-18-puppet-subterrain-model-design.md(design basis)docs/adrs/0040-puppet-subterrain-model-pivot.md(architectural rationale)architecture/puppet.md(lib-core.puppet reference)audit/2026-05-18-puppet-subterrain-model-manual-test.md(manual verification checklist)- ADR-0028 (Launcher-Flow)
- ADR-0036 (Lib-Override for multi-module reuse)
- ADR-0037 (Tests-as-Libs)
- ADR-0038 (API-Doc-Convention)
CHANGELOG
v0.13.0 — Workbench Multi-Panel via lib-core.panel v0.2.0
- Right-click on Workbench now opens BOTH panels simultaneously:
workbench_crafting(layout="left-half") +workbench_inv(layout="right-half"). - Replaces the v0.12.0 auto-switch workaround that was forced by
panel v0.1.1's single-active limitation. The Workbench Gate-2
walkthrough per
2026-06-14-phase-D-workbench-design.md§7 now runs cleanly. - Deps: lib-core.panel 0.1.1 → 0.2.0 (multi-active + layout-slots).
v0.12.0 — Workbench
- New: Workbench entity in the world at (350, 380) —
composition.define_templatewithcontainer = {kind="list"}, taggedrenderable,container,workbench,interactable. Placeholder visual re-uses theslot_07_tee_fullblob_rect_stone tile (visually distinct from rock/stick/rock_pick/rock_pile). - New: Right-Click on Workbench opens two panels:
- Workbench Crafting — recipes are still listed by
lib-core.crafting, but availability + craft-action use a multi-source locale{sources={backpack, workbench}, sink=workbench}(crafting v0.2.0 API). Inputs are drained greedy in source-array order (backpack first, then workbench). Output lands in the Workbench, not the Backpack. - Workbench Inventory — workbench contents with Take + Inspect
actions on right-click. Take moves the item to the Backpack and fires
a
transferevent-toast.
- Workbench Crafting — recipes are still listed by
- Still works: C-key Backpack-Crafting (basic recipes still craftable
with Backpack-only via the bw-compat shim in crafting v0.2.0). The
Phase-C C-key callsite passes
state.backpackas a bare entity-handle; crafting v0.2.0's resolve_locale wraps that as{sources={backpack}, sink=backpack}transparently. - Caveat (panel-lib v0.1 single-active):
lib-core.panelv0.1.1 enforces one active widget. Opening both Workbench panels means only the second (workbench_inv) is visible until the player closes it (X); closing it revealsworkbench_crafting. Proper side-by-side stacking is a future panel-lib slice. Resolved in v0.13.0 via panel v0.2.0 multi-active + layout-slots. - Deps: crafting 0.1.0 → 0.2.0, crafting-display 0.1.0 → 0.2.0.
- Notify events-Channel: filter um
"transfer"erweitert, damit Take-Action-Toasts im events-channel landen. - Input binding:
"use"bound tomouse_right(mirrors the lib-core.input v0.3.0 unified key+mouse binding).
v0.11.0 — crafting integration
- New: stick pickup-item neben rock am Boden (Position 190,400 — 160px links
vom Backpack). Re-uses
slot_04_straightaus blob_rect_stone als Visual- Placeholder (Phase C-Q10). - New: 2 Recipes via
lib-core.crafting:- Stone Pick = 1 rock + 1 stick →
rock_pick - Rock Pile = 3 rocks →
rock_pile
- Stone Pick = 1 rock + 1 stick →
- New: C-Taste öffnet ein Crafting-Panel via
lib-core.crafting-display. Recipe-Zeilen sind dimmed, wenn inputs im Backpack fehlen. - New: Right-Click auf Recipe-Row → Context-Menu mit Craft + Inspect. Craft konsumiert inputs aus dem Backpack und legt das Output-Template als neues item-Entity in den Backpack. Inspect routet recipe details (name, description, inputs, output) zum notify-detail-channel.
- New: 3 item-templates —
stick,rock_pick,rock_pile— jedes mit player-facing properties (name, description, weight, volume, composition.). Placeholder atlas-slots: stick=slot_04_straight, rock_pick=slot_05_tee_open, rock_pile=slot_13_solid. - Deps: +lib-core.crafting 0.1.0, +lib-core.crafting-display 0.1.0. composition bleibt auf 0.3.0 (gebumpt in Phase C.0).
- Notify events-Channel: filter um
"craft"erweitert, damit Craft- Erfolgs- und Fehler-Toasts im events-channel landen.
v0.10.0 — notification system integration
- Inspect modal: Inspect action replaced with
notify.post{tags={"inspect"}, ...}routing to a modal detail-panel (notify-displaydetail-channel). Item name, description, weight, volume, and material composition are passed as structured data. Inspect no longer calls engine.print directly; the notify lib's engine-log mirror routes the event to engine.log for backward-compat with dev tools. - Pickup world-float: after
inventory.add, aworld_overlay.spawnfires a "+1 Rock" floating text anchored to the player (yellow, rises 24px, fades in 1.2s). Anotify.postsimultaneously emits a pickup event-toast and log entry. - Drop toast: the Drop action (both right-click menu and Q quick-drop) emits a
notify.post{tags={"drop","inventory"}}event-toast after the item is relocated. - Game-log panel: a catch-all log channel (
filter={}) captures every pickup, drop, and inspect event chronologically. L toggles the panel. - New deps: lib-core.notify v0.1.0, lib-core.notify-display v0.1.0, lib-core.world-overlay v0.1.0.
- Render order: camera.finish() → world_overlay.render(camera_handle) → panel.render() → notify_display.render() (toasts on top of all UI).
v0.9.1 — item-template player-facing properties
- Rock template enriched:
name,description,weight(kg),volume(L), andcomposition.stone = 1.0properties added. Other item templates can follow the same convention (optional with fallbacks). - Inspect prints structured info:
name,description,weight,volume, and acomposition: <material>=<value>line;reg_idparenthetical at the end. Future notification system will route this to an in-game text panel. - Module-side label_resolver override removed:
inventory-list-displayv0.1.1'sdefault_label_resolverreads thenameproperty; the hardcoded"Rock"override is obsolete.
v0.9.0 — inventory panel UI
- Panel overlay:
lib-core.panel+lib-core.inventory-list-displaywired in. Tab key toggles a panel overlay showing backpack contents. - Context menu actions: right-clicking an item row opens a context menu with
Drop (re-parents item to world at player position, re-registers pickup trigger)
and Inspect (prints
reg_idto console). - Q-drop unchanged: LIFO quick-drop via Q remains available in parallel to the panel-based drop action.
- Interaction guard:
interaction.updateis suppressed while the panel is open so world triggers don't fire during menu navigation.
v0.8.0 — end-to-end inventory demo functional
- Backpack container:
composition.define_template{id="backpack"}now includescontainer={kind="list"}. The composition v0.2 validator accepts this; the backpack can now receive items viainventory.add. - End-to-end pickup/drop: E on the rock calls
inventory.add(state.backpack, rock); inventory-list removes the "renderable" tag so the rock disappears from the world. Q callsinventory.remove, restores "renderable", relocates item to player position +30px right, and re-registers the pickup trigger. Full loop functional. - Inventory inspection trace: after every add or remove,
engine.printemitsvagrant: backpack count=N items=[...](unconditional; user-facing demo feedback). - Tree-check traces (CI-gated): after pickup,
vagrant: tree_check_after_pickup=okverifies item appears asitem.*child of the backpack in the composition tree. After drop,vagrant: tree_check_after_drop=okverifies noitem.*children remain. - Backpack interaction.register removed: the old print-only proximity trigger on the backpack sprite has been deleted. The rock's pickup trigger is the only interaction trigger registered.
v0.7.3
- Rock entity:
composition.define_template{id="rock"}added (stack_mode=individual, tags=renderable+item). One rock entity spawned at (270, 400) — 80px left of the backpack. Usesslot_00_isolatedUV from theblob_rect_stoneatlas (lib-asset.prototype-blob-geom).register_pickup(state.rock)wires an interaction trigger so pressing E near the rock fires the pickup callback.render.draw_entities{tag="renderable"}(existing Phase A render path) draws the rock without any special-case render code.
v0.4.4
- Sprint (Shift+WASD): doubles
WALK_SPEED(120 → 240) and walk-animspeed(0.45 → 0.9). Mid-walk sprint toggle usespuppet.set_play_speed(new v0.4.4 API) — no animation cycle restart. - Backpack interaction:
lib-core.interaction.register(350, 400, 40, "interact", callback)registered at init. Pressing E within range prints log message viaengine.printwith actor distance. - New deps: lib-core.interaction ^0.1.0. lib-core.puppet bumped to >=0.4.4.
v0.4.3
inherit_scale: falseon foot_l, foot_r: feet no longer Y-flip when leg.scl_y inverts during walk cycle. foot.rest.scl reset to [1,1].- foot procedural callback writes foot.rot to cancel leg.rot → foot.world.rot = body.world.rot.
v0.4.2
- Procedural orientation callback writes leg.rot + foot.rot (per-channel guard allows this alongside walk_fwd_lower which writes only leg.scl).
v0.4.1
- head look-at uses target_local normalization (-π, π]) to fix flick at body south-facing.
v0.4.0
- Multi-look-target: body tracks "soft", head tracks "aim" (both currently raw mouse).
- body.look_at_slerp = 6 (lazy rotation); head instant-snap with rot_min/max ±60°.
- Legs re-parented from root to body (hip-static body-relative positioning).
- Procedural "leg_foot_orientation" callback: leg faces walk direction, foot cancels leg rotation to stay body-aligned.
- walk_fwd_lower plays at speed = 0.45x for human-feel cadence.
- Rendering wrapped in camera.begin()/camera.finish() block.
v0.3.3
- sprite_rot applied to leg bones to compensate for sprite natural orientation.
v0.3.2
- Persistent look_at_state; fixes rate-limit reset each frame.
v0.3.1
- look-at atan math corrected: atan(dx, -dy) for sprite-top convention.
v0.3.0
- Sprite-mode rendering: 13-bone humanoid puppet with multi-channel animation.
- walk_fwd_lower (lower track): leg.scl_y cycle +1/0/-1 (substrate walk_fwd_lower).
- walk_upper (upper track): arm-swing animation.
- idle + idle_lower: upper body sway + leg collapse.
- Sprite-tilemap with 4 floor variants; hardcoded furniture.
v0.2.0
- Sprite-mode rendering throughout: 13-bone humanoid puppet via lib-core.puppet v0.2 with PROTOTYPE asset-pack textures; sprite-tilemap via lib-core.maps v0.1.2 with 4 floor variants; hardcoded furniture (bed, bench, backpack).