Files
sporel-module-vagrant-skeleton/README.md
calic f93fef5990 crafting: actor-precondition (requires) + reward (grants) — Skills half of Phase H (ADR-0056)
lib-core.crafting v0.4.0: two optional domain-free recipe fields.
- `requires`: a match-table evaluated against ctx.actor (not container items) — a hard gate. Failing -> {ok=false, error="requires_unmet", unmet={keys}}; fail-closed when ctx.actor is nil. Same predicate machinery as slots; distinct from is_known (discovery vs lock).
- `grants`: property->number reward, returned as `granted` from craft(); the consuming MODULE applies it to the actor (crafting stays container-scoped). Crafting knows no 'skill' vocabulary (ADR-0001).

vagrant-skeleton v0.21.0: player-actor carries skill.knapping (entity property, Phase-A lesson); knap_sharp_stone grants knapping XP, craft_stone_axe requires knapping>=3; module applies granted; skill-locked recipes show 'Skill too low'. Gate->earn->unlock loop headless-verified (axe locked @0 requires_unmet -> 3 knaps -> unlocks).

Quality-band reserved (Phase J): the requires threshold = the future quality floor (no re-authoring). Docs synced: ADR-0056, crafting-model.md (v0.4.0 + Actor-Precondition section), libraries.md, crafting README, vagrant README.
2026-07-31 10:00:27 +00:00

391 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.21.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.4.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.21.0:** **Skill-gating (crafting v0.4.0 / ADR-0056).** Recipes can now gate
> on and reward the ACTOR: the player-actor carries `skill.knapping` (an entity
> property), `knap_sharp_stone` **grants** knapping-XP, and `craft_stone_axe`
> **requires** `skill.knapping>=3`. The module applies the craft's returned
> `granted` to the actor. Demonstrates the gate→earn→unlock loop (gate downstream,
> grant upstream). Headless-verified: axe locked at skill 0 (`requires_unmet`) →
> 3 knaps → axe unlocks. Skill-locked recipes show "Skill too low" instead of a
> generic failure.
>
> **v0.20.0:** **`form` retired → `affordance` is the one capability axis**
> (ADR-0055 amendment). The `form` role-tag mixed geometry, identity and
> capability; it is gone. Its functional members folded into `affordance.*`
> (`blade→cutting`, `shaft→leverage`, plus `knappable` / `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 consumed `inputs` and non-consumed `tools`. 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 `form` tag, since folded
> into `affordance`. 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 **gates `knap_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.lua` into `content/stone_age.lua` — pure declarative
> data (`c.T{}` item, `c.R{}` recipe, `c.S{}` spawn), loaded via a content facade
> in `init.lua` (`engine.module.dir_of` + `dofile`, the sanctioned multi-file
> pattern). `c.T` auto-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 crafted `sharp_stone` rendering (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 BOTH `saw_planks` (as the non-consumed tool) AND
> `craft_stone_axe`. `twist_cordage` (3× plant_fiber → cordage) + the axe haft
> (`sharp_stone + stick + cordage → stone_axe`, carries `affordance.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.crafting` v0.3.0 (ADR-0055). New
> stone-age slice: a **Log** (heavy wood item) + a **Sharp Stone** (cutting
> tool) spawn in the world, and the `saw_planks` recipe matches its input by
> **property-constraint** (`composition.wood > 0.5` AND `weight > 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. Existing `rock_pick`/`rock_pile`
> recipes 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 hardcoded `draw_sprite_transform` calls for furniture are gone. First
> concrete consumer of ADR-0054 Item-Visual-Identity. The Subterrain dep
> remains for the puppet sprite parts (`subterrain_player` alias).
## Topology
<!-- topology:start (auto-generated; do not edit) -->
<!-- topology:end -->
## 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 <name>" 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:
1. **Player** — spawns at (320, 240), controlled via WASD + mouse.
2. **Backpack** — a visible sprite at (350, 400). Acts as a container (kind="list").
Not directly interactable; it receives items via inventory.add.
3. **Rock** — a standalone stone tile at (270, 400), 80px to the left of the backpack.
Tagged "renderable" + "item"; starts visible on the ground.
4. **Stick** — a placeholder pickup-item at (190, 400), 160px to the left of the backpack.
Re-uses the `slot_04_straight` blob_rect_stone tile for its visual (Phase C-Q10
pragmatic resolution — echtes Stick-Sprite folgt mit dem ersten atlas-baker-decals-Slice).
5. **Workbench** — a crafting bench at (350, 380), just above the backpack. Container
(kind="list"); placeholder visual re-uses the `slot_07_tee_full` blob_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 <item>" 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: false` on 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_speed` doubles 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 prints `vagrant: 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. Emits `vagrant: frame_avg_ms=N`
and `vagrant: 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_template`
with `container = {kind="list"}`, tagged `renderable`, `container`, `workbench`,
`interactable`. Placeholder visual re-uses the `slot_07_tee_full`
blob_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 `transfer` event-toast.
- **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.backpack` as 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.panel` v0.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 reveals `workbench_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 to `mouse_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_straight` aus 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`
- **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.<material>). 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-display` detail-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`, a `world_overlay.spawn` fires a "+1 Rock"
floating text anchored to the player (yellow, rises 24px, fades in 1.2s). A `notify.post`
simultaneously 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), and `composition.stone = 1.0` properties added. Other item
templates can follow the same convention (optional with fallbacks).
- **Inspect prints structured info**: `name`, `description`, `weight`,
`volume`, and a `composition: <material>=<value>` line; `reg_id`
parenthetical at the end. Future notification system will route this
to an in-game text panel.
- **Module-side label_resolver override removed**: `inventory-list-display`
v0.1.1's `default_label_resolver` reads the `name` property; the
hardcoded `"Rock"` override is obsolete.
### v0.9.0 — inventory panel UI
- **Panel overlay**: `lib-core.panel` + `lib-core.inventory-list-display` wired 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_id` to console).
- **Q-drop unchanged**: LIFO quick-drop via Q remains available in parallel to the
panel-based drop action.
- **Interaction guard**: `interaction.update` is 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 includes
`container={kind="list"}`. The composition v0.2 validator accepts this; the backpack
can now receive items via `inventory.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 calls `inventory.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.print` emits
`vagrant: backpack count=N items=[...]` (unconditional; user-facing demo feedback).
- **Tree-check traces (CI-gated)**: after pickup, `vagrant: tree_check_after_pickup=ok`
verifies item appears as `item.*` child of the backpack in the composition tree.
After drop, `vagrant: tree_check_after_drop=ok` verifies no `item.*` 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.
Uses `slot_00_isolated` UV from the `blob_rect_stone` atlas (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-anim
`speed` (0.45 → 0.9). Mid-walk sprint toggle uses `puppet.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 via `engine.print` with actor distance.
- New deps: lib-core.interaction ^0.1.0. lib-core.puppet bumped to >=0.4.4.
### v0.4.3
- `inherit_scale: false` on 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).