class_name CompanionController extends Node2D enum State { IDLE, DANCE, SLEEP } @export_group("Audio Reactivity") @export_range(0.01, 1.0, 0.01) var audio_threshold: float = 0.04 @export var dance_decay_time: float = 0.5 @export var idle_to_sleep_time: float = 2.5 @export_group("Rhythm & Squish") @export_range(0.05, 0.5, 0.01) var squish_amount: float = 0.22 @export var min_dance_animation_speed: float = 0.9 @export var max_dance_animation_speed: float = 2.0 @export_group("Dependencies") @export var audio_analyzer: SystemAudioAnalyzer @export var sprite: AnimatedSprite2D @export var debug_label: Label @export var volume_bar: ProgressBar @export var app_selector: OptionButton var current_state: State = State.IDLE var _silence_timer: float = 0.0 var _current_rms: float = 0.0 var _current_genre: String = "" var _current_genre_conf: float = 0.0 var _current_genre_source: String = "rules" var _current_genre_detail: String = "" # BPM dinamico: parte da 0 finché la canzone non viene agganciata var _current_bpm: float = 0.0 var _target_bpm: float = 0.0 var _base_scale: Vector2 = Vector2.ONE var _beat_phase: float = 0.0 var _current_squish: float = 0.0 var _breathe_time: float = 0.0 var _selected_pid: int = 0 var _known_apps: Array[Dictionary] = [] func _ready() -> void: _base_scale = scale _current_bpm = 0.0 _target_bpm = 0.0 if audio_analyzer: audio_analyzer.volume_updated.connect(_on_volume_updated) audio_analyzer.bpm_updated.connect(_on_bpm_updated) audio_analyzer.applications_updated.connect(_on_applications_updated) audio_analyzer.genre_updated.connect(_on_genre_updated) if app_selector: app_selector.item_selected.connect(_on_app_selected) app_selector.clear() app_selector.add_item("Tutto il sistema", 0) _set_state(State.IDLE) func _on_genre_updated(genre: String, confidence: float, _probs: Dictionary, source: String, detail: String) -> void: _current_genre = genre _current_genre_conf = confidence _current_genre_source = source _current_genre_detail = detail func _on_applications_updated(apps: Array[Dictionary]) -> void: if not app_selector: return # Evita di ricreare la lista se gli elementi sono identici (previene glitch della UI) var issame = (apps.size() == _known_apps.size()) if issame: for i in range(apps.size()): if apps[i]["pid"] != _known_apps[i]["pid"] or apps[i]["name"] != _known_apps[i]["name"]: issame = false break if issame: return _known_apps = apps var current_selected_pid = _selected_pid app_selector.clear() var selected_idx = 0 for i in range(apps.size()): var app = apps[i] var label_text = app["name"] if app["pid"] == 0: label_text = "Tutto il sistema" else: label_text = "%s (PID %d)" % [app["name"], app["pid"]] app_selector.add_item(label_text, i) if app["pid"] == current_selected_pid: selected_idx = i app_selector.select(selected_idx) func _on_app_selected(index: int) -> void: if index >= 0 and index < _known_apps.size(): var chosen = _known_apps[index] _selected_pid = chosen["pid"] # Resetta il BPM per rilevare prontamente la nuova applicazione _target_bpm = 0.0 _current_bpm = 0.0 if audio_analyzer: audio_analyzer.select_application_pid(_selected_pid) func _on_bpm_updated(detected_bpm: float) -> void: if detected_bpm > 50.0: _target_bpm = detected_bpm if _current_bpm <= 10.0: _current_bpm = detected_bpm func _process(delta: float) -> void: if _target_bpm > 50.0: _current_bpm = lerpf(_current_bpm, _target_bpm, delta * 3.0) _handle_state_logic(delta) _handle_rhythm_squish(delta) _update_debug_ui() func _on_volume_updated(rms: float) -> void: _current_rms = lerpf(_current_rms, rms, 0.35) func _handle_state_logic(delta: float) -> void: var is_audio_playing: bool = _current_rms >= audio_threshold match current_state: State.DANCE: if is_audio_playing: _silence_timer = 0.0 _scale_dance_speed(_current_rms) else: _silence_timer += delta if _silence_timer >= dance_decay_time: _set_state(State.IDLE) State.IDLE: if is_audio_playing: _set_state(State.DANCE) else: _silence_timer += delta if _silence_timer >= idle_to_sleep_time: _set_state(State.SLEEP) State.SLEEP: if is_audio_playing: _set_state(State.DANCE) func _set_state(new_state: State) -> void: current_state = new_state _silence_timer = 0.0 match current_state: State.IDLE: if sprite: sprite.speed_scale = 1.0 _play_animation("idle") State.DANCE: _scale_dance_speed(_current_rms) _play_animation("dance") State.SLEEP: if sprite: sprite.speed_scale = 1.0 _play_animation("sleep") func _scale_dance_speed(rms: float) -> void: var normalized_intensity: float = clampf((rms - audio_threshold) * 2.5, 0.0, 1.0) var target_speed: float = lerpf(min_dance_animation_speed, max_dance_animation_speed, normalized_intensity) if sprite: sprite.speed_scale = target_speed func _play_animation(anim_name: String) -> void: if sprite and sprite.sprite_frames and sprite.sprite_frames.has_animation(anim_name): sprite.play(anim_name) func _handle_rhythm_squish(delta: float) -> void: match current_state: State.DANCE: var effective_bpm = _current_bpm if _current_bpm > 50.0 else 130.0 var beats_per_second: float = effective_bpm / 60.0 _beat_phase += delta * beats_per_second * TAU var raw_wave: float = -cos(_beat_phase) var shaped_wave: float = sign(raw_wave) * pow(abs(raw_wave), 0.75) var dynamic_depth: float = squish_amount * clampf(0.8 + _current_rms * 0.8, 0.8, 1.5) var target_squish: float = shaped_wave * dynamic_depth _current_squish = lerpf(_current_squish, target_squish, delta * 25.0) scale.x = _base_scale.x * (1.0 + _current_squish * 0.85) scale.y = _base_scale.y * (1.0 - _current_squish) State.IDLE: _current_squish = lerpf(_current_squish, 0.0, delta * 6.0) _breathe_time += delta * 2.2 var breathe: float = sin(_breathe_time) * 0.025 scale = Vector2(_base_scale.x * (1.0 + breathe), _base_scale.y * (1.0 - breathe)) State.SLEEP: _current_squish = lerpf(_current_squish, 0.0, delta * 6.0) _breathe_time += delta * 1.2 var deep_breathe: float = sin(_breathe_time) * 0.018 scale = Vector2(_base_scale.x * (1.0 - deep_breathe), _base_scale.y * (1.0 + deep_breathe)) func _update_debug_ui() -> void: if volume_bar: volume_bar.value = _current_rms if debug_label: var bpm_str = "Rilevamento..." if _current_bpm > 50.0: bpm_str = "%d BPM" % round(_current_bpm) var genre_str: String = "Genere: --" if not _current_genre.is_empty() and _current_genre != "unknown": # Con i metadati mostriamo il genere specifico del catalogo ("Hard Rock"), # che e' piu' informativo della classe grossolana usata dal gameplay. var shown: String = _current_genre_detail if not _current_genre_detail.is_empty() else _current_genre var source_label: String = "meta" if _current_genre_source == "metadata" else _current_genre_source genre_str = "%s (%d%%) [%s]" % [shown, roundi(_current_genre_conf * 100.0), source_label] debug_label.text = "Stato: %s | Volume: %.2f | %s | %s" % [ State.keys()[current_state], _current_rms, bpm_str, genre_str ]