Combat tactics
This commit is contained in:
@@ -1,29 +1,120 @@
|
||||
class_name CombatSystem extends Node
|
||||
|
||||
func create_proposal(attacker: Unit, defender: Unit) -> CombatProposal:
|
||||
func create_proposal(attacker: Unit, defender: Unit, distance: int) -> CombatProposal:
|
||||
var proposal := CombatProposal.new()
|
||||
|
||||
proposal.attacker = _snapshot(attacker, defender)
|
||||
proposal.defender = _snapshot(defender, attacker)
|
||||
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 _snapshot(unit: Unit, opponent: Unit) -> CombatProposal.CombatantStats:
|
||||
|
||||
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.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
|
||||
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 process_combat(attacker: Unit, defender: Unit) -> void:
|
||||
|
||||
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)
|
||||
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])
|
||||
@@ -41,15 +132,20 @@ func apply_proposal(proposal: CombatProposal) -> void:
|
||||
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.take_damage(damage)
|
||||
# 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
|
||||
if def_unit.is_alive():
|
||||
# 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
|
||||
|
||||
@@ -12,6 +12,7 @@ func _ready() -> void:
|
||||
player_controller.camera_drag.connect(camera.apply_drag)
|
||||
combat_ui.fight_confirmed.connect(_on_fight_confirmed)
|
||||
combat_ui.fight_cancelled.connect(_on_fight_cancelled)
|
||||
combat_ui.combat_system = combat_system
|
||||
|
||||
|
||||
func _on_mouse_grid_changed(coords: Vector2i) -> void:
|
||||
@@ -20,7 +21,10 @@ func _on_mouse_grid_changed(coords: Vector2i) -> void:
|
||||
|
||||
|
||||
func _on_combat_requested(attacker: Unit, defender: Unit) -> void:
|
||||
var proposal := combat_system.create_proposal(attacker, defender)
|
||||
var atk_coords := combat_map.world_to_coords(attacker.position)
|
||||
var def_coords := combat_map.world_to_coords(defender.position)
|
||||
var distance := absi(atk_coords.x - def_coords.x) + absi(atk_coords.y - def_coords.y)
|
||||
var proposal := combat_system.create_proposal(attacker, defender, distance)
|
||||
_set_input_disabled(true)
|
||||
combat_ui.show_proposal(proposal)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ enum UnitState { ALIVE, DEAD }
|
||||
@export var stat_template: UnitStats
|
||||
@export var info_template: UnitInfo
|
||||
@export var allegiance_template: UnitAllegiance
|
||||
@export var tactics: Array[CombatTactic] = []
|
||||
#endregion
|
||||
|
||||
var current_stats: UnitStats
|
||||
@@ -21,8 +22,20 @@ func _ready() -> void:
|
||||
current_stats = stat_template.duplicate(true)
|
||||
current_info = info_template.duplicate(true)
|
||||
current_allegiance = allegiance_template.duplicate(true)
|
||||
_append_builtin_tactics()
|
||||
unit_allegiance_changed.emit(self, current_allegiance)
|
||||
|
||||
func _append_builtin_tactics() -> void:
|
||||
var attack := AttackCombatTactic.new()
|
||||
attack.tactic_name = "Attack"
|
||||
attack.tactic_range = UnitMatchingCombatTacticRange.new()
|
||||
tactics.append(attack)
|
||||
|
||||
var defend := DefendCombatTactic.new()
|
||||
defend.tactic_name = "Defend"
|
||||
defend.tactic_range = AnyCombatTacticRange.new()
|
||||
tactics.append(defend)
|
||||
|
||||
func set_selected(selected: bool) -> void:
|
||||
unit_selected_changed.emit(self, selected)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user