feat: v0.2.0 locale_factory replaces single container

M.create now accepts either a function (locale_factory, called per
frame to resolve the current crafting locale) or a bare entity-handle
(wrapped internally as a constant factory for v0.1 bw-compat). The
factory's return value is forwarded to crafting.can_craft as the new
multi-source / single-sink locale, enabling dynamic topologies
(adjacent-container sets that change as the actor moves) without
recreating the widget.

Action callbacks now receive ctx_inner with both `locale` (the
currently-resolved locale, either the Form-2 table or the Form-1 bare
handle) and `container` as a bw-compat alias pointing at locale.sink
(Form-2) or the handle itself (Form-1). Existing v0.1 callers using
ctx.container keep working unchanged.

New backdoor _test_get_locale exposes the locale_factory's current
return value. README documents both Form-1 and Form-2 with examples
plus the updated callback contract.

Depends on lib-core.crafting v0.2.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Calic
2026-06-14 19:43:49 +02:00
parent b42b707a8e
commit 31e51ee8b7
3 changed files with 128 additions and 30 deletions

View File

@@ -7,9 +7,9 @@ per-row availability (full-colour when craftable, dim when blocked).
Right-clicking a row opens a context-menu populated from actions Right-clicking a row opens a context-menu populated from actions
registered via `register_action`. registered via `register_action`.
**Version:** 0.1.0 **Version:** 0.2.0
**Lib-ID:** lib-core.crafting-display **Lib-ID:** lib-core.crafting-display
**Requires:** lib-core.crafting v0.1.0, lib-core.panel v0.1.1, lib-core.inventory-list v0.1.0, lib-core.composition v0.3.0 **Requires:** lib-core.crafting v0.2.0, lib-core.panel v0.1.1, lib-core.inventory-list v0.1.0, lib-core.composition v0.3.0
**Tags:** crafting, ui, panel, widget, recipes **Tags:** crafting, ui, panel, widget, recipes
## Topology ## Topology
@@ -86,7 +86,11 @@ For every frame, the widget rebuilds its row-list by walking
| Gate | Source | Effect on row | | Gate | Source | Effect on row |
|-----------------|------------------------------------------|----------------------------------------------| |-----------------|------------------------------------------|----------------------------------------------|
| **Visibility** | `recipe.is_known(ctx)` | `false` → row is **hidden** entirely | | **Visibility** | `recipe.is_known(ctx)` | `false` → row is **hidden** entirely |
| **Availability**| `crafting.can_craft(id, container, ctx)` | `ok=false` → row visible but **dim colour** | | **Availability**| `crafting.can_craft(id, locale, ctx)` | `ok=false` → row visible but **dim colour** |
The `locale` here is whatever the widget's locale-factory returns for
the current frame (v0.2: per-frame fresh; v0.1 bw-compat: constant
single-container).
`ctx` is built from `opts.ctx_factory()` (default `function() return {} end`). `ctx` is built from `opts.ctx_factory()` (default `function() return {} end`).
Override `ctx_factory` to wire in actor-state, skill-level, faction-membership, Override `ctx_factory` to wire in actor-state, skill-level, faction-membership,
@@ -99,11 +103,21 @@ there propagate (consistent with rest of the lib stack).
## API ## API
### `display.create(container_entity, opts)` ### `display.create(arg, opts) -> widget_def`
**Syntax:** `display.create(container_entity: entity, opts: table|nil) -> widget_def` Creates a crafting widget. **`arg`** ist eines von:
**Example:** - **Form 1 (Bw-Compat v0.1):** bare container-entity-handle. Intern
als constant locale_factory gewrappt (`function() return arg end`).
- **Form 2 (v0.2 NEU):** `function() -> locale` — wird pro Frame
aufgerufen. Locale ist entweder ein entity-handle (Form-1 sub-
semantik) oder eine Table `{sources={...}, sink=...}`. Forward-
compat zu dynamischen Topologien (player-bewegt-sich →
adjacent-Container-Set ändert sich, ohne widget-Recreate).
**Syntax:** `display.create(arg: entity | function() -> locale, opts: table|nil) -> widget_def`
**Example (Form 1, bw-compat):**
```lua ```lua
local display = require("lib-core.crafting-display") local display = require("lib-core.crafting-display")
local panel = require("lib-core.panel") local panel = require("lib-core.panel")
@@ -120,8 +134,17 @@ panel.register(widget.widget_id, widget)
panel.bind_default_trigger("c", widget.widget_id) panel.bind_default_trigger("c", widget.widget_id)
``` ```
Creates a widget_def bound to `container_entity` (used as the input **Example (Form 2, locale-factory):**
source for `can_craft` availability checks). `opts` keys: ```lua
local widget = display.create(function()
return {
sources = adjacent_containers(player),
sink = player_backpack,
}
end, { title = "Workbench" })
```
`opts` keys:
- `title` (string, default `"Crafting"`) — panel title bar text - `title` (string, default `"Crafting"`) — panel title bar text
- `widget_id` (string, default `"crafting"`) — key for `panel.register` - `widget_id` (string, default `"crafting"`) — key for `panel.register`
- `pause_on_open` (bool, default `false`) — passed to panel for `is_pausing()` - `pause_on_open` (bool, default `false`) — passed to panel for `is_pausing()`
@@ -130,7 +153,28 @@ source for `can_craft` availability checks). `opts` keys:
- `label_resolver` (function) — overrides default label resolver at creation time - `label_resolver` (function) — overrides default label resolver at creation time
- `summary_resolver` (function) — overrides default summary resolver at creation time - `summary_resolver` (function) — overrides default summary resolver at creation time
Loud-error if `container_entity` is `nil`. Loud-error if `arg` is `nil`.
---
### Action-Callback Signatur (v0.2.0 Update)
Callbacks erhalten `(recipe_id, ctx_inner)` wo `ctx_inner`:
```lua
{
locale = <currently-resolved locale>, -- v0.2 NEU
container = <sink (Form-2) or handle (Form-1)>, -- bw-compat alias
close_menu = function() ... end,
refresh = function() ... end,
}
```
Modul-Aktion ruft typisch `crafting.craft(recipe_id,
ctx_inner.locale, ctx)`. Existing v0.1 callers die
`ctx_inner.container` benutzen funktionieren weiter via Form-1
bw-compat-shim (sink-handle wird intern als single-container
behandelt).
--- ---
@@ -141,14 +185,18 @@ Loud-error if `container_entity` is `nil`.
**Example:** **Example:**
```lua ```lua
display.register_action(widget, "Craft", function(recipe_id, ctx) display.register_action(widget, "Craft", function(recipe_id, ctx)
crafting.craft(recipe_id, ctx.container, {}) -- v0.2: prefer ctx.locale; ctx.container is a bw-compat alias.
crafting.craft(recipe_id, ctx.locale, {})
ctx.close_menu() ctx.close_menu()
end) end)
``` ```
Registers a context-menu action shown on right-click of any row. `callback` Registers a context-menu action shown on right-click of any row. `callback`
receives `(recipe_id, context)` where receives `(recipe_id, ctx_inner)` where
`context = { container, close_menu, refresh }`. `ctx_inner = { locale, container, close_menu, refresh }`.
`locale` carries the currently-resolved locale (table for Form-2, bare
handle for Form-1). `container` is a bw-compat alias pointing at
`locale.sink` (Form-2) or the handle itself (Form-1).
Loud-error on duplicate `label` or if `callback` is not a function. Loud-error on duplicate `label` or if `callback` is not a function.
The default action-set is empty — all actions must be registered explicitly. The default action-set is empty — all actions must be registered explicitly.
@@ -222,11 +270,13 @@ suffixing `"×N"` when `count>1`.
- `display._test_get_rows(widget_def)` — returns the per-frame row list - `display._test_get_rows(widget_def)` — returns the per-frame row list
(`{recipe, available}` pairs after is_known filtering) (`{recipe, available}` pairs after is_known filtering)
- `display._test_get_locale(widget_def)` — invokes the configured
locale_factory and returns whatever it produces (v0.2)
- `display._test_resolve_icon(widget_def, recipe)` — invokes current icon resolver - `display._test_resolve_icon(widget_def, recipe)` — invokes current icon resolver
- `display._test_resolve_label(widget_def, recipe)` — invokes current label resolver - `display._test_resolve_label(widget_def, recipe)` — invokes current label resolver
- `display._test_resolve_summary(widget_def, recipe)` — invokes current summary resolver - `display._test_resolve_summary(widget_def, recipe)` — invokes current summary resolver
All four are for test modules only and should not be called in All five are for test modules only and should not be called in
production code. production code.
## Glue-Pattern ## Glue-Pattern
@@ -258,7 +308,8 @@ local widget = display.create(workbench_entity, {
-- 3. Register actions -- 3. Register actions
display.register_action(widget, "Craft", function(recipe_id, ctx) display.register_action(widget, "Craft", function(recipe_id, ctx)
crafting.craft(recipe_id, ctx.container, {}) -- v0.2: prefer ctx.locale (ctx.container is a bw-compat alias).
crafting.craft(recipe_id, ctx.locale, {})
ctx.close_menu() ctx.close_menu()
end) end)

View File

@@ -1,5 +1,5 @@
-- ===================================================================== -- =====================================================================
-- lib-core.crafting-display v0.1.0 — Recipe Panel-Widget -- lib-core.crafting-display v0.2.0 — Recipe Panel-Widget
-- --
-- Sits on top of lib-core.panel and reads lib-core.crafting + -- Sits on top of lib-core.panel and reads lib-core.crafting +
-- lib-core.inventory-list + lib-core.composition. Provides a ready-to- -- lib-core.inventory-list + lib-core.composition. Provides a ready-to-
@@ -10,7 +10,14 @@
-- registered via M.register_action. -- registered via M.register_action.
-- --
-- Public API: -- Public API:
-- display.create(container_entity, opts) -> widget_def -- display.create(arg, opts) -> widget_def
-- `arg` is either:
-- - a bare entity-handle (Form-1, v0.1 bw-compat) — wrapped as
-- a constant locale_factory internally; or
-- - a `function() -> locale` (Form-2, v0.2) — invoked per frame
-- to obtain the currently-resolved locale, which itself may be
-- a bare entity-handle or a `{sources={...}, sink=...}` table
-- as understood by `lib-core.crafting`.
-- display.register_action(widget_def, label, callback) -- display.register_action(widget_def, label, callback)
-- display.unregister_action(widget_def, label) -- display.unregister_action(widget_def, label)
-- display.set_icon_resolver(widget_def, fn) -- display.set_icon_resolver(widget_def, fn)
@@ -87,6 +94,10 @@ end
-- --------------------------------------------------------------------- -- ---------------------------------------------------------------------
local function build_rows(widget) local function build_rows(widget)
local locale = widget._locale_factory()
if locale == nil then
return {}
end
local ctx = widget._ctx_factory() local ctx = widget._ctx_factory()
if type(ctx) ~= "table" then ctx = {} end if type(ctx) ~= "table" then ctx = {} end
local out = {} local out = {}
@@ -99,7 +110,7 @@ local function build_rows(widget)
recipe.id, tostring(known))) recipe.id, tostring(known)))
end end
elseif known == true then elseif known == true then
local match = crafting.can_craft(recipe.id, widget._container, ctx) local match = crafting.can_craft(recipe.id, locale, ctx)
out[#out + 1] = { out[#out + 1] = {
recipe = recipe, recipe = recipe,
available = match.ok == true, available = match.ok == true,
@@ -113,9 +124,15 @@ end
-- Public API: create -- Public API: create
-- --------------------------------------------------------------------- -- ---------------------------------------------------------------------
--- M.create(container_entity, opts) -> widget_def --- M.create(arg, opts) -> widget_def
--- Creates a crafting-recipe widget bound to `container_entity` (used as --- Creates a crafting-recipe widget. `arg` is either:
--- the input source for can_craft availability checks). --- - a bare entity-handle (Form-1, bw-compat) used as the input source
--- for can_craft availability; internally wrapped as a constant
--- locale_factory `function() return arg end`; or
--- - a `function() -> locale` (Form-2) — called per frame to obtain
--- the current locale. The returned locale may itself be a bare
--- handle (Form-1 sub-semantics) or a `{sources={...}, sink=...}`
--- table as accepted by `lib-core.crafting`.
--- opts = { --- opts = {
--- title = string, default "Crafting" --- title = string, default "Crafting"
--- widget_id = string, default "crafting" --- widget_id = string, default "crafting"
@@ -125,14 +142,23 @@ end
--- label_resolver = function(recipe) -> string --- label_resolver = function(recipe) -> string
--- summary_resolver = function(recipe) -> string --- summary_resolver = function(recipe) -> string
--- } --- }
--- Loud-error if container_entity is nil. --- Loud-error if `arg` is nil.
function M.create(container_entity, opts) function M.create(arg, opts)
if container_entity == nil then
error("crafting-display.create: container must not be nil", 2)
end
opts = opts or {} opts = opts or {}
if arg == nil then
error("crafting-display.create: first arg (locale or "
.. "container) must not be nil", 2)
end
local locale_factory
if type(arg) == "function" then
locale_factory = arg
else
-- Bw-compat: bare entity-handle (userdata in Sporel) wrapped
-- as constant factory.
locale_factory = function() return arg end
end
local widget = { local widget = {
_container = container_entity, _locale_factory = locale_factory,
_opts = opts, _opts = opts,
_actions = {}, _actions = {},
_ctx_factory = opts.ctx_factory or default_ctx_factory, _ctx_factory = opts.ctx_factory or default_ctx_factory,
@@ -143,6 +169,8 @@ function M.create(container_entity, opts)
widget_id = opts.widget_id or "crafting", widget_id = opts.widget_id or "crafting",
pause_on_open = opts.pause_on_open == true, pause_on_open = opts.pause_on_open == true,
} }
-- widget.render(ctx) + widget.handle_input(ctx, event) — panel
-- contract per ADR-0049 / panel/README.md
function widget.render(ctx) function widget.render(ctx)
M._render_widget(widget, ctx) M._render_widget(widget, ctx)
end end
@@ -158,7 +186,12 @@ end
--- M.register_action(widget, label, callback) --- M.register_action(widget, label, callback)
--- Registers a context-menu action shown on right-click of any row. --- Registers a context-menu action shown on right-click of any row.
--- callback(recipe_id, context) where context = { container, close_menu, refresh }. --- callback(recipe_id, ctx_inner) where
--- ctx_inner = { locale, container, close_menu, refresh }
--- `locale` is the currently-resolved locale (Form-2 table or Form-1
--- bare handle, depending on what the locale_factory returns).
--- `container` is a bw-compat alias pointing at `locale.sink` (Form-2)
--- or the handle itself (Form-1).
--- Loud-error on duplicate label or non-function callback. --- Loud-error on duplicate label or non-function callback.
function M.register_action(widget, label, callback) function M.register_action(widget, label, callback)
if type(label) ~= "string" or label == "" then if type(label) ~= "string" or label == "" then
@@ -262,6 +295,14 @@ function M._handle_input_widget(widget, _ctx, event)
end end
function M._invoke_context_menu(widget, recipe_id, mx, my) function M._invoke_context_menu(widget, recipe_id, mx, my)
local locale = widget._locale_factory()
local sink_alias
if type(locale) == "table" then
sink_alias = locale.sink
else
-- Form-1: bare handle is both source + sink.
sink_alias = locale
end
local entries = {} local entries = {}
for _, a in ipairs(widget._actions) do for _, a in ipairs(widget._actions) do
local cb = a.callback -- capture for closure local cb = a.callback -- capture for closure
@@ -269,8 +310,10 @@ function M._invoke_context_menu(widget, recipe_id, mx, my)
label = a.label, label = a.label,
callback = function(menu_ctx) callback = function(menu_ctx)
cb(recipe_id, { cb(recipe_id, {
container = widget._container, locale = locale,
close_menu = menu_ctx.close_menu, container = sink_alias, -- bw-compat alias
close_menu = (menu_ctx and menu_ctx.close_menu)
or function() end,
refresh = function() end, -- v0.1: free (next frame re-reads) refresh = function() end, -- v0.1: free (next frame re-reads)
}) })
end, end,
@@ -290,6 +333,10 @@ function M._test_get_rows(widget)
return build_rows(widget) return build_rows(widget)
end end
function M._test_get_locale(widget)
return widget._locale_factory()
end
function M._test_resolve_icon(widget, recipe) function M._test_resolve_icon(widget, recipe)
return widget._icon_resolver(recipe) return widget._icon_resolver(recipe)
end end

View File

@@ -1 +1 @@
{"id":"lib-core.crafting-display","version":"0.1.0","api_min":"0.1","deps":[{"id":"lib-core.crafting","version":"0.1.0"},{"id":"lib-core.panel","version":"0.1.1"},{"id":"lib-core.inventory-list","version":"0.1.0"},{"id":"lib-core.composition","version":"0.3.0"}]} {"id":"lib-core.crafting-display","version":"0.2.0","api_min":"0.1","deps":[{"id":"lib-core.crafting","version":"0.2.0"},{"id":"lib-core.panel","version":"0.1.1"},{"id":"lib-core.inventory-list","version":"0.1.0"},{"id":"lib-core.composition","version":"0.3.0"}]}