class_name RhythmTrack extends Control @onready var judgement_label: Label = $JudgementLabel @onready var center_base: TextureRect = $CenterBase @onready var center_flash: TextureRect = $CenterFlash @onready var left_mover: TextureRect = $LeftMover @onready var right_mover: TextureRect = $RightMover @onready var chart_marker_container: Control = $ChartMarkerContainer @export var bpm := 80.0 var chart_markers: Array[Control] = [] 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 center_flash_color := Color.WHITE var beat_flash := 0.0 var beat_age := 0.0 var feedback_flash := 0.0 func _ready() -> void: _cache_rhythm_track_layout() center_flash.modulate = Color(1.0, 1.0, 1.0, 0.0) var bus := _event_bus() bus.connect("beat_ticked", _on_beat_ticked) bus.connect("action_judged", _on_action_judged) bus.connect("chart_event_upcoming", _on_chart_event_upcoming) bus.connect("chart_event_triggered", _on_chart_event_triggered) func _process(delta: float) -> void: var visual_delta := minf(delta, 1.0 / 30.0) beat_age += delta beat_flash = maxf(0.0, beat_flash - visual_delta * 8.0) _update_movers() if feedback_flash > 0.0: feedback_flash = maxf(0.0, feedback_flash - visual_delta * 4.0) judgement_label.scale = Vector2.ONE * (1.0 + feedback_flash * 0.18) func _on_beat_ticked(_beat_index: int) -> void: center_flash_color = Color.WHITE beat_flash = 1.0 beat_age = 0.0 _update_movers() func _on_action_judged(action_name: StringName, rating: Dictionary) -> void: var diff := float(rating.get("diff", INF)) var color: Color = rating.get("color", Color("ff0055")) as Color judgement_label.text = "%s %s %s" % [ str(action_name).to_upper(), str(rating.get("label", "miss")).to_upper(), _format_signed_ms(diff), ] judgement_label.modulate = color judgement_label.scale = Vector2(1.18, 1.18) feedback_flash = 1.0 func _on_chart_event_upcoming(event: Resource, time_to_event: float) -> void: var marker := Label.new() marker.text = _chart_marker_text(event) marker.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER marker.vertical_alignment = VERTICAL_ALIGNMENT_CENTER marker.add_theme_font_size_override("font_size", 14) marker.add_theme_color_override("font_color", _chart_marker_color(event)) marker.custom_minimum_size = Vector2(54, 24) marker.position = _chart_marker_position(time_to_event) chart_marker_container.add_child(marker) chart_markers.append(marker) var tween := create_tween() tween.tween_property(marker, "modulate:a", 0.25, maxf(0.1, time_to_event)) tween.tween_callback(marker.queue_free) func _on_chart_event_triggered(event: Resource) -> void: if StringName(str(event.get("event_type"))) == &"camera_pulse": center_flash_color = Color(1.0, 0.84, 0.26, 1.0) else: center_flash_color = _chart_marker_color(event) beat_flash = 1.0 _update_movers() func _update_movers() -> void: var seconds_per_beat := 60.0 / maxf(1.0, bpm) var progress := clampf(beat_age / seconds_per_beat, 0.0, 1.0) 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(center_flash_color.r, center_flash_color.g, center_flash_color.b, 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 _chart_marker_text(event: Resource) -> String: match StringName(str(event.get("event_type"))): &"show_accent_marker": return "ACC" &"enemy_prepare_attack": return "WARN" &"enemy_attack_active": return "ATK" &"enemy_recovery": return "REC" &"camera_pulse": return "CAM" return str(event.get("event_type")).to_upper() func _chart_marker_color(event: Resource) -> Color: match StringName(str(event.get("event_type"))): &"show_accent_marker": return Color("ffd84a") &"enemy_prepare_attack": return Color("ff7a33") &"enemy_attack_active": return Color("ff3355") &"enemy_recovery": return Color("8aa0ff") &"camera_pulse": return Color("ffffff") return Color("00f2ff") func _chart_marker_position(time_to_event: float) -> Vector2: var seconds_per_beat := 60.0 / maxf(1.0, bpm) var beat_distance := clampf(time_to_event / seconds_per_beat, 0.0, 4.0) var x := track_center.x + beat_distance * 92.0 return Vector2(x - 27.0, track_center.y + 34.0) func _format_signed_ms(seconds: float) -> String: if is_inf(seconds): return "-- ms" return "%+.0f ms" % (seconds * 1000.0) func _event_bus() -> Node: var root := get_tree().root var bus := root.get_node_or_null("EventBus") if bus == null: bus = load("res://autoload/event_bus.gd").new() bus.name = "EventBus" root.add_child(bus) return bus