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

199 lines
6.2 KiB
GDScript

class_name ChargeComponent
extends Node
signal charge_changed(current: float, maximum: float, charge_ready: bool, active: bool)
# wave: CHARGING UP OVERLAY loops while holding S (author spec 50fps).
# blade_rain: SWORD RAIN PREP OVERLAY plays frames 1-16 (ring forms) and holds;
# frames 17-19 (ring launches) belong to the blade_rain_cast release animation.
const OVERLAY_PROFILES := {
&"wave": {
"path": "res://assets/art/characters/player/10_wave_charge/charging_up_overlay_fx.png",
"hframes": 5, "vframes": 5, "first": 0, "last": 24, "fps": 50.0,
"offset": Vector2(-92.0, -140.0), "loop": true,
},
&"blade_rain": {
"path": "res://assets/art/characters/player/08_sword_charge/sword_rain_prep_overlay_fx.png",
"hframes": 10, "vframes": 2, "first": 1, "last": 16, "fps": 25.0,
"offset": Vector2(-64.0, -305.0), "loop": false,
},
}
# Fallback ramp when no ActionController drives the gauge (standalone use).
@export var charge_duration := 1.1
@export var animation_player_path: NodePath
@export var effect_sprite_path: NodePath
var value := 0.0
var charge_ready := false
var active := false
var overlay_profile: StringName = &"wave"
var _effect_time := 0.0
var _animation_time := 0.0
var _maximum := 1.1
var _last_level := 0
@onready var _animation_player: AnimationPlayer = get_node_or_null(animation_player_path) as AnimationPlayer
@onready var _effect_sprite: Sprite2D = get_node_or_null(effect_sprite_path) as Sprite2D
@onready var _action_controller: Node = get_node_or_null("../ActionController")
func tick(delta: float, is_charging: bool) -> void:
if not is_charging:
if active or value > 0.0 or charge_ready:
cancel()
return
if not active:
_start()
if not active:
return
_update_charge_animation(delta)
var charge_state := _controller_charge_state()
if charge_state.is_empty():
_maximum = charge_duration
value = minf(charge_duration, value + delta)
charge_ready = value >= charge_duration
else:
# The gauge is level-space: 0 units = level 1 fresh, max units = top
# level. The cast level itself is owned by ActionController.
var max_level := maxi(2, int(charge_state.get("max_level", 3)))
var level := int(charge_state.get("level", 1))
_maximum = float(max_level - 1)
value = clampf(float(charge_state.get("progress_units", 0.0)), 0.0, _maximum)
charge_ready = level >= max_level
if level > _last_level and _last_level > 0:
_pulse_level_up()
_last_level = level
_update_charge_effect(delta)
_emit_changed()
func cancel() -> void:
active = false
value = 0.0
charge_ready = false
_animation_time = 0.0
_last_level = 0
_set_effect_visible(false)
if _effect_sprite != null:
_effect_sprite.modulate = Color.WHITE
_emit_changed()
func is_active() -> bool:
return active
func is_ready() -> bool:
return charge_ready
func maximum() -> float:
return _maximum
func _start() -> void:
active = true
value = 0.0
charge_ready = false
_effect_time = 0.0
_animation_time = 0.0
_last_level = 1
_setup_charge_overlay()
if _effect_sprite != null:
_effect_sprite.modulate = Color.WHITE
_update_charge_effect(0.0)
_emit_changed()
func _controller_charge_state() -> Dictionary:
if _action_controller == null:
_action_controller = get_node_or_null("../ActionController")
if _action_controller == null or not _action_controller.has_method("charge_state"):
return {}
var charge_state = _action_controller.call("charge_state")
if charge_state is Dictionary and bool((charge_state as Dictionary).get("charging", false)):
return charge_state
return {}
func _pulse_level_up() -> void:
if _effect_sprite != null:
_effect_sprite.modulate = Color(1.7, 1.7, 1.7)
func set_overlay_profile(profile: StringName) -> void:
if profile == overlay_profile or not OVERLAY_PROFILES.has(profile):
return
overlay_profile = profile
if active:
_effect_time = 0.0
_setup_charge_overlay()
func _current_overlay() -> Dictionary:
return OVERLAY_PROFILES.get(overlay_profile, OVERLAY_PROFILES[&"wave"])
func _setup_charge_overlay() -> void:
if _effect_sprite == null:
return
var overlay := _current_overlay()
var texture_path := str(overlay.get("path", ""))
var texture: Texture2D = load(texture_path) if ResourceLoader.exists(texture_path) else null
if texture == null:
return
_effect_sprite.texture = texture
_effect_sprite.hframes = maxi(1, int(overlay.get("hframes", 1)))
_effect_sprite.vframes = maxi(1, int(overlay.get("vframes", 1)))
_effect_sprite.offset = overlay.get("offset", Vector2.ZERO)
_effect_sprite.frame = clampi(int(overlay.get("first", 0)), 0, _effect_sprite.hframes * _effect_sprite.vframes - 1)
func _update_charge_effect(delta: float) -> void:
if _effect_sprite == null:
return
_effect_sprite.visible = active
if not active:
return
_effect_time += delta
if _effect_sprite.modulate != Color.WHITE:
_effect_sprite.modulate = _effect_sprite.modulate.lerp(Color.WHITE, minf(1.0, delta * 6.0))
var overlay := _current_overlay()
var first := int(overlay.get("first", 0))
var last := int(overlay.get("last", first))
var span := maxi(1, last - first + 1)
var step := int(_effect_time * float(overlay.get("fps", 25.0)))
var frame_index := first + (step % span if bool(overlay.get("loop", true)) else mini(step, span - 1))
_effect_sprite.frame = clampi(frame_index, 0, _effect_sprite.hframes * _effect_sprite.vframes - 1)
func _update_charge_animation(delta: float) -> void:
_animation_time += delta
var intro_length := _animation_length(&"warrior_charge_intro")
if _animation_time < intro_length:
_play_charge_animation(&"warrior_charge_intro")
else:
_play_charge_animation(&"warrior_charge_loop")
func _play_charge_animation(animation_name: StringName) -> void:
if _animation_player != null and _animation_player.has_animation(animation_name) and _animation_player.current_animation != animation_name:
_animation_player.play(animation_name)
func _animation_length(animation_name: StringName) -> float:
if _animation_player != null and _animation_player.has_animation(animation_name):
return maxf(0.1, _animation_player.get_animation(animation_name).length)
return 0.1
func _set_effect_visible(is_visible: bool) -> void:
if _effect_sprite != null:
_effect_sprite.visible = is_visible
func _emit_changed() -> void:
charge_changed.emit(value, _maximum, charge_ready, active)