Combat base added

This commit is contained in:
gamer147
2026-04-02 08:29:24 -04:00
parent 470e89b15b
commit ce92c6e435
14 changed files with 402 additions and 18 deletions

View File

@@ -0,0 +1,205 @@
# 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`.

View File

@@ -0,0 +1,87 @@
# Combat System
## Overview
A stateless CombatSystem node that creates pre-combat proposals and applies combat resolution between two units. The system encapsulates all combat stat calculation so other systems don't need to know proposal logic.
## CombatProposal Resource
`resources/resource_definitions/combat_proposal.gd`
A Resource containing two sides — attacker and defender. Each side holds:
- A reference to the `Unit` node
- Pre-combat snapshot stats via an inner class `CombatantStats`:
- `hp: int` — from `current_stats.current_hp`
- `sp: int` — from `current_stats.current_sp`
- `hit: int` — calculated as `own hit - opponent's eva`
- `atk: int` — from `current_stats.phys_atk`
- `def: int` — from `current_stats.phys_def`
- `spd: int` — from `current_stats.spd`
### Stat Calculation
For the **attacker** side:
- `atk` = attacker's `phys_atk`
- `def` = attacker's `phys_def`
- `hit` = attacker's `hit` - defender's `eva`
- `hp`, `sp`, `spd` copied directly from attacker's `current_stats`
For the **defender** side:
- `atk` = defender's `phys_atk`
- `def` = defender's `phys_def`
- `hit` = defender's `hit` - attacker's `eva`
- `hp`, `sp`, `spd` copied directly from defender's `current_stats`
In the future, different attacks may swap between physical and magic stats, but for now only physical stats are used.
## CombatSystem Node
`nodes/combat_system.gd`
A `Node` added to the strategy phase scene tree. Stateless — operates purely on units passed in.
### `create_proposal(attacker: Unit, defender: Unit) -> CombatProposal`
- Reads both units' `current_stats`
- Builds a `CombatProposal` with snapshot stats for each side
- Applies cross-calculations (hit - eva)
- Returns the proposal without modifying any unit state
### `apply_proposal(proposal: CombatProposal) -> void`
1. **Attacker strikes:** Roll random int 1100. If roll <= attacker's calculated `hit`, apply `max(attacker.atk - defender.def, 0)` damage to the defender Unit's `current_stats.current_hp` (modifies the actual unit, not the snapshot).
2. **Counterattack:** If the defender Unit's `current_stats.current_hp > 0` after the attack, roll 1100 for defender. If roll <= defender's calculated `hit`, apply `max(defender.atk - attacker.def, 0)` damage to the attacker Unit's `current_stats.current_hp`.
- Damage has a floor of 0 (no negative damage / healing).
- Range is ignored for now. In the future, counterattack will depend on whether the defender has skills at appropriate range (defaulting to a "defend" action if not).
## Scene Integration
The CombatSystem node is added to `scenes/strategy_phase.tscn` as a sibling of PlayerController, CombatMap, etc. No exports needed.
```
CombatTest (Node2D)
├─ CombatUI
├─ CombatMap
├─ PlayerController
├─ CombatSystem <-- new
├─ Camera2D
└─ AudioStreamPlayer
```
## Files Changed
| File | Change |
|------|--------|
| `resources/resource_definitions/combat_proposal.gd` | New — CombatProposal resource with CombatantStats inner class |
| `nodes/combat_system.gd` | New — CombatSystem node with `create_proposal` and `apply_proposal` |
| `scenes/strategy_phase.tscn` | Add CombatSystem node to scene tree |
## Out of Scope
- Magic attack/defense selection (future: different attack types swap stats)
- Range-based counterattack eligibility (future: defend action when no skills in range)
- Critical hits
- UI for displaying the combat proposal
- Animations or visual feedback for combat resolution