feat: lib-core.crafting v0.2.0 — Multi-Source / Single-Sink Locale-API
Extends can_craft + craft to take a `locale` parameter that describes
WHERE inputs come from and WHERE outputs go. Two forms are accepted;
the v0.1 single-container call signature continues to work unchanged
via a transparent bw-compat shim.
Locale-Schema:
Form 1 (bw-compat): a bare container-entity-handle. Internally
wrapped to {sources={h}, sink=h}; the single
container is both the sole input source AND the
output sink. v0.1 call sites need no migration.
Form 2 (explicit): { sources = {c1, c2, ...}, sink = c_out } where
sources is a non-empty array of container-entity-
handles and sink is a single container-entity-
handle. sink MAY appear in sources (e.g. workbench
buffer as both source and sink).
can_craft aggregates input counts across the union of all sources
before comparing against the recipe's needs. `missing` entries report
the aggregated `have` count across sources.
craft greedy-drains in array-order: for each input.need it walks
sources left-to-right, fully draining matching-template items from
the first source before moving on to the next. The order is stable
and deterministic so callers can encode priority via the array.
A file-local resolve_locale helper centralizes the Form-1-vs-Form-2
dispatch and validates Form 2 with loud errors:
locale == nil → "locale must not be nil"
Form 2, sources not array or empty → "sources must be non-empty array"
Form 2, sink == nil → "sink must not be nil"
The count_by_template helper now takes a sources array and walks each
container's inventory in turn, summing per-template counts.
Manifest bumped 0.1.0 → 0.2.0. Deps unchanged (composition 0.3.0,
inventory-list 0.1.0). README documents both locale forms, the
greedy-drain order, the bw-compat guarantee, and the loud-error
conditions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
91
init.lua
91
init.lua
@@ -1,14 +1,22 @@
|
||||
-- =====================================================================
|
||||
-- lib-core.crafting v0.1.0 — Recipe Registry + Craft Action
|
||||
-- Spec: meta/docs/superpowers/specs/2026-06-14-phase-C-crafting-design.md
|
||||
-- lib-core.crafting v0.2.0 — Recipe Registry + Craft Action
|
||||
-- Spec: meta/docs/superpowers/specs/2026-06-14-phase-D-workbench-design.md
|
||||
--
|
||||
-- Surface:
|
||||
-- crafting.define_recipe(recipe_def) -- register a recipe
|
||||
-- crafting.list_recipes() -> {recipe_def, ...}
|
||||
-- crafting.get_recipe(id) -> recipe_def or nil
|
||||
-- crafting.is_known(recipe_id, ctx) -> bool
|
||||
-- crafting.can_craft(recipe_id, container, ctx) -> result (non-mutating)
|
||||
-- crafting.craft(recipe_id, container, ctx) -> result (mutating)
|
||||
-- crafting.can_craft(recipe_id, locale, ctx) -> result (non-mutating)
|
||||
-- crafting.craft(recipe_id, locale, ctx) -> result (mutating)
|
||||
--
|
||||
-- Locale-Param (v0.2.0):
|
||||
-- Form 1 (bw-compat): a bare entity_handle. Treated as both the sole
|
||||
-- source AND the sink.
|
||||
-- Form 2 (explicit): a table { sources = {c1, c2, ...}, sink = c_out }
|
||||
-- where `sources` is a non-empty array of
|
||||
-- container-entity-handles and `sink` is a single
|
||||
-- container-entity-handle.
|
||||
--
|
||||
-- Recipe-Schema:
|
||||
-- {
|
||||
@@ -68,12 +76,40 @@ local function shallow_copy(t)
|
||||
return out
|
||||
end
|
||||
|
||||
local function count_by_template(container)
|
||||
-- Locale-Resolver: akzeptiert entity_handle (bw-compat) ODER table
|
||||
-- {sources={...}, sink=...}. Returns immer Form-2 mit validierten
|
||||
-- Feldern. Loud-Error bei kaputter Form-2.
|
||||
local function resolve_locale(locale, fn_name)
|
||||
-- Form 2: explicit table
|
||||
if type(locale) == "table" and locale.sources ~= nil then
|
||||
if type(locale.sources) ~= "table" or #locale.sources == 0 then
|
||||
error(string.format(
|
||||
"crafting.%s: locale.sources must be non-empty array",
|
||||
fn_name), 3)
|
||||
end
|
||||
if locale.sink == nil then
|
||||
error(string.format(
|
||||
"crafting.%s: locale.sink must not be nil",
|
||||
fn_name), 3)
|
||||
end
|
||||
return locale
|
||||
end
|
||||
-- Form 1 (bw-compat): bare entity_handle
|
||||
if locale == nil then
|
||||
error(string.format(
|
||||
"crafting.%s: locale must not be nil", fn_name), 3)
|
||||
end
|
||||
return { sources = {locale}, sink = locale }
|
||||
end
|
||||
|
||||
local function count_by_template(sources)
|
||||
local out = {}
|
||||
for _, ent in ipairs(inv.contents(container)) do
|
||||
local tpl = composition.template_of(ent)
|
||||
if tpl ~= nil then
|
||||
out[tpl] = (out[tpl] or 0) + 1
|
||||
for _, source in ipairs(sources) do
|
||||
for _, ent in ipairs(inv.contents(source)) do
|
||||
local tpl = composition.template_of(ent)
|
||||
if tpl ~= nil then
|
||||
out[tpl] = (out[tpl] or 0) + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
return out
|
||||
@@ -149,7 +185,7 @@ function M.is_known(recipe_id, ctx)
|
||||
return r.is_known(ctx) == true
|
||||
end
|
||||
|
||||
function M.can_craft(recipe_id, container, ctx)
|
||||
function M.can_craft(recipe_id, locale_arg, ctx)
|
||||
if type(ctx) ~= "table" then
|
||||
error("crafting.can_craft: ctx must be table", 2)
|
||||
end
|
||||
@@ -160,7 +196,8 @@ function M.can_craft(recipe_id, container, ctx)
|
||||
if r.is_known(ctx) ~= true then
|
||||
return { ok = false, error = "unknown_recipe" }
|
||||
end
|
||||
local have = count_by_template(container)
|
||||
local locale = resolve_locale(locale_arg, "can_craft")
|
||||
local have = count_by_template(locale.sources)
|
||||
local missing = {}
|
||||
for _, need in ipairs(r.inputs) do
|
||||
local have_n = have[need.template] or 0
|
||||
@@ -178,34 +215,40 @@ function M.can_craft(recipe_id, container, ctx)
|
||||
return { ok = true }
|
||||
end
|
||||
|
||||
function M.craft(recipe_id, container, ctx)
|
||||
local pre = M.can_craft(recipe_id, container, ctx)
|
||||
function M.craft(recipe_id, locale_arg, ctx)
|
||||
local pre = M.can_craft(recipe_id, locale_arg, ctx)
|
||||
if not pre.ok then return pre end
|
||||
|
||||
local locale = resolve_locale(locale_arg, "craft")
|
||||
local r = recipes[recipe_id]
|
||||
|
||||
-- Consume inputs: per-input-need, snapshot contents BEFORE the
|
||||
-- inner loop because we mutate via inv.remove during iteration.
|
||||
-- 1. Greedy-drain from sources in array-order. Per input-need, walk
|
||||
-- sources left-to-right and snapshot each source's contents
|
||||
-- BEFORE the inner loop because we mutate via inv.remove during
|
||||
-- iteration.
|
||||
local consumed = {}
|
||||
for _, need in ipairs(r.inputs) do
|
||||
local remaining = need.count
|
||||
local snapshot = inv.contents(container)
|
||||
for _, ent in ipairs(snapshot) do
|
||||
for _, source in ipairs(locale.sources) do
|
||||
if remaining == 0 then break end
|
||||
if composition.template_of(ent) == need.template then
|
||||
inv.remove(container, ent)
|
||||
consumed[#consumed + 1] = ent
|
||||
composition.destroy(ent)
|
||||
remaining = remaining - 1
|
||||
local snapshot = inv.contents(source)
|
||||
for _, ent in ipairs(snapshot) do
|
||||
if remaining == 0 then break end
|
||||
if composition.template_of(ent) == need.template then
|
||||
inv.remove(source, ent)
|
||||
consumed[#consumed + 1] = ent
|
||||
composition.destroy(ent)
|
||||
remaining = remaining - 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Produce outputs: composition.create + inv.add per output count.
|
||||
-- 2. Create outputs + add to sink.
|
||||
local crafted = {}
|
||||
for _ = 1, r.output.count do
|
||||
local out = composition.create{ template = r.output.template }
|
||||
inv.add(container, out)
|
||||
inv.add(locale.sink, out)
|
||||
crafted[#crafted + 1] = out
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user