Files
lddc/scripts/CompanionController.gd
2026-11-04 18:10:32 +01:00

234 lines
7.1 KiB
GDScript

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
]