Compare commits

..

21 commits

Author SHA1 Message Date
3c28c1f0c2 Auto-sync: 20260314_190000 2026-03-14 19:00:00 +01:00
bbcd80a6a9 Auto-sync: 20260314_180000 2026-03-14 18:00:00 +01:00
e1861031a8 Auto-sync: 20260314_000001 2026-03-14 00:00:01 +01:00
321810a6b4 Auto-sync: 20260313_230001 2026-03-13 23:00:01 +01:00
518cb22ec4 Auto-sync: 20260313_220000 2026-03-13 22:00:00 +01:00
da52ad9d80 Auto-sync: 20260312_213211 2026-03-12 21:32:11 +01:00
7c7bf0ef2d Auto-sync: 20260312_210001 2026-03-12 21:00:08 +01:00
99ae270818 Auto-sync: 20260312_150000 2026-03-12 15:00:00 +01:00
a84c0b51b0 Auto-sync: 20260312_141056 2026-03-12 14:10:56 +01:00
384a9d7bd7 Auto-sync: 20260312_140000 2026-03-12 14:00:01 +01:00
50f67320a5 Auto-sync: 20260311_230000 2026-03-11 23:00:01 +01:00
5aa831f750 Auto-sync: 20260311_220000 2026-03-11 22:00:01 +01:00
37e59d5652 Auto-sync: 20260305_200000 2026-03-05 20:00:01 +01:00
18b0965aae Auto-sync: 20260305_160000 2026-03-05 16:00:01 +01:00
b2b94a5e72 Auto-sync: 20260305_150000 2026-03-05 15:00:00 +01:00
434ea96444 Auto-sync: 20260304_222820 2026-03-04 22:28:20 +01:00
a711b3dec2 Auto-sync: 20260304_220742 2026-03-04 22:07:43 +01:00
3acfb76fac Auto-sync: 20260304_220000 2026-03-04 22:00:00 +01:00
c83cc6b9ae Auto-sync: 20260304_210000 2026-03-04 21:00:00 +01:00
37d638816a Auto-sync: 20260304_190000 2026-03-04 19:00:00 +01:00
a3a30ebf1e Auto-sync: 20260304_180001 2026-03-04 18:00:01 +01:00
78 changed files with 4953 additions and 1828 deletions

BIN
.DS_Store vendored

Binary file not shown.

View file

@ -0,0 +1,2 @@
index.html,1773344753424,0d5d4b835a7d632ad11d249230a15561286f2bfcd1da8305c6fb294d37e5da09
404.html,1773344753356,05cbc6f94d7a69ce2e29646eab13be2c884e61ba93e3094df5028866876d18b3

5
.firebaserc Normal file
View file

@ -0,0 +1,5 @@
{
"projects": {
"default": "tetraq-32a4a"
}
}

View file

@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.
version:
revision: "ff37bef603469fb030f2b72995ab929ccfc227f0"
revision: "3b62efc2a3da49882f43c372e0bc53daef7295a6"
channel: "stable"
project_type: app
@ -13,11 +13,17 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
- platform: android
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
- platform: ios
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
- platform: macos
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
# User provided section

View file

