# lib-core.crafting Recipe Registry + Match-Check + Craft Action. Headless data + logic layer. Recipes reference item-template-ids; inputs are consumed from the locale's source containers, outputs are placed into the locale's sink container. **Version:** 0.2.0 **Lib-ID:** lib-core.crafting **Requires:** `lib-core.composition` 0.3.0, `lib-core.inventory-list` 0.1.0 **Tags:** crafting, recipe, registry ## Topology ```mermaid graph LR this["lib-core.crafting"] composition["lib-core.composition"] inventory["lib-core.inventory-list"] this --> composition this --> inventory ``` ## Scope (v0.2.0) Solid-Cut: recipe registry + match-check + craft-action + per-recipe discovery hook. Multi-Source / Single-Sink locale (Form 2) plus bw-compat bare-handle locale (Form 1). Count-form recipe-inputs. **Supported:** - `define_recipe{id, inputs, output, name?, description?, is_known?}` - `list_recipes()`, `get_recipe(id)` - `is_known(recipe_id, ctx)` — per-recipe discovery gating - `can_craft(recipe_id, locale, ctx)` — non-mutating availability check - `craft(recipe_id, locale, ctx)` — mutating action - `locale` parameter accepts either a bare container-entity (Form 1, bw-compat) or an explicit `{sources={...}, sink=...}` table (Form 2) **Deferred (Phase D+ / E):** - Workpiece-Model / multi-step crafting / Batch / Time-coupled craft - Tool-Requirements (Hammer-required, Workbench-required) - Skill-Checks (skill-property-min for recipe) - Property-driven recipes (`composition.iron ≥ 0.8`) — Phase E - Multi-Container-UI (input from A, output to B) - Action-Property-Tags (`crafting.cutting`, `crafting.hammering`) — Domain-Lib substrate ## API ### `crafting.define_recipe(def)` **Syntax:** `crafting.define_recipe({id, inputs, output, name?, description?, is_known?}) -> void` **Example:** ```lua crafting.define_recipe{ id = "rock_pick", inputs = { { template = "rock", count = 1 }, { template = "stick", count = 1 }, }, output = { template = "rock_pick", count = 1 }, name = "Stone Pick", description = "A pick for mining stone.", is_known = function(ctx) return true end, } ``` **Description:** Registers a recipe under `id`. `inputs` is a non-empty array of `{template, count}` entries; `output` is a single `{template, count}`. `name` / `description` are optional player-facing display strings. `is_known(ctx) -> bool` is an optional discovery hook; default is `function() return true end`. The hook receives the same `ctx` table the caller passes to `is_known` / `can_craft` / `craft`. Loud `error(...)` on: non-table `def`; missing / non-string / empty `id`; duplicate `id`; non-table / empty `inputs`; any input or output entry that isn't a `{template: string, count: positive int}` table; non-table `output`; non-function `is_known` if provided. Error messages follow the pattern `"crafting.define_recipe '': "` so test-suites can pattern-match. ### `crafting.list_recipes()` **Syntax:** `crafting.list_recipes() -> {recipe_def, ...}` **Description:** Returns a shallow-copied list of all currently-registered recipes. Mutating the returned list (or the entries themselves) does not affect the registry. Order is unspecified. ### `crafting.get_recipe(id)` **Syntax:** `crafting.get_recipe(id: string) -> recipe_def or nil` **Description:** Returns a shallow-copy of the registered recipe with the given `id`, or `nil` if no recipe is registered under that id. ### `crafting.is_known(recipe_id, ctx)` **Syntax:** `crafting.is_known(recipe_id: string, ctx: table) -> bool` **Description:** Returns the result of `recipe.is_known(ctx)` for the named recipe, or `false` if the `recipe_id` is unregistered. `ctx` must be a table (loud-error otherwise); empty `{}` is allowed. Used by display libs to filter the recipe list to known recipes only. ### `crafting.can_craft(recipe_id, locale, ctx)` **Syntax:** `crafting.can_craft(recipe_id, locale, ctx) -> result` **Result-Shape:** ```lua -- success { ok = true } -- recipe missing / not known { ok = false, error = "unknown_recipe" } -- inputs insufficient { ok = false, error = "missing_inputs", missing = { { template = "stick", needed = 1, have = 0 }, ... }, } ``` **Description:** Non-mutating check whether `recipe_id` can be crafted right now from items in the locale's sources. Returns `ok=true` if the recipe is registered, `is_known(ctx)` returns `true`, AND the union of `inventory.contents` across all `locale.sources` contains at least `input.count` items per `input.template` for every input. Otherwise returns `ok=false` with an `error` discriminator. For `missing_inputs`, the `missing` array lists each input that's under-supplied with its aggregated `needed` and `have` count (summed across all sources). The `locale` parameter accepts two forms (see "Locale-Schema (v0.2.0)" below for details): a bare container-entity (Form 1, bw-compat to v0.1) or an explicit `{sources={c1, c2, ...}, sink=c_out}` table (Form 2). Loud-Error: `ctx` must be a table (empty `{}` OK); `locale` must not be `nil`; for Form 2, `locale.sources` must be a non-empty array and `locale.sink` must not be `nil`. ### `crafting.craft(recipe_id, locale, ctx)` **Syntax:** `crafting.craft(recipe_id, locale, ctx) -> result` **Result-Shape:** ```lua -- success { ok = true, crafted_items = { entity_handle, ... }, -- new output entities consumed = { entity_handle, ... }, -- input entities (already destroyed) } -- failure (same shape as can_craft) { ok = false, error = "unknown_recipe" } { ok = false, error = "missing_inputs", missing = {...} } ``` **Description:** Performs an internal `can_craft` check first; on failure returns the same result early (sources untouched). On success: 1. For each `input.need`, greedy-drain matching-template items from the sources in array-order (`locale.sources[1]` first, then `[2]`, ...). Each item is removed via `inventory.remove` from its source and then `composition.destroy`d. The first source is fully drained of matching items before moving on to the next. 2. Creates `output.count` new items via `composition.create{template=output.template}`. 3. Adds each new output to `locale.sink` via `inventory.add`. The greedy-drain-order is deterministic and stable: caller controls which source supplies first by ordering the `sources` array. Common patterns: put the workpiece-stash first to consume its leftovers; put the player backpack first to leave the workbench-buffer for next time. The returned `consumed` array references the input entity handles AFTER they were destroyed; they're useful for debug logging but must not be operated on (their composition meta-record is gone). Loud-Error: `ctx` must be a table (empty `{}` OK); `locale` must not be `nil`; for Form 2, `locale.sources` must be a non-empty array and `locale.sink` must not be `nil`. ## Recipe-Schema ```lua { id = "rock_pick", -- string, unique, non-empty inputs = { -- non-empty array { template = "rock", count = 1 }, -- count > 0, integer { template = "stick", count = 1 }, }, output = { template = "rock_pick", count = 1 }, name = "Stone Pick", -- optional, display description = "A pick for mining stone.", -- optional, used by Inspect is_known = function(ctx) return true end, -- optional discovery hook -- ctx is the caller-supplied table -- default: returns true unconditionally } ``` ## Locale-Schema (v0.2.0) The `locale` parameter to `can_craft` / `craft` describes WHERE inputs come from and WHERE outputs go. Two forms are accepted: ### Form 1 (bw-compat, v0.1) A bare container-entity-handle: ```lua crafting.craft("rock_pick", player_backpack, ctx) ``` Internally treated as `{sources = {player_backpack}, sink = player_backpack}` — the single container is both the sole input source AND the output sink. This is the v0.1 single-container behavior and remains supported unchanged. ### Form 2 (explicit, v0.2) A table with `sources` (non-empty array) and `sink`: ```lua crafting.craft("rock_pick", { sources = {workbench_buffer, player_backpack}, sink = workbench_buffer }, ctx) ``` - `sources` is an array of container-entity-handles. Inputs are consumed greedy-left-to-right: the first source is fully drained of matching items before moving to the next. Caller controls priority via array order. - `sink` is a single container-entity-handle. All outputs are added to this container. - The `sink` MAY appear in `sources` (e.g. the workbench buffer is both an input source AND the output sink). It does not have to. - `can_craft` aggregates `have` counts across ALL sources before comparing against `needed`. ### Bw-Compat Guarantee Any v0.1 call site that passed a bare container-entity-handle as the second argument continues to work unchanged in v0.2; the shim wraps it into `{sources = {h}, sink = h}` transparently. No call-site migration is required. ### Loud-Error Conditions | Condition | Error message | |---|---| | `locale == nil` | `crafting.: locale must not be nil` | | Form 2, `sources` not a table or empty | `crafting.: locale.sources must be non-empty array` | | Form 2, `sink == nil` | `crafting.: locale.sink must not be nil` | ## Test Backdoors ```lua crafting._test_clear_all() -- wipe the recipe registry; for test isolation crafting._test_get_recipes() -- raw internal recipes-by-id table ``` These are not part of the stable surface; they exist so test-libs can re-initialize state between assertions. ## Conventions - **Locale-Parameter:** v0.2 takes a `locale` describing input sources and output sink. Single-container call sites stay simple via Form-1 bw-compat (bare-handle); workbench-style "merge inputs from N containers into a sink-entity" call sites use Form-2 explicit table. - **Discovery-Hook:** Per-recipe `is_known(ctx)` lets modders gate recipes on schematic-store-lookups, skill-property-checks, etc. without expanding the surface. - **No silent fail:** `craft` always re-checks `can_craft` and returns a structured error if conditions changed since the UI's last frame. - **Greedy-drain-order is stable:** `craft` consumes from sources in array-order, fully draining each matching template before moving on. Callers can rely on this for sink-priority patterns. ## Future Phases | Phase | Surface addition | Spec | |---|---|---| | D+ (Workpiece) | Multi-step crafting with intermediate workpiece-entities | crafting-model.md | | D+ (Batch / Time) | Batch parameter, time-system coupling, abort-decon | crafting-model.md | | E (Property-driven) | `composition.iron ≥ 0.8` as input matcher | composition-model.md, crafting-model.md | | Domain-Libs | `lib-core.metalwork` / `textile` / `woodwork` consume crafting substrate | libraries.md §8 Catalog |