class_name AttackBuffVisual extends Node2D const DEFAULT_FRAME_DIRECTORY := "res://assets/ui/buff_effect" const DEFAULT_MAX_STACKS := 7 @export var frame_directory := DEFAULT_FRAME_DIRECTORY @export var frames_per_second := 18.0 @export var center_offset := Vector2(0.0, -86.0) @export var minimum_scale := 0.85 @export var maximum_scale := 1.85 @export var minimum_alpha := 0.28 @export var maximum_alpha := 0.95 var _stacks := 0 var _max_stacks := DEFAULT_MAX_STACKS var _frame_index := 0 var _frame_elapsed := 0.0 var _pulse_elapsed := 0.0 var _frames: Array[Texture2D] = [] var _sprite: Sprite2D func _ready() -> void: z_index = 1 _sprite = Sprite2D.new() _sprite.name = "BuffEffect" _sprite.centered = true _sprite.position = center_offset _sprite.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST _sprite.visible = false add_child(_sprite) _load_frames() _connect_event_buses() call_deferred("_connect_event_buses") call_deferred("_sync_from_attack_buff_component") set_attack_buff(0, DEFAULT_MAX_STACKS) func _process(delta: float) -> void: if _stacks <= 0 or _frames.is_empty(): return var intensity := _intensity() _pulse_elapsed = fmod(_pulse_elapsed + delta * lerpf(1.3, 2.8, intensity), TAU) _frame_elapsed += delta var frame_duration := 1.0 / maxf(1.0, frames_per_second * lerpf(0.85, 1.35, intensity)) while _frame_elapsed >= frame_duration: _frame_elapsed -= frame_duration _frame_index = (_frame_index + 1) % _frames.size() _sprite.texture = _frames[_frame_index] _apply_effect_state() func set_attack_buff(stacks: int, max_stacks: int) -> void: _max_stacks = maxi(1, max_stacks) _stacks = clampi(stacks, 0, _max_stacks) set_process(_stacks > 0 and not _frames.is_empty()) visible = _stacks > 0 and not _frames.is_empty() if _sprite != null: _sprite.visible = visible if visible and _sprite.texture == null and not _frames.is_empty(): _sprite.texture = _frames[_frame_index] _apply_effect_state() func frame_count() -> int: return _frames.size() func _on_attack_buff_changed(stacks: int, max_stacks: int) -> void: set_attack_buff(stacks, max_stacks) func _load_frames() -> void: _frames.clear() var dir := DirAccess.open(frame_directory) if dir == null: return # 导出包内贴图在目录列表里显示为 xxx.png.import / xxx.png.remap; # 还原原名后 load() 才能命中。编辑器里原图与 .import 同时在列,需去重。 var files := PackedStringArray() dir.list_dir_begin() var file_name := dir.get_next() while not file_name.is_empty(): if not dir.current_is_dir(): var resource_name := file_name.trim_suffix(".import").trim_suffix(".remap") if resource_name.begins_with("frame") and resource_name.ends_with(".png") and not files.has(resource_name): files.append(resource_name) file_name = dir.get_next() dir.list_dir_end() files.sort() for path_name: String in files: var texture := load("%s/%s" % [frame_directory, path_name]) as Texture2D if texture != null: _frames.append(texture) if not _frames.is_empty() and _sprite != null: _sprite.texture = _frames[0] func _apply_effect_state() -> void: if _sprite == null: return if _stacks <= 0 or _frames.is_empty(): _sprite.visible = false return var intensity := _intensity() var pulse := 0.5 + 0.5 * sin(_pulse_elapsed) _sprite.position = center_offset _sprite.scale = Vector2.ONE * (lerpf(minimum_scale, maximum_scale, intensity) + pulse * lerpf(0.03, 0.18, intensity)) _sprite.modulate = Color(1.0, 1.0, 1.0, lerpf(minimum_alpha, maximum_alpha, intensity) * lerpf(0.78, 1.0, pulse)) _sprite.visible = true func _sync_from_attack_buff_component() -> void: var actor := get_parent() if actor != null: actor = actor.get_parent() var component := actor.get_node_or_null("AttackBuffComponent") if actor != null else null if component != null and component.has_method("attack_buff_stacks"): set_attack_buff(int(component.call("attack_buff_stacks")), DEFAULT_MAX_STACKS) func _connect_event_buses() -> void: if not is_inside_tree(): return for child: Node in get_tree().root.get_children(): if child.has_signal("attack_buff_changed") and not child.is_connected("attack_buff_changed", _on_attack_buff_changed): child.connect("attack_buff_changed", _on_attack_buff_changed) func _intensity() -> float: return clampf(float(_stacks) / float(maxi(1, _max_stacks)), 0.0, 1.0)