diff --git a/scripts/main.gd b/scripts/main.gd index b64c53f..677e982 100644 --- a/scripts/main.gd +++ b/scripts/main.gd @@ -29,6 +29,16 @@ const COLORS := { "viola": Color("9b59b6"), } +# Note pentatoniche (Do4 Re4 Mi4 Sol4 La4): qualsiasi pallina toccata +# produce sempre un suono armonico e piacevole (sensory-friendly). +const BALL_NOTES := { + "rosso": 261.63, + "blu": 293.66, + "verde": 329.63, + "giallo": 392.00, + "viola": 440.00, +} + var balls := {} # color_name -> Ball var target_color := "" # bersaglio corrente (vuoto = nessun bersaglio) var score := 0 @@ -51,6 +61,7 @@ var sound_launch: AudioStreamWAV var sound_error: AudioStreamWAV var sound_timeout: AudioStreamWAV var sound_milestone: AudioStreamWAV +var ball_sounds := {} # color_name -> AudioStreamWAV (tono pentatonico) var bgm: AudioStreamPlayer var options_menu: CanvasLayer @@ -70,6 +81,8 @@ func _process(delta: float) -> void: elapsed += delta if target_color == "": return # in attesa che una pallina torni a casa: nessun countdown + if Settings.zen_mode: + return # modalità zen: nessun countdown né decay (niente pressione) # il combo decade in proporzione al tempo che aspetto prima di cliccare if combo > 0.0: combo = maxf(0.0, combo - _combo_decay() * delta) @@ -214,10 +227,15 @@ func _build_ui() -> void: # ------------------------------------------------------------------- audio func _init_audio() -> void: - sound_launch = _create_tone(587.33, 0.12, "sine") # Re5 - sound_error = _create_tone(164.81, 0.18, "square") # Mi3 basso - sound_timeout = _create_tone(220.0, 0.22, "saw") # La3 - sound_milestone = _create_arpeggio([523.25, 659.25, 783.99, 1046.5], 0.12) # Do5 Mi5 Sol5 Do6 + # Suoni MORBIDI (sine, inviluppo dolce) per non sovrastimolare i bambini + # neurodivergenti: niente onde quadre/sega, volumi attenuati. + sound_launch = _create_tone(660.0, 0.18, 0.3) # marimba morbido + sound_error = _create_tone(220.0, 0.15, 0.16) # "pop" basso e attenuato + sound_timeout = _create_tone(330.0, 0.2, 0.16) # tono dolce + sound_milestone = _create_soft_arpeggio([523.25, 659.25, 783.99, 1046.5], 0.12) + # tono pentatonico per ogni pallina colorata + for cn in BALL_NOTES: + ball_sounds[cn] = _create_tone(BALL_NOTES[cn], 0.2, 0.3) func _setup_bgm() -> void: @@ -236,7 +254,7 @@ func _setup_bgm() -> void: bgm.play() -func _create_arpeggio(freqs: Array, note_dur: float = 0.12) -> AudioStreamWAV: +func _create_soft_arpeggio(freqs: Array, note_dur: float = 0.12) -> AudioStreamWAV: var sample_rate := 22050 var total := 0 for f in freqs: @@ -244,13 +262,17 @@ func _create_arpeggio(freqs: Array, note_dur: float = 0.12) -> AudioStreamWAV: var data := PackedByteArray() data.resize(total) var idx := 0 + var attack := int(sample_rate * 0.01) for f in freqs: var n := int(sample_rate * note_dur) for i in n: var t := float(i) / sample_rate - var env := 1.0 - (float(i) / n) + var env := 1.0 + if i < attack: + env = float(i) / attack + env *= exp(-3.0 * t / note_dur) var s := sin(t * f * TAU) * env - var val := int(clampf(s * 0.35 * 127.0, -128.0, 127.0)) + var val := int(clampf(s * 0.3 * 127.0, -128.0, 127.0)) data[idx] = val & 0xFF idx += 1 var stream := AudioStreamWAV.new() @@ -260,22 +282,22 @@ func _create_arpeggio(freqs: Array, note_dur: float = 0.12) -> AudioStreamWAV: return stream -func _create_tone(freq: float, duration: float, wave_type: String = "sine") -> AudioStreamWAV: +func _create_tone(freq: float, duration: float, amp: float = 0.3) -> AudioStreamWAV: + # Onda sinusoidale pura con attacco morbido e decadimento esponenziale: + # timbro dolce, senza clic né armoniche spigolose. var sample_rate := 22050 var num_samples := int(sample_rate * duration) var data := PackedByteArray() data.resize(num_samples) + var attack := int(sample_rate * 0.01) for i in num_samples: var t := float(i) / sample_rate - var env := 1.0 - (float(i) / num_samples) - var s := 0.0 - if wave_type == "sine": - s = sin(t * freq * TAU) - elif wave_type == "square": - s = 1.0 if sin(t * freq * TAU) > 0.0 else -1.0 - elif wave_type == "saw": - s = fmod(t * freq, 1.0) * 2.0 - 1.0 - var val := int(clampf(s * env * 0.35 * 127.0, -128.0, 127.0)) + var env := 1.0 + if i < attack: + env = float(i) / attack + env *= exp(-3.0 * t / duration) + var s := sin(t * freq * TAU) + var val := int(clampf(s * env * amp * 127.0, -128.0, 127.0)) data[i] = val & 0xFF var stream := AudioStreamWAV.new() stream.format = AudioStreamWAV.FORMAT_8_BITS @@ -324,10 +346,10 @@ func _handle_click(pos: Vector2) -> void: return # click su spazio vuoto if clicked.color_name == target_color: - # corretto: suono lancio, punti, nuovo bersaglio, reset timer - combo += 1.0 + # corretto: suono pentatonico della pallina, punti, nuovo bersaglio + combo = minf(float(Settings.max_combo), combo + 1.0) score += _points_for_combo() - _play_sound(sound_launch) + _play_sound(ball_sounds.get(clicked.color_name, sound_launch)) var mt := _current_time() clicked.launch(mt) window_time = mt @@ -336,16 +358,22 @@ func _handle_click(pos: Vector2) -> void: _update_hud() _check_milestone() else: - # tasto sbagliato: suono errore, reset combo, nessun game over + # tasto sbagliato: suono morbido, penalità gentile (combo -1) o reset _play_sound(sound_error) - combo = 0.0 + if Settings.gentle_errors: + combo = maxf(0.0, combo - 1.0) + else: + combo = 0.0 _update_hud() func _on_timeout() -> void: - # tempo scaduto: suono timeout, reset combo, nuovo bersaglio (se possibile), nessun game over + # tempo scaduto: suono dolce, penalità gentile, nuovo bersaglio, nessun game over _play_sound(sound_timeout) - combo = 0.0 + if Settings.gentle_errors: + combo = maxf(0.0, combo - 1.0) + else: + combo = 0.0 window_timer = 0.0 _pick_target() _update_hud() diff --git a/scripts/options_menu.gd b/scripts/options_menu.gd index 450b2d1..cdb5ef2 100644 --- a/scripts/options_menu.gd +++ b/scripts/options_menu.gd @@ -5,6 +5,8 @@ extends CanvasLayer var rows := {} # key -> {slider, spin} var curve_option: OptionButton +var zen_cb: CheckButton +var gentle_cb: CheckButton func _ready() -> void: @@ -53,6 +55,7 @@ func _build() -> void: ["time_ramp", "Rampa tempo (s)", 30.0, 600.0, 10.0], ["score_ramp", "Rampa punteggio", 100.0, 10000.0, 100.0], ["points_per_combo", "Punti per combo", 1.0, 50.0, 1.0], + ["max_combo", "Combo massimo", 1.0, 20.0, 1.0], ["combo_decay_threshold", "Soglia decay combo", 0.0, 30.0, 1.0], ["combo_decay_rate", "Rate decay combo", 0.0, 0.5, 0.01], ["difficulty_decay_rate", "Rate decay difficoltà", 0.0, 2.0, 0.05], @@ -78,6 +81,19 @@ func _build() -> void: curve_option.item_selected.connect(func(idx: int): Settings.score_curve = idx) ccurve.add_child(curve_option) + # opzioni accessibilità / neurodivergenza + zen_cb = CheckButton.new() + zen_cb.text = "Modalità Zen (senza tempo)" + zen_cb.button_pressed = Settings.zen_mode + zen_cb.toggled.connect(func(on: bool): Settings.zen_mode = on) + vb.add_child(zen_cb) + + gentle_cb = CheckButton.new() + gentle_cb.text = "Errori gentili (combo -1)" + gentle_cb.button_pressed = Settings.gentle_errors + gentle_cb.toggled.connect(func(on: bool): Settings.gentle_errors = on) + vb.add_child(gentle_cb) + # pulsanti var hb := HBoxContainer.new() hb.add_theme_constant_override("separation", 8) @@ -142,6 +158,10 @@ func show_menu() -> void: rows[key].spin.value = val if curve_option: curve_option.select(Settings.score_curve) + if zen_cb: + zen_cb.button_pressed = Settings.zen_mode + if gentle_cb: + gentle_cb.button_pressed = Settings.gentle_errors visible = true get_tree().paused = true @@ -165,6 +185,10 @@ func _on_reset() -> void: rows[key].spin.value = val if curve_option: curve_option.select(Settings.score_curve) + if zen_cb: + zen_cb.button_pressed = Settings.zen_mode + if gentle_cb: + gentle_cb.button_pressed = Settings.gentle_errors func _on_cancel() -> void: diff --git a/scripts/settings.gd b/scripts/settings.gd index a66e465..2ba3c34 100644 --- a/scripts/settings.gd +++ b/scripts/settings.gd @@ -4,7 +4,7 @@ extends Node ## al riavvio della scena. const PATH := "user://settings.cfg" -const VERSION := 2 # incrementa quando cambiano i default, per resettare i file vecchi +const VERSION := 3 # incrementa quando cambiano i default, per resettare i file vecchi var version := VERSION var start_time := 4.5 # tempo iniziale (s) tra un ordine e l'altro @@ -18,6 +18,9 @@ var combo_decay_rate := 0.15 # combo persi al secondo per unità sopra la s var difficulty_decay_rate := 0.3 # decay aggiuntivo legato alla difficoltà # Curva del punteggio: 0=quadratica, 1=scaglioni, 2=radice, 3=additiva var score_curve := 0 +# Accessibilità / neurodivergenza +var gentle_errors := true # errore: combo -1 invece di azzerare +var zen_mode := false # modalità senza tempo (niente countdown né decay) func _ready() -> void: @@ -44,6 +47,8 @@ func load_settings() -> void: combo_decay_rate = cfg.get_value("gameplay", "combo_decay_rate", combo_decay_rate) difficulty_decay_rate = cfg.get_value("gameplay", "difficulty_decay_rate", difficulty_decay_rate) score_curve = cfg.get_value("gameplay", "score_curve", score_curve) + gentle_errors = cfg.get_value("accessibility", "gentle_errors", gentle_errors) + zen_mode = cfg.get_value("accessibility", "zen_mode", zen_mode) func save_settings() -> void: @@ -59,6 +64,8 @@ func save_settings() -> void: cfg.set_value("gameplay", "combo_decay_rate", combo_decay_rate) cfg.set_value("gameplay", "difficulty_decay_rate", difficulty_decay_rate) cfg.set_value("gameplay", "score_curve", score_curve) + cfg.set_value("accessibility", "gentle_errors", gentle_errors) + cfg.set_value("accessibility", "zen_mode", zen_mode) cfg.save(PATH) @@ -73,3 +80,5 @@ func reset_defaults() -> void: combo_decay_rate = 0.15 difficulty_decay_rate = 0.3 score_curve = 0 + gentle_errors = true + zen_mode = false