Calic a4f60b4825 feat: lib-core.crafting v0.1.0 — Recipe Registry + Craft Action
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>
2026-06-14 12:20:42 +02:00

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 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:

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:

  1. Removes input.count matching-template items from the container via inventory.remove, then composition.destroys 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

{
    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: 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
Description
No description provided
Readme 96 KiB
Languages
Lua 100%