Files
rthythm_archor_game/autoload/combat_manager.gd
T
2026-07-06 22:59:47 -07:00

177 lines
6.8 KiB
GDScript

extends Node
const StatResolverScript := preload("res://scripts/resolvers/stat_resolver.gd")
const CombatResolverScript := preload("res://scripts/resolvers/combat_resolver.gd")
var _resolved_pairs: Dictionary = {}
func _physics_process(_delta: float) -> void:
_resolved_pairs.clear()
func resolve_damage(base_attack: float, action: Resource, judgement: Dictionary, buffs: Variant = null, burst: Variant = null) -> float:
return StatResolverScript.resolve_damage(base_attack, action, judgement, buffs, burst)
func resolve_cost(action: Resource, judgement: Variant = {}, effects: Variant = null) -> float:
return StatResolverScript.resolve_cost(action, judgement, effects)
func resolve_reward(action: Resource, judgement: Dictionary, effects: Variant = null) -> float:
return StatResolverScript.resolve_reward(action, judgement, effects)
func resolve_move(action: Resource, judgement: Dictionary, burst: Variant = null) -> Vector2:
return StatResolverScript.resolve_move(action, judgement, burst)
func resolve_action_snapshot(action: Resource, judgement: Dictionary = {}, effects: Variant = null) -> Dictionary:
return StatResolverScript.resolve_action_snapshot(action, judgement, effects)
func resolve_hit(emitter: Area2D, receiver: Area2D) -> Dictionary:
if emitter == null or receiver == null:
return {}
if _receiver_is_dead(receiver):
return {}
var key := "%s:%s" % [emitter.get_instance_id(), receiver.get_instance_id()]
if _resolved_pairs.has(key):
return _resolved_pairs[key]
var result := CombatResolverScript.resolve_hit(emitter, receiver)
_resolved_pairs[key] = result
_apply_interrupt(receiver, result)
_apply_target_health(receiver, result)
_apply_knockback(receiver, result)
if receiver.has_method("receive_hit"):
receiver.call("receive_hit", result)
_apply_action_on_hit_effects(receiver, result)
_dispatch_effect_events(emitter, receiver, result)
var bus := _event_bus_or_null()
if bus != null and bus.has_signal("hit_confirmed"):
bus.emit_signal("hit_confirmed", result)
return result
func _apply_target_health(receiver: Area2D, result: Dictionary) -> void:
var target := receiver.get_parent()
if target == null:
return
var health_component := target.get_node_or_null("HealthComponent")
if health_component != null and health_component.has_method("receive_hit"):
health_component.call("receive_hit", result)
func _apply_interrupt(receiver: Area2D, result: Dictionary) -> void:
if not bool(result.get("interrupts", true)):
return
var target := receiver.get_parent()
if target == null:
return
var action_controller := target.get_node_or_null("ActionController")
if action_controller != null and action_controller.has_method("cancel_current"):
action_controller.call("cancel_current", _cancel_reason_for_hit(target, result))
func _cancel_reason_for_hit(target: Node, result: Dictionary) -> StringName:
var health_component := target.get_node_or_null("HealthComponent")
if health_component != null and int(result.get("damage", 0)) > 0:
var current := int(health_component.get("current"))
if current > 0 and int(result.get("damage", 0)) >= current:
return &"death"
return &"interrupt"
func _apply_knockback(receiver: Area2D, result: Dictionary) -> void:
var knockback := result.get("knockback", Vector2.ZERO) as Vector2
if knockback == Vector2.ZERO:
return
var target := receiver.get_parent()
if target == null:
return
knockback.x = _knockback_x_away_from_attacker(target, result, knockback.x)
knockback.x *= _knockback_velocity_scale_for_receiver(target)
var movement_motor := target.get_node_or_null("MovementMotor")
if movement_motor != null and movement_motor.has_method("apply_knockback"):
movement_motor.call("apply_knockback", knockback)
## 受击方可选钩子(鸭子类型,同 shrugs_off_hit):knockback_distance_taken_mult()
## 返回"水平滑行距离"倍数。线性摩擦下距离 ∝ 初速²,距离 ×N 需初速 ×√N。
func _knockback_velocity_scale_for_receiver(target: Node) -> float:
if not target.has_method("knockback_distance_taken_mult"):
return 1.0
return sqrt(maxf(float(target.call("knockback_distance_taken_mult")), 0.0))
func _knockback_x_away_from_attacker(target: Node, result: Dictionary, knockback_x: float) -> float:
# resolve_knockback 输出无符号水平分量(数值契约,禁改);施加时按受击者
# 相对攻击者的位置定向为"背离攻击者"。dx 恰好为 0 时沿用历史 +X 语义。
if not (target is Node2D):
return knockback_x
var from = result.get("from")
if not (from is Vector2):
return knockback_x
var dx := (target as Node2D).global_position.x - (from as Vector2).x
if dx == 0.0:
return knockback_x
return signf(dx) * absf(knockback_x)
func _receiver_is_dead(receiver: Area2D) -> bool:
var target := receiver.get_parent()
if target == null:
return false
var state_machine := target.get_node_or_null("StateMachine")
if state_machine == null or not state_machine.has_method("build_context"):
return false
return StringName(str(state_machine.call("build_context").get("life_state", &"Alive"))) == &"Dead"
func _dispatch_effect_events(emitter: Area2D, receiver: Area2D, result: Dictionary) -> void:
_dispatch_actor_effect_event(emitter.get_parent(), &"on_hit", result)
_dispatch_actor_effect_event(receiver.get_parent(), &"on_hurt", result)
if _result_defense_state(result) == &"Parrying":
_dispatch_actor_effect_event(receiver.get_parent(), &"on_parry_success", result)
if int(result.get("damage", 0)) > 0 and _receiver_is_dead(receiver):
_dispatch_actor_effect_event(emitter.get_parent(), &"on_kill", result)
func _apply_action_on_hit_effects(receiver: Area2D, result: Dictionary) -> void:
var target := receiver.get_parent()
if target == null:
return
var effect_container := target.get_node_or_null("EffectContainer")
if effect_container == null or not effect_container.has_method("add_effect"):
return
var action: Resource = result.get("action", null) as Resource
if action == null:
return
var effects = action.get("on_hit_effects")
if not effects is Array:
return
for definition: Resource in effects:
if definition != null:
effect_container.call("add_effect", definition, StringName(str(action.get("id"))))
func _dispatch_actor_effect_event(actor: Node, event_name: StringName, result: Dictionary) -> void:
if actor == null:
return
var effect_container := actor.get_node_or_null("EffectContainer")
if effect_container != null and effect_container.has_method("dispatch_event"):
effect_container.call("dispatch_event", event_name, {"result": result})
func _result_defense_state(result: Dictionary) -> StringName:
var defense = result.get("defense", {})
if defense is Dictionary:
return StringName(str(defense.get("defense_state", &"Vulnerable")))
return &"Vulnerable"
func _event_bus_or_null() -> Node:
if not is_inside_tree():
return null
return get_tree().root.get_node_or_null("EventBus")