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>
This commit is contained in:
parent
19d28ca9f8
commit
b9093dd24b
25 changed files with 2138 additions and 44 deletions
230
scenes/ui/work_priority_matrix.gd
Normal file
230
scenes/ui/work_priority_matrix.gd
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue