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
|
are consumed from the locale's source containers, non-consumed tools are
|
||||||
presence-checked, and outputs are placed into the locale's sink container.
|
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
|
**Lib-ID:** lib-core.crafting
|
||||||
**Requires:** `lib-core.composition` 0.3.0, `lib-core.inventory-list` 0.1.0
|
**Requires:** `lib-core.composition` 0.3.0, `lib-core.inventory-list` 0.1.0
|
||||||
**Tags:** crafting, recipe, registry
|
**Tags:** crafting, recipe, registry
|
||||||
@@ -22,7 +22,7 @@ graph LR
|
|||||||
this --> inventory
|
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`
|
Property-constraint recipe slots. A recipe slot matches items by a `match`
|
||||||
table of property-constraints (AND-combined), evaluated via `get_property`;
|
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
|
recipes are the trivial constraint through the SAME match-loop. Adds
|
||||||
non-consumed `tools` (presence checks) and multi-output. Multi-Source /
|
non-consumed `tools` (presence checks) and multi-output. Multi-Source /
|
||||||
Single-Sink locale (Form 2) plus bw-compat bare-handle locale (Form 1).
|
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:**
|
**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)
|
- `inputs` entries: `{template=..., count}` **or** `{match={k=v,...}, count}` (consumed)
|
||||||
- `tools` entries: `{template=...}` **or** `{match={...}}` (NON-consumed presence check)
|
- `tools` entries: `{template=...}` **or** `{match={...}}` (NON-consumed presence check)
|
||||||
- `outputs` array (plural) **or** `output` singular (bw-compat)
|
- `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
|
- Constraint values: exact string/number/bool, or comparison string
|
||||||
`">5"` / `">=0.2"` / `"<10"` / `"<=1"` / `"==x"` (numeric; string for `==`)
|
`">5"` / `">=0.2"` / `"<10"` / `"<=1"` / `"==x"` (numeric; string for `==`)
|
||||||
- `list_recipes()`, `get_recipe(id)`, `is_known(recipe_id, ctx)`
|
- `list_recipes()`, `get_recipe(id)`, `is_known(recipe_id, ctx)`
|
||||||
- `can_craft(recipe_id, locale, ctx)` — non-mutating; `craft(...)` — mutating
|
- `can_craft(recipe_id, locale, ctx)` — non-mutating; `craft(...)` — mutating
|
||||||
- `locale`: bare container-entity (Form 1) or `{sources={...}, sink=...}` (Form 2)
|
- `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+):**
|
**Deferred:**
|
||||||
- Skill-Checks (skill-property-min for recipe)
|
- 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
|
- Workpiece-Model / multi-step / Batch / Time-coupled craft
|
||||||
- True bipartite input↔item matching (v0.3 uses greedy first-fit — a solvable
|
- 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 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)`
|
### `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):**
|
**Example (template-id, bw-compat):**
|
||||||
```lua
|
```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`.
|
**Description:** Registers a recipe under `id`.
|
||||||
- `inputs` (required, non-empty): consumed slots. Each entry has a `count` plus
|
- `inputs` (required, non-empty): consumed slots. Each entry has a `count` plus
|
||||||
either `template = "<id>"` or `match = {key=constraint, ...}`.
|
either `template = "<id>"` or `match = {key=constraint, ...}`.
|
||||||
- `tools` (optional): non-consumed presence checks. Each entry is `{template}`
|
- `tools` (optional): non-consumed presence checks. Each entry is `{template}`
|
||||||
or `{match}` (no count — a matching item need only be present).
|
or `{match}` (no count — a matching item need only be present).
|
||||||
- `outputs` (array) or `output` (single, bw-compat): items created into the sink.
|
- `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
|
- A `match` constraint value is an exact string/number/bool, or a comparison
|
||||||
string `">5"` / `">=0.2"` / `"<10"` / `"<=1"` / `"==x"`. Keys AND-combine.
|
string `">5"` / `">=0.2"` / `"<10"` / `"<=1"` / `"==x"`. Keys AND-combine.
|
||||||
The pseudo-key `template` matches `composition.template_of`; all other keys
|
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`;
|
Loud `error(...)` on: non-table `def`; missing / non-string / empty `id`;
|
||||||
duplicate `id`; empty `inputs`; a slot with neither `template` nor `match`;
|
duplicate `id`; empty `inputs`; a slot with neither `template` nor `match`;
|
||||||
an empty `match`; bad `count`; missing both `outputs` and `output`;
|
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>"`
|
Error messages follow the pattern `"crafting.define_recipe '<id>': <reason>"`
|
||||||
so test-suites can pattern-match.
|
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
|
-- recipe missing / not known
|
||||||
{ ok = false, error = "unknown_recipe" }
|
{ ok = false, error = "unknown_recipe" }
|
||||||
|
|
||||||
|
-- actor precondition not met (ADR-0056)
|
||||||
|
{ ok = false, error = "requires_unmet", unmet = { "skill.knapping", ... } }
|
||||||
|
|
||||||
-- inputs insufficient
|
-- inputs insufficient
|
||||||
{
|
{
|
||||||
ok = false, error = "missing_inputs",
|
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
|
**Description:** Non-mutating check whether `recipe_id` can be crafted
|
||||||
right now from items in the locale's sources. Returns `ok=true` if the
|
right now. Returns `ok=true` if the recipe is registered, `is_known(ctx)`
|
||||||
recipe is registered, `is_known(ctx)` returns `true`, AND the union of
|
returns `true`, the recipe's `requires` (if any) are satisfied by
|
||||||
`inventory.contents` across all `locale.sources` contains at least
|
`ctx.actor`, AND the union of `inventory.contents` across all
|
||||||
`input.count` items per `input.template` for every input. Otherwise
|
`locale.sources` supplies every input slot. Otherwise `ok=false` with an
|
||||||
returns `ok=false` with an `error` discriminator. For `missing_inputs`,
|
`error` discriminator (checked in that order: `unknown_recipe` →
|
||||||
the `missing` array lists each input that's under-supplied with its
|
`requires_unmet` → `missing_inputs` → `missing_tools`). For
|
||||||
aggregated `needed` and `have` count (summed across all sources).
|
`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)"
|
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)
|
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,
|
ok = true,
|
||||||
crafted_items = { entity_handle, ... }, -- new output entities
|
crafted_items = { entity_handle, ... }, -- new output entities
|
||||||
consumed = { entity_handle, ... }, -- input entities (already destroyed)
|
consumed = { entity_handle, ... }, -- input entities (already destroyed)
|
||||||
|
granted = { ["skill.knapping"] = 1 }, -- recipe.grants, or nil; MODULE applies
|
||||||
}
|
}
|
||||||
|
|
||||||
-- failure (same shape as can_craft)
|
-- failure (same shape as can_craft)
|
||||||
{ ok = false, error = "unknown_recipe" }
|
{ ok = false, error = "unknown_recipe" }
|
||||||
|
{ ok = false, error = "requires_unmet", unmet = {...} }
|
||||||
{ ok = false, error = "missing_inputs", missing = {...} }
|
{ ok = false, error = "missing_inputs", missing = {...} }
|
||||||
```
|
```
|
||||||
|
|
||||||
**Description:** Performs an internal `can_craft` check first; on failure
|
**Description:** Performs the same registration / `requires` / availability
|
||||||
returns the same result early (sources untouched). On success:
|
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
|
1. For each `input.need`, greedy-drain matching-template items from the
|
||||||
sources in array-order (`locale.sources[1]` first, then `[2]`, ...).
|
sources in array-order (`locale.sources[1]` first, then `[2]`, ...).
|
||||||
Each item is removed via `inventory.remove` from its source and then
|
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 },
|
{ template = "stick", count = 1 },
|
||||||
},
|
},
|
||||||
output = { template = "rock_pick", 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
|
name = "Stone Pick", -- optional, display
|
||||||
description = "A pick for mining stone.", -- optional, used by Inspect
|
description = "A pick for mining stone.", -- optional, used by Inspect
|
||||||
is_known = function(ctx) return true end,
|
is_known = function(ctx) return true end,
|
||||||
@@ -307,7 +349,11 @@ re-initialize state between assertions.
|
|||||||
|
|
||||||
| Phase | Surface addition | Spec |
|
| 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+ (Workpiece) | Multi-step crafting with intermediate workpiece-entities | crafting-model.md |
|
||||||
| D+ (Batch / Time) | Batch parameter, time-system coupling, abort-decon | 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 |
|
| Domain-Libs | `lib-core.metalwork` / `textile` / `woodwork` consume crafting substrate | libraries.md §8 Catalog |
|
||||||
|
|||||||
85
init.lua
85
init.lua
@@ -1,7 +1,9 @@
|
|||||||
-- =====================================================================
|
-- =====================================================================
|
||||||
-- lib-core.crafting v0.3.0 — Recipe Registry + Craft Action
|
-- lib-core.crafting v0.4.0 — Recipe Registry + Craft Action
|
||||||
-- Spec: meta/docs/adrs/0055-recipe-slot-property-constraint.md
|
-- Spec: meta/docs/adrs/0055-recipe-slot-property-constraint.md
|
||||||
|
-- meta/docs/adrs/0056-crafting-actor-precondition-reward.md
|
||||||
-- meta/docs/design/2026-07-28-crafting-property-constraint-slots-design.md
|
-- meta/docs/design/2026-07-28-crafting-property-constraint-slots-design.md
|
||||||
|
-- meta/docs/design/2026-07-31-crafting-skill-gating-design.md
|
||||||
--
|
--
|
||||||
-- Surface:
|
-- Surface:
|
||||||
-- crafting.define_recipe(recipe_def) -- register a recipe
|
-- crafting.define_recipe(recipe_def) -- register a recipe
|
||||||
@@ -11,7 +13,7 @@
|
|||||||
-- crafting.can_craft(recipe_id, locale, ctx) -> result (non-mutating)
|
-- crafting.can_craft(recipe_id, locale, ctx) -> result (non-mutating)
|
||||||
-- crafting.craft(recipe_id, locale, ctx) -> result (mutating)
|
-- crafting.craft(recipe_id, locale, ctx) -> result (mutating)
|
||||||
--
|
--
|
||||||
-- Recipe-Schema (v0.3.0 — ADR-0055):
|
-- Recipe-Schema (v0.4.0 — ADR-0055 + ADR-0056):
|
||||||
-- {
|
-- {
|
||||||
-- id = "saw_planks",
|
-- id = "saw_planks",
|
||||||
-- inputs = { -- consumed; each entry is
|
-- inputs = { -- consumed; each entry is
|
||||||
@@ -22,9 +24,20 @@
|
|||||||
-- { match = {["affordance.cutting"] = true} },
|
-- { match = {["affordance.cutting"] = true} },
|
||||||
-- },
|
-- },
|
||||||
-- outputs = { {template="plank", count=4} },-- plural; `output` singular ok
|
-- outputs = { {template="plank", count=4} },-- plural; `output` singular ok
|
||||||
|
-- requires = { ["skill.knapping"] = ">=1" },-- ADR-0056: actor-precondition
|
||||||
|
-- -- gate, matched vs ctx.actor
|
||||||
|
-- grants = { ["skill.knapping"] = 5 }, -- ADR-0056: reward returned as
|
||||||
|
-- -- `granted`; MODULE writes it
|
||||||
-- name = "...", description = "...", is_known = function(ctx) ... end,
|
-- name = "...", description = "...", is_known = function(ctx) ... end,
|
||||||
-- }
|
-- }
|
||||||
--
|
--
|
||||||
|
-- `requires` is a match-table (same predicate machinery as slots) evaluated
|
||||||
|
-- against ctx.actor, NOT against container items. It is a hard gate on a KNOWN
|
||||||
|
-- recipe ("can this actor execute it?") — distinct from is_known (discovery).
|
||||||
|
-- `grants` (property -> number) is static reward data; craft() returns it as
|
||||||
|
-- `granted` and the consuming module applies it to the actor (crafting stays
|
||||||
|
-- container-scoped). Both are domain-free: crafting knows no "skill" vocabulary.
|
||||||
|
--
|
||||||
-- A recipe SLOT is a property-CONSTRAINT over an item's (possibly derived)
|
-- A recipe SLOT is a property-CONSTRAINT over an item's (possibly derived)
|
||||||
-- properties, evaluated via ent:get_property. The pseudo-key "template" maps
|
-- properties, evaluated via ent:get_property. The pseudo-key "template" maps
|
||||||
-- to composition.template_of, so an old {template="rock"} slot is just the
|
-- to composition.template_of, so an old {template="rock"} slot is just the
|
||||||
@@ -44,8 +57,12 @@
|
|||||||
-- Form 2 (explicit): { sources = {c1,...}, sink = c_out }.
|
-- Form 2 (explicit): { sources = {c1,...}, sink = c_out }.
|
||||||
--
|
--
|
||||||
-- Match-Result-Schema:
|
-- Match-Result-Schema:
|
||||||
-- { ok, error?, missing?, missing_tools?, crafted_items?, consumed? }
|
-- { ok, error?, missing?, missing_tools?, unmet?, crafted_items?, consumed?,
|
||||||
-- error ∈ { "unknown_recipe" | "missing_inputs" | "missing_tools" }
|
-- granted? }
|
||||||
|
-- error ∈ { "unknown_recipe" | "requires_unmet" | "missing_inputs"
|
||||||
|
-- | "missing_tools" }
|
||||||
|
-- unmet = { "<requires-key>", ... } (which actor-preconditions failed)
|
||||||
|
-- granted = the recipe's `grants` table on a successful craft (module applies)
|
||||||
--
|
--
|
||||||
-- Deps: lib-core.composition (template_of, create, destroy),
|
-- Deps: lib-core.composition (template_of, create, destroy),
|
||||||
-- lib-core.inventory-list (contents, add, remove)
|
-- lib-core.inventory-list (contents, add, remove)
|
||||||
@@ -129,6 +146,17 @@ local function entity_matches(ent, fields)
|
|||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Evaluate `requires` fields against the actor (ADR-0056). Returns the list of
|
||||||
|
-- unmet keys ({} = all satisfied). A nil actor fails every field (fail-closed).
|
||||||
|
local function eval_requires(actor, fields)
|
||||||
|
local unmet = {}
|
||||||
|
for _, f in ipairs(fields) do
|
||||||
|
local v = (actor ~= nil) and read_prop(actor, f.key) or nil
|
||||||
|
if not f.test(v) then unmet[#unmet + 1] = f.key end
|
||||||
|
end
|
||||||
|
return unmet
|
||||||
|
end
|
||||||
|
|
||||||
-- ---------- helpers ----------
|
-- ---------- helpers ----------
|
||||||
|
|
||||||
local function shallow_copy(t)
|
local function shallow_copy(t)
|
||||||
@@ -308,6 +336,37 @@ function M.define_recipe(def)
|
|||||||
error(string.format("crafting.define_recipe '%s': is_known must be function", id), 2)
|
error(string.format("crafting.define_recipe '%s': is_known must be function", id), 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- requires (optional, ADR-0056): actor-precondition gate. Same match-table
|
||||||
|
-- shape as a slot; compiled here, evaluated against ctx.actor at craft-time.
|
||||||
|
local requires = nil
|
||||||
|
if def.requires ~= nil then
|
||||||
|
requires = compile_match(def.requires, "requires", id)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- grants (optional, ADR-0056): property -> number reward, returned as
|
||||||
|
-- `granted`; the module applies it to the actor.
|
||||||
|
local grants = nil
|
||||||
|
if def.grants ~= nil then
|
||||||
|
if type(def.grants) ~= "table" then
|
||||||
|
error(string.format("crafting.define_recipe '%s': grants must be table", id), 2)
|
||||||
|
end
|
||||||
|
grants = {}
|
||||||
|
local n = 0
|
||||||
|
for k, v in pairs(def.grants) do
|
||||||
|
if type(k) ~= "string" then
|
||||||
|
error(string.format("crafting.define_recipe '%s': grants keys must be strings", id), 2)
|
||||||
|
end
|
||||||
|
if type(v) ~= "number" then
|
||||||
|
error(string.format("crafting.define_recipe '%s': grants['%s'] must be number", id, k), 2)
|
||||||
|
end
|
||||||
|
grants[k] = v
|
||||||
|
n = n + 1
|
||||||
|
end
|
||||||
|
if n == 0 then
|
||||||
|
error(string.format("crafting.define_recipe '%s': grants must be non-empty", id), 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
recipes[id] = {
|
recipes[id] = {
|
||||||
id = id,
|
id = id,
|
||||||
inputs = inputs,
|
inputs = inputs,
|
||||||
@@ -316,6 +375,8 @@ function M.define_recipe(def)
|
|||||||
-- bw-compat alias: consumers (e.g. UI icon resolvers) that read
|
-- bw-compat alias: consumers (e.g. UI icon resolvers) that read
|
||||||
-- `recipe.output.template` keep working; points at the first output.
|
-- `recipe.output.template` keep working; points at the first output.
|
||||||
output = outputs[1],
|
output = outputs[1],
|
||||||
|
requires = requires,
|
||||||
|
grants = grants,
|
||||||
name = def.name,
|
name = def.name,
|
||||||
description = def.description,
|
description = def.description,
|
||||||
is_known = is_known,
|
is_known = is_known,
|
||||||
@@ -353,6 +414,12 @@ function M.can_craft(recipe_id, locale_arg, ctx)
|
|||||||
if r == nil or r.is_known(ctx) ~= true then
|
if r == nil or r.is_known(ctx) ~= true then
|
||||||
return { ok = false, error = "unknown_recipe" }
|
return { ok = false, error = "unknown_recipe" }
|
||||||
end
|
end
|
||||||
|
if r.requires then
|
||||||
|
local unmet = eval_requires(ctx.actor, r.requires)
|
||||||
|
if #unmet > 0 then
|
||||||
|
return { ok = false, error = "requires_unmet", unmet = unmet }
|
||||||
|
end
|
||||||
|
end
|
||||||
local locale = resolve_locale(locale_arg, "can_craft")
|
local locale = resolve_locale(locale_arg, "can_craft")
|
||||||
local p = plan(r, locale.sources)
|
local p = plan(r, locale.sources)
|
||||||
if not p.ok then
|
if not p.ok then
|
||||||
@@ -369,6 +436,12 @@ function M.craft(recipe_id, locale_arg, ctx)
|
|||||||
if r == nil or r.is_known(ctx) ~= true then
|
if r == nil or r.is_known(ctx) ~= true then
|
||||||
return { ok = false, error = "unknown_recipe" }
|
return { ok = false, error = "unknown_recipe" }
|
||||||
end
|
end
|
||||||
|
if r.requires then
|
||||||
|
local unmet = eval_requires(ctx.actor, r.requires)
|
||||||
|
if #unmet > 0 then
|
||||||
|
return { ok = false, error = "requires_unmet", unmet = unmet }
|
||||||
|
end
|
||||||
|
end
|
||||||
local locale = resolve_locale(locale_arg, "craft")
|
local locale = resolve_locale(locale_arg, "craft")
|
||||||
local p = plan(r, locale.sources)
|
local p = plan(r, locale.sources)
|
||||||
if not p.ok then
|
if not p.ok then
|
||||||
@@ -393,7 +466,9 @@ function M.craft(recipe_id, locale_arg, ctx)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return { ok = true, crafted_items = crafted, consumed = consumed }
|
-- `granted` is static reward data (ADR-0056); the module writes it onto the
|
||||||
|
-- actor. nil when the recipe has no grants.
|
||||||
|
return { ok = true, crafted_items = crafted, consumed = consumed, granted = r.grants }
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ---------- test backdoors ----------
|
-- ---------- test backdoors ----------
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"id":"lib-core.crafting","version":"0.3.0","api_min":"0.1","deps":[{"id":"lib-core.composition","version":"0.3.0"},{"id":"lib-core.inventory-list","version":"0.1.0"}]}
|
{"id":"lib-core.crafting","version":"0.4.0","api_min":"0.1","deps":[{"id":"lib-core.composition","version":"0.3.0"},{"id":"lib-core.inventory-list","version":"0.1.0"}]}
|
||||||
Reference in New Issue
Block a user