feat: lib-core.crafting-display v0.1.0 — Recipe Panel-Widget
UI layer for recipe-based crafting. Reads recipes from
lib-core.crafting, renders the known-subset as a vertical row-list
inside a panel managed by lib-core.panel, and dispatches right-click
context-menu actions registered by the consuming module.
Surface:
create(container_entity, opts) -> widget_def
register_action(widget, label, callback)
unregister_action(widget, label)
set_icon_resolver(widget, fn)
set_label_resolver(widget, fn)
set_summary_resolver(widget, fn)
Row-build is rebuilt per render frame from crafting.list_recipes():
- Visibility gate: recipe.is_known(ctx); errors hide row + emit [WARN]
- Availability gate: crafting.can_craft(id, container, ctx).ok
drives row colour (full vs dim text_color).
ctx comes from opts.ctx_factory() — default returns {}; override to
feed actor/skill/faction state into both predicates. label_resolver
defaults to recipe.name or recipe.id; summary_resolver joins input
template-ids with " + " and suffixes "xN" for count>1.
Right-click hit-tests row-rects captured during render, then opens
the menu via panel.show_context_menu(x, y, actions). Action
callbacks receive (recipe_id, {container, close_menu, refresh}).
Registration order is preserved (array-based, not pairs-based) so
context-menu rendering matches registration sequence.
Depends on lib-core.crafting 0.1.0, lib-core.panel 0.1.1,
lib-core.inventory-list 0.1.0, lib-core.composition 0.3.0. The last
two are not called directly but are required as direct deps so
modules consuming this widget satisfy crafting's transitive needs
(engine resolver is per-module non-transitive).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
245
README.md
Normal file
245
README.md
Normal file
@@ -0,0 +1,245 @@
|
||||
# lib-core.crafting-display
|
||||
|
||||
Recipe panel-widget. Reads recipes via `lib-core.crafting` and renders
|
||||
the known-subset as a vertical row-list inside a panel managed by
|
||||
`lib-core.panel`. Each row shows label + summary; row colour reflects
|
||||
per-row availability (full-colour when craftable, dim when blocked).
|
||||
Right-clicking a row opens a context-menu populated from actions
|
||||
registered via `register_action`.
|
||||
|
||||
**Version:** 0.1.0
|
||||
**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
|
||||
**Tags:** crafting, ui, panel, widget, recipes
|
||||
|
||||
## Topology
|
||||
|
||||
<!-- topology:start (auto-generated; do not edit) -->
|
||||
```mermaid
|
||||
graph LR
|
||||
this["lib-core.crafting-display"]
|
||||
crafting["lib-core.crafting"]
|
||||
panel["lib-core.panel"]
|
||||
inv["lib-core.inventory-list"]
|
||||
comp["lib-core.composition"]
|
||||
this --> crafting
|
||||
this --> panel
|
||||
this --> inv
|
||||
this --> comp
|
||||
```
|
||||
<!-- topology:end -->
|
||||
|
||||
`inventory-list` and `composition` are declared as direct deps for the
|
||||
engine's per-module non-transitive resolver: this lib does not call
|
||||
them directly, but `crafting` does, and modules consuming this widget
|
||||
must satisfy them.
|
||||
|
||||
## Scope (v0.1.0)
|
||||
|
||||
v0.1 ships a minimal recipe-list widget:
|
||||
|
||||
- `create(container_entity, opts)` — builds a panel-compatible widget_def
|
||||
- `register_action` / `unregister_action` — context-menu actions (right-click on row)
|
||||
- `set_icon_resolver` / `set_label_resolver` / `set_summary_resolver` — override default row content
|
||||
- Render: label (left-aligned) + summary (right-aligned)
|
||||
- Row colour: full when `can_craft.ok==true`, dim otherwise
|
||||
- Input: right-click row opens context-menu via `panel.show_context_menu`
|
||||
|
||||
**Intentional non-goals (deferred):**
|
||||
- Row scrolling
|
||||
- Icon-column rendering (resolver returns nil by default)
|
||||
- Tooltip / hover-detail
|
||||
- Stack-count display
|
||||
|
||||
## Row Visibility + Availability rules
|
||||
|
||||
For every frame, the widget rebuilds its row-list by walking
|
||||
`crafting.list_recipes()` and applying two independent gates:
|
||||
|
||||
| Gate | Source | Effect on row |
|
||||
|-----------------|------------------------------------------|----------------------------------------------|
|
||||
| **Visibility** | `recipe.is_known(ctx)` | `false` → row is **hidden** entirely |
|
||||
| **Availability**| `crafting.can_craft(id, container, ctx)` | `ok=false` → row visible but **dim colour** |
|
||||
|
||||
`ctx` is built from `opts.ctx_factory()` (default `function() return {} end`).
|
||||
Override `ctx_factory` to wire in actor-state, skill-level, faction-membership,
|
||||
etc. — whatever the consuming module needs `is_known` and `can_craft` to see.
|
||||
|
||||
If `recipe.is_known(ctx)` raises, the row is hidden and a `[WARN]`
|
||||
message is emitted via `engine.print` (defensive — visibility predicate
|
||||
must not crash UI). No equivalent guard around `can_craft`: errors
|
||||
there propagate (consistent with rest of the lib stack).
|
||||
|
||||
## API
|
||||
|
||||
### `display.create(container_entity, opts)`
|
||||
|
||||
**Syntax:** `display.create(container_entity: entity, opts: table|nil) -> widget_def`
|
||||
|
||||
**Example:**
|
||||
```lua
|
||||
local display = require("lib-core.crafting-display")
|
||||
local panel = require("lib-core.panel")
|
||||
|
||||
local widget = display.create(workbench_entity, {
|
||||
title = "Workbench",
|
||||
widget_id = "crafting",
|
||||
pause_on_open = true,
|
||||
ctx_factory = function()
|
||||
return { actor = current_actor() }
|
||||
end,
|
||||
})
|
||||
panel.register(widget.widget_id, widget)
|
||||
panel.bind_default_trigger("c", widget.widget_id)
|
||||
```
|
||||
|
||||
Creates a widget_def bound to `container_entity` (used as the input
|
||||
source for `can_craft` availability checks). `opts` keys:
|
||||
- `title` (string, default `"Crafting"`) — panel title bar text
|
||||
- `widget_id` (string, default `"crafting"`) — key for `panel.register`
|
||||
- `pause_on_open` (bool, default `false`) — passed to panel for `is_pausing()`
|
||||
- `ctx_factory` (function, default `function() return {} end`) — per-frame ctx builder
|
||||
- `icon_resolver` (function) — overrides default icon resolver at creation time
|
||||
- `label_resolver` (function) — overrides default label resolver at creation time
|
||||
- `summary_resolver` (function) — overrides default summary resolver at creation time
|
||||
|
||||
Loud-error if `container_entity` is `nil`.
|
||||
|
||||
---
|
||||
|
||||
### `display.register_action(widget_def, label, callback)`
|
||||
|
||||
**Syntax:** `display.register_action(widget_def: table, label: string, callback: function) -> void`
|
||||
|
||||
**Example:**
|
||||
```lua
|
||||
display.register_action(widget, "Craft", function(recipe_id, ctx)
|
||||
crafting.craft(recipe_id, ctx.container, {})
|
||||
ctx.close_menu()
|
||||
end)
|
||||
```
|
||||
|
||||
Registers a context-menu action shown on right-click of any row. `callback`
|
||||
receives `(recipe_id, context)` where
|
||||
`context = { container, close_menu, refresh }`.
|
||||
|
||||
Loud-error on duplicate `label` or if `callback` is not a function.
|
||||
The default action-set is empty — all actions must be registered explicitly.
|
||||
|
||||
Registration order is preserved (entries are kept in an array). Identical
|
||||
order is reflected in the context-menu rendering.
|
||||
|
||||
---
|
||||
|
||||
### `display.unregister_action(widget_def, label)`
|
||||
|
||||
**Syntax:** `display.unregister_action(widget_def: table, label: string) -> void`
|
||||
|
||||
Removes a context-menu action. Idempotent: no error if `label` was never registered.
|
||||
|
||||
---
|
||||
|
||||
### `display.set_icon_resolver(widget_def, fn)`
|
||||
|
||||
**Syntax:** `display.set_icon_resolver(widget_def: table, fn: function) -> void`
|
||||
|
||||
Replaces the icon resolver. `fn(recipe) -> any|nil` — return value is
|
||||
opaque to v0.1 (default icon column not yet rendered; resolver is wired
|
||||
in for v0.2 atlas-icon support and for tests).
|
||||
|
||||
---
|
||||
|
||||
### `display.set_label_resolver(widget_def, fn)`
|
||||
|
||||
**Syntax:** `display.set_label_resolver(widget_def: table, fn: function) -> void`
|
||||
|
||||
**Example:**
|
||||
```lua
|
||||
display.set_label_resolver(widget, function(recipe)
|
||||
return localize("recipe." .. recipe.id) or recipe.name or recipe.id
|
||||
end)
|
||||
```
|
||||
|
||||
Replaces the label resolver. `fn(recipe) -> string`. Called each render frame per row.
|
||||
Default returns `recipe.name or recipe.id`.
|
||||
|
||||
---
|
||||
|
||||
### `display.set_summary_resolver(widget_def, fn)`
|
||||
|
||||
**Syntax:** `display.set_summary_resolver(widget_def: table, fn: function) -> void`
|
||||
|
||||
**Example:**
|
||||
```lua
|
||||
display.set_summary_resolver(widget, function(recipe)
|
||||
return string.format("%d ingredients", #recipe.inputs)
|
||||
end)
|
||||
```
|
||||
|
||||
Replaces the summary resolver. `fn(recipe) -> string`. Called each render
|
||||
frame per row. Default joins `recipe.inputs` template-ids with `" + "`,
|
||||
suffixing `"×N"` when `count>1`.
|
||||
|
||||
---
|
||||
|
||||
## Default Resolvers
|
||||
|
||||
| Resolver | Default behavior |
|
||||
|--------------------|-------------------------------------------------------------|
|
||||
| `label_resolver` | `recipe.name or recipe.id` |
|
||||
| `summary_resolver` | `inputs[*].template` joined with `" + "`, `"×N"` when `count>1` |
|
||||
| `icon_resolver` | returns `nil` (no icon in v0.1) |
|
||||
| `ctx_factory` | returns `{}` (empty table; replace to feed actor/skill ctx) |
|
||||
|
||||
## Test backdoors
|
||||
|
||||
- `display._test_get_rows(widget_def)` — returns the per-frame row list
|
||||
(`{recipe, available}` pairs after is_known filtering)
|
||||
- `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_summary(widget_def, recipe)` — invokes current summary resolver
|
||||
|
||||
All four are for test modules only and should not be called in
|
||||
production code.
|
||||
|
||||
## Glue-Pattern
|
||||
|
||||
Minimal module setup:
|
||||
|
||||
```lua
|
||||
local display = require("lib-core.crafting-display")
|
||||
local crafting = require("lib-core.crafting")
|
||||
local panel = require("lib-core.panel")
|
||||
|
||||
-- 1. Define recipes
|
||||
crafting.define_recipe{
|
||||
id = "rock_pick",
|
||||
inputs = {
|
||||
{ template = "items.rock", count = 1 },
|
||||
{ template = "items.stick", count = 1 },
|
||||
},
|
||||
output = { template = "items.rock_pick", count = 1 },
|
||||
name = "Rock Pick",
|
||||
}
|
||||
|
||||
-- 2. Create widget (workbench_entity is a list-container)
|
||||
local widget = display.create(workbench_entity, {
|
||||
title = "Workbench",
|
||||
widget_id = "crafting",
|
||||
pause_on_open = true,
|
||||
})
|
||||
|
||||
-- 3. Register actions
|
||||
display.register_action(widget, "Craft", function(recipe_id, ctx)
|
||||
crafting.craft(recipe_id, ctx.container, {})
|
||||
ctx.close_menu()
|
||||
end)
|
||||
|
||||
-- 4. Register with panel + bind toggle key
|
||||
panel.register(widget.widget_id, widget)
|
||||
panel.bind_default_trigger("c", widget.widget_id)
|
||||
|
||||
-- 5. Wire into update + render
|
||||
function M.update(dt) panel.update(dt) end
|
||||
function M.render() panel.render() end
|
||||
```
|
||||
Reference in New Issue
Block a user