Sensory-friendly: soft sine sounds, pentatonic ball tones, gentle errors, Zen mode

This commit is contained in:
enne2
2026-08-10 19:35:10 +02:00
parent e20482676f
commit f9837153a0
3 changed files with 86 additions and 25 deletions
+52 -24
View File
@@ -29,6 +29,16 @@ const COLORS := {
"viola": Color("9b59b6"), "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 balls := {} # color_name -> Ball
var target_color := "" # bersaglio corrente (vuoto = nessun bersaglio) var target_color := "" # bersaglio corrente (vuoto = nessun bersaglio)
var score := 0 var score := 0
@@ -51,6 +61,7 @@ var sound_launch: AudioStreamWAV
var sound_error: AudioStreamWAV var sound_error: AudioStreamWAV
var sound_timeout: AudioStreamWAV var sound_timeout: AudioStreamWAV
var sound_milestone: AudioStreamWAV var sound_milestone: AudioStreamWAV
var ball_sounds := {} # color_name -> AudioStreamWAV (tono pentatonico)
var bgm: AudioStreamPlayer var bgm: AudioStreamPlayer
var options_menu: CanvasLayer var options_menu: CanvasLayer
@@ -70,6 +81,8 @@ func _process(delta: float) -> void:
elapsed += delta elapsed += delta
if target_color == "": if target_color == "":
return # in attesa che una pallina torni a casa: nessun countdown 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 # il combo decade in proporzione al tempo che aspetto prima di cliccare
if combo > 0.0: if combo > 0.0:
combo = maxf(0.0, combo - _combo_decay() * delta) combo = maxf(0.0, combo - _combo_decay() * delta)
@@ -214,10 +227,15 @@ func _build_ui() -> void:
# ------------------------------------------------------------------- audio # ------------------------------------------------------------------- audio
func _init_audio() -> void: func _init_audio() -> void:
sound_launch = _create_tone(587.33, 0.12, "sine") # Re5 # Suoni MORBIDI (sine, inviluppo dolce) per non sovrastimolare i bambini
sound_error = _create_tone(164.81, 0.18, "square") # Mi3 basso # neurodivergenti: niente onde quadre/sega, volumi attenuati.
sound_timeout = _create_tone(220.0, 0.22, "saw") # La3 sound_launch = _create_tone(660.0, 0.18, 0.3) # marimba morbido
sound_milestone = _create_arpeggio([523.25, 659.25, 783.99, 1046.5], 0.12) # Do5 Mi5 Sol5 Do6 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: func _setup_bgm() -> void:
@@ -236,7 +254,7 @@ func _setup_bgm() -> void:
bgm.play() 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 sample_rate := 22050
var total := 0 var total := 0
for f in freqs: for f in freqs:
@@ -244,13 +262,17 @@ func _create_arpeggio(freqs: Array, note_dur: float = 0.12) -> AudioStreamWAV:
var data := PackedByteArray() var data := PackedByteArray()
data.resize(total) data.resize(total)
var idx := 0 var idx := 0
var attack := int(sample_rate * 0.01)
for f in freqs: for f in freqs:
var n := int(sample_rate * note_dur) var n := int(sample_rate * note_dur)
for i in n: for i in n:
var t := float(i) / sample_rate 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 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 data[idx] = val & 0xFF
idx += 1 idx += 1
var stream := AudioStreamWAV.new() var stream := AudioStreamWAV.new()
@@ -260,22 +282,22 @@ func _create_arpeggio(freqs: Array, note_dur: float = 0.12) -> AudioStreamWAV:
return stream 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 sample_rate := 22050
var num_samples := int(sample_rate * duration) var num_samples := int(sample_rate * duration)
var data := PackedByteArray() var data := PackedByteArray()
data.resize(num_samples) data.resize(num_samples)
var attack := int(sample_rate * 0.01)
for i in num_samples: for i in num_samples:
var t := float(i) / sample_rate var t := float(i) / sample_rate
var env := 1.0 - (float(i) / num_samples) var env := 1.0
var s := 0.0 if i < attack:
if wave_type == "sine": env = float(i) / attack
s = sin(t * freq * TAU) env *= exp(-3.0 * t / duration)
elif wave_type == "square": var s := sin(t * freq * TAU)
s = 1.0 if sin(t * freq * TAU) > 0.0 else -1.0 var val := int(clampf(s * env * amp * 127.0, -128.0, 127.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))
data[i] = val & 0xFF data[i] = val & 0xFF
var stream := AudioStreamWAV.new() var stream := AudioStreamWAV.new()
stream.format = AudioStreamWAV.FORMAT_8_BITS stream.format = AudioStreamWAV.FORMAT_8_BITS
@@ -324,10 +346,10 @@ func _handle_click(pos: Vector2) -> void:
return # click su spazio vuoto return # click su spazio vuoto
if clicked.color_name == target_color: if clicked.color_name == target_color:
# corretto: suono lancio, punti, nuovo bersaglio, reset timer # corretto: suono pentatonico della pallina, punti, nuovo bersaglio
combo += 1.0 combo = minf(float(Settings.max_combo), combo + 1.0)
score += _points_for_combo() score += _points_for_combo()
_play_sound(sound_launch) _play_sound(ball_sounds.get(clicked.color_name, sound_launch))
var mt := _current_time() var mt := _current_time()
clicked.launch(mt) clicked.launch(mt)
window_time = mt window_time = mt
@@ -336,16 +358,22 @@ func _handle_click(pos: Vector2) -> void:
_update_hud() _update_hud()
_check_milestone() _check_milestone()
else: else:
# tasto sbagliato: suono errore, reset combo, nessun game over # tasto sbagliato: suono morbido, penalità gentile (combo -1) o reset
_play_sound(sound_error) _play_sound(sound_error)
combo = 0.0 if Settings.gentle_errors:
combo = maxf(0.0, combo - 1.0)
else:
combo = 0.0
_update_hud() _update_hud()
func _on_timeout() -> void: 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) _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 window_timer = 0.0
_pick_target() _pick_target()
_update_hud() _update_hud()
+24
View File
@@ -5,6 +5,8 @@ extends CanvasLayer
var rows := {} # key -> {slider, spin} var rows := {} # key -> {slider, spin}
var curve_option: OptionButton var curve_option: OptionButton
var zen_cb: CheckButton
var gentle_cb: CheckButton
func _ready() -> void: func _ready() -> void:
@@ -53,6 +55,7 @@ func _build() -> void:
["time_ramp", "Rampa tempo (s)", 30.0, 600.0, 10.0], ["time_ramp", "Rampa tempo (s)", 30.0, 600.0, 10.0],
["score_ramp", "Rampa punteggio", 100.0, 10000.0, 100.0], ["score_ramp", "Rampa punteggio", 100.0, 10000.0, 100.0],
["points_per_combo", "Punti per combo", 1.0, 50.0, 1.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_threshold", "Soglia decay combo", 0.0, 30.0, 1.0],
["combo_decay_rate", "Rate decay combo", 0.0, 0.5, 0.01], ["combo_decay_rate", "Rate decay combo", 0.0, 0.5, 0.01],
["difficulty_decay_rate", "Rate decay difficoltà", 0.0, 2.0, 0.05], ["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) curve_option.item_selected.connect(func(idx: int): Settings.score_curve = idx)
ccurve.add_child(curve_option) 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 # pulsanti
var hb := HBoxContainer.new() var hb := HBoxContainer.new()
hb.add_theme_constant_override("separation", 8) hb.add_theme_constant_override("separation", 8)
@@ -142,6 +158,10 @@ func show_menu() -> void:
rows[key].spin.value = val rows[key].spin.value = val
if curve_option: if curve_option:
curve_option.select(Settings.score_curve) 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 visible = true
get_tree().paused = true get_tree().paused = true
@@ -165,6 +185,10 @@ func _on_reset() -> void:
rows[key].spin.value = val rows[key].spin.value = val
if curve_option: if curve_option:
curve_option.select(Settings.score_curve) 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: func _on_cancel() -> void:
+10 -1
View File
@@ -4,7 +4,7 @@ extends Node
## al riavvio della scena. ## al riavvio della scena.
const PATH := "user://settings.cfg" 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 version := VERSION
var start_time := 4.5 # tempo iniziale (s) tra un ordine e l'altro 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à var difficulty_decay_rate := 0.3 # decay aggiuntivo legato alla difficoltà
# Curva del punteggio: 0=quadratica, 1=scaglioni, 2=radice, 3=additiva # Curva del punteggio: 0=quadratica, 1=scaglioni, 2=radice, 3=additiva
var score_curve := 0 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: func _ready() -> void:
@@ -44,6 +47,8 @@ func load_settings() -> void:
combo_decay_rate = cfg.get_value("gameplay", "combo_decay_rate", combo_decay_rate) 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) difficulty_decay_rate = cfg.get_value("gameplay", "difficulty_decay_rate", difficulty_decay_rate)
score_curve = cfg.get_value("gameplay", "score_curve", score_curve) 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: 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", "combo_decay_rate", combo_decay_rate)
cfg.set_value("gameplay", "difficulty_decay_rate", difficulty_decay_rate) cfg.set_value("gameplay", "difficulty_decay_rate", difficulty_decay_rate)
cfg.set_value("gameplay", "score_curve", score_curve) 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) cfg.save(PATH)
@@ -73,3 +80,5 @@ func reset_defaults() -> void:
combo_decay_rate = 0.15 combo_decay_rate = 0.15
difficulty_decay_rate = 0.3 difficulty_decay_rate = 0.3
score_curve = 0 score_curve = 0
gentle_errors = true
zen_mode = false