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:
calic
2026-07-31 10:00:27 +00:00
parent b28afd5290
commit f93fef5990
4 changed files with 53 additions and 6 deletions

View File

@@ -239,6 +239,10 @@ composition.define_template{
properties = {
position = {x = 0, y = 0},
movement_speed = 120, -- WALK_SPEED_NORMAL
-- Skill as an actor entity-property (ADR-0056 / Phase-A lesson: NOT a
-- Lua-local, so the derivation machinery + get_property reach it).
-- Raw XP counter; recipe `requires`/`grants` gate/reward against it.
["skill.knapping"] = 0,
},
tags = {}, -- intentionally untagged: not renderable
}
@@ -492,6 +496,22 @@ local function update_lower_anim(moving, move_x, move_y)
end
end
-- Apply a craft's `granted` rewards (ADR-0056) to the actor: additive numeric
-- accumulation (skill XP, etc.). crafting returns the reward; the module owns
-- the actor-write (crafting stays container-scoped). Granted keys must be
-- declared actor properties (see the vagrant_player template).
local function apply_grants(actor_handle, granted)
if not granted then return end
for key, amount in pairs(granted) do
local cur = actor_handle:get_property(key) or 0
actor_handle:set_property(key, cur + amount)
if CI_FRAME_PERF then
engine.print(string.format("vagrant: grant %s +%s -> %s",
key, tostring(amount), tostring(cur + amount)))
end
end
end
function M.init(ctx)
local aliases = engine.module.asset_aliases()
@@ -910,6 +930,7 @@ function M.init(ctx)
local r = crafting.craft(recipe_id, c.container,
{ actor = state.actor })
if r.ok then
apply_grants(state.actor, r.granted)
local recipe = crafting.get_recipe(recipe_id)
notify.post{
tags = {"craft", "event"},
@@ -923,6 +944,8 @@ function M.init(ctx)
local msg
if r.error == "missing_inputs" then
msg = "Missing inputs"
elseif r.error == "requires_unmet" then
msg = "Skill too low: " .. table.concat(r.unmet or {}, ", ")
else
msg = "Cannot craft: " .. tostring(r.error)
end
@@ -993,6 +1016,7 @@ function M.init(ctx)
local r = crafting.craft(recipe_id, ctx_inner.locale,
{ actor = state.actor, at_workbench = true })
if r.ok then
apply_grants(state.actor, r.granted)
local recipe = crafting.get_recipe(recipe_id)
notify.post{
tags = {"craft", "event"},
@@ -1007,6 +1031,8 @@ function M.init(ctx)
local msg
if r.error == "missing_inputs" then
msg = "Missing inputs"
elseif r.error == "requires_unmet" then
msg = "Skill too low: " .. table.concat(r.unmet or {}, ", ")
else
msg = "Cannot craft: " .. tostring(r.error)
end