177 lines
6.8 KiB
GDScript
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")
|