Files
MaidEngine/docs/superpowers/plans/2026-04-02-combat-system.md
2026-04-02 08:29:24 -04:00

206 lines
6.5 KiB
Markdown

# Combat System 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:** Create a stateless combat system that produces pre-combat proposals and resolves attacks between two units.
**Architecture:** A `CombatProposal` resource holds snapshot stats for attacker and defender. A `CombatSystem` node exposes `create_proposal()` to build proposals and `apply_proposal()` to resolve combat, modifying unit HP directly. The system is added to the strategy phase scene tree.
**Tech Stack:** Godot 4.6, GDScript
---
## File Structure
| File | Responsibility |
|------|---------------|
| `resources/resource_definitions/combat_proposal.gd` | CombatProposal resource with CombatantStats inner class — holds unit references and snapshot stats |
| `nodes/combat_system.gd` | CombatSystem node — `create_proposal()` and `apply_proposal()` methods |
| `scenes/strategy_phase.tscn` | Scene tree — add CombatSystem node |
---
### Task 1: Create the CombatProposal resource
**Files:**
- Create: `resources/resource_definitions/combat_proposal.gd`
- [ ] **Step 1: Create the CombatantStats inner class and CombatProposal resource**
```gdscript
# resources/resource_definitions/combat_proposal.gd
class_name CombatProposal extends Resource
class CombatantStats:
var unit: Unit
var hp: int
var sp: int
var hit: int
var atk: int
var def: int
var spd: int
var attacker: CombatantStats
var defender: CombatantStats
```
- [ ] **Step 2: Verify the file loads without errors**
Open the Godot editor and check the Output panel for parse errors. Confirm `CombatProposal` appears as a recognized class by typing it in any script's autocomplete.
---
### Task 2: Create the CombatSystem node
**Files:**
- Create: `nodes/combat_system.gd`
- Reference: `resources/resource_definitions/combat_proposal.gd`
- Reference: `resources/resource_definitions/unit_stats.gd`
- Reference: `nodes/unit.gd`
- [ ] **Step 1: Create combat_system.gd with create_proposal**
```gdscript
# nodes/combat_system.gd
class_name CombatSystem extends Node
func create_proposal(attacker: Unit, defender: Unit) -> CombatProposal:
var proposal := CombatProposal.new()
proposal.attacker = _snapshot(attacker, defender)
proposal.defender = _snapshot(defender, attacker)
return proposal
func _snapshot(unit: Unit, opponent: Unit) -> CombatProposal.CombatantStats:
var stats := CombatProposal.CombatantStats.new()
stats.unit = unit
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 2: Add apply_proposal method**
Append to `nodes/combat_system.gd`:
```gdscript
func apply_proposal(proposal: CombatProposal) -> void:
var atk_stats := proposal.attacker
var def_stats := proposal.defender
var atk_unit := atk_stats.unit
var def_unit := def_stats.unit
# Attacker strikes
var atk_roll := randi_range(1, 100)
if atk_roll <= atk_stats.hit:
var damage := maxi(atk_stats.atk - def_stats.def, 0)
def_unit.current_stats.current_hp -= damage
# Counterattack if defender survives
if def_unit.current_stats.current_hp > 0:
var def_roll := randi_range(1, 100)
if def_roll <= def_stats.hit:
var damage := maxi(def_stats.atk - atk_stats.def, 0)
atk_unit.current_stats.current_hp -= damage
```
- [ ] **Step 3: Verify the file loads without errors**
Open the Godot editor and check the Output panel for parse errors on `combat_system.gd`.
---
### Task 3: Add CombatSystem to the strategy phase scene
**Files:**
- Modify: `scenes/strategy_phase.tscn`
- [ ] **Step 1: Add CombatSystem node and script reference to the scene**
Add a new `ext_resource` entry for the combat_system.gd script and a new node to the scene file. Append after the existing ext_resources and add the node as a child of the root `CombatTest` node:
New ext_resource (use the next available id, `6_xxxxx` — the exact suffix will be assigned):
```
[ext_resource type="Script" path="res://nodes/combat_system.gd" id="6_combat"]
```
New node (add after the PlayerController node block):
```
[node name="CombatSystem" type="Node" parent="."]
script = ExtResource("6_combat")
```
The resulting scene tree order:
```
CombatTest (Node2D)
├─ CombatUI
├─ CombatMap
├─ PlayerController
├─ CombatSystem <-- new
├─ Camera2D
└─ AudioStreamPlayer
```
- [ ] **Step 2: Verify in the Godot editor**
Open `scenes/strategy_phase.tscn` in the editor. Confirm:
- CombatSystem node appears in the scene tree
- No errors in the Output panel
- The scene runs without crashes
---
### Task 4: Manual integration verification
- [ ] **Step 1: Verify create_proposal works**
Temporarily add the following to `CombatSystem._ready()` to test proposal creation (remove after verification):
```gdscript
func _ready() -> void:
# Temporary test — remove after verification
var units := get_tree().get_nodes_in_group("units")
if units.size() >= 2:
var proposal := create_proposal(units[0], units[1])
print("Proposal created:")
print(" Attacker: ", proposal.attacker.unit.current_info.name,
" HP=", proposal.attacker.hp,
" ATK=", proposal.attacker.atk,
" DEF=", proposal.attacker.def,
" HIT=", proposal.attacker.hit)
print(" Defender: ", proposal.defender.unit.current_info.name,
" HP=", proposal.defender.hp,
" ATK=", proposal.defender.atk,
" DEF=", proposal.defender.def,
" HIT=", proposal.defender.hit)
```
Run the scene with at least 2 units deployed. Check the Output panel for correct stat snapshots.
- [ ] **Step 2: Verify apply_proposal works**
Extend the temporary `_ready()` to also apply the proposal:
```gdscript
print("Before combat: Attacker HP=", proposal.attacker.unit.current_stats.current_hp,
" Defender HP=", proposal.defender.unit.current_stats.current_hp)
apply_proposal(proposal)
print("After combat: Attacker HP=", proposal.attacker.unit.current_stats.current_hp,
" Defender HP=", proposal.defender.unit.current_stats.current_hp)
```
Run multiple times (results will vary due to hit rolls). Confirm:
- Defender HP decreases when attacker hits
- Attacker HP decreases when defender counterattacks
- HP never increases (no negative damage)
- If defender dies (HP <= 0), no counterattack occurs
- [ ] **Step 3: Remove temporary test code**
Delete the entire `_ready()` method from `combat_system.gd`.