# lib-core.crafting Recipe Registry + Match-Check + Craft Action. Headless data + logic layer. Recipes reference item-template-ids; inputs are consumed from a container, outputs are placed back into the same container. **Version:** 0.1.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.1.0) Solid-Cut: recipe registry + match-check + craft-action + per-recipe discovery hook. Single-container parameter (inputs source AND output destination). 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, container, ctx)` — non-mutating availability check - `craft(recipe_id, container, ctx)` — mutating action **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, container, ctx)` **Syntax:** `crafting.can_craft(recipe_id, container, 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 `container`. Returns `ok=true` if the recipe is registered, `is_known(ctx)` returns `true`, AND `inventory.contents(container)` 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 `needed` and `have` count. Loud-Error: `ctx` must be a table (empty `{}` OK). ### `crafting.craft(recipe_id, container, ctx)` **Syntax:** `crafting.craft(recipe_id, container, 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 (container untouched). On success: 1. Removes `input.count` matching-template items from the container via `inventory.remove`, then `composition.destroy`s each consumed item. 2. Creates `output.count` new items via `composition.create{template=output.template}`. 3. Adds each new output to the container via `inventory.add`. 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). ## 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 } ``` ## 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 - **Container-Parameter:** v0.1 uses a single container for both inputs and output. Modules pass the appropriate container (e.g. the player's backpack). Workbench-Entity / multi-container splits are deferred to Phase D. - **Discovery-Hook:** Per-recipe `is_known(ctx)` lets modders gate recipes on schematic-store-lookups, skill-property-checks, etc. without expanding the v0.1 surface. - **No silent fail:** `craft` always re-checks `can_craft` and returns a structured error if conditions changed since the UI's last frame. ## Future Phases | Phase | Surface addition | Spec | |---|---|---| | D (Workbench) | Workbench-Entity-Container parameter; post-craft-relocate callback | crafting-model.md | | 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 |