rimlike/scenes/ui/work_priority_matrix.gd
megaproxy b9093dd24b Phase 17: Touch UX (PawnDetail+BuildDrawer+WorkMatrix+AlertsLog+Settings)
Three-agent fan-out shipping the major touch UI surfaces. Opus pre-wrote
6 EventBus signals (pawn_selected/deselected, pawn_priority_changed,
alert_added, request_wolf_spawn, day_ended) + Pawn.work_priorities
Dictionary stub before dispatch. Pattern proven across Phases 12-17.

Pawn detail + Settings (Agent A):
- scenes/ui/pawn_detail_panel.gd — right-side CanvasLayer (layer 18),
  ~360px wide, opens on EventBus.pawn_selected. Renders portrait,
  HP/Hunger/Sleep bars with threshold colors, current job, mood +
  sulking, statuses, top 5 mood thoughts, full skill table,
  read-only work-priorities row. Live-refreshes each sim tick.
- scenes/ui/settings_menu.gd — modal CanvasLayer (layer 26), opened
  via Settings button. Auto-pause toggles (Threat/Wanderer/Pawn-Down/
  Modal), audio sliders (stubs for Phase 18), accessibility checkboxes.
  Persists via GameState.apply_settings.
- scenes/world/selection.gd — extended to emit pawn_selected/deselected
  through EventBus on tap.

Build drawer + 12 new Designation tools (Agent B):
- scenes/ui/build_drawer.gd — bottom-sheet CanvasLayer (layer 16) with
  4 tabs (Designate/Build/Stockpile/Cancel) + FAB ⊕ open button.
  Each tab has HFlowContainer of 80×80 buttons with procedural colored
  icons + label. Tap → Designation.set_active_tool + alert + auto-close.
- Designation: added TOOL_CHOP, TOOL_MINE, TOOL_BUILD_CRATE,
  TOOL_BUILD_BED, TOOL_BUILD_TORCH, 5× TOOL_BUILD_WORKBENCH_* variants,
  TOOL_PAINT_STOCKPILE. Plus tool_material override for wall/floor.
- World._on_designation_added: extended dispatch for all 12 new tools;
  added _spawn_workbench() helper for the 5 bench kinds.

Work matrix + Alerts log + Decision refactor + Wolf signal (Agent C):
- scenes/ai/decision.gd: Layer 4 now filters by pawn.work_priorities
  (0=OFF skip, sort by level ascending with provider.priority tiebreak).
  NEEDS_CATEGORIES (rest/eat/sleep) bypass the filter — a pawn can
  never starve from misconfiguration. Audit log prefixes work decisions
  with (pri=N).
- scenes/ui/work_priority_matrix.gd — CanvasLayer (layer 17) bottom-sheet
  grid: rows=pawns × cols=8 work categories. Each cell tap-cycles
  1→2→3→4→0→1, color-coded (red/orange/yellow/blue/gray). Writes back
  to pawn.work_priorities + emits pawn_priority_changed.
- scenes/ui/alerts_log.gd — CanvasLayer (layer 19) ring buffer 50
  entries. Newest first, severity icon (info/warn/danger), Day HH:MM
  timestamp, Go-there camera pan. Listens to alert_added +
  storyteller_event_fired + day_ended.
- EventBus.request_wolf_spawn wired end-to-end: EventCatalog
  _spawn_wolves emits; WolfSpawner._on_request_wolf_spawn force-spawns
  bypassing the darkness/cooldown gates.
- Clock emits EventBus.day_ended(summary) at dusk→night transition.

Top bar buttons added in order: ‖ / 1× / 5× / 12× / Save / Load /
Settings / Build / Work / Log[N]. Plus the ⊕ FAB at bottom-right.

MCP runtime verified all 4 surfaces via screenshot:
- PawnDetailPanel: Bram shows Crafting=8 / Cooking=2 / Manual=0
  matching seed; bars green; Mood: 50; work-priorities readout
- BuildDrawer: 4 tabs visible, Designate tab shows Chop/Mine/Dig grave/
  No roof buttons with procedural icons
- WorkPriorityMatrix: 3 pawns × 8 categories, all '3' (NORMAL default)
  cells in yellow, tap-to-cycle ready
- AlertsLog: 4 entries — red 'Wolf pack approaching!' danger, blue
  'Bram is at the cabin' info, yellow 'Test alert' warn, blue 'Spring
  Awakens' from boot storyteller roll. Go-there button per entry.

