Initial project sync
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
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
|
||||
Reference in New Issue
Block a user