class_name CombatSystem extends Node func create_proposal(attacker: DeployedUnit, defender: DeployedUnit, 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(deployed: DeployedUnit, distance: int) -> Array[CombatTactic]: var valid: Array[CombatTactic] = [] for tactic in deployed.tactics: if tactic.tactic_range and tactic.tactic_range.is_valid_range(distance, deployed.current_stats): 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(deployed: DeployedUnit, opponent: DeployedUnit, available: Array[CombatTactic], selected: CombatTactic, opponent_selected: CombatTactic) -> CombatProposal.CombatantStats: var current := deployed.current_stats var opp_current := opponent.current_stats var stats := CombatProposal.CombatantStats.new() stats.deployed = deployed stats.max_hp = current.max_hp stats.hp = current.current_hp stats.sp = current.current_sp stats.spd = current.spd stats.available_tactics = available stats.selected_tactic = selected if selected and selected.deals_damage(): var offensive: Dictionary = selected.get_offensive_stats(current) stats.atk = offensive["atk"] stats.hit = offensive["hit"] - opp_current.eva else: stats.atk = 0 stats.hit = 0 if opponent_selected and opponent_selected.deals_damage(): stats.def = opponent_selected.get_relevant_defense(current) else: stats.def = current.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.deployed.current_stats) self_stats.atk = offensive["atk"] self_stats.hit = offensive["hit"] - opp_stats.deployed.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.deployed.current_stats) else: opp_stats.def = opp_stats.deployed.current_stats.phys_def func select_ai_tactic(deployed: DeployedUnit, opponent: DeployedUnit, 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(deployed.current_stats) var defense: int = tactic.get_relevant_defense(opponent.current_stats) 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 apply_proposal(proposal: CombatProposal) -> void: var atk_stats := proposal.attacker var def_stats := proposal.defender var atk_deployed := atk_stats.deployed var def_deployed := def_stats.deployed if not is_instance_valid(atk_deployed) or not is_instance_valid(def_deployed): return # 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_deployed.take_damage(damage) # Counterattack if defender survives and their tactic deals damage if is_instance_valid(def_deployed) and def_deployed.is_alive() \ and is_instance_valid(atk_deployed) \ 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_deployed.take_damage(damage) func _is_player_controlled(deployed: DeployedUnit) -> bool: return deployed.unit.allegiance.type == UnitAllegiance.AllegianceType.PLAYER