Mouse drag-paint works as-is (user noted). Existing
Selection/Designation _unhandled_input handles drag.

Deferred to Phase 17.5 polish:
- Per-pawn/per-job view layers on the matrix
- Stockpile 4×4 chip filter UI (paint creates 1×1 zones today)
- Bill UI for workbenches (programmatic only today)
- 'No stockpile accepts X' / 'Bill blocked' alert emit wiring
- DaySummaryCard visual (signal emits today, no card UI)
- Wanderer recruit UI, resource buff system

Delegation: 3× gdscript-refactor (Sonnet) agents in parallel;
integration + MCP verify on Opus.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 19:45:35 +01:00

230 lines
7.6 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

class_name WorkPriorityMatrix extends CanvasLayer
## Phase 17 — Per-pawn work-priority matrix bottom-sheet.
##
## Opened via TopBar "Work" button (injected by main.gd).
## Displays one row per pawn × one column per player-configurable work category.
## Tapping a cell cycles the priority: 1 (Critical) → 2 → 3 → 4 (Low) → 0 (OFF) → 1.
##
## Priority semantics (mirrors Pawn.work_priorities and Decision layer 4):
## 0 = OFF (gray) — provider never picks this pawn for this category.
## 1 = Critical (red)
## 2 = High (orange)
## 3 = Normal (yellow) — default
## 4 = Low (blue)
##
## Category order matches the locked list in pawn.gd / Decision layer 4.
## &"doctor" is included — players can opt a pawn out of doctor duty even
## though the needs-driven healing path bypasses the filter at Layer 3.
##
## Save / load: NOT session-persistent for MVP — the matrix re-reads
## pawn.work_priorities live from the Pawn nodes every open, so any save that
## round-trips pawn.work_priorities (via pawn.to_dict) will reflect correctly.
const CATEGORIES: Array[StringName] = [
&"construction", &"chop", &"plant", &"mine", &"crafting", &"haul", &"clean", &"doctor"
]
const CATEGORY_LABELS: Dictionary = {
&"construction": "Build",
&"chop": "Chop",
&"plant": "Plant",
&"mine": "Mine",
&"crafting": "Craft",
&"haul": "Haul",
&"clean": "Clean",
&"doctor": "Doctor",
}
## Color per priority level for the cell buttons.
const PRIORITY_COLORS: Dictionary = {
0: Color(0.45, 0.45, 0.45, 1.0), # OFF — gray
1: Color(0.85, 0.18, 0.18, 1.0), # Critical — red
2: Color(0.90, 0.55, 0.10, 1.0), # High — orange
3: Color(0.85, 0.80, 0.10, 1.0), # Normal — yellow
4: Color(0.20, 0.40, 0.85, 1.0), # Low — blue
}
const PRIORITY_DISPLAY: Dictionary = {
0: "X",
1: "1",
2: "2",
3: "3",
4: "4",
}
## Panel height in pixels.
const PANEL_HEIGHT: float = 600.0
## Row height for each pawn row.
const ROW_H: float = 48.0
## Column width for category cells.
const COL_W: float = 64.0
## Width of the pawn-name column.
const NAME_COL_W: float = 100.0
var _root: Control = null
var _scroll: ScrollContainer = null
var _grid: GridContainer = null
## Flat list of [pawn, category, button] triples so we can refresh on open.
var _cells: Array = []
func _ready() -> void:
layer = 17
_build_ui()
_root.visible = false
Audit.log("work_priority_ui", "WorkPriorityMatrix ready")
# ── public API ────────────────────────────────────────────────────────────────
## Open the matrix panel and rebuild the grid from current pawn state.
func open() -> void:
_rebuild_grid()
_root.visible = true
Audit.log("work_priority_ui", "opened (pawns=%d)" % World.pawns.size())
## Close the panel.
func close() -> void:
_root.visible = false
Audit.log("work_priority_ui", "closed")
# ── UI construction ───────────────────────────────────────────────────────────
func _build_ui() -> void:
# Full-screen semi-transparent backdrop.
var backdrop := ColorRect.new()
backdrop.name = "Backdrop"
backdrop.set_anchors_preset(Control.PRESET_FULL_RECT)
backdrop.color = Color(0.0, 0.0, 0.0, 0.45)
backdrop.mouse_filter = Control.MOUSE_FILTER_STOP
backdrop.gui_input.connect(_on_backdrop_input)
add_child(backdrop)
# Bottom-sheet panel anchored to the bottom of the screen.
_root = Control.new()
_root.name = "MatrixPanel"
_root.set_anchors_preset(Control.PRESET_BOTTOM_WIDE)
_root.custom_minimum_size = Vector2(0.0, PANEL_HEIGHT)
_root.offset_top = -PANEL_HEIGHT
_root.offset_bottom = 0.0
_root.mouse_filter = Control.MOUSE_FILTER_STOP
backdrop.add_child(_root)
var bg := PanelContainer.new()
bg.name = "BG"
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
_root.add_child(bg)
var vbox := VBoxContainer.new()
vbox.add_theme_constant_override("separation", 4)
bg.add_child(vbox)
# Header row.
var header := HBoxContainer.new()
header.add_theme_constant_override("separation", 8)
vbox.add_child(header)
var title := Label.new()
title.text = "Work Priorities"
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
header.add_child(title)
var close_btn := Button.new()
close_btn.text = "Close"
close_btn.focus_mode = Control.FOCUS_NONE
close_btn.custom_minimum_size = Vector2(72.0, 36.0)
close_btn.pressed.connect(close)
header.add_child(close_btn)
# Scroll area for the grid (handles many pawns).
_scroll = ScrollContainer.new()
_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
vbox.add_child(_scroll)
# GridContainer: 1 + CATEGORIES.size() columns.
_grid = GridContainer.new()
_grid.columns = 1 + CATEGORIES.size()
_grid.add_theme_constant_override("h_separation", 4)
_grid.add_theme_constant_override("v_separation", 4)
_scroll.add_child(_grid)
# ── grid rebuild ──────────────────────────────────────────────────────────────
func _rebuild_grid() -> void:
# Clear existing children and cell tracking.
for child in _grid.get_children():
child.queue_free()
_cells.clear()
# Header row: "Pawn" + one label per category.
_add_header_cell("Pawn")
for cat in CATEGORIES:
_add_header_cell(CATEGORY_LABELS.get(cat, String(cat)))
# One data row per pawn in World.pawns.
for pawn in World.pawns:
# Pawn name cell.
var name_lbl := Label.new()
name_lbl.text = pawn.pawn_name
name_lbl.custom_minimum_size = Vector2(NAME_COL_W, ROW_H)
name_lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
name_lbl.clip_text = true
_grid.add_child(name_lbl)
# One button per category.
for cat in CATEGORIES:
var lvl: int = int(pawn.work_priorities.get(cat, 3))
var btn := Button.new()
btn.focus_mode = Control.FOCUS_NONE
btn.custom_minimum_size = Vector2(COL_W, ROW_H)
_apply_cell_style(btn, lvl)
# Capture pawn + category in the closure.
var p = pawn
var c: StringName = cat
btn.pressed.connect(func() -> void: _on_cell_pressed(p, c, btn))
_grid.add_child(btn)
_cells.append([pawn, cat, btn])
func _add_header_cell(text: String) -> void:
var lbl := Label.new()
lbl.text = text
lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
lbl.custom_minimum_size = Vector2(COL_W, 32.0)
lbl.add_theme_font_size_override("font_size", 11)
_grid.add_child(lbl)
func _apply_cell_style(btn: Button, lvl: int) -> void:
btn.text = PRIORITY_DISPLAY.get(lvl, "?")
btn.modulate = PRIORITY_COLORS.get(lvl, Color.WHITE)
# ── cell interaction ──────────────────────────────────────────────────────────
func _on_cell_pressed(pawn, category: StringName, btn: Button) -> void:
# Cycle: 1 → 2 → 3 → 4 → 0 → 1
var current: int = int(pawn.work_priorities.get(category, 3))
var next: int
match current:
0: next = 1
1: next = 2
2: next = 3
3: next = 4
4: next = 0
_: next = 3
pawn.work_priorities[category] = next
_apply_cell_style(btn, next)
EventBus.pawn_priority_changed.emit(pawn, category, next)
Audit.log("work_priority_ui", "%s: %s%d" % [pawn.pawn_name, String(category), next])
func _on_backdrop_input(event: InputEvent) -> void:
# Tap on the backdrop (outside the panel) closes the matrix.
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
close()
elif event is InputEventScreenTouch and event.pressed:
close()