Sensory-friendly: soft sine sounds, pentatonic ball tones, gentle errors, Zen mode
This commit is contained in:
+52
-24
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user