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