Workbench bill editor — tap a workbench, see/edit bills

Tap-to-select chain extended to workbenches (pawn always wins on shared
tile). Mutually exclusive with pawn selection via EventBus —
selecting one clears the other.

New WorkbenchPanel (scenes/ui/workbench_panel.gd, ~432 LOC, layer 18,
right-anchored 360 px) mirrors PawnDetailPanel shape. Bill rows expose
recipe name, mode (FOREVER / COUNT / UNTIL_N), target count, completed
progress, pause, and remove. Add-bill popup filters RecipeCatalog.all()
by accepted_skill so a Hearth only offers cooking recipes.

Supporting plumbing:
- EventBus.workbench_selected / workbench_deselected signals.
- Workbench.remove_bill() — interrupts mid-craft cleanly via
  on_craft_interrupted() before erasing.
- RecipeCatalog.all() static enumerator + Recipe.display_name() helper.
- World.workbench_at_tile() lookup.
- i18n keys ui.bill.* and ui.workbench.* in strings.gd.

Closes the deferred Phase 17 "Bill UI for workbenches" item. Player-
built workbenches are now functionally configurable; before this
landed, only world.gd-hardcoded bills worked.
This commit is contained in:
megaproxy 2026-05-16 00:29:46 +01:00
parent e5f3693ad9
commit bdd435202d
9 changed files with 551 additions and 10 deletions

View file

@ -14,6 +14,9 @@ const CLICK_MAX_DURATION_MS: int = 300
var _pathfinder: Pathfinder = null
var _selected_pawn: Pawn = null
## Currently selected workbench, or null. Mutually exclusive with _selected_pawn —
## selecting one clears the other (see _select / _select_workbench).
var _selected_workbench: Workbench = null
var _camera = null # Camera2D (CameraRig) — set via bind_camera(); duck-typed to avoid circular preload
# When Designation paint mode is active this flag is raised by Designation so
@ -62,23 +65,32 @@ func _unhandled_input(event: InputEvent) -> void:
# ── Keyboard: Escape → deselect (lowest-priority; consumed last) ─────────────
# Designation._input handles Escape first; panels handle it in _unhandled_input
# before reaching here. If we still see it and have a selection, consume it.
if event.is_action_pressed("cancel") and _selected_pawn != null:
_deselect()
get_viewport().set_input_as_handled()
Audit.log("selection", "escape: deselected")
return
if event.is_action_pressed("cancel"):
if _selected_pawn != null:
_deselect()
get_viewport().set_input_as_handled()
Audit.log("selection", "escape: deselected pawn")
return
if _selected_workbench != null:
_deselect_workbench()
get_viewport().set_input_as_handled()
Audit.log("selection", "escape: deselected workbench")
return
# ── Mouse: only handle button events below ───────────────────────────────────
if not (event is InputEventMouseButton):
return
# ── Right-click: cancel designation (if active) or deselect pawn ─────────────
# ── Right-click: cancel designation (if active) or deselect pawn / workbench ──
if event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
# Designation cancellation is handled by Designation._input; if we see
# this right-click, no designation was active. Deselect any selected pawn.
# this right-click, no designation was active. Deselect whatever is selected.
if _selected_pawn != null:
_deselect()
get_viewport().set_input_as_handled()
elif _selected_workbench != null:
_deselect_workbench()
get_viewport().set_input_as_handled()
return
if event.button_index != MOUSE_BUTTON_LEFT:
@ -114,14 +126,23 @@ func _handle_click(screen_pos: Vector2) -> void:
floori(world_pos.y / float(Pawn.TILE_SIZE_PX)),
)
# Click on a pawn → select.
# Click on a pawn → select. Pawn wins over workbench when they share a tile
# (a pawn working at a bench is selectable; tap empty bench tile to inspect bills).
var hit_pawn: Pawn = World.pawn_at_tile(tile)
if hit_pawn != null:
_select(hit_pawn)
return
# Empty tile with no current selection → no-op.
# Click on a workbench → open the bill-editor panel.
var hit_workbench = World.workbench_at_tile(tile)
if hit_workbench != null:
_select_workbench(hit_workbench)
return
# Empty tile with no current pawn selection → also clear any workbench selection.
if _selected_pawn == null:
if _selected_workbench != null:
_deselect_workbench()
return
# Empty walkable tile with a selection → queue a forced job. Decision picks
@ -140,6 +161,9 @@ func _handle_click(screen_pos: Vector2) -> void:
func _select(pawn: Pawn) -> void:
if _selected_pawn == pawn:
return
# Mutual exclusion with workbench selection: clear it before promoting pawn.
if _selected_workbench != null:
_deselect_workbench()
if _selected_pawn != null:
_selected_pawn.set_selected(false)
EventBus.pawn_deselected.emit()
@ -159,6 +183,28 @@ func _deselect() -> void:
_selected_pawn = null
## Select a workbench → opens the bill-editor panel via EventBus.
## Mutually exclusive with pawn selection: clears _selected_pawn first.
func _select_workbench(wb) -> void:
if _selected_workbench == wb:
return
if _selected_pawn != null:
_deselect()
if _selected_workbench != null:
EventBus.workbench_deselected.emit()
_selected_workbench = wb
EventBus.workbench_selected.emit(wb)
Audit.log("selection", "selected workbench %s at %s" % [wb.label_text, wb.tile])
func _deselect_workbench() -> void:
if _selected_workbench == null:
return
Audit.log("selection", "deselected workbench %s" % _selected_workbench.label_text)
_selected_workbench = null
EventBus.workbench_deselected.emit()
## Cycle the selection forward (dir=1) or backward (dir=-1) through World.pawns.
## Wraps around. If no pawn currently selected, picks World.pawns[0].
## Pans the camera to the newly selected pawn's tile.