feat(crafting): lib-core.crafting v0.3.0 — property-constraint slots + non-consumed tools + multi-output (ADR-0055); vagrant-skeleton debug-drive hook for headless scenario testing

This commit is contained in:
Calic
2026-07-28 10:35:54 +00:00
parent ee56ecaabf
commit b60364df84
2 changed files with 320 additions and 185 deletions

View File

@@ -1,10 +1,12 @@
# 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.
A recipe SLOT is a property-constraint over an item's (possibly derived)
properties; template-id is the trivial constraint `{template="rock"}`. Inputs
are consumed from the locale's source containers, non-consumed tools are
presence-checked, and outputs are placed into the locale's sink container.
**Version:** 0.2.0
**Version:** 0.3.0
**Lib-ID:** lib-core.crafting
**Requires:** `lib-core.composition` 0.3.0, `lib-core.inventory-list` 0.1.0
**Tags:** crafting, recipe, registry
@@ -20,61 +22,80 @@ graph LR
this --> inventory
```
## Scope (v0.2.0)
## Scope (v0.3.0 — ADR-0055)
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.
Property-constraint recipe slots. A recipe slot matches items by a `match`
table of property-constraints (AND-combined), evaluated via `get_property`;
the pseudo-key `template` maps to `composition.template_of`, so old template-id
recipes are the trivial constraint through the SAME match-loop. Adds
non-consumed `tools` (presence checks) and multi-output. Multi-Source /
Single-Sink locale (Form 2) plus bw-compat bare-handle locale (Form 1).
**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)
- `define_recipe{id, inputs, tools?, outputs|output, name?, description?, is_known?}`
- `inputs` entries: `{template=..., count}` **or** `{match={k=v,...}, count}` (consumed)
- `tools` entries: `{template=...}` **or** `{match={...}}` (NON-consumed presence check)
- `outputs` array (plural) **or** `output` singular (bw-compat)
- Constraint values: exact string/number/bool, or comparison string
`">5"` / `">=0.2"` / `"<10"` / `"<=1"` / `"==x"` (numeric; string for `==`)
- `list_recipes()`, `get_recipe(id)`, `is_known(recipe_id, ctx)`
- `can_craft(recipe_id, locale, ctx)` — non-mutating; `craft(...)` — mutating
- `locale`: bare container-entity (Form 1) or `{sources={...}, sink=...}` (Form 2)
- Result `error ∈ {unknown_recipe, missing_inputs, missing_tools}`
**Deferred (Phase D+ / E):**
- Workpiece-Model / multi-step crafting / Batch / Time-coupled craft
- Tool-Requirements (Hammer-required, Workbench-required)
**Deferred (Phase E+):**
- 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
- Workpiece-Model / multi-step / Batch / Time-coupled craft
- True bipartite input↔item matching (v0.3 uses greedy first-fit — a solvable
recipe where one item satisfies two slots can be missed; no stone-age
recipe hits this)
- `density × volume` derived mass on the material substrate (v0.3 reads
whatever property keys the recipe names)
## API
### `crafting.define_recipe(def)`
**Syntax:** `crafting.define_recipe({id, inputs, output, name?, description?, is_known?}) -> void`
**Syntax:** `crafting.define_recipe({id, inputs, tools?, outputs|output, name?, description?, is_known?}) -> void`
**Example:**
**Example (template-id, bw-compat):**
```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,
inputs = { { template = "rock", count = 1 }, { template = "stick", count = 1 } },
output = { template = "rock_pick", count = 1 },
name = "Stone Pick",
}
```
**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`.
**Example (property-constraint + tool + multi-output):**
```lua
crafting.define_recipe{
id = "saw_planks",
inputs = { { match = { category = "wood", mass = ">5" }, count = 1 } },
tools = { { match = { ["affordance.cutting"] = true } } }, -- NOT consumed
outputs = { { template = "plank", count = 4 } },
name = "Saw Planks",
}
```
**Description:** Registers a recipe under `id`.
- `inputs` (required, non-empty): consumed slots. Each entry has a `count` plus
either `template = "<id>"` or `match = {key=constraint, ...}`.
- `tools` (optional): non-consumed presence checks. Each entry is `{template}`
or `{match}` (no count — a matching item need only be present).
- `outputs` (array) or `output` (single, bw-compat): items created into the sink.
- A `match` constraint value is an exact string/number/bool, or a comparison
string `">5"` / `">=0.2"` / `"<10"` / `"<=1"` / `"==x"`. Keys AND-combine.
The pseudo-key `template` matches `composition.template_of`; all other keys
match `get_property(key)` (read safely — an item lacking the property fails).
- `name` / `description` optional display strings; `is_known(ctx) -> bool`
optional discovery hook (default `true`).
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.
duplicate `id`; empty `inputs`; a slot with neither `template` nor `match`;
an empty `match`; bad `count`; missing both `outputs` and `output`;
non-function `is_known`.
Error messages follow the pattern `"crafting.define_recipe '<id>': <reason>"`
so test-suites can pattern-match.