Add snare movement lock tactic

This commit is contained in:
2026-06-18 10:11:16 +09:00
parent dcf45a3cd1
commit 14ca76d8cb
11 changed files with 121 additions and 55 deletions

View File

@@ -558,6 +558,9 @@ func try_move_selected(cell: Vector2i) -> bool:
return false
if unit.get("moved", false) or unit.get("acted", false):
return false
if _unit_has_action_lock(unit, "move"):
_emit_log("%s cannot move while snared." % unit["name"])
return false
if not get_movement_range(unit["id"]).has(cell):
return false
if not get_unit_at(cell).is_empty():
@@ -606,7 +609,7 @@ func try_cast_selected_skill(skill_id: String, target_cell: Vector2i) -> bool:
return false
if caster.get("acted", false):
return false
if _unit_has_skill_lock(caster):
if _unit_has_action_lock(caster, "skill"):
_emit_log("%s cannot use tactics while sealed." % caster["name"])
return false
if not _unit_has_skill(caster, skill_id):
@@ -732,7 +735,7 @@ 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_skill_lock(caster)
var skill_locked := _unit_has_action_lock(caster, "skill")
var preview := {
"skill_id": skill_id,
"skill_name": str(skill.get("name", skill_id)),
@@ -826,7 +829,7 @@ func is_unit_skill_sealed(unit_id: String) -> bool:
var unit := get_unit(unit_id)
if unit.is_empty():
return false
return _unit_has_skill_lock(unit)
return _unit_has_action_lock(unit, "skill")
func _effective_stat(unit: Dictionary, stat: String) -> int:
@@ -862,7 +865,7 @@ func _skill_has_support_effects(skill: Dictionary) -> bool:
return true
if effect_type == "damage_over_time" and int(effect.get("amount", 0)) > 0:
return true
if effect_type == "action_lock" and str(effect.get("action", "skill")) == "skill" and not str(effect.get("status", "seal")).strip_edges().is_empty():
if effect_type == "action_lock" and not str(effect.get("action", "")).strip_edges().is_empty() and not str(effect.get("status", "")).strip_edges().is_empty():
return true
return false
@@ -901,16 +904,17 @@ func _resolve_skill_support(caster: Dictionary, target: Dictionary, skill_id: St
"amount": damage,
"remaining_phases": max(1, int(effect.get("duration_turns", 1)))
})
elif effect_type == "action_lock" and str(effect.get("action", "skill")) == "skill":
var status := str(effect.get("status", "seal"))
if status.strip_edges().is_empty():
elif effect_type == "action_lock":
var action := str(effect.get("action", ""))
var status := str(effect.get("status", ""))
if action.strip_edges().is_empty() or status.strip_edges().is_empty():
continue
status_effects.append({
"source_skill": skill_id,
"name": str(skill.get("name", skill_id)),
"type": effect_type,
"status": status,
"action": "skill",
"action": action,
"remaining_phases": max(1, int(effect.get("duration_turns", 1)))
})
target["status_effects"] = status_effects
@@ -961,9 +965,9 @@ func _format_support_effects(skill: Dictionary) -> String:
continue
var status_name := _status_display_name(str(effect.get("status", "poison")))
parts.append("%s -%d HP for %s" % [status_name, damage, duration_text])
elif effect_type == "action_lock" and str(effect.get("action", "skill")) == "skill":
var status_name := _status_display_name(str(effect.get("status", "seal")))
parts.append("%s tactics for %s" % [status_name, duration_text])
elif effect_type == "action_lock":
var status_name := _status_display_name(str(effect.get("status", "status")))
parts.append("%s %s for %s" % [status_name, _action_lock_display_name(str(effect.get("action", ""))), duration_text])
if parts.is_empty():
return "No support effect."
return _join_strings(parts, ", ")
@@ -986,8 +990,8 @@ func _format_support_popup(skill: Dictionary) -> String:
var damage := int(effect.get("amount", 0))
if damage > 0:
parts.append(_status_display_name(str(effect.get("status", "poison"))).to_upper())
elif effect_type == "action_lock" and str(effect.get("action", "skill")) == "skill":
parts.append(_status_display_name(str(effect.get("status", "seal"))).to_upper())
elif effect_type == "action_lock":
parts.append(_status_display_name(str(effect.get("status", "status"))).to_upper())
if parts.is_empty():
return "Support"
return _join_strings(parts, ", ")
@@ -997,8 +1001,8 @@ func _support_feedback_kind(skill: Dictionary) -> String:
for effect in skill.get("effects", []):
if typeof(effect) != TYPE_DICTIONARY:
continue
if str(effect.get("type", "")) == "action_lock" and str(effect.get("action", "skill")) == "skill":
return str(effect.get("status", "seal"))
if str(effect.get("type", "")) == "action_lock":
return str(effect.get("status", "status"))
for effect in skill.get("effects", []):
if typeof(effect) != TYPE_DICTIONARY:
continue
@@ -1021,8 +1025,11 @@ func _format_active_status_effects(unit: Dictionary) -> String:
if amount > 0:
parts.append("%s -%d HP" % [_status_display_name(str(effect.get("status", "poison"))), amount])
continue
if effect_type == "action_lock" and str(effect.get("action", "skill")) == "skill":
parts.append("%s tactics" % _status_display_name(str(effect.get("status", "seal"))))
if effect_type == "action_lock":
parts.append("%s %s" % [
_status_display_name(str(effect.get("status", "status"))),
_action_lock_display_name(str(effect.get("action", "")))
])
continue
var stat := str(effect.get("stat", ""))
if not _is_supported_status_stat(stat) or amount == 0:
@@ -1043,13 +1050,24 @@ func _is_supported_status_stat(stat: String) -> bool:
return stat == "atk" or stat == "def" or stat == "int" or stat == "agi"
func _unit_has_skill_lock(unit: Dictionary) -> bool:
func _unit_has_action_lock(unit: Dictionary, action: String) -> bool:
var normalized_action := action.strip_edges().to_lower()
if normalized_action.is_empty():
return false
for effect in _active_status_effects(unit):
if str(effect.get("type", "")) == "action_lock" and str(effect.get("action", "skill")) == "skill":
if str(effect.get("type", "")) == "action_lock" and str(effect.get("action", "")).strip_edges().to_lower() == normalized_action:
return true
return false
func _action_lock_display_name(action: String) -> String:
if action == "skill":
return "tactics"
if action == "move":
return "movement"
return "action"
func try_wait_selected() -> bool:
var unit := get_selected_unit()
if unit.is_empty() or battle_status != STATUS_ACTIVE:
@@ -1148,6 +1166,8 @@ func get_movement_range(unit_id: String) -> Array[Vector2i]:
return []
if unit.get("team", "") == TEAM_PLAYER and not unit.get("deployed", true):
return []
if _unit_has_action_lock(unit, "move"):
return []
return _movement_range_for_unit(unit)
@@ -1171,7 +1191,7 @@ func get_skill_cells(unit_id: String, skill_id: String) -> Array[Vector2i]:
return []
if unit.get("team", "") == TEAM_PLAYER and not unit.get("deployed", true):
return []
if _unit_has_skill_lock(unit):
if _unit_has_action_lock(unit, "skill"):
return []
if not _unit_has_skill(unit, skill_id):
return []
@@ -1228,7 +1248,7 @@ func get_first_usable_skill_id(unit_id: String) -> String:
var unit := get_unit(unit_id)
if unit.is_empty() or typeof(unit.get("skills", [])) != TYPE_ARRAY:
return ""
if _unit_has_skill_lock(unit):
if _unit_has_action_lock(unit, "skill"):
return ""
for skill_id in unit.get("skills", []):
var normalized := str(skill_id)
@@ -1247,7 +1267,7 @@ func get_usable_skill_ids(unit_id: String) -> Array:
return result
if unit.get("team", "") == TEAM_PLAYER and not unit.get("deployed", true):
return result
if _unit_has_skill_lock(unit):
if _unit_has_action_lock(unit, "skill"):
return result
for skill_id in unit.get("skills", []):
var normalized := str(skill_id)
@@ -1624,6 +1644,8 @@ func get_turn_limit() -> int:
func _movement_range_for_unit(unit: Dictionary) -> Array[Vector2i]:
if _unit_has_action_lock(unit, "move"):
return []
var start: Vector2i = unit["pos"]
var frontier: Array[Vector2i] = [start]
var costs := {start: 0}
@@ -2053,7 +2075,7 @@ func _try_enemy_skill_action(enemy: Dictionary) -> bool:
return false
if typeof(enemy.get("skills", [])) != TYPE_ARRAY:
return false
if _unit_has_skill_lock(enemy):
if _unit_has_action_lock(enemy, "skill"):
return false
var finishing_action := _best_ai_damage_skill_action(enemy, true)
@@ -2183,10 +2205,11 @@ func _support_ai_score(caster: Dictionary, target: Dictionary, skill: Dictionary
var duration: int = max(1, int(effect.get("duration_turns", 1)))
score += damage * duration * 5
score += _ai_target_priority_score(target)
elif effect_type == "action_lock" and not target_is_ally and str(effect.get("action", "skill")) == "skill":
if _unit_has_skill_lock(target):
elif effect_type == "action_lock" and not target_is_ally:
var action := str(effect.get("action", ""))
if _unit_has_action_lock(target, action):
continue
var threat_score := _unit_skill_threat_score(target)
var threat_score := _unit_action_threat_score(target, action)
if threat_score <= 0:
continue
score += 18 + threat_score
@@ -2229,6 +2252,17 @@ func _unit_skill_threat_score(unit: Dictionary) -> int:
return score
func _unit_action_threat_score(unit: Dictionary, action: String) -> int:
if action == "skill":
return _unit_skill_threat_score(unit)
if action == "move":
var move_score := int(unit.get("move", 0)) * 5
if int(unit.get("range", 1)) <= 1:
move_score += 10
return move_score
return 0
func _best_ai_damage_skill_action(caster: Dictionary, defeating_only := false) -> Dictionary:
var best_action := {}
var best_score := -1
@@ -2274,7 +2308,7 @@ func _execute_ai_skill_action(caster: Dictionary, action: Dictionary) -> bool:
return false
if not _unit_has_skill(caster, skill_id):
return false
if _unit_has_skill_lock(caster):
if _unit_has_action_lock(caster, "skill"):
return false
if int(caster.get("mp", 0)) < int(skill.get("mp_cost", 0)):
return false