crafting: actor-precondition (requires) + reward (grants) — Skills half of Phase H (ADR-0056)
lib-core.crafting v0.4.0: two optional domain-free recipe fields.
- `requires`: a match-table evaluated against ctx.actor (not container items) — a hard gate. Failing -> {ok=false, error="requires_unmet", unmet={keys}}; fail-closed when ctx.actor is nil. Same predicate machinery as slots; distinct from is_known (discovery vs lock).
- `grants`: property->number reward, returned as `granted` from craft(); the consuming MODULE applies it to the actor (crafting stays container-scoped). Crafting knows no 'skill' vocabulary (ADR-0001).
vagrant-skeleton v0.21.0: player-actor carries skill.knapping (entity property, Phase-A lesson); knap_sharp_stone grants knapping XP, craft_stone_axe requires knapping>=3; module applies granted; skill-locked recipes show 'Skill too low'. Gate->earn->unlock loop headless-verified (axe locked @0 requires_unmet -> 3 knaps -> unlocks).
Quality-band reserved (Phase J): the requires threshold = the future quality floor (no re-authoring). Docs synced: ADR-0056, crafting-model.md (v0.4.0 + Actor-Precondition section), libraries.md, crafting README, vagrant README.
This commit is contained in:
82
README.md
82
README.md
@@ -6,7 +6,7 @@ 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
|
||||
**Version:** 0.4.0
|
||||
**Lib-ID:** lib-core.crafting
|
||||
**Requires:** `lib-core.composition` 0.3.0, `lib-core.inventory-list` 0.1.0
|
||||
**Tags:** crafting, recipe, registry
|
||||
@@ -22,7 +22,7 @@ graph LR
|
||||
this --> inventory
|
||||
```
|
||||
|
||||
## Scope (v0.3.0 — ADR-0055)
|
||||
## Scope (v0.4.0 — ADR-0055 + ADR-0056)
|
||||
|
||||
Property-constraint recipe slots. A recipe slot matches items by a `match`
|
||||
table of property-constraints (AND-combined), evaluated via `get_property`;
|
||||
@@ -30,21 +30,30 @@ 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).
|
||||
v0.4.0 (ADR-0056): actor-side `requires` (precondition gate vs `ctx.actor`) +
|
||||
`grants` (reward returned as `granted`).
|
||||
|
||||
**Supported:**
|
||||
- `define_recipe{id, inputs, tools?, outputs|output, name?, description?, is_known?}`
|
||||
- `define_recipe{id, inputs, tools?, outputs|output, requires?, grants?, 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)
|
||||
- `requires`: a `match` table evaluated against **`ctx.actor`** (hard gate;
|
||||
same predicate machinery as slots) — e.g. `{["skill.knapping"]=">=3"}`
|
||||
- `grants`: `property → number` reward; returned as `granted`, the **module**
|
||||
writes it to the actor — e.g. `{["skill.knapping"]=1}`
|
||||
- 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}`
|
||||
- Result `error ∈ {unknown_recipe, requires_unmet, missing_inputs, missing_tools}`;
|
||||
on `requires_unmet` also `unmet={keys}`; on a successful `craft` also `granted`
|
||||
|
||||
**Deferred (Phase E+):**
|
||||
- Skill-Checks (skill-property-min for recipe)
|
||||
**Deferred:**
|
||||
- Skill XP→level curve, skill decay, `lib-core.skill` extraction (requires/grants
|
||||
are the generic substrate; skill vocabulary is the module's — ADR-0056)
|
||||
- Quality-band coupling (skill/ingredient/tool quality → output quality, Phase J)
|
||||
- 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
|
||||
@@ -56,7 +65,7 @@ Single-Sink locale (Form 2) plus bw-compat bare-handle locale (Form 1).
|
||||
|
||||
### `crafting.define_recipe(def)`
|
||||
|
||||
**Syntax:** `crafting.define_recipe({id, inputs, tools?, outputs|output, name?, description?, is_known?}) -> void`
|
||||
**Syntax:** `crafting.define_recipe({id, inputs, tools?, outputs|output, requires?, grants?, name?, description?, is_known?}) -> void`
|
||||
|
||||
**Example (template-id, bw-compat):**
|
||||
```lua
|
||||
@@ -79,12 +88,30 @@ crafting.define_recipe{
|
||||
}
|
||||
```
|
||||
|
||||
**Example (actor gate + reward, ADR-0056):**
|
||||
```lua
|
||||
crafting.define_recipe{
|
||||
id = "craft_stone_axe",
|
||||
inputs = { { match = { ["affordance.cutting"] = true }, count = 1 }, ... },
|
||||
requires = { ["skill.knapping"] = ">=3" }, -- gate vs ctx.actor
|
||||
grants = { ["skill.knapping"] = 1 }, -- reward -> `granted`, module writes
|
||||
outputs = { { template = "stone_axe", count = 1 } },
|
||||
}
|
||||
```
|
||||
|
||||
**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.
|
||||
- `requires` (optional): a `match` table evaluated against **`ctx.actor`** (not
|
||||
container items) — a hard gate. Failing → `{ok=false, error="requires_unmet",
|
||||
unmet={keys}}`. Fail-closed when `ctx.actor` is nil. Distinct from `is_known`
|
||||
(discovery). Same predicate/`match` shape as slots.
|
||||
- `grants` (optional): `property → number` map. Returned as `granted` from a
|
||||
successful `craft`; the **consuming module** applies it to the actor (crafting
|
||||
stays container-scoped). Domain-free: crafting knows no "skill" meaning.
|
||||
- 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
|
||||
@@ -95,7 +122,8 @@ crafting.define_recipe{
|
||||
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`.
|
||||
non-function `is_known`; non-table or empty `grants`; non-string `grants` key
|
||||
or non-number `grants` value.
|
||||
|
||||
Error messages follow the pattern `"crafting.define_recipe '<id>': <reason>"`
|
||||
so test-suites can pattern-match.
|
||||
@@ -136,6 +164,9 @@ display libs to filter the recipe list to known recipes only.
|
||||
-- recipe missing / not known
|
||||
{ ok = false, error = "unknown_recipe" }
|
||||
|
||||
-- actor precondition not met (ADR-0056)
|
||||
{ ok = false, error = "requires_unmet", unmet = { "skill.knapping", ... } }
|
||||
|
||||
-- inputs insufficient
|
||||
{
|
||||
ok = false, error = "missing_inputs",
|
||||
@@ -147,13 +178,15 @@ display libs to filter the recipe list to known recipes only.
|
||||
```
|
||||
|
||||
**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).
|
||||
right now. Returns `ok=true` if the recipe is registered, `is_known(ctx)`
|
||||
returns `true`, the recipe's `requires` (if any) are satisfied by
|
||||
`ctx.actor`, AND the union of `inventory.contents` across all
|
||||
`locale.sources` supplies every input slot. Otherwise `ok=false` with an
|
||||
`error` discriminator (checked in that order: `unknown_recipe` →
|
||||
`requires_unmet` → `missing_inputs` → `missing_tools`). For
|
||||
`requires_unmet`, `unmet` lists the failing actor-precondition keys (a nil
|
||||
`ctx.actor` fails all — fail-closed). For `missing_inputs`, `missing`
|
||||
lists each under-supplied input with aggregated `needed`/`have`.
|
||||
|
||||
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)
|
||||
@@ -174,15 +207,20 @@ Loud-Error: `ctx` must be a table (empty `{}` OK); `locale` must not be
|
||||
ok = true,
|
||||
crafted_items = { entity_handle, ... }, -- new output entities
|
||||
consumed = { entity_handle, ... }, -- input entities (already destroyed)
|
||||
granted = { ["skill.knapping"] = 1 }, -- recipe.grants, or nil; MODULE applies
|
||||
}
|
||||
|
||||
-- failure (same shape as can_craft)
|
||||
{ ok = false, error = "unknown_recipe" }
|
||||
{ ok = false, error = "requires_unmet", unmet = {...} }
|
||||
{ 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:
|
||||
**Description:** Performs the same registration / `requires` / availability
|
||||
checks as `can_craft` first; on failure returns the same result early
|
||||
(sources untouched). `granted` echoes the recipe's `grants` (or nil) — the
|
||||
**consuming module** writes it to `ctx.actor`; `craft` itself does not
|
||||
mutate the actor. 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
|
||||
@@ -215,6 +253,10 @@ Loud-Error: `ctx` must be a table (empty `{}` OK); `locale` must not be
|
||||
{ template = "stick", count = 1 },
|
||||
},
|
||||
output = { template = "rock_pick", count = 1 },
|
||||
requires = { ["skill.knapping"] = ">=3" }, -- optional (ADR-0056): gate
|
||||
-- vs ctx.actor; same match shape
|
||||
grants = { ["skill.knapping"] = 1 }, -- optional (ADR-0056): reward,
|
||||
-- returned as `granted`
|
||||
name = "Stone Pick", -- optional, display
|
||||
description = "A pick for mining stone.", -- optional, used by Inspect
|
||||
is_known = function(ctx) return true end,
|
||||
@@ -307,7 +349,11 @@ re-initialize state between assertions.
|
||||
|
||||
| Phase | Surface addition | Spec |
|
||||
|---|---|---|
|
||||
| ~~E (Property-driven)~~ | **Shipped v0.3.0** (`match` constraints, ADR-0055) | crafting-model.md |
|
||||
| ~~H (Tools)~~ | **Shipped v0.3.0** (`tools` slot array) | crafting-model.md |
|
||||
| ~~H (Skills, gate)~~ | **Shipped v0.4.0** (`requires`/`grants`, ADR-0056) | crafting-model.md |
|
||||
| H (Skills, progression) | XP→level curve, decay, `lib-core.skill` extraction | — |
|
||||
| J (Quality) | `quality`-block: skill/ingredient/tool → output quality | crafting-model.md §Actor-Precondition |
|
||||
| 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 |
|
||||
|
||||
Reference in New Issue
Block a user