extends Node2D @onready var rhythm_conductor: Node = $RhythmConductor @onready var rhythm_track: Control = $RhythmFeedback/RhythmTrack @onready var rhythm_feedback_label: Label = $RhythmFeedback/JudgementLabel @onready var player: Node = $Player @onready var center_base: TextureRect = $RhythmFeedback/RhythmTrack/CenterBase @onready var center_flash: TextureRect = $RhythmFeedback/RhythmTrack/CenterFlash @onready var left_mover: TextureRect = $RhythmFeedback/RhythmTrack/LeftMover @onready var right_mover: TextureRect = $RhythmFeedback/RhythmTrack/RightMover @onready var combo_skill_label: Label = $RhythmFeedback/ComboSkillLabel @onready var health_bar: ProgressBar = $RhythmFeedback/StatusBars/HealthBar @onready var charge_bar: ProgressBar = $RhythmFeedback/StatusBars/ChargeBar @onready var energy_segments: Array[Panel] = [ $RhythmFeedback/StatusBars/EnergyBar/Segment0, $RhythmFeedback/StatusBars/EnergyBar/Segment1, $RhythmFeedback/StatusBars/EnergyBar/Segment2, $RhythmFeedback/StatusBars/EnergyBar/Segment3, $RhythmFeedback/StatusBars/EnergyBar/Segment4, $RhythmFeedback/StatusBars/EnergyBar/Segment5, $RhythmFeedback/StatusBars/EnergyBar/Segment6, $RhythmFeedback/StatusBars/EnergyBar/Segment7, $RhythmFeedback/StatusBars/EnergyBar/Segment8, $RhythmFeedback/StatusBars/EnergyBar/Segment9, ] @onready var combo_slot_panels: Array[PanelContainer] = [ $RhythmFeedback/ComboWindow/Slot0, $RhythmFeedback/ComboWindow/Slot1, $RhythmFeedback/ComboWindow/Slot2, $RhythmFeedback/ComboWindow/Slot3, ] @onready var combo_key_labels: Array[Label] = [ $RhythmFeedback/ComboWindow/Slot0/Key, $RhythmFeedback/ComboWindow/Slot1/Key, $RhythmFeedback/ComboWindow/Slot2/Key, $RhythmFeedback/ComboWindow/Slot3/Key, ] var combo_clear_tween: Tween var combo_clear_flash := 0.0 var charge_bar_ready := false var charge_flash := 0.0 var track_center := Vector2.ZERO var left_mover_start := Vector2.ZERO var right_mover_start := Vector2.ZERO var mover_size := Vector2.ZERO var center_flash_size := Vector2.ZERO var feedback_flash := 0.0 var beat_flash := 0.0 func _ready() -> void: _cache_rhythm_track_layout() rhythm_conductor.action_judged.connect(_on_rhythm_action_judged) rhythm_conductor.beat.connect(_on_rhythm_beat) if player.has_signal("combo_window_changed"): player.connect("combo_window_changed", _on_combo_window_changed) if player.has_signal("combo_window_cleared"): player.connect("combo_window_cleared", _on_combo_window_cleared) if player.has_signal("skill_requested"): player.connect("skill_requested", _on_skill_requested) if player.has_signal("energy_changed"): player.connect("energy_changed", _on_energy_changed) if player.has_signal("health_changed"): player.connect("health_changed", _on_health_changed) if player.has_signal("charge_changed"): player.connect("charge_changed", _on_charge_changed) rhythm_feedback_label.text = "READY" _on_combo_window_changed([]) if player.has_method("get_energy") and player.has_method("get_max_energy"): _on_energy_changed(player.call("get_energy"), player.call("get_max_energy")) if player.has_method("get_health") and player.has_method("get_max_health"): _on_health_changed(player.call("get_health"), player.call("get_max_health")) if player.has_method("get_charge") and player.has_method("get_max_charge") and player.has_method("is_charge_ready") and player.has_method("is_charge_active"): _on_charge_changed(player.call("get_charge"), player.call("get_max_charge"), player.call("is_charge_ready"), player.call("is_charge_active")) _update_rhythm_track(0.0) func _process(delta: float) -> void: _update_rhythm_track(delta) _update_combo_clear_animation(delta) _update_charge_bar_flash(delta) if feedback_flash > 0.0: feedback_flash = maxf(0.0, feedback_flash - delta * 4.0) rhythm_feedback_label.scale = Vector2.ONE * (1.0 + feedback_flash * 0.18) func _on_rhythm_action_judged(action_name: String, rating: Dictionary) -> void: var rating_name: String = str(rating.get("label", "miss")) var color: Color = rating.get("color", Color("ff0055")) as Color var diff: float = float(rating.get("diff", INF)) rhythm_feedback_label.text = "%s %s %s" % [ _format_action_name(action_name), rating_name.to_upper(), _format_signed_ms(diff), ] rhythm_feedback_label.modulate = color feedback_flash = 1.0 func _on_rhythm_beat(_position: int) -> void: beat_flash = 1.0 func _on_combo_window_changed(slots: Array) -> void: for index: int in range(combo_key_labels.size()): var filled := index < slots.size() var label := combo_key_labels[index] var panel := combo_slot_panels[index] label.text = str(slots[index]) if filled else "·" label.modulate = Color(1.0, 1.0, 1.0, 1.0 if filled else 0.32) panel.modulate = Color(1.0, 1.0, 1.0, 1.0 if filled else 0.48) if filled: _pulse_combo_slot(panel) func _on_combo_window_cleared(_reason: String) -> void: _play_combo_clear_animation() func _on_skill_requested(skill_id: String) -> void: combo_skill_label.text = _format_skill_name(skill_id) func _on_energy_changed(current: int, maximum: int) -> void: var filled_segments := clampi(current, 0, min(maximum, energy_segments.size())) for index: int in range(energy_segments.size()): var filled := index < filled_segments var panel := energy_segments[index] panel.modulate = Color(1.0, 1.0, 1.0, 1.0 if filled else 0.38) func _on_health_changed(current: int, maximum: int) -> void: health_bar.max_value = max(1, maximum) health_bar.value = clampi(current, 0, maximum) func _on_charge_changed(current: float, maximum: float, ready: bool, active: bool) -> void: charge_bar.max_value = maxf(0.01, maximum) charge_bar.value = clampf(current, 0.0, maximum) charge_bar_ready = ready and active if charge_bar_ready: return charge_bar.modulate = Color(1.0, 1.0, 1.0, 1.0 if active or current > 0.0 else 0.45) func _update_charge_bar_flash(delta: float) -> void: if not charge_bar_ready: charge_flash = 0.0 return charge_flash = fmod(charge_flash + delta * 7.0, TAU) var alpha := 0.62 + 0.38 * absf(sin(charge_flash)) charge_bar.modulate = Color(1.0, 1.0, 1.0, alpha) func _play_combo_clear_animation() -> void: if combo_clear_tween != null and combo_clear_tween.is_valid(): combo_clear_tween.kill() combo_clear_flash = 1.0 for panel: PanelContainer in combo_slot_panels: panel.scale = Vector2(1.16, 1.16) panel.modulate = Color(1.0, 1.0, 1.0, 1.0) func _update_combo_clear_animation(delta: float) -> void: if combo_clear_flash <= 0.0: return combo_clear_flash = maxf(0.0, combo_clear_flash - delta * 5.0) var eased := combo_clear_flash * combo_clear_flash for panel: PanelContainer in combo_slot_panels: panel.scale = Vector2.ONE * (1.0 + 0.16 * eased) panel.modulate = Color(1.0, 1.0, 1.0, 0.48 + 0.52 * eased) if combo_clear_flash <= 0.0: _restore_empty_combo_slots() func _pulse_combo_slot(panel: PanelContainer) -> void: var tween := create_tween() panel.scale = Vector2(1.08, 1.08) tween.tween_property(panel, "scale", Vector2.ONE, 0.09) func _restore_empty_combo_slots() -> void: for index: int in range(combo_slot_panels.size()): combo_slot_panels[index].modulate = Color(1.0, 1.0, 1.0, 0.48) combo_slot_panels[index].scale = Vector2.ONE combo_key_labels[index].text = "·" combo_key_labels[index].modulate = Color(1.0, 1.0, 1.0, 0.32) func _update_rhythm_track(delta: float) -> void: beat_flash = maxf(0.0, beat_flash - delta * 8.0) var progress := 0.0 if rhythm_conductor.has_method("get_current_beat_progress"): progress = float(rhythm_conductor.call("get_current_beat_progress")) if beat_flash > 0.15: progress = 1.0 _set_control_center(left_mover, left_mover_start.lerp(track_center, progress), mover_size) _set_control_center(right_mover, right_mover_start.lerp(track_center, progress), mover_size) _set_control_center(center_flash, track_center, center_flash_size) center_flash.modulate = Color(1.0, 1.0, 1.0, beat_flash) func _cache_rhythm_track_layout() -> void: track_center = _control_center(center_base) left_mover_start = _control_center(left_mover) right_mover_start = _control_center(right_mover) mover_size = left_mover.size center_flash_size = center_flash.size func _control_center(control: Control) -> Vector2: return Vector2( (control.offset_left + control.offset_right) * 0.5, (control.offset_top + control.offset_bottom) * 0.5 ) func _set_control_center(control: Control, center: Vector2, size: Vector2) -> void: control.offset_left = center.x - size.x * 0.5 control.offset_top = center.y - size.y * 0.5 control.offset_right = center.x + size.x * 0.5 control.offset_bottom = center.y + size.y * 0.5 func _format_action_name(action_name: String) -> String: match action_name: "w": return "W" "a": return "A" "d": return "D" "s": return "S" "space": return "SP" "skill_w": return "W" "skill_wa": return "W+A" "skill_wd": return "W+D" "skill_s": return "S" "skill_a": return "A" "skill_d": return "D" "skill_aa": return "A+A" "skill_dd": return "D+D" "skill_aaa": return "A+A+A" "skill_ddd": return "D+D+D" "skill_a_space": return "A+SP" "skill_d_space": return "D+SP" "skill_a_space_space": return "A+SP+SP" "skill_d_space_space": return "D+SP+SP" "skill_aa_space": return "A+A+SP" "skill_ad_space": return "A+D+SP" "skill_da_space": return "D+A+SP" "skill_dd_space": return "D+D+SP" "skill_s_projectile_1": return "S+SP" "skill_s_projectile_2": return "S+SP+SP" "skill_s_projectile_3": return "S+SP+SP+SP" _: return action_name.to_upper() func _format_skill_name(skill_id: String) -> String: match skill_id: "skill_w": return "W" "skill_wa": return "W+A" "skill_wd": return "W+D" "skill_s": return "S" "skill_a": return "A" "skill_d": return "D" "skill_aa": return "A+A" "skill_dd": return "D+D" "skill_aaa": return "A+A+A" "skill_ddd": return "D+D+D" "skill_a_space": return "A+SP" "skill_d_space": return "D+SP" "skill_a_space_space": return "A+SP+SP" "skill_d_space_space": return "D+SP+SP" "skill_aa_space": return "A+A+SP" "skill_ad_space": return "A+D+SP" "skill_da_space": return "D+A+SP" "skill_dd_space": return "D+D+SP" "skill_s_projectile_1": return "S+SP" "skill_s_projectile_2": return "S+SP+SP" "skill_s_projectile_3": return "S+SP+SP+SP" _: return skill_id.to_upper() func _format_signed_ms(seconds: float) -> String: if is_inf(seconds): return "-- ms" return "%+.0f ms" % (seconds * 1000.0)