Show hostile tactic threat hints
This commit is contained in:
@@ -1370,6 +1370,45 @@ func get_physical_threat_previews(cell: Vector2i, source_team := TEAM_ENEMY) ->
|
||||
return result
|
||||
|
||||
|
||||
func get_skill_threat_previews(cell: Vector2i, source_team := TEAM_ENEMY) -> Array[Dictionary]:
|
||||
var result: Array[Dictionary] = []
|
||||
if not is_inside(cell):
|
||||
return result
|
||||
var target := get_unit_at(cell)
|
||||
if target.is_empty() or target.get("team", "") == source_team:
|
||||
return result
|
||||
|
||||
for unit in get_living_units(source_team):
|
||||
if _unit_has_action_lock(unit, "skill"):
|
||||
continue
|
||||
if typeof(unit.get("skills", [])) != TYPE_ARRAY:
|
||||
continue
|
||||
for skill_id_value in unit.get("skills", []):
|
||||
var skill_id := str(skill_id_value)
|
||||
var skill := get_skill_def(skill_id)
|
||||
if skill.is_empty():
|
||||
continue
|
||||
if int(unit.get("mp", 0)) < int(skill.get("mp_cost", 0)):
|
||||
continue
|
||||
if not _skill_threatens_opponents(skill):
|
||||
continue
|
||||
if not _skill_target_valid(unit, target, skill):
|
||||
continue
|
||||
var preview := _best_skill_threat_preview_for_target(unit, skill_id, skill, target, cell)
|
||||
if not preview.is_empty():
|
||||
result.append(preview)
|
||||
result.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||
var a_score := _skill_threat_preview_sort_score(a)
|
||||
var b_score := _skill_threat_preview_sort_score(b)
|
||||
if a_score != b_score:
|
||||
return a_score > b_score
|
||||
if str(a.get("source_name", "")) != str(b.get("source_name", "")):
|
||||
return str(a.get("source_name", "")) < str(b.get("source_name", ""))
|
||||
return str(a.get("skill_name", "")) < str(b.get("skill_name", ""))
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
func get_objective_cells() -> Array[Vector2i]:
|
||||
var result: Array[Vector2i] = []
|
||||
_collect_objective_cells(battle_conditions.get("victory", {}), result)
|
||||
@@ -1894,6 +1933,124 @@ func _unit_skill_threat_cells(unit: Dictionary) -> Array[Vector2i]:
|
||||
return result
|
||||
|
||||
|
||||
func _best_skill_threat_preview_for_target(caster: Dictionary, skill_id: String, skill: Dictionary, target: Dictionary, target_cell: Vector2i) -> Dictionary:
|
||||
var best_preview := {}
|
||||
var best_score := -999999
|
||||
for origin in _unit_threat_origins(caster):
|
||||
for center_cell in _skill_cells_from_origin(caster, skill, origin):
|
||||
if not _skill_area_cells(center_cell, skill).has(target_cell):
|
||||
continue
|
||||
var targets := _skill_targets_for_cast(caster, skill_id, skill, center_cell)
|
||||
if targets.is_empty():
|
||||
continue
|
||||
var preview := _build_skill_threat_preview(caster, skill_id, skill, target, origin, center_cell, targets)
|
||||
var score := _skill_threat_preview_sort_score(preview)
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best_preview = preview
|
||||
return best_preview
|
||||
|
||||
|
||||
func _build_skill_threat_preview(caster: Dictionary, skill_id: String, skill: Dictionary, target: Dictionary, origin: Vector2i, center_cell: Vector2i, targets: Array[Dictionary]) -> Dictionary:
|
||||
var skill_kind := str(skill.get("kind", "damage"))
|
||||
var preview := {
|
||||
"source_id": str(caster.get("id", "")),
|
||||
"source_name": str(caster.get("name", caster.get("id", "Unit"))),
|
||||
"skill_id": skill_id,
|
||||
"skill_name": str(skill.get("name", skill_id)),
|
||||
"kind": skill_kind,
|
||||
"mp_cost": int(skill.get("mp_cost", 0)),
|
||||
"origin_cell": origin,
|
||||
"cast_cell": center_cell,
|
||||
"requires_move": origin != caster.get("pos", origin),
|
||||
"has_area": _skill_has_area(skill),
|
||||
"area_shape": _skill_area_shape(skill),
|
||||
"area_radius": _skill_area_radius(skill),
|
||||
"target_count": targets.size()
|
||||
}
|
||||
if skill_kind == "support":
|
||||
preview["effect_text"] = _format_skill_threat_effect_summary(skill)
|
||||
preview["already_active"] = _has_status_effect_from_skill(target, skill_id)
|
||||
else:
|
||||
var damage := calculate_skill_damage(caster, target, skill)
|
||||
var total_damage := 0
|
||||
var defeat_count := 0
|
||||
for area_target in targets:
|
||||
var area_damage := calculate_skill_damage(caster, area_target, skill)
|
||||
total_damage += area_damage
|
||||
if int(area_target.get("hp", 0)) - area_damage <= 0:
|
||||
defeat_count += 1
|
||||
preview["damage"] = damage
|
||||
preview["total_damage"] = total_damage
|
||||
preview["target_hp_after"] = max(0, int(target.get("hp", 0)) - damage)
|
||||
preview["would_defeat"] = int(target.get("hp", 0)) - damage <= 0
|
||||
preview["defeat_count"] = defeat_count
|
||||
return preview
|
||||
|
||||
|
||||
func _skill_threat_preview_sort_score(preview: Dictionary) -> int:
|
||||
var score := 0
|
||||
if str(preview.get("kind", "")) == "damage":
|
||||
score += int(preview.get("damage", 0)) * 10
|
||||
score += int(preview.get("defeat_count", 0)) * 2000
|
||||
if bool(preview.get("would_defeat", false)):
|
||||
score += 10000
|
||||
else:
|
||||
score += _skill_threat_effect_score(preview)
|
||||
if bool(preview.get("already_active", false)):
|
||||
score -= 250
|
||||
score += int(preview.get("target_count", 0)) * 20
|
||||
if bool(preview.get("has_area", false)):
|
||||
score += 5
|
||||
if not bool(preview.get("requires_move", false)):
|
||||
score += 2
|
||||
return score
|
||||
|
||||
|
||||
func _skill_threat_effect_score(preview: Dictionary) -> int:
|
||||
var skill := get_skill_def(str(preview.get("skill_id", "")))
|
||||
if skill.is_empty():
|
||||
return 0
|
||||
var score := 400
|
||||
for effect in skill.get("effects", []):
|
||||
if typeof(effect) != TYPE_DICTIONARY:
|
||||
continue
|
||||
var effect_type := str(effect.get("type", ""))
|
||||
if effect_type == "action_lock" and not str(effect.get("action", "")).strip_edges().is_empty():
|
||||
score += 350
|
||||
elif effect_type == "damage_over_time":
|
||||
score += 250 + max(0, int(effect.get("amount", 0))) * 20
|
||||
elif effect_type == "stat_bonus":
|
||||
score += 150 + abs(int(effect.get("amount", 0))) * 25
|
||||
return score
|
||||
|
||||
|
||||
func _format_skill_threat_effect_summary(skill: Dictionary) -> String:
|
||||
var parts := []
|
||||
for effect in skill.get("effects", []):
|
||||
if typeof(effect) != TYPE_DICTIONARY:
|
||||
continue
|
||||
var effect_type := str(effect.get("type", ""))
|
||||
if effect_type == "stat_bonus":
|
||||
var stat := str(effect.get("stat", ""))
|
||||
var amount := int(effect.get("amount", 0))
|
||||
if not _is_supported_status_stat(stat) or amount == 0:
|
||||
continue
|
||||
var sign := "+" if amount > 0 else ""
|
||||
parts.append("%s %s%d" % [stat.to_upper(), sign, amount])
|
||||
elif effect_type == "damage_over_time":
|
||||
var damage := int(effect.get("amount", 0))
|
||||
if damage <= 0:
|
||||
continue
|
||||
parts.append("%s -%d HP" % [_status_display_name(str(effect.get("status", "poison"))), damage])
|
||||
elif effect_type == "action_lock":
|
||||
var status_name := _status_display_name(str(effect.get("status", "status")))
|
||||
parts.append("%s %s" % [status_name, _action_lock_display_name(str(effect.get("action", "")))])
|
||||
if parts.is_empty():
|
||||
return _format_support_effects(skill)
|
||||
return _join_strings(parts, ", ")
|
||||
|
||||
|
||||
func _skill_threatens_opponents(skill: Dictionary) -> bool:
|
||||
var target_rule := str(skill.get("target", "enemy"))
|
||||
if target_rule != "enemy" and target_rule != "any":
|
||||
|
||||
Reference in New Issue
Block a user