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:
79
README.md
79
README.md
@@ -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)
|
||||||
|
|
||||||
|
|||||||
77
init.lua
77
init.lua
@@ -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
|
||||||
|
|||||||
@@ -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"}]}
|
||||||
|
|||||||
Reference in New Issue
Block a user