19 Commits
Author SHA1 Message Date
enne2 ea5b2aebbf Release v0.2: refine menus and JuggleBoard 2026-09-06 22:53:52 +02:00
enne2 f4a9da6bc3 Refactor Godot architecture into AppShell, modular menus and game domains 2026-09-06 17:07:05 +02:00
enne2 a73fb4b55f Rewrite technical README: features, modes, architecture, build/publish guides (web, Android APK, Gitea release) 2026-09-06 14:02:04 +02:00
enne2 de7dfdf9f7 Mode select dialog at startup (Target/Zen/Infinite), Zen only for Target, refined circus-wood look (codex) 2026-08-19 00:17:35 +02:00
enne2 725183290b Survival mode: 4 hearts HUD, lose heart on error/timeout, game over, mode selector (Classica/Zen/Survival), hearts vectorized (codex) 2026-08-18 21:58:35 +02:00
enne2 7c5850eb1d UI polish: victory overlay & lock dialog in wood/brass circus theme (codex); rounded board corners; per-ball veins 2026-08-18 20:18:05 +02:00
enne2 44160adc1a Add target score setting + victory overlay (animation, victory audio, reset button) 2026-08-18 18:45:21 +02:00
enne2 94f05adf34 Fix ball rolling animation (queue_redraw on roll_angle) and realistic marble design (codex) 2026-08-18 17:46:34 +02:00
enne2 8a38bb09dc Pixel-perfect vs mockup: circus arena bg, HUD cartouche with rivets, serif display font, rounded board, recessed sockets and target pocket (codex) 2026-08-18 17:16:33 +02:00
enne2 0e85992d40 Wood texture generated with Antigravity image model (replaces procedural) 2026-08-18 17:03:32 +02:00
enne2 fccc39c5d1 Wooden Juggle Board: full-width varying lanes, sockets, numbers 1-5, glossy rotating balls, wooden HUD (codex-verified) 2026-08-18 16:55:17 +02:00
enne2 cdcaf76531 Mockup v3: full-width varying lanes wooden board, circus arena, integrated wooden HUD (agy) 2026-08-18 16:34:24 +02:00
enne2 5db4660471 Mockup v2: wooden circus board with real game HUD preserved 2026-08-18 16:09:49 +02:00
enne2 9811ffe5b4 Add circus arena wooden board visual mockup 2026-08-18 15:58:47 +02:00
enne2 4d7aa698e3 Board hand-drawn style: cream board, recessed gray lanes with sockets, glossy balls 2026-08-18 14:59:56 +02:00
enne2 54619a2385 Score = combo x point value (configurable, default 1); difficulty score-only at 8000 2026-08-10 20:00:30 +02:00
enne2 f9837153a0 Sensory-friendly: soft sine sounds, pentatonic ball tones, gentle errors, Zen mode 2026-08-10 19:35:10 +02:00
enne2 e20482676f Fix combo decay: reset stale settings via version; cap combo at 5 by default 2026-08-10 19:25:01 +02:00
enne2 c7918d9f55 Add UPENDI branding: boot splash, icon, README logo (Gemini-generated) 2026-08-10 19:02:30 +02:00
220 changed files with 4079 additions and 803 deletions
+6 -2
View File
@@ -1,13 +1,17 @@
# Godot editor cache
.godot/
# Build output (web export)
build/
# Build output (web export); conserva solo il marker che impedisce il reimport Godot.
build/*
!build/.gdignore
# Os temporanei
*.tmp
*.log
# Pacchetti sorgente grezzi (archivi locali troppo grandi per Git)
docs/source_packages/*.zip
# OS
.DS_Store
Thumbs.db
+170 -38
View File
@@ -1,54 +1,186 @@
![UPENDI](/branding/upendi_logo.png)
# JuggleBoard Game
Un gioco arcade **reaction & chaining** per dispositivi mobili (orizzontale), realizzato in **Godot 4.7** (GDScript), ispirato al prodotto riabilitativo "Juggle Board".
Gioco arcade **reaction & chaining** per dispositivi mobili (orizzontale), realizzato in **Godot 4.7** (GDScript), ispirato al prodotto riabilitativo **"Juggle Board" by Craig Quat / Play Juggling**.
**Prodotto per la [Cooperativa Sociale UPENDI](https://www.upendi.it/)** (Gravina in Puglia) — Pedagogia del Circo e Circo Sociale. Target: bambini, anche neurodivergenti (autismo, ADHD, iperreattività sensoriale): suoni morbidi (sine, niente onde quadre/sega), palette tenue, modalità Zen e errori gentili.
---
## Gameplay
- **5 corsie orizzontali**, ognuna con una **pallina colorata** a riposo sul lato destro.
- Un **bersaglio in alto** indica quale pallina toccare.
- Toccando la pallina giusta: scivola a sinistra e poi torna a destra, con un **suono di conferma**.
- Un nuovo bersaglio appare **solo se c'è almeno una pallina a riposo**.
- Devi concatenare i lanci **prima che scada il timeout**: più concateni, più punti (combo).
- **Difficoltà crescente**: il tempo parte molto disteso (~4,5s) e **si riduce** con il tempo trascorso e il punteggio.
- **Tap sbagliato** → suono di errore e reset combo (nessun game over).
- **Combo con decadimento**: il combo cresce con i tap rapidi ma **si riduce pian piano** in proporzione al tempo che aspetti prima di cliccare la pallina bersaglio (rate configurabile `COMBO_DECAY`).
- **Timeout scaduto** → suono dedicato, reset combo, nuovo bersaglio (nessun game over).
- I suoni (lancio, errore, timeout) sono generati proceduralmente in 8-bit.
- **5 corsie orizzontali in legno** (plancia a tutta larghezza), ognuna con una **pallina colorata** glossy (corallo, blu, verde, giallo, viola) a riposo sull'alloggiamento destro.
- Un **bersaglio in alto** (HUD a cartiglio) indica quale pallina toccare; la pallina rotola a sinistra e torna (con **rotazione realistica** attorno all'asse perpendicolare al moto).
- Devi concatenare i lanci prima che scada il timeout. **Difficoltà crescente legata SOLO al punteggio** (`score_ramp = 8000` → velocità massima).
- Punteggio per lancio = **combo × valore punto** (`points_per_combo`, default 1; combo max 5).
- **Suoni generati proceduralmente** in 8-bit (sine morbide); BGM circense AI (Lyria) in OGG.
### Modalità di gioco (dialog all'avvio)
| Modalità | Descrizione |
|----------|-------------|
| **Target (Classica)** | Raggiungi il punteggio `target_score` (default 1000) → VITTORIA |
| **Target (Zen)** | Come Target ma **senza countdown né decay** combo |
| **Infinita (Survival)** | **A vite**: 4 cuori; errore o timeout = −1 cuore; 0 cuori → **GAME OVER** (ignora il target) |
Le modalità sono **mutuamente esclusive**; lo Zen è disponibile **solo** per la modalità Target.
### Protezione adulti (lock)
Il menù **Opzioni** è protetto: va sbloccato premendo la sequenza **giallo → verde → blu → rosso** tra le 5 biglie (pallini che mostrano i colori premuti; reset dopo feedback rosso 2s se la sequenza di 4 è errata).
---
## Requisiti
- Godot 4.x (sviluppato e testato con 4.7.1, renderer GL Compatibility).
- **Godot 4.7.x** (testato con 4.7.1), renderer **GL Compatibility**.
- Per l'APK Android: JDK 17, Android SDK (vedi sotto), template Android.
## Eseguire
```bash
# dal percorso del progetto
godot --path .
```
Oppure apri il progetto con l'editor Godot (`godot -e .`) e premi **Play** (F5).
## Controlli
- **Desktop**: click del mouse sulla pallina.
- **Mobile**: tocco sullo schermo.
```bash
godot --path . # gioco
godot -e . # editor
godot --headless --path . --quit-after 120 # smoke test (errori)
```
## Struttura
```
project.godot # configurazione (viewport 1280x720, landscape, GL Compatibility)
icon.svg
scenes/
main.tscn # scena principale (radice Node2D + script)
scripts/
main.gd # logica di gioco, HUD, punteggio, difficoltà
ball.gd # pallina: Area2D, scivolata sinistra↔destra, input
target_dot.gd # indicatore del colore bersaglio
project.godot # viewport 1280×720 orizzontale, GL Compatibility, ETC2/ASTC
export_presets.cfg # preset Web e Android
app/
scenes/app_shell.tscn # scena iniziale configurata in project.godot
scripts/app_shell.gd # coordina menu, selettore modalità e gioco attivo
menus/
scenes/ # shell menu e schermate P1, P2a, P3a
scripts/main_menu_controller.gd # navigazione e transizioni tra le schermate
scripts/screens/ # costruzione delle schermate Home, Juggle e Palestra
scripts/components/ # UI e audio condivisi dei menu
assets/ # fondali, tendoni, pulsanti e shader dei menu
games/
juggleboard/
scenes/ # scena di gioco, opzioni, lock adulto e fine partita
scripts/game_controller.gd # regole, punteggio, combo, timer e modalità
scripts/components/ # plancia, biglie e bersaglio
scripts/ui/ # HUD, cuori, opzioni e overlay
scripts/audio/ # BGM e feedback sonori
assets/ # texture e BGM di JuggleBoard
juggling_gym/ # asset provvisori della futura Palestra di giocoleria
shared/
settings/settings.gd # AUTOLOAD Settings: parametri e user://settings.cfg
audio/ # fabbrica dei suoni procedurali
ui/ # tema, cornici e decorazioni circensi riusabili
assets/ # font e texture condivise
branding/ # logo UPENDI e boot splash
docs/ # mockup, materiali di design e ARCHITECTURE.md
build/ # directory ignorata per gli export Web e Android
```
## Personalizzare la difficoltà
In `scripts/main.gd` (sezione "Tuning difficoltà"):
- `START_TIME` → tempo iniziale tra un ordine e l'altro (s). Più alto = partenza più distesa.
- `MIN_TIME` → tempo minimo (velocità massima) raggiunto a difficoltà piena.
- `TIME_RAMP` → secondi di gioco per raggiungere la difficoltà piena.
- `SCORE_RAMP` → punteggio per raggiungere la difficoltà piena.
Il flusso e le responsabilità dei componenti sono descritti in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
La difficoltà cresce col massimo tra il fattore-tempo e il fattore-punteggio.
## Impostazioni (`shared/settings/settings.gd` → `user://settings.cfg`)
## Esportazione mobile
Installa dal Project Manager il template **Android** (o iOS su macOS), poi
*Project → Export* con preset `Android` / `Web`.
Parametri modificabili dal menù Opzioni: tempi (start/min), `score_ramp`, `points_per_combo`, `target_score`, `max_combo`, decay combo (threshold/rate), `difficulty_decay_rate`, `gentle_errors`, `game_mode` (0 Classica, 1 Zen, 2 Survival).
> **CRITICO**: `shared/settings/settings.gd` ha `const VERSION`. Quando si cambiano i **default**, incrementare `VERSION`: i file `settings.cfg` salvati con versione più vecchia vengono **resettati ai nuovi default** (altrimenti i vecchi valori salvati sovrascrivono i nuovi default).
---
## Build Web
```bash
godot --headless --path . --export-release "Web" build/web/index.html
```
## Pubblicare il Web (enne2.net)
```bash
cd build/web
scp -r . debian@enne2.net:/home/debian/public_html/autgame/
```
→ URL: **https://enne2.net/pub/autgame/** (Nginx: `location /pub/` alias `/home/debian/public_html/`, autoindex on).
Nota: troppe connessioni SSH rapide attivano fail2ban → raggruppa i comandi.
## Build APK Android
Setup una tantum (macchina di sviluppo, senza sudo):
```bash
# 1) JDK portatile (Temurin 17)
# ~/.local/opt/jdk-17.0.20+8 (scarica da api.adoptium.net)
# 2) Android SDK in ~/Android/Sdk:
# cmdline-tools/latest/bin/sdkmanager (scaricare commandlinetools)
# sdkmanager "platform-tools" "platforms;android-35" "platforms;android-36" \
# "build-tools;35.0.0" "build-tools;36.0.0" (con JAVA_HOME esportato)
# 3) Impostazioni editor Godot (~/.config/godot/editor_settings-4.7.tres):
# export/android/android_sdk_path = "~/Android/Sdk"
# export/android/java_sdk_path = "~/.local/opt/jdk-17.0.20+8"
# 4) Debug keystore (alias androiddebugkey, pass android):
# ~/.local/share/godot/keystores/debug.keystore
# (usato anche come release keystore nel preset export_presets.cfg)
# 5) project.godot deve avere rendering/textures/vram_compression/import_etc2_astc=true
```
Build + firma:
```bash
export JAVA_HOME=~/.local/opt/jdk-17.0.20+8
export ANDROID_HOME=~/Android/Sdk
export PATH="$JAVA_HOME/bin:$ANDROID_HOME/build-tools/36.0.0:$PATH"
godot --headless --path . --export-release "Android" build/android/autgame.apk
# output: APK firmato v2/v3 (apksigner) in build/android/autgame.apk
```
Il preset usa l'**export classico** (`gradle_build/use_gradle_build=false`), che richiede i template Android in `~/.local/share/godot/export_templates/4.7.1.stable/` (da Godot_v4.7.1-stable_export_templates.tpz: `android_release.apk`, `android_debug.apk`, `android_source.zip`).
## Pubblicare la release su git.enne2.net (API)
```bash
TOKEN=$(python3 -c "import yaml;d=yaml.safe_load(open('$HOME/.config/tea/config.yml'));print([l['token'] for l in d['logins'] if l['name']=='enne2'][0])")
# nuova release (o riusa l'id esistente, es. 15 per v0.1):
curl -s -X POST https://git.enne2.net/api/v1/repos/enne2/autgame/releases \
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-d '{"tag_name":"v0.2","name":"0.2","body":"..."}' # -> id RELEASE_ID
# sostituire l'asset APK:
AID=$(curl -s https://git.enne2.net/api/v1/repos/enne2/autgame/releases/$RELEASE_ID \
-H "Authorization: token $TOKEN" | python3 -c "import sys,json;print(json.load(sys.stdin)['assets'][0]['id'])")
curl -s -X DELETE .../releases/$RELEASE_ID/assets/$AID -H "Authorization: token $TOKEN"
curl -s -X POST .../releases/$RELEASE_ID/assets?name=autgame.apk -H "Authorization: token $TOKEN" \
-F "attachment=@build/android/autgame.apk"
```
Note: il CLI `tea repos create` ha un bug ("GetOrgByName") con owner utente → creare repo via `POST /api/v1/user/repos`. Avatar repo: `POST /api/v1/repos/{owner}/{repo}/avatar` con body JSON `{"image": "<base64 puro>"}`.
## Cattura screenshot interna Godot
Per il debugging grafico usare il Viewport Capture interno, non KDE/Spectacle come metodo primario. Il controller menu include flag di verifica:
```bash
# P1 (default), P2a, P3a o dialog modalità:
godot --path . -- --capture-menus
godot --path . -- --capture-menus --capture-p2a
godot --path . -- --capture-menus --capture-p3a
godot --path . -- --capture-menus --capture-mode-dialog
```
Gli screenshot vengono salvati in `/tmp/autgame_menu_*.png` dopo `await RenderingServer.frame_post_draw`, quindi includono il Viewport completo senza bordi della finestra.
## Workflow sviluppo (pi/Codex)
- La **logica** va implementata e verificata direttamente in Godot (test headless + screenshot).
- Per il **polish visivo** (tema legno/ottone/circo), mandare gli **screenshot** dello stato attuale a Codex CLI con prompt chiaro:
```bash
codex exec -C /home/enne2/Dev/autgame --dangerously-bypass-approvals-and-sandbox \
-i shot1.png -i shot2.png < prompt.txt
```
- **Chiudere sempre il processo godot dopo gli screenshot** (altrimenti interferisce).
## Asset generati con AI
- Texture legno e sfondo circo: modello immagini Antigravity.
- BGM circense: **API Gemini Lyria** (`lyria-3-pro-preview`), convertita in **OGG Vorbis** (Godot non importa .opus) con loop attivo nell'import.
- Logo UPENDI (boot splash, avatar repo, README): Gemini image / immagine da upendi.it.
---
© Cooperativa Sociale UPENDI — Gravina in Puglia
+6
View File
@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://app/scripts/app_shell.gd" id="1"]
[node name="AppShell" type="Node"]
script = ExtResource("1")
+66
View File
@@ -0,0 +1,66 @@
class_name AppShell
extends Node
## Entry point dell'app: coordina menu e caricamento dei giochi.
const MAIN_MENU_SCENE := preload("res://menus/scenes/main_menu.tscn")
const JUGGLEBOARD_SCENE := preload("res://games/juggleboard/scenes/juggleboard.tscn")
var main_menu: MainMenuController
var active_game: Node
func _ready() -> void:
_create_menus()
_show_debug_target_or_home()
func _create_menus() -> void:
main_menu = MAIN_MENU_SCENE.instantiate()
# P2A avvia direttamente Target: il vecchio dialog Target/Infinita non viene
# più creato né mostrato.
main_menu.mode_requested.connect(_start_juggleboard)
main_menu.exit_requested.connect(_exit_app)
add_child(main_menu)
func _show_debug_target_or_home() -> void:
var args := OS.get_cmdline_user_args()
if args.has("--start-juggleboard"):
_start_juggleboard()
if args.has("--capture-menus"):
_capture_game_after_frame()
elif args.has("--capture-mode-dialog"):
# Compatibilità del flag di debug: il dialog non fa più parte del flusso.
main_menu.show_juggle()
elif args.has("--capture-p2a"):
main_menu.show_juggle()
elif args.has("--capture-p3a"):
main_menu.show_gym()
else:
main_menu.show_home()
func _return_to_juggle_menu() -> void:
if is_instance_valid(active_game):
active_game.queue_free()
active_game = null
main_menu.show_juggle()
func _start_juggleboard() -> void:
main_menu.hide_menu()
if is_instance_valid(active_game):
active_game.queue_free()
active_game = JUGGLEBOARD_SCENE.instantiate()
active_game.return_requested.connect(_return_to_juggle_menu)
add_child(active_game)
func _capture_game_after_frame() -> void:
await RenderingServer.frame_post_draw
get_viewport().get_texture().get_image().save_png("/tmp/autgame_juggleboard.png")
func _exit_app() -> void:
if not OS.has_feature("web"):
get_tree().quit()
+1
View File
@@ -0,0 +1 @@
uid://gt2e303kou65
-19
View File
@@ -1,19 +0,0 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://b2gfccy54hwyd"
path="res://.godot/imported/circus.ogg-5b91588fe544e7aa78021ac22d7fcfc9.oggvorbisstr"
[deps]
source_file="res://audio/circus.ogg"
dest_files=["res://.godot/imported/circus.ogg-5b91588fe544e7aa78021ac22d7fcfc9.oggvorbisstr"]
[params]
loop=true
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
+1
View File
@@ -0,0 +1 @@
# Output di esportazione: non reimportare nel progetto Godot.
+1
View File
@@ -0,0 +1 @@
# Materiale di progettazione: non importare come risorse Godot.
+86
View File
@@ -0,0 +1,86 @@
# Architettura tecnica
Questo documento descrive la struttura del repository dopo il refactoring che ha separato shell dell'applicazione, menu, domini di gioco e componenti condivisi.
## Avvio e composizione
`project.godot` avvia `res://app/scenes/app_shell.tscn` e registra `Settings` come autoload da `res://shared/settings/settings.gd`.
`AppShell` (`app/scripts/app_shell.gd`) è il punto di composizione dell'applicazione:
1. istanzia il menu principale e il dialog di selezione modalità;
2. mostra la schermata Home, oppure una destinazione di debug richiesta dalla riga di comando;
3. quando l'utente conferma una modalità, nasconde il menu e istanzia JuggleBoard;
4. mantiene un solo gioco attivo alla volta.
## Menu
`menus/scenes/main_menu.tscn` usa `MainMenuController`, che sostituisce dinamicamente la schermata corrente e gestisce transizioni e feedback audio.
| Schermata | Scena | Script | Stato |
| --- | --- | --- | --- |
| Home | `menus/scenes/p1_home.tscn` | `menus/scripts/screens/p1_home_screen.gd` | Disponibile |
| Menu JuggleBoard | `menus/scenes/p2a_juggle_menu.tscn` | `menus/scripts/screens/p2a_juggle_menu_screen.gd` | Disponibile |
| Palestra di giocoleria | `menus/scenes/p3a_gym_menu.tscn` | `menus/scripts/screens/p3a_gym_menu_screen.gd` | Anteprima: il pulsante mostra “Presto disponibile” |
| Selettore modalità | `menus/scenes/mode_select_dialog.tscn` | `menus/scripts/mode_select_dialog.gd` e `mode_select_view.gd` | Disponibile |
I componenti `menus/scripts/components/menu_ui.gd` e `menu_audio.gd` raccolgono rispettivamente costruzione visuale e feedback sonori. Le immagini e lo shader del menu risiedono in `menus/assets/`.
## Dominio JuggleBoard
La scena `games/juggleboard/scenes/juggleboard.tscn` usa `JuggleBoardGame` in `games/juggleboard/scripts/game_controller.gd`.
Il controller crea plancia, biglie, HUD, audio e overlay, poi applica le regole di gioco:
- seleziona una biglia bersaglio tra quelle ferme;
- assegna punti in base alla combo;
- riduce il tempo di risposta in funzione del punteggio;
- applica il decadimento della combo e la penalità degli errori;
- gestisce Target, Zen e Survival;
- mostra vittoria al raggiungimento del punteggio obiettivo oppure game over dopo l'esaurimento delle vite in Survival.
### Componenti del dominio
| Area | Percorso | Responsabilità |
| --- | --- | --- |
| Configurazione di layout | `scripts/juggleboard_config.gd` | Costanti di viewport, corsie, colori e frequenze musicali. |
| Plancia e biglie | `scripts/components/board.gd`, `ball.gd` | Disegno della plancia e delle biglie, rilevamento di prossimità e animazione Tween del rotolamento. |
| Bersaglio | `scripts/components/target_dot.gd` | Rappresentazione della biglia richiesta nell'HUD. |
| HUD | `scripts/ui/game_hud.gd`, `hearts_display.gd`, `hud_cartouche.gd` | Punteggio, combo, timer, vite e cartiglio. |
| Opzioni | `scripts/ui/options_menu.gd`, `options_schema.gd`, `option_slider_row.gd` | Configurazione dei parametri di gioco. |
| Protezione adulto | `scripts/ui/lock_menu.gd` e relativi componenti | Sblocco delle opzioni tramite sequenza colori. |
| Fine partita | `scripts/ui/victory_overlay.gd` | Overlay per vittoria e game over, con reset. |
| Audio | `scripts/audio/game_audio.gd`, `circus_music.gd` | BGM e suoni di gioco; il BGM OGG è in `assets/audio/`. |
Le scene accessorie sono `options_menu.tscn`, `lock_menu.tscn` e `victory_overlay.tscn` nella stessa directory `games/juggleboard/scenes/`.
## Risorse condivise
- `shared/settings/settings.gd`: persiste i parametri in `user://settings.cfg`. Incrementare `VERSION` quando cambiano i valori predefiniti.
- `shared/audio/audio_factory.gd`: genera e riproduce feedback sonori morbidi.
- `shared/ui/`: definisce tema, finiture e cornici circensi riusabili.
- `shared/assets/`: contiene font e texture comuni.
- `shared/branding/`: contiene logo UPENDI e risorse del boot splash.
## Dominio futuro: Palestra di giocoleria
`games/juggling_gym/` conserva solo asset provvisori. Non esiste ancora una scena o un controller per questo dominio e `AppShell` non lo istanzia.
## Verifica visuale
`AppShell` e `MainMenuController` supportano la cattura del viewport per verificare i menu senza screenshot desktop:
```bash
godot --path . -- --capture-menus
godot --path . -- --capture-menus --capture-p2a
godot --path . -- --capture-menus --capture-p3a
godot --path . -- --capture-menus --capture-mode-dialog
```
Le immagini risultanti sono salvate in `/tmp/autgame_menu_*.png`. Per una cattura diretta del gioco usare:
```bash
godot --path . -- --start-juggleboard --capture-menus
```
che salva `/tmp/autgame_juggleboard.png`.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 625 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 781 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 844 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 703 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 875 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 740 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

@@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://d0614ke5vtpfr"
path="res://.godot/imported/circus.ogg-e561da631f264aa640a1148eea69f0db.oggvorbisstr"
[deps]
source_file="res://games/juggleboard/assets/audio/circus.ogg"
dest_files=["res://.godot/imported/circus.ogg-e561da631f264aa640a1148eea69f0db.oggvorbisstr"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dpqfmbyrhjwu5"
path="res://.godot/imported/2b_pallina_150__blu.png-fbee7d8a6af9e82bc1aa68345fa35b50.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://games/juggleboard/assets/p2b/2b_pallina_150__blu.png"
dest_files=["res://.godot/imported/2b_pallina_150__blu.png-fbee7d8a6af9e82bc1aa68345fa35b50.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: 254 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://chh47nt0hwikk"
path="res://.godot/imported/2b_pallina_150__gialla.png-f0f6338d028fd5c066c4e5cf989d400f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://games/juggleboard/assets/p2b/2b_pallina_150__gialla.png"
dest_files=["res://.godot/imported/2b_pallina_150__gialla.png-f0f6338d028fd5c066c4e5cf989d400f.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: 262 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dp7betlnoa0o5"
path="res://.godot/imported/2b_pallina_150__rosa.png-f0099789f3d8dde8d375070e08e9c5d3.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://games/juggleboard/assets/p2b/2b_pallina_150__rosa.png"
dest_files=["res://.godot/imported/2b_pallina_150__rosa.png-f0099789f3d8dde8d375070e08e9c5d3.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: 254 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://oamjgbblksst"
path="res://.godot/imported/2b_pallina_150__rossa.png-78b1e52842e087a16fc59f0ce831a2d1.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://games/juggleboard/assets/p2b/2b_pallina_150__rossa.png"
dest_files=["res://.godot/imported/2b_pallina_150__rossa.png-78b1e52842e087a16fc59f0ce831a2d1.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: 259 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://tj3ifbperj5j"
path="res://.godot/imported/2b_pallina_150__verde.png-95b5b437807ff5e94e5f452ba653e202.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://games/juggleboard/assets/p2b/2b_pallina_150__verde.png"
dest_files=["res://.godot/imported/2b_pallina_150__verde.png-95b5b437807ff5e94e5f452ba653e202.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: 13 MiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cykrbve556tom"
path="res://.godot/imported/board_background.png-f880d79e6bc48224225b63a494d21f64.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://games/juggleboard/assets/p2b/board_background.png"
dest_files=["res://.godot/imported/board_background.png-f880d79e6bc48224225b63a494d21f64.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
@@ -0,0 +1 @@
# Asset sorgente/provvisori non usati dal runtime.
Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://brmume11lce0i"
path="res://.godot/imported/circus_arena_bg.png-f5908cbdda38fe986db81a71545cd9c4.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://games/juggleboard/assets/textures/circus_arena_bg.png"
dest_files=["res://.godot/imported/circus_arena_bg.png-f5908cbdda38fe986db81a71545cd9c4.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: 1.2 MiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dp017bktk6s7u"
path="res://.godot/imported/wood_board.png-7c9d3269573396a28d085ced67feefd7.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://games/juggleboard/assets/textures/wood_board.png"
dest_files=["res://.godot/imported/wood_board.png-7c9d3269573396a28d085ced67feefd7.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
@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://games/juggleboard/scripts/game_controller.gd" id="1"]
[node name="JuggleBoard" type="Node2D"]
script = ExtResource("1")
+6
View File
@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://games/juggleboard/scripts/ui/lock_menu.gd" id="1"]
[node name="LockMenu" type="CanvasLayer"]
script = ExtResource("1")
@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://games/juggleboard/scripts/ui/options_menu.gd" id="1"]
[node name="OptionsMenu" type="CanvasLayer"]
script = ExtResource("1")
@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://games/juggleboard/scripts/ui/victory_overlay.gd" id="1"]
[node name="VictoryOverlay" type="CanvasLayer"]
script = ExtResource("1")
@@ -0,0 +1,58 @@
class_name JuggleBoardAudio
extends Node
## Audio e BGM del JuggleBoard. Nessuna logica di gameplay.
const BGM_STREAM := preload("res://games/juggleboard/assets/audio/circus.ogg")
const MUSIC_FALLBACK := preload("res://games/juggleboard/scripts/audio/circus_music.gd")
var launch: AudioStreamWAV
var error: AudioStreamWAV
var timeout: AudioStreamWAV
var milestone: AudioStreamWAV
var victory: AudioStreamWAV
var defeat: AudioStreamWAV
var lose_heart: AudioStreamWAV
var ball_tones: Dictionary = {}
var bgm: AudioStreamPlayer
func _ready() -> void:
_build_effects()
_build_bgm()
func _build_effects() -> void:
launch = AudioFactory.create_tone(660.0, 0.18, 0.3)
error = AudioFactory.create_tone(220.0, 0.15, 0.16)
timeout = AudioFactory.create_tone(330.0, 0.2, 0.16)
milestone = AudioFactory.create_arpeggio([523.25, 659.25, 783.99, 1046.5], 0.12)
victory = AudioFactory.create_arpeggio([523.25, 659.25, 783.99, 1046.5, 1318.51, 1567.98], 0.16)
defeat = AudioFactory.create_arpeggio([392.0, 329.63, 261.63], 0.18)
lose_heart = AudioFactory.create_tone(330.0, 0.15, 0.2)
for color_name in JuggleBoardConfig.BALL_NOTES:
ball_tones[color_name] = AudioFactory.create_tone(JuggleBoardConfig.BALL_NOTES[color_name], 0.2, 0.3)
func _build_bgm() -> void:
bgm = AudioStreamPlayer.new()
bgm.stream = BGM_STREAM if BGM_STREAM else MUSIC_FALLBACK.generate()
bgm.volume_db = -14.0
bgm.process_mode = Node.PROCESS_MODE_ALWAYS
add_child(bgm)
func play(stream: AudioStream) -> void:
if stream:
AudioFactory.play_once(self, stream)
func play_ball(color_name: String) -> void:
play(ball_tones.get(color_name, launch))
func start_music() -> void:
bgm.play()
func stop_music() -> void:
bgm.stop()
@@ -0,0 +1 @@
uid://dsywfqv2xa0vx
@@ -0,0 +1,156 @@
extends Area2D
## Biglia dinamica P2B: usa gli asset illustrati del mockup e conserva
## movimento, ritorno e hitbox della logica originale.
signal returned
const ART := {
"rosso": preload("res://games/juggleboard/assets/p2b/2b_pallina_150__rossa.png"),
"blu": preload("res://games/juggleboard/assets/p2b/2b_pallina_150__blu.png"),
"verde": preload("res://games/juggleboard/assets/p2b/2b_pallina_150__verde.png"),
"giallo": preload("res://games/juggleboard/assets/p2b/2b_pallina_150__gialla.png"),
"viola": preload("res://games/juggleboard/assets/p2b/2b_pallina_150__rosa.png"),
}
var color_name := ""
var ball_color := Color.WHITE
var radius := 48.0
var home_pos := Vector2.ZERO
var left_pos := Vector2.ZERO
var in_motion := false
var roll_angle := 0.0:
set(value):
roll_angle = value
queue_redraw()
var texture: Texture2D
var surface_marks: Array[Dictionary] = []
var _tween: Tween
func _draw() -> void:
if texture == null:
return
# La sagoma non ruota nel piano (asse Z). Venature, puntinature e piccoli
# riflessi scorrono invece sulla superficie come su una sfera che ruota
# attorno all'asse verticale, con schiacciamento ai lati.
draw_texture_rect(texture, Rect2(-radius, -radius, radius * 2.0, radius * 2.0), false)
_draw_surface_texture()
func _draw_surface_texture() -> void:
var dark := ball_color.darkened(0.55)
var light := ball_color.lightened(0.18)
for mark in surface_marks:
var points := PackedVector2Array()
var visible_sum := 0.0
var angle := float(mark["angle"])
var span := float(mark["span"])
var latitude := float(mark["latitude"])
var wobble := float(mark["wobble"])
var waves := float(mark["waves"])
var phase := float(mark["phase"])
for step in range(13):
var t := float(step) / 12.0
var theta := angle + lerpf(-span, span, t) + roll_angle
var front := cos(theta)
if front <= 0.0:
continue
var wave: float = sin(t * TAU * waves + phase) * wobble
var y: float = (latitude + wave) * radius * (0.32 + 0.68 * front)
points.append(Vector2(sin(theta) * radius, y))
visible_sum += front
if points.size() < 2:
continue
var visibility := visible_sum / points.size()
_draw_mark_segments(points, dark, 0.10 + 0.20 * visibility,
1.2 + float(mark["weight"]) * visibility)
if bool(mark["highlight"]):
_draw_mark_segments(points, light, 0.08 + 0.13 * visibility, 0.7)
func _draw_mark_segments(points: PackedVector2Array, color: Color, base_alpha: float, width: float) -> void:
# Il pallino bianco è il riflesso speculare dell'asset: qualunque venatura
# che lo attraversa viene nascosta sotto di esso e sfuma ai suoi bordi.
for index in range(1, points.size()):
var a := points[index - 1]
var b := points[index]
var specular_visibility := (_outside_specular(a) + _outside_specular(b)) * 0.5
if specular_visibility <= 0.01:
continue
var segment_color := color
segment_color.a = base_alpha * specular_visibility
draw_line(a, b, segment_color, width, true)
func _outside_specular(point: Vector2) -> float:
# Posizione e raggio del punto luce dell'illustrazione P2B, in coordinate
# locali della sfera. Nel nucleo la venatura sparisce; la transizione è soft.
var highlight_center := Vector2(-0.30, -0.34) * radius
var distance_from_highlight := point.distance_to(highlight_center) / radius
return smoothstep(0.12, 0.30, distance_from_highlight)
func setup(cn: String, color: Color, home: Vector2, left: Vector2, r: float) -> void:
color_name = cn
ball_color = color
radius = r
texture = ART[cn]
surface_marks = _make_surface_marks(cn)
home_pos = home
left_pos = left
position = home
var shape := CollisionShape2D.new()
var circle := CircleShape2D.new()
circle.radius = r * 1.35
shape.shape = circle
add_child(shape)
queue_redraw()
func _make_surface_marks(seed_name: String) -> Array[Dictionary]:
var rng := RandomNumberGenerator.new()
rng.seed = hash(seed_name)
var marks: Array[Dictionary] = []
for index in range(10):
marks.append({
"angle": rng.randf_range(-PI, PI),
"span": rng.randf_range(0.22, 0.62),
"latitude": rng.randf_range(-0.62, 0.62),
"wobble": rng.randf_range(0.025, 0.11),
"waves": rng.randf_range(0.7, 2.1),
"phase": rng.randf_range(0.0, TAU),
"weight": rng.randf_range(0.4, 1.5),
"highlight": index % 3 == 0,
})
return marks
func is_home() -> bool:
return not in_motion
func reset() -> void:
if _tween and _tween.is_valid():
_tween.kill()
in_motion = false
position = home_pos
roll_angle = 0.0
func launch(total_time: float) -> void:
in_motion = true
if _tween and _tween.is_valid():
_tween.kill()
var half := maxf(total_time * 0.5, 0.01)
var total_angle := (home_pos.x - left_pos.x) / radius
_tween = create_tween()
_tween.tween_property(self, "position", left_pos, half).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN_OUT)
_tween.parallel().tween_property(self, "roll_angle", total_angle, half).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN_OUT)
_tween.tween_property(self, "position", home_pos, half).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN_OUT)
_tween.parallel().tween_property(self, "roll_angle", 0.0, half).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN_OUT)
_tween.finished.connect(_on_motion_done)
func _on_motion_done() -> void:
in_motion = false
returned.emit()
@@ -0,0 +1,16 @@
extends Node2D
## Fondale P2B: plancia illustrata, pannello laterale e cinque corsie.
const VIEW := Vector2(1280, 720)
const P2B_BACKGROUND := preload("res://games/juggleboard/assets/p2b/board_background.png")
func _ready() -> void:
z_index = -10
queue_redraw()
func _draw() -> void:
# L'illustrazione 2B contiene pannello, plancia vuota e alloggiamenti;
# gli elementi dinamici sono aggiunti sopra dal controller e dall'HUD.
draw_texture_rect(P2B_BACKGROUND, Rect2(Vector2.ZERO, VIEW), false)
@@ -0,0 +1 @@
uid://l7l3t71lt50f
@@ -0,0 +1,29 @@
extends Node2D
## Bersaglio P2B: stessa famiglia di asset delle palline interattive.
const R := 48.0
const ART := {
Color("de6b7e"): preload("res://games/juggleboard/assets/p2b/2b_pallina_150__rossa.png"),
Color("1852de"): preload("res://games/juggleboard/assets/p2b/2b_pallina_150__blu.png"),
Color("88ce96"): preload("res://games/juggleboard/assets/p2b/2b_pallina_150__verde.png"),
Color("e8b84b"): preload("res://games/juggleboard/assets/p2b/2b_pallina_150__gialla.png"),
Color("9b7ede"): preload("res://games/juggleboard/assets/p2b/2b_pallina_150__rosa.png"),
}
var texture: Texture2D
func _ready() -> void:
texture = ART[Color("de6b7e")]
queue_redraw()
func _draw() -> void:
if texture:
draw_texture_rect(texture, Rect2(-R, -R, R * 2.0, R * 2.0), false)
func set_color(color: Color) -> void:
# I colori vengono dalle costanti JuggleBoardConfig, quindi la lookup è esatta.
texture = ART[color]
queue_redraw()
@@ -0,0 +1,270 @@
class_name JuggleBoardGame
extends Node2D
## Controller del gameplay JuggleBoard. Menu, HUD e audio sono moduli separati.
signal return_requested
const BOARD_SCRIPT := preload("res://games/juggleboard/scripts/components/board.gd")
const BALL_SCRIPT := preload("res://games/juggleboard/scripts/components/ball.gd")
const HUD_SCRIPT := preload("res://games/juggleboard/scripts/ui/game_hud.gd")
const AUDIO_SCRIPT := preload("res://games/juggleboard/scripts/audio/game_audio.gd")
const OPTIONS_SCENE := preload("res://games/juggleboard/scenes/options_menu.tscn")
const LOCK_SCENE := preload("res://games/juggleboard/scenes/lock_menu.tscn")
const END_SCENE := preload("res://games/juggleboard/scenes/victory_overlay.tscn")
var balls: Dictionary = {}
var target_color := ""
var score := 0
var combo := 0.0
var lives := 4
var elapsed := 0.0
var window_timer := 0.0
var window_time := Settings.start_time
var next_milestone := 100
var end_shown := false
var hud: JuggleBoardHud
var audio: JuggleBoardAudio
var options_menu: CanvasLayer
var lock_menu: CanvasLayer
var end_overlay: CanvasLayer
func _ready() -> void:
add_child(BOARD_SCRIPT.new())
_build_balls()
_build_services()
_new_game()
audio.start_music()
func _process(delta: float) -> void:
elapsed += delta
if target_color == "" or Settings.game_mode == 1:
return
if combo > 0.0:
combo = maxf(0.0, combo - _combo_decay() * delta)
window_timer += delta
if window_timer >= window_time:
_on_timeout()
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
_toggle_options()
return
var position := Vector2.INF
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
position = event.position
elif event is InputEventScreenTouch and event.pressed:
position = event.position
if position != Vector2.INF:
_handle_click(position)
func _build_balls() -> void:
var names := JuggleBoardConfig.COLORS.keys()
for index in range(names.size()):
var color_name: String = names[index]
var home := Vector2(JuggleBoardConfig.HOME_X, JuggleBoardConfig.LANE_TOP + index * JuggleBoardConfig.LANE_GAP)
var left := Vector2(JuggleBoardConfig.LEFT_X, JuggleBoardConfig.LANE_TOP + index * JuggleBoardConfig.LANE_GAP)
var ball: Area2D = BALL_SCRIPT.new()
add_child(ball)
ball.setup(color_name, JuggleBoardConfig.COLORS[color_name], home, left, JuggleBoardConfig.BALL_RADIUS)
ball.returned.connect(_on_ball_returned)
balls[color_name] = ball
func _build_services() -> void:
hud = HUD_SCRIPT.new()
hud.options_requested.connect(_request_options)
hud.back_requested.connect(func(): return_requested.emit())
add_child(hud)
audio = AUDIO_SCRIPT.new()
add_child(audio)
options_menu = OPTIONS_SCENE.instantiate()
add_child(options_menu)
lock_menu = LOCK_SCENE.instantiate()
lock_menu.unlocked.connect(_open_options)
add_child(lock_menu)
end_overlay = END_SCENE.instantiate()
end_overlay.reset_requested.connect(_reset_after_end)
add_child(end_overlay)
func _request_options() -> void:
lock_menu.show_menu()
func _open_options() -> void:
options_menu.show_menu()
func _toggle_options() -> void:
if options_menu.visible:
options_menu.hide_menu()
else:
_request_options()
func _new_game() -> void:
score = 0
combo = 0.0
elapsed = 0.0
window_timer = 0.0
window_time = Settings.start_time
next_milestone = 100
lives = 4
for ball in balls.values():
ball.reset()
_pick_target()
hud.reset(score, combo, lives, Settings.game_mode == 2)
func _handle_click(position: Vector2) -> void:
var clicked: Area2D
var best := JuggleBoardConfig.HIT_RADIUS
for ball in balls.values():
if not ball.is_home():
continue
var distance := position.distance_to(ball.position)
if distance < best:
best = distance
clicked = ball
if clicked == null:
return
if clicked.color_name == target_color:
_on_correct_ball(clicked)
else:
_on_wrong_ball()
func _on_correct_ball(ball: Area2D) -> void:
combo = minf(float(Settings.max_combo), combo + 1.0)
score += _points_for_combo()
audio.play_ball(ball.color_name)
var current_time := _current_time()
ball.launch(current_time)
window_time = current_time
window_timer = 0.0
_pick_target()
hud.set_score(score, combo)
_check_milestone()
_check_victory()
func _on_wrong_ball() -> void:
audio.play(audio.error)
_apply_combo_penalty()
hud.set_score(score, combo)
if Settings.game_mode == 2:
_lose_life()
func _on_timeout() -> void:
audio.play(audio.timeout)
_apply_combo_penalty()
window_timer = 0.0
_pick_target()
hud.set_score(score, combo)
if Settings.game_mode == 2:
_lose_life()
func _apply_combo_penalty() -> void:
if Settings.gentle_errors:
combo = maxf(0.0, combo - 1.0)
else:
combo = 0.0
func _on_ball_returned() -> void:
if target_color == "":
_pick_target()
func _current_time() -> float:
return lerpf(Settings.start_time, Settings.min_time, _difficulty())
func _difficulty() -> float:
var factor := clampf(score / Settings.score_ramp, 0.0, 1.0)
return factor * factor * (3.0 - 2.0 * factor)
func _combo_decay() -> float:
var combo_penalty := maxf(0.0, combo - Settings.combo_decay_threshold) * Settings.combo_decay_rate
return combo_penalty + _difficulty() * Settings.difficulty_decay_rate
func _points_for_combo() -> int:
return int(combo) * Settings.points_per_combo
func _pick_target() -> void:
var homes := _home_names()
if homes.is_empty():
target_color = ""
hud.set_target(Color.WHITE, false)
return
var pool := homes.duplicate()
if target_color in pool and pool.size() > 1:
pool.erase(target_color)
target_color = pool[randi() % pool.size()]
hud.set_target(JuggleBoardConfig.COLORS[target_color])
func _home_names() -> Array:
var result := []
for color_name in balls:
if balls[color_name].is_home():
result.append(color_name)
return result
func _check_victory() -> void:
if Settings.game_mode != 2 and not end_shown and score >= Settings.target_score:
_show_end(true)
func _lose_life() -> void:
if end_shown:
return
lives -= 1
audio.play(audio.lose_heart)
hud.hearts.set_lives(maxi(lives, 0))
if lives <= 0:
_show_end(false)
func _show_end(victory: bool) -> void:
if end_shown:
return
end_shown = true
audio.stop_music()
audio.play(audio.victory if victory else audio.defeat)
get_tree().paused = true
if victory:
end_overlay.show_victory(score)
else:
end_overlay.show_game_over(score)
func _reset_after_end() -> void:
get_tree().paused = false
end_shown = false
end_overlay.hide_victory()
audio.start_music()
_new_game()
func _check_milestone() -> void:
if score < next_milestone:
return
var celebrated := next_milestone
while next_milestone <= score:
celebrated = next_milestone
next_milestone *= 10
audio.play(audio.milestone)
hud.celebrate(celebrated)
@@ -0,0 +1 @@
uid://cemlq3d1gmot2
@@ -0,0 +1,32 @@
class_name JuggleBoardConfig
extends RefCounted
## Costanti di layout, colori e note del JuggleBoard.
const VIEW := Vector2(1280, 720)
# Coordinate della plancia P2B, ricavate dall'illustrazione board_background.
const LANE_TOP := 132.0
const LANE_GAP := 115.0
const BALL_RADIUS := 48.0
const HOME_X := 1100.0
const LEFT_X := 405.0
const HIT_RADIUS := BALL_RADIUS * 1.35
# Centro della cornice bersaglio misurato nello sfondo P2B (2796×1290) e
# convertito nel viewport 1280×720 usato da board.gd.
const TARGET_CENTER := Vector2(222.4, 360.5)
const COLORS := {
"rosso": Color("de6b7e"),
"blu": Color("1852de"),
"verde": Color("88ce96"),
"giallo": Color("e8b84b"),
"viola": Color("9b7ede"),
}
const BALL_NOTES := {
"rosso": 261.63,
"blu": 293.66,
"verde": 329.63,
"giallo": 392.00,
"viola": 440.00,
}

Some files were not shown because too many files have changed in this diff Show More