From e38d396b87732b5a0b2db5876bd72bf211984103 Mon Sep 17 00:00:00 2001 From: Axel Meyer Date: Sat, 13 Jun 2026 18:52:39 +0200 Subject: [PATCH] =?UTF-8?q?feat(vagrant):=20backpack=20as=20inventory=20co?= =?UTF-8?q?ntainer=20=E2=80=94=20end-to-end=20pickup/drop=20demo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- README.md | 78 ++++++++++++++++++++++++++++++++++++----------- init.lua | 81 +++++++++++++++++++++++++++++++++++++------------ manifest.module | 2 +- 3 files changed, 123 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 1ca5182..da2289c 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,12 @@ 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. +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.7.3 +**Version:** 0.8.0 **Module-ID:** vagrant-skeleton -**Requires:** lib-core.input >=0.4.0, lib-core.camera >=0.3.0, lib-core.render >=0.1.0, lib-core.maps >=0.1.2, lib-core.puppet >=0.4.4, lib-core.interaction >=0.1.0, lib-asset.prototype-subterrain >=0.1.0 +**Requires:** lib-core.input >=0.4.0, lib-core.camera >=0.3.0, lib-core.render >=0.1.0, lib-core.maps >=0.1.2, lib-core.puppet >=0.4.4, lib-core.interaction >=0.1.0, lib-core.composition >=0.2.0, lib-core.inventory-list >=0.1.0, lib-asset.prototype-subterrain >=0.1.0 **Tags:** test-chamber, puppet, walking-sim, interaction ## Topology @@ -22,15 +23,37 @@ cycle, and inheritance-decoupled foot sprites. - **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 with nearest proximity-trigger in range (pickup nearby item into backpack) -- **Q** — drop most-recent item from backpack at player position + re-register pickup trigger +- **E** — interact: if within range of the rock, picks it up into the backpack +- **Q** — drop: places the most-recently-added backpack item at player position (+30px right) - **ESC** — return to launcher -> **Note:** The world now contains a rock (near the backpack, 80px to its left). Walk up -> and press E to pick it up into the backpack; press Q to drop it back at your feet. -> Picking up the rock currently throws a loud error because the backpack is not yet a -> container — this confirms the pickup wiring is alive. Full pickup-into-backpack works -> once the backpack becomes a container (next slice). +## Demo Walkthrough + +The world contains three 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. + +**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=[]` + +**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. ## Demonstrates @@ -48,8 +71,10 @@ cycle, and inheritance-decoupled foot sprites. - 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 backpack sprite at (350, 400), - range 40px, "interact" action bound to E — fires log on dispatch +- 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 @@ -58,8 +83,10 @@ cycle, and inheritance-decoupled foot sprites. - 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 backpack sprite → fires `"interact"` action; - callback logs `vagrant: interact with backpack (distance=X.X)` +- 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 @@ -82,15 +109,30 @@ cycle, and inheritance-decoupled foot sprites. ## CHANGELOG +### 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. The callback calls `inventory.add(state.backpack, …)` which - errors until the backpack becomes a container (next slice) — the error confirms the wiring - is alive. `render.draw_entities{tag="renderable"}` (existing Phase A render path) draws - the rock without any special-case render code. + 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 diff --git a/init.lua b/init.lua index 302c292..e9d1b93 100644 --- a/init.lua +++ b/init.lua @@ -1,9 +1,9 @@ --- sporel-module-vagrant-skeleton v0.7.3 +-- sporel-module-vagrant-skeleton v0.8.0 -- Sprite-mode test-chamber: 13-bone humanoid puppet + sprite-tilemap + -- hardcoded furniture. 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 --- backpack) added v0.4.4. +-- pickup-item) added v0.4.4. -- -- v0.6.0 (Phase A.4): Backpack promoted to composition-Entity (Atlas-UV -- render-path via render.draw_entities{tag="renderable"}). Bed + Bench @@ -15,6 +15,13 @@ -- via puppet.move_to. Movement-Logic moves the actor; puppet renders. -- Untagged actor — render.draw_entities doesn't see it; puppet handles -- the render via puppet.render(state.player) unchanged. +-- +-- v0.8.0: Backpack template gains container={kind="list"}. +-- End-to-end pickup/drop loop: E on rock → inventory.add(backpack, rock) +-- → rock disappears from draw_entities; Q → rock reappears at player +-- position. engine.print shows inventory count + item-IDs after each +-- add/remove. Tree-check (CI-gated) verifies child placement in +-- composition tree. local input = require("lib-core.input") local camera = require("lib-core.camera") @@ -37,11 +44,13 @@ 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 Phase B (container-on-bed) or similar triggers). +-- atlas-furniture until a container-on-bed feature or similar triggers). local BACKPACK_X, BACKPACK_Y = 350, 400 local INTERACTION_RANGE = 40 -- pixels -- 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). -- 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). @@ -53,6 +62,7 @@ composition.define_template{ position = {x = 0, y = 0}, }, tags = {"renderable"}, + container = { kind = "list" }, -- receives items via inventory.add } -- Rock-Template: an item sitting in the world that the player can pick up. @@ -85,7 +95,7 @@ composition.define_template{ tags = {}, -- intentionally untagged: not renderable } --- Item-Template-Convention (Phase B): +-- Item-Template-Convention: -- An "item" is a composition-template with: -- - properties.stack_mode = "individual" (required by inventory-list) -- - properties.sprite_atlas + sprite_uv (Atlas-UV render path) @@ -101,8 +111,8 @@ composition.define_template{ -- re-parents it to the world at the player position, and re-registers a -- pickup-trigger at the new location. -- --- Rock template (B.4) and container block on backpack (B.5) activate this --- convention end-to-end. B.3 establishes the glue only. +-- Rock template (v0.7.3) and container block on backpack (v0.8.0) activate this +-- convention end-to-end. The pickup/drop glue (v0.7.2) sets up the wiring. local state = { player = nil, -- puppet handle (state-bearing for animation) @@ -138,7 +148,7 @@ local WALK_ANIMS = {"walk_fwd_lower", "walk_back_lower", "walk_left_lower", "wal -- When the player presses E within INTERACTION_RANGE, the item is moved -- into state.backpack via inventory.add (renderable-tag flips off) and -- the trigger is unregistered so the item no longer shows as interactable. --- Called by B.4 after placing the rock, and by drop-glue after relocation. +-- Called after placing the rock, and by drop-glue after relocation. local function register_pickup(item) local x = item:get_property("position.x") local y = item:get_property("position.y") @@ -148,6 +158,27 @@ local function register_pickup(item) inventory.add(state.backpack, item) interaction.unregister(trigger_id) if CI_FRAME_PERF then engine.print("vagrant: event=pickup") end + + -- Inventory inspection trace (unconditional — user-facing demo output). + local count = inventory.count(state.backpack) + local items = inventory.contents(state.backpack) + local ids = {} + for _, it in ipairs(items) do + table.insert(ids, tostring(it:get_property("composition.reg_id"))) + end + engine.print(string.format("vagrant: backpack count=%d items=[%s]", + count, table.concat(ids, ","))) + + -- Tree-check (CI-gated): verify the item was actually placed as a + -- child of the backpack entity in the composition tree. + if CI_FRAME_PERF then + local found = false + for slot_name, _ in pairs(state.backpack:get_children()) do + if slot_name:match("^item%.%d+$") then found = true; break end + end + engine.print(string.format("vagrant: tree_check_after_pickup=%s", + found and "ok" or "MISSING")) + end end) end @@ -368,18 +399,9 @@ function M.init(ctx) input.bind("drop", { "q" }) -- drop most-recent item from backpack input.bind("quit_to_launcher", { "escape" }) - -- Proximity trigger on the backpack-Entity. Anchor read from the - -- entity's position-Properties; sign is stationary so a snapshot is - -- correct (interaction-API takes raw numbers, not closures, in v0.1). - interaction.register( - state.backpack:get_property("position.x"), - state.backpack:get_property("position.y"), - INTERACTION_RANGE, "interact", - function(ctx) - engine.print(string.format( - "vagrant: interact with backpack (distance=%.1f)", - ctx.distance)) - end) + -- Note: no backpack proximity-trigger registered here. The backpack is now + -- a container; interaction is driven by the rock's register_pickup + -- trigger. The old backpack-print callback has been removed. -- CI trace: assets loaded count (4 atlas textures: player + tiles + world + stone). if CI_FRAME_PERF and not ci_assets_traced then @@ -411,6 +433,27 @@ function M.update(ctx, dt) item:set_property("position.y", py) register_pickup(item) if CI_FRAME_PERF then engine.print("vagrant: event=drop") end + + -- Inventory inspection trace (unconditional — user-facing demo output). + local count = inventory.count(state.backpack) + local items = inventory.contents(state.backpack) + local ids = {} + for _, it in ipairs(items) do + table.insert(ids, tostring(it:get_property("composition.reg_id"))) + end + engine.print(string.format("vagrant: backpack count=%d items=[%s]", + count, table.concat(ids, ","))) + + -- Tree-check (CI-gated): verify the backpack has no item.* children + -- after drop (item was re-parented to world). + if CI_FRAME_PERF then + local found = false + for slot_name, _ in pairs(state.backpack:get_children()) do + if slot_name:match("^item%.%d+$") then found = true; break end + end + engine.print(string.format("vagrant: tree_check_after_drop=%s", + found and "MISSING" or "ok")) + end end end diff --git a/manifest.module b/manifest.module index 29359a4..a1b3620 100644 --- a/manifest.module +++ b/manifest.module @@ -1,6 +1,6 @@ { "id": "vagrant-skeleton", - "version": "0.7.3", + "version": "0.8.0", "kind": "module", "api": "^0.1", "ci_frames": 60,