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