class_name CombatResolver extends RefCounted const StatResolverScript := preload("res://scripts/resolvers/stat_resolver.gd") const DefenseResolverScript := preload("res://scripts/resolvers/defense_resolver.gd") static func resolve_hit(emitter: Area2D, receiver: Area2D) -> Dictionary: var action: Resource = emitter.get("action_context") as Resource var judgement: Dictionary = emitter.get("judgement_context") var receiver_context := _receiver_context(receiver, emitter) var attacker_effects := _owner_effect_container(emitter) var defense := DefenseResolverScript.resolve_effective_defense(receiver_context, _action_tags(action)) var raw_damage := StatResolverScript.resolve_damage(float(emitter.get("damage")), action, judgement, attacker_effects, null) var final_damage := int(round(raw_damage * float(defense.get("damage_mult", 1.0)))) var attacker_interrupts := _attacker_interrupts(emitter) # 受击方免压制钩子(2026-07-05 定案):Boss 对不耗能量的攻击恒霸体—— # 伤害照常结算,但打断与击退一并取消。 if attacker_interrupts and _receiver_shrugs_off(receiver, action): attacker_interrupts = false var knockback := _resolved_knockback(emitter, action, judgement, attacker_effects, defense) if attacker_interrupts else Vector2.ZERO return { "damage": final_damage, "hit_type": StringName(str(emitter.get("hit_type"))), "action": action, "action_tags": _action_tags(action), "judgement": judgement, "from": emitter.global_position, "emitter": emitter, "receiver": receiver, "defense": defense, "interrupts": bool(defense.get("interrupts", true)) and attacker_interrupts, "knockback": knockback, } static func _attacker_interrupts(emitter: Area2D) -> bool: var value = emitter.get("attacker_interrupts") if value is bool: return value return true static func _receiver_shrugs_off(receiver: Area2D, action: Resource) -> bool: var owner := receiver.get_parent() if owner == null or not owner.has_method("shrugs_off_hit"): return false return bool(owner.call("shrugs_off_hit", action)) static func _receiver_context(receiver: Area2D, emitter: Area2D = null) -> Dictionary: var owner := receiver.get_parent() if owner != null: var context := {} var state_machine := owner.get_node_or_null("StateMachine") if state_machine != null and state_machine.has_method("build_context"): context = state_machine.call("build_context") var effect_container := owner.get_node_or_null("EffectContainer") if effect_container != null: context["effect_container"] = effect_container var block_judgement := _receiver_block_judgement(owner) if not block_judgement.is_empty(): context["block_judgement"] = block_judgement if emitter != null and owner is Node2D: context["attack_from_front"] = _attack_from_front(owner as Node2D, emitter) return context return {} static func _receiver_block_judgement(owner: Node) -> StringName: var action_controller := owner.get_node_or_null("ActionController") if action_controller == null: return &"" var intent = action_controller.get("current_intent") if intent == null: return &"" var judgement = intent.get("judgement") if judgement is Dictionary and judgement.has("label"): return StringName(str(judgement["label"])) return &"" static func _attack_from_front(owner: Node2D, emitter: Area2D) -> bool: var heading = owner.get("heading") if not (heading is Vector2) or is_zero_approx((heading as Vector2).x): return true var to_attacker := emitter.global_position.x - owner.global_position.x if is_zero_approx(to_attacker): return true return signf(to_attacker) == signf((heading as Vector2).x) static func _owner_effect_container(area: Area2D) -> Node: var source = area.get("source_actor") if source is Node and is_instance_valid(source): return (source as Node).get_node_or_null("EffectContainer") var owner := area.get_parent() if owner == null: return null return owner.get_node_or_null("EffectContainer") static func _action_tags(action: Resource) -> Array[StringName]: var result: Array[StringName] = [] if action != null and action.get("action_tags") is Array: for tag: Variant in action.get("action_tags"): result.append(StringName(str(tag))) return result static func _resolved_knockback(emitter: Area2D, action: Resource, judgement: Dictionary, attacker_effects: Variant, defense: Dictionary) -> Vector2: var defense_state := StringName(str(defense.get("defense_state", &"Vulnerable"))) if defense_state == &"Invincible" or defense_state == &"SuperArmor": return Vector2.ZERO return StatResolverScript.resolve_knockback(action, judgement, _base_knockback(emitter), attacker_effects) static func _base_knockback(emitter: Area2D) -> Vector2: var value = emitter.get("base_knockback") if value is Vector2: return value return Vector2.ZERO