diff --git a/lib/logic/game_controller.dart b/lib/logic/game_controller.dart index d4daec4..a671845 100644 --- a/lib/logic/game_controller.dart +++ b/lib/logic/game_controller.dart @@ -37,9 +37,11 @@ class GameController extends ChangeNotifier { bool _hasSavedResult = false; Timer? _blitzTimer; - int timeLeft = 15; - final int maxTime = 15; - bool isTimeMode = true; + int timeLeft = 10; + int maxTime = 10; + String timeModeSetting = 'fixed'; // 'fixed', 'relax', 'dynamic' + bool get isTimeMode => timeModeSetting != 'relax'; + int consecutiveRematches = 0; // Contatore per la modalità Dinamica String effectText = ''; Color effectColor = Colors.transparent; @@ -56,8 +58,6 @@ class GameController extends ChangeNotifier { bool opponentWantsRematch = false; int lastMatchXP = 0; - // --- NUOVA ROADMAP DINAMICA DEGLI SBLOCCHI --- - // Aggiungi qui le tue future meccaniche! Il popup le leggerà in automatico. static const Map>> rewardsRoadmap = { 2: [{'title': 'Bomba & Oro', 'desc': 'Appaiono le caselle speciali: Oro (+2) e Bomba (-1)!', 'icon': Icons.stars, 'color': Colors.amber}], 3: [ @@ -81,7 +81,7 @@ class GameController extends ChangeNotifier { bool hasLeveledUp = false; int newlyReachedLevel = 1; - List> unlockedRewards = []; // Ora è una lista di mappe dinamiche + List> unlockedRewards = []; bool isSetupPhase = true; bool myJokerPlaced = false; @@ -124,7 +124,7 @@ class GameController extends ChangeNotifier { 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}) { + void startNewGame(int radius, {bool vsCPU = false, bool isOnline = false, String? roomCode, bool isHost = false, ArenaShape shape = ArenaShape.classic, String timeMode = 'fixed', bool isRematch = false}) { _onlineSubscription?.cancel(); _onlineSubscription = null; _blitzTimer?.cancel(); @@ -151,7 +151,29 @@ class GameController extends ChangeNotifier { this.isOnline = isOnline; this.roomCode = roomCode; this.isHost = isHost; - this.isTimeMode = timeMode; + + if (!isRematch) consecutiveRematches = 0; + this.timeModeSetting = timeMode; + + // --- LOGICA TIMER --- + if (this.isVsCPU) { + // La CPU usa sempre la sua formula basata sul Livello Profilo + int pLevel = StorageService.instance.playerLevel; + int calculatedTime = 15 - ((pLevel - 1) * 12 / 14).round(); + maxTime = calculatedTime.clamp(3, 15); + } else { + // Multiplayer e Locale + if (timeModeSetting == 'dynamic') { + // Parte da 10s e toglie 2s per ogni rivincita (Minimo 2s) + maxTime = max(2, 10 - (consecutiveRematches * 2)); + } else if (timeModeSetting == 'relax') { + maxTime = 0; // Il timer non scatterà + } else { + maxTime = 10; // Fisso 10s + } + } + timeLeft = maxTime; + // ------------------- int finalRadius = radius; ArenaShape finalShape = shape; @@ -330,10 +352,9 @@ class GameController extends ChangeNotifier { void _startTimer() { _blitzTimer?.cancel(); - if (isSetupPhase) return; + if (isSetupPhase || !isTimeMode) return; timeLeft = maxTime; - if (!isTimeMode) { notifyListeners(); return; } _blitzTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (isGameOver || isCPUThinking) { timer.cancel(); return; } @@ -407,9 +428,14 @@ class GameController extends ChangeNotifier { bool p2Rematch = data['p2_rematch'] ?? false; opponentWantsRematch = isHost ? p2Rematch : p1Rematch; + // === LA RIVINCITA INCREMENTA IL CONTATORE DELLA MODALITA' DINAMICA === if (data['status'] == 'playing' && (data['moves'] as List).isEmpty && rematchRequested) { currentSeed = data['seed']; - startNewGame(data['radius'], isOnline: true, roomCode: roomCode, isHost: isHost, shape: ArenaShape.values.firstWhere((e) => e.name == data['shape']), timeMode: data['timeMode']); + consecutiveRematches++; + + String tMode = data['timeMode'] is String ? data['timeMode'] : (data['timeMode'] == true ? 'fixed' : 'relax'); + + startNewGame(data['radius'], isOnline: true, roomCode: roomCode, isHost: isHost, shape: ArenaShape.values.firstWhere((e) => e.name == data['shape']), timeMode: tMode, isRematch: true); return; } @@ -442,7 +468,9 @@ class GameController extends ChangeNotifier { String shapeStr = data['shape'] ?? 'classic'; ArenaShape hostShape = ArenaShape.values.firstWhere((e) => e.name == shapeStr, orElse: () => ArenaShape.classic); onlineShape = hostShape; - isTimeMode = data['timeMode'] ?? true; + + String hostTimeMode = data['timeMode'] is String ? data['timeMode'] : (data['timeMode'] == true ? 'fixed' : 'relax'); + timeModeSetting = hostTimeMode; if (!rematchRequested && (hostLevel > currentMatchLevel || (isOnline && currentSeed == null && hostSeed != null) || (hostSeed != null && hostSeed != currentSeed))) { currentMatchLevel = hostLevel; currentSeed = hostSeed; @@ -566,7 +594,6 @@ class GameController extends ChangeNotifier { } } - // --- LOGICA DI ESTRAZIONE SBLOCCHI DINAMICA --- List> _getUnlocks(int oldLevel, int newLevel) { List> unlocks = []; for(int i = oldLevel + 1; i <= newLevel; i++) { @@ -634,6 +661,6 @@ class GameController extends ChangeNotifier { void increaseLevelAndRestart() { cpuLevel++; StorageService.instance.saveCpuLevel(cpuLevel); - startNewGame(board.radius, vsCPU: true, shape: board.shape, timeMode: isTimeMode); + startNewGame(board.radius, vsCPU: true, shape: board.shape, timeMode: timeModeSetting); } } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index b5ac3ec..fd47b5a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,12 +15,8 @@ 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'; - -// --- NUOVI IMPORT PER GLI AGGIORNAMENTI --- import 'package:upgrader/upgrader.dart'; import 'package:in_app_update/in_app_update.dart'; - -// --- IMPORT PER IL SUPPORTO MULTILINGUA --- import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:tetraq/l10n/app_localizations.dart'; @@ -69,15 +65,11 @@ class TetraQApp extends StatelessWidget { 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, - // ------------------------------------ - // Avvolgiamo la HomeScreen nel nostro nuovo gestore di aggiornamenti! - home: const UpdateWrapper(child: HomeScreen()), + // Nessun modificatore const richiesto qui. + home: UpdateWrapper(child: HomeScreen()), ); } } @@ -97,7 +89,6 @@ class _UpdateWrapperState extends State { @override void initState() { super.initState(); - // Controlla gli aggiornamenti in background solo se siamo su Android if (!kIsWeb && Platform.isAndroid) { _checkForAndroidUpdate(); } @@ -107,12 +98,10 @@ class _UpdateWrapperState extends State { try { final info = await InAppUpdate.checkForUpdate(); if (info.updateAvailability == UpdateAvailability.updateAvailable) { - // Se possibile, fai scaricare l'aggiornamento in background mentre l'utente gioca if (info.flexibleUpdateAllowed) { await InAppUpdate.startFlexibleUpdate(); - await InAppUpdate.completeFlexibleUpdate(); // Chiede il riavvio rapido dell'app + await InAppUpdate.completeFlexibleUpdate(); } - // Se l'aggiornamento è impostato come critico dalla console di Google Play else if (info.immediateUpdateAllowed) { await InAppUpdate.performImmediateUpdate(); } @@ -124,22 +113,17 @@ class _UpdateWrapperState extends State { @override Widget build(BuildContext context) { - // Su iOS e macOS usiamo "upgrader" che si occupa di mostrare il pop-up nativo if (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) { return UpgradeAlert( dialogStyle: (Platform.isIOS || Platform.isMacOS) ? UpgradeDialogStyle.cupertino : UpgradeDialogStyle.material, - showIgnore: false, // <-- Spostato qui - showLater: true, // <-- Spostato qui - upgrader: Upgrader( - // debugDisplayAlways: true, // <--- Scommenta questa riga se vuoi testare la UI del pop-up sul Mac ora! - ), + showIgnore: false, + showLater: true, + upgrader: Upgrader(), child: widget.child, ); } - - // Su Android restituiamo la UI normale (l'aggiornamento è gestito nel background da initState) return widget.child; } } \ No newline at end of file diff --git a/lib/services/multiplayer_service.dart b/lib/services/multiplayer_service.dart index 9e3a307..71d253d 100644 --- a/lib/services/multiplayer_service.dart +++ b/lib/services/multiplayer_service.dart @@ -15,7 +15,8 @@ class MultiplayerService { CollectionReference get _gamesCollection => _firestore.collection('games'); CollectionReference get _invitesCollection => _firestore.collection('invites'); - Future createGameRoom(int boardRadius, String hostName, String shapeName, bool isTimeMode, {bool isPublic = true}) async { + // --- MODIFICA QUI: bool isTimeMode è diventato String timeMode --- + Future createGameRoom(int boardRadius, String hostName, String shapeName, String timeMode, {bool isPublic = true}) async { String roomCode = _generateRoomCode(); int randomSeed = Random().nextInt(1000000); @@ -31,7 +32,7 @@ class MultiplayerService { 'hostUid': _auth.currentUser?.uid, 'guestName': '', 'shape': shapeName, - 'timeMode': isTimeMode, + 'timeMode': timeMode, // Salva la stringa ('fixed', 'relax' o 'dynamic') 'isPublic': isPublic, 'p1_reaction': null, 'p2_reaction': null, diff --git a/lib/ui/game/game_screen.dart b/lib/ui/game/game_screen.dart index a1b1958..50306ff 100644 --- a/lib/ui/game/game_screen.dart +++ b/lib/ui/game/game_screen.dart @@ -164,7 +164,7 @@ class _GameScreenState extends State with TickerProviderStateMixin { 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); }, + onPressed: () { controller.startNewGame(controller.board.radius, vsCPU: controller.isVsCPU, shape: controller.board.shape, timeMode: controller.timeModeSetting); }, child: Text("RIGIOCA", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 2))), ), const SizedBox(height: 12), diff --git a/lib/ui/home/home_modals.dart b/lib/ui/home/home_modals.dart index c1dda13..064846d 100644 --- a/lib/ui/home/home_modals.dart +++ b/lib/ui/home/home_modals.dart @@ -33,7 +33,7 @@ class HomeModals { showDialog( context: context, - barrierDismissible: false, // Impedisce di chiudere tappando fuori + barrierDismissible: false, barrierColor: Colors.black.withOpacity(0.8), builder: (dialogContext) { final themeManager = dialogContext.watch(); @@ -206,7 +206,6 @@ class HomeModals { if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) dialogContent = AnimatedCyberBorder(child: dialogContent); - // LA PROTEZIONE ANTI-BACK DI ANDROID: Impedisce l'uscita non autorizzata return PopScope( canPop: false, child: Dialog(backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.all(20), child: dialogContent) @@ -217,8 +216,224 @@ class HomeModals { ); } + // --- SELETTORE DEL TEMPO A 3 OPZIONI --- + static Widget _buildTimeOption(String label, String sub, String value, String current, ThemeColors theme, AppThemeType type, VoidCallback onTap) { + bool isSel = value == current; + return Expanded( + child: GestureDetector( + onTap: onTap, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 4), + height: 50, + decoration: BoxDecoration( + color: isSel ? Colors.orange.shade600 : (type == AppThemeType.doodle ? Colors.white : theme.text.withOpacity(0.05)), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: isSel ? Colors.orange.shade800 : (type == AppThemeType.doodle ? const Color(0xFF111122) : Colors.white24), width: isSel ? 2 : 1.5), + boxShadow: isSel && type != AppThemeType.doodle ? [BoxShadow(color: Colors.orange.withOpacity(0.5), blurRadius: 8)] : [], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(label, style: getSharedTextStyle(type, TextStyle(color: isSel ? Colors.white : (type == AppThemeType.doodle ? const Color(0xFF111122) : theme.text), fontWeight: FontWeight.w900, fontSize: 13))), + if (sub.isNotEmpty) Text(sub, style: getSharedTextStyle(type, TextStyle(color: isSel ? Colors.white70 : (type == AppThemeType.doodle ? Colors.black54 : theme.text.withOpacity(0.5)), fontWeight: FontWeight.bold, fontSize: 8))), + ], + ), + ), + ), + ); + } + + static void showChallengeSetupDialog(BuildContext context, String targetName, Function(int radius, ArenaShape shape, String timeMode) onStart) { + int localRadius = 4; ArenaShape localShape = ArenaShape.classic; String localTimeMode = 'fixed'; + bool isChaosUnlocked = StorageService.instance.playerLevel >= 7; + + showDialog( + context: context, barrierColor: Colors.black.withOpacity(0.8), + builder: (ctx) { + final themeManager = ctx.watch(); + final theme = themeManager.currentColors; final themeType = themeManager.currentThemeType; + Color inkColor = const Color(0xFF111122); + + return StatefulBuilder( + builder: (context, setStateDialog) { + Widget dialogContent = themeType == AppThemeType.doodle + ? Transform.rotate( + angle: 0.015, + child: CustomPaint( + painter: DoodleBackgroundPainter(fillColor: Colors.white.withOpacity(0.95), strokeColor: inkColor, seed: 200), + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Padding( + padding: const EdgeInsets.all(25.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("SFIDA $targetName", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: theme.playerRed, letterSpacing: 2))), + const SizedBox(height: 10), + Text("IMPOSTAZIONI STANZA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), + const SizedBox(height: 25), + + Text("FORMA ARENA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 15), + Wrap( + spacing: 12, runSpacing: 12, alignment: WrapAlignment.center, + children: [ + NeonShapeButton(icon: Icons.diamond_outlined, label: 'Rombo', isSelected: localShape == ArenaShape.classic, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.classic)), + NeonShapeButton(icon: Icons.add, label: 'Croce', isSelected: localShape == ArenaShape.cross, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.cross)), + NeonShapeButton(icon: Icons.donut_large, label: 'Buco', isSelected: localShape == ArenaShape.donut, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.donut)), + NeonShapeButton(icon: Icons.hourglass_bottom, label: 'Clessidra', isSelected: localShape == ArenaShape.hourglass, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.hourglass)), + NeonShapeButton(icon: Icons.all_inclusive, label: 'Caos', isSelected: localShape == ArenaShape.chaos, theme: theme, themeType: themeType, isSpecial: true, isLocked: !isChaosUnlocked, onTap: () => setStateDialog(() => localShape = ArenaShape.chaos)), + ], + ), + const SizedBox(height: 25), Divider(color: inkColor.withOpacity(0.3), thickness: 2.5), const SizedBox(height: 20), + + Text("GRANDEZZA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 15), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + NeonSizeButton(label: 'S', isSelected: localRadius == 3, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 3)), + NeonSizeButton(label: 'M', isSelected: localRadius == 4, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 4)), + NeonSizeButton(label: 'L', isSelected: localRadius == 5, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 5)), + NeonSizeButton(label: 'MAX', isSelected: localRadius == 6, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 6)), + ], + ), + const SizedBox(height: 25), Divider(color: inkColor.withOpacity(0.3), thickness: 2.5), const SizedBox(height: 20), + + Text("TEMPO E OPZIONI", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 10), + Row( + children: [ + _buildTimeOption('10s', 'FISSO', 'fixed', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'fixed')), + _buildTimeOption('RELAX', 'INFINITO', 'relax', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'relax')), + _buildTimeOption('DINAMICO', '-2s A PARTITA', 'dynamic', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'dynamic')), + ], + ), + const SizedBox(height: 35), + + Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () { + Navigator.pop(ctx); + onStart(localRadius, localShape, localTimeMode); + }, + child: CustomPaint(painter: DoodleBackgroundPainter(fillColor: theme.playerRed, strokeColor: inkColor, seed: 300), child: Container(height: 55, alignment: Alignment.center, child: Text("AVVIA", style: getSharedTextStyle(themeType, const TextStyle(fontSize: 18, fontWeight: FontWeight.w900, letterSpacing: 2.0, color: Colors.white))))), + ), + ), + const SizedBox(width: 15), + Expanded( + child: GestureDetector( + onTap: () => Navigator.pop(ctx), + child: CustomPaint(painter: DoodleBackgroundPainter(fillColor: Colors.grey.shade400, strokeColor: inkColor, seed: 301), child: Container(height: 55, alignment: Alignment.center, child: Text("ANNULLA", style: getSharedTextStyle(themeType, const TextStyle(fontSize: 18, fontWeight: FontWeight.w900, letterSpacing: 2.0, color: Colors.white))))), + ), + ), + ], + ) + ], + ), + ), + ), + ), + ) + : Container( + 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: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("SFIDA $targetName", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 24, fontWeight: FontWeight.w900, color: theme.playerRed, letterSpacing: 2))), + const SizedBox(height: 10), + Text("IMPOSTAZIONI STANZA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), + const SizedBox(height: 20), + + Text("FORMA ARENA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10), + Wrap( + spacing: 10, runSpacing: 10, alignment: WrapAlignment.center, + children: [ + NeonShapeButton(icon: Icons.diamond_outlined, label: 'Rombo', isSelected: localShape == ArenaShape.classic, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.classic)), + NeonShapeButton(icon: Icons.add, label: 'Croce', isSelected: localShape == ArenaShape.cross, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.cross)), + NeonShapeButton(icon: Icons.donut_large, label: 'Buco', isSelected: localShape == ArenaShape.donut, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.donut)), + NeonShapeButton(icon: Icons.hourglass_bottom, label: 'Clessidra', isSelected: localShape == ArenaShape.hourglass, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.hourglass)), + NeonShapeButton(icon: Icons.all_inclusive, label: 'Caos', isSelected: localShape == ArenaShape.chaos, theme: theme, themeType: themeType, isSpecial: true, isLocked: !isChaosUnlocked, onTap: () => setStateDialog(() => localShape = ArenaShape.chaos)), + ], + ), + const SizedBox(height: 20), Divider(color: Colors.white.withOpacity(0.05), thickness: 2), const SizedBox(height: 20), + + Text("GRANDEZZA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + NeonSizeButton(label: 'S', isSelected: localRadius == 3, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 3)), + NeonSizeButton(label: 'M', isSelected: localRadius == 4, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 4)), + NeonSizeButton(label: 'L', isSelected: localRadius == 5, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 5)), + NeonSizeButton(label: 'MAX', isSelected: localRadius == 6, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 6)), + ], + ), + const SizedBox(height: 20), Divider(color: Colors.white.withOpacity(0.05), thickness: 2), const SizedBox(height: 20), + + Text("TEMPO E OPZIONI", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10), + Row( + children: [ + _buildTimeOption('10s', 'FISSO', 'fixed', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'fixed')), + _buildTimeOption('RELAX', 'INFINITO', 'relax', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'relax')), + _buildTimeOption('DINAMICO', '-2s A PARTITA', 'dynamic', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'dynamic')), + ], + ), + const SizedBox(height: 30), + + Row( + children: [ + Expanded( + child: SizedBox( + height: 55, + child: ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: theme.playerRed, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), + onPressed: () { + Navigator.pop(ctx); + onStart(localRadius, localShape, localTimeMode); + }, + child: const Text("AVVIA", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2)), + ), + ), + ), + const SizedBox(width: 15), + Expanded( + child: SizedBox( + height: 55, + child: ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: Colors.grey.shade800, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), + onPressed: () => Navigator.pop(ctx), + child: const Text("ANNULLA", 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: 15, vertical: 20), child: dialogContent); + }, + ); + } + ); + } + static void showMatchSetupDialog(BuildContext context, bool isVsCPU) { - int localRadius = 4; ArenaShape localShape = ArenaShape.classic; bool localTimeMode = true; + int localRadius = 4; ArenaShape localShape = ArenaShape.classic; String localTimeMode = 'fixed'; bool isChaosUnlocked = StorageService.instance.playerLevel >= 7; final loc = AppLocalizations.of(context)!; @@ -249,7 +464,7 @@ class HomeModals { if (isVsCPU) ...[ Icon(Icons.smart_toy, size: 50, color: inkColor.withOpacity(0.6)), const SizedBox(height: 10), Text("MODALITÀ CAMPAGNA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.w900, color: inkColor))), const SizedBox(height: 10), - Text("Livello CPU: ${StorageService.instance.cpuLevel}\nForma e dimensioni si adatteranno alla tua bravura!", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 13, color: inkColor.withOpacity(0.8), height: 1.4))), const SizedBox(height: 25), + Text("Livello CPU: ${StorageService.instance.cpuLevel}\nForma e tempo si adatteranno alla tua bravura!", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 13, color: inkColor.withOpacity(0.8), height: 1.4))), const SizedBox(height: 25), Divider(color: inkColor.withOpacity(0.3), thickness: 2.5), const SizedBox(height: 20), ] else ...[ Text("FORMA ARENA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 15), @@ -276,10 +491,17 @@ class HomeModals { ], ), const SizedBox(height: 25), Divider(color: inkColor.withOpacity(0.3), thickness: 2.5), const SizedBox(height: 20), - ], - Text("TEMPO", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 10), - NeonTimeSwitch(isTimeMode: localTimeMode, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localTimeMode = !localTimeMode)), const SizedBox(height: 35), + // TEMPO È ORA ESCLUSIVO PER IL MULTIPLAYER + Text("TEMPO", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 10), + Row( + children: [ + _buildTimeOption('10s', 'FISSO', 'fixed', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'fixed')), + _buildTimeOption('RELAX', 'INFINITO', 'relax', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'relax')), + _buildTimeOption('DINAMICO', '-2s A PARTITA', 'dynamic', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'dynamic')), + ], + ), const SizedBox(height: 35), + ], Transform.rotate( angle: -0.02, @@ -313,7 +535,7 @@ class HomeModals { if (isVsCPU) ...[ Icon(Icons.smart_toy, size: 50, color: theme.playerBlue), const SizedBox(height: 10), Text("MODALITÀ CAMPAGNA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 1.5))), const SizedBox(height: 10), - Text("Livello CPU: ${StorageService.instance.cpuLevel}\nForma e dimensioni si adatteranno alla tua bravura!", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 13, color: theme.text.withOpacity(0.7), height: 1.4))), const SizedBox(height: 20), + Text("Livello CPU: ${StorageService.instance.cpuLevel}\nForma e tempo si adatteranno alla tua bravura!", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 13, color: theme.text.withOpacity(0.7), height: 1.4))), const SizedBox(height: 20), Divider(color: Colors.white.withOpacity(0.05), thickness: 2), const SizedBox(height: 20), ] else ...[ Text("FORMA ARENA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10), @@ -340,10 +562,16 @@ class HomeModals { ], ), const SizedBox(height: 20), Divider(color: Colors.white.withOpacity(0.05), thickness: 2), const SizedBox(height: 20), - ], - Text("TEMPO", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10), - NeonTimeSwitch(isTimeMode: localTimeMode, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localTimeMode = !localTimeMode)), const SizedBox(height: 30), + Text("TEMPO", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10), + Row( + children: [ + _buildTimeOption('10s', 'FISSO', 'fixed', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'fixed')), + _buildTimeOption('RELAX', 'INFINITO', 'relax', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'relax')), + _buildTimeOption('DINAMICO', '-2s A PARTITA', 'dynamic', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'dynamic')), + ], + ), const SizedBox(height: 30), + ], SizedBox( width: double.infinity, height: 60, @@ -376,7 +604,7 @@ class HomeModals { required bool isPublicRoom, required int selectedRadius, required ArenaShape selectedShape, - required bool isTimeMode, + required String selectedTimeMode, required MultiplayerService multiplayerService, required VoidCallback onRoomStarted, required VoidCallback onCleanup, @@ -410,9 +638,9 @@ class HomeModals { child: Column( children: [ 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: getSharedTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.w900, fontSize: 18))), + Text(isPublicRoom ? "Sei in Bacheca!" : "Invito inviato", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.w900, fontSize: 18))), const SizedBox(height: 8), - Text(isPublicRoom ? "Aspettiamo che uno sfidante si unisca dalla lobby pubblica." : "Condividi il codice. La partita inizierà appena si unirà.", textAlign: TextAlign.center, style: getSharedTextStyle(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." : "Attendi che il tuo amico accetti la sfida. Non chiudere questa finestra.", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.8), fontSize: 14, height: 1.5))), ], ), ), @@ -444,8 +672,8 @@ class HomeModals { onRoomStarted(); WidgetsBinding.instance.addPostFrameCallback((_) { Navigator.pop(ctx); - context.read().startNewGame(selectedRadius, isOnline: true, roomCode: code, isHost: true, shape: selectedShape, timeMode: isTimeMode); - Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const GameScreen())); + context.read().startNewGame(selectedRadius, isOnline: true, roomCode: code, isHost: true, shape: selectedShape, timeMode: selectedTimeMode); + Navigator.push(context, MaterialPageRoute(builder: (_) => const GameScreen())); }); } } diff --git a/lib/ui/home/home_screen.dart b/lib/ui/home/home_screen.dart index 9764d53..3bdb984 100644 --- a/lib/ui/home/home_screen.dart +++ b/lib/ui/home/home_screen.dart @@ -3,7 +3,7 @@ // =========================================================================== import 'dart:ui'; -import 'dart:math'; // Aggiunto per generare il codice della stanza randomico +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter/services.dart'; @@ -45,13 +45,12 @@ class _HomeScreenState extends State with WidgetsBindingObserver { late AppLinks _appLinks; StreamSubscription? _linkSubscription; StreamSubscription? _favoritesSubscription; - StreamSubscription? _invitesSubscription; // <--- Nuovo Listener per gli inviti in arrivo + StreamSubscription? _invitesSubscription; Map _lastOnlineNotifications = {}; final int _selectedRadius = 4; final ArenaShape _selectedShape = ArenaShape.classic; - final bool _isTimeMode = true; final bool _isPublicRoom = true; bool _isLoading = false; @@ -68,12 +67,12 @@ class _HomeScreenState extends State with WidgetsBindingObserver { if (FirebaseAuth.instance.currentUser == null) { HomeModals.showNameDialog(context, () { StorageService.instance.syncLeaderboard(); - _listenToInvites(); // <--- Ascoltiamo gli inviti appena loggati + _listenToInvites(); setState(() {}); }); } else { StorageService.instance.syncLeaderboard(); - _listenToInvites(); // <--- Ascoltiamo gli inviti se eravamo già loggati + _listenToInvites(); } _checkThemeSafety(); }); @@ -96,7 +95,7 @@ class _HomeScreenState extends State with WidgetsBindingObserver { _cleanupGhostRoom(); _linkSubscription?.cancel(); _favoritesSubscription?.cancel(); - _invitesSubscription?.cancel(); // <--- Chiusura Listener + _invitesSubscription?.cancel(); super.dispose(); } @@ -218,9 +217,6 @@ class _HomeScreenState extends State with WidgetsBindingObserver { overlay.insert(entry); } - // ========================================================================= - // SISTEMA INVITI DIRETTO TRAMITE FIRESTORE - // ========================================================================= void _listenToInvites() { final user = FirebaseAuth.instance.currentUser; if (user == null) return; @@ -241,7 +237,6 @@ class _HomeScreenState extends State with WidgetsBindingObserver { String from = data['fromName']; String inviteId = change.doc.id; - // Filtro sicurezza: Evita di mostrare inviti fantasma vecchi di oltre 2 minuti Timestamp? ts = data['timestamp']; if (ts != null) { if (DateTime.now().difference(ts.toDate()).inMinutes > 2) { @@ -296,35 +291,41 @@ class _HomeScreenState extends State with WidgetsBindingObserver { ); } - Future _sendChallenge(String targetUid, String targetName) async { + void _startDirectChallengeFlow(String targetUid, String targetName) { + HomeModals.showChallengeSetupDialog( + context, + targetName, + (int radius, ArenaShape shape, String timeMode) { + _executeSendChallenge(targetUid, targetName, radius, shape, timeMode); + } + ); + } + + Future _executeSendChallenge(String targetUid, String targetName, int radius, ArenaShape shape, String timeMode) async { setState(() => _isLoading = true); - // Generiamo un codice stanza casuale univoco const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; final rnd = Random(); String roomCode = String.fromCharCodes(Iterable.generate(5, (_) => chars.codeUnitAt(rnd.nextInt(chars.length)))); try { - // 1. IL SEGRETO DELLA SINCRONIZZAZIONE: Generiamo un "Seme" (Seed) comune! int gameSeed = rnd.nextInt(9999999); - // Creiamo la stanza privata con tutti i crismi (e il seed!) await FirebaseFirestore.instance.collection('games').doc(roomCode).set({ 'status': 'waiting', 'hostName': StorageService.instance.playerName, 'hostUid': FirebaseAuth.instance.currentUser?.uid, - 'radius': 4, - 'shape': 'classic', - 'timeMode': true, - 'isPublic': false, // È una stanza privata + 'radius': radius, + 'shape': shape.name, + 'timeMode': timeMode, + 'isPublic': false, 'createdAt': FieldValue.serverTimestamp(), 'players': [FirebaseAuth.instance.currentUser?.uid], 'turn': 0, 'moves': [], - 'seed': gameSeed, // <--- ECCO IL PEZZO MANCANTE CHE GARANTISCE GRIGLIE IDENTICHE! + 'seed': gameSeed, }); - // 2. Inviamo l'invito al nostro avversario await FirebaseFirestore.instance.collection('invites').add({ 'toUid': targetUid, 'fromName': StorageService.instance.playerName, @@ -334,19 +335,17 @@ class _HomeScreenState extends State with WidgetsBindingObserver { setState(() => _isLoading = false); - // 3. Apriamo il radar d'attesa (che ascolta quando lui accetta) if (mounted) { HomeModals.showWaitingDialog( context: context, code: roomCode, isPublicRoom: false, - selectedRadius: 4, - selectedShape: ArenaShape.classic, - isTimeMode: true, + selectedRadius: radius, + selectedShape: shape, + selectedTimeMode: timeMode, multiplayerService: _multiplayerService, onRoomStarted: () {}, onCleanup: () { - // Se noi annulliamo, cancelliamo la stanza FirebaseFirestore.instance.collection('games').doc(roomCode).delete(); } ); @@ -358,7 +357,6 @@ class _HomeScreenState extends State with WidgetsBindingObserver { } } } - // ========================================================================= Future _joinRoomByCode(String code) async { if (_isLoading) return; @@ -375,15 +373,16 @@ class _HomeScreenState extends State with WidgetsBindingObserver { setState(() => _isLoading = false); if (roomData != null) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Stanza trovata! Partita in avvio..."), backgroundColor: Colors.green)); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("La sfida inizierà a breve..."), backgroundColor: Colors.green)); int hostRadius = roomData['radius'] ?? 4; String shapeStr = roomData['shape'] ?? 'classic'; ArenaShape hostShape = ArenaShape.values.firstWhere((e) => e.name == shapeStr, orElse: () => ArenaShape.classic); - bool hostTimeMode = roomData['timeMode'] ?? true; + + String hostTimeMode = roomData['timeMode'] is String ? roomData['timeMode'] : (roomData['timeMode'] == true ? 'fixed' : 'relax'); context.read().startNewGame(hostRadius, isOnline: true, roomCode: code, isHost: false, shape: hostShape, timeMode: hostTimeMode); - Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const GameScreen())); + Navigator.push(context, MaterialPageRoute(builder: (_) => const GameScreen())); } else { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Stanza non trovata, piena o partita già iniziata.", style: TextStyle(color: Colors.white)), backgroundColor: Colors.red)); } @@ -624,7 +623,7 @@ class _HomeScreenState extends State with WidgetsBindingObserver { Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded(child: MusicKnobCard(title: loc.leaderboardTitle, icon: FontAwesomeIcons.compactDisc, iconColor: Colors.amber, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => LeaderboardDialog(onChallenge: _sendChallenge)))), + Expanded(child: MusicKnobCard(title: loc.leaderboardTitle, icon: FontAwesomeIcons.compactDisc, iconColor: Colors.amber, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => LeaderboardDialog(onChallenge: _startDirectChallengeFlow)))), Expanded(child: MusicKnobCard(title: loc.questsTitle, icon: FontAwesomeIcons.microphoneLines, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => const QuestsDialog()))), Expanded(child: MusicKnobCard(title: loc.themesTitle, icon: FontAwesomeIcons.palette, themeType: themeType, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => SettingsScreen())))), Expanded(child: MusicKnobCard(title: loc.tutorialTitle, icon: FontAwesomeIcons.bookOpen, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => const TutorialDialog()))), @@ -643,7 +642,7 @@ class _HomeScreenState extends State with WidgetsBindingObserver { Row( children: [ - Expanded(child: _buildCyberCard(FeatureCard(title: loc.leaderboardTitle, subtitle: "Top 50 Globale", icon: Icons.leaderboard, color: Colors.amber.shade200, theme: theme, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => LeaderboardDialog(onChallenge: _sendChallenge)), compact: true), themeType)), + Expanded(child: _buildCyberCard(FeatureCard(title: loc.leaderboardTitle, subtitle: "Top 50 Globale", icon: Icons.leaderboard, color: Colors.amber.shade200, theme: theme, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => LeaderboardDialog(onChallenge: _startDirectChallengeFlow)), compact: true), themeType)), const SizedBox(width: 12), Expanded(child: _buildCyberCard(FeatureCard(title: loc.questsTitle, subtitle: "Missioni", icon: Icons.assignment_turned_in, color: Colors.green.shade200, theme: theme, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => const QuestsDialog()), compact: true), themeType)), ], @@ -744,7 +743,6 @@ class FullScreenGridPainter extends CustomPainter { bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } -// --- WIDGET POPUP AMICO ONLINE (Ripristinato in coda!) --- class FavoriteOnlinePopup extends StatefulWidget { final String name; final VoidCallback onDismiss; @@ -768,7 +766,6 @@ class _FavoriteOnlinePopupState extends State with SingleTi _controller.forward(); - // Chiude il popup automaticamente dopo 3 secondi Future.delayed(const Duration(seconds: 3), () { if (mounted) { _controller.reverse().then((_) => widget.onDismiss()); diff --git a/lib/ui/multiplayer/lobby_screen.dart b/lib/ui/multiplayer/lobby_screen.dart index 459bfbd..0baf564 100644 --- a/lib/ui/multiplayer/lobby_screen.dart +++ b/lib/ui/multiplayer/lobby_screen.dart @@ -16,7 +16,7 @@ import '../../services/multiplayer_service.dart'; import '../../services/storage_service.dart'; import '../game/game_screen.dart'; import '../../widgets/painters.dart'; -import '../../widgets/cyber_border.dart'; // <--- ECCO L'IMPORT MANCANTE! +import '../../widgets/cyber_border.dart'; import 'lobby_widgets.dart'; class LobbyScreen extends StatefulWidget { @@ -40,7 +40,9 @@ class _LobbyScreenState extends State with WidgetsBindingObserver { int _selectedRadius = 4; ArenaShape _selectedShape = ArenaShape.classic; - bool _isTimeMode = true; + + String _timeModeSetting = 'fixed'; + bool _isPublicRoom = true; bool _roomStarted = false; @@ -87,7 +89,7 @@ class _LobbyScreenState extends State with WidgetsBindingObserver { try { String code = await _multiplayerService.createGameRoom( - _selectedRadius, _playerName, _selectedShape.name, _isTimeMode, isPublic: _isPublicRoom + _selectedRadius, _playerName, _selectedShape.name, _timeModeSetting, isPublic: _isPublicRoom ); if (!mounted) return; @@ -108,7 +110,7 @@ class _LobbyScreenState extends State with WidgetsBindingObserver { try { String code = await _multiplayerService.createGameRoom( - _selectedRadius, _playerName, _selectedShape.name, _isTimeMode, isPublic: _isPublicRoom + _selectedRadius, _playerName, _selectedShape.name, _timeModeSetting, isPublic: _isPublicRoom ); await _multiplayerService.sendInvite(targetUid, code, _playerName); @@ -145,7 +147,8 @@ class _LobbyScreenState extends State with WidgetsBindingObserver { int hostRadius = roomData['radius'] ?? 4; String shapeStr = roomData['shape'] ?? 'classic'; ArenaShape hostShape = ArenaShape.values.firstWhere((e) => e.name == shapeStr, orElse: () => ArenaShape.classic); - bool hostTimeMode = roomData['timeMode'] ?? true; + + String hostTimeMode = roomData['timeMode'] is String ? roomData['timeMode'] : (roomData['timeMode'] == true ? 'fixed' : 'relax'); context.read().startNewGame(hostRadius, isOnline: true, roomCode: code, isHost: false, shape: hostShape, timeMode: hostTimeMode); Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const GameScreen())); @@ -276,7 +279,7 @@ class _LobbyScreenState extends State with WidgetsBindingObserver { _roomStarted = true; WidgetsBinding.instance.addPostFrameCallback((_) { Navigator.pop(context); - context.read().startNewGame(_selectedRadius, isOnline: true, roomCode: code, isHost: true, shape: _selectedShape, timeMode: _isTimeMode); + context.read().startNewGame(_selectedRadius, isOnline: true, roomCode: code, isHost: true, shape: _selectedShape, timeMode: _timeModeSetting); Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const GameScreen())); }); } @@ -314,6 +317,32 @@ class _LobbyScreenState extends State with WidgetsBindingObserver { ); } + Widget _buildTimeOption(String label, String sub, String value, ThemeColors theme, AppThemeType type) { + bool isSel = value == _timeModeSetting; + return Expanded( + child: GestureDetector( + onTap: () => setState(() => _timeModeSetting = value), + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 4), + height: 50, + decoration: BoxDecoration( + color: isSel ? Colors.orange.shade600 : (type == AppThemeType.doodle ? Colors.white : theme.text.withOpacity(0.05)), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: isSel ? Colors.orange.shade800 : (type == AppThemeType.doodle ? const Color(0xFF111122) : Colors.white24), width: isSel ? 2 : 1.5), + boxShadow: isSel && type != AppThemeType.doodle ? [BoxShadow(color: Colors.orange.withOpacity(0.5), blurRadius: 8)] : [], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(label, style: getLobbyTextStyle(type, TextStyle(color: isSel ? Colors.white : (type == AppThemeType.doodle ? const Color(0xFF111122) : theme.text), fontWeight: FontWeight.w900, fontSize: 13))), + if (sub.isNotEmpty) Text(sub, style: getLobbyTextStyle(type, TextStyle(color: isSel ? Colors.white70 : (type == AppThemeType.doodle ? Colors.black54 : theme.text.withOpacity(0.5)), fontWeight: FontWeight.bold, fontSize: 8))), + ], + ), + ), + ), + ); + } + @override Widget build(BuildContext context) { final themeManager = context.watch(); @@ -329,16 +358,15 @@ class _LobbyScreenState extends State with WidgetsBindingObserver { bool isChaosUnlocked = StorageService.instance.playerLevel >= 7; - // --- MODIFICA COLORE SFONDO HOST PANEL --- Color panelBackgroundColor = Colors.transparent; if (themeType == AppThemeType.cyberpunk) { panelBackgroundColor = Colors.black.withOpacity(0.1); } else if (themeType == AppThemeType.doodle) { panelBackgroundColor = Colors.white.withOpacity(0.5); } else if (themeType == AppThemeType.grimorio) { - panelBackgroundColor = Colors.white.withOpacity(0.2); // Sfumatura bianca leggera per Grimorio + panelBackgroundColor = Colors.white.withOpacity(0.2); } else if (themeType == AppThemeType.arcade) { - panelBackgroundColor = Colors.black.withOpacity(0.4); // <-- AGGIUNGI QUESTO PER L'ARCADE + panelBackgroundColor = Colors.black.withOpacity(0.4); } @@ -405,7 +433,9 @@ class _LobbyScreenState extends State with WidgetsBindingObserver { Row( children: [ - Expanded(child: NeonTimeSwitch(isTimeMode: _isTimeMode, theme: theme, themeType: themeType, onTap: () => setState(() => _isTimeMode = !_isTimeMode))), + _buildTimeOption('10s', 'FISSO', 'fixed', theme, themeType), + _buildTimeOption('RELAX', 'INFINITO', 'relax', theme, themeType), + _buildTimeOption('DINAMICO', '-2s', 'dynamic', theme, themeType), ], ), const SizedBox(height: 10), @@ -583,7 +613,11 @@ class _LobbyScreenState extends State with WidgetsBindingObserver { String host = data['hostName'] ?? 'Sconosciuto'; int r = data['radius'] ?? 4; String shapeStr = data['shape'] ?? 'classic'; - bool time = data['timeMode'] ?? true; + + String tMode = data['timeMode'] is String ? data['timeMode'] : (data['timeMode'] == true ? 'fixed' : 'relax'); + String prettyTime = "10s"; + if (tMode == 'relax') prettyTime = "Relax"; + else if (tMode == 'dynamic') prettyTime = "Dinamico"; String prettyShape = "Rombo"; if (shapeStr == 'cross') prettyShape = "Croce"; @@ -612,7 +646,7 @@ class _LobbyScreenState extends State with WidgetsBindingObserver { children: [ Text("Stanza di $host", style: getLobbyTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 18))), const SizedBox(height: 6), - Text("Raggio: $r • $prettyShape • ${time ? 'A Tempo' : 'Relax'}", style: getLobbyTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.6), fontSize: 12))), + Text("Raggio: $r • $prettyShape • $prettyTime", style: getLobbyTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.6), fontSize: 12))), ], ), ),