Add support and debuff tactics

This commit is contained in:
2026-06-18 04:27:05 +09:00
parent cf09306b8e
commit 01c7851b1d
8 changed files with 148 additions and 16 deletions

View File

@@ -852,7 +852,7 @@ func _resolve_skill_support(caster: Dictionary, target: Dictionary, skill_id: St
target["name"],
_format_support_effects(skill)
])
_emit_combat_feedback(target, _format_support_popup(skill), "support")
_emit_combat_feedback(target, _format_support_popup(skill), _support_feedback_kind(skill))
return true
@@ -910,6 +910,15 @@ func _format_support_popup(skill: Dictionary) -> String:
return _join_strings(parts, ", ")
func _support_feedback_kind(skill: Dictionary) -> String:
for effect in skill.get("effects", []):
if typeof(effect) != TYPE_DICTIONARY:
continue
if str(effect.get("type", "")) == "stat_bonus" and int(effect.get("amount", 0)) < 0:
return "debuff"
return "support"
func _format_active_status_effects(unit: Dictionary) -> String:
var parts := []
for effect in _active_status_effects(unit):
@@ -1871,6 +1880,10 @@ func _try_enemy_skill_action(enemy: Dictionary) -> bool:
if not heal_action.is_empty() and _execute_ai_skill_action(enemy, heal_action):
return true
var support_action := _best_ai_support_skill_action(enemy)
if not support_action.is_empty() and _execute_ai_skill_action(enemy, support_action):
return true
var damage_action := _best_ai_damage_skill_action(enemy, false)
if not damage_action.is_empty() and _execute_ai_skill_action(enemy, damage_action):
return true
@@ -1911,6 +1924,89 @@ func _best_ai_heal_skill_action(caster: Dictionary) -> Dictionary:
return best_action
func _best_ai_support_skill_action(caster: Dictionary) -> Dictionary:
var best_action := {}
var best_score := -1
for skill_id in caster.get("skills", []):
var normalized_skill_id := str(skill_id)
var skill := get_skill_def(normalized_skill_id)
if skill.is_empty() or str(skill.get("kind", "")) != "support":
continue
if int(caster.get("mp", 0)) < int(skill.get("mp_cost", 0)):
continue
if not _skill_has_support_effects(skill):
continue
for target in _support_ai_targets_for_skill(caster, skill):
if not _skill_cells_from(caster, skill).has(target["pos"]):
continue
if not _skill_target_valid(caster, target, skill):
continue
if _has_status_effect_from_skill(target, normalized_skill_id):
continue
var score := _support_ai_score(caster, target, skill)
if score <= 0:
continue
score -= _manhattan(caster["pos"], target["pos"])
if score > best_score:
best_score = score
best_action = {
"skill_id": normalized_skill_id,
"skill": skill,
"target": target
}
return best_action
func _support_ai_targets_for_skill(caster: Dictionary, skill: Dictionary) -> Array[Dictionary]:
var target_rule := str(skill.get("target", "ally"))
if target_rule == "self":
return [caster]
if target_rule == "enemy":
var target_team: String = _opposing_team(str(caster.get("team", "")))
return get_living_units(target_team) if not target_team.is_empty() else []
if target_rule == "any":
var targets: Array[Dictionary] = get_living_units(str(caster.get("team", "")))
var opposing_team: String = _opposing_team(str(caster.get("team", "")))
if not opposing_team.is_empty():
targets.append_array(get_living_units(opposing_team))
return targets
return get_living_units(str(caster.get("team", "")))
func _support_ai_score(caster: Dictionary, target: Dictionary, skill: Dictionary) -> int:
var score: int = 0
var target_is_ally: bool = target.get("team", "") == caster.get("team", "")
for effect in skill.get("effects", []):
if typeof(effect) != TYPE_DICTIONARY:
continue
if str(effect.get("type", "")) != "stat_bonus":
continue
var stat := str(effect.get("stat", ""))
var amount := int(effect.get("amount", 0))
if not _is_supported_status_stat(stat) or amount == 0:
continue
if amount > 0 and target_is_ally:
score += amount * _support_ai_stat_weight(stat)
if target.get("id", "") == caster.get("id", ""):
score += 3
elif amount < 0 and not target_is_ally:
score += absi(amount) * _support_ai_stat_weight(stat)
score += _ai_target_priority_score(target)
return score
func _support_ai_stat_weight(stat: String) -> int:
if stat == "atk":
return 9
if stat == "def":
return 7
if stat == "int":
return 6
if stat == "agi":
return 4
return 1
func _best_ai_damage_skill_action(caster: Dictionary, defeating_only := false) -> Dictionary:
var best_action := {}
var best_score := -1
@@ -1964,14 +2060,20 @@ func _execute_ai_skill_action(caster: Dictionary, action: Dictionary) -> bool:
return false
if not _skill_target_valid(caster, target, skill):
return false
if str(skill.get("kind", "")) == "heal" and calculate_skill_heal(caster, target, skill) <= 0:
var skill_kind := str(skill.get("kind", "damage"))
if skill_kind == "heal" and calculate_skill_heal(caster, target, skill) <= 0:
return false
if skill_kind == "support" and (not _skill_has_support_effects(skill) or _has_status_effect_from_skill(target, skill_id)):
return false
caster["mp"] = max(0, int(caster.get("mp", 0)) - int(skill.get("mp_cost", 0)))
if str(skill.get("kind", "")) == "heal":
if skill_kind == "heal":
var healed := _resolve_skill_heal(caster, target, skill)
if healed > 0:
_grant_experience(caster, EXP_HEAL)
elif skill_kind == "support":
if _resolve_skill_support(caster, target, skill_id, skill):
_grant_experience(caster, EXP_SUPPORT)
else:
var defeated := _resolve_skill_damage(caster, target, skill)
_grant_experience(caster, EXP_SKILL + (EXP_DEFEAT_BONUS if defeated else 0))