7.6 KiB
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:
PlayerControlleremitscombat_requested(attacker, defender)- Parent scene script catches it, calls
CombatSystem.create_proposal(attacker, defender) - Parent calls
CombatUI.show_proposal(proposal) - Player sees the combat forecast panel
On Fight:
CombatUIemitsfight_confirmed(proposal)- Parent catches it, calls
CombatSystem.apply_proposal(proposal) - Panel closes
On Cancel (Cancel button or right-click):
CombatUIemitsfight_cancelled- Parent catches it (no action needed beyond the signal existing for future use)
- 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()andapply_proposal(). - Parent scene script (
strategy_phase.tscnroot 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
@onreadyreferences toPlayerController,CombatSystem, andCombatUI - Connects
PlayerController.combat_requestedto a local method that creates a proposal and passes it toCombatUI.show_proposal() - Connects
CombatUI.fight_confirmedto a local method that callsCombatSystem.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 pressedfight_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'sCombatantStats, 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()— emitsfight_confirmed(stored_proposal), calls_hide_proposal()CancelButton.pressed→_on_cancel_pressed()— emitsfight_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 |