Add area tactic effects
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user