Add area tactic effects

This commit is contained in:
2026-06-18 11:09:24 +09:00
parent 65e2508257
commit 47d02e1537
9 changed files with 314 additions and 94 deletions

View File

@@ -623,28 +623,36 @@ func try_cast_selected_skill(skill_id: String, target_cell: Vector2i) -> bool:
if not _skill_cells_from(caster, skill).has(target_cell):
return false
var target := get_unit_at(target_cell)
if not _skill_target_valid(caster, target, skill):
return false
var skill_kind := str(skill.get("kind", "damage"))
if skill_kind == "heal" and calculate_skill_heal(caster, target, skill) <= 0:
_emit_log("%s is already at full HP." % target["name"])
var targets := _skill_targets_for_cast(caster, skill_id, skill, target_cell)
if targets.is_empty():
if skill_kind == "heal":
_emit_log("%s has no injured targets in range." % skill.get("name", "Skill"))
else:
_emit_log("%s has no valid targets." % skill.get("name", "Skill"))
return false
if skill_kind == "support" and not _skill_has_support_effects(skill):
return false
caster["mp"] = max(0, int(caster.get("mp", 0)) - int(skill.get("mp_cost", 0)))
if skill_kind == "heal":
var healed := _resolve_skill_heal(caster, target, skill)
if healed > 0:
var total_healed := 0
for target in targets:
total_healed += _resolve_skill_heal(caster, target, skill)
if total_healed > 0:
_grant_experience(caster, EXP_HEAL)
elif skill_kind == "support":
if _resolve_skill_support(caster, target, skill_id, skill):
var applied_support := false
for target in targets:
applied_support = _resolve_skill_support(caster, target, skill_id, skill) or applied_support
if applied_support:
_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))
var defeated_count := 0
for target in targets:
if _resolve_skill_damage(caster, target, skill):
defeated_count += 1
_grant_experience(caster, EXP_SKILL + defeated_count * EXP_DEFEAT_BONUS)
caster["acted"] = true
caster["moved"] = true
@@ -741,6 +749,9 @@ func get_skill_preview(caster_id: String, skill_id: String, target_cell: Vector2
var target := get_unit_at(target_cell)
var skill_kind := str(skill.get("kind", "damage"))
var skill_locked := _unit_has_action_lock(caster, "skill")
var targets := _skill_targets_for_cast(caster, skill_id, skill, target_cell)
var area_cells := _skill_area_cells(target_cell, skill)
var has_area := _skill_has_area(skill)
var preview := {
"skill_id": skill_id,
"skill_name": str(skill.get("name", skill_id)),
@@ -748,25 +759,42 @@ func get_skill_preview(caster_id: String, skill_id: String, target_cell: Vector2
"mp_cost": int(skill.get("mp_cost", 0)),
"has_mp": int(caster.get("mp", 0)) >= int(skill.get("mp_cost", 0)),
"in_range": not skill_locked and _skill_cells_from(caster, skill).has(target_cell),
"valid_target": not skill_locked and _skill_target_valid(caster, target, skill),
"skill_locked": skill_locked
"valid_target": not skill_locked and not targets.is_empty(),
"skill_locked": skill_locked,
"has_area": has_area,
"area_cells": area_cells,
"target_count": targets.size()
}
if target.is_empty():
if targets.is_empty():
return preview
preview["target_id"] = target.get("id", "")
var primary_target: Dictionary = target if not target.is_empty() and _skill_target_valid(caster, target, skill) else targets[0]
preview["target_id"] = primary_target.get("id", "")
if skill_kind == "heal":
var heal_amount := calculate_skill_heal(caster, target, skill)
var heal_amount := calculate_skill_heal(caster, primary_target, skill)
var total_heal := 0
for area_target in targets:
total_heal += calculate_skill_heal(caster, area_target, skill)
preview["heal"] = heal_amount
preview["target_hp_after"] = min(int(target.get("max_hp", 1)), int(target.get("hp", 0)) + heal_amount)
preview["total_heal"] = total_heal
preview["target_hp_after"] = min(int(primary_target.get("max_hp", 1)), int(primary_target.get("hp", 0)) + heal_amount)
elif skill_kind == "support":
preview["effect_text"] = _format_support_effects(skill)
preview["already_active"] = _has_status_effect_from_skill(target, skill_id)
preview["already_active"] = _has_status_effect_from_skill(primary_target, skill_id)
else:
var damage := calculate_skill_damage(caster, target, skill)
var damage := calculate_skill_damage(caster, primary_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["target_hp_after"] = max(0, int(target.get("hp", 0)) - damage)
preview["would_defeat"] = int(target.get("hp", 0)) - damage <= 0
preview["total_damage"] = total_damage
preview["defeat_count"] = defeat_count
preview["target_hp_after"] = max(0, int(primary_target.get("hp", 0)) - damage)
preview["would_defeat"] = int(primary_target.get("hp", 0)) - damage <= 0
return preview
@@ -1209,6 +1237,18 @@ func get_skill_cells(unit_id: String, skill_id: String) -> Array[Vector2i]:
return _skill_cells_from(unit, skill)
func get_skill_area_cells(unit_id: String, skill_id: String, target_cell: Vector2i) -> Array[Vector2i]:
var unit := get_unit(unit_id)
var skill := get_skill_def(skill_id)
if unit.is_empty() or skill.is_empty() or not is_inside(target_cell):
return []
if not _unit_has_skill(unit, skill_id):
return []
if not _skill_cells_from(unit, skill).has(target_cell):
return []
return _skill_area_cells(target_cell, skill)
func get_item_target_cells(unit_id: String, item_id: String) -> Array[Vector2i]:
var unit := get_unit(unit_id)
var item := get_item_def(item_id)
@@ -1762,9 +1802,10 @@ func _unit_skill_threat_cells(unit: Dictionary) -> Array[Vector2i]:
if not _skill_threatens_opponents(skill):
continue
for origin in _unit_threat_origins(unit):
for cell in _skill_cells_from_origin(unit, skill, origin):
if not result.has(cell):
result.append(cell)
for center_cell in _skill_cells_from_origin(unit, skill, origin):
for cell in _skill_area_cells(center_cell, skill):
if not result.has(cell):
result.append(cell)
return result
@@ -1835,6 +1876,57 @@ func _skill_cells_from_origin(_unit: Dictionary, skill: Dictionary, origin: Vect
return result
func _skill_area_cells(center: Vector2i, skill: Dictionary) -> Array[Vector2i]:
var result: Array[Vector2i] = []
if not is_inside(center):
return result
var radius: int = _skill_area_radius(skill)
var shape := _skill_area_shape(skill)
if radius <= 0 or shape == "single":
result.append(center)
return result
for y in range(center.y - radius, center.y + radius + 1):
for x in range(center.x - radius, center.x + radius + 1):
var cell := Vector2i(x, y)
if not is_inside(cell):
continue
var distance := _manhattan(center, cell)
if shape == "diamond" and distance <= radius:
result.append(cell)
return result
func _skill_has_area(skill: Dictionary) -> bool:
return _skill_area_radius(skill) > 0 and _skill_area_shape(skill) != "single"
func _skill_area_shape(skill: Dictionary) -> String:
var shape := str(skill.get("area_shape", "single")).strip_edges().to_lower()
if shape == "diamond":
return shape
return "single"
func _skill_area_radius(skill: Dictionary) -> int:
return maxi(0, int(skill.get("area_radius", 0)))
func _skill_targets_for_cast(caster: Dictionary, skill_id: String, skill: Dictionary, target_cell: Vector2i, skip_existing_support := false) -> Array[Dictionary]:
var result: Array[Dictionary] = []
var skill_kind := str(skill.get("kind", "damage"))
for cell in _skill_area_cells(target_cell, skill):
var target := get_unit_at(cell)
if not _skill_target_valid(caster, target, skill):
continue
if skill_kind == "heal" and calculate_skill_heal(caster, target, skill) <= 0:
continue
if skill_kind == "support" and skip_existing_support and _has_status_effect_from_skill(target, skill_id):
continue
result.append(target)
return result
func _skill_range_pair(skill: Dictionary) -> Dictionary:
var value = skill.get("range", 1)
if typeof(value) == TYPE_ARRAY:
@@ -2230,25 +2322,31 @@ func _best_ai_heal_skill_action(caster: Dictionary) -> Dictionary:
continue
if int(caster.get("mp", 0)) < int(skill.get("mp_cost", 0)):
continue
for ally in get_living_units(str(caster.get("team", ""))):
if not _skill_cells_from(caster, skill).has(ally["pos"]):
for target_cell in _skill_cells_from(caster, skill):
var targets := _skill_targets_for_cast(caster, normalized_skill_id, skill, target_cell)
if targets.is_empty():
continue
if not _skill_target_valid(caster, ally, skill):
var total_heal := 0
var total_missing_hp := 0
var lowest_hp_ratio := 1.0
for ally in targets:
var heal_amount := calculate_skill_heal(caster, ally, skill)
var missing_hp := int(ally.get("max_hp", 1)) - int(ally.get("hp", 0))
var hp_ratio := float(ally.get("hp", 0)) / float(max(1, int(ally.get("max_hp", 1))))
total_heal += heal_amount
total_missing_hp += missing_hp
lowest_hp_ratio = minf(lowest_hp_ratio, hp_ratio)
if lowest_hp_ratio > 0.5 and total_missing_hp < 10:
continue
var heal_amount := calculate_skill_heal(caster, ally, skill)
if heal_amount <= 0:
continue
var missing_hp := int(ally.get("max_hp", 1)) - int(ally.get("hp", 0))
var hp_ratio := float(ally.get("hp", 0)) / float(max(1, int(ally.get("max_hp", 1))))
if hp_ratio > 0.5 and missing_hp < 10:
continue
var score := heal_amount * 10 + missing_hp
var score := total_heal * 10 + total_missing_hp + targets.size() * 4
score -= _manhattan(caster["pos"], target_cell)
if score > best_score:
best_score = score
best_action = {
"skill_id": normalized_skill_id,
"skill": skill,
"target": ally
"target": targets[0],
"target_cell": target_cell
}
return best_action
@@ -2265,23 +2363,24 @@ func _best_ai_support_skill_action(caster: Dictionary) -> Dictionary:
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"]):
for target_cell in _skill_cells_from(caster, skill):
var targets := _skill_targets_for_cast(caster, normalized_skill_id, skill, target_cell, true)
if targets.is_empty():
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)
var score := 0
for target in targets:
score += _support_ai_score(caster, target, skill)
if score <= 0:
continue
score -= _manhattan(caster["pos"], target["pos"])
score += targets.size() * 3
score -= _manhattan(caster["pos"], target_cell)
if score > best_score:
best_score = score
best_action = {
"skill_id": normalized_skill_id,
"skill": skill,
"target": target
"target": targets[0],
"target_cell": target_cell
}
return best_action
@@ -2404,25 +2503,32 @@ func _best_ai_damage_skill_action(caster: Dictionary, defeating_only := false) -
continue
if int(caster.get("mp", 0)) < int(skill.get("mp_cost", 0)):
continue
for target in get_living_units(target_team):
if not _skill_cells_from(caster, skill).has(target["pos"]):
for target_cell in _skill_cells_from(caster, skill):
var targets := _skill_targets_for_cast(caster, normalized_skill_id, skill, target_cell)
if targets.is_empty():
continue
if not _skill_target_valid(caster, target, skill):
var total_damage := 0
var defeat_count := 0
var priority_score := 0
for target in targets:
var damage := calculate_skill_damage(caster, target, skill)
total_damage += damage
priority_score += _ai_target_priority_score(target)
if int(target.get("hp", 0)) - damage <= 0:
defeat_count += 1
if defeating_only and defeat_count <= 0:
continue
var damage := calculate_skill_damage(caster, target, skill)
var would_defeat := int(target.get("hp", 0)) - damage <= 0
if defeating_only and not would_defeat:
continue
var score := damage + _ai_target_priority_score(target)
if would_defeat:
score += 1000
score -= _manhattan(caster["pos"], target["pos"])
var score := total_damage + priority_score + targets.size() * 5
if defeat_count > 0:
score += 1000 * defeat_count
score -= _manhattan(caster["pos"], target_cell)
if score > best_score:
best_score = score
best_action = {
"skill_id": normalized_skill_id,
"skill": skill,
"target": target
"target": targets[0],
"target_cell": target_cell
}
return best_action
@@ -2431,7 +2537,7 @@ func _execute_ai_skill_action(caster: Dictionary, action: Dictionary) -> bool:
var skill_id := str(action.get("skill_id", ""))
var skill: Dictionary = action.get("skill", {})
var target: Dictionary = action.get("target", {})
if skill_id.is_empty() or skill.is_empty() or target.is_empty():
if skill_id.is_empty() or skill.is_empty():
return false
if not _unit_has_skill(caster, skill_id):
return false
@@ -2439,29 +2545,38 @@ func _execute_ai_skill_action(caster: Dictionary, action: Dictionary) -> bool:
return false
if int(caster.get("mp", 0)) < int(skill.get("mp_cost", 0)):
return false
if not target.get("alive", false):
return false
if not _skill_cells_from(caster, skill).has(target["pos"]):
return false
if not _skill_target_valid(caster, target, skill):
var target_cell: Vector2i = action.get("target_cell", target.get("pos", Vector2i(-999, -999)))
if not is_inside(target_cell):
return false
var skill_kind := str(skill.get("kind", "damage"))
if skill_kind == "heal" and calculate_skill_heal(caster, target, skill) <= 0:
var skip_existing_support := skill_kind == "support"
var targets := _skill_targets_for_cast(caster, skill_id, skill, target_cell, skip_existing_support)
if targets.is_empty():
return false
if skill_kind == "support" and (not _skill_has_support_effects(skill) or _has_status_effect_from_skill(target, skill_id)):
if not _skill_cells_from(caster, skill).has(target_cell):
return false
if skill_kind == "support" and not _skill_has_support_effects(skill):
return false
caster["mp"] = max(0, int(caster.get("mp", 0)) - int(skill.get("mp_cost", 0)))
if skill_kind == "heal":
var healed := _resolve_skill_heal(caster, target, skill)
if healed > 0:
var total_healed := 0
for area_target in targets:
total_healed += _resolve_skill_heal(caster, area_target, skill)
if total_healed > 0:
_grant_experience(caster, EXP_HEAL)
elif skill_kind == "support":
if _resolve_skill_support(caster, target, skill_id, skill):
var applied_support := false
for area_target in targets:
applied_support = _resolve_skill_support(caster, area_target, skill_id, skill) or applied_support
if applied_support:
_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))
var defeated_count := 0
for area_target in targets:
if _resolve_skill_damage(caster, area_target, skill):
defeated_count += 1
_grant_experience(caster, EXP_SKILL + defeated_count * EXP_DEFEAT_BONUS)
_emit_log("%s spends %d MP." % [caster["name"], int(skill.get("mp_cost", 0))])
return true