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

6.5 KiB

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

# 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

# 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:

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):

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:

		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.