feat(vagrant): backpack as inventory container — end-to-end pickup/drop demo
- 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>
This commit is contained in:
78
README.md
78
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
|
slerp, head snaps to mouse with ±60° clamp). Demonstrates the Subterrain-style
|
||||||
puppet control model: leg orientation decoupled from body via procedural
|
puppet control model: leg orientation decoupled from body via procedural
|
||||||
callback, foot-body-orientation ("tactical twist"), scale-deformation walk
|
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
|
**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
|
**Tags:** test-chamber, puppet, walking-sim, interaction
|
||||||
|
|
||||||
## Topology
|
## Topology
|
||||||
@@ -22,15 +23,37 @@ cycle, and inheritance-decoupled foot sprites.
|
|||||||
- **W A S D** — direct movement (4-way, normalized for diagonals)
|
- **W A S D** — direct movement (4-way, normalized for diagonals)
|
||||||
- **Shift + WASD** — sprint (2x player speed + 2x walk-anim cadence)
|
- **Shift + WASD** — sprint (2x player speed + 2x walk-anim cadence)
|
||||||
- **Mouse** — body + head look-at targets (same position, different smoothing)
|
- **Mouse** — body + head look-at targets (same position, different smoothing)
|
||||||
- **E** — interact with nearest proximity-trigger in range (pickup nearby item into backpack)
|
- **E** — interact: if within range of the rock, picks it up into the backpack
|
||||||
- **Q** — drop most-recent item from backpack at player position + re-register pickup trigger
|
- **Q** — drop: places the most-recently-added backpack item at player position (+30px right)
|
||||||
- **ESC** — return to launcher
|
- **ESC** — return to launcher
|
||||||
|
|
||||||
> **Note:** The world now contains a rock (near the backpack, 80px to its left). Walk up
|
## Demo Walkthrough
|
||||||
> 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
|
The world contains three notable entities at startup:
|
||||||
> container — this confirms the pickup wiring is alive. Full pickup-into-backpack works
|
|
||||||
> once the backpack becomes a container (next slice).
|
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=[<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.
|
||||||
|
|
||||||
## Demonstrates
|
## Demonstrates
|
||||||
|
|
||||||
@@ -48,8 +71,10 @@ cycle, and inheritance-decoupled foot sprites.
|
|||||||
- Idle state → idle + idle_lower animations
|
- Idle state → idle + idle_lower animations
|
||||||
- Camera-chase via lib-core.camera target-provider
|
- Camera-chase via lib-core.camera target-provider
|
||||||
- Sprite-tilemap via lib-core.maps
|
- Sprite-tilemap via lib-core.maps
|
||||||
- lib-core.interaction proximity trigger on the backpack sprite at (350, 400),
|
- lib-core.interaction proximity trigger on the rock at (270, 400), range 40px,
|
||||||
range 40px, "interact" action bound to E — fires log on dispatch
|
"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
|
## 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
|
- 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
|
- 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
|
- 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;
|
- E pressed within 40px of the rock → fires `"interact"` action; rock moves into backpack;
|
||||||
callback logs `vagrant: interact with backpack (distance=X.X)`
|
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
|
## CI Hooks
|
||||||
|
|
||||||
@@ -82,15 +109,30 @@ cycle, and inheritance-decoupled foot sprites.
|
|||||||
|
|
||||||
## CHANGELOG
|
## 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
|
### v0.7.3
|
||||||
- **Rock entity**: `composition.define_template{id="rock"}` added (stack_mode=individual,
|
- **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.
|
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).
|
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
|
`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
|
fires the pickup callback. `render.draw_entities{tag="renderable"}` (existing Phase A
|
||||||
errors until the backpack becomes a container (next slice) — the error confirms the wiring
|
render path) draws the rock without any special-case render code.
|
||||||
is alive. `render.draw_entities{tag="renderable"}` (existing Phase A render path) draws
|
|
||||||
the rock without any special-case render code.
|
|
||||||
|
|
||||||
### v0.4.4
|
### v0.4.4
|
||||||
- **Sprint** (Shift+WASD): doubles `WALK_SPEED` (120 → 240) and walk-anim
|
- **Sprint** (Shift+WASD): doubles `WALK_SPEED` (120 → 240) and walk-anim
|
||||||
|
|||||||
81
init.lua
81
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 +
|
-- Sprite-mode test-chamber: 13-bone humanoid puppet + sprite-tilemap +
|
||||||
-- hardcoded furniture. Subterrain-style puppet control model:
|
-- hardcoded furniture. Subterrain-style puppet control model:
|
||||||
-- multi-look-target (aim/soft), slerp body rotation, hip-static legs,
|
-- multi-look-target (aim/soft), slerp body rotation, hip-static legs,
|
||||||
-- foot-body-orientation. Sprint (shift+wasd) + interaction (E near
|
-- 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
|
-- v0.6.0 (Phase A.4): Backpack promoted to composition-Entity (Atlas-UV
|
||||||
-- render-path via render.draw_entities{tag="renderable"}). Bed + Bench
|
-- 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.
|
-- via puppet.move_to. Movement-Logic moves the actor; puppet renders.
|
||||||
-- Untagged actor — render.draw_entities doesn't see it; puppet handles
|
-- Untagged actor — render.draw_entities doesn't see it; puppet handles
|
||||||
-- the render via puppet.render(state.player) unchanged.
|
-- 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 input = require("lib-core.input")
|
||||||
local camera = require("lib-core.camera")
|
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
|
-- Furniture positions (Bed/Bench remain hardcoded for v0.1; Backpack
|
||||||
-- promoted to Inactive-Entity via composition. Bed/Bench have no
|
-- promoted to Inactive-Entity via composition. Bed/Bench have no
|
||||||
-- interaction-trigger and no demo-pflicht to be entities — they stay
|
-- 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 BACKPACK_X, BACKPACK_Y = 350, 400
|
||||||
local INTERACTION_RANGE = 40 -- pixels
|
local INTERACTION_RANGE = 40 -- pixels
|
||||||
|
|
||||||
-- Backpack-Template (Phase A.4 — Atlas-UV render-path).
|
-- 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
|
-- Real sprite_atlas + sprite_uv values are filled in M.init after the
|
||||||
-- atlas-JSON is parsed; defaults here are placeholders so the inert-
|
-- atlas-JSON is parsed; defaults here are placeholders so the inert-
|
||||||
-- declaration types are correct (string + numbers).
|
-- declaration types are correct (string + numbers).
|
||||||
@@ -53,6 +62,7 @@ composition.define_template{
|
|||||||
position = {x = 0, y = 0},
|
position = {x = 0, y = 0},
|
||||||
},
|
},
|
||||||
tags = {"renderable"},
|
tags = {"renderable"},
|
||||||
|
container = { kind = "list" }, -- receives items via inventory.add
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Rock-Template: an item sitting in the world that the player can pick up.
|
-- 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
|
tags = {}, -- intentionally untagged: not renderable
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Item-Template-Convention (Phase B):
|
-- Item-Template-Convention:
|
||||||
-- An "item" is a composition-template with:
|
-- An "item" is a composition-template with:
|
||||||
-- - properties.stack_mode = "individual" (required by inventory-list)
|
-- - properties.stack_mode = "individual" (required by inventory-list)
|
||||||
-- - properties.sprite_atlas + sprite_uv (Atlas-UV render path)
|
-- - 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
|
-- re-parents it to the world at the player position, and re-registers a
|
||||||
-- pickup-trigger at the new location.
|
-- pickup-trigger at the new location.
|
||||||
--
|
--
|
||||||
-- Rock template (B.4) and container block on backpack (B.5) activate this
|
-- Rock template (v0.7.3) and container block on backpack (v0.8.0) activate this
|
||||||
-- convention end-to-end. B.3 establishes the glue only.
|
-- convention end-to-end. The pickup/drop glue (v0.7.2) sets up the wiring.
|
||||||
|
|
||||||
local state = {
|
local state = {
|
||||||
player = nil, -- puppet handle (state-bearing for animation)
|
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
|
-- When the player presses E within INTERACTION_RANGE, the item is moved
|
||||||
-- into state.backpack via inventory.add (renderable-tag flips off) and
|
-- into state.backpack via inventory.add (renderable-tag flips off) and
|
||||||
-- the trigger is unregistered so the item no longer shows as interactable.
|
-- 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 function register_pickup(item)
|
||||||
local x = item:get_property("position.x")
|
local x = item:get_property("position.x")
|
||||||
local y = item:get_property("position.y")
|
local y = item:get_property("position.y")
|
||||||
@@ -148,6 +158,27 @@ local function register_pickup(item)
|
|||||||
inventory.add(state.backpack, item)
|
inventory.add(state.backpack, item)
|
||||||
interaction.unregister(trigger_id)
|
interaction.unregister(trigger_id)
|
||||||
if CI_FRAME_PERF then engine.print("vagrant: event=pickup") end
|
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)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -368,18 +399,9 @@ function M.init(ctx)
|
|||||||
input.bind("drop", { "q" }) -- drop most-recent item from backpack
|
input.bind("drop", { "q" }) -- drop most-recent item from backpack
|
||||||
input.bind("quit_to_launcher", { "escape" })
|
input.bind("quit_to_launcher", { "escape" })
|
||||||
|
|
||||||
-- Proximity trigger on the backpack-Entity. Anchor read from the
|
-- Note: no backpack proximity-trigger registered here. The backpack is now
|
||||||
-- entity's position-Properties; sign is stationary so a snapshot is
|
-- a container; interaction is driven by the rock's register_pickup
|
||||||
-- correct (interaction-API takes raw numbers, not closures, in v0.1).
|
-- trigger. The old backpack-print callback has been removed.
|
||||||
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)
|
|
||||||
|
|
||||||
-- CI trace: assets loaded count (4 atlas textures: player + tiles + world + stone).
|
-- CI trace: assets loaded count (4 atlas textures: player + tiles + world + stone).
|
||||||
if CI_FRAME_PERF and not ci_assets_traced then
|
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)
|
item:set_property("position.y", py)
|
||||||
register_pickup(item)
|
register_pickup(item)
|
||||||
if CI_FRAME_PERF then engine.print("vagrant: event=drop") end
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"id": "vagrant-skeleton",
|
"id": "vagrant-skeleton",
|
||||||
"version": "0.7.3",
|
"version": "0.8.0",
|
||||||
"kind": "module",
|
"kind": "module",
|
||||||
"api": "^0.1",
|
"api": "^0.1",
|
||||||
"ci_frames": 60,
|
"ci_frames": 60,
|
||||||
|
|||||||
Reference in New Issue
Block a user