166 lines
7.6 KiB
Markdown
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 |
|