# 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"
```
