Files
MaidEngine/docs/superpowers/specs/2026-04-02-combat-proposal-ui-design.md

166 lines
7.6 KiB
Markdown

# Combat Proposal UI Design
## Overview
Add a centered combat forecast overlay to the existing CombatUI that displays a `CombatProposal` with attacker/defender stats and Fight/Cancel buttons. The system uses parent-wired signals — no node references outside parent-child relationships.
## Signal Flow
When a player clicks a defender while an attacker is selected:
1. `PlayerController` emits `combat_requested(attacker, defender)`
2. Parent scene script catches it, calls `CombatSystem.create_proposal(attacker, defender)`
3. Parent calls `CombatUI.show_proposal(proposal)`
4. Player sees the combat forecast panel
On Fight:
1. `CombatUI` emits `fight_confirmed(proposal)`
2. Parent catches it, calls `CombatSystem.apply_proposal(proposal)`
3. Panel closes
On Cancel (Cancel button or right-click):
1. `CombatUI` emits `fight_cancelled`
2. Parent catches it (no action needed beyond the signal existing for future use)
3. Panel closes
## Architecture
### Encapsulation Rules
- **CombatUI** knows only about its own children (UnitPanel, CombatProposalPanel, buttons). Exposes signals and public methods.
- **PlayerController** knows nothing about CombatUI or CombatSystem. Emits `combat_requested`.
- **CombatSystem** knows nothing about UI. Exposes `create_proposal()` and `apply_proposal()`.
- **Parent scene script** (`strategy_phase.tscn` root node) is the only place that references sibling nodes via `@onready`. It wires signals in `_ready()` and contains the orchestration logic.
### Parent Scene Script
A new script on the `strategy_phase.tscn` root node (~20 lines) that:
- Holds `@onready` references to `PlayerController`, `CombatSystem`, and `CombatUI`
- Connects `PlayerController.combat_requested` to a local method that creates a proposal and passes it to `CombatUI.show_proposal()`
- Connects `CombatUI.fight_confirmed` to a local method that calls `CombatSystem.apply_proposal()`
- Connects `CombatUI.fight_cancelled` (no-op for now, but wired for future use)
This replaces the current direct signal connection from `PlayerController.combat_requested` to `CombatSystem.process_combat` in the scene editor.
## CombatUI Changes
### New Signals
- `fight_confirmed(proposal: CombatProposal)` — emitted when Fight button pressed
- `fight_cancelled` — emitted when Cancel button pressed or right-click detected
### New Public Methods
- `show_proposal(proposal: CombatProposal)` — stores the proposal reference, populates all labels and progress bars from the proposal's `CombatantStats`, shows the CombatProposalPanel
- `_hide_proposal()` — hides the panel, clears stored proposal reference
### Input Handling
Right-click while the proposal panel is visible triggers `_hide_proposal()` and emits `fight_cancelled`. Handled via `_unhandled_input` on CombatUI.
### Existing Unit Panel
The bottom-left unit info panel remains visible and unaffected. It continues to show the selected unit's info, which provides useful context alongside the forecast.
## CombatProposalPanel Layout
New `PanelContainer` child of the CombatUI CanvasLayer, centered on screen via anchor presets. Hidden by default.
```
┌─────────────────────────────────────────────┐
│ COMBAT FORECAST │
│ │
│ [Attacker] [Defender] │
│ Name Name │
│ HP ████████░░ HP ██████░░░░ │
│ ATK: 12 ATK: 8 │
│ DEF: 5 DEF: 6 │
│ HIT: 78% HIT: 65% │
│ SPD: 10 SPD: 7 │
│ │
│ [ Fight ] [ Cancel ] │
└─────────────────────────────────────────────┘
```
### Scene Structure
```
CombatUI (CanvasLayer) — combat_ui.gd
├── UnitPanel (PanelContainer) — existing, bottom-left
│ └── MarginContainer
│ └── VBoxContainer
│ ├── NameLabel (Label)
│ └── HPBar (ProgressBar)
└── CombatProposalPanel (PanelContainer) — new, centered
└── MarginContainer
└── VBoxContainer
├── TitleLabel (Label) — "COMBAT FORECAST"
├── StatsContainer (HBoxContainer)
│ ├── AttackerStats (VBoxContainer)
│ │ ├── AttackerNameLabel (Label)
│ │ ├── AttackerHPBar (ProgressBar)
│ │ ├── AttackerATKLabel (Label) — "ATK: {value}"
│ │ ├── AttackerDEFLabel (Label) — "DEF: {value}"
│ │ ├── AttackerHITLabel (Label) — "HIT: {value}%"
│ │ └── AttackerSPDLabel (Label) — "SPD: {value}"
│ └── DefenderStats (VBoxContainer)
│ ├── DefenderNameLabel (Label)
│ ├── DefenderHPBar (ProgressBar)
│ ├── DefenderATKLabel (Label) — "ATK: {value}"
│ ├── DefenderDEFLabel (Label) — "DEF: {value}"
│ ├── DefenderHITLabel (Label) — "HIT: {value}%"
│ └── DefenderSPDLabel (Label) — "SPD: {value}"
└── ButtonContainer (HBoxContainer) — centered
├── FightButton (Button) — "Fight"
└── CancelButton (Button) — "Cancel"
```
### Data Mapping
All values come from `CombatProposal.CombatantStats`:
| Panel Field | Source |
|---|---|
| Name | `combatant_stats.unit.info.name` |
| HP bar max | `combatant_stats.max_hp` |
| HP bar value | `combatant_stats.hp` |
| ATK | `combatant_stats.atk` |
| DEF | `combatant_stats.def` |
| HIT | `combatant_stats.hit` (already adjusted: attacker hit - defender eva) |
| SPD | `combatant_stats.spd` |
### CombatProposal Change
`CombatantStats` in `resources/resource_definitions/combat_proposal.gd` needs a new `max_hp: int` field added so the HP bar can display a meaningful ratio. `CombatSystem._snapshot()` must populate it from `unit.current_stats.max_hp`.
### Styling
- Uses the existing `main_ui_theme.tres` (Minecraft font)
- Panel background: default theme PanelContainer style
- Centered via `anchors_preset = CENTER`
- No fixed pixel size — let the content determine width naturally with margin padding
## Button Wiring
FightButton and CancelButton `pressed` signals connect to methods on `combat_ui.gd` within the scene (parent-child, no encapsulation issue):
- `FightButton.pressed``_on_fight_pressed()` — emits `fight_confirmed(stored_proposal)`, calls `_hide_proposal()`
- `CancelButton.pressed``_on_cancel_pressed()` — emits `fight_cancelled`, calls `_hide_proposal()`
## Files Changed
| File | Change |
|---|---|
| `resources/resource_definitions/combat_proposal.gd` | Add `max_hp` field to `CombatantStats` |
| `nodes/combat_system.gd` | Populate `max_hp` in `_snapshot()` |
| `scripts/combat_ui.gd` | Add signals, `show_proposal()`, `_hide_proposal()`, `_unhandled_input` for right-click cancel, button handlers |
| `prefabs/combat_ui.tscn` | Add CombatProposalPanel subtree, wire button signals |
| `scenes/strategy_phase.tscn` | Add root script, remove direct PlayerController→CombatSystem signal connection |
## Files Added
| File | Purpose |
|---|---|
| `nodes/strategy_phase.gd` | Parent orchestration script for strategy_phase.tscn root node |