Combat panel, mouse movement still passes through
This commit is contained in:
367
docs/superpowers/plans/2026-04-02-combat-proposal-ui.md
Normal file
367
docs/superpowers/plans/2026-04-02-combat-proposal-ui.md
Normal file
@@ -0,0 +1,367 @@
|
||||
# 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"
|
||||
```
|
||||
165
docs/superpowers/specs/2026-04-02-combat-proposal-ui-design.md
Normal file
165
docs/superpowers/specs/2026-04-02-combat-proposal-ui-design.md
Normal file
@@ -0,0 +1,165 @@
|
||||
# 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 |
|
||||
Reference in New Issue
Block a user