Add tactic seal action lock

This commit is contained in:
2026-06-18 09:55:54 +09:00
parent a20c099301
commit 23642a14e1
10 changed files with 181 additions and 28 deletions

View File

@@ -606,6 +606,9 @@ 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):
_emit_log("%s cannot use tactics while sealed." % caster["name"])
return false
if not _unit_has_skill(caster, skill_id):
return false
if int(caster.get("mp", 0)) < int(skill.get("mp_cost", 0)):
@@ -729,14 +732,16 @@ 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 preview := {
"skill_id": skill_id,
"skill_name": str(skill.get("name", skill_id)),
"kind": skill_kind,
"mp_cost": int(skill.get("mp_cost", 0)),
"has_mp": int(caster.get("mp", 0)) >= int(skill.get("mp_cost", 0)),
"in_range": _skill_cells_from(caster, skill).has(target_cell),
"valid_target": _skill_target_valid(caster, target, skill)
"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
}
if target.is_empty():
return preview
@@ -817,6 +822,13 @@ func get_status_effect_summary(unit_id: String) -> String:
return _format_active_status_effects(unit)
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)
func _effective_stat(unit: Dictionary, stat: String) -> int:
var base_value := int(unit.get(stat, 0))
for effect in _active_status_effects(unit):
@@ -850,6 +862,8 @@ 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():
return true
return false
@@ -887,6 +901,18 @@ 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():
continue
status_effects.append({
"source_skill": skill_id,
"name": str(skill.get("name", skill_id)),
"type": effect_type,
"status": status,
"action": "skill",
"remaining_phases": max(1, int(effect.get("duration_turns", 1)))
})
target["status_effects"] = status_effects
_emit_log("%s casts %s on %s. %s" % [
caster["name"],
@@ -935,6 +961,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])
if parts.is_empty():
return "No support effect."
return _join_strings(parts, ", ")
@@ -957,12 +986,19 @@ 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())
if parts.is_empty():
return "Support"
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", "")) == "action_lock" and str(effect.get("action", "skill")) == "skill":
return str(effect.get("status", "seal"))
for effect in skill.get("effects", []):
if typeof(effect) != TYPE_DICTIONARY:
continue
@@ -985,6 +1021,9 @@ 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"))))
continue
var stat := str(effect.get("stat", ""))
if not _is_supported_status_stat(stat) or amount == 0:
continue
@@ -1004,6 +1043,13 @@ 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:
for effect in _active_status_effects(unit):
if str(effect.get("type", "")) == "action_lock" and str(effect.get("action", "skill")) == "skill":
return true
return false
func try_wait_selected() -> bool:
var unit := get_selected_unit()
if unit.is_empty() or battle_status != STATUS_ACTIVE:
@@ -1125,6 +1171,8 @@ 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):
return []
if not _unit_has_skill(unit, skill_id):
return []
if int(unit.get("mp", 0)) < int(skill.get("mp_cost", 0)):
@@ -1180,6 +1228,8 @@ 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):
return ""
for skill_id in unit.get("skills", []):
var normalized := str(skill_id)
var skill := get_skill_def(normalized)
@@ -1197,6 +1247,8 @@ 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):
return result
for skill_id in unit.get("skills", []):
var normalized := str(skill_id)
var skill := get_skill_def(normalized)
@@ -2005,6 +2057,8 @@ 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):
return false
var finishing_action := _best_ai_damage_skill_action(enemy, true)
if not finishing_action.is_empty() and _execute_ai_skill_action(enemy, finishing_action):
@@ -2133,6 +2187,14 @@ 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):
continue
var threat_score := _unit_skill_threat_score(target)
if threat_score <= 0:
continue
score += 18 + threat_score
score += _ai_target_priority_score(target)
return score
@@ -2148,6 +2210,29 @@ func _support_ai_stat_weight(stat: String) -> int:
return 1
func _unit_skill_threat_score(unit: Dictionary) -> int:
if typeof(unit.get("skills", [])) != TYPE_ARRAY:
return 0
if int(unit.get("mp", 0)) <= 0:
return 0
var score := 0
for skill_id in unit.get("skills", []):
var skill := get_skill_def(str(skill_id))
if skill.is_empty():
continue
if int(unit.get("mp", 0)) < int(skill.get("mp_cost", 0)):
continue
var kind := str(skill.get("kind", "damage"))
if kind == "damage":
score += 12
elif kind == "heal":
score += 9
elif kind == "support":
score += 7
return score
func _best_ai_damage_skill_action(caster: Dictionary, defeating_only := false) -> Dictionary:
var best_action := {}
var best_score := -1
@@ -2193,6 +2278,8 @@ 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):
return false
if int(caster.get("mp", 0)) < int(skill.get("mp_cost", 0)):
return false
if not target.get("alive", false):