diff --git a/docs/superpowers/plans/2026-04-02-combat-proposal-ui.md b/docs/superpowers/plans/2026-04-02-combat-proposal-ui.md new file mode 100644 index 0000000..3820136 --- /dev/null +++ b/docs/superpowers/plans/2026-04-02-combat-proposal-ui.md @@ -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" +``` diff --git a/docs/superpowers/specs/2026-04-02-combat-proposal-ui-design.md b/docs/superpowers/specs/2026-04-02-combat-proposal-ui-design.md new file mode 100644 index 0000000..11fd571 --- /dev/null +++ b/docs/superpowers/specs/2026-04-02-combat-proposal-ui-design.md @@ -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 | diff --git a/nodes/combat_system.gd b/nodes/combat_system.gd index 9a1c6b2..9c75b78 100644 --- a/nodes/combat_system.gd +++ b/nodes/combat_system.gd @@ -11,6 +11,7 @@ func create_proposal(attacker: Unit, defender: Unit) -> CombatProposal: 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 diff --git a/nodes/strategy_phase.gd b/nodes/strategy_phase.gd new file mode 100644 index 0000000..5cc0d6f --- /dev/null +++ b/nodes/strategy_phase.gd @@ -0,0 +1,16 @@ +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) diff --git a/nodes/strategy_phase.gd.uid b/nodes/strategy_phase.gd.uid new file mode 100644 index 0000000..7421a06 --- /dev/null +++ b/nodes/strategy_phase.gd.uid @@ -0,0 +1 @@ +uid://dnsqtsx4u2hx4 diff --git a/prefabs/combat_ui.tscn b/prefabs/combat_ui.tscn index ec79222..7a9372e 100644 --- a/prefabs/combat_ui.tscn +++ b/prefabs/combat_ui.tscn @@ -40,3 +40,125 @@ layout_mode = 2 max_value = 100.0 value = 100.0 show_percentage = false + +[node name="BackgroundTint" type="ColorRect" parent="."] +unique_name_in_owner = true +visible = false +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 0 +color = Color(0, 0, 0, 0.4) + +[node name="CombatProposalPanel" type="PanelContainer" parent="."] +unique_name_in_owner = true +visible = false +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +grow_horizontal = 2 +grow_vertical = 2 +theme = ExtResource("1_2ro41") + +[node name="MarginContainer" type="MarginContainer" parent="CombatProposalPanel"] +layout_mode = 2 +theme_override_constants/margin_left = 12 +theme_override_constants/margin_top = 12 +theme_override_constants/margin_right = 12 +theme_override_constants/margin_bottom = 12 + +[node name="VBoxContainer" type="VBoxContainer" parent="CombatProposalPanel/MarginContainer"] +layout_mode = 2 + +[node name="TitleLabel" type="Label" parent="CombatProposalPanel/MarginContainer/VBoxContainer"] +layout_mode = 2 +text = "COMBAT FORECAST" +horizontal_alignment = 1 + +[node name="StatsContainer" type="HBoxContainer" parent="CombatProposalPanel/MarginContainer/VBoxContainer"] +layout_mode = 2 +theme_override_constants/separation = 24 + +[node name="AttackerStats" type="VBoxContainer" parent="CombatProposalPanel/MarginContainer/VBoxContainer/StatsContainer"] +layout_mode = 2 + +[node name="AttackerNameLabel" type="Label" parent="CombatProposalPanel/MarginContainer/VBoxContainer/StatsContainer/AttackerStats"] +unique_name_in_owner = true +layout_mode = 2 +text = "Attacker" + +[node name="AttackerHPBar" type="ProgressBar" parent="CombatProposalPanel/MarginContainer/VBoxContainer/StatsContainer/AttackerStats"] +unique_name_in_owner = true +layout_mode = 2 +show_percentage = false + +[node name="AttackerATKLabel" type="Label" parent="CombatProposalPanel/MarginContainer/VBoxContainer/StatsContainer/AttackerStats"] +unique_name_in_owner = true +layout_mode = 2 +text = "ATK: 0" + +[node name="AttackerDEFLabel" type="Label" parent="CombatProposalPanel/MarginContainer/VBoxContainer/StatsContainer/AttackerStats"] +unique_name_in_owner = true +layout_mode = 2 +text = "DEF: 0" + +[node name="AttackerHITLabel" type="Label" parent="CombatProposalPanel/MarginContainer/VBoxContainer/StatsContainer/AttackerStats"] +unique_name_in_owner = true +layout_mode = 2 +text = "HIT: 0%" + +[node name="AttackerSPDLabel" type="Label" parent="CombatProposalPanel/MarginContainer/VBoxContainer/StatsContainer/AttackerStats"] +unique_name_in_owner = true +layout_mode = 2 +text = "SPD: 0" + +[node name="DefenderStats" type="VBoxContainer" parent="CombatProposalPanel/MarginContainer/VBoxContainer/StatsContainer"] +layout_mode = 2 + +[node name="DefenderNameLabel" type="Label" parent="CombatProposalPanel/MarginContainer/VBoxContainer/StatsContainer/DefenderStats"] +unique_name_in_owner = true +layout_mode = 2 +text = "Defender" + +[node name="DefenderHPBar" type="ProgressBar" parent="CombatProposalPanel/MarginContainer/VBoxContainer/StatsContainer/DefenderStats"] +unique_name_in_owner = true +layout_mode = 2 +show_percentage = false + +[node name="DefenderATKLabel" type="Label" parent="CombatProposalPanel/MarginContainer/VBoxContainer/StatsContainer/DefenderStats"] +unique_name_in_owner = true +layout_mode = 2 +text = "ATK: 0" + +[node name="DefenderDEFLabel" type="Label" parent="CombatProposalPanel/MarginContainer/VBoxContainer/StatsContainer/DefenderStats"] +unique_name_in_owner = true +layout_mode = 2 +text = "DEF: 0" + +[node name="DefenderHITLabel" type="Label" parent="CombatProposalPanel/MarginContainer/VBoxContainer/StatsContainer/DefenderStats"] +unique_name_in_owner = true +layout_mode = 2 +text = "HIT: 0%" + +[node name="DefenderSPDLabel" type="Label" parent="CombatProposalPanel/MarginContainer/VBoxContainer/StatsContainer/DefenderStats"] +unique_name_in_owner = true +layout_mode = 2 +text = "SPD: 0" + +[node name="ButtonContainer" type="HBoxContainer" parent="CombatProposalPanel/MarginContainer/VBoxContainer"] +layout_mode = 2 +alignment = 1 + +[node name="FightButton" type="Button" parent="CombatProposalPanel/MarginContainer/VBoxContainer/ButtonContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Fight" + +[node name="CancelButton" type="Button" parent="CombatProposalPanel/MarginContainer/VBoxContainer/ButtonContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Cancel" diff --git a/resources/resource_definitions/combat_proposal.gd b/resources/resource_definitions/combat_proposal.gd index d31911b..b32146c 100644 --- a/resources/resource_definitions/combat_proposal.gd +++ b/resources/resource_definitions/combat_proposal.gd @@ -2,6 +2,7 @@ class_name CombatProposal extends Resource class CombatantStats: var unit: Unit + var max_hp: int var hp: int var sp: int var hit: int diff --git a/scenes/strategy_phase.tscn b/scenes/strategy_phase.tscn index 3017670..7ff4642 100644 --- a/scenes/strategy_phase.tscn +++ b/scenes/strategy_phase.tscn @@ -6,8 +6,10 @@ [ext_resource type="Script" uid="uid://csdcbi2gtwrly" path="res://scripts/camera_controller.gd" id="4_ww3c6"] [ext_resource type="AudioStream" uid="uid://dsikulned64qt" path="res://assets/music/combat_bgm_01.OGG" id="5_ficdm"] [ext_resource type="Script" uid="uid://cf4ivrcbky0s3" path="res://nodes/combat_system.gd" id="6_combat"] +[ext_resource type="Script" path="res://nodes/strategy_phase.gd" id="7_strat"] [node name="CombatTest" type="Node2D" unique_id=855645983] +script = ExtResource("7_strat") [node name="CombatUI" parent="." unique_id=329168107 instance=ExtResource("1_6gip4")] @@ -27,5 +29,3 @@ script = ExtResource("4_ww3c6") [node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="." unique_id=1057500234] stream = ExtResource("5_ficdm") autoplay = true - -[connection signal="combat_requested" from="PlayerController" to="CombatSystem" method="process_combat"] diff --git a/scripts/combat_ui.gd b/scripts/combat_ui.gd index 2f89299..fc86313 100644 --- a/scripts/combat_ui.gd +++ b/scripts/combat_ui.gd @@ -1,13 +1,37 @@ 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 background_tint: ColorRect = %BackgroundTint +@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) @@ -24,12 +48,21 @@ 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 @@ -40,3 +73,39 @@ func _on_unit_selected_changed(unit: Unit, selected: bool) -> void: 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 + background_tint.visible = true + proposal_panel.visible = true + +func _hide_proposal() -> void: + background_tint.visible = false + 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()