@ -8,8 +8,19 @@ plugins {
id("dev.flutter.flutter-gradle-plugin")
}
// Aggiungiamo esplicitamente gli import richiesti da Kotlin
import java.io.FileInputStream
import java.util.Properties
// Carichiamo il file con le password
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
android {
namespace = "com.sanza.tetraq"
namespace = "com.amastra.tetraq"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
@ -18,13 +29,16 @@ android {
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
// Sintassi aggiornata come richiesto dal compilatore Kotlin
kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.sanza.tetraq"
applicationId = "com.amastra.tetraq"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
@ -33,15 +47,28 @@ android {
versionName = flutter.versionName
}
// Aggiunto il blocco per la firma in formato Kotlin DSL
signingConfigs {
create("release") {
keyAlias = keystoreProperties.getProperty("keyAlias")
keyPassword = keystoreProperties.getProperty("keyPassword")
val storeFileString = keystoreProperties.getProperty("storeFile")
if (storeFileString != null) {
storeFile = file(storeFileString)
}
storePassword = keystoreProperties.getProperty("storePassword")
}
}
buildTypes {
release {
getByName("release") {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
// Ora usiamo la chiave di release appena creata
signingConfig = signingConfigs.getByName("release")
}
}
}
flutter {
source = "../.."
}
}

View file

@ -5,6 +5,25 @@
"storage_bucket": "tetraq-32a4a.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:705460445314:android:ceac21bb06b7a9f07b949b",
"android_client_info": {
"package_name": "com.amastra.tetraq"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyBsXO595xVITDPrRnXrW8HPQLOe7Rz4Gg4"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:705460445314:android:4d35fef29cfd63727b949b",

View file

@ -1,4 +1,4 @@
package com.sanza.tetraq
package com.amastra.tetraq
import io.flutter.embedding.android.FlutterActivity

BIN
assets/.DS_Store vendored

Binary file not shown.

BIN
assets/audio/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/images/egizi_bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
assets/images/music_bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View file

@ -1 +1,47 @@
{"flutter":{"platforms":{"android":{"default":{"projectId":"tetraq-32a4a","appId":"1:705460445314:android:4d35fef29cfd63727b949b","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"tetraq-32a4a","appId":"1:705460445314:ios:da11cbca5d1f6bc27b949b","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"macos":{"default":{"projectId":"tetraq-32a4a","appId":"1:705460445314:ios:da11cbca5d1f6bc27b949b","uploadDebugSymbols":false,"fileOutput":"macos/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"tetraq-32a4a","configurations":{"android":"1:705460445314:android:4d35fef29cfd63727b949b","ios":"1:705460445314:ios:da11cbca5d1f6bc27b949b","macos":"1:705460445314:ios:da11cbca5d1f6bc27b949b"}}}}}}
{
"flutter": {
"platforms": {
"android": {
"default": {
"projectId": "tetraq-32a4a",
"appId": "1:705460445314:android:ceac21bb06b7a9f07b949b",
"fileOutput": "android/app/google-services.json"
}
},
"ios": {
"default": {
"projectId": "tetraq-32a4a",
"appId": "1:705460445314:ios:54d64cb7592954327b949b",
"uploadDebugSymbols": false,
"fileOutput": "ios/Runner/GoogleService-Info.plist"
}
},
"macos": {
"default": {
"projectId": "tetraq-32a4a",
"appId": "1:705460445314:ios:da11cbca5d1f6bc27b949b",
"uploadDebugSymbols": false,
"fileOutput": "macos/Runner/GoogleService-Info.plist"
}
},
"dart": {
"lib/firebase_options.dart": {
"projectId": "tetraq-32a4a",
"configurations": {
"android": "1:705460445314:android:ceac21bb06b7a9f07b949b",
"ios": "1:705460445314:ios:54d64cb7592954327b949b",
"macos": "1:705460445314:ios:da11cbca5d1f6bc27b949b"
}
}
}
}
},
"hosting": {
"public": "public",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
]
}
}

27
genera_lingue.dart Normal file
View file

@ -0,0 +1,27 @@
import 'dart:io';
import 'dart:convert';
void main() async {
final dir = Directory('lib/l10n');
if (!await dir.exists()) await dir.create(recursive: true);
final Map<String, Map<String, String>> translations = {
'it': {"appTitle": "TetraQ", "welcomeTitle": "BENVENUTO IN TETRAQ!", "nameHint": "NOME", "saveAndPlay": "SALVA E GIOCA", "onlineTitle": "ONLINE", "onlineSub": "Sfida il mondo", "cpuTitle": "VS CPU", "cpuSub": "Allenati con l'IA", "localTitle": "LOCALE", "localSub": "Stesso schermo", "leaderboardTitle": "CLASSIFICA", "questsTitle": "SFIDE", "themesTitle": "TEMI", "tutorialTitle": "TUTORIAL", "startGame": "AVVIA PARTITA", "createMatch": "CREA PARTITA", "joinMatch": "UNISCITI", "gameOver": "FINE PARTITA", "mainMenu": "TORNA AL MENU", "exit": "ESCI"},
'en': {"appTitle": "TetraQ", "welcomeTitle": "WELCOME TO TETRAQ!", "nameHint": "NAME", "saveAndPlay": "SAVE & PLAY", "onlineTitle": "ONLINE", "onlineSub": "Challenge the world", "cpuTitle": "VS CPU", "cpuSub": "Train with AI", "localTitle": "LOCAL", "localSub": "Same screen", "leaderboardTitle": "LEADERBOARD", "questsTitle": "QUESTS", "themesTitle": "THEMES", "tutorialTitle": "TUTORIAL", "startGame": "START GAME", "createMatch": "CREATE MATCH", "joinMatch": "JOIN", "gameOver": "GAME OVER", "mainMenu": "BACK TO MENU", "exit": "EXIT"},
'es': {"appTitle": "TetraQ", "welcomeTitle": "¡BIENVENIDO A TETRAQ!", "nameHint": "NOMBRE", "saveAndPlay": "GUARDAR Y JUGAR", "onlineTitle": "ONLINE", "onlineSub": "Desafía al mundo", "cpuTitle": "VS CPU", "cpuSub": "Entrena con IA", "localTitle": "LOCAL", "localSub": "Misma pantalla", "leaderboardTitle": "RANKING", "questsTitle": "MISIONES", "themesTitle": "TEMAS", "tutorialTitle": "TUTORIAL", "startGame": "INICIAR JUEGO", "createMatch": "CREAR PARTIDA", "joinMatch": "UNIRSE", "gameOver": "FIN DEL JUEGO", "mainMenu": "VOLVER AL MENÚ", "exit": "SALIR"},
'fr': {"appTitle": "TetraQ", "welcomeTitle": "BIENVENUE DANS TETRAQ !", "nameHint": "NOM", "saveAndPlay": "SAUVEGARDER ET JOUER", "onlineTitle": "EN LIGNE", "onlineSub": "Défiez le monde", "cpuTitle": "VS CPU", "cpuSub": "Entraînez avec l'IA", "localTitle": "LOCAL", "localSub": "Même écran", "leaderboardTitle": "CLASSEMENT", "questsTitle": "QUÊTES", "themesTitle": "THÈMES", "tutorialTitle": "TUTORIEL", "startGame": "JOUER", "createMatch": "CRÉER UN MATCH", "joinMatch": "REJOINDRE", "gameOver": "FIN DE PARTIE", "mainMenu": "RETOUR AU MENU", "exit": "QUITTER"},
'de': {"appTitle": "TetraQ", "welcomeTitle": "WILLKOMMEN BEI TETRAQ!", "nameHint": "NAME", "saveAndPlay": "SPEICHERN & SPIELEN", "onlineTitle": "ONLINE", "onlineSub": "Fordere die Welt heraus", "cpuTitle": "VS CPU", "cpuSub": "Trainiere mit KI", "localTitle": "LOKAL", "localSub": "Gleicher Bildschirm", "leaderboardTitle": "RANGLISTE", "questsTitle": "MISSIONEN", "themesTitle": "THEMEN", "tutorialTitle": "TUTORIAL", "startGame": "SPIEL STARTEN", "createMatch": "SPIEL ERSTELLEN", "joinMatch": "BEITRETEN", "gameOver": "SPIELENDE", "mainMenu": "ZURÜCK ZUM MENÜ", "exit": "BEENDEN"},
'pt': {"appTitle": "TetraQ", "welcomeTitle": "BEM-VINDO AO TETRAQ!", "nameHint": "NOME", "saveAndPlay": "SALVAR E JOGAR", "onlineTitle": "ONLINE", "onlineSub": "Desafie o mundo", "cpuTitle": "VS CPU", "cpuSub": "Treine com a IA", "localTitle": "LOCAL", "localSub": "Mesma tela", "leaderboardTitle": "CLASSIFICAÇÃO", "questsTitle": "DESAFIOS", "themesTitle": "TEMAS", "tutorialTitle": "TUTORIAL", "startGame": "INICIAR JOGO", "createMatch": "CRIAR PARTIDA", "joinMatch": "ENTRAR", "gameOver": "FIM DE JOGO", "mainMenu": "VOLTAR AO MENU", "exit": "SAIR"},
'ru': {"appTitle": "TetraQ", "welcomeTitle": "ДОБРО ПОЖАЛОВАТЬ В TETRAQ!", "nameHint": "ИМЯ", "saveAndPlay": "СОХРАНИТЬ И ИГРАТЬ", "onlineTitle": "ОНЛАЙН", "onlineSub": "Брось вызов миру", "cpuTitle": "VS ИИ", "cpuSub": "Тренировка с ИИ", "localTitle": "ЛОКАЛЬНО", "localSub": "Один экран", "leaderboardTitle": "РЕЙТИНГ", "questsTitle": "ЗАДАНИЯ", "themesTitle": "ТЕМЫ", "tutorialTitle": "ОБУЧЕНИЕ", "startGame": "НАЧАТЬ ИГРУ", "createMatch": "СОЗДАТЬ ИГРУ", "joinMatch": "ПРИСОЕДИНИТЬСЯ", "gameOver": "ИГРА ОКОНЧЕНА", "mainMenu": "В ГЛАВНОЕ МЕНЮ", "exit": "ВЫХОД"},
'zh': {"appTitle": "TetraQ", "welcomeTitle": "欢迎来到 TETRAQ", "nameHint": "名字", "saveAndPlay": "保存并开始", "onlineTitle": "在线匹配", "onlineSub": "挑战世界", "cpuTitle": "人机对战", "cpuSub": "与AI训练", "localTitle": "本地游戏", "localSub": "同屏对战", "leaderboardTitle": "排行榜", "questsTitle": "任务", "themesTitle": "主题", "tutorialTitle": "教程", "startGame": "开始游戏", "createMatch": "创建比赛", "joinMatch": "加入", "gameOver": "游戏结束", "mainMenu": "返回主菜单", "exit": "退出"}
};
for (var lang in translations.keys) {
final file = File('lib/l10n/app_$lang.arb');
final Map<String, dynamic> finalContent = {"@@locale": lang, ...translations[lang]!};
await file.writeAsString(JsonEncoder.withIndent(' ').convert(finalContent));
}
// Crea anche il file di configurazione
await File('l10n.yaml').writeAsString("arb-dir: lib/l10n\ntemplate-arb-file: app_it.arb\noutput-localization-file: app_localizations.dart\n");
}

BIN
ios/.DS_Store vendored

Binary file not shown.

View file

@ -1190,6 +1190,10 @@ PODS:
- abseil/xcprivacy (1.20240722.0)
- app_links (7.0.0):
- Flutter
- AppCheckCore (11.2.0):
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- PromisesObjC (~> 2.4)
- audioplayers_darwin (0.0.1):
- Flutter
- BoringSSL-GRPC (0.0.37):
@ -1199,32 +1203,60 @@ PODS:
- BoringSSL-GRPC/Interface (= 0.0.37)
- BoringSSL-GRPC/Interface (0.0.37)
- cloud_firestore (6.1.2):
- Firebase/Firestore (= 12.8.0)
- Firebase/Firestore (= 12.9.0)
- firebase_core
- Flutter
- Firebase/CoreOnly (12.8.0):
- FirebaseCore (~> 12.8.0)
- Firebase/Firestore (12.8.0):
- Firebase/Auth (12.9.0):
- Firebase/CoreOnly
- FirebaseFirestore (~> 12.8.0)
- firebase_core (4.4.0):
- Firebase/CoreOnly (= 12.8.0)
- FirebaseAuth (~> 12.9.0)
- Firebase/CoreOnly (12.9.0):
- FirebaseCore (~> 12.9.0)
- Firebase/Firestore (12.9.0):
- Firebase/CoreOnly
- FirebaseFirestore (~> 12.9.0)
- firebase_app_check (0.4.1-5):
- Firebase/CoreOnly (~> 12.9.0)
- firebase_core
- FirebaseAppCheck (~> 12.9.0)
- Flutter
- FirebaseAppCheckInterop (12.8.0)
- FirebaseCore (12.8.0):
- FirebaseCoreInternal (~> 12.8.0)
- firebase_auth (6.1.4):
- Firebase/Auth (= 12.9.0)
- firebase_core
- Flutter
- firebase_core (4.5.0):
- Firebase/CoreOnly (= 12.9.0)
- Flutter
- FirebaseAppCheck (12.9.0):
- AppCheckCore (~> 11.0)
- FirebaseAppCheckInterop (~> 12.9.0)
- FirebaseCore (~> 12.9.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- FirebaseAppCheckInterop (12.9.0)
- FirebaseAuth (12.9.0):
- FirebaseAppCheckInterop (~> 12.9.0)
- FirebaseAuthInterop (~> 12.9.0)
- FirebaseCore (~> 12.9.0)
- FirebaseCoreExtension (~> 12.9.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/Environment (~> 8.1)
- GTMSessionFetcher/Core (< 6.0, >= 3.4)
- RecaptchaInterop (~> 101.0)
- FirebaseAuthInterop (12.9.0)
- FirebaseCore (12.9.0):
- FirebaseCoreInternal (~> 12.9.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreExtension (12.8.0):
- FirebaseCore (~> 12.8.0)
- FirebaseCoreInternal (12.8.0):
- FirebaseCoreExtension (12.9.0):
- FirebaseCore (~> 12.9.0)
- FirebaseCoreInternal (12.9.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseFirestore (12.8.0):
- FirebaseCore (~> 12.8.0)
- FirebaseCoreExtension (~> 12.8.0)
- FirebaseFirestoreInternal (~> 12.8.0)
- FirebaseSharedSwift (~> 12.8.0)
- FirebaseFirestoreInternal (12.8.0):
- FirebaseFirestore (12.9.0):
- FirebaseCore (~> 12.9.0)
- FirebaseCoreExtension (~> 12.9.0)
- FirebaseFirestoreInternal (~> 12.9.0)
- FirebaseSharedSwift (~> 12.9.0)
- FirebaseFirestoreInternal (12.9.0):
- abseil/algorithm (~> 1.20240722.0)
- abseil/base (~> 1.20240722.0)
- abseil/container/flat_hash_map (~> 1.20240722.0)
@ -1233,22 +1265,38 @@ PODS:
- abseil/strings/strings (~> 1.20240722.0)
- abseil/time (~> 1.20240722.0)
- abseil/types (~> 1.20240722.0)
- FirebaseAppCheckInterop (~> 12.8.0)
- FirebaseCore (~> 12.8.0)
- FirebaseAppCheckInterop (~> 12.9.0)
- FirebaseCore (~> 12.9.0)
- "gRPC-C++ (~> 1.69.0)"
- gRPC-Core (~> 1.69.0)
- leveldb-library (~> 1.22)
- nanopb (~> 3.30910.0)
- FirebaseSharedSwift (12.8.0)
- FirebaseSharedSwift (12.9.0)
- Flutter (1.0.0)
- GoogleUtilities/AppDelegateSwizzler (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
- GoogleUtilities/Network
- GoogleUtilities/Privacy
- GoogleUtilities/Environment (8.1.0):
- GoogleUtilities/Privacy
- GoogleUtilities/Logger (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/Network (8.1.0):
- GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib"
- GoogleUtilities/Privacy
- GoogleUtilities/Reachability
- "GoogleUtilities/NSData+zlib (8.1.0)":
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (8.1.0)
- GoogleUtilities/Reachability (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/UserDefaults (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- "gRPC-C++ (1.69.0)":
- "gRPC-C++/Implementation (= 1.69.0)"
- "gRPC-C++/Interface (= 1.69.0)"
@ -1341,12 +1389,15 @@ PODS:
- gRPC-Core/Privacy (= 1.69.0)
- gRPC-Core/Interface (1.69.0)
- gRPC-Core/Privacy (1.69.0)
- GTMSessionFetcher/Core (5.1.0)
- leveldb-library (1.22.6)
- nanopb (3.30910.0):
- nanopb/decode (= 3.30910.0)
- nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0)
- PromisesObjC (2.4.0)
- RecaptchaInterop (101.0.0)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
@ -1357,6 +1408,8 @@ DEPENDENCIES:
- app_links (from `.symlinks/plugins/app_links/ios`)
- audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/ios`)
- cloud_firestore (from `.symlinks/plugins/cloud_firestore/ios`)
- firebase_app_check (from `.symlinks/plugins/firebase_app_check/ios`)
- firebase_auth (from `.symlinks/plugins/firebase_auth/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- Flutter (from `Flutter`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
@ -1365,9 +1418,13 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- abseil
- AppCheckCore
- BoringSSL-GRPC
- Firebase
- FirebaseAppCheck
- FirebaseAppCheckInterop
- FirebaseAuth
- FirebaseAuthInterop
- FirebaseCore
- FirebaseCoreExtension
- FirebaseCoreInternal
@ -1377,8 +1434,11 @@ SPEC REPOS:
- GoogleUtilities
- "gRPC-C++"
- gRPC-Core
- GTMSessionFetcher
- leveldb-library
- nanopb
- PromisesObjC
- RecaptchaInterop
EXTERNAL SOURCES:
app_links:
@ -1387,6 +1447,10 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/audioplayers_darwin/ios"
cloud_firestore:
:path: ".symlinks/plugins/cloud_firestore/ios"
firebase_app_check:
:path: ".symlinks/plugins/firebase_app_check/ios"
firebase_auth:
:path: ".symlinks/plugins/firebase_auth/ios"
firebase_core:
:path: ".symlinks/plugins/firebase_core/ios"
Flutter:
@ -1399,24 +1463,33 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
abseil: a05cc83bf02079535e17169a73c5be5ba47f714b
app_links: a754cbec3c255bd4bbb4d236ecc06f28cd9a7ce8
AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
audioplayers_darwin: ccf9c770ee768abb07e26d90af093f7bab1c12ab
BoringSSL-GRPC: dded2a44897e45f28f08ae87a55ee4bcd19bc508
cloud_firestore: 4bd00c3464706d9e09dabac0bb8e9610456109f5
Firebase: 9a58fdbc9d8655ed7b79a19cf9690bb007d3d46d
firebase_core: ee30637e6744af8e0c12a6a1e8a9718506ec2398
FirebaseAppCheckInterop: ba3dc604a89815379e61ec2365101608d365cf7d
FirebaseCore: 0dbad74bda10b8fb9ca34ad8f375fb9dd3ebef7c
FirebaseCoreExtension: 6605938d51f765d8b18bfcafd2085276a252bee2
FirebaseCoreInternal: fe5fa466aeb314787093a7dce9f0beeaad5a2a21
FirebaseFirestore: 67f23000ca238ccbab79127ed59636a9a2689e74
FirebaseFirestoreInternal: a0e7382af3d208898dcd1d4d52d8a7870632e881
FirebaseSharedSwift: f57ed48f4542b2d7eb4738f4f23ba443f78b3780
cloud_firestore: 81f6c428ecee874dc3808afe0e0c48a87beb5bdf
Firebase: 065f2bb395062046623036d8e6dc857bc2521d56
firebase_app_check: 33f1df6830ec8ebadee0db0120956c44a65c7213
firebase_auth: fecf9fe293464b52063f5f2a7110e63ff2ab3403
firebase_core: afac1aac13c931e0401c7e74ed1276112030efab
FirebaseAppCheck: 94dae4d9bb682bdef85a778b0c1024a4613f1e89
FirebaseAppCheckInterop: 4bade10286cc977e516f75d2d8312cbdfa534789
FirebaseAuth: 3a39f6436c21ebfd7919b698228b4f89ff94c23b
FirebaseAuthInterop: f8f6ff72dc24621906497fbe5cf3c42ee815e59c
FirebaseCore: 428912f751178b06bef0a1793effeb4a5e09a9b8
FirebaseCoreExtension: e911052d59cd0da237a45d706fc0f81654f035c1
FirebaseCoreInternal: b321eafae5362113bc182956fafc9922cfc77b72
FirebaseFirestore: d8b76ca1feb4ca0b0f078c45f7d1bd8014a49ef1
FirebaseFirestoreInternal: 02341a9ba87f6309227b04685022a5e16307bbf7
FirebaseSharedSwift: 9d2fa84a46676302b89dbd5e6e62bce2fe376909
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
"gRPC-C++": cc207623316fb041a7a3e774c252cf68a058b9e8
gRPC-Core: 860978b7db482de8b4f5e10677216309b5ff6330
GTMSessionFetcher: b8ab00db932816e14b0a0664a08cb73dda6d164b
leveldb-library: cc8b8f8e013647a295ad3f8cd2ddf49a6f19be19
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
RecaptchaInterop: 11e0b637842dfb48308d242afc3f448062325aba
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb

View file

@ -52,6 +52,7 @@
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
4867B86862DC650EC26D5F9C /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
52CB81B72F635109004C3F43 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
5C30E1EF56D9EC1CAEADBE23 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@ -146,6 +147,7 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
52CB81B72F635109004C3F43 /* Runner.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
@ -473,6 +475,9 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_TEAM = 2BX6QRR7GG;
ENABLE_BITCODE = NO;
@ -485,6 +490,7 @@
MARKETING_VERSION = 1.0.2;
PRODUCT_BUNDLE_IDENTIFIER = com.sanza.tetraq;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
@ -500,7 +506,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.sanza.tetraq.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.amastra.tetraq.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -518,7 +524,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.sanza.tetraq.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.amastra.tetraq.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@ -534,7 +540,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.sanza.tetraq.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.amastra.tetraq.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@ -658,6 +664,9 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_TEAM = 2BX6QRR7GG;
ENABLE_BITCODE = NO;
@ -670,6 +679,7 @@
MARKETING_VERSION = 1.0.2;
PRODUCT_BUNDLE_IDENTIFIER = com.sanza.tetraq;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@ -683,6 +693,9 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_TEAM = 2BX6QRR7GG;
ENABLE_BITCODE = NO;
@ -695,6 +708,7 @@
MARKETING_VERSION = 1.0.2;
PRODUCT_BUNDLE_IDENTIFIER = com.sanza.tetraq;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";

View file

@ -9,7 +9,7 @@
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.sanza.tetraq</string>
<string>com.amastra.tetraq</string>
<key>PROJECT_ID</key>
<string>tetraq-32a4a</string>
<key>STORAGE_BUCKET</key>
@ -25,6 +25,6 @@
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:705460445314:ios:da11cbca5d1f6bc27b949b</string>
<string>1:705460445314:ios:54d64cb7592954327b949b</string>
</dict>
</plist>

View file

@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
@ -20,10 +22,25 @@
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>com.sanza.tetraq</string>
<key>CFBundleURLSchemes</key>
<array>
<string>tetraq</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
@ -41,9 +58,5 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:tetraq-32a4a.web.app</string>
</array>
</dict>
</plist>

4
l10n.yaml Normal file
View file

@ -0,0 +1,4 @@
arb-dir: lib/l10n
template-arb-file: app_it.arb
output-localization-file: app_localizations.dart
output-dir: lib/l10n

BIN
lib/.DS_Store vendored

Binary file not shown.

View file

@ -5,7 +5,7 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
enum AppThemeType { minimal, doodle, cyberpunk, wood, arcade, grimorio }
enum AppThemeType { doodle, wood, cyberpunk, arcade, grimorio, music } // <-- Aggiunto 'music'
class ThemeColors {
final Color background;
@ -24,11 +24,6 @@ class ThemeColors {
}
class AppColors {
static const ThemeColors minimal = ThemeColors(
background: Color(0xFFF5F7FA), gridLine: Color(0xFFCFD8DC),
playerRed: Color(0xFFE53935), playerBlue: Color(0xFF1E88E5), text: Color(0xFF263238),
);
static const ThemeColors doodle = ThemeColors(
background: Color(0xFFFFF9E6), gridLine: Color(0xFFB0BEC5),
playerRed: Color(0xFFD32F2F), playerBlue: Color(0xFF1565C0), text: Color(0xFF37474F),
@ -54,14 +49,23 @@ class AppColors {
playerRed: Color(0xFFE91E63), playerBlue: Color(0xFF4FC3F7), text: Color(0xFFFFF3E0),
);
// --- NUOVO TEMA MUSICA ---
static const ThemeColors music = ThemeColors(
background: Color(0xFF120B29), // Viola scuro (stile Synthwave)
gridLine: Color(0xFF6A1B9A), // Viola elettrico
playerRed: Color(0xFFFF2A6D), // Rosa acceso
playerBlue: Color(0xFF05D5FF), // Ciano
text: Color(0xFFE0E0E0),
);
static ThemeColors getTheme(AppThemeType type) {
switch (type) {
case AppThemeType.minimal: return minimal;
case AppThemeType.doodle: return doodle;
case AppThemeType.cyberpunk: return cyberpunk;
case AppThemeType.wood: return wood;
case AppThemeType.cyberpunk: return cyberpunk;
case AppThemeType.arcade: return arcade;
case AppThemeType.grimorio: return grimorio;
case AppThemeType.music: return music;
}
}
}
@ -69,65 +73,66 @@ class AppColors {
class ThemeIcons {
static IconData gold(AppThemeType type) {
switch (type) {
case AppThemeType.minimal: return Icons.star_rounded;
case AppThemeType.doodle: return FontAwesomeIcons.star;
case AppThemeType.wood: return FontAwesomeIcons.gem;
case AppThemeType.cyberpunk: return FontAwesomeIcons.microchip;
case AppThemeType.arcade: return FontAwesomeIcons.coins;
case AppThemeType.grimorio: return FontAwesomeIcons.crown;
case AppThemeType.music: return FontAwesomeIcons.compactDisc; // CD/Vinile per i punti
}
}
static IconData bomb(AppThemeType type) {
switch (type) {
case AppThemeType.minimal: return Icons.mood_bad_rounded;
case AppThemeType.doodle: return FontAwesomeIcons.virus;
case AppThemeType.wood: return FontAwesomeIcons.fire;
case AppThemeType.cyberpunk: return FontAwesomeIcons.bug;
case AppThemeType.arcade: return FontAwesomeIcons.ghost;
case AppThemeType.grimorio: return FontAwesomeIcons.hatWizard;
case AppThemeType.music: return FontAwesomeIcons.volumeXmark; // Muto/Errore per la bomba
}
}
static IconData swap(AppThemeType type) {
switch (type) {
case AppThemeType.minimal: return Icons.sync_rounded;
case AppThemeType.doodle: return FontAwesomeIcons.arrowsRotate;
case AppThemeType.wood: return FontAwesomeIcons.rightLeft;
case AppThemeType.cyberpunk: return FontAwesomeIcons.networkWired;
case AppThemeType.arcade: return FontAwesomeIcons.shuffle;
case AppThemeType.grimorio: return FontAwesomeIcons.hurricane;
case AppThemeType.music: return FontAwesomeIcons.sliders; // Fader da DJ
}
}
static IconData joker(AppThemeType type) {
switch (type) {
case AppThemeType.minimal: return Icons.sentiment_satisfied_alt;
case AppThemeType.doodle: return FontAwesomeIcons.faceSmileBeam;
case AppThemeType.wood: return FontAwesomeIcons.key;
case AppThemeType.cyberpunk: return FontAwesomeIcons.robot;
case AppThemeType.arcade: return FontAwesomeIcons.gamepad;
case AppThemeType.grimorio: return FontAwesomeIcons.masksTheater;
case AppThemeType.music: return FontAwesomeIcons.headphones; // Cuffie per il Jolly
}
}
static IconData block(AppThemeType type) {
switch (type) {
case AppThemeType.minimal: return Icons.block;
case AppThemeType.doodle: return FontAwesomeIcons.squareXmark;
case AppThemeType.wood: return FontAwesomeIcons.ban;
case AppThemeType.cyberpunk: return FontAwesomeIcons.shieldHalved;
case AppThemeType.arcade: return FontAwesomeIcons.powerOff;
case AppThemeType.grimorio: return FontAwesomeIcons.meteor;
case AppThemeType.music: return FontAwesomeIcons.pause; // Pausa per il blocco vuoto
}
}
// --- NUOVE ICONE ---
static IconData ice(AppThemeType type) {
if (type == AppThemeType.music) return FontAwesomeIcons.music; // Nota musicale ghiacciata
return FontAwesomeIcons.snowflake;
}
static IconData multiplier(AppThemeType type) {
if (type == AppThemeType.music) return FontAwesomeIcons.forwardFast; // Fast Forward per il x2
return FontAwesomeIcons.bolt;
}
}

View file

@ -1,6 +1,11 @@
// ===========================================================================
// FILE: lib/core/theme_manager.dart
// ===========================================================================
import 'package:flutter/material.dart';
import 'app_colors.dart';
import '../services/storage_service.dart';
import '../services/audio_service.dart'; // <-- NUOVO IMPORT PER LA MUSICA
class ThemeManager extends ChangeNotifier {
late AppThemeType _currentThemeType;
@ -8,6 +13,9 @@ class ThemeManager extends ChangeNotifier {
ThemeManager() {
// Quando l'app parte, legge il tema dalla memoria!
_currentThemeType = AppThemeType.values[StorageService.instance.savedThemeIndex];
// Fai partire subito la colonna sonora del tema salvato!
AudioService.instance.playBgm(_currentThemeType);
}
AppThemeType get currentThemeType => _currentThemeType;
@ -16,6 +24,10 @@ class ThemeManager extends ChangeNotifier {
void setTheme(AppThemeType type) {
_currentThemeType = type;
StorageService.instance.saveTheme(type); // Salva la scelta nel "disco fisso"
// Cambia magicamente la canzone in sottofondo!
AudioService.instance.playBgm(type);
notifyListeners();
}
}

View file

@ -48,7 +48,7 @@ class DefaultFirebaseOptions {
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyBsXO595xVITDPrRnXrW8HPQLOe7Rz4Gg4',
appId: '1:705460445314:android:4d35fef29cfd63727b949b',
appId: '1:705460445314:android:ceac21bb06b7a9f07b949b',
messagingSenderId: '705460445314',
projectId: 'tetraq-32a4a',
storageBucket: 'tetraq-32a4a.firebasestorage.app',
@ -56,11 +56,11 @@ class DefaultFirebaseOptions {
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyB77j18Jgeb9gBAEwp-uyOQvr4m-RJ_rAE',
appId: '1:705460445314:ios:da11cbca5d1f6bc27b949b',
appId: '1:705460445314:ios:54d64cb7592954327b949b',
messagingSenderId: '705460445314',
projectId: 'tetraq-32a4a',
storageBucket: 'tetraq-32a4a.firebasestorage.app',
iosBundleId: 'com.sanza.tetraq',
iosBundleId: 'com.amastra.tetraq',
);
static const FirebaseOptions macos = FirebaseOptions(
@ -71,4 +71,5 @@ class DefaultFirebaseOptions {
storageBucket: 'tetraq-32a4a.firebasestorage.app',
iosBundleId: 'com.sanza.tetraq',
);
}
}

23
lib/l10n/app_de.arb Normal file
View file

@ -0,0 +1,23 @@
{
"@@locale": "de",
"appTitle": "TetraQ",
"welcomeTitle": "WILLKOMMEN BEI TETRAQ!",
"nameHint": "NAME",
"saveAndPlay": "SPEICHERN & SPIELEN",
"onlineTitle": "ONLINE",
"onlineSub": "Fordere die Welt heraus",
"cpuTitle": "VS CPU",
"cpuSub": "Trainiere mit KI",
"localTitle": "LOKAL",
"localSub": "Gleicher Bildschirm",
"leaderboardTitle": "RANGLISTE",
"questsTitle": "MISSIONEN",
"themesTitle": "THEMEN",
"tutorialTitle": "TUTORIAL",
"startGame": "SPIEL STARTEN",
"createMatch": "SPIEL ERSTELLEN",
"joinMatch": "BEITRETEN",
"gameOver": "SPIELENDE",
"mainMenu": "ZURÜCK ZUM MENÜ",
"exit": "BEENDEN"
}

View file

@ -1,4 +1,23 @@
{
"@@locale": "en",
"appTitle": "TetraQ",
"playLocal": "PASS & PLAY (Local)"
}
"welcomeTitle": "WELCOME TO TETRAQ!",
"nameHint": "NAME",
"saveAndPlay": "SAVE & PLAY",
"onlineTitle": "ONLINE",
"onlineSub": "Challenge the world",
"cpuTitle": "VS CPU",
"cpuSub": "Train with AI",
"localTitle": "LOCAL",
"localSub": "Same screen",
"leaderboardTitle": "LEADERBOARD",
"questsTitle": "QUESTS",
"themesTitle": "THEMES",
"tutorialTitle": "TUTORIAL",
"startGame": "START GAME",
"createMatch": "CREATE MATCH",
"joinMatch": "JOIN",
"gameOver": "GAME OVER",
"mainMenu": "BACK TO MENU",
"exit": "EXIT"
}

23
lib/l10n/app_es.arb Normal file
View file

@ -0,0 +1,23 @@
{
"@@locale": "es",
"appTitle": "TetraQ",
"welcomeTitle": "¡BIENVENIDO A TETRAQ!",
"nameHint": "NOMBRE",
"saveAndPlay": "GUARDAR Y JUGAR",
"onlineTitle": "ONLINE",
"onlineSub": "Desafía al mundo",
"cpuTitle": "VS CPU",
"cpuSub": "Entrena con IA",
"localTitle": "LOCAL",
"localSub": "Misma pantalla",
"leaderboardTitle": "RANKING",
"questsTitle": "MISIONES",
"themesTitle": "TEMAS",
"tutorialTitle": "TUTORIAL",
"startGame": "INICIAR JUEGO",
"createMatch": "CREAR PARTIDA",
"joinMatch": "UNIRSE",
"gameOver": "FIN DEL JUEGO",
"mainMenu": "VOLVER AL MENÚ",
"exit": "SALIR"
}

23
lib/l10n/app_fr.arb Normal file
View file

@ -0,0 +1,23 @@
{
"@@locale": "fr",
"appTitle": "TetraQ",
"welcomeTitle": "BIENVENUE DANS TETRAQ !",
"nameHint": "NOM",
"saveAndPlay": "SAUVEGARDER ET JOUER",
"onlineTitle": "EN LIGNE",
"onlineSub": "Défiez le monde",
"cpuTitle": "VS CPU",
"cpuSub": "Entraînez avec l'IA",
"localTitle": "LOCAL",
"localSub": "Même écran",
"leaderboardTitle": "CLASSEMENT",
"questsTitle": "QUÊTES",
"themesTitle": "THÈMES",
"tutorialTitle": "TUTORIEL",
"startGame": "JOUER",
"createMatch": "CRÉER UN MATCH",
"joinMatch": "REJOINDRE",
"gameOver": "FIN DE PARTIE",
"mainMenu": "RETOUR AU MENU",
"exit": "QUITTER"
}

View file

@ -1,4 +1,23 @@
{
"@@locale": "it",
"appTitle": "TetraQ",
"playLocal": "PASS & PLAY (Locale)"
}
"welcomeTitle": "BENVENUTO IN TETRAQ!",
"nameHint": "NOME",
"saveAndPlay": "SALVA E GIOCA",
"onlineTitle": "ONLINE",
"onlineSub": "Sfida il mondo",
"cpuTitle": "VS CPU",
"cpuSub": "Allenati con l'IA",
"localTitle": "LOCALE",
"localSub": "Stesso schermo",
"leaderboardTitle": "CLASSIFICA",
"questsTitle": "SFIDE",
"themesTitle": "TEMI",
"tutorialTitle": "TUTORIAL",
"startGame": "AVVIA PARTITA",
"createMatch": "CREA PARTITA",
"joinMatch": "UNISCITI",
"gameOver": "FINE PARTITA",
"mainMenu": "TORNA AL MENU",
"exit": "ESCI"
}

View file

@ -0,0 +1,286 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.dart' as intl;
import 'app_localizations_de.dart';
import 'app_localizations_en.dart';
import 'app_localizations_es.dart';
import 'app_localizations_fr.dart';
import 'app_localizations_it.dart';
import 'app_localizations_pt.dart';
import 'app_localizations_ru.dart';
import 'app_localizations_zh.dart';
// ignore_for_file: type=lint
/// Callers can lookup localized strings with an instance of AppLocalizations
/// returned by `AppLocalizations.of(context)`.
///
/// Applications need to include `AppLocalizations.delegate()` in their app's
/// `localizationDelegates` list, and the locales they support in the app's
/// `supportedLocales` list. For example:
///
/// ```dart
/// import 'l10n/app_localizations.dart';
///
/// return MaterialApp(
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
/// supportedLocales: AppLocalizations.supportedLocales,
/// home: MyApplicationHome(),
/// );
/// ```
///
/// ## Update pubspec.yaml
///
/// Please make sure to update your pubspec.yaml to include the following
/// packages:
///
/// ```yaml
/// dependencies:
/// # Internationalization support.
/// flutter_localizations:
/// sdk: flutter
/// intl: any # Use the pinned version from flutter_localizations
///
/// # Rest of dependencies
/// ```
///
/// ## iOS Applications
///
/// iOS applications define key application metadata, including supported
/// locales, in an Info.plist file that is built into the application bundle.
/// To configure the locales supported by your app, youll need to edit this
/// file.
///
/// First, open your projects ios/Runner.xcworkspace Xcode workspace file.
/// Then, in the Project Navigator, open the Info.plist file under the Runner
/// projects Runner folder.
///
/// Next, select the Information Property List item, select Add Item from the
/// Editor menu, then select Localizations from the pop-up menu.
///
/// Select and expand the newly-created Localizations item then, for each
/// locale your application supports, add a new item and select the locale
/// you wish to add from the pop-up menu in the Value field. This list should
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
/// property.
abstract class AppLocalizations {
AppLocalizations(String locale)
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
final String localeName;
static AppLocalizations? of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations);
}
static const LocalizationsDelegate<AppLocalizations> delegate =
_AppLocalizationsDelegate();
/// A list of this localizations delegate along with the default localizations
/// delegates.
///
/// Returns a list of localizations delegates containing this delegate along with
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
/// and GlobalWidgetsLocalizations.delegate.
///
/// Additional delegates can be added by appending to this list in
/// MaterialApp. This list does not have to be used at all if a custom list
/// of delegates is preferred or required.
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
<LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
/// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[
Locale('de'),
Locale('en'),
Locale('es'),
Locale('fr'),
Locale('it'),
Locale('pt'),
Locale('ru'),
Locale('zh'),
];
/// No description provided for @appTitle.
///
/// In it, this message translates to:
/// **'TetraQ'**
String get appTitle;
/// No description provided for @welcomeTitle.
///
/// In it, this message translates to:
/// **'BENVENUTO IN TETRAQ!'**
String get welcomeTitle;
/// No description provided for @nameHint.
///
/// In it, this message translates to:
/// **'NOME'**
String get nameHint;
/// No description provided for @saveAndPlay.
///
/// In it, this message translates to:
/// **'SALVA E GIOCA'**
String get saveAndPlay;
/// No description provided for @onlineTitle.
///
/// In it, this message translates to:
/// **'ONLINE'**
String get onlineTitle;
/// No description provided for @onlineSub.
///
/// In it, this message translates to:
/// **'Sfida il mondo'**
String get onlineSub;
/// No description provided for @cpuTitle.
///
/// In it, this message translates to:
/// **'VS CPU'**
String get cpuTitle;
/// No description provided for @cpuSub.
///
/// In it, this message translates to:
/// **'Allenati con l\'IA'**
String get cpuSub;
/// No description provided for @localTitle.
///
/// In it, this message translates to:
/// **'LOCALE'**
String get localTitle;
/// No description provided for @localSub.
///
/// In it, this message translates to:
/// **'Stesso schermo'**
String get localSub;
/// No description provided for @leaderboardTitle.
///
/// In it, this message translates to:
/// **'CLASSIFICA'**
String get leaderboardTitle;
/// No description provided for @questsTitle.
///
/// In it, this message translates to:
/// **'SFIDE'**
String get questsTitle;
/// No description provided for @themesTitle.
///
/// In it, this message translates to:
/// **'TEMI'**
String get themesTitle;
/// No description provided for @tutorialTitle.
///
/// In it, this message translates to:
/// **'TUTORIAL'**
String get tutorialTitle;
/// No description provided for @startGame.
///
/// In it, this message translates to:
/// **'AVVIA PARTITA'**
String get startGame;
/// No description provided for @createMatch.
///
/// In it, this message translates to:
/// **'CREA PARTITA'**
String get createMatch;
/// No description provided for @joinMatch.
///
/// In it, this message translates to:
/// **'UNISCITI'**
String get joinMatch;
/// No description provided for @gameOver.
///
/// In it, this message translates to:
/// **'FINE PARTITA'**
String get gameOver;
/// No description provided for @mainMenu.
///
/// In it, this message translates to:
/// **'TORNA AL MENU'**
String get mainMenu;
/// No description provided for @exit.
///
/// In it, this message translates to:
/// **'ESCI'**
String get exit;
}
class _AppLocalizationsDelegate
extends LocalizationsDelegate<AppLocalizations> {
const _AppLocalizationsDelegate();
@override
Future<AppLocalizations> load(Locale locale) {
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
}
@override
bool isSupported(Locale locale) => <String>[
'de',
'en',
'es',
'fr',
'it',
'pt',
'ru',
'zh',
].contains(locale.languageCode);
@override
bool shouldReload(_AppLocalizationsDelegate old) => false;
}
AppLocalizations lookupAppLocalizations(Locale locale) {
// Lookup logic when only language code is specified.
switch (locale.languageCode) {
case 'de':
return AppLocalizationsDe();
case 'en':
return AppLocalizationsEn();
case 'es':
return AppLocalizationsEs();
case 'fr':
return AppLocalizationsFr();
case 'it':
return AppLocalizationsIt();
case 'pt':
return AppLocalizationsPt();
case 'ru':
return AppLocalizationsRu();
case 'zh':
return AppLocalizationsZh();
}
throw FlutterError(
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
'an issue with the localizations generation tool. Please file an issue '
'on GitHub with a reproducible sample app and the gen-l10n configuration '
'that was used.',
);
}

View file

@ -0,0 +1,70 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for German (`de`).
class AppLocalizationsDe extends AppLocalizations {
AppLocalizationsDe([String locale = 'de']) : super(locale);
@override
String get appTitle => 'TetraQ';
@override
String get welcomeTitle => 'WILLKOMMEN BEI TETRAQ!';
@override
String get nameHint => 'NAME';
@override
String get saveAndPlay => 'SPEICHERN & SPIELEN';
@override
String get onlineTitle => 'ONLINE';
@override
String get onlineSub => 'Fordere die Welt heraus';
@override
String get cpuTitle => 'VS CPU';
@override
String get cpuSub => 'Trainiere mit KI';
@override
String get localTitle => 'LOKAL';
@override
String get localSub => 'Gleicher Bildschirm';
@override
String get leaderboardTitle => 'RANGLISTE';
@override
String get questsTitle => 'MISSIONEN';
@override
String get themesTitle => 'THEMEN';
@override
String get tutorialTitle => 'TUTORIAL';
@override
String get startGame => 'SPIEL STARTEN';
@override
String get createMatch => 'SPIEL ERSTELLEN';
@override
String get joinMatch => 'BEITRETEN';
@override
String get gameOver => 'SPIELENDE';
@override
String get mainMenu => 'ZURÜCK ZUM MENÜ';
@override
String get exit => 'BEENDEN';
}

View file

@ -0,0 +1,70 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for English (`en`).
class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale);
@override
String get appTitle => 'TetraQ';
@override
String get welcomeTitle => 'WELCOME TO TETRAQ!';
@override
String get nameHint => 'NAME';
@override
String get saveAndPlay => 'SAVE & PLAY';
@override
String get onlineTitle => 'ONLINE';
@override
String get onlineSub => 'Challenge the world';
@override
String get cpuTitle => 'VS CPU';
@override
String get cpuSub => 'Train with AI';
@override
String get localTitle => 'LOCAL';
@override
String get localSub => 'Same screen';
@override
String get leaderboardTitle => 'LEADERBOARD';
@override
String get questsTitle => 'QUESTS';
@override
String get themesTitle => 'THEMES';
@override
String get tutorialTitle => 'TUTORIAL';
@override
String get startGame => 'START GAME';
@override
String get createMatch => 'CREATE MATCH';
@override
String get joinMatch => 'JOIN';
@override
String get gameOver => 'GAME OVER';
@override
String get mainMenu => 'BACK TO MENU';
@override
String get exit => 'EXIT';
}

View file

@ -0,0 +1,70 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Spanish Castilian (`es`).
class AppLocalizationsEs extends AppLocalizations {
AppLocalizationsEs([String locale = 'es']) : super(locale);
@override
String get appTitle => 'TetraQ';
@override
String get welcomeTitle => '¡BIENVENIDO A TETRAQ!';
@override
String get nameHint => 'NOMBRE';
@override
String get saveAndPlay => 'GUARDAR Y JUGAR';
@override
String get onlineTitle => 'ONLINE';
@override
String get onlineSub => 'Desafía al mundo';
@override
String get cpuTitle => 'VS CPU';
@override
String get cpuSub => 'Entrena con IA';
@override
String get localTitle => 'LOCAL';
@override
String get localSub => 'Misma pantalla';
@override
String get leaderboardTitle => 'RANKING';
@override
String get questsTitle => 'MISIONES';
@override
String get themesTitle => 'TEMAS';
@override
String get tutorialTitle => 'TUTORIAL';
@override
String get startGame => 'INICIAR JUEGO';
@override
String get createMatch => 'CREAR PARTIDA';
@override
String get joinMatch => 'UNIRSE';
@override
String get gameOver => 'FIN DEL JUEGO';
@override
String get mainMenu => 'VOLVER AL MENÚ';
@override
String get exit => 'SALIR';
}

View file

@ -0,0 +1,70 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for French (`fr`).
class AppLocalizationsFr extends AppLocalizations {
AppLocalizationsFr([String locale = 'fr']) : super(locale);
@override
String get appTitle => 'TetraQ';
@override
String get welcomeTitle => 'BIENVENUE DANS TETRAQ !';
@override
String get nameHint => 'NOM';
@override
String get saveAndPlay => 'SAUVEGARDER ET JOUER';
@override
String get onlineTitle => 'EN LIGNE';
@override
String get onlineSub => 'Défiez le monde';
@override
String get cpuTitle => 'VS CPU';
@override
String get cpuSub => 'Entraînez avec l\'IA';
@override
String get localTitle => 'LOCAL';
@override
String get localSub => 'Même écran';
@override
String get leaderboardTitle => 'CLASSEMENT';
@override
String get questsTitle => 'QUÊTES';
@override
String get themesTitle => 'THÈMES';
@override
String get tutorialTitle => 'TUTORIEL';
@override
String get startGame => 'JOUER';
@override
String get createMatch => 'CRÉER UN MATCH';
@override
String get joinMatch => 'REJOINDRE';
@override
String get gameOver => 'FIN DE PARTIE';
@override
String get mainMenu => 'RETOUR AU MENU';
@override
String get exit => 'QUITTER';
}

View file

@ -0,0 +1,70 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Italian (`it`).
class AppLocalizationsIt extends AppLocalizations {
AppLocalizationsIt([String locale = 'it']) : super(locale);
@override
String get appTitle => 'TetraQ';
@override
String get welcomeTitle => 'BENVENUTO IN TETRAQ!';
@override
String get nameHint => 'NOME';
@override
String get saveAndPlay => 'SALVA E GIOCA';
@override
String get onlineTitle => 'ONLINE';
@override
String get onlineSub => 'Sfida il mondo';
@override
String get cpuTitle => 'VS CPU';
@override
String get cpuSub => 'Allenati con l\'IA';
@override
String get localTitle => 'LOCALE';
@override
String get localSub => 'Stesso schermo';
@override
String get leaderboardTitle => 'CLASSIFICA';
@override
String get questsTitle => 'SFIDE';
@override
String get themesTitle => 'TEMI';
@override
String get tutorialTitle => 'TUTORIAL';
@override
String get startGame => 'AVVIA PARTITA';
@override
String get createMatch => 'CREA PARTITA';
@override
String get joinMatch => 'UNISCITI';
@override
String get gameOver => 'FINE PARTITA';
@override
String get mainMenu => 'TORNA AL MENU';
@override
String get exit => 'ESCI';
}

View file

@ -0,0 +1,70 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Portuguese (`pt`).
class AppLocalizationsPt extends AppLocalizations {
AppLocalizationsPt([String locale = 'pt']) : super(locale);
@override
String get appTitle => 'TetraQ';
@override
String get welcomeTitle => 'BEM-VINDO AO TETRAQ!';
@override
String get nameHint => 'NOME';
@override
String get saveAndPlay => 'SALVAR E JOGAR';
@override
String get onlineTitle => 'ONLINE';
@override
String get onlineSub => 'Desafie o mundo';
@override
String get cpuTitle => 'VS CPU';
@override
String get cpuSub => 'Treine com a IA';
@override
String get localTitle => 'LOCAL';
@override
String get localSub => 'Mesma tela';
@override
String get leaderboardTitle => 'CLASSIFICAÇÃO';
@override
String get questsTitle => 'DESAFIOS';
@override
String get themesTitle => 'TEMAS';
@override
String get tutorialTitle => 'TUTORIAL';
@override
String get startGame => 'INICIAR JOGO';
@override
String get createMatch => 'CRIAR PARTIDA';
@override
String get joinMatch => 'ENTRAR';
@override
String get gameOver => 'FIM DE JOGO';
@override
String get mainMenu => 'VOLTAR AO MENU';
@override
String get exit => 'SAIR';
}

View file

@ -0,0 +1,70 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Russian (`ru`).
class AppLocalizationsRu extends AppLocalizations {
AppLocalizationsRu([String locale = 'ru']) : super(locale);
@override
String get appTitle => 'TetraQ';
@override
String get welcomeTitle => 'ДОБРО ПОЖАЛОВАТЬ В TETRAQ!';
@override
String get nameHint => 'ИМЯ';
@override
String get saveAndPlay => 'СОХРАНИТЬ И ИГРАТЬ';
@override
String get onlineTitle => 'ОНЛАЙН';
@override
String get onlineSub => 'Брось вызов миру';
@override
String get cpuTitle => 'VS ИИ';
@override
String get cpuSub => 'Тренировка с ИИ';
@override
String get localTitle => 'ЛОКАЛЬНО';
@override
String get localSub => 'Один экран';
@override
String get leaderboardTitle => 'РЕЙТИНГ';
@override
String get questsTitle => 'ЗАДАНИЯ';
@override
String get themesTitle => 'ТЕМЫ';
@override
String get tutorialTitle => 'ОБУЧЕНИЕ';
@override
String get startGame => 'НАЧАТЬ ИГРУ';
@override
String get createMatch => 'СОЗДАТЬ ИГРУ';
@override
String get joinMatch => 'ПРИСОЕДИНИТЬСЯ';
@override
String get gameOver => 'ИГРА ОКОНЧЕНА';
@override
String get mainMenu => 'В ГЛАВНОЕ МЕНЮ';
@override
String get exit => 'ВЫХОД';
}

View file

@ -0,0 +1,70 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Chinese (`zh`).
class AppLocalizationsZh extends AppLocalizations {
AppLocalizationsZh([String locale = 'zh']) : super(locale);
@override
String get appTitle => 'TetraQ';
@override
String get welcomeTitle => '欢迎来到 TETRAQ';
@override
String get nameHint => '名字';
@override
String get saveAndPlay => '保存并开始';
@override
String get onlineTitle => '在线匹配';
@override
String get onlineSub => '挑战世界';
@override
String get cpuTitle => '人机对战';
@override
String get cpuSub => '与AI训练';
@override
String get localTitle => '本地游戏';
@override
String get localSub => '同屏对战';
@override
String get leaderboardTitle => '排行榜';
@override
String get questsTitle => '任务';
@override
String get themesTitle => '主题';
@override
String get tutorialTitle => '教程';
@override
String get startGame => '开始游戏';
@override
String get createMatch => '创建比赛';
@override
String get joinMatch => '加入';
@override
String get gameOver => '游戏结束';
@override
String get mainMenu => '返回主菜单';
@override
String get exit => '退出';
}

23
lib/l10n/app_pt.arb Normal file
View file

@ -0,0 +1,23 @@
{
"@@locale": "pt",
"appTitle": "TetraQ",
"welcomeTitle": "BEM-VINDO AO TETRAQ!",
"nameHint": "NOME",
"saveAndPlay": "SALVAR E JOGAR",
"onlineTitle": "ONLINE",
"onlineSub": "Desafie o mundo",
"cpuTitle": "VS CPU",
"cpuSub": "Treine com a IA",
"localTitle": "LOCAL",
"localSub": "Mesma tela",
"leaderboardTitle": "CLASSIFICAÇÃO",
"questsTitle": "DESAFIOS",
"themesTitle": "TEMAS",
"tutorialTitle": "TUTORIAL",
"startGame": "INICIAR JOGO",
"createMatch": "CRIAR PARTIDA",
"joinMatch": "ENTRAR",
"gameOver": "FIM DE JOGO",
"mainMenu": "VOLTAR AO MENU",
"exit": "SAIR"
}

23
lib/l10n/app_ru.arb Normal file
View file

@ -0,0 +1,23 @@
{
"@@locale": "ru",
"appTitle": "TetraQ",
"welcomeTitle": "ДОБРО ПОЖАЛОВАТЬ В TETRAQ!",
"nameHint": "ИМЯ",
"saveAndPlay": "СОХРАНИТЬ И ИГРАТЬ",
"onlineTitle": "ОНЛАЙН",
"onlineSub": "Брось вызов миру",
"cpuTitle": "VS ИИ",
"cpuSub": "Тренировка с ИИ",
"localTitle": "ЛОКАЛЬНО",
"localSub": "Один экран",
"leaderboardTitle": "РЕЙТИНГ",
"questsTitle": "ЗАДАНИЯ",
"themesTitle": "ТЕМЫ",
"tutorialTitle": "ОБУЧЕНИЕ",
"startGame": "НАЧАТЬ ИГРУ",
"createMatch": "СОЗДАТЬ ИГРУ",
"joinMatch": "ПРИСОЕДИНИТЬСЯ",
"gameOver": "ИГРА ОКОНЧЕНА",
"mainMenu": "В ГЛАВНОЕ МЕНЮ",
"exit": "ВЫХОД"
}

23
lib/l10n/app_zh.arb Normal file
View file

@ -0,0 +1,23 @@
{
"@@locale": "zh",
"appTitle": "TetraQ",
"welcomeTitle": "欢迎来到 TETRAQ",
"nameHint": "名字",
"saveAndPlay": "保存并开始",
"onlineTitle": "在线匹配",
"onlineSub": "挑战世界",
"cpuTitle": "人机对战",
"cpuSub": "与AI训练",
"localTitle": "本地游戏",
"localSub": "同屏对战",
"leaderboardTitle": "排行榜",
"questsTitle": "任务",
"themesTitle": "主题",
"tutorialTitle": "教程",
"startGame": "开始游戏",
"createMatch": "创建比赛",
"joinMatch": "加入",
"gameOver": "游戏结束",
"mainMenu": "返回主菜单",
"exit": "退出"
}

View file

@ -17,6 +17,12 @@ import '../services/storage_service.dart';
import '../services/multiplayer_service.dart';
import '../core/app_colors.dart';
class CpuMatchSetup {
final int radius;
final ArenaShape shape;
CpuMatchSetup(this.radius, this.shape);
}
class GameController extends ChangeNotifier {
late GameBoard board;
bool isVsCPU = false;
@ -50,6 +56,10 @@ class GameController extends ChangeNotifier {
bool opponentWantsRematch = false;
int lastMatchXP = 0;
bool hasLeveledUp = false;
int newlyReachedLevel = 1;
List<String> unlockedFeatures = [];
bool isSetupPhase = true;
bool myJokerPlaced = false;
bool oppJokerPlaced = false;
@ -61,7 +71,7 @@ class GameController extends ChangeNotifier {
int cpuLevel = 1;
int currentMatchLevel = 1;
int? currentSeed;
AppThemeType _activeTheme = AppThemeType.cyberpunk;
AppThemeType _activeTheme = AppThemeType.doodle;
String onlineHostName = "ROSSO";
String onlineGuestName = "BLU";
@ -72,6 +82,25 @@ class GameController extends ChangeNotifier {
startNewGame(radius);
}
CpuMatchSetup _getSetupForCpuLevel(int level) {
final rand = Random();
if (level == 1) return CpuMatchSetup(3, ArenaShape.classic);
if (level == 2) return CpuMatchSetup(4, ArenaShape.classic);
if (level == 3) return CpuMatchSetup(4, ArenaShape.cross);
if (level == 4) return CpuMatchSetup(4, ArenaShape.donut);
if (level == 5) return CpuMatchSetup(5, ArenaShape.classic);
if (level == 6) return CpuMatchSetup(4, ArenaShape.hourglass);
if (level == 7) return CpuMatchSetup(5, ArenaShape.cross);
if (level == 8) return CpuMatchSetup(5, ArenaShape.donut);
if (level == 9) return CpuMatchSetup(5, ArenaShape.hourglass);
List<ArenaShape> hardShapes = [ArenaShape.classic, ArenaShape.cross, ArenaShape.donut, ArenaShape.hourglass, ArenaShape.chaos];
ArenaShape chosenShape = hardShapes[rand.nextInt(hardShapes.length)];
int chosenRadius = (chosenShape == ArenaShape.chaos) ? (rand.nextInt(2) + 4) : (rand.nextInt(2) + 5);
return CpuMatchSetup(chosenRadius, chosenShape);
}
void startNewGame(int radius, {bool vsCPU = false, bool isOnline = false, String? roomCode, bool isHost = false, ArenaShape shape = ArenaShape.classic, bool timeMode = true}) {
_onlineSubscription?.cancel();
_onlineSubscription = null;
@ -81,6 +110,9 @@ class GameController extends ChangeNotifier {
_hasSavedResult = false;
lastMatchXP = 0;
hasLeveledUp = false;
unlockedFeatures.clear();
myReaction = null;
opponentReaction = null;
_lastOpponentReactionTime = null;
@ -98,10 +130,19 @@ class GameController extends ChangeNotifier {
this.isHost = isHost;
this.isTimeMode = timeMode;
onlineShape = shape;
int finalRadius = radius;
ArenaShape finalShape = shape;
if (this.isVsCPU) {
CpuMatchSetup setup = _getSetupForCpuLevel(cpuLevel);
finalRadius = setup.radius;
finalShape = setup.shape;
}
onlineShape = finalShape;
int levelToUse = isOnline ? (currentMatchLevel == 1 ? 2 : currentMatchLevel) : cpuLevel;
board = GameBoard(radius: radius, level: levelToUse, seed: currentSeed, shape: onlineShape);
board = GameBoard(radius: finalRadius, level: levelToUse, seed: currentSeed, shape: finalShape);
board.currentPlayer = Player.red;
isCPUThinking = false;
@ -114,12 +155,11 @@ class GameController extends ChangeNotifier {
notifyListeners();
}
// --- AGGIUNTA LA COORDINATA Z PER IL 3D ---
void placeJoker(int bx, int by, {int bz = 0}) {
void placeJoker(int bx, int by) {
if (!isSetupPhase) return;
Box? target;
try { target = board.boxes.firstWhere((b) => b.x == bx && b.y == by && b.z == bz); } catch(e) {}
try { target = board.boxes.firstWhere((b) => b.x == bx && b.y == by); } catch(e) {}
if (target == null || target.type == BoxType.invisible || target.hiddenJokerOwner != null) return;
@ -132,7 +172,7 @@ class GameController extends ChangeNotifier {
String prefix = isHost ? 'p1' : 'p2';
FirebaseFirestore.instance.collection('games').doc(roomCode).update({
'${prefix}_joker': {'x': bx, 'y': by, 'z': bz}
'${prefix}_joker': {'x': bx, 'y': by}
});
} else {
target.hiddenJokerOwner = jokerTurn;
@ -286,16 +326,21 @@ class GameController extends ChangeNotifier {
void _handleTimeOut() {
if (!isTimeMode || isSetupPhase) return;
if (isOnline) {
Line randomMove = AIEngine.getBestMove(board, 5);
handleLineTap(randomMove, _activeTheme, forced: true);
} else if (isVsCPU && board.currentPlayer == Player.red) {
Line randomMove = AIEngine.getBestMove(board, cpuLevel);
handleLineTap(randomMove, _activeTheme, forced: true);
} else if (!isVsCPU) {
Line randomMove = AIEngine.getBestMove(board, 5);
handleLineTap(randomMove, _activeTheme, forced: true);
}
// Solo chi deve giocare può subire il timeout (se è online)
if (isOnline && board.currentPlayer != myPlayer) return;
// 1. Raccogliamo TUTTE le linee ancora libere e giocabili
List<Line> availableLines = board.lines.where((l) => l.owner == Player.none && l.isPlayable).toList();
// Sicurezza: se non ci sono mosse, non facciamo nulla
if (availableLines.isEmpty) return;
// 2. Scegliamo una linea in modo PURAMENTE CASUALE (nessuna intelligenza artificiale)
final random = Random();
Line randomMove = availableLines[random.nextInt(availableLines.length)];
// 3. Eseguiamo la mossa forzata
handleLineTap(randomMove, _activeTheme, forced: true);
}
void disconnectOnlineGame() {
@ -361,15 +406,14 @@ class GameController extends ChangeNotifier {
}
if (isSetupPhase) {
// --- SINCRONIZZAZIONE JOLLY IN 3D ---
if (!isHost && data['p1_joker'] != null && !oppJokerPlaced) {
int jx = data['p1_joker']['x']; int jy = data['p1_joker']['y']; int jz = data['p1_joker']['z'] ?? 0;
board.boxes.firstWhere((b) => b.x == jx && b.y == jy && b.z == jz).hiddenJokerOwner = Player.red;
int jx = data['p1_joker']['x']; int jy = data['p1_joker']['y'];
board.boxes.firstWhere((b) => b.x == jx && b.y == jy).hiddenJokerOwner = Player.red;
oppJokerPlaced = true; _checkSetupComplete();
}
if (isHost && data['p2_joker'] != null && !oppJokerPlaced) {
int jx = data['p2_joker']['x']; int jy = data['p2_joker']['y']; int jz = data['p2_joker']['z'] ?? 0;
board.boxes.firstWhere((b) => b.x == jx && b.y == jy && b.z == jz).hiddenJokerOwner = Player.blue;
int jx = data['p2_joker']['x']; int jy = data['p2_joker']['y'];
board.boxes.firstWhere((b) => b.x == jx && b.y == jy).hiddenJokerOwner = Player.blue;
oppJokerPlaced = true; _checkSetupComplete();
}
}
@ -505,6 +549,20 @@ class GameController extends ChangeNotifier {
}
}
List<String> _getUnlocks(int oldLevel, int newLevel) {
List<String> unlocks = [];
for(int i = oldLevel + 1; i <= newLevel; i++) {
if (i == 3) unlocks.add("Tema: Legno & Fiammiferi");
if (i == 7) unlocks.add("Tema: Cyberpunk");
if (i == 10) {
unlocks.add("Tema: 8-Bit Arcade");
unlocks.add("Forma Arena: Caos");
}
if (i == 15) unlocks.add("Tema: Grimorio");
}
return unlocks;
}
void _saveMatchResult() {
if (_hasSavedResult) return;
_hasSavedResult = true;
@ -514,6 +572,8 @@ class GameController extends ChangeNotifier {
String myRealName = StorageService.instance.playerName;
if (myRealName.isEmpty) myRealName = "IO";
int oldLevel = StorageService.instance.playerLevel;
if (isOnline) {
bool isWin = isHost ? board.scoreRed > board.scoreBlue : board.scoreBlue > board.scoreRed;
calculatedXP = isWin ? 20 : (isDraw ? 5 : 2);
@ -521,7 +581,9 @@ class GameController extends ChangeNotifier {
int myScore = isHost ? board.scoreRed : board.scoreBlue;
int oppScore = isHost ? board.scoreBlue : board.scoreRed;
StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: oppName, myScore: myScore, oppScore: oppScore, isOnline: true);
if (isWin) StorageService.instance.updateQuestProgress(0, 1);
} else if (isVsCPU) {
int myScore = board.scoreRed; int cpuScore = board.scoreBlue;
bool isWin = myScore > cpuScore;
@ -543,7 +605,17 @@ class GameController extends ChangeNotifier {
StorageService.instance.updateQuestProgress(2, 1);
}
lastMatchXP = calculatedXP; StorageService.instance.addXP(calculatedXP); notifyListeners();
lastMatchXP = calculatedXP;
StorageService.instance.addXP(calculatedXP);
int newLevel = StorageService.instance.playerLevel;
if (newLevel > oldLevel) {
hasLeveledUp = true;
newlyReachedLevel = newLevel;
unlockedFeatures = _getUnlocks(oldLevel, newLevel);
}
notifyListeners();
}
void increaseLevelAndRestart() {

View file

@ -1,23 +1,44 @@
// ===========================================================================
// FILE: lib/main.dart
// ===========================================================================
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:provider/provider.dart';
import 'core/theme_manager.dart';
import 'logic/game_controller.dart';
import 'ui/home/home_screen.dart';
import 'services/storage_service.dart'; // <-- Importiamo il servizio
import 'services/storage_service.dart';
import 'services/audio_service.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'firebase_options.dart';
import 'package:firebase_app_check/firebase_app_check.dart';
// --- IMPORT PER IL SUPPORTO MULTILINGUA ---
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:tetraq/l10n/app_localizations.dart';
void main() async {
// Assicuriamoci che i motori di Flutter siano pronti
WidgetsFlutterBinding.ensureInitialized();
// 1. Accendiamo Firebase! (Questo ti era sfuggito)
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
// 2. Accendiamo la Memoria Locale!
await FirebaseAppCheck.instance.activate(
androidProvider: kDebugMode ? AndroidProvider.debug : AndroidProvider.playIntegrity,
appleProvider: kDebugMode ? AppleProvider.debug : AppleProvider.deviceCheck,
);
try {
await FirebaseAuth.instance.signInAnonymously();
} catch (e) {
debugPrint("Errore Auth: $e");
}
await StorageService.instance.init();
await AudioService.instance.init();
runApp(
MultiProvider(
@ -42,6 +63,14 @@ class TetraQApp extends StatelessWidget {
fontFamily: 'Roboto',
useMaterial3: true,
),
// --- BIVIO DELLE LINGUE ATTIVATO! ---
// Flutter si occuperà di caricare automaticamente tutte le lingue
// che hai generato tramite lo script.
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
// ------------------------------------
home: const HomeScreen(),
);
}

View file

@ -5,20 +5,18 @@
import 'dart:math';
enum Player { red, blue, none }
enum BoxType { normal, gold, bomb, invisible, swap, ice, multiplier }
// --- AGGIUNTA LA FORMA 3D ---
enum ArenaShape { classic, cross, donut, hourglass, chaos, test, pyramid3D }
enum BoxType { normal, gold, bomb, invisible, swap, ice, multiplier } // Aggiunti ice e multiplier
enum ArenaShape { classic, cross, donut, hourglass, chaos }
class Dot {
final int x;
final int y;
final int z; // --- NUOVO: ALTEZZA 3D ---
Dot(this.x, this.y, {this.z = 0});
Dot(this.x, this.y);
@override
bool operator ==(Object other) => identical(this, other) || other is Dot && runtimeType == other.runtimeType && x == other.x && y == other.y && z == other.z;
bool operator ==(Object other) => identical(this, other) || other is Dot && runtimeType == other.runtimeType && x == other.x && y == other.y;
@override
int get hashCode => x.hashCode ^ y.hashCode ^ z.hashCode;
int get hashCode => x.hashCode ^ y.hashCode;
}
class Line {
@ -26,7 +24,7 @@ class Line {
final Dot p2;
Player owner = Player.none;
bool isPlayable = false;
bool isIceCracked = false;
bool isIceCracked = false; // NUOVO: Stato per il blocco di ghiaccio
Line(this.p1, this.p2);
@ -36,7 +34,6 @@ class Line {
class Box {
final int x;
final int y;
final int z; // --- NUOVO: PIANO DELLA SCATOLA ---
Player owner = Player.none;
late Line top, bottom, left, right;
BoxType type = BoxType.normal;
@ -45,7 +42,7 @@ class Box {
Player? hiddenJokerOwner;
bool isJokerRevealed = false;
Box(this.x, this.y, {this.z = 0});
Box(this.x, this.y);
bool isClosed() {
if (type == BoxType.invisible) return false;
@ -53,13 +50,13 @@ class Box {
}
int getCalculatedValue(Player closer) {
if (hiddenJokerOwner != null) return (closer == hiddenJokerOwner) ? 2 : -1;
if (hiddenJokerOwner != null) {
return (closer == hiddenJokerOwner) ? 2 : -1;
}
if (type == BoxType.gold) return 2;
if (type == BoxType.bomb) return -1;
if (type == BoxType.swap || type == BoxType.ice || type == BoxType.multiplier) return 0;
// --- NUOVO: NELLA PIRAMIDE I PIANI ALTI VALGONO DI PIÙ! ---
return 1 + z;
if (type == BoxType.swap || type == BoxType.ice || type == BoxType.multiplier) return 0; // Il moltiplicatore e il ghiaccio non danno punti base
return 1;
}
}
@ -80,7 +77,10 @@ class GameBoard {
int scoreRed = 0;
int scoreBlue = 0;
bool isGameOver = false;
Line? lastMove;
// Variabili per il Moltiplicatore
bool redHasMultiplier = false;
bool blueHasMultiplier = false;
@ -89,111 +89,205 @@ class GameBoard {
}
void _generateBoard() {
dots.clear(); lines.clear(); boxes.clear(); lastMove = null;
final random = seed != null ? Random(seed) : Random();
int chaosAlgorithm = random.nextInt(5);
if (shape == ArenaShape.pyramid3D) {
// --- LOGICA GENERAZIONE PIRAMIDE 3D ---
columns = 4; rows = 4; // Base 4x4
int maxZ = 4; // 4 Piani (0, 1, 2, 3)
for (int z = 0; z < maxZ; z++) {
int currentLayerSize = columns - z; // Il piano si restringe man mano che sale
for (int y = 0; y < currentLayerSize; y++) {
for (int x = 0; x < currentLayerSize; x++) {
var box = Box(x, y, z: z);
// Più sali, più possibilità ci sono di trovare Oro o Bombe
if (z > 0 && Random().nextDouble() > 0.8) box.type = BoxType.gold;
if (z > 1 && Random().nextDouble() > 0.85) box.type = BoxType.bomb;
boxes.add(box);
Dot tl = _getOrAddDot(x, y, z);
Dot tr = _getOrAddDot(x + 1, y, z);
Dot bl = _getOrAddDot(x, y + 1, z);
Dot br = _getOrAddDot(x + 1, y + 1, z);
box.top = _getOrAddLine(tl, tr); box.bottom = _getOrAddLine(bl, br);
box.left = _getOrAddLine(tl, bl); box.right = _getOrAddLine(tr, br);
}
}
}
_updatePyramidPlayability(); // Blocchiamo i piani superiori!
if (shape == ArenaShape.chaos) {
columns = radius * 2 + 1;
rows = (radius * 3) + 2;
} else {
// (Qui c'è il codice standard 2D che abbiamo lasciato invariato per non rompere nulla)
columns = shape == ArenaShape.chaos ? radius * 2 + 1 : (shape == ArenaShape.test ? 3 : radius * 2 + 1);
rows = shape == ArenaShape.chaos ? (radius * 3) + 2 : (shape == ArenaShape.test ? 3 : radius * 2 + 1);
for (int y = 0; y < rows; y++) {
for (int x = 0; x < columns; x++) {
var box = Box(x, y);
boxes.add(box);
Dot tl = _getOrAddDot(x, y, 0); Dot tr = _getOrAddDot(x + 1, y, 0);
Dot bl = _getOrAddDot(x, y + 1, 0); Dot br = _getOrAddDot(x + 1, y + 1, 0);
box.top = _getOrAddLine(tl, tr); box.bottom = _getOrAddLine(bl, br);
box.left = _getOrAddLine(tl, bl); box.right = _getOrAddLine(tr, br);
box.top.isPlayable = true; box.bottom.isPlayable = true; box.left.isPlayable = true; box.right.isPlayable = true;
}
}
columns = radius * 2 + 1;
rows = radius * 2 + 1;
}
}
// Sblocca i piani superiori solo se il "pavimento" è solido
void _updatePyramidPlayability() {
if (shape != ArenaShape.pyramid3D) return;
for (var box in boxes) {
if (box.z == 0) {
if (box.owner == Player.none) {
box.top.isPlayable = true; box.bottom.isPlayable = true; box.left.isPlayable = true; box.right.isPlayable = true;
}
} else {
// Cerca i 4 quadrati del piano di sotto che lo sorreggono
bool isSupported = true;
for (int dy = 0; dy <= 1; dy++) {
for (int dx = 0; dx <= 1; dx++) {
var supportBox = boxes.where((b) => b.z == box.z - 1 && b.x == box.x + dx && b.y == box.y + dy).firstOrNull;
if (supportBox == null || !supportBox.isClosed()) isSupported = false;
dots.clear();
lines.clear();
boxes.clear();
lastMove = null;
for (int y = 0; y < rows; y++) {
for (int x = 0; x < columns; x++) {
var box = Box(x, y);
bool isVisible = true;
if (shape != ArenaShape.chaos) {
int dx = (x - radius).abs();
int dy = (y - radius).abs();
isVisible = (dx + dy) <= radius;
if (isVisible) {
switch (shape) {
case ArenaShape.cross:
int spessoreBraccio = radius > 3 ? 1 : 0;
if (dx > spessoreBraccio && dy > spessoreBraccio) isVisible = false; break;
case ArenaShape.donut:
int dimensioneBuco = radius > 3 ? 2 : 1;
if ((dx + dy) <= dimensioneBuco) isVisible = false; break;
case ArenaShape.hourglass:
if (dx > dy) isVisible = false;
if (x == radius && y == radius) isVisible = true; break;
default: break;
}
}
} else {
double percentY = y / rows;
if (chaosAlgorithm == 0) {
isVisible = (x % 2 == 0) && (random.nextDouble() > 0.15);
} else if (chaosAlgorithm == 1) {
double chance = 0.2 + (percentY * 0.7);
isVisible = random.nextDouble() < chance;
} else if (chaosAlgorithm == 2) {
int midY = rows ~/ 2;
int distFromCenterY = (y - midY).abs();
int allowedWidth = (distFromCenterY / midY * radius).ceil() + 1;
int dx = (x - radius).abs();
isVisible = dx <= allowedWidth && random.nextDouble() > 0.1;
} else if (chaosAlgorithm == 3) {
isVisible = (y % 2 == 0) ? (x < columns - 1) : (x > 0);
if (random.nextDouble() > 0.8) isVisible = false;
} else if (chaosAlgorithm == 4) {
isVisible = random.nextDouble() > 0.45;
}
if (x == radius && y == rows ~/ 2) isVisible = true;
}
if (box.owner == Player.none) {
box.top.isPlayable = isSupported; box.bottom.isPlayable = isSupported;
box.left.isPlayable = isSupported; box.right.isPlayable = isSupported;
if (!isVisible) {
box.type = BoxType.invisible;
} else if (level > 1) {
double chance = random.nextDouble();
if (chance < 0.08) box.type = BoxType.gold;
else if (chance > 0.92) box.type = BoxType.bomb;
else if (level >= 5 && chance > 0.88 && chance <= 0.92) box.type = BoxType.swap;
else if (level >= 10 && chance > 0.83 && chance <= 0.88) box.type = BoxType.ice; // Nuova Scatola Ghiaccio
else if (level >= 15 && chance > 0.78 && chance <= 0.83) box.type = BoxType.multiplier; // Nuova Scatola x2
}
boxes.add(box);
}
}
for (var box in boxes) {
Dot tl = _getOrAddDot(box.x, box.y);
Dot tr = _getOrAddDot(box.x + 1, box.y);
Dot bl = _getOrAddDot(box.x, box.y + 1);
Dot br = _getOrAddDot(box.x + 1, box.y + 1);
box.top = _getOrAddLine(tl, tr);
box.bottom = _getOrAddLine(bl, br);
box.left = _getOrAddLine(tl, bl);
box.right = _getOrAddLine(tr, br);
if (box.type != BoxType.invisible) {
box.top.isPlayable = true; box.bottom.isPlayable = true;
box.left.isPlayable = true; box.right.isPlayable = true;
}
}
}
Dot _getOrAddDot(int x, int y, int z) {
for (var dot in dots) { if (dot.x == x && dot.y == y && dot.z == z) return dot; }
var newDot = Dot(x, y, z: z); dots.add(newDot); return newDot;
Dot _getOrAddDot(int x, int y) {
for (var dot in dots) { if (dot.x == x && dot.y == y) return dot; }
var newDot = Dot(x, y);
dots.add(newDot); return newDot;
}
Line _getOrAddLine(Dot a, Dot b) {
for (var line in lines) { if (line.connects(a, b)) return line; }
var newLine = Line(a, b); lines.add(newLine); return newLine;
var newLine = Line(a, b);
lines.add(newLine); return newLine;
}
bool playMove(Line lineToPlay, {Player? forcedPlayer}) {
if (isGameOver) return false;
Player playerMakingMove = forcedPlayer ?? currentPlayer;
Line? actualLine;
for (var l in lines) { if (l.connects(lineToPlay.p1, lineToPlay.p2)) { actualLine = l; break; } }
for (var l in lines) {
if (l.connects(lineToPlay.p1, lineToPlay.p2)) { actualLine = l; break; }
}
if (actualLine == null || actualLine.owner != Player.none || !actualLine.isPlayable) return false;
actualLine.owner = playerMakingMove;
lastMove = actualLine;
bool scoredPoint = false;
// --- LOGICA BLOCCO DI GHIACCIO ---
bool closesIce = false;
for (var box in boxes) {
if (box.owner == Player.none && box.isClosed()) {
box.owner = playerMakingMove; scoredPoint = true;
int points = box.getCalculatedValue(playerMakingMove);
if (playerMakingMove == Player.red) scoreRed += points; else scoreBlue += points;
if (box.type == BoxType.ice && box.owner == Player.none) {
int linesCount = 0;
if (box.top.owner != Player.none || box.top == actualLine) linesCount++;
if (box.bottom.owner != Player.none || box.bottom == actualLine) linesCount++;
if (box.left.owner != Player.none || box.left == actualLine) linesCount++;
if (box.right.owner != Player.none || box.right == actualLine) linesCount++;
if (linesCount == 4) closesIce = true;
}
}
if (shape == ArenaShape.pyramid3D) _updatePyramidPlayability(); // Ricalcola chi può giocare sopra!
if (closesIce && !actualLine.isIceCracked) {
actualLine.isIceCracked = true; // Si incrina ma non si chiude!
lastMove = actualLine;
if (forcedPlayer == null) currentPlayer = (currentPlayer == Player.red) ? Player.blue : Player.red;
else currentPlayer = (forcedPlayer == Player.red) ? Player.blue : Player.red;
return true; // Mossa valida, ma turno finito.
}
if (lines.where((l) => l.isPlayable).every((l) => l.owner != Player.none)) isGameOver = true;
if (!scoredPoint && !isGameOver) currentPlayer = (playerMakingMove == Player.red) ? Player.blue : Player.red;
// Mossa normale o secondo colpo al ghiaccio
actualLine.isIceCracked = false;
actualLine.owner = playerMakingMove;
lastMove = actualLine;
bool scoredPoint = false;
bool triggeredSwap = false;
for (var box in boxes) {
if (box.owner == Player.none && box.isClosed()) {
box.owner = playerMakingMove;
scoredPoint = true;
if (box.hiddenJokerOwner != null) box.isJokerRevealed = true;
int points = box.getCalculatedValue(playerMakingMove);
// --- LOGICA MOLTIPLICATORE x2 ---
if (box.type == BoxType.multiplier) {
if (playerMakingMove == Player.red) redHasMultiplier = true;
else blueHasMultiplier = true;
} else if (points != 0) {
// Se la scatola chiusa punti e il giocatore ha un x2 attivo...
if (playerMakingMove == Player.red && redHasMultiplier) {
points *= 2;
redHasMultiplier = false; // Si consuma
} else if (playerMakingMove == Player.blue && blueHasMultiplier) {
points *= 2;
blueHasMultiplier = false; // Si consuma
}
}
if (playerMakingMove == Player.red) { scoreRed += points; }
else { scoreBlue += points; }
if (box.type == BoxType.swap && box.hiddenJokerOwner == null) {
triggeredSwap = true;
}
}
if (box.type == BoxType.invisible && !box.isRevealed) {
if (box.top.owner != Player.none && box.bottom.owner != Player.none &&
box.left.owner != Player.none && box.right.owner != Player.none) {
box.isRevealed = true;
}
}
}
if (triggeredSwap) {
int temp = scoreRed; scoreRed = scoreBlue; scoreBlue = temp;
}
if (lines.where((l) => l.isPlayable).every((l) => l.owner != Player.none)) { isGameOver = true; }
if (forcedPlayer == null) {
if (!scoredPoint && !isGameOver) { currentPlayer = (currentPlayer == Player.red) ? Player.blue : Player.red; }
else if (scoredPoint && !isGameOver) { currentPlayer = playerMakingMove; }
} else {
if (!scoredPoint && !isGameOver) { currentPlayer = (forcedPlayer == Player.red) ? Player.blue : Player.red; }
else { currentPlayer = forcedPlayer; }
}
return true;
}

View file

@ -5,6 +5,7 @@
import 'package:flutter/material.dart';
import 'package:audioplayers/audioplayers.dart';
import '../core/app_colors.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AudioService extends ChangeNotifier {
static final AudioService instance = AudioService._internal();
@ -12,35 +13,104 @@ class AudioService extends ChangeNotifier {
bool isMuted = false;
final AudioPlayer _sfxPlayer = AudioPlayer();
final AudioPlayer _bgmPlayer = AudioPlayer();
void toggleMute() {
AppThemeType _currentTheme = AppThemeType.doodle;
Future<void> init() async {
final prefs = await SharedPreferences.getInstance();
isMuted = prefs.getBool('isMuted') ?? false;
await _bgmPlayer.setReleaseMode(ReleaseMode.loop);
}
void toggleMute() async {
isMuted = !isMuted;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('isMuted', isMuted);
if (isMuted) {
// Se abbiamo appena silenziato, FERMA TUTTO immediatamente.
await _bgmPlayer.pause();
await _sfxPlayer.stop();
} else {
// Se riaccendiamo, fai ripartire la canzone
playBgm(_currentTheme);
}
notifyListeners();
}
Future<void> playBgm(AppThemeType theme) async {
_currentTheme = theme;
await _bgmPlayer.stop();
if (isMuted) return;
String audioPath = '';
switch (theme) {
case AppThemeType.cyberpunk:
audioPath = 'audio/bgm/Cyber_Dystopia.mp3';
break;
case AppThemeType.doodle:
audioPath = 'audio/bgm/Quad_Dreams.mp3';
break;
case AppThemeType.wood:
audioPath = 'audio/bgm/Legno_Canopy.mp3';
break;
case AppThemeType.arcade:
audioPath = 'audio/bgm/8-bit_Prowler.mp3';
break;
case AppThemeType.grimorio:
audioPath = 'audio/bgm/Grimorio_Astral.mp3';
break;
case AppThemeType.music:
audioPath = 'audio/bgm/Music_Loop.mp3'; // <-- DEVI INSERIRE QUESTO FILE IN ASSETS
break;
}
if (audioPath.isNotEmpty) {
try {
await _bgmPlayer.play(AssetSource(audioPath), volume: 0.15);
} catch (e) {
debugPrint("Errore riproduzione BGM: $e");
}
}
}
Future<void> stopBgm() async {
await _bgmPlayer.stop();
}
void playLineSfx(AppThemeType theme) async {
if (isMuted) return;
String file = '';
switch (theme) {
case AppThemeType.minimal:
case AppThemeType.arcade: // Suono secco per l'arcade
case AppThemeType.arcade:
case AppThemeType.music: // Usiamo l'effetto arcade o cyber per la musica
file = 'minimal_line.wav'; break;
case AppThemeType.doodle:
case AppThemeType.wood:
file = 'doodle_line.wav'; break;
case AppThemeType.cyberpunk:
case AppThemeType.grimorio: // Suono etereo per la magia
case AppThemeType.grimorio:
file = 'cyber_line.wav'; break;
}
await _sfxPlayer.play(AssetSource('audio/sfx/$file'));
if (file.isNotEmpty) {
try {
await _sfxPlayer.play(AssetSource('audio/sfx/$file'), volume: 1.0);
} catch (e) {
debugPrint("Errore SFX Linea: $file");
}
}
}
void playBoxSfx(AppThemeType theme) async {
if (isMuted) return;
String file = '';
switch (theme) {
case AppThemeType.minimal:
case AppThemeType.arcade:
case AppThemeType.music:
file = 'minimal_box.wav'; break;
case AppThemeType.doodle:
case AppThemeType.wood:
@ -49,16 +119,27 @@ class AudioService extends ChangeNotifier {
case AppThemeType.grimorio:
file = 'cyber_box.wav'; break;
}
await _sfxPlayer.play(AssetSource('audio/sfx/$file'));
if (file.isNotEmpty) {
try {
await _sfxPlayer.play(AssetSource('audio/sfx/$file'), volume: 1.0);
} catch (e) {
debugPrint("Errore SFX Box: $file");
}
}
}
void playBonusSfx() async {
if (isMuted) return;
await _sfxPlayer.play(AssetSource('audio/sfx/bonus.wav'));
try {
await _sfxPlayer.play(AssetSource('audio/sfx/bonus.wav'), volume: 1.0);
} catch(e) {}
}
void playBombSfx() async {
if (isMuted) return;
await _sfxPlayer.play(AssetSource('audio/sfx/bomb.wav'));
try {
await _sfxPlayer.play(AssetSource('audio/sfx/bomb.wav'), volume: 1.0);
} catch(e) {}
}
}

View file

@ -4,15 +4,17 @@
import 'dart:math';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:share_plus/share_plus.dart';
class MultiplayerService {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final FirebaseAuth _auth = FirebaseAuth.instance;
CollectionReference get _gamesCollection => _firestore.collection('games');
Future<String> createGameRoom(int boardRadius, String hostName, String shapeName, bool isTimeMode) async {
Future<String> createGameRoom(int boardRadius, String hostName, String shapeName, bool isTimeMode, {bool isPublic = true}) async {
String roomCode = _generateRoomCode();
int randomSeed = Random().nextInt(1000000);
@ -25,10 +27,11 @@ class MultiplayerService {
'moves': [],
'seed': randomSeed,
'hostName': hostName,
'hostUid': _auth.currentUser?.uid, // NUOVO: Salviamo l'ID univoco del creatore
'guestName': '',
'shape': shapeName,
'timeMode': isTimeMode,
// Nuovi campi per Emojis e Rivincita
'isPublic': isPublic,
'p1_reaction': null,
'p2_reaction': null,
'p1_rematch': false,
@ -52,8 +55,18 @@ class MultiplayerService {
return null;
}
Stream<QuerySnapshot> getPublicRooms() {
return _gamesCollection
.where('status', isEqualTo: 'waiting')
.where('isPublic', isEqualTo: true)
.snapshots();
}
void shareInviteLink(String roomCode) {
String message = "Ehi! Giochiamo a TetraQ? 🎮\nCopia questo intero messaggio e apri l'app per entrare direttamente, oppure inserisci manualmente il codice: $roomCode";
String message = "Ehi! Giochiamo a TetraQ? 🎮\n\n"
"Clicca su questo link per entrare direttamente in stanza:\n"
"tetraq://join?code=$roomCode\n\n"
"Oppure apri l'app e inserisci manualmente il codice: $roomCode";
Share.share(message);
}
@ -69,7 +82,6 @@ class MultiplayerService {
));
}
// --- NUOVI METODI PER REAZIONI E RIVINCITA ---
Future<void> sendReaction(String roomCode, bool isHost, String reaction) async {
try {
String prefix = isHost ? 'p1' : 'p2';

View file

@ -6,6 +6,8 @@ import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import '../core/app_colors.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart';
class StorageService {
static final StorageService instance = StorageService._internal();
@ -15,10 +17,11 @@ class StorageService {
Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
_checkDailyQuests(); // All'avvio controlliamo se ci sono nuove sfide
_checkDailyQuests();
}
int get savedThemeIndex => _prefs.getInt('theme') ?? AppThemeType.minimal.index;
// Doodle è il nuovo tema di partenza (index 0)
int get savedThemeIndex => _prefs.getInt('theme') ?? AppThemeType.doodle.index;
Future<void> saveTheme(AppThemeType theme) async => await _prefs.setInt('theme', theme.index);
int get savedRadius => _prefs.getInt('radius') ?? 2;
@ -29,7 +32,6 @@ class StorageService {
int get totalXP => _prefs.getInt('totalXP') ?? 0;
// Modificato per sincronizzare automaticamente la classifica su Firebase
Future<void> addXP(int xp) async {
await _prefs.setInt('totalXP', totalXP + xp);
syncLeaderboard();
@ -52,46 +54,44 @@ class StorageService {
String get playerName => _prefs.getString('playerName') ?? '';
Future<void> savePlayerName(String name) async {
await _prefs.setString('playerName', name);
syncLeaderboard(); // Aggiorna il nome in classifica
syncLeaderboard();
}
// --- NUOVO: SINCRONIZZAZIONE CLASSIFICA ONLINE ---
Future<void> syncLeaderboard() async {
if (playerName.isNotEmpty) {
try {
await FirebaseFirestore.instance.collection('leaderboard').doc(playerName).set({
'name': playerName,
'xp': totalXP,
'level': playerLevel,
'wins': wins,
'lastActive': FieldValue.serverTimestamp(),
}, SetOptions(merge: true));
final user = FirebaseAuth.instance.currentUser;
if (user != null) {
await FirebaseFirestore.instance.collection('leaderboard').doc(user.uid).set({
'name': playerName,
'xp': totalXP,
'level': playerLevel,
'wins': wins,
'lastActive': FieldValue.serverTimestamp(),
}, SetOptions(merge: true));
}
} catch(e) {
// Ignoriamo gli errori se manca la rete, si sincronizzerà dopo
debugPrint("Errore sinc. classifica: $e");
}
}
}
// --- NUOVO: GESTIONE SFIDE GIORNALIERE ---
void _checkDailyQuests() {
String today = DateTime.now().toIso8601String().substring(0, 10);
String lastDate = _prefs.getString('quest_date') ?? '';
if (today != lastDate) {
// Nuovo giorno, nuove sfide!
_prefs.setString('quest_date', today);
// Sfida 1: Gioca partite online
_prefs.setInt('q1_type', 0);
_prefs.setInt('q1_prog', 0);
_prefs.setInt('q1_target', 3);
// Sfida 2: Vinci contro la CPU
_prefs.setInt('q2_type', 1);
_prefs.setInt('q2_prog', 0);
_prefs.setInt('q2_target', 2);
// Sfida 3: Partite con forme speciali (Croce, Caos, ecc)
_prefs.setInt('q3_type', 2);
_prefs.setInt('q3_prog', 0);
_prefs.setInt('q3_target', 2);
@ -110,7 +110,6 @@ class StorageService {
}
}
// --- STORICO PARTITE ---
List<Map<String, dynamic>> get matchHistory {
List<String> history = _prefs.getStringList('matchHistory') ?? [];
return history.map((e) => jsonDecode(e) as Map<String, dynamic>).toList();

BIN
lib/ui/.DS_Store vendored Normal file

Binary file not shown.

View file

@ -0,0 +1,128 @@
// ===========================================================================
// FILE: lib/ui/admin/admin_screen.dart
// ===========================================================================
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import '../../core/theme_manager.dart';
class AdminScreen extends StatelessWidget {
const AdminScreen({super.key});
@override
Widget build(BuildContext context) {
final theme = context.watch<ThemeManager>().currentColors;
return Scaffold(
backgroundColor: theme.background,
appBar: AppBar(
title: Text("DASHBOARD ADMIN 🕵️‍♂️", style: TextStyle(color: theme.text, fontWeight: FontWeight.w900, letterSpacing: 2)),
backgroundColor: theme.background,
iconTheme: IconThemeData(color: theme.text),
elevation: 0,
),
body: StreamBuilder<QuerySnapshot>(
// Ordiniamo per Ultimo Accesso, così i giocatori attivi di recente sono in cima!
stream: FirebaseFirestore.instance.collection('leaderboard').orderBy('lastActive', descending: true).snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator(color: theme.playerBlue));
}
if (!snapshot.hasData || snapshot.data!.docs.isEmpty) {
return Center(child: Text("Nessun giocatore trovato nel database.", style: TextStyle(color: theme.text)));
}
var docs = snapshot.data!.docs;
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: docs.length,
itemBuilder: (context, index) {
var data = docs[index].data() as Map<String, dynamic>;
String name = data['name'] ?? 'Fantasma';
int level = data['level'] ?? 1;
int xp = data['xp'] ?? 0;
int wins = data['wins'] ?? 0;
String platform = data['platform'] ?? 'Sconosciuta';
// Formattazione Date (Se esistono)
DateTime? created;
if (data['accountCreated'] != null) created = (data['accountCreated'] as Timestamp).toDate();
DateTime? lastActive;
if (data['lastActive'] != null) lastActive = (data['lastActive'] as Timestamp).toDate();
String createdStr = created != null ? DateFormat('dd MMM yyyy').format(created) : 'N/D';
String lastActiveStr = lastActive != null ? DateFormat('dd MMM yyyy - HH:mm').format(lastActive) : 'N/D';
IconData platformIcon = Icons.device_unknown;
if (platform == 'iOS') platformIcon = Icons.apple;
if (platform == 'Android') platformIcon = Icons.android;
return Card(
color: theme.text.withOpacity(0.05),
elevation: 0,
margin: const EdgeInsets.only(bottom: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
side: BorderSide(color: theme.gridLine.withOpacity(0.3))
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(name, style: TextStyle(color: theme.playerBlue, fontSize: 22, fontWeight: FontWeight.w900)),
Icon(platformIcon, color: theme.text.withOpacity(0.7)),
],
),
const SizedBox(height: 8),
Row(
children: [
Text("Liv. $level", style: TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold, fontSize: 16)),
const SizedBox(width: 15),
Text("$xp XP", style: TextStyle(color: theme.text.withOpacity(0.7))),
const SizedBox(width: 15),
Text("Vittorie: $wins", style: TextStyle(color: Colors.amber.shade700, fontWeight: FontWeight.bold)),
],
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Divider(),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Registrato il:", style: TextStyle(color: theme.text.withOpacity(0.5), fontSize: 10)),
Text(createdStr, style: TextStyle(color: theme.text, fontSize: 12, fontWeight: FontWeight.bold)),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text("Ultimo Accesso:", style: TextStyle(color: theme.text.withOpacity(0.5), fontSize: 10)),
Text(lastActiveStr, style: TextStyle(color: Colors.green, fontSize: 12, fontWeight: FontWeight.bold)),
],
),
],
)
],
),
),
);
},
);
}
),
);
}
}

View file

@ -4,7 +4,6 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import '../../models/game_board.dart';
import '../../core/app_colors.dart';
@ -21,8 +20,6 @@ class BoardPainter extends CustomPainter {
final Player myPlayer;
final Player jokerTurn;
final double cameraAngle; // Angolazione della telecamera a 360 gradi!
BoardPainter({
required this.board,
required this.theme,
@ -32,231 +29,348 @@ class BoardPainter extends CustomPainter {
required this.isSetupPhase,
required this.myPlayer,
required this.jokerTurn,
this.blinkValue = 0.0,
this.cameraAngle = 0.0,
this.blinkValue = 0.0
});
Color _darken(Color c, [double amount = .1]) {
assert(amount >= 0 && amount <= 1);
final hsl = HSLColor.fromColor(c);
final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
return hslDark.toColor();
}
// LA MAGIA: Proiezione Isometrica Ruotabile
Offset projectLogical(double x, double y, double z, Size size) {
if (board.shape != ArenaShape.pyramid3D) {
int gridPoints = board.columns + 1;
double spacing = size.width / gridPoints;
return Offset(x * spacing + (spacing / 2), y * spacing + (spacing / 2));
}
double tileW = size.width / 3.8;
double tileH = tileW * 0.55;
double zHeight = tileW * 0.45; // L'altezza fisica del blocco 3D
// Calcoliamo la posizione tenendo conto del restringimento della piramide (+ z * 0.5)
double actualX = x + z * 0.5 - board.columns / 2.0;
double actualY = y + z * 0.5 - board.rows / 2.0;
// Matrice di rotazione della telecamera (Z-Axis)
double rx = actualX * cos(cameraAngle) - actualY * sin(cameraAngle);
double ry = actualX * sin(cameraAngle) + actualY * cos(cameraAngle);
// Proiezione Isometrica 2D
double sx = (rx - ry) * tileW;
// Il SEGRETO: -z sposta fisicamente in ALTO la faccia del cubo rispetto allo schermo
double sy = (rx + ry) * tileH - (z * zHeight);
return Offset(size.width / 2 + sx, size.height * 0.70 + sy);
}
// Calcola la distanza dalla telecamera per lo Z-Buffering
double getDepth(Box b) {
double actualX = b.x + b.z * 0.5 - board.columns / 2.0;
double actualY = b.y + b.z * 0.5 - board.rows / 2.0;
double rx = actualX * cos(cameraAngle) - actualY * sin(cameraAngle);
double ry = actualX * sin(cameraAngle) + actualY * cos(cameraAngle);
return rx + ry; // Ordina dal più lontano al più vicino
}
@override
void paint(Canvas canvas, Size size) {
if (board.shape == ArenaShape.pyramid3D) _paint3D(canvas, size);
else _paint2D(canvas, size);
}
if (themeType == AppThemeType.doodle) {
final Paint paperGridPaint = Paint()
..color = Colors.grey.withOpacity(0.3)
..strokeWidth = 1.0
..style = PaintingStyle.stroke;
void _paint3D(Canvas canvas, Size size) {
double visualZHeight = (size.width / 3.8) * 0.45;
int maxZ = 4;
Set<Line> drawnLines = {};
// Costruiamo la piramide piano per piano, partendo dal basso
for (int currentZ = 0; currentZ < maxZ; currentZ++) {
var currentLevelBoxes = board.boxes.where((b) => b.z == currentZ && b.type != BoxType.invisible).toList();
// Ordiniamo dal fondo allo schermo
currentLevelBoxes.sort((a, b) => getDepth(a).compareTo(getDepth(b)));
for (var box in currentLevelBoxes) {
bool isPlayable = box.top.isPlayable;
bool isOwned = box.owner != Player.none;
if (!isOwned && !isPlayable) continue;
// Disegniamo prima le LINEE DELLA GRIGLIA relative a questo cubo
void drawLine(Line l, double lx1, double ly1, double lx2, double ly2) {
if (drawnLines.contains(l)) return;
drawnLines.add(l);
if (!l.isPlayable && l.owner == Player.none) return;
Offset pt1 = projectLogical(lx1, ly1, currentZ.toDouble(), size);
Offset pt2 = projectLogical(lx2, ly2, currentZ.toDouble(), size);
if (l.isIceCracked) { _drawCrackedIceLine(canvas, pt1, pt2, blinkValue); return; }
Color lineColor = l.owner == Player.none ? theme.gridLine.withOpacity(0.5) : (l.owner == Player.red ? theme.playerRed : theme.playerBlue);
if (l == board.lastMove && l.owner != Player.none) canvas.drawLine(pt1, pt2, Paint()..color = Colors.white.withOpacity(blinkValue * 0.8)..strokeWidth = 10.0..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4.0));
canvas.drawLine(pt1, pt2, Paint()..color = lineColor..strokeWidth = 4.0..strokeCap = StrokeCap.round);
}
drawLine(box.top, box.x.toDouble(), box.y.toDouble(), box.x + 1.0, box.y.toDouble());
drawLine(box.right, box.x + 1.0, box.y.toDouble(), box.x + 1.0, box.y + 1.0);
drawLine(box.bottom, box.x.toDouble(), box.y + 1.0, box.x + 1.0, box.y + 1.0);
drawLine(box.left, box.x.toDouble(), box.y.toDouble(), box.x.toDouble(), box.y + 1.0);
// Disegniamo i PALLINI
void drawDot(Dot d) {
if (board.lines.any((l) => (l.p1 == d || l.p2 == d) && l.isPlayable)) {
canvas.drawCircle(projectLogical(d.x.toDouble(), d.y.toDouble(), currentZ.toDouble(), size), 5.0, Paint()..color = theme.text.withOpacity(0.8));
}
}
drawDot(box.top.p1); drawDot(box.top.p2); drawDot(box.bottom.p2); drawDot(box.bottom.p1);
// --- IL CUBO SOLIDO ---
if (isOwned) {
// Calcoliamo i 4 vertici del TETTO (Z + 1)
List<Offset> topCorners = [
projectLogical(box.x.toDouble(), box.y.toDouble(), currentZ + 1.0, size),
projectLogical(box.x + 1.0, box.y.toDouble(), currentZ + 1.0, size),
projectLogical(box.x + 1.0, box.y + 1.0, currentZ + 1.0, size),
projectLogical(box.x.toDouble(), box.y + 1.0, currentZ + 1.0, size),
];
// Algoritmo Geometrico Infallibile: troviamo la silhouette
topCorners.sort((a, b) => a.dy.compareTo(b.dy));
Offset screenTop = topCorners[0]; // Il punto più alto sullo schermo
Offset screenBottom = topCorners[3]; // Il punto più basso sullo schermo
Offset screenLeft = topCorners[1].dx < topCorners[2].dx ? topCorners[1] : topCorners[2];
Offset screenRight = topCorners[1].dx > topCorners[2].dx ? topCorners[1] : topCorners[2];
Color baseColor = box.owner == Player.red ? theme.playerRed : theme.playerBlue;
// PARETE SINISTRA: Dal tetto scende verso il basso
Path leftWall = Path()
..moveTo(screenLeft.dx, screenLeft.dy)
..lineTo(screenBottom.dx, screenBottom.dy)
..lineTo(screenBottom.dx, screenBottom.dy + visualZHeight)
..lineTo(screenLeft.dx, screenLeft.dy + visualZHeight)
..close();
canvas.drawPath(leftWall, Paint()..color = _darken(baseColor, 0.15)..style = PaintingStyle.fill);
canvas.drawPath(leftWall, Paint()..color = Colors.black.withOpacity(0.3)..style = PaintingStyle.stroke..strokeWidth = 1.0);
// PARETE DESTRA: Dal tetto scende verso il basso
Path rightWall = Path()
..moveTo(screenBottom.dx, screenBottom.dy)
..lineTo(screenRight.dx, screenRight.dy)
..lineTo(screenRight.dx, screenRight.dy + visualZHeight)
..lineTo(screenBottom.dx, screenBottom.dy + visualZHeight)
..close();
canvas.drawPath(rightWall, Paint()..color = _darken(baseColor, 0.35)..style = PaintingStyle.fill);
canvas.drawPath(rightWall, Paint()..color = Colors.black.withOpacity(0.3)..style = PaintingStyle.stroke..strokeWidth = 1.0);
// IL TETTO: Disegnato per ultimo, copre i muri ed è solido
Path roof = Path()..moveTo(screenTop.dx, screenTop.dy)..lineTo(screenRight.dx, screenRight.dy)..lineTo(screenBottom.dx, screenBottom.dy)..lineTo(screenLeft.dx, screenLeft.dy)..close();
canvas.drawPath(roof, Paint()..color = baseColor..style = PaintingStyle.fill);
canvas.drawPath(roof, Paint()..color = Colors.white.withOpacity(0.5)..style = PaintingStyle.stroke..strokeWidth = 2.0);
_drawBoxIcon(canvas, roof, box);
} else if (isPlayable) {
// PAVIMENTO VUOTO
Offset f0 = projectLogical(box.x.toDouble(), box.y.toDouble(), currentZ.toDouble(), size);
Offset f1 = projectLogical(box.x + 1.0, box.y.toDouble(), currentZ.toDouble(), size);
Offset f2 = projectLogical(box.x + 1.0, box.y + 1.0, currentZ.toDouble(), size);
Offset f3 = projectLogical(box.x.toDouble(), box.y + 1.0, currentZ.toDouble(), size);
Path floor = Path()..moveTo(f0.dx, f0.dy)..lineTo(f1.dx, f1.dy)..lineTo(f2.dx, f2.dy)..lineTo(f3.dx, f3.dy)..close();
canvas.drawPath(floor, Paint()..color = Colors.white.withOpacity(0.08)..style = PaintingStyle.fill);
_drawBoxIcon(canvas, floor, box);
}
double paperStep = 20.0;
for (double i = 0; i <= size.width; i += paperStep) {
canvas.drawLine(Offset(i, 0), Offset(i, size.height), paperGridPaint);
}
for (double i = 0; i <= size.height; i += paperStep) {
canvas.drawLine(Offset(0, i), Offset(size.width, i), paperGridPaint);
}
}
}
void _paint2D(Canvas canvas, Size size) {
int gridPoints = board.columns + 1;
double spacing = size.width / gridPoints;
double offset = spacing / 2;
Offset getScreenPos(int x, int y) => Offset(x * spacing + offset, y * spacing + offset);
// --- NUOVO SFONDO LUMINOSO SAGOMATO (Solo per tema Musica) ---
if (themeType == AppThemeType.music) {
Path arenaShape = Path();
// Uniamo la forma di ogni box giocabile per creare il tappeto
for (var box in board.boxes) {
if (box.type != BoxType.invisible) { // Ignora i buchi
Offset p1 = getScreenPos(box.x, box.y);
Offset p2 = getScreenPos(box.x + 1, box.y + 1);
arenaShape.addRect(Rect.fromPoints(p1, p2));
}
}
// Disegniamo lo sfondo unito, chiaro e con un po' di blur
canvas.drawPath(
arenaShape,
Paint()
..color = Colors.white.withOpacity(0.08) // Colore chiarissimo
..style = PaintingStyle.fill
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 10.0), // Effetto stacco/glow
);
}
// -----------------------------------------------------------
for (var box in board.boxes) {
if (box.type == BoxType.invisible) continue;
Offset p1 = projectLogical(box.top.p1.x.toDouble(), box.top.p1.y.toDouble(), 0, size);
Offset p2 = projectLogical(box.top.p2.x.toDouble(), box.top.p2.y.toDouble(), 0, size);
Offset p3 = projectLogical(box.bottom.p2.x.toDouble(), box.bottom.p2.y.toDouble(), 0, size);
Offset p4 = projectLogical(box.bottom.p1.x.toDouble(), box.bottom.p1.y.toDouble(), 0, size);
Path poly = Path()..moveTo(p1.dx, p1.dy)..lineTo(p2.dx, p2.dy)..lineTo(p3.dx, p3.dy)..lineTo(p4.dx, p4.dy)..close();
Offset p1 = getScreenPos(box.x, box.y);
Offset p2 = getScreenPos(box.x + 1, box.y + 1);
Rect rect = Rect.fromPoints(p1, p2);
if (box.type == BoxType.invisible) {
if (box.isRevealed) {
_drawIconInBox(canvas, rect, ThemeIcons.block(themeType), Colors.grey.shade500);
}
continue;
}
// Sfondo azzurrino se è di ghiaccio (anche prima di chiuderla)
if (box.type == BoxType.ice && box.owner == Player.none) {
canvas.drawRect(rect.deflate(2.0), Paint()..color = Colors.cyanAccent.withOpacity(0.05)..style=PaintingStyle.fill);
}
if (box.owner != Player.none) {
Color c = box.owner == Player.red ? theme.playerRed : theme.playerBlue;
canvas.drawPath(poly, Paint()..color = c.withOpacity(0.85)..style = PaintingStyle.fill);
final boxPaint = Paint()
..style = PaintingStyle.fill
..color = box.owner == Player.red ? theme.playerRed.withOpacity(0.6) : theme.playerBlue.withOpacity(0.6);
if (themeType == AppThemeType.wood) {
_drawFlameBox(canvas, rect, box.owner == Player.red);
} else if (themeType == AppThemeType.doodle) {
Color penColor = box.owner == Player.red ? Colors.redAccent.shade700 : Colors.blueAccent.shade700;
_drawScribbleBox(canvas, rect, penColor);
} else if (themeType == AppThemeType.arcade) {
_drawArcadeBox(canvas, rect, box.owner == Player.red ? theme.playerRed : theme.playerBlue);
} else if (themeType == AppThemeType.grimorio) {
_drawGrimorioBox(canvas, rect, box.owner == Player.red ? theme.playerRed : theme.playerBlue);
} else {
canvas.drawRect(rect, boxPaint);
}
}
if (box.hiddenJokerOwner != null) {
Color jokerColor = box.hiddenJokerOwner == Player.red ? theme.playerRed : theme.playerBlue;
if (box.isJokerRevealed) {
_drawIconInBox(canvas, rect, ThemeIcons.joker(themeType), jokerColor);
} else {
bool canSee = false;
if (isOnline || isVsCPU) {
canSee = box.hiddenJokerOwner == myPlayer;
} else {
canSee = false;
}
if (canSee) {
_drawIconInBox(canvas, rect, ThemeIcons.joker(themeType), jokerColor.withOpacity(0.3));
}
}
}
if (box.type == BoxType.gold) {
_drawIconInBox(canvas, rect, ThemeIcons.gold(themeType), Colors.amber);
} else if (box.type == BoxType.bomb) {
_drawIconInBox(canvas, rect, ThemeIcons.bomb(themeType), themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? Colors.greenAccent : Colors.deepPurple);
} else if (box.type == BoxType.swap) {
_drawIconInBox(canvas, rect, ThemeIcons.swap(themeType), Colors.purpleAccent);
} else if (box.type == BoxType.ice) {
_drawIconInBox(canvas, rect, ThemeIcons.ice(themeType), Colors.cyanAccent);
} else if (box.type == BoxType.multiplier) {
_drawIconInBox(canvas, rect, ThemeIcons.multiplier(themeType), Colors.yellowAccent);
}
_drawBoxIcon(canvas, poly, box);
}
for (var line in board.lines) {
if (!line.isPlayable && line.owner == Player.none) continue;
Offset p1 = projectLogical(line.p1.x.toDouble(), line.p1.y.toDouble(), 0, size);
Offset p2 = projectLogical(line.p2.x.toDouble(), line.p2.y.toDouble(), 0, size);
if (!line.isPlayable) continue;
if (line.isIceCracked) { _drawCrackedIceLine(canvas, p1, p2, blinkValue); continue; }
Color lineColor = line.owner == Player.none ? theme.gridLine.withOpacity(0.4) : (line.owner == Player.red ? theme.playerRed : theme.playerBlue);
if (line == board.lastMove && line.owner != Player.none) canvas.drawLine(p1, p2, Paint()..color = Colors.white.withOpacity(blinkValue * 0.8)..strokeWidth = 12.0..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4.0));
canvas.drawLine(p1, p2, Paint()..color = lineColor..strokeWidth = (line.owner == Player.none ? 3.0 : 6.0)..strokeCap = StrokeCap.round);
Offset p1 = getScreenPos(line.p1.x, line.p1.y);
Offset p2 = getScreenPos(line.p2.x, line.p2.y);
// --- DISEGNO DELLA LINEA "INCRINATA" DAL GHIACCIO ---
if (line.isIceCracked) {
_drawCrackedIceLine(canvas, p1, p2, blinkValue);
continue; // Non ha ancora un proprietario, passiamo alla prossima!
}
bool isLastMove = (line == board.lastMove);
Color lineColor = line.owner == Player.none
? theme.gridLine.withOpacity(0.4)
: (line.owner == Player.red ? theme.playerRed : theme.playerBlue);
if (isLastMove && line.owner != Player.none && themeType != AppThemeType.wood && themeType != AppThemeType.cyberpunk && themeType != AppThemeType.arcade && themeType != AppThemeType.grimorio) {
canvas.drawLine(p1, p2, Paint()..color = Colors.white.withOpacity(blinkValue * 0.5)..strokeWidth = 16.0..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6.0));
}
if (themeType == AppThemeType.wood) {
if (line.owner == Player.none) {
canvas.drawLine(p1, p2, Paint()..color = const Color(0xFF3E2723).withOpacity(0.3)..strokeWidth = 4.5..strokeCap = StrokeCap.round);
} else {
Color headColor = lineColor;
if (isLastMove) headColor = Color.lerp(headColor, Colors.yellow, blinkValue * 0.8) ?? headColor;
_drawRealisticMatch(canvas, p1, p2, headColor, isLastMove: isLastMove, blinkValue: blinkValue);
}
} else if (themeType == AppThemeType.cyberpunk) {
_drawNeonLine(canvas, p1, p2, lineColor, line.owner != Player.none, isLastMove: isLastMove, blinkValue: blinkValue);
} else if (themeType == AppThemeType.doodle) {
Color doodleColor = line.owner == Player.none ? Colors.black.withOpacity(0.05) : lineColor;
if (isLastMove && line.owner != Player.none) doodleColor = Color.lerp(doodleColor, Colors.black, blinkValue * 0.4) ?? doodleColor;
_drawWobblyLine(canvas, p1, p2, doodleColor, line.owner != Player.none, isLastMove: isLastMove, blinkValue: blinkValue);
} else if (themeType == AppThemeType.arcade) {
_drawArcadeLine(canvas, p1, p2, lineColor, line.owner != Player.none, isLastMove: isLastMove, blinkValue: blinkValue);
} else if (themeType == AppThemeType.grimorio) {
_drawGrimorioLine(canvas, p1, p2, lineColor, line.owner != Player.none, isLastMove: isLastMove, blinkValue: blinkValue);
} else if (themeType == AppThemeType.music) {
// Linee nere per la base nel tema musica
if (line.owner == Player.none) lineColor = Colors.black.withOpacity(0.4);
canvas.drawLine(p1, p2, Paint()..color = lineColor..strokeWidth = isLastMove ? 6.0 + (2.0 * blinkValue) : 6.0..strokeCap = StrokeCap.round);
} else {
if (isLastMove && line.owner != Player.none) lineColor = Color.lerp(lineColor, Colors.white, blinkValue * 0.5) ?? lineColor;
canvas.drawLine(p1, p2, Paint()..color = lineColor..strokeWidth = isLastMove ? 6.0 + (2.0 * blinkValue) : 6.0..strokeCap = StrokeCap.round);
}
}
for (var dot in board.dots) {
bool isVisible = board.lines.any((l) => (l.p1 == dot || l.p2 == dot) && l.isPlayable);
if (isVisible) canvas.drawCircle(projectLogical(dot.x.toDouble(), dot.y.toDouble(), 0, size), 4.0, Paint()..color = theme.text.withOpacity(0.8));
final dotPaint = Paint()..style = PaintingStyle.fill;
Set<Dot> activeDots = {};
for (var line in board.lines) {
if (line.isPlayable) {
activeDots.add(line.p1); activeDots.add(line.p2);
}
}
for (var dot in activeDots) {
Offset pos = getScreenPos(dot.x, dot.y);
if (themeType == AppThemeType.wood) {
canvas.drawCircle(pos, 3.5, dotPaint..color = const Color(0xFF3E2723).withOpacity(0.2));
} else if (themeType == AppThemeType.cyberpunk) {
canvas.drawCircle(pos, 6.0, Paint()..color = theme.gridLine.withOpacity(0.3));
canvas.drawCircle(pos, 3.0, Paint()..color = Colors.white.withOpacity(0.5));
} else if (themeType == AppThemeType.doodle) {
canvas.drawRect(Rect.fromCenter(center: pos, width: 4, height: 4), dotPaint..color = Colors.black.withOpacity(0.25));
} else if (themeType == AppThemeType.arcade) {
canvas.drawRect(Rect.fromCenter(center: pos, width: 8, height: 8), dotPaint..color = theme.gridLine.withOpacity(0.9));
canvas.drawRect(Rect.fromCenter(center: pos, width: 4, height: 4), dotPaint..color = theme.background);
} else if (themeType == AppThemeType.grimorio) {
canvas.drawCircle(pos, 6.0, Paint()..color = theme.gridLine.withOpacity(0.3)..maskFilter = const MaskFilter.blur(BlurStyle.normal, 3.0));
Path crystal = Path()..moveTo(pos.dx, pos.dy - 5)..lineTo(pos.dx + 3, pos.dy)..lineTo(pos.dx, pos.dy + 5)..lineTo(pos.dx - 3, pos.dy)..close();
canvas.drawPath(crystal, dotPaint..color = theme.gridLine.withOpacity(0.8));
} else if (themeType == AppThemeType.music) {
// Pallini (dots) neri per staccare dal fondo chiaro
canvas.drawCircle(pos, 4.5, dotPaint..color = Colors.black87);
} else {
canvas.drawCircle(pos, 5.0, dotPaint..color = theme.text.withOpacity(0.6));
}
}
}
void _drawBoxIcon(Canvas canvas, Path face, Box box) {
if (box.type == BoxType.gold) _drawIconOnPath(canvas, face, FontAwesomeIcons.crown, Colors.amber);
else if (box.type == BoxType.bomb) _drawIconOnPath(canvas, face, FontAwesomeIcons.skull, Colors.redAccent);
else if (box.type == BoxType.swap) _drawIconOnPath(canvas, face, FontAwesomeIcons.arrowsRotate, Colors.purpleAccent);
else if (box.type == BoxType.multiplier) _drawIconOnPath(canvas, face, FontAwesomeIcons.bolt, Colors.yellowAccent);
else if (box.type == BoxType.ice && box.owner == Player.none) {
canvas.drawPath(face, Paint()..color = Colors.cyanAccent.withOpacity(0.15)..style = PaintingStyle.fill);
_drawIconOnPath(canvas, face, FontAwesomeIcons.snowflake, Colors.cyanAccent);
}
}
void _drawIconOnPath(Canvas canvas, Path path, IconData icon, Color color) {
Rect bounds = path.getBounds();
TextPainter tp = TextPainter(text: TextSpan(text: String.fromCharCode(icon.codePoint), style: TextStyle(color: color, fontSize: 16, fontFamily: icon.fontFamily, package: icon.fontPackage)), textDirection: TextDirection.ltr)..layout();
tp.paint(canvas, Offset(bounds.center.dx - tp.width / 2, bounds.center.dy - tp.height / 2));
void _drawIconInBox(Canvas canvas, Rect rect, IconData icon, Color color) {
TextPainter textPainter = TextPainter(textDirection: TextDirection.ltr);
textPainter.text = TextSpan(
text: String.fromCharCode(icon.codePoint),
style: TextStyle(
color: themeType == AppThemeType.arcade ? color : color.withOpacity(0.7),
fontSize: rect.width * 0.45,
fontFamily: icon.fontFamily,
package: icon.fontPackage,
shadows: themeType == AppThemeType.arcade ? [] : [Shadow(color: color.withOpacity(0.6), blurRadius: 10, offset: const Offset(0, 0))]
),
);
textPainter.layout();
textPainter.paint(canvas, Offset(rect.center.dx - textPainter.width / 2, rect.center.dy - textPainter.height / 2));
}
void _drawCrackedIceLine(Canvas canvas, Offset p1, Offset p2, double blink) {
Paint crackPaint = Paint()..color = Colors.cyanAccent.withOpacity(0.6 + (0.4 * blink))..strokeWidth = 3.0..style = PaintingStyle.stroke..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.solid, 2.0);
Paint crackPaint = Paint()
..color = Colors.cyanAccent.withOpacity(0.6 + (0.4 * blink))
..strokeWidth = 3.0
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round
..maskFilter = const MaskFilter.blur(BlurStyle.solid, 2.0);
// Effetto linea frammentata
canvas.drawLine(p1, p2, Paint()..color = Colors.cyan.withOpacity(0.2)..strokeWidth=6.0);
double dx = p2.dx - p1.dx; double dy = p2.dy - p1.dy;
double len = sqrt(dx * dx + dy * dy);
if (len == 0) return;
double ndx = dx / len; double ndy = dy / len;
Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy);
double len = dir.length; Vector2 ndir = dir.normalized(); Vector2 perp = Vector2(-ndir.y, ndir.x);
Path crack = Path()..moveTo(p1.dx, p1.dy);
for (int i = 1; i < 6; i++) {
int zigzags = 6;
for (int i=1; i<zigzags; i++) {
double d = len * (i / zigzags);
Offset basePt = Offset(p1.dx + ndir.x * d, p1.dy + ndir.y * d);
double offset = (i % 2 == 0 ? 3.0 : -3.0);
crack.lineTo(p1.dx + ndx * (len * (i / 6)) + (-ndy) * offset, p1.dy + ndy * (len * (i / 6)) + ndx * offset);
crack.lineTo(basePt.dx + perp.x * offset, basePt.dy + perp.y * offset);
}
crack.lineTo(p2.dx, p2.dy);
canvas.drawPath(crack, crackPaint);
}
void _drawArcadeBox(Canvas canvas, Rect rect, Color color) {
double pixelSize = 4.0; Paint paint = Paint()..color = color.withOpacity(0.9)..style = PaintingStyle.fill;
for (double y = rect.top; y < rect.bottom; y += pixelSize) {
for (double x = rect.left; x < rect.right; x += pixelSize) {
int xi = ((x - rect.left) / pixelSize).floor(); int yi = ((y - rect.top) / pixelSize).floor();
if ((xi + yi) % 2 == 0) canvas.drawRect(Rect.fromLTWH(x, y, pixelSize, pixelSize), paint);
}
}
canvas.drawRect(rect.deflate(2.0), Paint()..color = Colors.white.withOpacity(0.4)..style = PaintingStyle.stroke..strokeWidth = 2.0);
}
void _drawGrimorioBox(Canvas canvas, Rect rect, Color color) {
canvas.drawRect(rect, Paint()..color = color.withOpacity(0.15)..style=PaintingStyle.fill);
Offset c = rect.center; double r = rect.width * 0.35;
Paint linePaint = Paint()..color = color.withOpacity(0.8)..style = PaintingStyle.stroke..strokeWidth = 1.5..maskFilter = const MaskFilter.blur(BlurStyle.solid, 1.0);
canvas.drawCircle(c, r, linePaint); canvas.drawCircle(c, r * 0.8, linePaint..strokeWidth = 0.5);
Path p = Path();
for(int i=0; i<3; i++) {
double a = -pi/2 + i * 2*pi/3; Offset pt = Offset(c.dx + r*cos(a), c.dy + r*sin(a));
if(i==0) p.moveTo(pt.dx, pt.dy); else p.lineTo(pt.dx, pt.dy);
}
p.close(); canvas.drawPath(p, linePaint..strokeWidth = 1.0);
}
void _drawArcadeLine(Canvas canvas, Offset p1, Offset p2, Color color, bool isConquered, {bool isLastMove = false, double blinkValue = 0.0}) {
double pixelSize = 6.0; Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy); double len = dir.length; Vector2 ndir = dir.normalized();
Paint paint = Paint()..color = isConquered ? color : color.withOpacity(0.15)..style = PaintingStyle.fill;
Paint highlight = Paint()..color = Colors.white.withOpacity(0.6)..style = PaintingStyle.fill;
for(double d = 0; d <= len; d += pixelSize + 1.0) {
Offset pt = Offset(p1.dx + ndir.x * d, p1.dy + ndir.y * d);
canvas.drawRect(Rect.fromCenter(center: pt, width: pixelSize, height: pixelSize), paint);
if (isConquered && (d / (pixelSize+1.0)).floor() % 3 == 0) canvas.drawRect(Rect.fromCenter(center: pt - const Offset(1,1), width: pixelSize*0.4, height: pixelSize*0.4), highlight);
}
if (isLastMove && isConquered) canvas.drawRect(Rect.fromPoints(p1, p2).inflate(4.0), Paint()..color = Colors.white.withOpacity(blinkValue*0.4)..style=PaintingStyle.stroke..strokeWidth=2.0);
}
void _drawGrimorioLine(Canvas canvas, Offset p1, Offset p2, Color color, bool isConquered, {bool isLastMove = false, double blinkValue = 0.0}) {
if (!isConquered) { canvas.drawLine(p1, p2, Paint()..color = color.withOpacity(0.15)..strokeWidth = 2.0..strokeCap = StrokeCap.round); return; }
canvas.drawLine(p1, p2, Paint()..color = color.withOpacity(0.6)..strokeWidth = 5.0..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4.0));
canvas.drawLine(p1, p2, Paint()..color = Colors.white.withOpacity(0.7)..strokeWidth = 1.5..strokeCap = StrokeCap.round);
int seed = (p1.dx * 1000 + p1.dy).toInt(); Random rand = Random(seed);
Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy); double len = dir.length; Vector2 ndir = dir.normalized(); Vector2 perp = Vector2(-ndir.y, ndir.x);
Path thread1 = Path(); Path thread2 = Path(); int segments = 15; double step = len / segments;
double phaseOffset = (isLastMove ? blinkValue * pi * 4 : 0) + rand.nextDouble()*pi;
for(int i = 0; i <= segments; i++) {
double d = i * step; Offset basePt = Offset(p1.dx + ndir.x * d, p1.dy + ndir.y * d);
double amplitude = 3.5; double wave1 = sin(d * 0.15 + phaseOffset) * amplitude; double wave2 = cos(d * 0.15 + phaseOffset) * amplitude;
Offset pt1 = basePt + Offset(perp.x * wave1, perp.y * wave1); Offset pt2 = basePt + Offset(perp.x * wave2, perp.y * wave2);
if (i == 0) { thread1.moveTo(pt1.dx, pt1.dy); thread2.moveTo(pt2.dx, pt2.dy); } else { thread1.lineTo(pt1.dx, pt1.dy); thread2.lineTo(pt2.dx, pt2.dy); }
}
Paint threadPaint = Paint()..color = color.withOpacity(0.9)..style = PaintingStyle.stroke..strokeWidth = 1.5..maskFilter = const MaskFilter.blur(BlurStyle.solid, 1.0);
canvas.drawPath(thread1, threadPaint); canvas.drawPath(thread2, threadPaint..color = Colors.white.withOpacity(0.5));
}
void _drawFlameBox(Canvas canvas, Rect baseRect, bool isRed) {
final rand = Random((baseRect.left + baseRect.top).toInt());
Offset center = baseRect.center; double w = baseRect.width * 0.35; double h = baseRect.height * 0.55; Offset bottomCenter = Offset(center.dx, center.dy + h * 0.5);
Color outerColor = isRed ? Colors.red.shade600.withOpacity(0.85) : Colors.blue.shade700.withOpacity(0.85); Color midColor = isRed ? Colors.orangeAccent : Colors.lightBlueAccent; Color coreColor = isRed ? Colors.yellowAccent : Colors.white;
canvas.drawOval(Rect.fromCenter(center: bottomCenter, width: w * 1.5, height: w * 0.5), Paint()..color = Colors.black.withOpacity(0.4)..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4.0));
void drawFlameLayer(double scale, Color color, double tipOffsetX) {
Path path = Path(); double fw = w * scale; double fh = h * scale;
path.moveTo(bottomCenter.dx, bottomCenter.dy); path.cubicTo(bottomCenter.dx + fw, bottomCenter.dy, bottomCenter.dx + fw * 0.8, bottomCenter.dy - fh * 0.6, bottomCenter.dx + tipOffsetX, bottomCenter.dy - fh); path.cubicTo(bottomCenter.dx - fw * 0.8, bottomCenter.dy - fh * 0.6, bottomCenter.dx - fw, bottomCenter.dy, bottomCenter.dx, bottomCenter.dy);
canvas.drawPath(path, Paint()..color = color..style = PaintingStyle.fill..maskFilter = const MaskFilter.blur(BlurStyle.normal, 1.5));
}
double randomTipX = (rand.nextDouble() - 0.5) * w * 0.8; drawFlameLayer(1.0, outerColor, randomTipX); drawFlameLayer(0.65, midColor.withOpacity(0.9), randomTipX * 0.6); drawFlameLayer(0.35, coreColor.withOpacity(0.9), randomTipX * 0.2);
}
void _drawScribbleBox(Canvas canvas, Rect baseRect, Color color) {
final rand = Random((baseRect.left + baseRect.top).toInt());
final paint = Paint()..color = color.withOpacity(0.85)..style = PaintingStyle.stroke..strokeWidth = 3.5..strokeCap = StrokeCap.round..strokeJoin = StrokeJoin.round;
final path = Path(); Rect rect = baseRect.deflate(4.0); int numZigs = 15 + rand.nextInt(6); double stepY = rect.height / numZigs;
path.moveTo(rect.left + rand.nextDouble() * 5, rect.top + rand.nextDouble() * 5);
for (int i = 1; i <= numZigs; i++) { double targetX = (i % 2 != 0) ? rect.right + (rand.nextDouble() * 4 - 2) : rect.left + (rand.nextDouble() * 4 - 2); double targetY = rect.top + stepY * i + (rand.nextDouble() - 0.5) * 3; double ctrlX = rect.center.dx + (rand.nextDouble() - 0.5) * 20; double ctrlY = targetY - stepY / 2; path.quadraticBezierTo(ctrlX, ctrlY, targetX, targetY); }
canvas.drawPath(path, paint);
}
void _drawRealisticMatch(Canvas canvas, Offset p1, Offset p2, Color headColor, {bool isLastMove = false, double blinkValue = 0.0}) {
int seed = (p1.dx * 1000 + p1.dy).toInt(); Random rand = Random(seed); Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy).normalized(); double shrink = 8.0; Offset start = Offset(p1.dx + dir.x * shrink, p1.dy + dir.y * shrink); Offset end = Offset(p2.dx - dir.x * shrink, p2.dy - dir.y * shrink); start += Offset(rand.nextDouble() * 4 - 2, rand.nextDouble() * 4 - 2); end += Offset(rand.nextDouble() * 4 - 2, rand.nextDouble() * 4 - 2); bool headAtEnd = rand.nextBool(); Offset headPos = headAtEnd ? end : start; Offset tailPos = headAtEnd ? start : end; Vector2 matchDir = Vector2(headPos.dx - tailPos.dx, headPos.dy - tailPos.dy).normalized();
canvas.drawLine(tailPos + const Offset(4, 4), headPos + const Offset(4, 4), Paint()..color = Colors.black.withOpacity(0.6)..strokeWidth = 7.0..strokeCap = StrokeCap.round);
if (isLastMove) { canvas.drawCircle(headPos, 8.0 + (blinkValue * 6.0), Paint()..color = Colors.orangeAccent.withOpacity(0.6 * blinkValue)..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6.0)); }
canvas.drawLine(tailPos, headPos, Paint()..color = const Color(0xFF6D4C41)..strokeWidth = 7.0..strokeCap = StrokeCap.round); canvas.drawLine(tailPos, headPos, Paint()..color = const Color(0xFFEDC498)..strokeWidth = 4.0..strokeCap = StrokeCap.round); Offset burnPos = Offset(headPos.dx - matchDir.x * 8, headPos.dy - matchDir.y * 8); canvas.drawLine(burnPos, headPos, Paint()..color = const Color(0xFF2E1A14)..strokeWidth = 6.0..strokeCap = StrokeCap.round);
canvas.save(); canvas.translate(headPos.dx, headPos.dy); double angle = atan2(matchDir.y, matchDir.x); canvas.rotate(angle); Rect headOval = Rect.fromCenter(center: Offset.zero, width: 18.0, height: 13.0); canvas.drawOval(headOval.shift(const Offset(1, 2)), Paint()..color = Colors.black.withOpacity(0.6)); canvas.drawOval(headOval, Paint()..color = headColor); canvas.restore();
}
void _drawNeonLine(Canvas canvas, Offset p1, Offset p2, Color color, bool isConquered, {bool isLastMove = false, double blinkValue = 0.0}) {
double mainWidth = isConquered ? (isLastMove ? 6.0 + (blinkValue * 3.0) : 6.0) : 3.0; Color coreColor = isConquered ? (isLastMove ? Color.lerp(Colors.white, color, 1.0 - blinkValue)! : Colors.white.withOpacity(0.9)) : color.withOpacity(0.6);
canvas.drawLine(p1, p2, Paint()..color = color.withOpacity(isConquered ? (isLastMove ? 0.4 + (0.4 * blinkValue) : 0.4) : 0.2)..strokeWidth = mainWidth * 4..strokeCap = StrokeCap.round..maskFilter = MaskFilter.blur(BlurStyle.normal, isConquered ? 12.0 : 6.0));
if (isConquered) { canvas.drawLine(p1, p2, Paint()..color = color.withOpacity(isLastMove ? 0.7 + (0.3 * blinkValue) : 0.7)..strokeWidth = mainWidth * 2..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6.0)); }
canvas.drawLine(p1, p2, Paint()..color = coreColor..strokeWidth = mainWidth..strokeCap = StrokeCap.round);
}
void _drawWobblyLine(Canvas canvas, Offset p1, Offset p2, Color color, bool isConquered, {bool isLastMove = false, double blinkValue = 0.0}) {
final random = Random((p1.dx + p1.dy + p2.dx + p2.dy).toInt()); final dx = p2.dx - p1.dx; final dy = p2.dy - p1.dy;
double strokeW = isConquered ? (isLastMove ? 4.5 + (2.0 * blinkValue) : 4.5) : 2.0;
final basePaint = Paint()..color = color..strokeWidth = strokeW..style = PaintingStyle.stroke..strokeCap = StrokeCap.round;
final mid1 = Offset(p1.dx + dx / 2 + (random.nextDouble() - 0.5) * 8, p1.dy + dy / 2 + (random.nextDouble() - 0.5) * 8); canvas.drawPath(Path()..moveTo(p1.dx, p1.dy)..quadraticBezierTo(mid1.dx, mid1.dy, p2.dx, p2.dy), basePaint);
final mid2 = Offset(p1.dx + dx / 2 + (random.nextDouble() - 0.5) * 6, p1.dy + dy / 2 + (random.nextDouble() - 0.5) * 6); canvas.drawPath(Path()..moveTo(p1.dx, p1.dy)..quadraticBezierTo(mid2.dx, mid2.dy, p2.dx, p2.dy), basePaint..strokeWidth = strokeW * 0.5..color = color.withOpacity(0.8));
}
@override bool shouldRepaint(covariant BoardPainter oldDelegate) => true;
}
class Vector2 {
final double x, y; Vector2(this.x, this.y); double get length => sqrt(x * x + y * y);
Vector2 normalized() { double l = length; return l == 0 ? Vector2(0, 0) : Vector2(x / l, y / l); }
}

View file

@ -5,15 +5,16 @@
import 'dart:ui';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../../logic/game_controller.dart';
import '../../core/theme_manager.dart';
import '../../core/app_colors.dart';
import '../../models/game_board.dart';
import 'board_painter.dart';
import 'score_board.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../services/storage_service.dart';
TextStyle _getTextStyle(AppThemeType themeType, TextStyle baseStyle) {
if (themeType == AppThemeType.doodle) {
@ -25,6 +26,8 @@ TextStyle _getTextStyle(AppThemeType themeType, TextStyle baseStyle) {
));
} else if (themeType == AppThemeType.grimorio) {
return GoogleFonts.cinzelDecorative(textStyle: baseStyle.copyWith(fontWeight: FontWeight.bold));
} else if (themeType == AppThemeType.music) {
return GoogleFonts.audiowide(textStyle: baseStyle.copyWith(letterSpacing: 1.5));
}
return baseStyle;
}
@ -45,8 +48,6 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
bool _wasSetupPhase = false;
Player _lastJokerTurn = Player.red;
double _cameraAngle = 0.0; // La nostra telecamera fluida a 360 gradi
@override
void initState() {
super.initState();
@ -58,18 +59,30 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
void _showGameOverDialog(BuildContext context, GameController game, ThemeColors theme, AppThemeType themeType) {
_gameOverDialogShown = true;
showDialog(
barrierDismissible: false, context: context,
barrierDismissible: false,
context: context,
builder: (dialogContext) => Consumer<GameController>(
builder: (context, controller, child) {
if (!controller.isGameOver) {
WidgetsBinding.instance.addPostFrameCallback((_) { if (_gameOverDialogShown) { _gameOverDialogShown = false; if (Navigator.canPop(dialogContext)) Navigator.pop(dialogContext); } });
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_gameOverDialogShown) {
_gameOverDialogShown = false;
if (Navigator.canPop(dialogContext)) Navigator.pop(dialogContext);
}
});
return const SizedBox.shrink();
}
int red = controller.board.scoreRed; int blue = controller.board.scoreBlue;
bool playerBeatCPU = controller.isVsCPU && red > blue;
String nameRed = controller.isOnline ? controller.onlineHostName.toUpperCase() : "TU";
String nameBlue = controller.isOnline ? controller.onlineGuestName.toUpperCase() : (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? "VERDE" : "BLU");
String myName = StorageService.instance.playerName.toUpperCase();
if (myName.isEmpty) myName = "TU";
String nameRed = controller.isOnline ? controller.onlineHostName.toUpperCase() : myName;
String nameBlue = controller.isOnline ? controller.onlineGuestName.toUpperCase() : (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? "VERDE" : "BLU");
if (controller.isVsCPU) nameBlue = "CPU";
String winnerText = ""; Color winnerColor = theme.text;
@ -78,7 +91,8 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
else { winnerText = "PAREGGIO!"; winnerColor = theme.text; }
return AlertDialog(
backgroundColor: theme.background, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: BorderSide(color: winnerColor.withOpacity(0.5), width: 2)),
backgroundColor: theme.background,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: BorderSide(color: winnerColor.withOpacity(0.5), width: 2)),
title: Text("FINE PARTITA", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 22))),
content: Column(
mainAxisSize: MainAxisSize.min,
@ -86,49 +100,83 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
Text(winnerText, textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: winnerColor))),
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), decoration: BoxDecoration(color: theme.text.withOpacity(0.05), borderRadius: BorderRadius.circular(15)),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text("$nameRed: $red", style: _getTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerRed))),
Text(" - ", style: _getTextStyle(themeType, TextStyle(fontSize: 18, color: theme.text))),
Text("$nameBlue: $blue", style: _getTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerBlue))),
],
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
decoration: BoxDecoration(color: theme.text.withOpacity(0.05), borderRadius: BorderRadius.circular(15)),
child: FittedBox(
fit: BoxFit.scaleDown,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text("$nameRed: $red", style: _getTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerRed))),
Text(" - ", style: _getTextStyle(themeType, TextStyle(fontSize: 18, color: theme.text))),
Text("$nameBlue: $blue", style: _getTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerBlue))),
],
),
),
),
if (controller.lastMatchXP > 0) ...[
const SizedBox(height: 15),
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
decoration: BoxDecoration(color: Colors.green.withOpacity(0.15), borderRadius: BorderRadius.circular(20), border: Border.all(color: Colors.greenAccent, width: 1.5), boxShadow: themeType == AppThemeType.cyberpunk ? [const BoxShadow(color: Colors.greenAccent, blurRadius: 10, spreadRadius: -5)] : []),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.greenAccent, width: 1.5),
boxShadow: (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) ? [const BoxShadow(color: Colors.greenAccent, blurRadius: 10, spreadRadius: -5)] : [],
),
child: Text("+ ${controller.lastMatchXP} XP", style: _getTextStyle(themeType, const TextStyle(color: Colors.greenAccent, fontWeight: FontWeight.w900, fontSize: 16, letterSpacing: 1.5))),
),
],
if (controller.isVsCPU) ...[
const SizedBox(height: 15),
Text("Difficoltà CPU: Livello ${controller.cpuLevel}", style: _getTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: theme.text.withOpacity(0.7)))),
],
if (controller.isOnline) ...[
const SizedBox(height: 20),
if (controller.rematchRequested && !controller.opponentWantsRematch) Text("In attesa di $nameBlue...", style: _getTextStyle(themeType, const TextStyle(color: Colors.amber, fontWeight: FontWeight.bold, fontStyle: FontStyle.italic))),
if (controller.opponentWantsRematch && !controller.rematchRequested) Text("$nameBlue vuole la rivincita!", style: _getTextStyle(themeType, const TextStyle(color: Colors.greenAccent, fontWeight: FontWeight.bold))),
if (controller.rematchRequested && controller.opponentWantsRematch) Text("Avvio nuova partita...", style: _getTextStyle(themeType, const TextStyle(color: Colors.green, fontWeight: FontWeight.bold))),
if (controller.rematchRequested && !controller.opponentWantsRematch)
Text("In attesa di $nameBlue...", style: _getTextStyle(themeType, const TextStyle(color: Colors.amber, fontWeight: FontWeight.bold, fontStyle: FontStyle.italic))),
if (controller.opponentWantsRematch && !controller.rematchRequested)
Text("$nameBlue vuole la rivincita!", style: _getTextStyle(themeType, const TextStyle(color: Colors.greenAccent, fontWeight: FontWeight.bold))),
if (controller.rematchRequested && controller.opponentWantsRematch)
Text("Avvio nuova partita...", style: _getTextStyle(themeType, const TextStyle(color: Colors.green, fontWeight: FontWeight.bold))),
]
],
),
actionsPadding: const EdgeInsets.only(left: 20, right: 20, bottom: 20, top: 10), actionsAlignment: MainAxisAlignment.center,
actionsPadding: const EdgeInsets.only(left: 20, right: 20, bottom: 20, top: 10),
actionsAlignment: MainAxisAlignment.center,
actions: [
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (playerBeatCPU)
ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: winnerColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5), onPressed: () { controller.increaseLevelAndRestart(); }, child: Text("PROSSIMO LIVELLO ➔", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16))))
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: winnerColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5),
onPressed: () { controller.increaseLevelAndRestart(); },
child: Text("PROSSIMO LIVELLO ➔", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16))),
)
else if (controller.isOnline)
ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: controller.rematchRequested ? Colors.grey : (winnerColor == theme.text ? theme.playerBlue : winnerColor), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5), onPressed: controller.rematchRequested ? null : () { controller.requestRematch(); }, child: Text(controller.opponentWantsRematch ? "ACCETTA RIVINCITA" : "CHIEDI RIVINCITA", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 1.0))))
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: controller.rematchRequested ? Colors.grey : (winnerColor == theme.text ? theme.playerBlue : winnerColor), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5),
onPressed: controller.rematchRequested ? null : () { controller.requestRematch(); },
child: Text(controller.opponentWantsRematch ? "ACCETTA RIVINCITA" : "CHIEDI RIVINCITA", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 1.0))),
)
else
ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: winnerColor == theme.text ? theme.playerBlue : winnerColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5), onPressed: () { controller.startNewGame(controller.board.radius, vsCPU: controller.isVsCPU, shape: controller.board.shape, timeMode: controller.isTimeMode); }, child: Text("RIGIOCA", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 2)))),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: winnerColor == theme.text ? theme.playerBlue : winnerColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5),
onPressed: () { controller.startNewGame(controller.board.radius, vsCPU: controller.isVsCPU, shape: controller.board.shape, timeMode: controller.isTimeMode); },
child: Text("RIGIOCA", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 2))),
),
const SizedBox(height: 12),
OutlinedButton(style: OutlinedButton.styleFrom(foregroundColor: theme.text, side: BorderSide(color: theme.text.withOpacity(0.3), width: 2), padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), onPressed: () { if (controller.isOnline) controller.disconnectOnlineGame(); _gameOverDialogShown = false; Navigator.pop(dialogContext); Navigator.pop(context); }, child: Text("TORNA AL MENU", style: _getTextStyle(themeType, TextStyle(fontWeight: FontWeight.bold, color: theme.text, fontSize: 14, letterSpacing: 1.5)))),
OutlinedButton(
style: OutlinedButton.styleFrom(foregroundColor: theme.text, side: BorderSide(color: theme.text.withOpacity(0.3), width: 2), padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))),
onPressed: () {
if (controller.isOnline) controller.disconnectOnlineGame();
_gameOverDialogShown = false;
Navigator.pop(dialogContext); Navigator.pop(context);
},
child: Text("TORNA AL MENU", style: _getTextStyle(themeType, TextStyle(fontWeight: FontWeight.bold, color: theme.text, fontSize: 14, letterSpacing: 1.5))),
),
],
)
],
@ -139,19 +187,64 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
}
Widget _buildThemedJokerMessage(ThemeColors theme, AppThemeType themeType, GameController gameController) {
String titleText = ""; String subtitleText = "";
if (gameController.isOnline) { titleText = gameController.myJokerPlaced ? "In attesa dell'avversario..." : "Nascondi il tuo Jolly!"; subtitleText = gameController.myJokerPlaced ? "" : "(Tocca qui per nascondere)"; }
else if (gameController.isVsCPU) { titleText = "Nascondi il tuo Jolly!"; subtitleText = "(Tocca qui per nascondere)"; }
else { String pName = gameController.jokerTurn == Player.red ? "ROSSO" : (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? "VERDE" : "BLU"); titleText = "TURNO GIOCATORE $pName"; subtitleText = "Passa il dispositivo.\nL'avversario NON deve guardare!\n\n(Tocca qui quando sei pronto)"; }
String titleText = "";
String subtitleText = "";
Widget content = Padding(padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 25), child: Column(mainAxisSize: MainAxisSize.min, children: [Icon(ThemeIcons.joker(themeType), color: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? Colors.yellowAccent : theme.playerBlue, size: 50), const SizedBox(height: 15), Text(titleText, textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.black87 : theme.text, fontSize: 20, fontWeight: FontWeight.bold))), const SizedBox(height: 25), Text(subtitleText, textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.black54 : theme.text.withOpacity(0.6), fontSize: 12, height: 1.5)))]));
if (gameController.isOnline) {
titleText = gameController.myJokerPlaced ? "In attesa dell'avversario..." : "Nascondi il tuo Jolly!";
subtitleText = gameController.myJokerPlaced ? "" : "(Tocca qui per nascondere)";
} else if (gameController.isVsCPU) {
titleText = "Nascondi il tuo Jolly!";
subtitleText = "(Tocca qui per nascondere)";
} else {
String pName = gameController.jokerTurn == Player.red ? "ROSSO" : (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? "VERDE" : "BLU");
titleText = "TURNO GIOCATORE $pName";
subtitleText = "Passa il dispositivo.\nL'avversario NON deve guardare!\n\n(Tocca qui quando sei pronto)";
}
if (themeType == AppThemeType.cyberpunk) return Container(decoration: BoxDecoration(color: Colors.black.withOpacity(0.9), borderRadius: BorderRadius.circular(20), border: Border.all(color: Colors.yellowAccent, width: 2), boxShadow: [const BoxShadow(color: Colors.yellowAccent, blurRadius: 15, spreadRadius: 0)]), child: content);
else if (themeType == AppThemeType.doodle) return Container(decoration: BoxDecoration(color: const Color(0xFFF9F9F9), borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.black87, width: 3), boxShadow: const [BoxShadow(color: Colors.black26, offset: Offset(6, 6))]), child: content);
else if (themeType == AppThemeType.wood) return Container(decoration: BoxDecoration(color: const Color(0xFF5D4037), borderRadius: BorderRadius.circular(15), border: Border.all(color: const Color(0xFF3E2723), width: 4), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.6), blurRadius: 15, offset: const Offset(0, 8))]), child: content);
else if (themeType == AppThemeType.arcade) return Container(decoration: BoxDecoration(color: Colors.black, borderRadius: BorderRadius.zero, border: Border.all(color: Colors.greenAccent, width: 4)), child: content);
else if (themeType == AppThemeType.grimorio) return Container(decoration: BoxDecoration(color: const Color(0xFF2C1E3D), borderRadius: BorderRadius.circular(30), border: Border.all(color: const Color(0xFFBCAAA4), width: 3), boxShadow: [BoxShadow(color: Colors.deepPurpleAccent.withOpacity(0.5), blurRadius: 20, spreadRadius: 5)]), child: content);
else return Container(decoration: BoxDecoration(color: theme.background, borderRadius: BorderRadius.circular(20), border: Border.all(color: theme.gridLine, width: 2), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.15), blurRadius: 20, offset: const Offset(0, 10))]), child: content);
Widget content = Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 25),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(ThemeIcons.joker(themeType), color: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? Colors.yellowAccent : theme.playerBlue, size: 50),
const SizedBox(height: 15),
Text(
titleText,
textAlign: TextAlign.center,
style: _getTextStyle(themeType, TextStyle(
color: themeType == AppThemeType.doodle ? Colors.black87 : theme.text,
fontSize: 20,
fontWeight: FontWeight.bold,
)),
),
const SizedBox(height: 25),
Text(
subtitleText,
textAlign: TextAlign.center,
style: _getTextStyle(themeType, TextStyle(
color: themeType == AppThemeType.doodle ? Colors.black54 : theme.text.withOpacity(0.6),
fontSize: 12,
height: 1.5
)),
),
],
),
);
if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) {
return Container(decoration: BoxDecoration(color: Colors.black.withOpacity(0.9), borderRadius: BorderRadius.circular(20), border: Border.all(color: Colors.purpleAccent, width: 2), boxShadow: [BoxShadow(color: Colors.purpleAccent.withOpacity(0.6), blurRadius: 15, spreadRadius: 0)]), child: content);
} else if (themeType == AppThemeType.doodle) {
return Container(decoration: BoxDecoration(color: const Color(0xFFF9F9F9), borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.black87, width: 3), boxShadow: const [BoxShadow(color: Colors.black26, offset: Offset(6, 6))]), child: content);
} else if (themeType == AppThemeType.wood) {
return Container(decoration: BoxDecoration(color: const Color(0xFF5D4037), borderRadius: BorderRadius.circular(15), border: Border.all(color: const Color(0xFF3E2723), width: 4), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.6), blurRadius: 15, offset: const Offset(0, 8))]), child: content);
} else if (themeType == AppThemeType.arcade) {
return Container(decoration: BoxDecoration(color: Colors.black, borderRadius: BorderRadius.zero, border: Border.all(color: Colors.greenAccent, width: 4)), child: content);
} else if (themeType == AppThemeType.grimorio) {
return Container(decoration: BoxDecoration(color: const Color(0xFF2C1E3D), borderRadius: BorderRadius.circular(30), border: Border.all(color: const Color(0xFFBCAAA4), width: 3), boxShadow: [BoxShadow(color: Colors.deepPurpleAccent.withOpacity(0.5), blurRadius: 20, spreadRadius: 5)]), child: content);
} else {
return Container(decoration: BoxDecoration(color: theme.background.withOpacity(0.95), borderRadius: BorderRadius.circular(20), border: Border.all(color: theme.gridLine.withOpacity(0.5), width: 2), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.3), blurRadius: 20, offset: const Offset(0, 10))]), child: content);
}
}
@override
@ -161,14 +254,36 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
final theme = themeManager.currentColors;
final gameController = context.watch<GameController>();
if (gameController.isSetupPhase && !_wasSetupPhase) { _hideJokerMessage = false; _lastJokerTurn = Player.red; }
else if (gameController.isSetupPhase && gameController.jokerTurn != _lastJokerTurn) { _hideJokerMessage = false; _lastJokerTurn = gameController.jokerTurn; }
if (gameController.isSetupPhase && !_wasSetupPhase) {
_hideJokerMessage = false;
_lastJokerTurn = Player.red;
} else if (gameController.isSetupPhase && gameController.jokerTurn != _lastJokerTurn) {
_hideJokerMessage = false;
_lastJokerTurn = gameController.jokerTurn;
}
_wasSetupPhase = gameController.isSetupPhase;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (gameController.opponentLeft && !_opponentLeftDialogShown) {
_opponentLeftDialogShown = true;
showDialog(barrierDismissible: false, context: context, builder: (dialogContext) => AlertDialog(backgroundColor: theme.background, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), title: Text("VITTORIA A TAVOLINO!", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold))), content: Text("L'avversario ha abbandonato la stanza.\nSei il vincitore incontestato!", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: theme.text, fontSize: 16))), actionsAlignment: MainAxisAlignment.center, actions: [ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: theme.playerBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), onPressed: () { gameController.disconnectOnlineGame(); Navigator.pop(dialogContext); Navigator.pop(context); }, child: Text("MENU PRINCIPALE", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold))))]));
showDialog(
barrierDismissible: false,
context: context,
builder: (dialogContext) => AlertDialog(
backgroundColor: theme.background,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: Text("VITTORIA A TAVOLINO!", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold))),
content: Text("L'avversario ha abbandonato la stanza.\nSei il vincitore incontestato!", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: theme.text, fontSize: 16))),
actionsAlignment: MainAxisAlignment.center,
actions: [
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: theme.playerBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))),
onPressed: () { gameController.disconnectOnlineGame(); Navigator.pop(dialogContext); Navigator.pop(context); },
child: Text("MENU PRINCIPALE", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold))),
)
],
)
);
} else if (gameController.board.isGameOver && !_gameOverDialogShown) {
_showGameOverDialog(context, gameController, theme, themeType);
}
@ -176,14 +291,30 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
String? bgImage;
if (themeType == AppThemeType.wood) bgImage = 'assets/images/wood_bg.jpg';
if (themeType == AppThemeType.doodle) bgImage = 'assets/images/doodle_bg.jpg';
if (themeType == AppThemeType.doodle) bgImage = 'assets/images/doodle_bg.jpg'; // Questo è lo sfondo carta
if (themeType == AppThemeType.cyberpunk) bgImage = 'assets/images/cyber_bg.jpg';
if (themeType == AppThemeType.music) bgImage = 'assets/images/music_bg.jpg';
Color indicatorColor = themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? Colors.white : Colors.black;
Color indicatorColor = themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? Colors.white : Colors.black;
Widget emojiBar = const SizedBox();
if (gameController.isOnline && !gameController.isGameOver) {
final List<String> emojis = ['😂', '😡', '😱', '🥳', '👀'];
emojiBar = Container(padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), decoration: BoxDecoration(color: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? Colors.black.withOpacity(0.6) : Colors.white.withOpacity(0.8), borderRadius: BorderRadius.circular(30), border: Border.all(color: themeType == AppThemeType.cyberpunk ? theme.playerBlue.withOpacity(0.3) : Colors.black12, width: 2)), child: Row(mainAxisSize: MainAxisSize.min, children: emojis.map((e) => GestureDetector(onTap: () => gameController.sendReaction(e), child: Padding(padding: const EdgeInsets.symmetric(horizontal: 6), child: Text(e, style: const TextStyle(fontSize: 22))))).toList()));
emojiBar = Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
color: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? Colors.black.withOpacity(0.6) : Colors.white.withOpacity(0.8),
borderRadius: BorderRadius.circular(30),
border: Border.all(color: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music ? theme.playerBlue.withOpacity(0.3) : Colors.white24, width: 2),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: emojis.map((e) => GestureDetector(
onTap: () => gameController.sendReaction(e),
child: Padding(padding: const EdgeInsets.symmetric(horizontal: 6), child: Text(e, style: const TextStyle(fontSize: 22))),
)).toList(),
),
);
}
Widget gameContent = SafeArea(
@ -208,31 +339,41 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
return SizedBox(
width: actualWidth, height: actualHeight,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTapUp: (details) => _handleTap(details.localPosition, actualWidth, actualHeight, gameController, themeType),
onPanUpdate: (details) {
if (gameController.board.shape == ArenaShape.pyramid3D) {
setState(() {
_cameraAngle -= details.delta.dx * 0.015;
});
}
},
child: AnimatedBuilder(
animation: _blinkController,
builder: (context, child) {
return CustomPaint(
size: Size(actualWidth, actualHeight),
painter: BoardPainter(
board: gameController.board, theme: theme, themeType: themeType,
blinkValue: _blinkController.value, isOnline: gameController.isOnline,
isVsCPU: gameController.isVsCPU, isSetupPhase: gameController.isSetupPhase,
myPlayer: gameController.myPlayer, jokerTurn: gameController.jokerTurn,
cameraAngle: _cameraAngle,
child: Stack(
children: [
// --- IL VERO SFONDO SFOCATO SAGOMATO ---
if (themeType == AppThemeType.music)
Positioned.fill(
child: ClipPath(
clipper: _ArenaClipper(gameController.board),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0),
child: Container(
color: Colors.white.withOpacity(0.08),
),
),
);
}
),
),
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTapDown: (details) => _handleTap(details.localPosition, actualWidth, actualHeight, gameController, themeType),
child: AnimatedBuilder(
animation: _blinkController,
builder: (context, child) {
return CustomPaint(
size: Size(actualWidth, actualHeight),
painter: BoardPainter(
board: gameController.board, theme: theme, themeType: themeType,
blinkValue: _blinkController.value, isOnline: gameController.isOnline,
isVsCPU: gameController.isVsCPU, isSetupPhase: gameController.isSetupPhase,
myPlayer: gameController.myPlayer, jokerTurn: gameController.jokerTurn,
),
);
}
),
),
],
),
);
}
@ -247,44 +388,118 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (gameController.isVsCPU)
Container(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration(color: indicatorColor.withOpacity(0.1), borderRadius: BorderRadius.circular(20), border: Border.all(color: indicatorColor.withOpacity(0.3))), child: Row(mainAxisSize: MainAxisSize.min, children: [Icon(Icons.smart_toy_rounded, size: 16, color: indicatorColor), const SizedBox(width: 8), Text("LIVELLO CPU: ${gameController.cpuLevel}", style: _getTextStyle(themeType, TextStyle(color: indicatorColor, fontWeight: FontWeight.bold, fontSize: 11, letterSpacing: 1.0)))]))
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(color: indicatorColor.withOpacity(0.1), borderRadius: BorderRadius.circular(20), border: Border.all(color: indicatorColor.withOpacity(0.3))),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.smart_toy_rounded, size: 16, color: indicatorColor), const SizedBox(width: 8),
Text("LIVELLO CPU: ${gameController.cpuLevel}", style: _getTextStyle(themeType, TextStyle(color: indicatorColor, fontWeight: FontWeight.bold, fontSize: 11, letterSpacing: 1.0))),
],
),
)
else
emojiBar,
Container(decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.4), offset: const Offset(0, 4), blurRadius: 5)]), child: TextButton.icon(style: TextButton.styleFrom(backgroundColor: bgImage != null || themeType == AppThemeType.arcade ? Colors.black87 : theme.background, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: BorderSide(color: Colors.white.withOpacity(0.1), width: 1))), icon: Icon(Icons.exit_to_app, color: bgImage != null || themeType == AppThemeType.arcade ? Colors.white : theme.text, size: 20), onPressed: () { gameController.disconnectOnlineGame(); Navigator.pop(context); }, label: Text("ESCI", style: _getTextStyle(themeType, TextStyle(color: bgImage != null || themeType == AppThemeType.arcade ? Colors.white : theme.text, fontWeight: FontWeight.bold, fontSize: 12))))),
Container(
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.4), offset: const Offset(0, 4), blurRadius: 5)]),
child: TextButton.icon(
style: TextButton.styleFrom(backgroundColor: bgImage != null || themeType == AppThemeType.arcade ? Colors.black87 : theme.background, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: BorderSide(color: Colors.white.withOpacity(0.1), width: 1))),
icon: Icon(Icons.exit_to_app, color: bgImage != null || themeType == AppThemeType.arcade ? Colors.white : theme.text, size: 20),
onPressed: () { gameController.disconnectOnlineGame(); Navigator.pop(context); },
label: Text("ESCI", style: _getTextStyle(themeType, TextStyle(color: bgImage != null || themeType == AppThemeType.arcade ? Colors.white : theme.text, fontWeight: FontWeight.bold, fontSize: 12))),
),
),
],
),
)
],
),
if (gameController.board.shape == ArenaShape.pyramid3D)
Positioned(top: 80, left: 0, right: 0, child: Center(child: Text("Scorri in orizzontale per ruotare", style: TextStyle(color: Colors.white.withOpacity(0.5), fontStyle: FontStyle.italic, fontWeight: FontWeight.bold, letterSpacing: 1.5)))),
if (gameController.myReaction != null) Positioned(top: 80, left: gameController.isHost ? 30 : null, right: gameController.isHost ? null : 30, child: _BouncingEmoji(emoji: gameController.myReaction!)),
if (gameController.opponentReaction != null) Positioned(top: 80, left: !gameController.isHost ? 30 : null, right: !gameController.isHost ? null : 30, child: _BouncingEmoji(emoji: gameController.opponentReaction!)),
if (gameController.myReaction != null)
Positioned(top: 80, left: gameController.isHost ? 30 : null, right: gameController.isHost ? null : 30, child: _BouncingEmoji(emoji: gameController.myReaction!)),
if (gameController.opponentReaction != null)
Positioned(top: 80, left: !gameController.isHost ? 30 : null, right: !gameController.isHost ? null : 30, child: _BouncingEmoji(emoji: gameController.opponentReaction!)),
],
),
);
return PopScope(
canPop: true, onPopInvoked: (didPop) { gameController.disconnectOnlineGame(); },
canPop: true,
onPopInvoked: (didPop) { gameController.disconnectOnlineGame(); },
child: Scaffold(
backgroundColor: bgImage != null ? Colors.transparent : theme.background,
body: CustomPaint(
painter: themeType == AppThemeType.minimal ? FullScreenGridPainter(Colors.black.withOpacity(0.06)) : null,
child: Container(
decoration: bgImage != null ? BoxDecoration(image: DecorationImage(image: AssetImage(bgImage), fit: BoxFit.cover, colorFilter: themeType == AppThemeType.doodle ? ColorFilter.mode(Colors.white.withOpacity(0.7), BlendMode.lighten) : null)) : null,
child: Stack(
children: [
if (gameController.isTimeMode && !gameController.isCPUThinking && !gameController.isGameOver && gameController.timeLeft > 0 && gameController.timeLeft <= 5 && !gameController.isSetupPhase) Positioned.fill(child: BlitzBackgroundEffect(timeLeft: gameController.timeLeft, color: theme.playerRed, themeType: themeType)),
if (gameController.effectText.isNotEmpty) Positioned.fill(child: SpecialEventBackgroundEffect(text: gameController.effectText, color: gameController.effectColor, themeType: themeType)),
Positioned.fill(child: gameContent),
if (gameController.isSetupPhase && !_hideJokerMessage) Positioned.fill(child: Container(color: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? Colors.black : theme.background.withOpacity(0.98), child: Center(child: Padding(padding: const EdgeInsets.symmetric(horizontal: 30.0), child: GestureDetector(onTap: () { setState(() { _hideJokerMessage = true; }); }, child: Material(color: Colors.transparent, child: _buildThemedJokerMessage(theme, themeType, gameController))))))),
if (gameController.isGameOver && gameController.board.scoreRed != gameController.board.scoreBlue) Positioned.fill(child: IgnorePointer(child: WinnerVFXOverlay(winnerColor: gameController.board.scoreRed > gameController.board.scoreBlue ? theme.playerRed : theme.playerBlue, themeType: themeType))),
],
),
),
backgroundColor: Colors.transparent, // Assicuriamo che la scaffold base sia trasparente
body: Stack(
children: [
// 1. Sfondo base a tinta unita (In caso non ci sia l'immagine)
Container(color: themeType == AppThemeType.doodle ? Colors.white : theme.background),
// 2. Immagine di Sfondo per tutti i temi che la supportano
if (bgImage != null)
Positioned.fill(
child: Image.asset(
bgImage,
fit: BoxFit.cover,
alignment: Alignment.center,
),
),
// 3. Griglia a righe incrociate per il doodle (Sopra l'immagine di carta)
if (themeType == AppThemeType.doodle)
Positioned.fill(
child: CustomPaint(
painter: FullScreenGridPainter(Colors.blue.withOpacity(0.15)),
),
),
// 4. Patina scura (Cyberpunk e Music) per far risaltare il neon
if (bgImage != null && (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music))
Positioned.fill(
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter, end: Alignment.bottomCenter,
colors: [Colors.black.withOpacity(0.4), Colors.black.withOpacity(0.8)]
)
),
),
),
// 5. Effetto "Furia" o Timeout
if (gameController.isTimeMode && !gameController.isCPUThinking && !gameController.isGameOver && gameController.timeLeft > 0 && gameController.timeLeft <= 5 && !gameController.isSetupPhase)
Positioned.fill(child: BlitzBackgroundEffect(timeLeft: gameController.timeLeft, color: theme.playerRed, themeType: themeType)),
// 6. Testo degli Eventi
if (gameController.effectText.isNotEmpty)
Positioned.fill(child: SpecialEventBackgroundEffect(text: gameController.effectText, color: gameController.effectColor, themeType: themeType)),
// 7. Il Gioco Vero e Proprio
Positioned.fill(child: gameContent),
// 8. Schermata Passaggio Dispositivo
if (gameController.isSetupPhase && !_hideJokerMessage)
Positioned.fill(
child: Container(
color: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music
? Colors.black.withOpacity(0.98)
: theme.background.withOpacity(0.98),
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 30.0),
child: GestureDetector(
onTap: () { setState(() { _hideJokerMessage = true; }); },
child: Material(color: Colors.transparent, child: _buildThemedJokerMessage(theme, themeType, gameController)),
),
),
),
),
),
// 9. Effetti di Vittoria
if (gameController.isGameOver && gameController.board.scoreRed != gameController.board.scoreBlue)
Positioned.fill(child: IgnorePointer(child: WinnerVFXOverlay(winnerColor: gameController.board.scoreRed > gameController.board.scoreBlue ? theme.playerRed : theme.playerBlue, themeType: themeType))),
],
),
),
);
@ -293,48 +508,21 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
void _handleTap(Offset tapPos, double width, double height, GameController controller, AppThemeType themeType) {
final board = controller.board;
if (board.isGameOver) return;
BoardPainter dummyPainter = BoardPainter(
board: board, theme: AppColors.minimal, themeType: AppThemeType.minimal,
isOnline: false, isVsCPU: false, isSetupPhase: false,
myPlayer: Player.red, jokerTurn: Player.red,
cameraAngle: _cameraAngle,
);
int cols = board.columns + 1; double spacing = width / cols; double offset = spacing / 2;
if (controller.isSetupPhase) {
var sortedBoxes = List<Box>.from(board.boxes);
sortedBoxes.sort((a,b) => dummyPainter.getDepth(b).compareTo(dummyPainter.getDepth(a)));
for (var box in sortedBoxes) {
if (box.type == BoxType.invisible) continue;
Offset p0 = dummyPainter.projectLogical(box.x.toDouble(), box.y.toDouble(), box.z.toDouble(), Size(width, height));
Offset p1 = dummyPainter.projectLogical(box.x + 1.0, box.y.toDouble(), box.z.toDouble(), Size(width, height));
Offset p2 = dummyPainter.projectLogical(box.x + 1.0, box.y + 1.0, box.z.toDouble(), Size(width, height));
Offset p3 = dummyPainter.projectLogical(box.x.toDouble(), box.y + 1.0, box.z.toDouble(), Size(width, height));
Path poly = Path()..moveTo(p0.dx, p0.dy)..lineTo(p1.dx, p1.dy)..lineTo(p2.dx, p2.dy)..lineTo(p3.dx, p3.dy)..close();
if (poly.contains(tapPos)) {
controller.placeJoker(box.x, box.y, bz: box.z);
return;
}
}
return;
int bx = ((tapPos.dx - offset) / spacing).floor(); int by = ((tapPos.dy - offset) / spacing).floor();
controller.placeJoker(bx, by); return;
}
Line? closestLine;
double minDistance = double.infinity;
double maxTouchDistance = 40.0;
Line? closestLine; double minDistance = double.infinity; double maxTouchDistance = spacing * 0.4;
for (var line in board.lines) {
if (line.owner != Player.none || !line.isPlayable) continue;
Offset screenP1 = dummyPainter.projectLogical(line.p1.x.toDouble(), line.p1.y.toDouble(), line.p1.z.toDouble(), Size(width, height));
Offset screenP2 = dummyPainter.projectLogical(line.p2.x.toDouble(), line.p2.y.toDouble(), line.p2.z.toDouble(), Size(width, height));
Offset screenP1 = Offset(line.p1.x * spacing + offset, line.p1.y * spacing + offset);
Offset screenP2 = Offset(line.p2.x * spacing + offset, line.p2.y * spacing + offset);
double dist = _distanceToSegment(tapPos, screenP1, screenP2);
if (dist < minDistance && dist < maxTouchDistance) { minDistance = dist; closestLine = line; }
}
if (closestLine != null) { controller.handleLineTap(closestLine, themeType); }
}
@ -347,6 +535,37 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
}
}
// ===========================================================================
// CLIPPER MAGICO: Ritaglia l'effetto sfocatura sull'esatta forma dell'arena
// ===========================================================================
class _ArenaClipper extends CustomClipper<Path> {
final GameBoard board;
_ArenaClipper(this.board);
@override
Path getClip(Size size) {
int cols = board.columns + 1;
double spacing = size.width / cols;
double offset = spacing / 2;
Path path = Path();
for (var box in board.boxes) {
if (box.type != BoxType.invisible) {
path.addRect(Rect.fromLTWH(
box.x * spacing + offset,
box.y * spacing + offset,
spacing,
spacing
));
}
}
return path;
}
@override
bool shouldReclip(covariant _ArenaClipper oldClipper) => true;
}
class _Particle {
double x, y, vx, vy, size, angle, spin;
Color color; int type;
@ -378,7 +597,7 @@ class _WinnerVFXOverlayState extends State<WinnerVFXOverlay> with SingleTickerPr
}
void _initParticles(Size screenSize) {
int particleCount = widget.themeType == AppThemeType.cyberpunk ? 150 : 100;
int particleCount = widget.themeType == AppThemeType.cyberpunk || widget.themeType == AppThemeType.music ? 150 : 100;
if (widget.themeType == AppThemeType.arcade) particleCount = 80;
if (widget.themeType == AppThemeType.grimorio) particleCount = 120;
@ -388,6 +607,7 @@ class _WinnerVFXOverlayState extends State<WinnerVFXOverlay> with SingleTickerPr
else if (widget.themeType == AppThemeType.wood) { palette = [Colors.orangeAccent, Colors.yellow, Colors.red, Colors.white]; }
else if (widget.themeType == AppThemeType.arcade) { palette = [widget.winnerColor, Colors.white, Colors.greenAccent]; }
else if (widget.themeType == AppThemeType.grimorio) { palette = [widget.winnerColor, Colors.deepPurpleAccent, Colors.white]; }
else if (widget.themeType == AppThemeType.music) { palette.add(Colors.pinkAccent); palette.add(Colors.cyanAccent); }
for (int i = 0; i < particleCount; i++) {
double speed = _rand.nextDouble() * 20 + 5;
@ -400,7 +620,7 @@ class _WinnerVFXOverlayState extends State<WinnerVFXOverlay> with SingleTickerPr
setState(() {
for (var p in _particles) {
p.x += p.vx; p.y += p.vy;
if (widget.themeType == AppThemeType.cyberpunk) { p.vy += 0.1; p.vx *= 0.98; p.vy *= 0.98; }
if (widget.themeType == AppThemeType.cyberpunk || widget.themeType == AppThemeType.music) { p.vy += 0.1; p.vx *= 0.98; p.vy *= 0.98; }
else if (widget.themeType == AppThemeType.wood) { p.vy -= 0.2; p.x += math.sin(p.y * 0.05) * 2; }
else if (widget.themeType == AppThemeType.arcade) { p.vy += 0.3; p.spin = 0; p.angle = 0; }
else if (widget.themeType == AppThemeType.grimorio) { p.vy -= 0.1; p.x += math.sin(p.y * 0.02) * 1.5; p.size *= 0.995; }
@ -423,7 +643,7 @@ class _VFXPainter extends CustomPainter {
for (var p in particles) {
if (p.size < 0.5) continue;
final paint = Paint()..color = p.color..style = PaintingStyle.fill;
if (themeType == AppThemeType.cyberpunk) { paint.maskFilter = const MaskFilter.blur(BlurStyle.solid, 4.0); }
if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) { paint.maskFilter = const MaskFilter.blur(BlurStyle.solid, 4.0); }
canvas.save(); canvas.translate(p.x, p.y); canvas.rotate(p.angle);
if (themeType == AppThemeType.doodle) {

View file

@ -4,12 +4,28 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// Import separati e puliti
import 'package:google_fonts/google_fonts.dart';
import '../../logic/game_controller.dart';
import '../../models/game_board.dart';
import '../../core/theme_manager.dart';
import '../../services/audio_service.dart';
import '../../core/app_colors.dart';
import '../../services/storage_service.dart';
import '../home/dialog.dart'; // <--- IMPORTANTE: Importa il TutorialDialog
TextStyle _getTextStyle(AppThemeType themeType, TextStyle baseStyle) {
if (themeType == AppThemeType.doodle) {
return GoogleFonts.permanentMarker(textStyle: baseStyle);
} else if (themeType == AppThemeType.arcade) {
return GoogleFonts.pressStart2p(textStyle: baseStyle.copyWith(
fontSize: baseStyle.fontSize != null ? baseStyle.fontSize! * 0.75 : null,
letterSpacing: 0.5,
));
} else if (themeType == AppThemeType.grimorio) {
return GoogleFonts.cinzelDecorative(textStyle: baseStyle.copyWith(fontWeight: FontWeight.bold));
}
return baseStyle;
}
class ScoreBoard extends StatefulWidget {
const ScoreBoard({super.key});
@ -32,21 +48,24 @@ class _ScoreBoardState extends State<ScoreBoard> {
bool isRedTurn = controller.board.currentPlayer == Player.red;
bool isMuted = AudioService.instance.isMuted;
String nameRed = "ROSSO";
String nameBlue = themeType == AppThemeType.cyberpunk ? "VERDE" : "BLU";
String myName = StorageService.instance.playerName.toUpperCase();
if (myName.isEmpty) myName = "TU";
String nameRed = myName;
String nameBlue = themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? "VERDE" : "BLU";
if (controller.isOnline) {
nameRed = controller.onlineHostName.toUpperCase();
nameBlue = controller.onlineGuestName.toUpperCase();
} else if (controller.isVsCPU) {
nameRed = "TU";
nameRed = myName;
nameBlue = "CPU";
}
return Container(
padding: const EdgeInsets.only(top: 10, bottom: 20, left: 20, right: 20),
decoration: BoxDecoration(
color: theme.background.withOpacity(0.95),
color: themeType == AppThemeType.doodle ? theme.background : theme.background.withOpacity(0.95),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
@ -62,33 +81,84 @@ class _ScoreBoardState extends State<ScoreBoard> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_PlayerScore(color: theme.playerRed, score: redScore, isTurn: isRedTurn, textColor: theme.text, title: nameRed),
_PlayerScore(color: theme.playerRed, score: redScore, isTurn: isRedTurn, textColor: theme.text, title: nameRed, themeType: themeType),
Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"TETRAQ",
style: TextStyle(
style: _getTextStyle(themeType, TextStyle(
fontSize: 24,
fontWeight: FontWeight.w900,
color: theme.text,
letterSpacing: 4,
shadows: [Shadow(color: Colors.black.withOpacity(0.3), offset: const Offset(1, 2), blurRadius: 2)]
)
shadows: themeType == AppThemeType.doodle
? [
// EFFETTO RILIEVO (Luce in alto a sx, ombra in basso a dx)
const Shadow(color: Colors.white, offset: Offset(-1.5, -1.5), blurRadius: 1),
Shadow(color: Colors.black.withOpacity(0.25), offset: const Offset(1.5, 1.5), blurRadius: 2),
]
: [Shadow(color: Colors.black.withOpacity(0.3), offset: const Offset(1, 2), blurRadius: 2)]
))
),
IconButton(
icon: Icon(isMuted ? Icons.volume_off : Icons.volume_up, color: theme.text.withOpacity(0.7)),
onPressed: () {
setState(() {
AudioService.instance.toggleMute();
});
},
const SizedBox(height: 8),
// --- ROW DEI PULSANTI AGGIORNATA ---
Row(
mainAxisSize: MainAxisSize.min,
children: [
// TASTO AUDIO CON CONTORNO
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
setState(() {
AudioService.instance.toggleMute();
});
},
child: Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: themeType == AppThemeType.doodle ? Colors.transparent : theme.text.withOpacity(0.05),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: themeType == AppThemeType.doodle ? const Color(0xFF111122) : theme.text.withOpacity(0.3), width: 1.5),
),
child: Icon(
isMuted ? Icons.volume_off : Icons.volume_up,
color: themeType == AppThemeType.doodle ? const Color(0xFF111122) : theme.text.withOpacity(0.8),
size: 16
),
),
),
const SizedBox(width: 10),
// TASTO INFORMAZIONI (TUTORIAL) CON CONTORNO
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
showDialog(context: context, builder: (ctx) => const TutorialDialog());
},
child: Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: themeType == AppThemeType.doodle ? Colors.transparent : theme.text.withOpacity(0.05),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: themeType == AppThemeType.doodle ? const Color(0xFF111122) : theme.text.withOpacity(0.3), width: 1.5),
),
child: Icon(
Icons.info_outline,
color: themeType == AppThemeType.doodle ? const Color(0xFF111122) : theme.text.withOpacity(0.8),
size: 16
),
),
),
],
),
],
),
_PlayerScore(color: theme.playerBlue, score: blueScore, isTurn: !isRedTurn, textColor: theme.text, title: nameBlue),
_PlayerScore(color: theme.playerBlue, score: blueScore, isTurn: !isRedTurn, textColor: theme.text, title: nameBlue, themeType: themeType),
],
),
);
@ -101,15 +171,16 @@ class _PlayerScore extends StatelessWidget {
final bool isTurn;
final Color textColor;
final String title;
final AppThemeType themeType;
const _PlayerScore({required this.color, required this.score, required this.isTurn, required this.textColor, required this.title});
const _PlayerScore({required this.color, required this.score, required this.isTurn, required this.textColor, required this.title, required this.themeType});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(title, style: TextStyle(fontWeight: FontWeight.bold, color: isTurn ? color : textColor.withOpacity(0.5), fontSize: 12)),
Text(title, style: _getTextStyle(themeType, TextStyle(fontWeight: FontWeight.bold, color: isTurn ? color : textColor.withOpacity(0.5), fontSize: 12))),
const SizedBox(height: 5),
AnimatedContainer(
duration: const Duration(milliseconds: 300),
@ -122,7 +193,7 @@ class _PlayerScore extends StatelessWidget {
BoxShadow(color: color.withOpacity(0.5), offset: const Offset(0, 4), blurRadius: 6)
] : [],
),
child: Text('$score', style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: isTurn ? Colors.white : textColor.withOpacity(0.5))),
child: Text('$score', style: _getTextStyle(themeType, TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: isTurn ? Colors.white : textColor.withOpacity(0.5)))),
),
],
);

375
lib/ui/home/dialog.dart Normal file
View file

@ -0,0 +1,375 @@
// ===========================================================================
// FILE: lib/ui/home/dialogs/dialog.dart
// ===========================================================================
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import '../../../core/theme_manager.dart';
import '../../../core/app_colors.dart';
import '../../../l10n/app_localizations.dart';
import '../../../widgets/painters.dart';
import '../../../widgets/cyber_border.dart';
// ===========================================================================
// 1. DIALOGO MISSIONI (QUESTS)
// ===========================================================================
class QuestsDialog extends StatelessWidget {
const QuestsDialog({super.key});
@override
Widget build(BuildContext context) {
final themeManager = context.watch<ThemeManager>();
final theme = themeManager.currentColors;
final themeType = themeManager.currentThemeType;
final loc = AppLocalizations.of(context)!;
return FutureBuilder<SharedPreferences>(
future: SharedPreferences.getInstance(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const SizedBox();
final prefs = snapshot.data!;
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.all(20),
child: Container(
padding: const EdgeInsets.all(25.0),
decoration: BoxDecoration(
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [theme.background.withOpacity(0.95), theme.background.withOpacity(0.8)]),
borderRadius: BorderRadius.circular(25),
border: Border.all(color: theme.playerBlue.withOpacity(0.5), width: 2),
boxShadow: [BoxShadow(color: theme.playerBlue.withOpacity(0.2), blurRadius: 20, spreadRadius: 5)]
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.assignment_turned_in, size: 50, color: theme.playerBlue),
const SizedBox(height: 10),
Text(loc.questsTitle, style: getSharedTextStyle(themeType, TextStyle(fontSize: 22, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 1.5))),
const SizedBox(height: 25),
...List.generate(3, (index) {
int i = index + 1;
int type = prefs.getInt('q${i}_type') ?? 0;
int prog = prefs.getInt('q${i}_prog') ?? 0;
int target = prefs.getInt('q${i}_target') ?? 1;
String title = "";
IconData icon = Icons.star;
if (type == 0) { title = "Vinci partite Online"; icon = Icons.public; }
else if (type == 1) { title = "Vinci contro la CPU"; icon = Icons.smart_toy; }
else { title = "Gioca in Arene Speciali"; icon = Icons.extension; }
bool completed = prog >= target;
double percent = (prog / target).clamp(0.0, 1.0);
return Container(
margin: const EdgeInsets.only(bottom: 15),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: completed ? Colors.green.withOpacity(0.1) : theme.text.withOpacity(0.05),
borderRadius: BorderRadius.circular(15),
border: Border.all(color: completed ? Colors.green : theme.gridLine.withOpacity(0.3)),
),
child: Row(
children: [
Icon(icon, color: completed ? Colors.green : theme.text.withOpacity(0.6), size: 30),
const SizedBox(width: 15),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: completed ? Colors.green : theme.text))),
const SizedBox(height: 6),
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: LinearProgressIndicator(value: percent, backgroundColor: theme.gridLine.withOpacity(0.2), color: completed ? Colors.green : theme.playerBlue, minHeight: 8),
)
],
),
),
const SizedBox(width: 10),
Text("$prog / $target", style: getSharedTextStyle(themeType, TextStyle(fontWeight: FontWeight.bold, color: theme.text.withOpacity(0.6)))),
],
),
);
}),
const SizedBox(height: 15),
SizedBox(
width: double.infinity, height: 50,
child: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: theme.playerBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))),
onPressed: () => Navigator.pop(context),
child: const Text("CHIUDI", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2)),
),
)
],
),
),
);
}
);
}
}
// ===========================================================================
// 2. DIALOGO CLASSIFICA (LEADERBOARD)
// ===========================================================================
class LeaderboardDialog extends StatelessWidget {
const LeaderboardDialog({super.key});
@override
Widget build(BuildContext context) {
final themeManager = context.watch<ThemeManager>();
final theme = themeManager.currentColors;
final themeType = themeManager.currentThemeType;
final loc = AppLocalizations.of(context)!;
Widget content = Container(
padding: const EdgeInsets.all(20.0),
decoration: BoxDecoration(
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [theme.background.withOpacity(0.95), theme.background.withOpacity(0.8)]),
borderRadius: BorderRadius.circular(25),
border: Border.all(color: Colors.amber.withOpacity(0.8), width: 2),
boxShadow: [BoxShadow(color: Colors.amber.withOpacity(0.2), blurRadius: 20, spreadRadius: 5)]
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.emoji_events, size: 50, color: Colors.amber),
const SizedBox(height: 10),
Text(loc.leaderboardTitle, style: getSharedTextStyle(themeType, TextStyle(fontSize: 20, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 1.5))),
const SizedBox(height: 20),
SizedBox(
height: 350,
child: StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance.collection('leaderboard').orderBy('xp', descending: true).limit(50).snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator(color: theme.playerBlue));
}
if (!snapshot.hasData || snapshot.data!.docs.isEmpty) {
return Center(child: Text("Ancora nessun campione...", style: TextStyle(color: theme.text.withOpacity(0.5))));
}
final docs = snapshot.data!.docs;
return ListView.builder(
physics: const BouncingScrollPhysics(),
itemCount: docs.length,
itemBuilder: (context, index) {
var data = docs[index].data() as Map<String, dynamic>;
String? myUid = FirebaseAuth.instance.currentUser?.uid;
bool isMe = docs[index].id == myUid;
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: isMe ? theme.playerBlue.withOpacity(0.2) : theme.text.withOpacity(0.05),
borderRadius: BorderRadius.circular(10),
border: isMe ? Border.all(color: theme.playerBlue, width: 1.5) : null
),
child: Row(
children: [
Text("#${index + 1}", style: getSharedTextStyle(themeType, TextStyle(fontWeight: FontWeight.w900, color: index == 0 ? Colors.amber : (index == 1 ? Colors.grey.shade400 : (index == 2 ? Colors.brown.shade300 : theme.text.withOpacity(0.5)))))),
const SizedBox(width: 15),
Expanded(child: Text(data['name'] ?? 'Unknown', style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: isMe ? FontWeight.w900 : FontWeight.bold, color: theme.text)))),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text("Lv. ${data['level'] ?? 1}", style: TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold, fontSize: 12)),
Text("${data['xp'] ?? 0} XP", style: TextStyle(color: theme.text.withOpacity(0.6), fontSize: 10)),
],
)
],
),
);
}
);
}
),
),
const SizedBox(height: 15),
SizedBox(
width: double.infinity, height: 50,
child: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.amber.shade700, foregroundColor: Colors.black, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))),
onPressed: () => Navigator.pop(context),
child: const Text("CHIUDI", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2)),
),
)
],
),
);
if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) {
content = AnimatedCyberBorder(child: content);
}
return Dialog(backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.all(20), child: content);
}
}
// ===========================================================================
// 3. DIALOGO TUTORIAL
// ===========================================================================
class TutorialDialog extends StatelessWidget {
const TutorialDialog({super.key});
@override
Widget build(BuildContext context) {
final themeManager = context.watch<ThemeManager>();
final theme = themeManager.currentColors;
final themeType = themeManager.currentThemeType;
Color inkColor = const Color(0xFF111122);
String goldLabel = themeType == AppThemeType.grimorio ? "CORONA:" : "ORO:";
String bombLabel = themeType == AppThemeType.grimorio ? "STREGA:" : "BOMBA:";
String jokerLabel = themeType == AppThemeType.grimorio ? "GIULLARE:" : "JOLLY:";
Widget dialogContent = themeType == AppThemeType.doodle
? Transform.rotate(
angle: -0.01,
child: CustomPaint(
painter: DoodleBackgroundPainter(fillColor: Colors.yellow.shade50, strokeColor: inkColor, seed: 400),
child: Padding(
padding: const EdgeInsets.all(25.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(child: Text("COME GIOCARE", style: getSharedTextStyle(themeType, TextStyle(fontSize: 28, fontWeight: FontWeight.w900, color: inkColor, letterSpacing: 2)))),
const SizedBox(height: 20),
TutorialStep(icon: Icons.line_axis, text: "Chiudi i 4 lati di un quadrato per conquistare 1 punto e avere una mossa extra!", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 15),
TutorialStep(icon: Icons.lens_blur, text: "Ma presta attenzione! Ogni quadrato nasconde un'insidia o un regalo!", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 15),
const Divider(color: Colors.black26, thickness: 2),
const SizedBox(height: 10),
Center(child: Text("GLOSSARIO ARENA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 18, fontWeight: FontWeight.w900, color: inkColor)))),
const SizedBox(height: 10),
TutorialStep(icon: ThemeIcons.gold(themeType), iconColor: Colors.amber.shade700, text: "$goldLabel Chiudilo per ottenere +2 Punti.", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 10),
TutorialStep(icon: ThemeIcons.bomb(themeType), iconColor: Colors.deepPurple, text: "$bombLabel Non chiuderlo! Perderai -1 Punto.", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 10),
TutorialStep(icon: ThemeIcons.swap(themeType), iconColor: Colors.purpleAccent, text: "SCAMBIO: Inverte istantaneamente i punteggi dei giocatori.", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 10),
TutorialStep(icon: ThemeIcons.joker(themeType), iconColor: Colors.green.shade600, text: "$jokerLabel Scegli dove nasconderlo a inizio partita. Se lo chiudi tu +2, se lo chiude l'avversario -1!", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 10),
TutorialStep(icon: ThemeIcons.ice(themeType), iconColor: Colors.cyanAccent, text: "GHIACCIO: Devi cliccarlo due volte per poterlo rompere e chiudere.", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 10),
TutorialStep(icon: ThemeIcons.multiplier(themeType), iconColor: Colors.yellowAccent, text: "x2: Non dà punti, ma raddoppia il punteggio della prossima casella che chiudi!", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 10),
TutorialStep(icon: ThemeIcons.block(themeType), iconColor: Colors.grey, text: "BUCO NERO: Questa casella non esiste. Se la chiudi perdi il turno.", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 25),
Center(
child: GestureDetector(
onTap: () => Navigator.pop(context),
child: CustomPaint(
painter: DoodleBackgroundPainter(fillColor: Colors.red.shade200, strokeColor: inkColor, seed: 401),
child: Container(
height: 50, width: 150, alignment: Alignment.center,
child: Text("HO CAPITO!", style: getSharedTextStyle(themeType, TextStyle(fontSize: 18, fontWeight: FontWeight.w900, color: inkColor))),
),
),
),
)
],
),
),
),
)
: Container(
padding: const EdgeInsets.all(25.0),
decoration: BoxDecoration(
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [theme.background.withOpacity(0.95), theme.background.withOpacity(0.8)]),
borderRadius: BorderRadius.circular(25),
border: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? null : Border.all(color: Colors.white.withOpacity(0.15), width: 1.5),
boxShadow: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? [] : [BoxShadow(color: Colors.black.withOpacity(0.5), blurRadius: 20, offset: const Offset(4, 10))],
),
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(child: Text("COME GIOCARE", style: getSharedTextStyle(themeType, TextStyle(fontSize: 24, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 2)))),
const SizedBox(height: 20),
TutorialStep(icon: Icons.grid_4x4, text: "Chiudi i 4 lati di un quadrato per conquistare 1 punto e avere una mossa extra!", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 15),
TutorialStep(icon: Icons.lens_blur, text: "Ma presta attenzione! Ogni quadrato nasconde un'insidia o un regalo!", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 15),
const Divider(color: Colors.white24, thickness: 1.5),
const SizedBox(height: 10),
Center(child: Text("GLOSSARIO ARENA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.7), letterSpacing: 1.5)))),
const SizedBox(height: 15),
TutorialStep(icon: ThemeIcons.gold(themeType), iconColor: Colors.amber, text: "$goldLabel Chiudilo per ottenere +2 Punti.", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 10),
TutorialStep(icon: ThemeIcons.bomb(themeType), iconColor: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? Colors.greenAccent : Colors.deepPurple, text: "$bombLabel Non chiuderlo! Perderai -1 Punto.", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 10),
TutorialStep(icon: ThemeIcons.swap(themeType), iconColor: Colors.purpleAccent, text: "SCAMBIO: Inverte istantaneamente i punteggi dei giocatori.", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 10),
TutorialStep(icon: ThemeIcons.joker(themeType), iconColor: theme.playerBlue, text: "$jokerLabel Scegli dove nasconderlo a inizio partita. Se lo chiudi tu +2, se lo chiude l'avversario -1!", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 10),
TutorialStep(icon: ThemeIcons.ice(themeType), iconColor: Colors.cyanAccent, text: "GHIACCIO: Devi cliccarlo due volte per poterlo rompere e chiudere.", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 10),
TutorialStep(icon: ThemeIcons.multiplier(themeType), iconColor: Colors.yellowAccent, text: "x2: Non dà punti, ma raddoppia il punteggio della prossima casella che chiudi!", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 10),
TutorialStep(icon: ThemeIcons.block(themeType), iconColor: Colors.grey, text: "BUCO NERO: Questa casella non esiste. Se la chiudi perdi il turno.", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 30),
SizedBox(
width: double.infinity, height: 50,
child: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: theme.playerBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))),
onPressed: () => Navigator.pop(context),
child: const Text("CHIUDI", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2)),
),
)
],
),
),
);
if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) {
dialogContent = AnimatedCyberBorder(child: dialogContent);
}
return Dialog(backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20), child: dialogContent);
}
}
class TutorialStep extends StatelessWidget {
final IconData icon;
final Color? iconColor;
final String text;
final AppThemeType themeType;
final Color inkColor;
final ThemeColors theme;
const TutorialStep({super.key, required this.icon, this.iconColor, required this.text, required this.themeType, required this.inkColor, required this.theme});
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, color: iconColor ?? (themeType == AppThemeType.doodle ? inkColor : theme.playerBlue), size: 28),
const SizedBox(width: 15),
Expanded(
child: Text(text, style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, color: themeType == AppThemeType.doodle ? inkColor : theme.text.withOpacity(0.8), height: 1.3))),
),
],
);
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,12 @@
// ===========================================================================
// FILE: lib/ui/multiplayer/lobby_screen.dart
// ===========================================================================
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'dart:math' as math;
import '../../logic/game_controller.dart';
import '../../models/game_board.dart';
@ -240,14 +245,14 @@ class _NeonTimeSwitch extends StatelessWidget {
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(isTimeMode ? Icons.timer : Icons.timer_off, color: isTimeMode ? Colors.white : doodleColor, size: 24),
const SizedBox(width: 12),
Icon(isTimeMode ? Icons.timer : Icons.timer_off, color: isTimeMode ? Colors.white : doodleColor, size: 20),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(isTimeMode ? 'A TEMPO' : 'RELAX', style: _getTextStyle(themeType, TextStyle(color: isTimeMode ? Colors.white : doodleColor, fontWeight: FontWeight.w900, fontSize: 14, letterSpacing: 2.0))),
Text(isTimeMode ? '15 sec a mossa' : 'Nessun limite', style: _getTextStyle(themeType, TextStyle(color: isTimeMode ? Colors.white : doodleColor.withOpacity(0.8), fontSize: 11, fontWeight: FontWeight.bold))),
Text(isTimeMode ? 'A TEMPO' : 'RELAX', style: _getTextStyle(themeType, TextStyle(color: isTimeMode ? Colors.white : doodleColor, fontWeight: FontWeight.w900, fontSize: 12, letterSpacing: 1.0))),
Text(isTimeMode ? '15s a mossa' : 'Senza limiti', style: _getTextStyle(themeType, TextStyle(color: isTimeMode ? Colors.white : doodleColor.withOpacity(0.8), fontSize: 9, fontWeight: FontWeight.bold))),
],
),
],
@ -284,14 +289,107 @@ class _NeonTimeSwitch extends StatelessWidget {
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(isTimeMode ? Icons.timer : Icons.timer_off, color: isTimeMode ? Colors.amber : theme.text.withOpacity(0.5), size: 24),
const SizedBox(width: 12),
Icon(isTimeMode ? Icons.timer : Icons.timer_off, color: isTimeMode ? Colors.amber : theme.text.withOpacity(0.5), size: 20),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(isTimeMode ? 'A TEMPO' : 'RELAX', style: _getTextStyle(themeType, TextStyle(color: isTimeMode ? Colors.white : theme.text.withOpacity(0.5), fontWeight: FontWeight.w900, fontSize: 13, letterSpacing: 1.5))),
Text(isTimeMode ? '15 sec a mossa' : 'Nessun limite', style: _getTextStyle(themeType, TextStyle(color: isTimeMode ? Colors.amber.shade200 : theme.text.withOpacity(0.4), fontSize: 10, fontWeight: FontWeight.bold))),
Text(isTimeMode ? 'A TEMPO' : 'RELAX', style: _getTextStyle(themeType, TextStyle(color: isTimeMode ? Colors.white : theme.text.withOpacity(0.5), fontWeight: FontWeight.w900, fontSize: 11, letterSpacing: 1.5))),
Text(isTimeMode ? '15s a mossa' : 'Senza limiti', style: _getTextStyle(themeType, TextStyle(color: isTimeMode ? Colors.amber.shade200 : theme.text.withOpacity(0.4), fontSize: 9, fontWeight: FontWeight.bold))),
],
),
],
),
),
);
}
}
class _NeonPrivacySwitch extends StatelessWidget {
final bool isPublic;
final ThemeColors theme;
final AppThemeType themeType;
final VoidCallback onTap;
const _NeonPrivacySwitch({required this.isPublic, required this.theme, required this.themeType, required this.onTap});
@override
Widget build(BuildContext context) {
if (themeType == AppThemeType.doodle) {
Color doodleColor = isPublic ? Colors.green.shade600 : Colors.red.shade600;
return Transform.rotate(
angle: 0.015,
child: GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
transform: Matrix4.translationValues(0, isPublic ? 3 : 0, 0),
decoration: BoxDecoration(
color: isPublic ? doodleColor : Colors.white,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(15), topRight: Radius.circular(8),
bottomLeft: Radius.circular(6), bottomRight: Radius.circular(15),
),
border: Border.all(color: isPublic ? theme.text : doodleColor.withOpacity(0.5), width: 2.5),
boxShadow: [BoxShadow(color: isPublic ? theme.text.withOpacity(0.8) : doodleColor.withOpacity(0.2), offset: const Offset(4, 5), blurRadius: 0)],
),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(isPublic ? Icons.public : Icons.lock, color: isPublic ? Colors.white : doodleColor, size: 20),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(isPublic ? 'PUBBLICA' : 'PRIVATA', style: _getTextStyle(themeType, TextStyle(color: isPublic ? Colors.white : doodleColor, fontWeight: FontWeight.w900, fontSize: 12, letterSpacing: 1.0))),
Text(isPublic ? 'In Bacheca' : 'Solo Codice', style: _getTextStyle(themeType, TextStyle(color: isPublic ? Colors.white : doodleColor.withOpacity(0.8), fontSize: 9, fontWeight: FontWeight.bold))),
],
),
],
),
),
),
);
}
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isPublic
? [Colors.greenAccent.withOpacity(0.25), Colors.greenAccent.withOpacity(0.05)]
: [theme.playerRed.withOpacity(0.25), theme.playerRed.withOpacity(0.05)],
),
border: Border.all(color: isPublic ? Colors.greenAccent : theme.playerRed, width: isPublic ? 2 : 1),
boxShadow: isPublic
? [BoxShadow(color: Colors.greenAccent.withOpacity(0.3), blurRadius: 15, spreadRadius: 2)]
: [
BoxShadow(color: Colors.black.withOpacity(0.4), blurRadius: 6, offset: const Offset(2, 4)),
],
),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(isPublic ? Icons.public : Icons.lock, color: isPublic ? Colors.greenAccent : theme.playerRed, size: 20),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(isPublic ? 'PUBBLICA' : 'PRIVATA', style: _getTextStyle(themeType, TextStyle(color: isPublic ? Colors.white : theme.text.withOpacity(0.8), fontWeight: FontWeight.w900, fontSize: 11, letterSpacing: 1.5))),
Text(isPublic ? 'Tutti ti vedono' : 'Solo con Codice', style: _getTextStyle(themeType, TextStyle(color: isPublic ? Colors.greenAccent.shade200 : theme.playerRed.withOpacity(0.7), fontSize: 9, fontWeight: FontWeight.bold))),
],
),
],
@ -313,7 +411,7 @@ class _NeonActionButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (themeType == AppThemeType.doodle) {
double tilt = (label == "UNISCITI") ? -0.015 : 0.02;
double tilt = (label == "UNISCITI" || label == "ANNULLA") ? -0.015 : 0.02;
return Transform.rotate(
angle: tilt,
child: GestureDetector(
@ -330,7 +428,13 @@ class _NeonActionButton extends StatelessWidget {
boxShadow: [BoxShadow(color: theme.text.withOpacity(0.9), offset: const Offset(4, 4), blurRadius: 0)],
),
child: Center(
child: Text(label, style: _getTextStyle(themeType, const TextStyle(fontSize: 20, fontWeight: FontWeight.w900, letterSpacing: 3.0, color: Colors.white))),
child: FittedBox(
fit: BoxFit.scaleDown,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Text(label, style: _getTextStyle(themeType, const TextStyle(fontSize: 20, fontWeight: FontWeight.w900, letterSpacing: 3.0, color: Colors.white))),
),
),
),
),
),
@ -351,7 +455,13 @@ class _NeonActionButton extends StatelessWidget {
],
),
child: Center(
child: Text(label, style: _getTextStyle(themeType, const TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2.0, color: Colors.white, shadows: [Shadow(color: Colors.black, blurRadius: 2, offset: Offset(1, 1))]))),
child: FittedBox(
fit: BoxFit.scaleDown,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Text(label, style: _getTextStyle(themeType, const TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2.0, color: Colors.white, shadows: [Shadow(color: Colors.black, blurRadius: 2, offset: Offset(1, 1))]))),
),
),
),
),
);
@ -423,7 +533,7 @@ class LobbyScreen extends StatefulWidget {
State<LobbyScreen> createState() => _LobbyScreenState();
}
class _LobbyScreenState extends State<LobbyScreen> {
class _LobbyScreenState extends State<LobbyScreen> with WidgetsBindingObserver {
final MultiplayerService _multiplayerService = MultiplayerService();
late TextEditingController _codeController;
@ -431,13 +541,20 @@ class _LobbyScreenState extends State<LobbyScreen> {
String? _myRoomCode;
String _playerName = '';
// Variabile per gestire l'effetto "sipario"
bool _isCreatingRoom = false;
int _selectedRadius = 4;
ArenaShape _selectedShape = ArenaShape.classic;
bool _isTimeMode = true;
bool _isPublicRoom = true;
bool _roomStarted = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_codeController = TextEditingController();
_playerName = StorageService.instance.playerName;
@ -449,30 +566,53 @@ class _LobbyScreenState extends State<LobbyScreen> {
}
@override
void dispose() { _codeController.dispose(); super.dispose(); }
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_cleanupGhostRoom();
_codeController.dispose();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused || state == AppLifecycleState.detached) {
_cleanupGhostRoom();
}
}
void _cleanupGhostRoom() {
if (_myRoomCode != null && !_roomStarted) {
FirebaseFirestore.instance.collection('games').doc(_myRoomCode).delete();
_myRoomCode = null;
}
}
Future<void> _createRoom() async {
if (_isLoading) return;
setState(() => _isLoading = true);
try {
String code = await _multiplayerService.createGameRoom(_selectedRadius, _playerName, _selectedShape.name, _isTimeMode);
String code = await _multiplayerService.createGameRoom(
_selectedRadius, _playerName, _selectedShape.name, _isTimeMode, isPublic: _isPublicRoom
);
if (!mounted) return;
setState(() { _myRoomCode = code; _isLoading = false; });
setState(() { _myRoomCode = code; _isLoading = false; _roomStarted = false; });
_multiplayerService.shareInviteLink(code);
if (!_isPublicRoom) {
_multiplayerService.shareInviteLink(code);
}
_showWaitingDialog(code);
} catch (e) {
if (mounted) { setState(() => _isLoading = false); _showError("Errore durante la creazione della partita."); }
}
}
Future<void> _joinRoom() async {
Future<void> _joinRoomByCode(String code) async {
if (_isLoading) return;
FocusScope.of(context).unfocus();
String code = _codeController.text.trim().toUpperCase();
code = code.trim().toUpperCase();
if (code.isEmpty || code.length != 5) { _showError("Inserisci un codice valido di 5 caratteri."); return; }
setState(() => _isLoading = true);
@ -532,10 +672,10 @@ class _LobbyScreenState extends State<LobbyScreen> {
),
child: Column(
children: [
Icon(Icons.share, color: theme.playerBlue, size: 32), const SizedBox(height: 12),
Text("Invita un amico", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.w900, fontSize: 18))),
Icon(_isPublicRoom ? Icons.podcasts : Icons.share, color: theme.playerBlue, size: 32), const SizedBox(height: 12),
Text(_isPublicRoom ? "Sei in Bacheca!" : "Invita un amico", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.w900, fontSize: 18))),
const SizedBox(height: 8),
Text("Condividi il codice. La partita inizierà appena si unirà.", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.8), fontSize: 14, height: 1.5))),
Text(_isPublicRoom ? "Aspettiamo che uno sfidante si unisca dalla lobby pubblica." : "Condividi il codice. La partita inizierà appena si unirà.", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.8), fontSize: 14, height: 1.5))),
],
),
),
@ -564,6 +704,7 @@ class _LobbyScreenState extends State<LobbyScreen> {
if (snapshot.hasData && snapshot.data!.exists) {
var data = snapshot.data!.data() as Map<String, dynamic>;
if (data['status'] == 'playing') {
_roomStarted = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.pop(context);
context.read<GameController>().startNewGame(_selectedRadius, isOnline: true, roomCode: code, isHost: true, shape: _selectedShape, timeMode: _isTimeMode);
@ -572,19 +713,30 @@ class _LobbyScreenState extends State<LobbyScreen> {
}
}
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
dialogContent,
const SizedBox(height: 20),
TextButton(
onPressed: () { FirebaseFirestore.instance.collection('games').doc(code).delete(); Navigator.pop(context); },
child: Text("ANNULLA", style: _getTextStyle(themeType, TextStyle(color: Colors.red, fontWeight: FontWeight.w900, fontSize: 20, letterSpacing: 2.0, shadows: themeType == AppThemeType.doodle ? [] : [const Shadow(color: Colors.black, blurRadius: 2)]))),
),
],
return PopScope(
canPop: false,
onPopInvoked: (didPop) {
if (didPop) return;
_cleanupGhostRoom();
Navigator.pop(context);
},
child: Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
dialogContent,
const SizedBox(height: 20),
TextButton(
onPressed: () {
_cleanupGhostRoom();
Navigator.pop(context);
},
child: Text("ANNULLA", style: _getTextStyle(themeType, TextStyle(color: Colors.red, fontWeight: FontWeight.w900, fontSize: 20, letterSpacing: 2.0, shadows: themeType == AppThemeType.doodle ? [] : [const Shadow(color: Colors.black, blurRadius: 2)]))),
),
],
),
),
);
},
@ -605,9 +757,9 @@ class _LobbyScreenState extends State<LobbyScreen> {
if (themeType == AppThemeType.cyberpunk) bgImage = 'assets/images/cyber_bg.jpg';
bool isChaosUnlocked = true;
Color doodlePenColor = const Color(0xFF00008B);
// --- PANNELLO IMPOSTAZIONI STANZA ---
Widget hostPanel = Transform.rotate(
angle: themeType == AppThemeType.doodle ? 0.01 : 0,
child: Container(
@ -625,7 +777,7 @@ class _LobbyScreenState extends State<LobbyScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(child: Text("IMPOSTAZIONI GRIGLIA", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.6), letterSpacing: 2.0)))),
Center(child: Text("IMPOSTAZIONI STANZA", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.6), letterSpacing: 2.0)))),
const SizedBox(height: 10),
Text("FORMA ARENA", style: _getTextStyle(themeType, TextStyle(fontSize: 10, fontWeight: FontWeight.w900, color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.5), letterSpacing: 1.5))),
@ -634,11 +786,15 @@ class _LobbyScreenState extends State<LobbyScreen> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_NeonShapeButton(icon: Icons.diamond_outlined, label: 'Rombo', isSelected: _selectedShape == ArenaShape.classic, theme: theme, themeType: themeType, onTap: () => setState(() => _selectedShape = ArenaShape.classic)),
_NeonShapeButton(icon: Icons.add, label: 'Croce', isSelected: _selectedShape == ArenaShape.cross, theme: theme, themeType: themeType, onTap: () => setState(() => _selectedShape = ArenaShape.cross)),
_NeonShapeButton(icon: Icons.donut_large, label: 'Buco', isSelected: _selectedShape == ArenaShape.donut, theme: theme, themeType: themeType, onTap: () => setState(() => _selectedShape = ArenaShape.donut)),
_NeonShapeButton(icon: Icons.hourglass_bottom, label: 'Clessidra', isSelected: _selectedShape == ArenaShape.hourglass, theme: theme, themeType: themeType, onTap: () => setState(() => _selectedShape = ArenaShape.hourglass)),
_NeonShapeButton(icon: Icons.all_inclusive, label: 'Caos', isSelected: _selectedShape == ArenaShape.chaos, theme: theme, themeType: themeType, isSpecial: true, isLocked: !isChaosUnlocked, onTap: () => setState(() => _selectedShape = ArenaShape.chaos)),
Expanded(child: _NeonShapeButton(icon: Icons.diamond_outlined, label: 'Rombo', isSelected: _selectedShape == ArenaShape.classic, theme: theme, themeType: themeType, onTap: () => setState(() => _selectedShape = ArenaShape.classic))),
const SizedBox(width: 4),
Expanded(child: _NeonShapeButton(icon: Icons.add, label: 'Croce', isSelected: _selectedShape == ArenaShape.cross, theme: theme, themeType: themeType, onTap: () => setState(() => _selectedShape = ArenaShape.cross))),
const SizedBox(width: 4),
Expanded(child: _NeonShapeButton(icon: Icons.donut_large, label: 'Buco', isSelected: _selectedShape == ArenaShape.donut, theme: theme, themeType: themeType, onTap: () => setState(() => _selectedShape = ArenaShape.donut))),
const SizedBox(width: 4),
Expanded(child: _NeonShapeButton(icon: Icons.hourglass_bottom, label: 'Clessidra', isSelected: _selectedShape == ArenaShape.hourglass, theme: theme, themeType: themeType, onTap: () => setState(() => _selectedShape = ArenaShape.hourglass))),
const SizedBox(width: 4),
Expanded(child: _NeonShapeButton(icon: Icons.all_inclusive, label: 'Caos', isSelected: _selectedShape == ArenaShape.chaos, theme: theme, themeType: themeType, isSpecial: true, isLocked: !isChaosUnlocked, onTap: () => setState(() => _selectedShape = ArenaShape.chaos))),
],
),
@ -662,9 +818,15 @@ class _LobbyScreenState extends State<LobbyScreen> {
Divider(color: themeType == AppThemeType.doodle ? theme.text.withOpacity(0.5) : Colors.white.withOpacity(0.05), thickness: themeType == AppThemeType.doodle ? 2.5 : 1.5),
const SizedBox(height: 12),
Text("TEMPO", style: _getTextStyle(themeType, TextStyle(fontSize: 10, fontWeight: FontWeight.w900, color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.5), letterSpacing: 1.5))),
Text("REGOLE E VISIBILITÀ", style: _getTextStyle(themeType, TextStyle(fontSize: 10, fontWeight: FontWeight.w900, color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.5), letterSpacing: 1.5))),
const SizedBox(height: 8),
_NeonTimeSwitch(isTimeMode: _isTimeMode, theme: theme, themeType: themeType, onTap: () => setState(() => _isTimeMode = !_isTimeMode)),
Row(
children: [
Expanded(child: _NeonTimeSwitch(isTimeMode: _isTimeMode, theme: theme, themeType: themeType, onTap: () => setState(() => _isTimeMode = !_isTimeMode))),
const SizedBox(width: 8),
Expanded(child: _NeonPrivacySwitch(isPublic: _isPublicRoom, theme: theme, themeType: themeType, onTap: () => setState(() => _isPublicRoom = !_isPublicRoom))),
],
)
],
),
),
@ -676,7 +838,9 @@ class _LobbyScreenState extends State<LobbyScreen> {
Widget uiContent = SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 10.0),
physics: const BouncingScrollPhysics(),
// Padding inferiore aumentato a 60 per evitare il taglio dei pulsanti
padding: EdgeInsets.only(left: 20.0, right: 20.0, top: 10.0, bottom: MediaQuery.of(context).padding.bottom + 60.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
@ -703,51 +867,198 @@ class _LobbyScreenState extends State<LobbyScreen> {
),
],
),
const SizedBox(height: 20),
hostPanel,
const SizedBox(height: 15),
_NeonActionButton(label: "CREA PARTITA", color: theme.playerRed, onTap: _createRoom, theme: theme, themeType: themeType),
const SizedBox(height: 20),
// --- L'EFFETTO SIPARIO CON ANIMATED SIZE ---
AnimatedSize(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
alignment: Alignment.topCenter,
child: _isCreatingRoom
? Column( // MENU CREAZIONE (Aperto)
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
hostPanel,
const SizedBox(height: 15),
Row(
children: [
Expanded( // Entrambi in un Expanded "liscio" si dividono il 50% di spazio
child: _NeonActionButton(label: "AVVIA", color: theme.playerRed, onTap: _createRoom, theme: theme, themeType: themeType),
),
const SizedBox(width: 10),
Expanded( // Entrambi in un Expanded "liscio" si dividono il 50% di spazio
child: _NeonActionButton(label: "ANNULLA", color: Colors.grey.shade600, onTap: () => setState(() => _isCreatingRoom = false), theme: theme, themeType: themeType),
),
],
),
],
)
: Column( // MENU BASE (Chiuso)
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_NeonActionButton(label: "CREA PARTITA", color: theme.playerRed, onTap: () { FocusScope.of(context).unfocus(); setState(() => _isCreatingRoom = true); }, theme: theme, themeType: themeType),
const SizedBox(height: 20),
Row(
children: [
Expanded(child: Divider(color: theme.text.withOpacity(0.4), thickness: themeType == AppThemeType.doodle ? 2 : 1.0)),
Padding(padding: const EdgeInsets.symmetric(horizontal: 10), child: Text("OPPURE", style: _getTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.5), fontWeight: FontWeight.bold, letterSpacing: 2.0, fontSize: 13)))),
Expanded(child: Divider(color: theme.text.withOpacity(0.4), thickness: themeType == AppThemeType.doodle ? 2 : 1.0)),
],
),
const SizedBox(height: 20),
Transform.rotate(
angle: themeType == AppThemeType.doodle ? 0.02 : 0,
child: Container(
decoration: themeType == AppThemeType.doodle ? BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.only(topLeft: Radius.circular(20), bottomRight: Radius.circular(20), topRight: Radius.circular(5), bottomLeft: Radius.circular(5)),
border: Border.all(color: theme.text, width: 2.5),
boxShadow: [BoxShadow(color: theme.text.withOpacity(0.8), offset: const Offset(5, 5), blurRadius: 0)],
) : BoxDecoration(
boxShadow: [BoxShadow(color: theme.playerBlue.withOpacity(0.15), blurRadius: 15, spreadRadius: 1)]
),
child: TextField(
controller: _codeController, textCapitalization: TextCapitalization.characters, textAlign: TextAlign.center, maxLength: 5,
style: _getTextStyle(themeType, TextStyle(fontSize: 28, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 12, shadows: themeType == AppThemeType.doodle ? [] : [Shadow(color: theme.playerBlue.withOpacity(0.5), blurRadius: 8)])),
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(vertical: 12),
hintText: "CODICE", hintStyle: _getTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.3), letterSpacing: 10, fontSize: 20)), counterText: "",
filled: themeType != AppThemeType.doodle,
fillColor: themeType == AppThemeType.cyberpunk ? Colors.black.withOpacity(0.85) : theme.text.withOpacity(0.05),
enabledBorder: themeType == AppThemeType.doodle ? InputBorder.none : OutlineInputBorder(borderSide: BorderSide(color: theme.gridLine.withOpacity(0.5), width: 2.0), borderRadius: BorderRadius.circular(15)),
focusedBorder: themeType == AppThemeType.doodle ? InputBorder.none : OutlineInputBorder(borderSide: BorderSide(color: theme.playerBlue, width: 3.0), borderRadius: BorderRadius.circular(15)),
),
),
),
),
const SizedBox(height: 15),
_NeonActionButton(label: "UNISCITI", color: theme.playerBlue, onTap: () => _joinRoomByCode(_codeController.text), theme: theme, themeType: themeType),
],
),
),
const SizedBox(height: 25),
Row(
children: [
Expanded(child: Divider(color: theme.text.withOpacity(0.4), thickness: themeType == AppThemeType.doodle ? 2 : 1.0)),
Padding(padding: const EdgeInsets.symmetric(horizontal: 10), child: Text("OPPURE", style: _getTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.5), fontWeight: FontWeight.bold, letterSpacing: 2.0, fontSize: 13)))),
Padding(padding: const EdgeInsets.symmetric(horizontal: 10), child: Text("LOBBY PUBBLICA", style: _getTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.5), fontWeight: FontWeight.bold, letterSpacing: 2.0, fontSize: 13)))),
Expanded(child: Divider(color: theme.text.withOpacity(0.4), thickness: themeType == AppThemeType.doodle ? 2 : 1.0)),
],
),
const SizedBox(height: 20),
Transform.rotate(
angle: themeType == AppThemeType.doodle ? 0.02 : 0,
child: Container(
decoration: themeType == AppThemeType.doodle ? BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.only(topLeft: Radius.circular(20), bottomRight: Radius.circular(20), topRight: Radius.circular(5), bottomLeft: Radius.circular(5)),
border: Border.all(color: theme.text, width: 2.5),
boxShadow: [BoxShadow(color: theme.text.withOpacity(0.8), offset: const Offset(5, 5), blurRadius: 0)],
) : BoxDecoration(
boxShadow: [BoxShadow(color: theme.playerBlue.withOpacity(0.15), blurRadius: 15, spreadRadius: 1)]
),
child: TextField(
controller: _codeController, textCapitalization: TextCapitalization.characters, textAlign: TextAlign.center, maxLength: 5,
style: _getTextStyle(themeType, TextStyle(fontSize: 28, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 12, shadows: themeType == AppThemeType.doodle ? [] : [Shadow(color: theme.playerBlue.withOpacity(0.5), blurRadius: 8)])),
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(vertical: 12),
hintText: "CODICE", hintStyle: _getTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.3), letterSpacing: 10, fontSize: 20)), counterText: "",
filled: themeType != AppThemeType.doodle,
fillColor: themeType == AppThemeType.cyberpunk ? Colors.black.withOpacity(0.85) : theme.text.withOpacity(0.05),
enabledBorder: themeType == AppThemeType.doodle ? InputBorder.none : OutlineInputBorder(borderSide: BorderSide(color: theme.gridLine.withOpacity(0.5), width: 2.0), borderRadius: BorderRadius.circular(15)),
focusedBorder: themeType == AppThemeType.doodle ? InputBorder.none : OutlineInputBorder(borderSide: BorderSide(color: theme.playerBlue, width: 3.0), borderRadius: BorderRadius.circular(15)),
),
),
),
),
const SizedBox(height: 15),
_NeonActionButton(label: "UNISCITI", color: theme.playerBlue, onTap: _joinRoom, theme: theme, themeType: themeType),
const SizedBox(height: 10),
// --- LA VERA E PROPRIA BACHECA PUBBLICA ---
StreamBuilder<QuerySnapshot>(
stream: _multiplayerService.getPublicRooms(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Padding(padding: const EdgeInsets.all(20), child: Center(child: CircularProgressIndicator(color: theme.playerBlue)));
}
if (!snapshot.hasData || snapshot.data!.docs.isEmpty) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 20.0),
child: Center(child: Text("Nessuna stanza pubblica al momento.\nCreane una tu!", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.6), height: 1.5)))),
);
}
DateTime now = DateTime.now();
String? myUid = FirebaseAuth.instance.currentUser?.uid;
var docs = snapshot.data!.docs.where((doc) {
var data = doc.data() as Map<String, dynamic>;
if (data['isPublic'] != true) return false;
if (data['hostUid'] != null && data['hostUid'] == myUid) return false;
Timestamp? createdAt = data['createdAt'] as Timestamp?;
if (createdAt != null) {
int ageInMinutes = now.difference(createdAt.toDate()).inMinutes;
if (ageInMinutes > 15) {
FirebaseFirestore.instance.collection('games').doc(doc.id).delete();
return false;
}
}
return true;
}).toList();
if (docs.isEmpty) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 20.0),
child: Center(child: Text("Nessuna stanza pubblica al momento.\nCreane una tu!", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.6), height: 1.5)))),
);
}
docs.sort((a, b) {
Timestamp? tA = (a.data() as Map<String, dynamic>)['createdAt'] as Timestamp?;
Timestamp? tB = (b.data() as Map<String, dynamic>)['createdAt'] as Timestamp?;
if (tA == null || tB == null) return 0;
return tB.compareTo(tA);
});
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
itemCount: docs.length,
itemBuilder: (context, index) {
var doc = docs[index];
var data = doc.data() as Map<String, dynamic>;
String host = data['hostName'] ?? 'Sconosciuto';
int r = data['radius'] ?? 4;
String shapeStr = data['shape'] ?? 'classic';
bool time = data['timeMode'] ?? true;
String prettyShape = "Rombo";
if (shapeStr == 'cross') prettyShape = "Croce";
else if (shapeStr == 'donut') prettyShape = "Buco";
else if (shapeStr == 'hourglass') prettyShape = "Clessidra";
else if (shapeStr == 'chaos') prettyShape = "Caos";
return Transform.rotate(
angle: themeType == AppThemeType.doodle ? (index % 2 == 0 ? 0.01 : -0.01) : 0,
child: Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: themeType == AppThemeType.doodle ? Colors.white : theme.text.withOpacity(0.05),
borderRadius: BorderRadius.circular(15),
border: Border.all(color: themeType == AppThemeType.doodle ? theme.text : theme.playerBlue.withOpacity(0.3), width: themeType == AppThemeType.doodle ? 2 : 1),
boxShadow: themeType == AppThemeType.doodle ? [BoxShadow(color: theme.text.withOpacity(0.6), offset: const Offset(3, 4))] : [],
),
child: Row(
children: [
CircleAvatar(backgroundColor: theme.playerRed.withOpacity(0.2), child: Icon(Icons.person, color: theme.playerRed)),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Stanza di $host", style: _getTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 16))),
const SizedBox(height: 4),
Text("Raggio: $r$prettyShape${time ? 'A Tempo' : 'Relax'}", style: _getTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.6), fontSize: 11))),
],
),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: theme.playerBlue, foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
elevation: themeType == AppThemeType.doodle ? 0 : 2,
side: themeType == AppThemeType.doodle ? BorderSide(color: theme.text, width: 2) : BorderSide.none,
),
onPressed: () => _joinRoomByCode(doc.id),
child: Text("ENTRA", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.w900, letterSpacing: 1.0))),
)
],
),
),
);
}
);
}
),
const SizedBox(height: 20),
],
),
),

View file

@ -35,10 +35,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
padding: const EdgeInsets.all(20),
children: [
_ThemeCard(
title: "Minimal",
subtitle: "Linee pulite, sfondo chiaro",
type: AppThemeType.minimal,
previewColors: AppColors.minimal,
title: "Quaderno (Doodle)",
subtitle: "Sfondo a quadretti, tratto a penna",
type: AppThemeType.doodle,
previewColors: AppColors.doodle,
requiredLevel: 1,
currentLevel: playerLevel,
),
@ -52,15 +52,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
currentLevel: playerLevel,
),
const SizedBox(height: 15),
_ThemeCard(
title: "Quaderno (Doodle)",
subtitle: "Sfondo a quadretti, tratto a penna",
type: AppThemeType.doodle,
previewColors: AppColors.doodle,
requiredLevel: 5,
currentLevel: playerLevel,
),
const SizedBox(height: 15),
_ThemeCard(
title: "Cyberpunk",
subtitle: "Nero profondo, luci al neon",
@ -87,6 +78,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
requiredLevel: 15,
currentLevel: playerLevel,
),
const SizedBox(height: 15),
_ThemeCard(
title: "Musica",
subtitle: "Vinili, cassette e vibrazioni sonore",
type: AppThemeType.music,
previewColors: AppColors.music,
requiredLevel: 20, // Tema Esclusivo di Livello 20!
currentLevel: playerLevel,
),
],
),
);
@ -164,9 +164,9 @@ class _ThemeCard extends StatelessWidget {
],
),
),
Container(width: 20, height: 20, decoration: BoxDecoration(color: previewColors.playerRed, shape: BoxShape.circle)),
Container(width: 20, height: 20, decoration: BoxDecoration(color: previewColors.playerRed, shape: BoxShape.circle, boxShadow: [BoxShadow(color: previewColors.playerRed.withOpacity(0.5), blurRadius: 4)])),
const SizedBox(width: 10),
Container(width: 20, height: 20, decoration: BoxDecoration(color: previewColors.playerBlue, shape: BoxShape.circle)),
Container(width: 20, height: 20, decoration: BoxDecoration(color: previewColors.playerBlue, shape: BoxShape.circle, boxShadow: [BoxShadow(color: previewColors.playerBlue.withOpacity(0.5), blurRadius: 4)])),
],
),
),

View file

@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../core/theme_manager.dart'; // Import aggiornato
import 'dart:math' as math;
class AnimatedCyberBorder extends StatefulWidget {
final Widget child;
const AnimatedCyberBorder({super.key, required this.child});
@override
State<AnimatedCyberBorder> createState() => _AnimatedCyberBorderState();
}
class _AnimatedCyberBorderState extends State<AnimatedCyberBorder> with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: const Duration(seconds: 3))..repeat(); }
@override
void dispose() { _controller.dispose(); super.dispose(); }
@override
Widget build(BuildContext context) {
final theme = context.watch<ThemeManager>().currentColors;
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return CustomPaint(
painter: CyberBorderPainter(animationValue: _controller.value, color1: theme.playerBlue, color2: theme.playerRed),
child: Container(
decoration: BoxDecoration(color: theme.background.withOpacity(0.9), borderRadius: BorderRadius.circular(15), boxShadow: [BoxShadow(color: theme.playerBlue.withOpacity(0.3), blurRadius: 25, spreadRadius: 2)]),
padding: const EdgeInsets.all(3),
child: widget.child,
),
);
},
child: widget.child,
);
}
}
class CyberBorderPainter extends CustomPainter {
final double animationValue;
final Color color1;
final Color color2;
CyberBorderPainter({required this.animationValue, required this.color1, required this.color2});
@override
void paint(Canvas canvas, Size size) {
final rect = Offset.zero & size;
final RRect rrect = RRect.fromRectAndRadius(rect, const Radius.circular(15));
final Paint paint = Paint()
..shader = SweepGradient(colors: [color1, color2, color1, color2, color1], stops: const [0.0, 0.25, 0.5, 0.75, 1.0], transform: GradientRotation(animationValue * 2 * math.pi)).createShader(rect)
..style = PaintingStyle.stroke
..strokeWidth = 4.0
..maskFilter = const MaskFilter.blur(BlurStyle.solid, 4);
canvas.drawRRect(rrect, paint);
}
@override bool shouldRepaint(covariant CyberBorderPainter oldDelegate) => oldDelegate.animationValue != animationValue;
}

View file

@ -1,8 +1,13 @@
// ===========================================================================
// FILE: lib/widgets/game_over_dialog.dart
// ===========================================================================
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../logic/game_controller.dart';
import '../core/theme_manager.dart';
import '../core/app_colors.dart';
import '../services/storage_service.dart';
class GameOverDialog extends StatelessWidget {
const GameOverDialog({super.key});
@ -19,15 +24,18 @@ class GameOverDialog extends StatelessWidget {
bool playerBeatCPU = game.isVsCPU && red > blue;
String myName = StorageService.instance.playerName.toUpperCase();
if (myName.isEmpty) myName = "TU";
// --- LOGICA NOMI ---
String nameRed = "ROSSO";
String nameBlue = themeType == AppThemeType.cyberpunk ? "VERDE" : "BLU";
String nameRed = myName;
String nameBlue = themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? "VERDE" : "BLU";
if (game.isOnline) {
nameRed = game.onlineHostName.toUpperCase();
nameBlue = game.onlineGuestName.toUpperCase();
} else if (game.isVsCPU) {
nameRed = "TU";
nameRed = myName;
nameBlue = "CPU";
}

View file

@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
import '../core/app_colors.dart'; // Import aggiornato
import 'painters.dart';
class FeatureCard extends StatelessWidget {
final String title;
final String subtitle;
final IconData icon;
final Color color;
final ThemeColors theme;
final AppThemeType themeType;
final VoidCallback onTap;
final bool isFeatured;
final bool compact;
const FeatureCard({super.key, required this.title, required this.subtitle, required this.icon, required this.color, required this.theme, required this.themeType, required this.onTap, this.isFeatured = false, this.compact = false});
@override
Widget build(BuildContext context) {
if (themeType == AppThemeType.doodle) {
double tilt = (title.length % 2 == 0) ? -0.015 : 0.02;
Color inkColor = const Color(0xFF111122);
return Transform.rotate(
angle: tilt,
child: GestureDetector(
onTap: onTap,
child: CustomPaint(
painter: DoodleBackgroundPainter(fillColor: color, strokeColor: inkColor, seed: title.length * 5),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: compact ? 12.0 : 22.0, vertical: compact ? 12.0 : 16.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(icon, color: inkColor, size: compact ? 24 : 32),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(title, style: getSharedTextStyle(themeType, TextStyle(color: inkColor, fontSize: compact ? 16 : 24, fontWeight: FontWeight.w900)))),
if (!compact) ...[ const SizedBox(height: 2), Text(subtitle, style: getSharedTextStyle(themeType, TextStyle(color: inkColor.withOpacity(0.8), fontSize: 14, fontWeight: FontWeight.bold))) ]
],
),
),
if (!compact) Icon(Icons.chevron_right_rounded, color: inkColor.withOpacity(0.6), size: 32),
],
),
),
),
),
);
}
return GestureDetector(
onTap: onTap,
child: Container(
padding: EdgeInsets.symmetric(horizontal: compact ? 12.0 : 20.0, vertical: compact ? 10.0 : 14.0),
decoration: BoxDecoration(
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: isFeatured ? [color.withOpacity(0.9), color.withOpacity(0.6)] : [color.withOpacity(0.25), color.withOpacity(0.05)]),
borderRadius: BorderRadius.circular(15), border: Border.all(color: color.withOpacity(isFeatured ? 0.5 : 0.2), width: 1.5),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.6), offset: const Offset(0, 8), blurRadius: 15), BoxShadow(color: color.withOpacity(isFeatured ? 0.3 : 0.05), offset: const Offset(-1, -1), blurRadius: 5)]
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
padding: EdgeInsets.all(compact ? 6 : 10),
decoration: BoxDecoration(gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Colors.white.withOpacity(0.3), Colors.white.withOpacity(0.05)]), shape: BoxShape.circle, border: Border.all(color: Colors.white.withOpacity(0.2)), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.2), blurRadius: 5, offset: const Offset(2, 4))]),
child: Icon(icon, color: isFeatured ? Colors.white : color, size: compact ? 20 : 26),
),
SizedBox(width: compact ? 10 : 20),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(title, style: getSharedTextStyle(themeType, TextStyle(color: isFeatured ? Colors.white : theme.text, fontSize: compact ? 14 : 18, fontWeight: FontWeight.w900, shadows: [Shadow(color: Colors.black.withOpacity(0.5), offset: const Offset(1, 2), blurRadius: 2)])))),
if (!compact) ...[ const SizedBox(height: 2), Text(subtitle, style: getSharedTextStyle(themeType, TextStyle(color: isFeatured ? Colors.white.withOpacity(0.8) : theme.text.withOpacity(0.6), fontSize: 12, fontWeight: FontWeight.bold))) ]
],
),
),
if (!compact) Icon(Icons.chevron_right_rounded, color: isFeatured ? Colors.white.withOpacity(0.7) : color.withOpacity(0.5), size: 30),
],
),
),
);
}
}

View file

@ -0,0 +1,129 @@
// ===========================================================================
// FILE: lib/widgets/music_theme_widgets.dart
// ===========================================================================
import 'package:flutter/material.dart';
import '../core/app_colors.dart';
import 'painters.dart';
class MusicCassetteCard extends StatelessWidget {
final String title;
final String subtitle;
final Color neonColor;
final double angle;
final IconData leftIcon;
final IconData rightIcon;
final VoidCallback onTap;
final AppThemeType themeType;
const MusicCassetteCard({super.key, required this.title, required this.subtitle, required this.neonColor, required this.angle, required this.leftIcon, required this.rightIcon, required this.onTap, required this.themeType});
@override
Widget build(BuildContext context) {
return Transform.rotate(
angle: angle,
child: GestureDetector(
onTap: onTap,
child: Container(
// Aumentato leggermente l'altezza a 125 per evitare l'overflow
height: 125, margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 10), padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF22222A), borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.black87, width: 2),
boxShadow: [ BoxShadow(color: neonColor.withOpacity(0.5), blurRadius: 25, spreadRadius: 2), const BoxShadow(color: Colors.black54, offset: Offset(5, 10), blurRadius: 15) ]
),
child: Column(
children: [
Expanded(
child: Container(
decoration: BoxDecoration(color: neonColor.withOpacity(0.15), borderRadius: BorderRadius.circular(4), border: Border.all(color: neonColor.withOpacity(0.5), width: 1.5)),
child: Row(
children: [
Padding(padding: const EdgeInsets.symmetric(horizontal: 12), child: Icon(leftIcon, color: neonColor, size: 28)),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
// Aggiunto minAxisSize per far stare il contenuto stretto
mainAxisSize: MainAxisSize.min,
children: [
Flexible(child: FittedBox(fit: BoxFit.scaleDown, child: Text(title, style: getSharedTextStyle(themeType, TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.w900, shadows: [Shadow(color: neonColor, blurRadius: 10)]))))),
Flexible(child: FittedBox(fit: BoxFit.scaleDown, child: Text(subtitle, style: getSharedTextStyle(themeType, const TextStyle(color: Colors.white70, fontSize: 11, fontWeight: FontWeight.bold))))),
],
),
),
Padding(padding: const EdgeInsets.symmetric(horizontal: 12), child: Icon(rightIcon, color: neonColor, size: 28)),
],
),
),
),
const SizedBox(height: 10),
Container(
height: 35, width: 180, decoration: BoxDecoration(color: const Color(0xFF0D0D12), borderRadius: BorderRadius.circular(20), border: Border.all(color: Colors.white24, width: 1)),
child: Stack(
alignment: Alignment.center,
children: [
Container(height: 2, width: 120, color: const Color(0xFF333333)),
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _buildSpool(), _buildSpool() ]),
],
),
),
],
),
),
),
);
}
Widget _buildSpool() {
return Container(
width: 26, height: 26, decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.white70, border: Border.all(color: Colors.black87, width: 5)),
child: Center(child: Container(width: 6, height: 6, decoration: const BoxDecoration(shape: BoxShape.circle, color: Colors.black))),
);
}
}
class MusicKnobCard extends StatelessWidget {
final String title;
final IconData icon;
final VoidCallback onTap;
final AppThemeType themeType;
final Color? iconColor;
const MusicKnobCard({super.key, required this.title, required this.icon, required this.onTap, required this.themeType, this.iconColor});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 65, height: 65,
decoration: BoxDecoration(
shape: BoxShape.circle, color: const Color(0xFF222222), border: Border.all(color: const Color(0xFF111111), width: 2),
boxShadow: const [BoxShadow(color: Colors.black87, blurRadius: 10, offset: Offset(2, 6)), BoxShadow(color: Colors.white12, blurRadius: 2, offset: Offset(-1, -1))],
),
child: Padding(
padding: const EdgeInsets.all(6.0),
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle, border: Border.all(color: Colors.black54, width: 1),
gradient: const SweepGradient(colors: [Color(0xFF555555), Color(0xFFAAAAAA), Color(0xFF555555), Color(0xFF222222), Color(0xFF555555)]),
),
child: Padding(
padding: const EdgeInsets.all(4.0),
child: Container(
decoration: const BoxDecoration(shape: BoxShape.circle, color: Color(0xFF1A1A1A)),
child: Center(child: Icon(icon, color: iconColor ?? Colors.white70, size: 20)),
),
),
),
),
),
const SizedBox(height: 10),
FittedBox(fit: BoxFit.scaleDown, child: Text(title, style: getSharedTextStyle(themeType, const TextStyle(color: Colors.white70, fontSize: 11, fontWeight: FontWeight.bold, letterSpacing: 1.0)))),
],
),
);
}
}

73
lib/widgets/painters.dart Normal file
View file

@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'dart:math' as math;
import '../core/app_colors.dart'; // Import aggiornato
TextStyle getSharedTextStyle(AppThemeType themeType, TextStyle baseStyle) {
if (themeType == AppThemeType.doodle) {
return GoogleFonts.permanentMarker(textStyle: baseStyle);
} else if (themeType == AppThemeType.arcade) {
return GoogleFonts.pressStart2p(textStyle: baseStyle.copyWith(fontSize: baseStyle.fontSize != null ? baseStyle.fontSize! * 0.75 : null, letterSpacing: 0.5));
} else if (themeType == AppThemeType.grimorio) {
return GoogleFonts.cinzelDecorative(textStyle: baseStyle.copyWith(fontWeight: FontWeight.bold));
} else if (themeType == AppThemeType.music) {
return GoogleFonts.audiowide(textStyle: baseStyle.copyWith(letterSpacing: 1.5));
}
return baseStyle;
}
class DoodleBackgroundPainter extends CustomPainter {
final Color fillColor; final Color strokeColor; final int seed; final bool isCircle;
DoodleBackgroundPainter({required this.fillColor, required this.strokeColor, required this.seed, this.isCircle = false});
@override
void paint(Canvas canvas, Size size) {
final math.Random random = math.Random(seed);
double wobble() => random.nextDouble() * 6 - 3;
final Paint fillPaint = Paint()..color = fillColor..style = PaintingStyle.fill;
final Paint strokePaint = Paint()..color = strokeColor..strokeWidth = 2.5..style = PaintingStyle.stroke..strokeCap = StrokeCap.round..strokeJoin = StrokeJoin.round;
if (isCircle) {
final Rect rect = Rect.fromLTWH(wobble(), wobble(), size.width + wobble(), size.height + wobble());
canvas.save(); canvas.translate(wobble(), wobble()); canvas.drawOval(rect, fillPaint); canvas.restore();
canvas.drawOval(rect, strokePaint);
canvas.save(); canvas.translate(random.nextDouble() * 4 - 2, random.nextDouble() * 4 - 2); canvas.drawOval(rect, strokePaint..strokeWidth = 1.0..color = strokeColor.withOpacity(0.6)); canvas.restore();
} else {
final Path path = Path()..moveTo(wobble(), wobble())..lineTo(size.width + wobble(), wobble())..lineTo(size.width + wobble(), size.height + wobble())..lineTo(wobble(), size.height + wobble())..close();
final Path fillPath = Path()..moveTo(wobble() * 1.5, wobble() * 1.5)..lineTo(size.width + wobble() * 1.5, wobble() * 1.5)..lineTo(size.width + wobble() * 1.5, size.height + wobble() * 1.5)..lineTo(wobble() * 1.5, size.height + wobble() * 1.5)..close();
canvas.drawPath(fillPath, fillPaint);
canvas.drawPath(path, strokePaint);
canvas.save(); canvas.translate(random.nextDouble() * 3 - 1.5, random.nextDouble() * 3 - 1.5); canvas.drawPath(path, strokePaint..strokeWidth = 1.0..color = strokeColor.withOpacity(0.6)); canvas.restore();
}
}
@override bool shouldRepaint(covariant DoodleBackgroundPainter oldDelegate) => oldDelegate.fillColor != fillColor || oldDelegate.strokeColor != strokeColor;
}
class AudioCablesPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = const Color(0xFF151515)..style = PaintingStyle.stroke..strokeWidth = 8.0..strokeCap = StrokeCap.round;
final highlight = Paint()..color = const Color(0xFF3A3A3A)..style = PaintingStyle.stroke..strokeWidth = 2.0..strokeCap = StrokeCap.round;
void drawCable(Path path) { canvas.drawPath(path, paint); canvas.drawPath(path, highlight); }
Path c1 = Path()..moveTo(-20, size.height * 0.2)..quadraticBezierTo(100, size.height * 0.25, 50, size.height * 0.4)..quadraticBezierTo(0, size.height * 0.5, -20, size.height * 0.55); drawCable(c1);
Path c2 = Path()..moveTo(size.width + 20, size.height * 0.4)..quadraticBezierTo(size.width - 100, size.height * 0.5, size.width - 50, size.height * 0.7)..quadraticBezierTo(size.width, size.height * 0.8, size.width + 20, size.height * 0.85); drawCable(c2);
Path c3 = Path()..moveTo(size.width * 0.2, size.height + 20)..quadraticBezierTo(size.width * 0.3, size.height - 80, size.width * 0.5, size.height - 60)..quadraticBezierTo(size.width * 0.7, size.height - 40, size.width * 0.8, size.height + 20); drawCable(c3);
_drawJack(canvas, Offset(80, size.height * 0.38), -0.5);
_drawJack(canvas, Offset(size.width - 60, size.height * 0.68), 0.8);
}
void _drawJack(Canvas canvas, Offset pos, double angle) {
canvas.save(); canvas.translate(pos.dx, pos.dy); canvas.rotate(angle);
canvas.drawRect(const Rect.fromLTWH(-15, -4, 15, 8), Paint()..color = const Color(0xFF151515));
canvas.drawRRect(RRect.fromRectAndRadius(const Rect.fromLTWH(0, -6, 25, 12), const Radius.circular(2)), Paint()..color = const Color(0xFF222222));
canvas.drawRRect(RRect.fromRectAndRadius(const Rect.fromLTWH(2, -4, 21, 8), const Radius.circular(2)), Paint()..color = const Color(0xFF444444));
canvas.drawRect(const Rect.fromLTWH(25, -2, 15, 4), Paint()..color = const Color(0xFFCCCCCC));
canvas.drawRect(const Rect.fromLTWH(40, -1.5, 5, 3), Paint()..color = const Color(0xFFAAAAAA));
canvas.drawLine(const Offset(30, -2), const Offset(30, 2), Paint()..color = Colors.black..strokeWidth = 1.5);
canvas.drawLine(const Offset(35, -2), const Offset(35, 2), Paint()..color = Colors.black..strokeWidth = 1.5);
canvas.restore();
}
@override bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

BIN
macos/.DS_Store vendored

Binary file not shown.

View file

@ -8,6 +8,8 @@ import Foundation
import app_links
import audioplayers_darwin
import cloud_firestore
import firebase_app_check
import firebase_auth
import firebase_core
import share_plus
import shared_preferences_foundation
@ -16,6 +18,8 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin"))
FLTFirebaseAppCheckPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAppCheckPlugin"))
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))

View file

@ -1190,6 +1190,10 @@ PODS:
- abseil/xcprivacy (1.20240722.0)
- app_links (6.4.1):
- FlutterMacOS
- AppCheckCore (11.2.0):
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- PromisesObjC (~> 2.4)
- audioplayers_darwin (0.0.1):
- FlutterMacOS
- BoringSSL-GRPC (0.0.37):
@ -1199,33 +1203,65 @@ PODS:
- BoringSSL-GRPC/Interface (= 0.0.37)
- BoringSSL-GRPC/Interface (0.0.37)
- cloud_firestore (6.1.2):
- Firebase/CoreOnly (~> 12.8.0)
- Firebase/Firestore (~> 12.8.0)
- Firebase/CoreOnly (~> 12.9.0)
- Firebase/Firestore (~> 12.9.0)
- firebase_core
- FlutterMacOS
- Firebase/CoreOnly (12.8.0):
- FirebaseCore (~> 12.8.0)
- Firebase/Firestore (12.8.0):
- Firebase/AppCheck (12.9.0):
- Firebase/CoreOnly
- FirebaseFirestore (~> 12.8.0)
- firebase_core (4.4.0):
- Firebase/CoreOnly (~> 12.8.0)
- FirebaseAppCheck (~> 12.9.0)
- Firebase/Auth (12.9.0):
- Firebase/CoreOnly
- FirebaseAuth (~> 12.9.0)
- Firebase/CoreOnly (12.9.0):
- FirebaseCore (~> 12.9.0)
- Firebase/Firestore (12.9.0):
- Firebase/CoreOnly
- FirebaseFirestore (~> 12.9.0)
- firebase_app_check (0.4.1-5):
- Firebase/AppCheck (~> 12.9.0)
- Firebase/CoreOnly (~> 12.9.0)
- firebase_core
- FlutterMacOS
- FirebaseAppCheckInterop (12.8.0)
- FirebaseCore (12.8.0):
- FirebaseCoreInternal (~> 12.8.0)
- firebase_auth (6.1.4):
- Firebase/Auth (~> 12.9.0)
- Firebase/CoreOnly (~> 12.9.0)
- firebase_core
- FlutterMacOS
- firebase_core (4.5.0):
- Firebase/CoreOnly (~> 12.9.0)
- FlutterMacOS
- FirebaseAppCheck (12.9.0):
- AppCheckCore (~> 11.0)
- FirebaseAppCheckInterop (~> 12.9.0)
- FirebaseCore (~> 12.9.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- FirebaseAppCheckInterop (12.9.0)
- FirebaseAuth (12.9.0):
- FirebaseAppCheckInterop (~> 12.9.0)
- FirebaseAuthInterop (~> 12.9.0)
- FirebaseCore (~> 12.9.0)
- FirebaseCoreExtension (~> 12.9.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/Environment (~> 8.1)
- GTMSessionFetcher/Core (< 6.0, >= 3.4)
- RecaptchaInterop (~> 101.0)
- FirebaseAuthInterop (12.9.0)
- FirebaseCore (12.9.0):
- FirebaseCoreInternal (~> 12.9.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreExtension (12.8.0):
- FirebaseCore (~> 12.8.0)
- FirebaseCoreInternal (12.8.0):
- FirebaseCoreExtension (12.9.0):
- FirebaseCore (~> 12.9.0)
- FirebaseCoreInternal (12.9.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseFirestore (12.8.0):
- FirebaseCore (~> 12.8.0)
- FirebaseCoreExtension (~> 12.8.0)
- FirebaseFirestoreInternal (~> 12.8.0)
- FirebaseSharedSwift (~> 12.8.0)
- FirebaseFirestoreInternal (12.8.0):
- FirebaseFirestore (12.9.0):
- FirebaseCore (~> 12.9.0)
- FirebaseCoreExtension (~> 12.9.0)
- FirebaseFirestoreInternal (~> 12.9.0)
- FirebaseSharedSwift (~> 12.9.0)
- FirebaseFirestoreInternal (12.9.0):
- abseil/algorithm (~> 1.20240722.0)
- abseil/base (~> 1.20240722.0)
- abseil/container/flat_hash_map (~> 1.20240722.0)
@ -1234,22 +1270,38 @@ PODS:
- abseil/strings/strings (~> 1.20240722.0)
- abseil/time (~> 1.20240722.0)
- abseil/types (~> 1.20240722.0)
- FirebaseAppCheckInterop (~> 12.8.0)
- FirebaseCore (~> 12.8.0)
- FirebaseAppCheckInterop (~> 12.9.0)
- FirebaseCore (~> 12.9.0)
- "gRPC-C++ (~> 1.69.0)"
- gRPC-Core (~> 1.69.0)
- leveldb-library (~> 1.22)
- nanopb (~> 3.30910.0)
- FirebaseSharedSwift (12.8.0)
- FirebaseSharedSwift (12.9.0)
- FlutterMacOS (1.0.0)
- GoogleUtilities/AppDelegateSwizzler (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
- GoogleUtilities/Network
- GoogleUtilities/Privacy
- GoogleUtilities/Environment (8.1.0):
- GoogleUtilities/Privacy
- GoogleUtilities/Logger (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/Network (8.1.0):
- GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib"
- GoogleUtilities/Privacy
- GoogleUtilities/Reachability
- "GoogleUtilities/NSData+zlib (8.1.0)":
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (8.1.0)
- GoogleUtilities/Reachability (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/UserDefaults (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- "gRPC-C++ (1.69.0)":
- "gRPC-C++/Implementation (= 1.69.0)"
- "gRPC-C++/Interface (= 1.69.0)"
@ -1342,12 +1394,14 @@ PODS:
- gRPC-Core/Privacy (= 1.69.0)
- gRPC-Core/Interface (1.69.0)
- gRPC-Core/Privacy (1.69.0)
- GTMSessionFetcher/Core (5.1.0)
- leveldb-library (1.22.6)
- nanopb (3.30910.0):
- nanopb/decode (= 3.30910.0)
- nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0)
- PromisesObjC (2.4.0)
- share_plus (0.0.1):
- FlutterMacOS
- shared_preferences_foundation (0.0.1):
@ -1358,6 +1412,8 @@ DEPENDENCIES:
- app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`)
- audioplayers_darwin (from `Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/macos`)
- cloud_firestore (from `Flutter/ephemeral/.symlinks/plugins/cloud_firestore/macos`)
- firebase_app_check (from `Flutter/ephemeral/.symlinks/plugins/firebase_app_check/macos`)
- firebase_auth (from `Flutter/ephemeral/.symlinks/plugins/firebase_auth/macos`)
- firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
@ -1366,9 +1422,13 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- abseil
- AppCheckCore
- BoringSSL-GRPC
- Firebase
- FirebaseAppCheck
- FirebaseAppCheckInterop
- FirebaseAuth
- FirebaseAuthInterop
- FirebaseCore
- FirebaseCoreExtension
- FirebaseCoreInternal
@ -1378,8 +1438,10 @@ SPEC REPOS:
- GoogleUtilities
- "gRPC-C++"
- gRPC-Core
- GTMSessionFetcher
- leveldb-library
- nanopb
- PromisesObjC
EXTERNAL SOURCES:
app_links:
@ -1388,6 +1450,10 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/macos
cloud_firestore:
:path: Flutter/ephemeral/.symlinks/plugins/cloud_firestore/macos
firebase_app_check:
:path: Flutter/ephemeral/.symlinks/plugins/firebase_app_check/macos
firebase_auth:
:path: Flutter/ephemeral/.symlinks/plugins/firebase_auth/macos
firebase_core:
:path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos
FlutterMacOS:
@ -1400,24 +1466,32 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
abseil: a05cc83bf02079535e17169a73c5be5ba47f714b
app_links: 05a6ec2341985eb05e9f97dc63f5837c39895c3f
AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
audioplayers_darwin: 761f2948df701d05b5db603220c384fb55720012
BoringSSL-GRPC: dded2a44897e45f28f08ae87a55ee4bcd19bc508
cloud_firestore: 71947b640bd24f6f849d9d185e5d0a619fa6b93b
Firebase: 9a58fdbc9d8655ed7b79a19cf9690bb007d3d46d
firebase_core: b1697fb64ff2b9ca16baaa821205f8b0c058e5d2
FirebaseAppCheckInterop: ba3dc604a89815379e61ec2365101608d365cf7d
FirebaseCore: 0dbad74bda10b8fb9ca34ad8f375fb9dd3ebef7c
FirebaseCoreExtension: 6605938d51f765d8b18bfcafd2085276a252bee2
FirebaseCoreInternal: fe5fa466aeb314787093a7dce9f0beeaad5a2a21
FirebaseFirestore: 67f23000ca238ccbab79127ed59636a9a2689e74
FirebaseFirestoreInternal: a0e7382af3d208898dcd1d4d52d8a7870632e881
FirebaseSharedSwift: f57ed48f4542b2d7eb4738f4f23ba443f78b3780
cloud_firestore: a2a9382e6cc4dd07345748b904b3b194ea46be44
Firebase: 065f2bb395062046623036d8e6dc857bc2521d56
firebase_app_check: 1ea404b52b0910bf632b1ea2e00ceb8d1730cb44
firebase_auth: 8db6796451d9aa44d4cc49b3e757865c65ce170f
firebase_core: c74b220e9288decea6bed17399c249734a7e76d2
FirebaseAppCheck: 94dae4d9bb682bdef85a778b0c1024a4613f1e89
FirebaseAppCheckInterop: 4bade10286cc977e516f75d2d8312cbdfa534789
FirebaseAuth: 3a39f6436c21ebfd7919b698228b4f89ff94c23b
FirebaseAuthInterop: f8f6ff72dc24621906497fbe5cf3c42ee815e59c
FirebaseCore: 428912f751178b06bef0a1793effeb4a5e09a9b8
FirebaseCoreExtension: e911052d59cd0da237a45d706fc0f81654f035c1
FirebaseCoreInternal: b321eafae5362113bc182956fafc9922cfc77b72
FirebaseFirestore: d8b76ca1feb4ca0b0f078c45f7d1bd8014a49ef1
FirebaseFirestoreInternal: 02341a9ba87f6309227b04685022a5e16307bbf7
FirebaseSharedSwift: 9d2fa84a46676302b89dbd5e6e62bce2fe376909
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
"gRPC-C++": cc207623316fb041a7a3e774c252cf68a058b9e8
gRPC-Core: 860978b7db482de8b4f5e10677216309b5ff6330
GTMSessionFetcher: b8ab00db932816e14b0a0664a08cb73dda6d164b
leveldb-library: cc8b8f8e013647a295ad3f8cd2ddf49a6f19be19
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb

View file

@ -2,13 +2,15 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>keychain-access-groups</key>
<array/>
</dict>
</plist>
</plist>

View file

@ -8,5 +8,7 @@
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>keychain-access-groups</key>
<array/>
</dict>
</plist>

33
public/404.html Normal file
View file

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Page Not Found</title>
<style media="screen">
body { background: #ECEFF1; color: rgba(0,0,0,0.87); font-family: Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 0; }
#message { background: white; max-width: 360px; margin: 100px auto 16px; padding: 32px 24px 16px; border-radius: 3px; }
#message h3 { color: #888; font-weight: normal; font-size: 16px; margin: 16px 0 12px; }
#message h2 { color: #ffa100; font-weight: bold; font-size: 16px; margin: 0 0 8px; }
#message h1 { font-size: 22px; font-weight: 300; color: rgba(0,0,0,0.6); margin: 0 0 16px;}
#message p { line-height: 140%; margin: 16px 0 24px; font-size: 14px; }
#message a { display: block; text-align: center; background: #039be5; text-transform: uppercase; text-decoration: none; color: white; padding: 16px; border-radius: 4px; }
#message, #message a { box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); }
#load { color: rgba(0,0,0,0.4); text-align: center; font-size: 13px; }
@media (max-width: 600px) {
body, #message { margin-top: 0; background: white; box-shadow: none; }
body { border-top: 16px solid #ffa100; }
}
</style>
</head>
<body>
<div id="message">
<h2>404</h2>
<h1>Page Not Found</h1>
<p>The specified file was not found on this website. Please check the URL for mistakes and try again.</p>
<h3>Why am I seeing this?</h3>
<p>This page was generated by the Firebase Command-Line Interface. To modify it, edit the <code>404.html</code> file in your project's configured <code>public</code> directory.</p>
</div>
</body>
</html>

89
public/index.html Normal file
View file

@ -0,0 +1,89 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Welcome to Firebase Hosting</title>
<!-- update the version number as needed -->
<script defer src="/__/firebase/12.10.0/firebase-app-compat.js"></script>
<!-- include only the Firebase features as you need -->
<script defer src="/__/firebase/12.10.0/firebase-auth-compat.js"></script>
<script defer src="/__/firebase/12.10.0/firebase-database-compat.js"></script>
<script defer src="/__/firebase/12.10.0/firebase-firestore-compat.js"></script>
<script defer src="/__/firebase/12.10.0/firebase-functions-compat.js"></script>
<script defer src="/__/firebase/12.10.0/firebase-messaging-compat.js"></script>
<script defer src="/__/firebase/12.10.0/firebase-storage-compat.js"></script>
<script defer src="/__/firebase/12.10.0/firebase-analytics-compat.js"></script>
<script defer src="/__/firebase/12.10.0/firebase-remote-config-compat.js"></script>
<script defer src="/__/firebase/12.10.0/firebase-performance-compat.js"></script>
<!--
initialize the SDK after all desired features are loaded, set useEmulator to false
to avoid connecting the SDK to running emulators.
-->
<script defer src="/__/firebase/init.js?useEmulator=true"></script>
<style media="screen">
body { background: #ECEFF1; color: rgba(0,0,0,0.87); font-family: Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 0; }
#message { background: white; max-width: 360px; margin: 100px auto 16px; padding: 32px 24px; border-radius: 3px; }
#message h2 { color: #ffa100; font-weight: bold; font-size: 16px; margin: 0 0 8px; }
#message h1 { font-size: 22px; font-weight: 300; color: rgba(0,0,0,0.6); margin: 0 0 16px;}
#message p { line-height: 140%; margin: 16px 0 24px; font-size: 14px; }
#message a { display: block; text-align: center; background: #039be5; text-transform: uppercase; text-decoration: none; color: white; padding: 16px; border-radius: 4px; }
#message, #message a { box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); }
#load { color: rgba(0,0,0,0.4); text-align: center; font-size: 13px; }
@media (max-width: 600px) {
body, #message { margin-top: 0; background: white; box-shadow: none; }
body { border-top: 16px solid #ffa100; }
}
</style>
</head>
<body>
<div id="message">
<h2>Welcome</h2>
<h1>Firebase Hosting Setup Complete</h1>
<p>You're seeing this because you've successfully setup Firebase Hosting. Now it's time to go build something extraordinary!</p>
<a target="_blank" href="https://firebase.google.com/docs/hosting/">Open Hosting Documentation</a>
</div>
<p id="load">Firebase SDK Loading&hellip;</p>
<script>
document.addEventListener('DOMContentLoaded', function() {
const loadEl = document.querySelector('#load');
// // 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
// // The Firebase SDK is initialized and available here!
//
// firebase.auth().onAuthStateChanged(user => { });
// firebase.database().ref('/path/to/ref').on('value', snapshot => { });
// firebase.firestore().doc('/foo/bar').get().then(() => { });
// firebase.functions().httpsCallable('yourFunction')().then(() => { });
// firebase.messaging().requestPermission().then(() => { });
// firebase.storage().ref('/path/to/ref').getDownloadURL().then(() => { });
// firebase.analytics(); // call to activate
// firebase.analytics().logEvent('tutorial_completed');
// firebase.performance(); // call to activate
//
// // 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
try {
let app = firebase.app();
let features = [
'auth',
'database',
'firestore',
'functions',
'messaging',
'storage',
'analytics',
'remoteConfig',
'performance',
].filter(feature => typeof app[feature] === 'function');
loadEl.textContent = `Firebase SDK loaded with ${features.join(', ')}`;
} catch (e) {
console.error(e);
loadEl.textContent = 'Error loading the Firebase SDK, check the console.';
}
});
</script>
</body>
</html>

View file

@ -5,10 +5,10 @@ packages:
dependency: transitive
description:
name: _flutterfire_internals
sha256: cd83f7d6bd4e4c0b0b4fef802e8796784032e1cc23d7b0e982cf5d05d9bbe182
sha256: afe15ce18a287d2f89da95566e62892df339b1936bbe9b83587df45b944ee72a
url: "https://pub.dev"
source: hosted
version: "1.3.66"
version: "1.3.67"
app_links:
dependency: "direct main"
description:
@ -129,14 +129,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.2"
change_app_package_name:
dependency: "direct dev"
description:
name: change_app_package_name
sha256: "8e43b754fe960426904d77ed4c62fa8c9834deaf6e293ae40963fa447482c4c5"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
characters:
dependency: transitive
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
version: "1.4.1"
version: "1.4.0"
checked_yaml:
dependency: transitive
description:
@ -249,14 +257,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.1"
firebase_app_check:
dependency: "direct main"
description:
name: firebase_app_check
sha256: cc0bfeb003c5747e963f4f378e72c8c5c5f91a243f2e85b7da29ff42f4709996
url: "https://pub.dev"
source: hosted
version: "0.4.1+5"
firebase_app_check_platform_interface:
dependency: transitive
description:
name: firebase_app_check_platform_interface
sha256: "54166319f9121bd94b2374864e84dd54507438095fc2d42ca49f23957b16b0d1"
url: "https://pub.dev"
source: hosted
version: "0.2.1+5"
firebase_app_check_web:
dependency: transitive
description:
name: firebase_app_check_web
sha256: f8dddba09bd7786e017b58e74455886106ac7ca9e798769e2cac3e988fc2d146
url: "https://pub.dev"
source: hosted
version: "0.2.2+3"
firebase_auth:
dependency: "direct main"
description:
name: firebase_auth
sha256: b20d1540460814c5984474c1e9dd833bdbcff6ecd8d6ad86cc9da8cfd581c172
url: "https://pub.dev"
source: hosted
version: "6.1.4"
firebase_auth_platform_interface:
dependency: transitive
description:
name: firebase_auth_platform_interface
sha256: fd0225320b6bbc92460c86352d16b60aea15f9ef88292774cca97b0522ea9f72
url: "https://pub.dev"
source: hosted
version: "8.1.6"
firebase_auth_web:
dependency: transitive
description:
name: firebase_auth_web
sha256: be7dccb263b89fbda2a564de9d8193118196e8481ffb937222a025cdfdf82c40
url: "https://pub.dev"
source: hosted
version: "6.1.2"
firebase_core:
dependency: "direct main"
description:
name: firebase_core
sha256: "923085c881663ef685269b013e241b428e1fb03cdd0ebde265d9b40ff18abf80"
sha256: f0997fee80fbb6d2c658c5b88ae87ba1f9506b5b37126db64fc2e75d8e977fbb
url: "https://pub.dev"
source: hosted
version: "4.4.0"
version: "4.5.0"
firebase_core_platform_interface:
dependency: transitive
description:
@ -269,10 +325,10 @@ packages:
dependency: transitive
description:
name: firebase_core_web
sha256: "83e7356c704131ca4d8d8dd57e360d8acecbca38b1a3705c7ae46cc34c708084"
sha256: "856ca92bf2d75a63761286ab8e791bda3a85184c2b641764433b619647acfca6"
url: "https://pub.dev"
source: hosted
version: "3.4.0"
version: "3.5.0"
fixnum:
dependency: transitive
description:
@ -449,18 +505,18 @@ packages:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
version: "0.12.19"
version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
version: "0.13.0"
version: "0.11.1"
meta:
dependency: transitive
description:
@ -734,10 +790,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.dev"
source: hosted
version: "0.7.10"
version: "0.7.7"
typed_data:
dependency: transitive
description:

View file

@ -1,7 +1,7 @@
name: tetraq
description: A new Flutter project.
publish_to: 'none'
version: 1.0.2+4
version: 1.1.4+6
environment:
sdk: ^3.10.7
@ -18,17 +18,20 @@ dependencies:
flutter_localizations: # Il sistema multilingua ufficiale
sdk: flutter
firebase_core: ^4.4.0
firebase_auth: ^6.1.4 # <--- NUOVO: LA CORAZZA DI SICUREZZA!
cloud_firestore: ^6.1.2
share_plus: ^12.0.1
app_links: ^7.0.0
google_fonts: ^8.0.2
font_awesome_flutter: ^10.12.0
firebase_app_check: ^0.4.1+5
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
flutter_launcher_icons: ^0.13.1
change_app_package_name: ^1.5.0
flutter:
uses-material-design: true
@ -41,6 +44,7 @@ flutter:
- assets/images/
- assets/audio/bgm/
- assets/audio/sfx/
- assets/audio/
flutter_icons:

View file

@ -0,0 +1,11 @@
{
"applinks": {
"apps": [],
"details": [
{
"appID": "2BX6QRR7GG.com.sanza.tetraq",
"paths": [ "/join" ]
}
]
}
}

View file

@ -0,0 +1,8 @@
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.amastra.tetraq",
"sha256_cert_fingerprints": ["DA_INSERIRE_IN_FUTURO"]
}
}]