Inspector panel for the selected pawn or building #142
Labels
No labels
assets
bug
duplicate
enhancement
help wanted
invalid
question
testing
ui
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Blocks
Depends on
#136 Worker slot control on a selected construction site
liamjd/UntitledColonyBuilder
#78 Select a pawn or building on the map
liamjd/UntitledColonyBuilder
Reference
liamjd/UntitledColonyBuilder#142
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Part of milestone #6 (Selection and inspection). Depends on #78 (closed).
A panel that shows detailed information about whatever
GameState.selectedholds, whether it was selected on the map (#78), from the pawn list (#140) or from the building list (#141). It is the player-facing counterpart to the debug panel, and the place #136's worker-slot control will go.Acceptance criteria
state,cell, and current task (the task's label, or "idle").status. While PLANNED or UNDER_CONSTRUCTION it also shows construction progress and whether construction is blocked. It always shows whether the building is marked for demolition. When the building has a task, it shows the pawns on that task.GameState.selection_changedand on everyGameClock.tick, so values stay current while it is open. No_process.tools/screenshot_inspector.gd).Decisions (2026-10-10)
building_status_changed. Nothing fires when a pawn's state, cell or task changes, when a building's progress, blocked flag or demolition mark changes, or when a task's assignees change. Nearly all of these change inside the tick, so redrawing onGameClock.tickshows them at the right moment, at negligible cost for one panel. This is the same approach as the debug panel. A pawn'scellfollows its view between ticks, so it can lag by up to one tick.label, which already names the kind of work and its target cell. "idle" when the pawn holds no live task.Buildings._task_for()becomes publictask_for(). The panel needs a building's task to list its workers. #126 is not a blocker: it only simplifies that method's insides.TaskManagerandBuildingsare injected bygame.gd, astoast_notifier.tasksis.SelectionController._unhandled_input().Out of scope
Spec (drafted with Claude)
Decisions are in the issue body (2026-10-10). This outline and
test/unit/test_inspector_panel.gdare one spec. Where they disagree with the code, they are the spec.2.1 Files
ui/inspector/inspector_panel.gdclass_name InspectorPanel: three static row builders, and a panel that renders them forGameState.selectedui/inspector/inspector_panel.tscnmouse_filter = STOP,visible = falseentities/buildings/buildings.gd_task_for()renamed to publictask_for(). Update its two callers,mark_for_demolition()andunmark_for_demolition(). No behaviour changecommon/game.gd@onready var inspector_panel, plus two injections inside thenot Engine.is_editor_hint()guardcommon/game.tscninspector_panel.tscnunderUIHUDLayerasInspectorPaneltest/unit/test_inspector_panel.gdtools/screenshot_inspector.gd2.2 API: full signatures
Exact row contract. The tests pin the keys, their order and the values.
Dictionarykeeps insertion order, so insert in this order:title_for(pawn)"Pawn %d" % pawn.id, e.g."Pawn 3"title_for(building)"%s %d" % [building.type.display_name, building.id], e.g."Warehouse 4", the same asBuildingCardtitle_for(null)or another Entity""pawn_rows"State"Pawn.PawnState.keys()[pawn.state], e.g."IDLING""Cell"str(pawn.cell), e.g."(3, 4)""Task"task.label, orIDLE_TEXTwhentask == nullbuilding_rows"Status"Building.Status.keys()[building.status], e.g."UNDER_CONSTRUCTION""Progress""%d%%" % roundi(building.build_progress * 100.0), e.g."40%""Blocked"YES_TEXT/NO_TEXTfromconstruction_blocked"Marked for demolition"YES_TEXT/NO_TEXTfrommarked_for_demolition"Workers""Pawn 1, Pawn 3"intask.assigneesorder, orNONE_TEXTwhen emptytask != nullError paths. None push an error, and no test matches error text.
title_fornull, or anEntitythat is neither Pawn nor Building""pawn_rows/building_rowstask == null_task_fortasks/buildingsnot injected, or no tasknullrefreshGameState.selected == nullThe builders do not accept a null pawn or building.
refresh()never passes one.2.3 Each method's job, in one line
New
title_for(entity): the panel heading, built from the entity's kind and id. Pure.pawn_rows(pawn, task): the pawn's rows per the table above. Pure; takes the task rather than a manager, so tests need noTaskManager.building_rows(building, task): the building's rows per the table above. Pure, for the same reason.refresh(): readsGameState.selected. If null, setsvisible = falseand returns. Otherwise setsvisible = true, setstitle_lbl.text, builds the rows for its kind with_task_for(), and calls_render()._on_selection_changed(...): callsrefresh(). It ignores the arguments, becauserefresh()readsGameState.selecteditself._on_tick(...): callsrefresh()only whenGameState.selected != null, so a hidden panel does no work on each tick._task_for(entity): for a Pawn,tasks.get_task(pawn.current_task_id)whentasks != null. For a Building,buildings.task_for(building.id)whenbuildings != null. Otherwise null._render(rows):free()s every child ofrows_grid, then adds twoLabels per row: the key, then the value. Usefree()rather thanqueue_free()soget_child_count()is correct the moment it returns. They are scriptless leaf Labels with no callbacks in flight, the same argument asBuildings._despawn_view()._ready(): connects the two signals (2.4), then callsrefresh()once, so a panel created while something is already selected starts correct.Buildings.task_for(id): the existing_task_for()body, unchanged and made public.Existing code the new code relies on
GameState.selectedsetter: emitsselection_changed(new, previous)itself, and only on a real change. It is set to null when the selected pawn or building is removed (_clear_if_selected) and wheninteraction_modechanges. That is what makes "removing or a mode change hides the panel" work with no extra code here.TaskManager.get_task(id): returns null for an unknown id, without an error, soget_task(-1)for a pawn with no task is safe.Buildings._task_for(id)(soontask_for): scanstasks.all()for aConstructionTaskorDeconstructionTaskwith a matchingbuilding_id. Returns null whentasks == null.all()excludes completed and cancelled tasks.Task.label: set byBuildings.place()/mark_for_demolition()to text like"Construct ore_mine at (12, 5) for 3.0 days". It already names the target, which is why decision 4 shows it as is.Task.assignees: Array[int]: pawn ids, oldest claim first.GameClock.tick: emitted fromGameClock._process()viaadvance(), atticks_per_second(10) × speed. Not emitted while paused.SelectionController._unhandled_input(): selects onselectin NONE mode. It only sees a click that no Control consumed, so the panel must beMOUSE_FILTER_STOP.PanelContaineralready defaults to STOP, andMarginContainer/VBoxContainerdefault to PASS, which passes up to the root. Do not copy the list panels here: theirPanelContainersetsmouse_filter = 1(PASS) explicitly. The test pins STOP for that reason.2.4 Signals
No new signals, and nothing in this issue emits one.
selection_changed(new: Entity, previous: Entity)GameStateGameState.selectedsetterInspectorPanel._on_selection_changed()InspectorPanel._ready()tick(run_tick: int)GameClockGameClock.advance()InspectorPanel._on_tick()InspectorPanel._ready()The panel subscribes to
GameClock.tickitself, unlikePawns.on_tick, whichgame.gdconnects. Pawns avoids self-subscription because the live clock would drive work during unrelated tests. The panel only reads state, so a stray tick during a test is harmless.2.5 Wiring pattern
Dictionary, so tests call them without a scene.2.6 Scene structure
BuildingPanel, which is the build bar along the bottom.res://ui/inspector/inspector_panel.tscn,%TitleLbland%Rows. The tests readtitle_lbl,rows_grid,visibleandmouse_filter._renderorder matters: the tests read%Rows' children as key/value pairs, child2i= key and2i + 1= value.visible,columns, anchors.mouse_filterstays at the PanelContainer default (STOP). Set in code: label texts, and the%Rowschildren.game.gdadditions:2.7 Tests
test/unit/test_inspector_panel.gd:title_for: a pawn, a building, and the""fallback.pawn_rows: the key order, each value, the task's label, and "idle" for no task.building_rows: the BUILT key set (no Progress or Blocked), the PLANNED / UNDER_CONSTRUCTION key set, progress rounding, the blocked and marked flags, and Workers listed, "none" and absent.MOUSE_FILTER_STOP.BuildingsandTaskManager: a PLANNED building's Workers row comes throughtask_for().Buildings.task_for: returns the construction task of a planned building; null for a BUILT one with no task; null with no manager.Covered by tests: every criterion except the screenshots and the position. Headed only: how the panel looks and where it sits, via
tools/screenshot_inspector.gd.2.8 Checks
%TitleLbl,%Rows) is in 2.6.emit(...)calls to write out.Resources in tests are not freed.Spec amendment (2026-10-10): what the panel shows, not how it says it
This replaces the Exact row contract table in the spec comment above. The panel will change a lot as the game grows, so the tests no longer fix the row labels, their order or the exact values. The builders' signatures, the
Dictionary[String, String]return type and everything else in the outline are unchanged.What the tests now pin
title_for(pawn)title_for(building)display_nameand the building's idtitle_for(null)or another Entity""pawn_rowsMOVING),str(pawn.cell), and the task'slabel, orIDLE_TEXTwith no taskbuilding_rowsconstruction_blockedis truemarked_for_demolitionis truetask != nullNONE_TEXTwhen the task has no assignees. With no task there is no workers row at allThe flag rule is the one real change.
YES_TEXTandNO_TEXTgo away: an unset flag is noise, so it gets no row.IDLE_TEXTandNONE_TEXTstay as constants. The tests read them by name, so their wording can change freely.Panel tests
These are unchanged in intent. They compare the rendered rows with the builder's own output, or check that a fact appears, so none of them fixes any wording. There is one new test:
test_a_building_with_only_the_manager_injected_still_renders, which guards the_task_fornull case found in review.