Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0991a0edf | ||
|
|
ea5b2aebbf | ||
|
|
f4a9da6bc3 | ||
|
|
a73fb4b55f | ||
|
|
de7dfdf9f7 | ||
|
|
725183290b | ||
|
|
7c5850eb1d | ||
|
|
44160adc1a | ||
|
|
94f05adf34 | ||
|
|
8a38bb09dc | ||
|
|
0e85992d40 | ||
|
|
fccc39c5d1 | ||
|
|
cdcaf76531 | ||
|
|
5db4660471 | ||
|
|
9811ffe5b4 | ||
|
|
4d7aa698e3 | ||
|
|
54619a2385 | ||
|
|
f9837153a0 | ||
|
|
e20482676f | ||
|
|
c7918d9f55 |
@@ -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
|
||||
|
||||
@@ -1,54 +1,186 @@
|
||||

|
||||
|
||||
# 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
|
||||
|
||||
@@ -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")
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://gt2e303kou65
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
# Output di esportazione: non reimportare nel progetto Godot.
|
||||
@@ -0,0 +1 @@
|
||||
# Materiale di progettazione: non importare come risorse Godot.
|
||||
@@ -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`.
|
||||
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 5.3 MiB |
|
After Width: | Height: | Size: 4.8 MiB |
|
After Width: | Height: | Size: 4.3 MiB |
|
After Width: | Height: | Size: 592 KiB |
|
After Width: | Height: | Size: 625 KiB |
|
After Width: | Height: | Size: 654 KiB |
|
After Width: | Height: | Size: 563 KiB |
|
After Width: | Height: | Size: 486 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 471 KiB |
|
After Width: | Height: | Size: 347 KiB |
|
After Width: | Height: | Size: 781 KiB |
|
After Width: | Height: | Size: 844 KiB |
|
After Width: | Height: | Size: 731 KiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 703 KiB |
|
After Width: | Height: | Size: 620 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 711 KiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 595 KiB |
|
After Width: | Height: | Size: 693 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 875 KiB |
|
After Width: | Height: | Size: 740 KiB |
|
After Width: | Height: | Size: 458 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 601 KiB |
|
After Width: | Height: | Size: 418 KiB |
|
After Width: | Height: | Size: 449 KiB |
@@ -75,8 +75,8 @@ architectures/arm64-v8a=true
|
||||
architectures/armeabi-v7a=true
|
||||
architectures/x86=false
|
||||
architectures/x86_64=false
|
||||
version/code=1
|
||||
version/name="0.1"
|
||||
version/code=2
|
||||
version/name="0.3"
|
||||
package/unique_name="net.enne2.autgame"
|
||||
package/name="JuggleBoard"
|
||||
package/signed=true
|
||||
|
||||
@@ -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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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.
|
||||
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 254 KiB |
|
After Width: | Height: | Size: 261 KiB |
|
After Width: | Height: | Size: 262 KiB |
|
After Width: | Height: | Size: 254 KiB |
|
After Width: | Height: | Size: 259 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 13 MiB |
|
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
|
||||
|
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")
|
||||
@@ -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,15 @@
|
||||
extends Node2D
|
||||
## Fondale P2B: plancia illustrata, pannello laterale e cinque corsie.
|
||||
|
||||
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 P2B occupa il viewport realmente disponibile: con
|
||||
# stretch=expand i display 20:9 non ricevono bande laterali.
|
||||
draw_texture_rect(P2B_BACKGROUND, get_viewport_rect(), 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,274 @@
|
||||
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()
|
||||
var width_scale := JuggleBoardConfig.horizontal_scale(get_viewport().get_visible_rect().size.x)
|
||||
for index in range(names.size()):
|
||||
var color_name: String = names[index]
|
||||
var lane_y := JuggleBoardConfig.LANE_TOP + index * JuggleBoardConfig.LANE_GAP
|
||||
# Il fondale si espande in orizzontale: alloggiamenti e biglie seguono
|
||||
# la stessa scala X, mentre dimensione e coordinate Y restano invariate.
|
||||
var home := Vector2(JuggleBoardConfig.HOME_X * width_scale, lane_y)
|
||||
var left := Vector2(JuggleBoardConfig.LEFT_X * width_scale, lane_y)
|
||||
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
|
||||