# Combat Proposal UI Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add a centered combat forecast overlay to CombatUI that displays a CombatProposal with stats and Fight/Cancel buttons, using parent-wired signals for encapsulation. **Architecture:** CombatUI gains a CombatProposalPanel child and exposes `show_proposal()`, `fight_confirmed`, and `fight_cancelled`. A new parent script on the strategy_phase root node orchestrates the flow between PlayerController, CombatSystem, and CombatUI — the only place sibling references exist. **Tech Stack:** Godot 4.6, GDScript, `.tscn` scene files --- ## File Structure | File | Role | |---|---| | `resources/resource_definitions/combat_proposal.gd` | **Modify** — add `max_hp` field to `CombatantStats` | | `nodes/combat_system.gd` | **Modify** — populate `max_hp` in `_snapshot()` | | `scripts/combat_ui.gd` | **Modify** — add proposal panel logic, signals, input handling | | `prefabs/combat_ui.tscn` | **Modify** — add CombatProposalPanel node subtree | | `nodes/strategy_phase.gd` | **Create** — parent orchestration script | | `scenes/strategy_phase.tscn` | **Modify** — attach root script, rewire signals | --- ### Task 1: Add `max_hp` to CombatProposal and CombatSystem **Files:** - Modify: `resources/resource_definitions/combat_proposal.gd` - Modify: `nodes/combat_system.gd` - [ ] **Step 1: Add `max_hp` field to `CombatantStats`** In `resources/resource_definitions/combat_proposal.gd`, add `max_hp` to the inner class: ```gdscript class CombatantStats: var unit: Unit var max_hp: int var hp: int var sp: int var hit: int var atk: int var def: int var spd: int ``` - [ ] **Step 2: Populate `max_hp` in `_snapshot()`** In `nodes/combat_system.gd`, add the `max_hp` line to `_snapshot()`: ```gdscript func _snapshot(unit: Unit, opponent: Unit) -> CombatProposal.CombatantStats: var stats := CombatProposal.CombatantStats.new() stats.unit = unit stats.max_hp = unit.current_stats.max_hp stats.hp = unit.current_stats.current_hp stats.sp = unit.current_stats.current_sp stats.hit = unit.current_stats.hit - opponent.current_stats.eva stats.atk = unit.current_stats.phys_atk stats.def = unit.current_stats.phys_def stats.spd = unit.current_stats.spd return stats ``` - [ ] **Step 3: Verify in editor** Open the project in the Godot editor. Confirm no parse errors in the Output panel. The game should still run and the existing `process_combat` flow should work unchanged. - [ ] **Step 4: Commit** ```bash git add resources/resource_definitions/combat_proposal.gd nodes/combat_system.gd git commit -m "feat: add max_hp to CombatProposal.CombatantStats" ``` --- ### Task 2: Add CombatProposalPanel to `combat_ui.tscn` **Files:** - Modify: `prefabs/combat_ui.tscn` - [ ] **Step 1: Add the CombatProposalPanel subtree** Open `prefabs/combat_ui.tscn` in the Godot editor. Add the following node tree as a child of the root `CombatUI` CanvasLayer node: ``` CombatProposalPanel (PanelContainer) — unique name, centered, hidden by default └── MarginContainer (margin 12 all sides) └── VBoxContainer ├── TitleLabel (Label) — text: "COMBAT FORECAST", horizontal alignment: center ├── StatsContainer (HBoxContainer, separation: 24) │ ├── AttackerStats (VBoxContainer) │ │ ├── AttackerNameLabel (Label) — unique name, text: "Attacker" │ │ ├── AttackerHPBar (ProgressBar) — unique name, show_percentage: false │ │ ├── AttackerATKLabel (Label) — unique name, text: "ATK: 0" │ │ ├── AttackerDEFLabel (Label) — unique name, text: "DEF: 0" │ │ ├── AttackerHITLabel (Label) — unique name, text: "HIT: 0%" │ │ └── AttackerSPDLabel (Label) — unique name, text: "SPD: 0" │ └── DefenderStats (VBoxContainer) │ ├── DefenderNameLabel (Label) — unique name, text: "Defender" │ ├── DefenderHPBar (ProgressBar) — unique name, show_percentage: false │ ├── DefenderATKLabel (Label) — unique name, text: "ATK: 0" │ ├── DefenderDEFLabel (Label) — unique name, text: "DEF: 0" │ ├── DefenderHITLabel (Label) — unique name, text: "HIT: 0%" │ └── DefenderSPDLabel (Label) — unique name, text: "SPD: 0" └── ButtonContainer (HBoxContainer, horizontal alignment: center) ├── FightButton (Button) — text: "Fight" └── CancelButton (Button) — text: "Cancel" ``` **CombatProposalPanel settings:** - `visible = false` - `anchors_preset = 8` (Center) - `grow_horizontal = 2` (Both) - `grow_vertical = 2` (Both) - `theme = main_ui_theme.tres` (same as UnitPanel) All Label, ProgressBar, and Button nodes listed above must have "unique name in owner" checked so they can be referenced via `%NodeName` in script. - [ ] **Step 2: Verify in editor** Run the scene. The proposal panel should not be visible. The existing UnitPanel should still work as before (selecting a unit shows name + HP). - [ ] **Step 3: Commit** ```bash git add prefabs/combat_ui.tscn git commit -m "feat: add CombatProposalPanel to combat_ui scene" ``` --- ### Task 3: Add proposal logic to `combat_ui.gd` **Files:** - Modify: `scripts/combat_ui.gd` - [ ] **Step 1: Add signals, onready vars, and proposal state** Add the new signals, `@onready` references for the proposal panel nodes, and a `_current_proposal` variable. The full updated script: ```gdscript class_name CombatUI extends CanvasLayer signal fight_confirmed(proposal: CombatProposal) signal fight_cancelled @onready var unit_panel: PanelContainer = %UnitPanel @onready var name_label: Label = %NameLabel @onready var hp_bar: ProgressBar = %HPBar @onready var proposal_panel: PanelContainer = %CombatProposalPanel @onready var atk_name_label: Label = %AttackerNameLabel @onready var atk_hp_bar: ProgressBar = %AttackerHPBar @onready var atk_atk_label: Label = %AttackerATKLabel @onready var atk_def_label: Label = %AttackerDEFLabel @onready var atk_hit_label: Label = %AttackerHITLabel @onready var atk_spd_label: Label = %AttackerSPDLabel @onready var def_name_label: Label = %DefenderNameLabel @onready var def_hp_bar: ProgressBar = %DefenderHPBar @onready var def_atk_label: Label = %DefenderATKLabel @onready var def_def_label: Label = %DefenderDEFLabel @onready var def_hit_label: Label = %DefenderHITLabel @onready var def_spd_label: Label = %DefenderSPDLabel @onready var fight_button: Button = %FightButton @onready var cancel_button: Button = %CancelButton var _selected_unit: Unit var _current_proposal: CombatProposal func _ready() -> void: unit_panel.visible = false proposal_panel.visible = false fight_button.pressed.connect(_on_fight_pressed) cancel_button.pressed.connect(_on_cancel_pressed) for unit: Unit in get_tree().get_nodes_in_group("units"): unit.unit_selected_changed.connect(_on_unit_selected_changed) unit.unit_died.connect(_on_unit_died) get_tree().node_added.connect(_on_node_added) func _on_node_added(node: Node) -> void: if node is Unit and node.is_in_group("units"): if not node.unit_selected_changed.is_connected(_on_unit_selected_changed): node.unit_selected_changed.connect(_on_unit_selected_changed) if not node.unit_died.is_connected(_on_unit_died): node.unit_died.connect(_on_unit_died) func _on_unit_died(unit: Unit) -> void: if _selected_unit == unit: _selected_unit = null unit_panel.visible = false if _current_proposal: if _current_proposal.attacker.unit == unit or _current_proposal.defender.unit == unit: _hide_proposal() func _process(_delta: float) -> void: if _selected_unit and is_instance_valid(_selected_unit): hp_bar.max_value = _selected_unit.current_stats.max_hp hp_bar.value = _selected_unit.current_stats.current_hp func _unhandled_input(event: InputEvent) -> void: if proposal_panel.visible and event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_RIGHT: _hide_proposal() fight_cancelled.emit() get_viewport().set_input_as_handled() func _on_unit_selected_changed(unit: Unit, selected: bool) -> void: if selected: _selected_unit = unit name_label.text = unit.current_info.name hp_bar.max_value = unit.current_stats.max_hp hp_bar.value = unit.current_stats.current_hp unit_panel.visible = true else: _selected_unit = null unit_panel.visible = false func show_proposal(proposal: CombatProposal) -> void: _current_proposal = proposal var atk := proposal.attacker var def := proposal.defender atk_name_label.text = atk.unit.current_info.name atk_hp_bar.max_value = atk.max_hp atk_hp_bar.value = atk.hp atk_atk_label.text = "ATK: %d" % atk.atk atk_def_label.text = "DEF: %d" % atk.def atk_hit_label.text = "HIT: %d%%" % atk.hit atk_spd_label.text = "SPD: %d" % atk.spd def_name_label.text = def.unit.current_info.name def_hp_bar.max_value = def.max_hp def_hp_bar.value = def.hp def_atk_label.text = "ATK: %d" % def.atk def_def_label.text = "DEF: %d" % def.def def_hit_label.text = "HIT: %d%%" % def.hit def_spd_label.text = "SPD: %d" % def.spd proposal_panel.visible = true func _hide_proposal() -> void: proposal_panel.visible = false _current_proposal = null func _on_fight_pressed() -> void: if _current_proposal: var proposal := _current_proposal _hide_proposal() fight_confirmed.emit(proposal) func _on_cancel_pressed() -> void: _hide_proposal() fight_cancelled.emit() ``` - [ ] **Step 2: Verify in editor** Open the project. Confirm no parse errors. Run the game — the proposal panel should remain hidden. Selecting units should still show the UnitPanel as before. - [ ] **Step 3: Commit** ```bash git add scripts/combat_ui.gd git commit -m "feat: add combat proposal panel logic to CombatUI" ``` --- ### Task 4: Create parent orchestration script and rewire signals **Files:** - Create: `nodes/strategy_phase.gd` - Modify: `scenes/strategy_phase.tscn` - [ ] **Step 1: Create `nodes/strategy_phase.gd`** ```gdscript class_name StrategyPhase extends Node2D @onready var player_controller: PlayerController = $PlayerController @onready var combat_system: CombatSystem = $CombatSystem @onready var combat_ui: CombatUI = $CombatUI func _ready() -> void: player_controller.combat_requested.connect(_on_combat_requested) combat_ui.fight_confirmed.connect(_on_fight_confirmed) func _on_combat_requested(attacker: Unit, defender: Unit) -> void: var proposal := combat_system.create_proposal(attacker, defender) combat_ui.show_proposal(proposal) func _on_fight_confirmed(proposal: CombatProposal) -> void: combat_system.apply_proposal(proposal) ``` - [ ] **Step 2: Attach script and remove old signal connection in `strategy_phase.tscn`** Open `scenes/strategy_phase.tscn` in the Godot editor: 1. Select the root node `CombatTest` 2. Attach the script `res://nodes/strategy_phase.gd` to it 3. Go to the Node > Signals panel on `PlayerController` 4. Disconnect the existing `combat_requested` → `CombatSystem.process_combat` connection If editing the `.tscn` file directly, the changes are: - Add `[ext_resource type="Script" path="res://nodes/strategy_phase.gd" id="7_strat"]` to the ext_resources - Add `script = ExtResource("7_strat")` to the root `CombatTest` node - Remove the line `[connection signal="combat_requested" from="PlayerController" to="CombatSystem" method="process_combat"]` - [ ] **Step 3: Verify full flow** Run the game from the main menu: 1. Click Start — strategy phase loads with two units 2. Click the player unit (Putit) — UnitPanel appears bottom-left with name and HP 3. Click the enemy unit — **CombatProposalPanel appears centered** showing both units' stats 4. Click Cancel or right-click — panel closes, nothing happens 5. Repeat step 3, click Fight — panel closes, combat resolves (check console for combat log from `process_combat`... wait, `process_combat` is no longer wired) **Important:** The parent script calls `apply_proposal()` directly, not `process_combat()`. The console debug prints from `process_combat()` will no longer appear. This is correct — `process_combat()` was a convenience wrapper that created and immediately applied a proposal with debug logging. The new flow separates these steps. The debug prints can be added to the parent script if needed for development, but are not required. Verify combat works by checking that clicking Fight causes the defender to take damage (HP bar in UnitPanel updates when you re-select the damaged unit). - [ ] **Step 4: Commit** ```bash git add nodes/strategy_phase.gd scenes/strategy_phase.tscn git commit -m "feat: add parent orchestration script, wire combat proposal flow" ``` --- ### Task 5: Final verification and cleanup **Files:** - None new — verification pass only - [ ] **Step 1: Test the full happy path** 1. Launch game from main menu 2. Click Start 3. Select player unit → UnitPanel shows 4. Click enemy unit → CombatProposalPanel shows with correct stats for both units 5. Click Fight → panel closes, combat resolves 6. Select the damaged unit → UnitPanel HP bar reflects new HP 7. Repeat combat until a unit dies → unit disappears, panels hide correctly - [ ] **Step 2: Test cancel paths** 1. Open proposal panel 2. Click Cancel → panel closes, no combat 3. Open proposal panel again 4. Right-click → panel closes, no combat 5. Verify units are unaffected after cancels - [ ] **Step 3: Test edge case — unit dies during proposal** This is unlikely in the current single-player flow but worth verifying the guard in `_on_unit_died`: 1. This would require a unit dying from another source while the panel is open. For now, confirm that the `_on_unit_died` check in `combat_ui.gd` exists and would hide the panel if the attacker or defender dies. - [ ] **Step 4: Commit any fixes** If any issues were found and fixed during verification: ```bash git add -u git commit -m "fix: address issues found during combat proposal UI verification" ```