Show hostile tactic threat hints

This commit is contained in:
2026-06-18 11:43:44 +09:00
parent cecdb05a4a
commit ca51680b1c
6 changed files with 204 additions and 6 deletions

View File

@@ -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":