Add first support tactic

This commit is contained in:
2026-06-18 03:11:35 +09:00
parent b0d26530f2
commit 618a780796
9 changed files with 263 additions and 23 deletions

View File

@@ -18,6 +18,7 @@ const EXP_COUNTER := 5
const EXP_MISS := 2
const EXP_SKILL := 12
const EXP_HEAL := 8
const EXP_SUPPORT := 8
const EXP_DEFEAT_BONUS := 30
const EXP_LEVEL := 100
const BASE_HIT_CHANCE := 90
@@ -453,6 +454,7 @@ func _prepare_unit(source_unit: Dictionary) -> Dictionary:
unit["controllable"] = bool(unit.get("controllable", true))
unit["persist_progression"] = bool(unit.get("persist_progression", true))
unit["ai_target_priority"] = max(0, int(unit.get("ai_target_priority", 0)))
unit["status_effects"] = []
unit["acted"] = false
unit["moved"] = false
return unit
@@ -593,12 +595,17 @@ func try_cast_selected_skill(skill_id: String, target_cell: Vector2i) -> bool:
if skill_kind == "heal" and calculate_skill_heal(caster, target, skill) <= 0:
_emit_log("%s is already at full HP." % target["name"])
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:
_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))
@@ -712,6 +719,9 @@ func get_skill_preview(caster_id: String, skill_id: String, target_cell: Vector2
var heal_amount := calculate_skill_heal(caster, target, skill)
preview["heal"] = heal_amount
preview["target_hp_after"] = min(int(target.get("max_hp", 1)), int(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)
else:
var damage := calculate_skill_damage(caster, target, skill)
preview["damage"] = damage
@@ -748,29 +758,151 @@ func get_item_preview(user_id: String, item_id: String, target_cell: Vector2i) -
func calculate_damage(attacker: Dictionary, target: Dictionary) -> int:
var terrain_defense := get_terrain_defense(target["pos"])
return max(1, int(attacker["atk"]) - int(target["def"]) - terrain_defense)
return max(1, _effective_stat(attacker, "atk") - _effective_stat(target, "def") - terrain_defense)
func calculate_hit_chance(attacker: Dictionary, target: Dictionary) -> int:
var terrain_avoid := get_terrain_avoid(target["pos"])
var agility_delta := int(attacker.get("agi", 0)) - int(target.get("agi", 0))
var agility_delta := _effective_stat(attacker, "agi") - _effective_stat(target, "agi")
return clampi(BASE_HIT_CHANCE + agility_delta - terrain_avoid, MIN_HIT_CHANCE, 100)
func calculate_skill_damage(caster: Dictionary, target: Dictionary, skill: Dictionary) -> int:
var terrain_defense := get_terrain_defense(target["pos"])
var stat_name := str(skill.get("stat", "int"))
var stat_value := int(caster.get(stat_name, 0))
return max(1, int(skill.get("power", 1)) + stat_value - int(target.get("def", 0)) - terrain_defense)
var stat_value := _effective_stat(caster, stat_name)
return max(1, int(skill.get("power", 1)) + stat_value - _effective_stat(target, "def") - terrain_defense)
func calculate_skill_heal(caster: Dictionary, target: Dictionary, skill: Dictionary) -> int:
var stat_name := str(skill.get("stat", "int"))
var raw_heal := int(skill.get("power", 1)) + int(caster.get(stat_name, 0))
var raw_heal := int(skill.get("power", 1)) + _effective_stat(caster, stat_name)
var missing_hp := int(target.get("max_hp", 1)) - int(target.get("hp", 0))
return max(0, min(raw_heal, missing_hp))
func get_status_effect_summary(unit_id: String) -> String:
var unit := get_unit(unit_id)
if unit.is_empty():
return ""
return _format_active_status_effects(unit)
func _effective_stat(unit: Dictionary, stat: String) -> int:
var base_value := int(unit.get(stat, 0))
for effect in _active_status_effects(unit):
if str(effect.get("stat", "")) != stat:
continue
base_value += int(effect.get("amount", 0))
if stat == "atk":
return max(1, base_value)
return max(0, base_value)
func _active_status_effects(unit: Dictionary) -> Array:
var result := []
if typeof(unit.get("status_effects", [])) != TYPE_ARRAY:
return result
for effect in unit.get("status_effects", []):
if typeof(effect) != TYPE_DICTIONARY:
continue
if int(effect.get("remaining_phases", 0)) <= 0:
continue
result.append(effect)
return result
func _skill_has_support_effects(skill: Dictionary) -> bool:
for effect in skill.get("effects", []):
if typeof(effect) != TYPE_DICTIONARY:
continue
if str(effect.get("type", "")) == "stat_bonus" and _is_supported_status_stat(str(effect.get("stat", ""))) and int(effect.get("amount", 0)) != 0:
return true
return false
func _resolve_skill_support(caster: Dictionary, target: Dictionary, skill_id: String, skill: Dictionary) -> bool:
if not _skill_has_support_effects(skill):
return false
var status_effects := _status_effects_without_skill(target, skill_id)
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
status_effects.append({
"source_skill": skill_id,
"name": str(skill.get("name", skill_id)),
"stat": stat,
"amount": amount,
"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"],
skill.get("name", "Skill"),
target["name"],
_format_support_effects(skill)
])
return true
func _status_effects_without_skill(unit: Dictionary, skill_id: String) -> Array:
var result := []
for effect in _active_status_effects(unit):
if str(effect.get("source_skill", "")) == skill_id:
continue
result.append(effect)
return result
func _has_status_effect_from_skill(unit: Dictionary, skill_id: String) -> bool:
for effect in _active_status_effects(unit):
if str(effect.get("source_skill", "")) == skill_id:
return true
return false
func _format_support_effects(skill: Dictionary) -> String:
var parts := []
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
var sign := "+" if amount > 0 else ""
var duration := max(1, int(effect.get("duration_turns", 1)))
var duration_text := "next phase" if duration == 1 else "%d phases" % duration
parts.append("%s %s%d until %s" % [stat.to_upper(), sign, amount, duration_text])
if parts.is_empty():
return "No support effect."
return _join_strings(parts, ", ")
func _format_active_status_effects(unit: Dictionary) -> String:
var parts := []
for effect in _active_status_effects(unit):
var stat := str(effect.get("stat", ""))
var amount := int(effect.get("amount", 0))
if not _is_supported_status_stat(stat) or amount == 0:
continue
var sign := "+" if amount > 0 else ""
parts.append("%s %s%d" % [stat.to_upper(), sign, amount])
return _join_strings(parts, ", ")
func _is_supported_status_stat(stat: String) -> bool:
return stat == "atk" or stat == "def" or stat == "int" or stat == "agi"
func try_wait_selected() -> bool:
var unit := get_selected_unit()
if unit.is_empty() or battle_status != STATUS_ACTIVE:
@@ -2366,12 +2498,36 @@ func _spawn_event_deployment(deployment) -> bool:
func _reset_team_actions(team: String) -> void:
_expire_status_effects_for_team(team)
for unit in units:
if unit.get("team", "") == team and unit.get("alive", false):
unit["acted"] = false
unit["moved"] = false
func _expire_status_effects_for_team(team: String) -> void:
for unit in units:
if unit.get("team", "") != team:
continue
if typeof(unit.get("status_effects", [])) != TYPE_ARRAY:
continue
var kept_effects := []
var expired_names := []
for effect in _active_status_effects(unit):
var next_remaining := int(effect.get("remaining_phases", 1)) - 1
if next_remaining > 0:
var kept_effect: Dictionary = effect.duplicate(true)
kept_effect["remaining_phases"] = next_remaining
kept_effects.append(kept_effect)
else:
var effect_name := str(effect.get("name", "Support"))
if not expired_names.has(effect_name):
expired_names.append(effect_name)
unit["status_effects"] = kept_effects
for expired_effect_name in expired_names:
_emit_log("%s's %s fades." % [unit["name"], expired_effect_name])
func _manhattan(a: Vector2i, b: Vector2i) -> int:
return absi(a.x - b.x) + absi(a.y - b.y)
@@ -2380,6 +2536,15 @@ func _format_cell(cell: Vector2i) -> String:
return "(%d, %d)" % [cell.x + 1, cell.y + 1]
func _join_strings(values: Array, delimiter: String) -> String:
var text := ""
for value in values:
if not text.is_empty():
text += delimiter
text += str(value)
return text
func _emit_log(message: String) -> void:
log_added.emit(message)