Add weapon move-type effectiveness

This commit is contained in:
2026-06-18 11:20:55 +09:00
parent 47d02e1537
commit 6ca276082e
9 changed files with 173 additions and 10 deletions

View File

@@ -708,16 +708,19 @@ func get_damage_preview(attacker_id: String, target_id: String) -> Dictionary:
return {}
var attack_locked := _unit_has_action_lock(attacker, "attack")
var effective_bonus := 0 if attack_locked else _weapon_effectiveness_bonus(attacker, target)
var damage := 0 if attack_locked else calculate_damage(attacker, target)
var target_hp_after: int = maxi(0, int(target["hp"]) - damage)
var counter_damage := 0
var attacker_hp_after := int(attacker["hp"])
var hit_chance := 0 if attack_locked else calculate_hit_chance(attacker, target)
var counter_hit_chance := 0
var counter_effective_bonus := 0
var counter_in_range := false
if not attack_locked and (target_hp_after > 0 or hit_chance < 100):
counter_in_range = _is_in_attack_range(target, attacker["pos"])
if counter_in_range:
counter_effective_bonus = _weapon_effectiveness_bonus(target, attacker)
counter_damage = calculate_damage(target, attacker)
counter_hit_chance = calculate_hit_chance(target, attacker)
attacker_hp_after = maxi(0, int(attacker["hp"]) - counter_damage)
@@ -725,6 +728,8 @@ func get_damage_preview(attacker_id: String, target_id: String) -> Dictionary:
"attacker_id": attacker_id,
"target_id": target_id,
"damage": damage,
"effective_bonus": effective_bonus,
"effective_target_type": _weapon_effectiveness_target_type(target) if effective_bonus > 0 else "",
"hit_chance": hit_chance,
"target_hp_after": target_hp_after,
"would_defeat": int(target["hp"]) - damage <= 0,
@@ -732,6 +737,8 @@ func get_damage_preview(attacker_id: String, target_id: String) -> Dictionary:
"attack_locked": attack_locked,
"counter_in_range": counter_in_range,
"counter_damage": counter_damage,
"counter_effective_bonus": counter_effective_bonus,
"counter_effective_target_type": _weapon_effectiveness_target_type(attacker) if counter_effective_bonus > 0 else "",
"counter_hit_chance": counter_hit_chance,
"attacker_hp_after": attacker_hp_after,
"counter_would_defeat": attacker_hp_after <= 0
@@ -828,7 +835,8 @@ 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, _effective_stat(attacker, "atk") - _effective_stat(target, "def") - terrain_defense)
var effective_bonus := _weapon_effectiveness_bonus(attacker, target)
return max(1, _effective_stat(attacker, "atk") + effective_bonus - _effective_stat(target, "def") - terrain_defense)
func calculate_hit_chance(attacker: Dictionary, target: Dictionary) -> int:
@@ -851,6 +859,49 @@ func calculate_skill_heal(caster: Dictionary, target: Dictionary, skill: Diction
return max(0, min(raw_heal, missing_hp))
func _weapon_effectiveness_bonus(attacker: Dictionary, target: Dictionary) -> int:
var weapon := _equipped_weapon(attacker)
if weapon.is_empty():
return 0
if not _weapon_is_effective_against(weapon, target):
return 0
return max(0, int(weapon.get("effective_bonus_damage", 0)))
func _weapon_is_effective_against(weapon: Dictionary, target: Dictionary) -> bool:
var target_move_type := _weapon_effectiveness_target_type(target)
if target_move_type.is_empty():
return false
var effective_types = weapon.get("effective_vs_move_types", [])
if typeof(effective_types) != TYPE_ARRAY:
return false
for move_type in effective_types:
if str(move_type) == target_move_type:
return true
return false
func _weapon_effectiveness_target_type(target: Dictionary) -> String:
return str(target.get("move_type", "foot")).strip_edges()
func _format_move_type(move_type: String) -> String:
return move_type.replace("_", " ").capitalize()
func _equipped_weapon(unit: Dictionary) -> Dictionary:
var equipment = unit.get("equipment", {})
if typeof(equipment) != TYPE_DICTIONARY:
return {}
var weapon_id := str(equipment.get("weapon", ""))
if weapon_id.is_empty():
return {}
var weapon := get_item_def(weapon_id)
if str(weapon.get("kind", "")) != "weapon":
return {}
return weapon
func get_status_effect_summary(unit_id: String) -> String:
var unit := get_unit(unit_id)
if unit.is_empty():
@@ -2630,7 +2681,11 @@ func _resolve_attack(attacker: Dictionary, target: Dictionary, is_counter := fal
return {"hit": false, "defeated": false}
var damage := calculate_damage(attacker, target)
target["hp"] = max(0, int(target["hp"]) - damage)
_emit_log("%s %s %s for %d damage. Hit %d%%." % [attacker["name"], verb, target["name"], damage, hit_chance])
var effective_bonus := _weapon_effectiveness_bonus(attacker, target)
var effective_text := ""
if effective_bonus > 0:
effective_text = " Effective +%d vs %s." % [effective_bonus, _format_move_type(_weapon_effectiveness_target_type(target))]
_emit_log("%s %s %s for %d damage. Hit %d%%.%s" % [attacker["name"], verb, target["name"], damage, hit_chance, effective_text])
_emit_combat_feedback(target, "-%d" % damage, "damage")
if int(target["hp"]) <= 0:

View File

@@ -1535,19 +1535,23 @@ func _update_forecast() -> void:
var range_text := "ready" if preview["in_range"] else "out of range"
var defeat_text := " Defeat" if preview["would_defeat"] else ""
var effective_text := _format_effective_forecast_text(preview, "effective_bonus", "effective_target_type")
var counter_text := ""
if preview["counter_in_range"]:
var counter_defeat := " Defeat" if preview["counter_would_defeat"] else ""
counter_text = "\nCounter %d, Hit %d%%, your HP %d/%d.%s" % [
var counter_effective_text := _format_effective_forecast_text(preview, "counter_effective_bonus", "counter_effective_target_type")
counter_text = "\nCounter %d%s, Hit %d%%, your HP %d/%d.%s" % [
preview["counter_damage"],
counter_effective_text,
preview.get("counter_hit_chance", 0),
preview["attacker_hp_after"],
selected["max_hp"],
counter_defeat
]
forecast_label.text = "Forecast: %s\n%d damage, Hit %d%%, target HP %d/%d.%s%s" % [
forecast_label.text = "Forecast: %s\n%d damage%s, Hit %d%%, target HP %d/%d.%s%s" % [
range_text,
preview["damage"],
effective_text,
preview.get("hit_chance", 100),
preview["target_hp_after"],
target["max_hp"],
@@ -1556,6 +1560,16 @@ func _update_forecast() -> void:
]
func _format_effective_forecast_text(preview: Dictionary, bonus_key: String, type_key: String) -> String:
var bonus := int(preview.get(bonus_key, 0))
if bonus <= 0:
return ""
var move_type := _format_move_type(str(preview.get(type_key, "")))
if move_type.is_empty():
move_type = "Target"
return " (Effective +%d vs %s)" % [bonus, move_type]
func _update_skill_forecast(selected: Dictionary) -> void:
var preview := state.get_skill_preview(selected["id"], selected_skill_id, hover_cell)
if preview.is_empty():
@@ -3763,11 +3777,37 @@ func _format_equipment_bonus_text(item: Dictionary) -> String:
continue
var sign := "+" if amount > 0 else ""
parts.append("%s %s%d" % [stat.to_upper(), sign, amount])
var effective_text := _format_equipment_effectiveness_text(item)
if not effective_text.is_empty():
parts.append(effective_text)
if parts.is_empty():
return str(item.get("kind", "equipment")).capitalize()
return _join_strings(parts, ", ")
func _format_equipment_effectiveness_text(item: Dictionary) -> String:
if str(item.get("kind", "")) != "weapon":
return ""
var bonus := int(item.get("effective_bonus_damage", 0))
if bonus <= 0:
return ""
var move_types = item.get("effective_vs_move_types", [])
if typeof(move_types) != TYPE_ARRAY or move_types.is_empty():
return ""
var labels := []
for move_type in move_types:
var label := _format_move_type(str(move_type))
if not label.is_empty():
labels.append(label)
if labels.is_empty():
return ""
return "Effective +%d vs %s" % [bonus, _join_strings(labels, "/")]
func _format_move_type(move_type: String) -> String:
return move_type.strip_edges().replace("_", " ").capitalize()
func _on_equip_item_pressed(item_id: String) -> void:
var selected := state.get_selected_unit()
if selected.is_empty():