Headless data + logic layer for recipe-based crafting. Manages a
recipe registry, performs match-checks against a container's
inventory contents, and atomically consumes inputs + creates outputs.
Surface:
define_recipe{id, inputs, output, name?, description?, is_known?}
list_recipes(), get_recipe(id)
is_known(recipe_id, ctx) -- per-recipe discovery gate
can_craft(recipe_id, container, ctx) -- non-mutating availability
craft(recipe_id, container, ctx) -- mutating action
Recipe-Schema: count-form inputs (array of {template, count}) +
single output {template, count} + optional is_known(ctx) hook
(default returns true). Match-result schema:
{ ok=true, crafted_items, consumed }
{ ok=false, error='unknown_recipe' }
{ ok=false, error='missing_inputs', missing={{template, needed, have}} }
Container is both inputs-source and output-destination (single-container
v0.1; multi-container deferred). craft re-runs can_craft internally
and returns the structured error if state changed since the last frame.
Depends on lib-core.composition 0.3.0 (template_of, create, destroy)
and lib-core.inventory-list 0.1.0 (contents, add, remove).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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
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 gatingcan_craft(recipe_id, container, ctx)— non-mutating availability checkcraft(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:
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 '<id>': <reason>"
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:
-- 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:
-- 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:
- Removes
input.countmatching-template items from the container viainventory.remove, thencomposition.destroys each consumed item. - Creates
output.countnew items viacomposition.create{template=output.template}. - 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
{
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
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:
craftalways re-checkscan_craftand 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 |