From 644bfe98fdc8f6b364e574044aadeb5486a30dc4 Mon Sep 17 00:00:00 2001 From: Axel Meyer Date: Sun, 14 Jun 2026 20:04:04 +0200 Subject: [PATCH] feat: v0.12.0 Workbench-Integration Workbench-Entity bei (350, 380) als Crafting-Context-Erweiterung. Right-Click oeffnet Workbench-Crafting-Panel + Workbench-Inventory. Crafting nutzt Multi-Source-Locale {backpack, workbench} mit sink = workbench; Take-Action verschiebt vom Workbench-Inventory zum Backpack. C-Key Backpack-Crafting bleibt unveraendert via Bw- Compat-Shim in crafting v0.2.0. Deps: crafting 0.1.0 -> 0.2.0, crafting-display 0.1.0 -> 0.2.0. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 65 ++++++++++-- init.lua | 256 +++++++++++++++++++++++++++++++++++++++++++++++- manifest.module | 6 +- 3 files changed, 314 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index c60ceca..7d64595 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ 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.11.0 +**Version:** 0.12.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-core.composition >=0.3.0, lib-core.actor >=0.1.0, lib-core.inventory-list >=0.1.0, lib-core.panel >=0.1.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.1.0, lib-core.crafting-display >=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.3.0, lib-core.actor >=0.1.0, lib-core.inventory-list >=0.1.0, lib-core.panel >=0.1.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.2.0, lib-core.crafting-display >=0.2.0, lib-asset.prototype-subterrain >=0.1.0 **Tags:** test-chamber, puppet, walking-sim, interaction ## Topology @@ -26,19 +26,21 @@ demo: pick up a rock into a backpack container, drop it back into the world. - **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 Crafting panel (lists known recipes; availability dimmed by inventory) -- **Right-Click on item row** (inventory panel): context menu with **Drop** / **Inspect** +- **C** — toggle Backpack-Crafting panel (lists known recipes; availability dimmed by inventory) +- **Right-Click on Workbench** (within range) — opens Workbench-Crafting + Workbench-Inventory panels (Phase D v0.2) +- **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 backpack, adds output, posts a success/failure toast -- **ESC** — return to launcher +- **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 four notable entities at startup: +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"). @@ -48,6 +50,9 @@ The world contains four notable entities at startup: 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). @@ -67,6 +72,21 @@ The world contains four notable entities at startup: **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):** +- 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. +- Two panels open simultaneously (Workbench-Crafting + Workbench-Inventory). + Because `lib-core.panel` v0.1 is single-active, only one panel is visible + at a time — closing it (X) reveals the other. +- 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). +- Open Workbench-Inventory, right-click on the crafted item → **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: @@ -121,6 +141,37 @@ Only one rock is spawned per session. ## CHANGELOG +### 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. +- **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 diff --git a/init.lua b/init.lua index bae956b..db4688e 100644 --- a/init.lua +++ b/init.lua @@ -1,4 +1,4 @@ --- sporel-module-vagrant-skeleton v0.11.0 +-- sporel-module-vagrant-skeleton v0.12.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, @@ -49,6 +49,17 @@ -- Right-click a recipe row → Craft + Inspect actions; Craft consumes -- inputs from the backpack and adds the output; Inspect routes -- recipe details to a notify-display detail-channel. +-- +-- v0.12.0: Workbench-Entity added at (350, 380). Right-Click on the +-- workbench opens a Workbench-Crafting panel + Workbench-Inventory +-- panel. The crafting panel uses a multi-source locale {backpack, +-- workbench} via crafting v0.2.0's locale-factory API; output lands +-- in the workbench. Take-action in workbench-inv moves an item back +-- to the backpack. C-key Backpack-Crafting remains via the bw-compat +-- shim in crafting v0.2.0 (Form-1 bare entity-handle stays valid). +-- Panel-lib v0.1 is single-active so opening both panels lets +-- workbench_inv win; user can close it (X) to reveal workbench_crafting +-- below — Gate-2 walkthrough documents the flow. local input = require("lib-core.input") local camera = require("lib-core.camera") @@ -196,7 +207,10 @@ local state = { backpack = nil, -- composition-entity, created in M.init after atlas load rock = nil, -- composition-entity (item), created in M.init after blob_rect_stone load stick = nil, -- composition-entity (item), placeholder for crafting demo - crafting_widget = nil, -- crafting-display widget handle + workbench = nil, -- composition-entity (container), Phase D Workbench + crafting_widget = nil, -- crafting-display widget handle (Backpack) + workbench_crafting_widget = nil, -- crafting-display widget handle (Workbench-context) + workbench_inv_widget = nil, -- inventory-list-display widget handle (Workbench-Inv) -- Subterrain-style controller state: walk_anim_current = nil, -- "walk_fwd_lower" / "walk_back_lower" / etc. (nil = idle_lower) lower_control_rot_deg = 0, @@ -276,6 +290,59 @@ local function register_pickup(item) end) end +-- register_interaction_use(entity) +-- Phase D Workbench: registers a right-click ("use") interaction-trigger at +-- the entity's current world position. When the player fires "use" within +-- INTERACTION_RANGE, the callback opens both the Workbench-Crafting and +-- Workbench-Inventory panels. +-- +-- lib-core.interaction v0.1.0 is XY-based (not entity-bound); the trigger +-- snapshots the position at registration time. Workbench is static in v0.12 +-- so a single registration suffices. Action-name "use" is bound to +-- mouse_right in M.init's input.bind block. +-- +-- Note on dual-panel-open: lib-core.panel v0.1.1 enforces a single active +-- widget. Calling panel.open twice means the second call wins +-- (workbench_inv shows on top; workbench_crafting is hidden until the +-- player closes the inv panel via X). This matches the spec §4.4 note that +-- "v0.2 default ist whatever panel-lib default" — proper side-by-side +-- layout is a panel-lib v0.2 task, out of scope for D.2. +local function register_interaction_use(entity) + local x = entity:get_property("position.x") + local y = entity:get_property("position.y") + interaction.register(x, y, INTERACTION_RANGE, "use", function() + panel.open("workbench_crafting") + panel.open("workbench_inv") + if CI_FRAME_PERF then engine.print("vagrant: event=workbench_use") end + end) +end + +-- discover_sources(workbench) → array of container entities +-- Phase D v0.2 source-discovery policy: Player-Backpack is always a source +-- (independent of workbench position; player carries it everywhere), and +-- the workbench itself is always a source. Forward-compat stub below +-- documents the intended adjacent-container scan (deferred to a future +-- slice with multi-container demos). +local function discover_sources(workbench) + local sources = { state.backpack, workbench } + -- Forward-compat stub for adjacent-container scan (deferred): + -- for _, ent in ipairs(composition.list_by_tag("container")) do + -- if ent ~= workbench and ent ~= state.backpack then + -- local wx = workbench:get_property("position.x") + -- local wy = workbench:get_property("position.y") + -- local ex = ent:get_property("position.x") + -- local ey = ent:get_property("position.y") + -- if ex and ey and wx and wy then + -- local dx, dy = math.abs(ex - wx), math.abs(ey - wy) + -- if math.max(dx, dy) <= 1 then -- Chebyshev radius 1 + -- sources[#sources + 1] = ent + -- end + -- end + -- end + -- end + return sources +end + -- Signed angle between two 2D vectors (in degrees, range [-180, 180]). -- Sporel uses Y-down screen-coords. Substrate's math assumes Y-up; the cross -- component is flipped so "positive num = move-direction is CCW from body-fwd @@ -511,6 +578,34 @@ function M.init(ctx) tags = {"renderable", "item"}, } + -- Workbench-Template (Phase D v0.2). Container-kind=list so it can + -- accept items via inventory.add (crafting output landing spot). + -- Placeholder visual re-uses the blob_rect_stone atlas with + -- slot_07_tee_full — visually distinct from the slots already taken + -- (00=rock, 04=stick, 05=rock_pick, 13=rock_pile). slot_07 is a + -- filled T-junction shape — dense + asymmetric, reads as a sturdy + -- piece of furniture against the rest of the in-world items. A + -- proper sprite would come from atlas-baker-decals in a future slice. + local workbench_uv = stone_uvs["slot_07_tee_full"] + composition.define_template{ + id = "workbench", + properties = { + stack_mode = "individual", + name = "Workbench", + description = "A crafting bench.", + weight = 50, + volume = 100, + ["composition.wood"] = 0.7, + ["composition.stone"] = 0.3, + sprite_atlas = stone_atlas_png, + sprite_uv = {x = workbench_uv.x, y = workbench_uv.y, + w = workbench_uv.w, h = workbench_uv.h}, + position = {x = 0, y = 0}, + }, + container = { kind = "list" }, + tags = { "renderable", "container", "workbench", "interactable" }, + } + -- Spawn Rock-Entity 80px to the left of the backpack so it sits in a -- distinct position: visible to the player on first load, not overlapping -- the backpack, reachable by walking left from spawn. @@ -546,6 +641,18 @@ function M.init(ctx) register_pickup(state.stick) if CI_FRAME_PERF then engine.print("vagrant: stick_spawned") end + -- Workbench spawn (Phase D v0.2). Placed at (350, 380) — 20px above + -- the backpack (at 350, 400) so they are visually distinct but still + -- on the visible-on-spawn area. register_interaction_use registers a + -- right-click ("use") proximity-trigger at the workbench position. + state.workbench = composition.create{ + template = "workbench", + properties = { + position = {x = 350, y = 380}, + }, + } + if CI_FRAME_PERF then engine.print("vagrant: workbench_spawned") end + -- ====== Crafting Recipes ====== crafting.define_recipe{ id = "rock_pick", @@ -692,12 +799,154 @@ function M.init(ctx) panel.register("crafting", state.crafting_widget) panel.bind_default_trigger("c", "crafting") + -- ====== Workbench Crafting Widget (Phase D v0.2) ====== + -- Locale-Factory returns Form-2 locale {sources, sink}: sources come + -- from discover_sources (player-backpack + workbench), sink is the + -- workbench. Crafting v0.2.0 greedy-drains sources in array-order + -- (backpack first, then workbench) before adding the output to sink. + state.workbench_crafting_widget = crafting_display.create( + function() + return { + sources = discover_sources(state.workbench), + sink = state.workbench, + } + end, + { + title = "Workbench Crafting", + widget_id = "workbench_crafting", + pause_on_open = false, + ctx_factory = function() + return { actor = state.actor, at_workbench = true } + end, + }) + + -- Craft action: invokes crafting.craft on the resolved locale (multi- + -- source), posts a success or warning toast. ctx_inner.locale carries + -- the per-frame-resolved Form-2 locale; we forward it verbatim. + crafting_display.register_action(state.workbench_crafting_widget, + "Craft", function(recipe_id, ctx_inner) + local r = crafting.craft(recipe_id, ctx_inner.locale, + { actor = state.actor, at_workbench = true }) + if r.ok then + local recipe = crafting.get_recipe(recipe_id) + notify.post{ + tags = {"craft", "event"}, + text = "Crafted " .. (recipe.name or recipe_id), + severity = "info", + } + if CI_FRAME_PERF then + engine.print("vagrant: event=craft_ok recipe=" .. recipe_id + .. " at_workbench=1") + end + else + local msg + if r.error == "missing_inputs" then + msg = "Missing inputs" + else + msg = "Cannot craft: " .. tostring(r.error) + end + notify.warn({"craft", "event"}, msg) + if CI_FRAME_PERF then + engine.print("vagrant: event=craft_fail recipe=" .. recipe_id + .. " error=" .. tostring(r.error) + .. " at_workbench=1") + end + end + ctx_inner.close_menu() + end) + + -- Inspect action: routes recipe details to the notify detail-channel + -- (same pattern as backpack-crafting Inspect). + crafting_display.register_action(state.workbench_crafting_widget, + "Inspect", function(recipe_id, ctx_inner) + local recipe = crafting.get_recipe(recipe_id) + local lines = { + recipe.name or recipe.id, + "", + recipe.description or "", + "", + "Inputs:", + } + for _, inp in ipairs(recipe.inputs) do + lines[#lines + 1] = " " .. inp.template + .. " \xc3\x97 " .. inp.count + end + lines[#lines + 1] = "" + lines[#lines + 1] = "Output: " .. recipe.output.template + .. " \xc3\x97 " .. recipe.output.count + notify.post{ + tags = {"inspect", "detail"}, + text = table.concat(lines, "\n"), + severity = "info", + } + ctx_inner.close_menu() + end) + + panel.register("workbench_crafting", state.workbench_crafting_widget) + -- KEIN bind_default_trigger: trigger is the workbench right-click via + -- register_interaction_use(state.workbench) below. + + -- ====== Workbench Inventory Widget (Phase D v0.2) ====== + state.workbench_inv_widget = inv_display.create(state.workbench, { + title = "Workbench Inventory", + widget_id = "workbench_inv", + pause_on_open = false, + }) + + -- Take action: moves an item from workbench → backpack via + -- inventory.remove + inventory.add. Posts a transfer-event toast. + inv_display.register_action(state.workbench_inv_widget, "Take", + function(item, c) + inventory.remove(state.workbench, item) + inventory.add(state.backpack, item) + notify.post{ + tags = {"transfer", "event"}, + text = "Took " .. (item:get_property("name") + or composition.template_of(item) or "item"), + severity = "info", + } + c.close_menu() + if CI_FRAME_PERF then engine.print("vagrant: event=take") end + if c.refresh then c.refresh() end + end) + + -- Inspect action: mirrors the backpack-inv Inspect so that workbench + -- contents have the same detail-modal payload (name + description + + -- weight + volume + composition.* materials). + inv_display.register_action(state.workbench_inv_widget, "Inspect", + function(item, c) + local props = item:get_properties() + local comp = {} + for k, v in pairs(props) do + local mat = k:match("^composition%.(.+)$") + if mat and mat ~= "reg_id" then comp[mat] = v end + end + notify.post{ + tags = {"inspect"}, + text = item:get_property("name") or "(unnamed)", + data = { + description = item:get_property("description"), + weight = item:get_property("weight"), + volume = item:get_property("volume"), + composition = comp, + }, + } + c.close_menu() + end) + + panel.register("workbench_inv", state.workbench_inv_widget) + + -- Right-click trigger registered AFTER both workbench widgets are + -- panel.register'd; the callback panel.open()s them by id, so they + -- must already be in the panel registry. + register_interaction_use(state.workbench) + -- ====== Notification System Setup ====== -- Channels (created BEFORE display attach + widget register) notify.create_channel{ id = "inspect", filter = {"inspect"}, display_mode = "detail" } - notify.create_channel{ id = "events", filter = {"pickup", "drop", "craft"}, + notify.create_channel{ id = "events", filter = {"pickup", "drop", "craft", "transfer"}, display_mode = "toast" } notify.create_channel{ id = "log", filter = {}, -- catch-all display_mode = "log" } @@ -726,6 +975,7 @@ function M.init(ctx) input.bind("walk_down", { "s" }) input.bind("sprint", { "shift" }) -- held while walking → 2x speed input.bind("interact", { "e" }) -- fires nearest proximity-trigger + input.bind("use", { "mouse_right" }) -- Phase D right-click "use" trigger (workbench) input.bind("drop", { "q" }) -- drop most-recent item from backpack input.bind("quit_to_launcher", { "escape" }) diff --git a/manifest.module b/manifest.module index a8319d4..49129bf 100644 --- a/manifest.module +++ b/manifest.module @@ -1,6 +1,6 @@ { "id": "vagrant-skeleton", - "version": "0.11.0", + "version": "0.12.0", "kind": "module", "api": "^0.1", "ci_frames": 60, @@ -26,8 +26,8 @@ {"id": "lib-core.puppet", "version": "0.5.0"}, {"id": "lib-core.interaction", "version": "0.1.0"}, {"id": "lib-core.inventory-list", "version": "0.1.0"}, - {"id": "lib-core.crafting", "version": "0.1.0"}, - {"id": "lib-core.crafting-display", "version": "0.1.0"}, + {"id": "lib-core.crafting", "version": "0.2.0"}, + {"id": "lib-core.crafting-display", "version": "0.2.0"}, {"id": "lib-asset.prototype-subterrain", "version": "0.2.0"}, {"id": "lib-asset.prototype-blob-geom", "version": "0.1.0"} ]