4 Commits

13 changed files with 285 additions and 81 deletions
+8
View File
@@ -1,5 +1,13 @@
![UPENDI](/branding/upendi_logo.png)
# JuggleBoard Game
Un gioco arcade **reaction & chaining** per dispositivi mobili (orizzontale), realizzato in **Godot 4.7** (GDScript).
**Prodotto per la [Cooperativa Sociale UPENDI](https://www.upendi.it/)** (Gravina in Puglia) — Pedagogia del Circo e Circo Sociale.
> Il logo UPENDI è stato generato con l'API Gemini.
Un gioco arcade **reaction & chaining** per dispositivi mobili (orizzontale), realizzato in **Godot 4.7** (GDScript), ispirato al prodotto riabilitativo "Juggle Board".
## Gameplay
Binary file not shown.

After

Width:  |  Height:  |  Size: 510 KiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cew543durakby"
path="res://.godot/imported/boot_splash.png-8f633205b8d0e7391635b9ec17b0cd20.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://branding/boot_splash.png"
dest_files=["res://.godot/imported/boot_splash.png-8f633205b8d0e7391635b9ec17b0cd20.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dhlb2eosjbair"
path="res://.godot/imported/upendi_avatar.png-48ff18b41ca3a4a18bc21bb70b9da5b2.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://branding/upendi_avatar.png"
dest_files=["res://.godot/imported/upendi_avatar.png-48ff18b41ca3a4a18bc21bb70b9da5b2.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 798 KiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://4shhqux1km4c"
path="res://.godot/imported/upendi_logo.png-4e597672e26c4524fe84d6f00a5d9c10.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://branding/upendi_logo.png"
dest_files=["res://.godot/imported/upendi_logo.png-4e597672e26c4524fe84d6f00a5d9c10.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cf6ae642ot6du"
path="res://.godot/imported/upendi_logo_transparent.png-a8de36b73118f430d663844359491d71.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://branding/upendi_logo_transparent.png"
dest_files=["res://.godot/imported/upendi_logo_transparent.png-a8de36b73118f430d663844359491d71.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
+6 -1
View File
@@ -4,9 +4,14 @@ config_version=5
[application]
config/name="JuggleBoard"
config/description="Gioco arcade JuggleBoard per la Cooperativa Sociale UPENDI (Pedagogia del Circo)"
run/main_scene="res://scenes/main.tscn"
config/features=PackedStringArray("4.7", "GL Compatibility")
config/icon="res://icon.svg"
config/icon="res://branding/upendi_logo.png"
boot_splash/bg_color=Color(0.0980392, 0.34902, 0.427451, 1)
boot_splash/image="res://branding/boot_splash.png"
boot_splash/show_image=true
boot_splash/show_logo=false
[autoload]
+57 -39
View File
@@ -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()
@@ -363,11 +391,9 @@ func _current_time() -> float:
func _difficulty() -> float:
# fattori lineari di tempo e punteggio
var tf := clampf(elapsed / Settings.time_ramp, 0.0, 1.0)
# La difficoltà (e quindi la velocità massima) è legata unicamente al punteggio
var sf := clampf(score / Settings.score_ramp, 0.0, 1.0)
# curva dolce: smoothstep rende l'accelerazione quasi impercettibile all'inizio
return _smoothstep(maxf(tf, sf))
return _smoothstep(sf)
func _combo_decay() -> float:
@@ -379,17 +405,9 @@ func _combo_decay() -> float:
func _points_for_combo() -> int:
# Punti per un tap corretto, in base alla curva selezionata nelle Opzioni.
match Settings.score_curve:
1: # scaglioni: +0.5x ogni 5 tap di combo
var mult := 1.0 + floori(combo / 5.0) * 0.5
return int(Settings.points_per_combo * mult)
2: # radice quadrata
return int(Settings.points_per_combo * sqrt(combo))
3: # additiva: base + combo
return Settings.points_per_combo + int(combo)
_: # 0 = quadratica (attuale): base × combo
return Settings.points_per_combo * int(combo)
# Punti per lancio = combo corrente × valore punto configurato.
# Es. combo 5 × 1 = 5 pt; combo 1 × 1 = 1 pt. Il valore punto è configurabile.
return int(combo) * Settings.points_per_combo
func _smoothstep(x: float) -> float:
+24 -24
View File
@@ -4,7 +4,8 @@ extends CanvasLayer
## scena applicando i nuovi valori.
var rows := {} # key -> {slider, spin}
var curve_option: OptionButton
var zen_cb: CheckButton
var gentle_cb: CheckButton
func _ready() -> void:
@@ -50,9 +51,9 @@ func _build() -> void:
var entries := [
["start_time", "Tempo iniziale (s)", 1.0, 10.0, 0.1],
["min_time", "Tempo minimo (s)", 0.3, 3.0, 0.1],
["time_ramp", "Rampa tempo (s)", 30.0, 600.0, 10.0],
["score_ramp", "Rampa punteggio", 100.0, 10000.0, 100.0],
["score_ramp", "Punteggio per velocità max", 100.0, 20000.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],
@@ -60,23 +61,18 @@ func _build() -> void:
for e in entries:
rows[e[0]] = _build_row(vb, e[0], e[1], e[2], e[3], e[4])
# selettore curva punteggio
var ccurve := HBoxContainer.new()
ccurve.add_theme_constant_override("separation", 6)
vb.add_child(ccurve)
var clbl := Label.new()
clbl.text = "Curva punteggio"
clbl.custom_minimum_size.x = 230
ccurve.add_child(clbl)
curve_option = OptionButton.new()
curve_option.custom_minimum_size.x = 360
curve_option.add_item("Quadratica (attuale)")
curve_option.add_item("A scaglioni (ogni 5 tap)")
curve_option.add_item("Radice quadrata")
curve_option.add_item("Additiva (base + combo)")
curve_option.select(Settings.score_curve)
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()
@@ -140,8 +136,10 @@ func show_menu() -> void:
var val: float = Settings.get(key)
rows[key].slider.value = val
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
@@ -163,8 +161,10 @@ func _on_reset() -> void:
var val: float = Settings.get(key)
rows[key].slider.value = val
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:
+30 -17
View File
@@ -4,17 +4,20 @@ extends Node
## al riavvio della scena.
const PATH := "user://settings.cfg"
const VERSION := 5 # 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
var min_time := 0.8 # tempo minimo (velocità massima)
var time_ramp := 180.0 # secondi di gioco per difficoltà piena
var score_ramp := 2000.0 # punteggio per difficoltà piena
var points_per_combo := 10 # punti base per ogni livello di combo
var combo_decay_threshold := 5.0 # combo sopra cui inizia il decay
var combo_decay_rate := 0.06 # combo persi per unità sopra la soglia
var score_ramp := 8000.0 # punteggio per difficoltà piena (velocità massima)
var points_per_combo := 1 # punti per ogni lancio corretto (lineare, default 1)
var max_combo := 5.0 # combo massimo di default (configurabile)
var combo_decay_threshold := 0.0 # combo sopra cui inizia il decay (0 = da subito)
var combo_decay_rate := 0.15 # combo persi al secondo per unità sopra la soglia
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:
@@ -25,38 +28,48 @@ func load_settings() -> void:
var cfg := ConfigFile.new()
if cfg.load(PATH) != OK:
return # usa i default
version = cfg.get_value("meta", "version", 0)
if version < VERSION:
# file di impostazioni vecchio: applica i nuovi default e risalva
reset_defaults()
save_settings()
return
start_time = cfg.get_value("gameplay", "start_time", start_time)
min_time = cfg.get_value("gameplay", "min_time", min_time)
time_ramp = cfg.get_value("gameplay", "time_ramp", time_ramp)
score_ramp = cfg.get_value("gameplay", "score_ramp", score_ramp)
points_per_combo = cfg.get_value("gameplay", "points_per_combo", points_per_combo)
max_combo = cfg.get_value("gameplay", "max_combo", max_combo)
combo_decay_threshold = cfg.get_value("gameplay", "combo_decay_threshold", combo_decay_threshold)
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:
var cfg := ConfigFile.new()
cfg.set_value("meta", "version", VERSION)
cfg.set_value("gameplay", "start_time", start_time)
cfg.set_value("gameplay", "min_time", min_time)
cfg.set_value("gameplay", "time_ramp", time_ramp)
cfg.set_value("gameplay", "score_ramp", score_ramp)
cfg.set_value("gameplay", "points_per_combo", points_per_combo)
cfg.set_value("gameplay", "max_combo", max_combo)
cfg.set_value("gameplay", "combo_decay_threshold", combo_decay_threshold)
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)
func reset_defaults() -> void:
start_time = 4.5
min_time = 0.8
time_ramp = 180.0
score_ramp = 2000.0
points_per_combo = 10
combo_decay_threshold = 5.0
combo_decay_rate = 0.06
score_ramp = 8000.0
points_per_combo = 1
max_combo = 5.0
combo_decay_threshold = 0.0
combo_decay_rate = 0.15
difficulty_decay_rate = 0.3
score_curve = 0
gentle_errors = true
zen_mode = false