Account for counter risk in auto attacks

This commit is contained in:
2026-06-19 20:49:45 +09:00
parent 184a542b04
commit 46df282eab
3 changed files with 220 additions and 43 deletions

View File

@@ -3618,16 +3618,20 @@ func _append_auto_attack_target_marker_entries(result: Array[Dictionary], select
var target_cell: Vector2i = target.get("pos", Vector2i(-1, -1))
if not state.is_inside(target_cell):
continue
var badge := _auto_attack_target_preview_badge_for_origin(selected, target, attack_origin)
if badge.is_empty():
var preview := _auto_attack_preview_for_origin(selected, target, attack_origin)
if preview.is_empty():
continue
result.append({
"cell": target_cell,
"target_id": target_id,
"text": "행공",
"kind": str(badge.get("kind", "advance")),
"kind": _auto_attack_preview_badge_kind(preview),
"origin": attack_origin,
"requires_move": true
"requires_move": true,
"damage": int(preview.get("damage", 0)),
"counter_damage": int(preview.get("counter_damage", 0)),
"counter_in_range": bool(preview.get("counter_in_range", false)),
"counter_would_defeat": bool(preview.get("counter_would_defeat", false))
})
@@ -3756,6 +3760,8 @@ func _best_auto_attack_origin(selected: Dictionary, target: Dictionary) -> Vecto
return Vector2i(-1, -1)
var best_cell := Vector2i(-1, -1)
var best_score := -999999
var best_preview := {}
var best_move_distance := 999999
for cell in state.get_movement_range(unit_id):
if not state.is_inside(cell):
continue
@@ -3764,19 +3770,50 @@ func _best_auto_attack_origin(selected: Dictionary, target: Dictionary) -> Vecto
var blocker := state.get_unit_at(cell)
if not blocker.is_empty() and str(blocker.get("id", "")) != unit_id:
continue
if not _can_attack_target_from(selected, cell, target_cell):
var preview := _auto_attack_preview_for_origin(selected, target, cell)
if preview.is_empty():
continue
var candidate_attacker := selected.duplicate(true)
candidate_attacker["pos"] = cell
var damage: int = state.calculate_damage(candidate_attacker, target)
var move_distance: int = _manhattan(current_cell, cell)
var score: int = damage * 20 - move_distance
if score > best_score:
var score: int = _auto_attack_origin_score(preview, move_distance)
if best_cell == Vector2i(-1, -1) or _auto_attack_origin_is_better(score, preview, move_distance, best_score, best_preview, best_move_distance):
best_score = score
best_preview = preview
best_move_distance = move_distance
best_cell = cell
return best_cell
func _auto_attack_origin_score(preview: Dictionary, move_distance: int) -> int:
var score := int(preview.get("damage", 0)) * 100 + int(preview.get("hit_chance", 0)) - move_distance
if bool(preview.get("would_defeat", false)):
score += 100000
if bool(preview.get("counter_in_range", false)):
score -= 1500
score -= int(preview.get("counter_damage", 0)) * 80
score -= int(preview.get("counter_hit_chance", 0)) * 2
if bool(preview.get("counter_would_defeat", false)):
score -= 100000
return score
func _auto_attack_origin_is_better(score: int, preview: Dictionary, move_distance: int, best_score: int, best_preview: Dictionary, best_move_distance: int) -> bool:
if score != best_score:
return score > best_score
var counter_defeat := bool(preview.get("counter_would_defeat", false))
var best_counter_defeat := bool(best_preview.get("counter_would_defeat", false))
if counter_defeat != best_counter_defeat:
return not counter_defeat
var counter_damage := int(preview.get("counter_damage", 0))
var best_counter_damage := int(best_preview.get("counter_damage", 0))
if counter_damage != best_counter_damage:
return counter_damage < best_counter_damage
var damage := int(preview.get("damage", 0))
var best_damage := int(best_preview.get("damage", 0))
if damage != best_damage:
return damage > best_damage
return move_distance < best_move_distance
func _can_attack_target_from(unit: Dictionary, origin: Vector2i, target_cell: Vector2i) -> bool:
var attack_range: int = maxi(1, int(unit.get("range", 1)))
var min_range: int = clampi(int(unit.get("min_range", mini(1, attack_range))), 1, attack_range)
@@ -4825,16 +4862,56 @@ func _auto_attack_target_preview_badge(selected: Dictionary, target: Dictionary)
func _auto_attack_target_preview_badge_for_origin(selected: Dictionary, target: Dictionary, attack_origin: Vector2i) -> Dictionary:
var preview := _auto_attack_preview_for_origin(selected, target, attack_origin)
if preview.is_empty():
return {}
var target_cell: Vector2i = preview.get("target_cell", Vector2i(-1, -1))
var damage := int(preview.get("damage", 0))
var hit_chance := int(preview.get("hit_chance", 0))
var result_text := "행공 퇴각" if bool(preview.get("would_defeat", false)) else "행공 -%d" % damage
var directional_text := _format_directional_badge_suffix(preview)
var counter_text := _format_auto_attack_counter_badge_suffix(preview)
return _make_target_preview_badge_at("%s%s %d%%%s" % [result_text, directional_text, hit_chance, counter_text], _auto_attack_preview_badge_kind(preview), target_cell)
func _auto_attack_preview_for_origin(selected: Dictionary, target: Dictionary, attack_origin: Vector2i) -> Dictionary:
var target_cell: Vector2i = target.get("pos", Vector2i(-1, -1))
if not state.is_inside(attack_origin) or not state.is_inside(target_cell):
if selected.is_empty() or target.is_empty() or not state.is_inside(attack_origin) or not state.is_inside(target_cell):
return {}
if not _can_attack_target_from(selected, attack_origin, target_cell):
return {}
var candidate_attacker := _auto_attack_candidate_after_move(selected, target_cell, attack_origin)
var damage := state.calculate_damage(candidate_attacker, target)
var hit_chance := state.calculate_hit_chance(candidate_attacker, target)
var result_text := "행공 퇴각" if int(target.get("hp", 0)) - damage <= 0 else "행공 -%d" % damage
var directional_text := _format_directional_badge_suffix_for_units(candidate_attacker, target)
var counter_text := _format_auto_attack_counter_badge_suffix(candidate_attacker, target, damage)
return _make_target_preview_badge_at("%s%s %d%%%s" % [result_text, directional_text, hit_chance, counter_text], _auto_attack_preview_badge_kind(candidate_attacker, target, damage), target_cell)
var target_hp_after := maxi(0, int(target.get("hp", 0)) - damage)
var directional_info := state.get_directional_attack_info(candidate_attacker, target)
var counter_in_range := false
var counter_damage := 0
var counter_hit_chance := 0
var counter_would_defeat := false
if bool(target.get("controllable", true)) and (target_hp_after > 0 or hit_chance < 100):
counter_in_range = _can_attack_target_from(target, target_cell, attack_origin)
if counter_in_range:
var counter_attacker := target.duplicate(true)
_face_unit_snapshot_toward_cell(counter_attacker, attack_origin)
counter_damage = state.calculate_damage(counter_attacker, candidate_attacker)
counter_hit_chance = state.calculate_hit_chance(counter_attacker, candidate_attacker)
counter_would_defeat = int(candidate_attacker.get("hp", 0)) - counter_damage <= 0
return {
"origin": attack_origin,
"target_cell": target_cell,
"damage": damage,
"hit_chance": hit_chance,
"target_hp_after": target_hp_after,
"would_defeat": target_hp_after <= 0,
"directional_attack": str(directional_info.get("kind", BattleState.DIRECTIONAL_FRONT_ATTACK)),
"directional_bonus": int(directional_info.get("bonus", 0)),
"directional_label": str(directional_info.get("label", "")),
"counter_in_range": counter_in_range,
"counter_damage": counter_damage,
"counter_hit_chance": counter_hit_chance,
"counter_would_defeat": counter_would_defeat
}
func _auto_attack_candidate_after_move(selected: Dictionary, target_cell: Vector2i, attack_origin: Vector2i) -> Dictionary:
@@ -4853,39 +4930,22 @@ func _face_unit_snapshot_toward_cell(unit: Dictionary, target_cell: Vector2i) ->
unit["facing_x"] = -1 if target_cell.x < from_cell.x else 1
func _format_directional_badge_suffix_for_units(attacker: Dictionary, target: Dictionary) -> String:
var directional_info := state.get_directional_attack_info(attacker, target)
return _format_directional_badge_suffix({
"directional_attack": str(directional_info.get("kind", BattleState.DIRECTIONAL_FRONT_ATTACK)),
"directional_bonus": int(directional_info.get("bonus", 0)),
"directional_label": str(directional_info.get("label", ""))
})
func _format_auto_attack_counter_badge_suffix(candidate_attacker: Dictionary, target: Dictionary, damage: int) -> String:
if int(target.get("hp", 0)) - damage <= 0:
func _format_auto_attack_counter_badge_suffix(preview: Dictionary) -> String:
if not bool(preview.get("counter_in_range", false)):
return ""
var origin: Vector2i = candidate_attacker.get("pos", Vector2i(-1, -1))
var target_cell: Vector2i = target.get("pos", Vector2i(-1, -1))
if not _can_attack_target_from(target, target_cell, origin):
return ""
var counter_damage := state.calculate_damage(target, candidate_attacker)
if int(candidate_attacker.get("hp", 0)) - counter_damage <= 0:
if bool(preview.get("counter_would_defeat", false)):
return " 반격!"
return "%d" % counter_damage
return "%d" % int(preview.get("counter_damage", 0))
func _auto_attack_preview_badge_kind(candidate_attacker: Dictionary, target: Dictionary, damage: int) -> String:
if int(target.get("hp", 0)) - damage <= 0:
func _auto_attack_preview_badge_kind(preview: Dictionary) -> String:
if bool(preview.get("would_defeat", false)) and not bool(preview.get("counter_in_range", false)):
return "advance"
var origin: Vector2i = candidate_attacker.get("pos", Vector2i(-1, -1))
var target_cell: Vector2i = target.get("pos", Vector2i(-1, -1))
if not _can_attack_target_from(target, target_cell, origin):
return "advance"
var counter_damage := state.calculate_damage(target, candidate_attacker)
if int(candidate_attacker.get("hp", 0)) - counter_damage <= 0:
if bool(preview.get("counter_would_defeat", false)):
return "danger"
return "counter"
if bool(preview.get("counter_in_range", false)):
return "counter"
return "advance"
func _skill_target_preview_badge(selected: Dictionary, target: Dictionary) -> Dictionary: