class_name CombatSystem extends Node func create_proposal(attacker: Unit, defender: Unit, distance: int) -> CombatProposal: var proposal := CombatProposal.new() var atk_tactics := _filter_tactics(attacker, distance) var def_tactics := _filter_tactics(defender, distance) var atk_tactic := _find_default_attack(atk_tactics) var def_tactic := _find_default_attack(def_tactics) # AI auto-selects for non-player units if not _is_player_controlled(defender): def_tactic = select_ai_tactic(defender, attacker, def_tactics) proposal.attacker = _snapshot(attacker, defender, atk_tactics, atk_tactic, def_tactic) proposal.defender = _snapshot(defender, attacker, def_tactics, def_tactic, atk_tactic) return proposal func _filter_tactics(unit: Unit, distance: int) -> Array[CombatTactic]: var valid: Array[CombatTactic] = [] for tactic in unit.tactics: if tactic.tactic_range and tactic.tactic_range.is_valid_range(distance, unit): valid.append(tactic) return valid func _find_default_attack(tactics: Array[CombatTactic]) -> CombatTactic: for tactic in tactics: if tactic is AttackCombatTactic: return tactic return tactics[0] if tactics.size() > 0 else null func _snapshot(unit: Unit, opponent: Unit, available: Array[CombatTactic], selected: CombatTactic, opponent_selected: CombatTactic) -> 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.spd = unit.current_stats.spd stats.available_tactics = available stats.selected_tactic = selected if selected and selected.deals_damage(): var offensive: Dictionary = selected.get_offensive_stats(unit) stats.atk = offensive["atk"] stats.hit = offensive["hit"] - opponent.current_stats.eva else: stats.atk = 0 stats.hit = 0 if opponent_selected and opponent_selected.deals_damage(): stats.def = opponent_selected.get_relevant_defense(unit) else: stats.def = unit.current_stats.phys_def return stats func update_tactic(proposal: CombatProposal, is_attacker: bool, tactic: CombatTactic) -> void: var self_stats: CombatProposal.CombatantStats var opp_stats: CombatProposal.CombatantStats if is_attacker: self_stats = proposal.attacker opp_stats = proposal.defender else: self_stats = proposal.defender opp_stats = proposal.attacker self_stats.selected_tactic = tactic # Recalculate this side's offensive stats if tactic and tactic.deals_damage(): var offensive: Dictionary = tactic.get_offensive_stats(self_stats.unit) self_stats.atk = offensive["atk"] self_stats.hit = offensive["hit"] - opp_stats.unit.current_stats.eva else: self_stats.atk = 0 self_stats.hit = 0 # Recalculate opponent's def based on this side's new tactic if tactic and tactic.deals_damage(): opp_stats.def = tactic.get_relevant_defense(opp_stats.unit) else: opp_stats.def = opp_stats.unit.current_stats.phys_def func select_ai_tactic(unit: Unit, opponent: Unit, available_tactics: Array[CombatTactic]) -> CombatTactic: var best_tactic: CombatTactic = null var best_damage := -1 for tactic in available_tactics: if not tactic.deals_damage(): continue var offensive: Dictionary = tactic.get_offensive_stats(unit) var defense: int = tactic.get_relevant_defense(opponent) var damage := maxi(offensive["atk"] - defense, 0) if damage > best_damage: best_damage = damage best_tactic = tactic if best_tactic == null or best_damage <= 0: for tactic in available_tactics: if tactic is DefendCombatTactic: return tactic return available_tactics[0] if available_tactics.size() > 0 else null return best_tactic func process_combat(attacker: Unit, defender: Unit, distance: int) -> void: if not attacker.is_alive() or not defender.is_alive(): return var proposal := create_proposal(attacker, defender, distance) var atk_name := attacker.current_info.name var def_name := defender.current_info.name print("=== Combat: %s vs %s ===" % [atk_name, def_name]) print(" %s — HP:%d ATK:%d DEF:%d HIT:%d" % [atk_name, proposal.attacker.hp, proposal.attacker.atk, proposal.attacker.def, proposal.attacker.hit]) print(" %s — HP:%d ATK:%d DEF:%d HIT:%d" % [def_name, proposal.defender.hp, proposal.defender.atk, proposal.defender.def, proposal.defender.hit]) apply_proposal(proposal) var atk_hp := attacker.current_stats.current_hp if is_instance_valid(attacker) else 0 var def_hp := defender.current_stats.current_hp if is_instance_valid(defender) else 0 print(" Result: %s HP=%d, %s HP=%d" % [atk_name, atk_hp, def_name, def_hp]) 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 (if their tactic deals damage) if atk_stats.selected_tactic and atk_stats.selected_tactic.deals_damage(): 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.take_damage(damage) # Counterattack if defender survives and their tactic deals damage if def_unit.is_alive() and def_stats.selected_tactic and def_stats.selected_tactic.deals_damage(): 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.take_damage(damage) func _is_player_controlled(unit: Unit) -> bool: return unit.current_allegiance.type == UnitAllegiance.AllegianceType.PLAYER