Files
sporel-lib-core.crafting/README.md

314 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# lib-core.crafting
Recipe Registry + Match-Check + Craft Action. Headless data + logic layer.
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.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
## Topology
```mermaid
graph LR
this["lib-core.crafting"]
composition["lib-core.composition"]
inventory["lib-core.inventory-list"]
this --> composition
this --> inventory
```
## Scope (v0.3.0 — ADR-0055)
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, 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 E+):**
- Skill-Checks (skill-property-min for recipe)
- 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, tools?, outputs|output, name?, description?, is_known?}) -> void`
**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",
}
```
**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`; 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.
### `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.<fn>: locale must not be nil` |
| Form 2, `sources` not a table or empty | `crafting.<fn>: locale.sources must be non-empty array` |
| Form 2, `sink == nil` | `crafting.<fn>: 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 |