Files
rthythm_archor_game/scenes/components/attack_buff_visual.gd
T
2026-07-06 22:59:47 -07:00

133 lines
4.3 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)