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) <noreply@anthropic.com>
This commit is contained in:
256
init.lua
256
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" })
|
||||
|
||||
|
||||
Reference in New Issue
Block a user