171 lines
6.5 KiB
GDScript
171 lines
6.5 KiB
GDScript
class_name AttackBuffComponent
|
|
extends Node
|
|
|
|
## Sole writer of the time-anchor attack buff stacks. The stack storage itself
|
|
## is the Effect instance inside EffectContainer; damage injection rides the
|
|
## existing buffs multiplier slot in resolve_damage (no resolver changes).
|
|
|
|
const BUFF_EFFECT_PATH := "res://resources/effects/time_phase/effect_time_anchor_attack_buff.tres"
|
|
const BUFF_EFFECT_ID := &"time_anchor_attack_buff"
|
|
const MAX_STACKS := 7
|
|
const HEAL_WINDOW_BEATS := 4
|
|
const HEAL_BY_JUDGEMENT := {
|
|
&"perfect": 30,
|
|
&"good": 20,
|
|
&"bad": 10,
|
|
}
|
|
|
|
## 受伤规则:进入受伤状态掉一半 Buff 层数(向下取整),
|
|
## 每掉 1 层换 0.5 秒霸体(SuperArmor:伤害照吃、不打断、无击退)。
|
|
const HURT_ARMOR_EFFECT_PATH := "res://resources/effects/effect_hurt_super_armor.tres"
|
|
const HURT_ARMOR_EFFECT_ID := &"hurt_super_armor"
|
|
const HURT_ARMOR_SECONDS_PER_STACK := 0.5
|
|
|
|
@export var effect_container_path := NodePath("../EffectContainer")
|
|
@export var health_component_path := NodePath("../HealthComponent")
|
|
@export var state_machine_path := NodePath("../StateMachine")
|
|
|
|
@onready var effect_container: Node = get_node_or_null(effect_container_path)
|
|
@onready var health_component: Node = get_node_or_null(health_component_path)
|
|
@onready var state_machine: Node = get_node_or_null(state_machine_path)
|
|
|
|
var buff_definition: Resource
|
|
var hurt_armor_definition: Resource
|
|
var _last_healed_window := -1
|
|
|
|
|
|
func _ready() -> void:
|
|
if buff_definition == null and ResourceLoader.exists(BUFF_EFFECT_PATH):
|
|
buff_definition = load(BUFF_EFFECT_PATH)
|
|
if hurt_armor_definition == null and ResourceLoader.exists(HURT_ARMOR_EFFECT_PATH):
|
|
hurt_armor_definition = load(HURT_ARMOR_EFFECT_PATH)
|
|
if state_machine != null and state_machine.has_signal("axis_changed") and not state_machine.is_connected("axis_changed", _on_state_axis_changed):
|
|
state_machine.connect("axis_changed", _on_state_axis_changed)
|
|
for bus: Node in _event_buses():
|
|
if bus.has_signal("time_anchor_resolved") and not bus.is_connected("time_anchor_resolved", _on_time_anchor_resolved):
|
|
bus.connect("time_anchor_resolved", _on_time_anchor_resolved)
|
|
if bus.has_signal("time_phase_changed") and not bus.is_connected("time_phase_changed", _on_time_phase_changed):
|
|
bus.connect("time_phase_changed", _on_time_phase_changed)
|
|
if bus.has_signal("chart_reset") and not bus.is_connected("chart_reset", _on_chart_reset):
|
|
bus.connect("chart_reset", _on_chart_reset)
|
|
|
|
|
|
func attack_buff_stacks() -> int:
|
|
if effect_container == null or not effect_container.has_method("effect_stacks"):
|
|
return 0
|
|
return int(effect_container.call("effect_stacks", BUFF_EFFECT_ID))
|
|
|
|
|
|
func _on_time_anchor_resolved(anchor: Dictionary, held: bool, judgement: Dictionary) -> void:
|
|
if not held:
|
|
return
|
|
_apply_time_anchor_heal(anchor, judgement)
|
|
var label := _judgement_label(judgement)
|
|
if label == &"perfect" or label == &"good":
|
|
if effect_container != null and buff_definition != null:
|
|
effect_container.call("add_effect", buff_definition, &"time_anchor")
|
|
_broadcast()
|
|
|
|
|
|
func _on_time_phase_changed(_previous: StringName, _current: StringName, _reason: StringName) -> void:
|
|
if effect_container == null or not effect_container.has_method("set_effect_stacks"):
|
|
return
|
|
var reduced := maxi(0, attack_buff_stacks() - 2)
|
|
effect_container.call("set_effect_stacks", BUFF_EFFECT_ID, reduced)
|
|
_broadcast()
|
|
|
|
|
|
func _on_state_axis_changed(axis: StringName, _previous: StringName, current: StringName) -> void:
|
|
if axis == &"life_state" and current == &"Hitstun":
|
|
_drop_stacks_for_hurt()
|
|
|
|
|
|
## 受伤惩罚与补偿一体:掉 floor(层数 / 2) 层,掉几层就换几段霸体时间。
|
|
## 霸体期间后续攻击不再触发受伤状态,因此不会连锁掉层。
|
|
func _drop_stacks_for_hurt() -> void:
|
|
if effect_container == null or not effect_container.has_method("set_effect_stacks"):
|
|
return
|
|
var stacks := attack_buff_stacks()
|
|
var dropped := floori(stacks * 0.5)
|
|
if dropped <= 0:
|
|
return
|
|
effect_container.call("set_effect_stacks", BUFF_EFFECT_ID, stacks - dropped)
|
|
_apply_hurt_super_armor(dropped)
|
|
_broadcast()
|
|
|
|
|
|
func _apply_hurt_super_armor(dropped_stacks: int) -> void:
|
|
if hurt_armor_definition == null or not effect_container.has_method("add_effect"):
|
|
return
|
|
var armor := hurt_armor_definition.duplicate() as Resource
|
|
armor.set("duration", HURT_ARMOR_SECONDS_PER_STACK * float(dropped_stacks))
|
|
effect_container.call("add_effect", armor, &"hurt_buff_drop")
|
|
|
|
|
|
func cap_stacks(max_kept: int) -> void:
|
|
if effect_container == null or not effect_container.has_method("set_effect_stacks"):
|
|
return
|
|
var capped := clampi(attack_buff_stacks(), 0, maxi(0, max_kept))
|
|
effect_container.call("set_effect_stacks", BUFF_EFFECT_ID, capped)
|
|
_broadcast()
|
|
|
|
|
|
func _on_chart_reset(_chart_id: StringName) -> void:
|
|
_last_healed_window = -1
|
|
if effect_container != null and effect_container.has_method("remove_effect"):
|
|
effect_container.call("remove_effect", BUFF_EFFECT_ID)
|
|
_broadcast()
|
|
|
|
|
|
func _apply_time_anchor_heal(anchor: Dictionary, judgement: Dictionary) -> void:
|
|
var beat_value: Variant = anchor.get("beat", null)
|
|
if beat_value == null:
|
|
return
|
|
var label: StringName = _judgement_label(judgement)
|
|
if not HEAL_BY_JUDGEMENT.has(label):
|
|
return
|
|
var window_index: int = int(floor(float(int(beat_value)) / float(HEAL_WINDOW_BEATS)))
|
|
if window_index == _last_healed_window:
|
|
return
|
|
var health: Node = _health_component_or_null()
|
|
if health == null or not health.has_method("heal"):
|
|
return
|
|
var amount: int = int(HEAL_BY_JUDGEMENT[label])
|
|
var maximum: int = maxi(1, int(health.get("maximum")))
|
|
var current: int = int(health.get("current"))
|
|
if current < int(ceil(float(maximum) * 0.5)):
|
|
amount *= 2
|
|
health.call("heal", amount)
|
|
_last_healed_window = window_index
|
|
|
|
|
|
func _judgement_label(judgement: Dictionary) -> StringName:
|
|
if judgement.has("label"):
|
|
return StringName(str(judgement["label"]).to_lower())
|
|
if judgement.has(&"label"):
|
|
return StringName(str(judgement[&"label"]).to_lower())
|
|
return &""
|
|
|
|
|
|
func _health_component_or_null() -> Node:
|
|
if health_component != null and is_instance_valid(health_component):
|
|
return health_component
|
|
health_component = get_node_or_null(health_component_path)
|
|
return health_component
|
|
|
|
|
|
func _broadcast() -> void:
|
|
for bus: Node in _event_buses():
|
|
if bus.has_signal("attack_buff_changed"):
|
|
bus.emit_signal("attack_buff_changed", attack_buff_stacks(), MAX_STACKS)
|
|
|
|
|
|
func _event_buses() -> Array[Node]:
|
|
var buses: Array[Node] = []
|
|
if not is_inside_tree():
|
|
return buses
|
|
for child: Node in get_tree().root.get_children():
|
|
if child.has_signal("time_anchor_resolved") and child.has_signal("attack_buff_changed"):
|
|
buses.append(child)
|
|
return buses
|