206 lines
6.5 KiB
Markdown
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`.
|