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 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 if not is_instance_valid(atk_unit) or not is_instance_valid(def_unit): 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_unit.take_damage(damage) # Counterattack if defender survives and their tactic deals damage if is_instance_valid(def_unit) and def_unit.is_alive() \ and is_instance_valid(atk_unit) \ 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