199 lines
6.2 KiB
GDScript
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)
|