diff --git a/.firebase/hosting.cHVibGlj.cache b/.firebase/hosting.cHVibGlj.cache index 78bb9a7..bfcb48b 100644 --- a/.firebase/hosting.cHVibGlj.cache +++ b/.firebase/hosting.cHVibGlj.cache @@ -1,3 +1,3 @@ index.html,1773586765860,5737ce966fa8786becaf7f36a32992cf44102fb3a217c226c30576c993b33e63 404.html,1773344753356,05cbc6f94d7a69ce2e29646eab13be2c884e61ba93e3094df5028866876d18b3 -report.html,1773588057140,876c6baaa912c9abfb81ee70e9868d84476b1c204ebca4c99f458f300661a36b +report.html,1774223974711,2848745a7b4437e80aabba9bd776c1c7f90b1be21e67ddaf062c22a21ac99554 diff --git a/lib/logic/ai_engine.dart b/lib/logic/ai_engine.dart index 816ff35..921e3bf 100644 --- a/lib/logic/ai_engine.dart +++ b/lib/logic/ai_engine.dart @@ -9,7 +9,9 @@ class _ClosureResult { final bool closesSomething; final int netValue; final bool causesSwap; - _ClosureResult(this.closesSomething, this.netValue, this.causesSwap); + final bool isIceTrap; // NUOVO: Identifica le mosse suicide sul ghiaccio + + _ClosureResult(this.closesSomething, this.netValue, this.causesSwap, this.isIceTrap); } class AIEngine { @@ -19,6 +21,7 @@ class AIEngine { if (availableLines.isEmpty) return board.lines.first; + // Più il livello è alto, più l'IA è "intelligente" double smartChance = 0.50 + ((level - 1) * 0.10); if (smartChance > 1.0) smartChance = 1.0; @@ -29,9 +32,16 @@ class AIEngine { List goodClosingMoves = []; List badClosingMoves = []; + List iceTraps = []; // Le mosse da evitare assolutamente for (var line in availableLines) { var result = _checkClosure(board, line); + + if (result.isIceTrap) { + iceTraps.add(line); // Segna la linea come trappola e passa alla prossima + continue; + } + if (result.closesSomething) { if (result.causesSwap) { if (myScore < oppScore) { @@ -56,10 +66,10 @@ class AIEngine { } } - // --- REGOLA 2: Mosse Sicure --- + // --- REGOLA 2: Mosse Sicure (Ora include le esche del ghiaccio!) --- List safeMoves = []; for (var line in availableLines) { - if (!badClosingMoves.contains(line) && !goodClosingMoves.contains(line) && _isSafeMove(board, line, myScore, oppScore)) { + if (!badClosingMoves.contains(line) && !goodClosingMoves.contains(line) && !iceTraps.contains(line) && _isSafeMove(board, line, myScore, oppScore)) { safeMoves.add(line); } } @@ -76,17 +86,19 @@ class AIEngine { // --- REGOLA 3: Scegliere il male minore --- if (beSmart) { - List riskyButNotTerrible = availableLines.where((l) => !badClosingMoves.contains(l) && !goodClosingMoves.contains(l)).toList(); + List riskyButNotTerrible = availableLines.where((l) => !badClosingMoves.contains(l) && !goodClosingMoves.contains(l) && !iceTraps.contains(l)).toList(); if (riskyButNotTerrible.isNotEmpty) { return riskyButNotTerrible[random.nextInt(riskyButNotTerrible.length)]; } } - List nonTerribleMoves = availableLines.where((l) => !badClosingMoves.contains(l)).toList(); + // Ultima spiaggia prima del disastro: qualsiasi cosa tranne bombe e trappole ghiacciate + List nonTerribleMoves = availableLines.where((l) => !badClosingMoves.contains(l) && !iceTraps.contains(l)).toList(); if (nonTerribleMoves.isNotEmpty) { return nonTerribleMoves[random.nextInt(nonTerribleMoves.length)]; } + // Se l'IA è messa all'angolo ed è costretta a suicidarsi... pesca a caso return availableLines[random.nextInt(availableLines.length)]; } @@ -94,6 +106,7 @@ class AIEngine { int netValue = 0; bool closesSomething = false; bool causesSwap = false; + bool isIceTrap = false; for (var box in board.boxes) { if (box.type == BoxType.invisible) continue; @@ -106,25 +119,29 @@ class AIEngine { if (box.right.owner != Player.none || box.right == line) linesCount++; if (linesCount == 4) { - closesSomething = true; - - // FIX: Togliamo la "vista a raggi X" all'Intelligenza Artificiale! - if (box.hiddenJokerOwner == board.currentPlayer) { - // L'IA conosce il suo Jolly, sa che vale +2 e cercherà di chiuderlo - netValue += 2; + if (box.type == BoxType.ice && !line.isIceCracked) { + // L'IA capisce che questa mossa non chiuderà il box, ma le farà perdere il turno. + isIceTrap = true; } else { - // Se c'è il Jolly del giocatore, l'IA NON DEVE SAPERLO e valuta la casella normalmente! - if (box.type == BoxType.gold) netValue += 2; - else if (box.type == BoxType.bomb) netValue -= 1; - else if (box.type == BoxType.swap) netValue += 0; - else netValue += 1; - } + closesSomething = true; - if (box.type == BoxType.swap) causesSwap = true; + if (box.hiddenJokerOwner == board.currentPlayer) { + netValue += 2; + } else { + if (box.type == BoxType.gold) netValue += 2; + else if (box.type == BoxType.bomb) netValue -= 1; + else if (box.type == BoxType.swap) netValue += 0; + else if (box.type == BoxType.ice) netValue += 0; // Rompere il ghiaccio vale 0 punti, ma fa rigiocare + else if (box.type == BoxType.multiplier) netValue += 1; // Leggero boost per dare priorità al x2 + else netValue += 1; + } + + if (box.type == BoxType.swap) causesSwap = true; + } } } } - return _ClosureResult(closesSomething, netValue, causesSwap); + return _ClosureResult(closesSomething, netValue, causesSwap, isIceTrap); } static bool _isSafeMove(GameBoard board, Line line, int myScore, int oppScore) { @@ -139,21 +156,24 @@ class AIEngine { if (box.right.owner != Player.none) currentLinesCount++; if (currentLinesCount == 2) { - - // Nuova logica di sicurezza: cosa succede se l'IA lascia questa scatola all'avversario? + // L'IA valuta cosa succede se lascia questa casella con 3 linee all'avversario int valueForOpponent = 0; - if (box.hiddenJokerOwner == board.currentPlayer) { - // Se l'avversario la chiude, becca la trappola dell'IA (-1). - // Quindi PER L'IA È SICURISSIMO LASCIARE QUESTA CASELLA APERTA! + + if (box.type == BoxType.ice) { + // Il ghiaccio è la trappola perfetta. Lasciarlo con 3 linee spingerà l'avversario a incrinarlo e a perdere il turno. + // L'IA valuta questa mossa come SICURISSIMA! + valueForOpponent = -5; + } else if (box.hiddenJokerOwner == board.currentPlayer) { valueForOpponent = -1; } else { if (box.type == BoxType.gold) valueForOpponent = 2; else if (box.type == BoxType.bomb) valueForOpponent = -1; else if (box.type == BoxType.swap) valueForOpponent = 0; + else if (box.type == BoxType.multiplier) valueForOpponent = 1; else valueForOpponent = 1; } - // Se per l'avversario vale -1 (bomba normale o trappola dell'IA), lasciamogliela! + // Se per l'avversario è una trappola (bomba o ghiaccio), lascia pure la mossa libera if (valueForOpponent < 0) { continue; } @@ -166,7 +186,7 @@ class AIEngine { } } - return false; + return false; // La mossa regalerebbe punti, quindi NON è sicura } } } diff --git a/lib/logic/game_controller.dart b/lib/logic/game_controller.dart index 8b7ae3b..379264a 100644 --- a/lib/logic/game_controller.dart +++ b/lib/logic/game_controller.dart @@ -553,6 +553,7 @@ class GameController extends ChangeNotifier { if (isOnline && roomCode != null) { Map moveData = { + 'id': DateTime.now().millisecondsSinceEpoch, 'x1': line.p1.x, 'y1': line.p1.y, 'x2': line.p2.x, 'y2': line.p2.y, 'player': myPlayer == Player.red ? 'red' : 'blue' }; @@ -609,7 +610,7 @@ class GameController extends ChangeNotifier { return unlocks; } - void _saveMatchResult() { + Future _saveMatchResult() async { if (_hasSavedResult) return; _hasSavedResult = true; @@ -626,9 +627,9 @@ class GameController extends ChangeNotifier { String oppName = isHost ? onlineGuestName : onlineHostName; 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); + await StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: oppName, myScore: myScore, oppScore: oppScore, isOnline: true); - if (isWin) StorageService.instance.updateQuestProgress(0, 1); + if (isWin) await StorageService.instance.updateQuestProgress(0, 1); } else if (isVsCPU) { int myScore = board.scoreRed; int cpuScore = board.scoreBlue; @@ -636,23 +637,23 @@ class GameController extends ChangeNotifier { calculatedXP = isWin ? (10 + (cpuLevel * 2)) : (isDraw ? 5 : 2); if (isWin) { - StorageService.instance.addWin(); - StorageService.instance.updateQuestProgress(1, 1); + await StorageService.instance.addWin(); + await StorageService.instance.updateQuestProgress(1, 1); } else if (cpuScore > myScore) { - StorageService.instance.addLoss(); + await StorageService.instance.addLoss(); } - StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "CPU (Liv. $cpuLevel)", myScore: myScore, oppScore: cpuScore, isOnline: false); + await StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "CPU (Liv. $cpuLevel)", myScore: myScore, oppScore: cpuScore, isOnline: false); } else { calculatedXP = 2; - StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "Ospite (Locale)", myScore: board.scoreRed, oppScore: board.scoreBlue, isOnline: false); + await StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "Ospite (Locale)", myScore: board.scoreRed, oppScore: board.scoreBlue, isOnline: false); } if (board.shape != ArenaShape.classic) { - StorageService.instance.updateQuestProgress(2, 1); + await StorageService.instance.updateQuestProgress(2, 1); } lastMatchXP = calculatedXP; - StorageService.instance.addXP(calculatedXP); + await StorageService.instance.addXP(calculatedXP); int newLevel = StorageService.instance.playerLevel; if (newLevel > oldLevel) { diff --git a/lib/services/audio_service.dart b/lib/services/audio_service.dart index db890e2..e310c05 100644 --- a/lib/services/audio_service.dart +++ b/lib/services/audio_service.dart @@ -12,7 +12,7 @@ class AudioService extends ChangeNotifier { AudioService._internal(); bool isMuted = false; - final AudioPlayer _sfxPlayer = AudioPlayer(); + // Abbiamo rimosso _sfxPlayer perché ora ogni suono crea un player usa e getta final AudioPlayer _bgmPlayer = AudioPlayer(); AppThemeType _currentTheme = AppThemeType.doodle; @@ -30,7 +30,6 @@ class AudioService extends ChangeNotifier { if (isMuted) { await _bgmPlayer.pause(); - await _sfxPlayer.stop(); } else { playBgm(_currentTheme); } @@ -92,9 +91,11 @@ class AudioService extends ChangeNotifier { if (file.isNotEmpty) { try { - await _sfxPlayer.play(AssetSource('audio/sfx/$file'), volume: 1.0); + final player = AudioPlayer(); // Player dedicato + await player.play(AssetSource('audio/sfx/$file'), volume: 1.0); + player.onPlayerComplete.listen((_) => player.dispose()); } catch (e) { - debugPrint("Errore SFX Linea: $file"); + debugPrint("Errore SFX Linea: $e"); } } } @@ -115,9 +116,11 @@ class AudioService extends ChangeNotifier { if (file.isNotEmpty) { try { - await _sfxPlayer.play(AssetSource('audio/sfx/$file'), volume: 1.0); + final player = AudioPlayer(); // Player dedicato + await player.play(AssetSource('audio/sfx/$file'), volume: 1.0); + player.onPlayerComplete.listen((_) => player.dispose()); } catch (e) { - debugPrint("Errore SFX Box: $file"); + debugPrint("Errore SFX Box: $e"); } } } @@ -125,14 +128,18 @@ class AudioService extends ChangeNotifier { void playBonusSfx() async { if (isMuted) return; try { - await _sfxPlayer.play(AssetSource('audio/sfx/bonus.wav'), volume: 1.0); + final player = AudioPlayer(); // Player dedicato + await player.play(AssetSource('audio/sfx/bonus.wav'), volume: 1.0); + player.onPlayerComplete.listen((_) => player.dispose()); } catch(e) {} } void playBombSfx() async { if (isMuted) return; try { - await _sfxPlayer.play(AssetSource('audio/sfx/bomb.wav'), volume: 1.0); + final player = AudioPlayer(); // Player dedicato + await player.play(AssetSource('audio/sfx/bomb.wav'), volume: 1.0); + player.onPlayerComplete.listen((_) => player.dispose()); } catch(e) {} } } \ No newline at end of file diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart index 585cf12..a02dec3 100644 --- a/lib/services/storage_service.dart +++ b/lib/services/storage_service.dart @@ -66,10 +66,7 @@ class StorageService { // --- SICUREZZA XP: Inviamo solo INCREMENTI al server --- Future addXP(int xp) async { - // Aggiorniamo il locale per la UI await _prefs.setInt('totalXP', totalXP + xp); - - // Aggiorniamo il server in modo sicuro tramite incremento relativo final user = FirebaseAuth.instance.currentUser; if (user != null) { await FirebaseFirestore.instance.collection('leaderboard').doc(user.uid).set({ @@ -83,7 +80,6 @@ class StorageService { int get wins => _prefs.getInt('wins') ?? 0; - // --- SICUREZZA WINS: Inviamo solo INCREMENTI al server --- Future addWin() async { await _prefs.setInt('wins', wins + 1); final user = FirebaseAuth.instance.currentUser; @@ -96,7 +92,6 @@ class StorageService { int get losses => _prefs.getInt('losses') ?? 0; - // --- SICUREZZA LOSSES: Inviamo solo INCREMENTI al server --- Future addLoss() async { await _prefs.setInt('losses', losses + 1); final user = FirebaseAuth.instance.currentUser; @@ -116,10 +111,12 @@ class StorageService { syncLeaderboard(); } + // ====================================================================== + // FIX: ORA IL SYNC MANDA I DATI REALI ALLA DASHBOARD ADMIN! + // ====================================================================== Future syncLeaderboard() async { try { final user = FirebaseAuth.instance.currentUser; - if (user == null) return; String name = playerName; @@ -127,12 +124,53 @@ class StorageService { String targetUid = user.uid; - // --- SICUREZZA: Non inviamo PIÙ i valori assoluti di xp, wins e losses! --- - // Vengono aggiornati solo dagli incrementi protetti nelle funzioni sopra. + // 1. Recupero Versione App e Modello Dispositivo + String appVer = "N/D"; + String devModel = "N/D"; + String osName = kIsWeb ? "Web" : Platform.operatingSystem; + + try { + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + appVer = "${packageInfo.version}+${packageInfo.buildNumber}"; + + DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + if (!kIsWeb) { + if (Platform.isAndroid) { + AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + devModel = "${androidInfo.brand} ${androidInfo.model}".toUpperCase(); + osName = "Android"; + } else if (Platform.isIOS) { + IosDeviceInfo iosInfo = await deviceInfo.iosInfo; + devModel = iosInfo.utsname.machine; // Es. "iPhone13,2" + osName = "iOS"; + } else if (Platform.isMacOS) { + MacOsDeviceInfo macInfo = await deviceInfo.macOsInfo; + devModel = macInfo.model; // Es. "MacBookPro17,1" + osName = "macOS"; + } + } + } catch (e) { + debugPrint("Errore device info: $e"); + } + + // 2. Calcolo del Playtime effettivo (aggiornato ad ogni sync) + int sessionDurationSec = (DateTime.now().millisecondsSinceEpoch - _sessionStart) ~/ 1000; + int savedPlaytime = _prefs.getInt('total_playtime') ?? 0; + int totalPlaytime = savedPlaytime + sessionDurationSec; + await _prefs.setInt('total_playtime', totalPlaytime); + _sessionStart = DateTime.now().millisecondsSinceEpoch; // Resetta il timer di sessione + + // 3. Creazione del payload per Firebase Map dataToSave = { 'name': name, 'level': playerLevel, 'lastActive': FieldValue.serverTimestamp(), + 'appVersion': appVer, + 'deviceModel': devModel, + 'platform': osName, + 'ip': lastIp, + 'city': lastCity, + 'playtime': totalPlaytime, }; if (user.metadata.creationTime != null) { @@ -184,15 +222,12 @@ class StorageService { if (today != lastDate) { _prefs.setString('quest_date', today); - _prefs.setInt('q1_type', 0); _prefs.setInt('q1_prog', 0); _prefs.setInt('q1_target', 3); - _prefs.setInt('q2_type', 1); _prefs.setInt('q2_prog', 0); _prefs.setInt('q2_target', 2); - _prefs.setInt('q3_type', 2); _prefs.setInt('q3_prog', 0); _prefs.setInt('q3_target', 2); diff --git a/lib/ui/home/home_screen.dart b/lib/ui/home/home_screen.dart index 2feff8f..89a3274 100644 --- a/lib/ui/home/home_screen.dart +++ b/lib/ui/home/home_screen.dart @@ -4,6 +4,7 @@ import 'dart:ui'; import 'dart:math'; +import 'dart:io' show Platform; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter/services.dart'; @@ -13,6 +14,9 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'dart:async'; import 'package:app_links/app_links.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:upgrader/upgrader.dart'; +import 'package:in_app_update/in_app_update.dart'; import '../../logic/game_controller.dart'; import '../../core/theme_manager.dart'; @@ -58,6 +62,9 @@ class _HomeScreenState extends State with WidgetsBindingObserver { String? _myRoomCode; bool _roomStarted = false; + String _appVersion = ''; + bool _updateAvailable = false; + final MultiplayerService _multiplayerService = MultiplayerService(); @override @@ -80,6 +87,59 @@ class _HomeScreenState extends State with WidgetsBindingObserver { _checkClipboardForInvite(); _initDeepLinks(); _listenToFavoritesOnline(); + _loadAppVersion(); + _checkStoreForUpdate(); + } + + Future _loadAppVersion() async { + try { + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + if (mounted) { + setState(() { + _appVersion = "v. ${packageInfo.version}"; + }); + } + } catch (e) { + debugPrint("Errore lettura versione: $e"); + } + } + + Future _checkStoreForUpdate() async { + + if (kIsWeb) return; + try { + if (Platform.isAndroid) { + final info = await InAppUpdate.checkForUpdate(); + if (info.updateAvailability == UpdateAvailability.updateAvailable) { + if (mounted) setState(() => _updateAvailable = true); + } + } else if (Platform.isIOS || Platform.isMacOS) { + final upgrader = Upgrader(); + await upgrader.initialize(); + if (upgrader.isUpdateAvailable()) { + if (mounted) setState(() => _updateAvailable = true); + } + } + } catch (e) { + debugPrint("Errore controllo aggiornamenti: $e"); + } + + } + + void _triggerUpdate() async { + if (kIsWeb) return; + if (Platform.isAndroid) { + try { + final info = await InAppUpdate.checkForUpdate(); + if (info.updateAvailability == UpdateAvailability.updateAvailable) { + await InAppUpdate.performImmediateUpdate(); + } + } catch(e) { + Upgrader().sendUserToAppStore(); + } + } else { + Upgrader().sendUserToAppStore(); + } } void _checkThemeSafety() { @@ -179,7 +239,9 @@ class _HomeScreenState extends State with WidgetsBindingObserver { if (diffInSeconds.abs() < 180) { String name = data['name'] ?? 'Un amico'; - _showFavoriteOnlinePopup(name); + if (ModalRoute.of(context)?.isCurrent == true) { + _showFavoriteOnlinePopup(name); + } } } } @@ -245,7 +307,10 @@ class _HomeScreenState extends State with WidgetsBindingObserver { continue; } } - _showInvitePopup(from, code, inviteId); + + if (ModalRoute.of(context)?.isCurrent == true) { + _showInvitePopup(from, code, inviteId); + } } } } @@ -742,12 +807,120 @@ class _HomeScreenState extends State with WidgetsBindingObserver { ), Positioned.fill(child: uiContent), + + // --- NUMERO DI VERSIONE APP E BADGE AGGIORNAMENTO --- + if (_appVersion.isNotEmpty) + Positioned( + bottom: MediaQuery.of(context).padding.bottom + 10, + left: 20, + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Opacity( + opacity: 0.6, + child: Text( + _appVersion, + style: getSharedTextStyle(themeType, TextStyle( + color: themeType == AppThemeType.doodle ? inkColor : theme.text, + fontSize: 12, + fontWeight: FontWeight.bold, + letterSpacing: 1.0, + )), + ), + ), + if (_updateAvailable) ...[ + const SizedBox(width: 15), + _PulsingUpdateBadge( + themeType: themeType, + theme: theme, + onTap: _triggerUpdate, + ), + ] + ], + ), + ), ], ), ); } } +// --- NUOVO WIDGET: BADGE AGGIORNAMENTO PULSANTE --- +class _PulsingUpdateBadge extends StatefulWidget { + final AppThemeType themeType; + final ThemeColors theme; + final VoidCallback onTap; + + const _PulsingUpdateBadge({ + required this.themeType, + required this.theme, + required this.onTap, + }); + + @override + State<_PulsingUpdateBadge> createState() => _PulsingUpdateBadgeState(); +} + +class _PulsingUpdateBadgeState extends State<_PulsingUpdateBadge> with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 800))..repeat(reverse: true); + _scaleAnimation = Tween(begin: 0.95, end: 1.05).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + Color badgeColor = widget.themeType == AppThemeType.doodle ? Colors.red.shade700 : widget.theme.playerRed; + Color textColor = widget.themeType == AppThemeType.doodle ? Colors.white : widget.theme.playerRed; + Color bgColor = widget.themeType == AppThemeType.doodle ? badgeColor : badgeColor.withOpacity(0.15); + + return GestureDetector( + onTap: widget.onTap, + child: ScaleTransition( + scale: _scaleAnimation, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: badgeColor, width: 1.5), + boxShadow: widget.themeType == AppThemeType.doodle + ? [const BoxShadow(color: Colors.black26, offset: Offset(2, 2))] + : [BoxShadow(color: badgeColor.withOpacity(0.4), blurRadius: 8)], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.system_update_alt, color: textColor, size: 14), + const SizedBox(width: 6), + Text( + "AGGIORNAMENTO DISPONIBILE", + style: getSharedTextStyle(widget.themeType, TextStyle( + color: textColor, + fontSize: 10, + fontWeight: FontWeight.w900, + )), + ), + ], + ), + ), + ), + ); + } +} +// ---------------------------------------------------- + class FullScreenGridPainter extends CustomPainter { final Color gridColor; FullScreenGridPainter(this.gridColor); diff --git a/lib/widgets/game_over_dialog.dart b/lib/widgets/game_over_dialog.dart index 8f3994d..1847d33 100644 --- a/lib/widgets/game_over_dialog.dart +++ b/lib/widgets/game_over_dialog.dart @@ -8,6 +8,7 @@ import '../logic/game_controller.dart'; import '../core/theme_manager.dart'; import '../core/app_colors.dart'; import '../services/storage_service.dart'; +import 'painters.dart'; class GameOverDialog extends StatelessWidget { const GameOverDialog({super.key}); @@ -18,6 +19,7 @@ class GameOverDialog extends StatelessWidget { final themeManager = context.read(); final theme = themeManager.currentColors; final themeType = themeManager.currentThemeType; + Color inkColor = const Color(0xFF111122); int red = game.board.scoreRed; int blue = game.board.scoreBlue; @@ -51,172 +53,269 @@ class GameOverDialog extends StatelessWidget { winnerColor = theme.playerBlue; } else { winnerText = "PAREGGIO!"; - winnerColor = theme.text; + winnerColor = themeType == AppThemeType.doodle ? inkColor : theme.text; } - return AlertDialog( - 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: TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 22)), - content: SingleChildScrollView( - physics: const BouncingScrollPhysics(), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(winnerText, textAlign: TextAlign.center, style: TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: winnerColor)), - const SizedBox(height: 20), - Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + Widget dialogContent = Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(winnerText, textAlign: TextAlign.center, style: getSharedTextStyle(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: themeType == AppThemeType.doodle ? Colors.transparent : theme.text.withOpacity(0.05), + borderRadius: BorderRadius.circular(15), + border: themeType == AppThemeType.doodle ? Border.all(color: inkColor.withOpacity(0.3), width: 1.5) : null, + ), + child: FittedBox( + fit: BoxFit.scaleDown, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text("$nameRed: $red", style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerRed))), + Text(" - ", style: getSharedTextStyle(themeType, TextStyle(fontSize: 18, color: themeType == AppThemeType.doodle ? inkColor : theme.text))), + Text("$nameBlue: $blue", style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerBlue))), + ], + ), + ), + ), + + if (game.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: themeType == AppThemeType.doodle ? Colors.green.shade700 : Colors.greenAccent, width: 1.5), + boxShadow: (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) ? [const BoxShadow(color: Colors.greenAccent, blurRadius: 10, spreadRadius: -5)] : [], + ), + child: Text("+ ${game.lastMatchXP} XP", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.green.shade700 : Colors.greenAccent, fontWeight: FontWeight.w900, fontSize: 16, letterSpacing: 1.5))), + ), + ], + + if (game.isVsCPU) ...[ + const SizedBox(height: 15), + Text("Difficoltà CPU: Livello ${game.cpuLevel}", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.7) : theme.text.withOpacity(0.7)))), + ], + + if (game.isOnline) ...[ + const SizedBox(height: 20), + if (game.rematchRequested && !game.opponentWantsRematch) + Text("In attesa di $nameBlue...", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.orange.shade700 : Colors.amber, fontWeight: FontWeight.bold, fontStyle: FontStyle.italic))), + if (game.opponentWantsRematch && !game.rematchRequested) + Text("$nameBlue vuole la rivincita!", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.green.shade700 : Colors.greenAccent, fontWeight: FontWeight.bold))), + if (game.rematchRequested && game.opponentWantsRematch) + Text("Avvio nuova partita...", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.green.shade800 : Colors.green, fontWeight: FontWeight.bold))), + ], + + // --- SEZIONE LEVEL UP E ROADMAP DINAMICA --- + if (game.hasLeveledUp && game.unlockedRewards.isNotEmpty) ...[ + const SizedBox(height: 30), + Divider(color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.3) : theme.text.withOpacity(0.2)), + const SizedBox(height: 15), + Container( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 20), + decoration: BoxDecoration( + color: themeType == AppThemeType.doodle ? Colors.amber.withOpacity(0.1) : Colors.amber.withOpacity(0.2), + borderRadius: BorderRadius.circular(30), + border: Border.all(color: themeType == AppThemeType.doodle ? Colors.amber.shade700 : Colors.amber, width: 2) + ), + child: Text("🎉 LIVELLO ${game.newlyReachedLevel}! 🎉", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.amber.shade700 : Colors.amber, fontWeight: FontWeight.w900, fontSize: 18))), + ), + const SizedBox(height: 15), + + ...game.unlockedRewards.map((reward) { + Color rewardColor = themeType == AppThemeType.doodle ? (reward['color'] as Color).withOpacity(0.8) : reward['color']; + return Container( + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: theme.text.withOpacity(0.05), - borderRadius: BorderRadius.circular(15), + color: rewardColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: rewardColor.withOpacity(0.5), width: 1.5), ), child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text("$nameRed: $red", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerRed)), - Text(" - ", style: TextStyle(fontSize: 18, color: theme.text)), - Text("$nameBlue: $blue", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerBlue)), - ], - ), - ), - - if (game.isVsCPU) ...[ - const SizedBox(height: 15), - Text("Difficoltà CPU: Livello ${game.cpuLevel}", style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: theme.text.withOpacity(0.7))), - ], - - // --- SEZIONE LEVEL UP E ROADMAP DINAMICA --- - if (game.hasLeveledUp && game.unlockedRewards.isNotEmpty) ...[ - const SizedBox(height: 30), - const Divider(), - const SizedBox(height: 15), - Container( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 20), - decoration: BoxDecoration( - color: Colors.amber.withOpacity(0.2), - borderRadius: BorderRadius.circular(30), - border: Border.all(color: Colors.amber, width: 2) - ), - child: Text("🎉 LIVELLO ${game.newlyReachedLevel}! 🎉", style: const TextStyle(color: Colors.amber, fontWeight: FontWeight.w900, fontSize: 18)), - ), - const SizedBox(height: 15), - - ...game.unlockedRewards.map((reward) => Container( - margin: const EdgeInsets.only(bottom: 10), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: (reward['color'] as Color).withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: (reward['color'] as Color).withOpacity(0.5), width: 1.5), - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: (reward['color'] as Color).withOpacity(0.2), - shape: BoxShape.circle, - ), - child: Icon(reward['icon'], color: reward['color'], size: 28), - ), - const SizedBox(width: 15), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(reward['title'], style: TextStyle(color: reward['color'], fontWeight: FontWeight.w900, fontSize: 16)), - const SizedBox(height: 4), - Text(reward['desc'], style: TextStyle(color: theme.text.withOpacity(0.9), fontSize: 12, height: 1.3)), - ], - ) + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: rewardColor.withOpacity(0.2), + shape: BoxShape.circle, + ), + child: Icon(reward['icon'], color: rewardColor, size: 28), + ), + const SizedBox(width: 15), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(reward['title'], style: getSharedTextStyle(themeType, TextStyle(color: rewardColor, fontWeight: FontWeight.w900, fontSize: 16))), + const SizedBox(height: 4), + Text(reward['desc'], style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.9) : theme.text.withOpacity(0.9), fontSize: 12, height: 1.3))), + ], ) - ] - ) - )), - ] - // --------------------------------------------- - ], - ), - ), - actionsPadding: const EdgeInsets.only(left: 20, right: 20, bottom: 20, top: 10), - actionsAlignment: MainAxisAlignment.center, - actions: [ + ) + ] + ), + ); + }), + ], + + const SizedBox(height: 30), + + // --- BOTTONI AZIONE --- 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: () { + _buildPrimaryButton( + "PROSSIMO LIVELLO ➔", + winnerColor, + themeType, + inkColor, + () { Navigator.pop(context); game.increaseLevelAndRestart(); }, - child: const Text("PROSSIMO LIVELLO ➔", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), ) else if (game.isOnline) - 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: () { - Navigator.pop(context); - if (game.board.isGameOver) { - game.requestRematch(); - } - }, - child: const Text("RIGIOCA ONLINE", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 1.5)), + _buildPrimaryButton( + game.opponentWantsRematch ? "ACCETTA RIVINCITA" : "CHIEDI RIVINCITA", + game.rematchRequested ? Colors.grey : (winnerColor == (themeType == AppThemeType.doodle ? inkColor : theme.text) ? theme.playerBlue : winnerColor), + themeType, + inkColor, + game.rematchRequested ? () {} : () => game.requestRematch(), ) 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: () { + _buildPrimaryButton( + "RIGIOCA", + winnerColor == (themeType == AppThemeType.doodle ? inkColor : theme.text) ? theme.playerBlue : winnerColor, + themeType, + inkColor, + () { Navigator.pop(context); game.startNewGame(game.board.radius, vsCPU: game.isVsCPU); }, - child: const Text("RIGIOCA", style: 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: () { + _buildSecondaryButton( + "TORNA AL MENU", + themeType, + inkColor, + theme, + () { if (game.isOnline) { game.disconnectOnlineGame(); } Navigator.pop(context); Navigator.pop(context); }, - child: Text("TORNA AL MENU", style: TextStyle(fontWeight: FontWeight.bold, color: theme.text, fontSize: 14, letterSpacing: 1.5)), ), ], ) ], ); + + if (themeType == AppThemeType.doodle) { + dialogContent = Transform.rotate( + angle: 0.015, + child: CustomPaint( + painter: DoodleBackgroundPainter(fillColor: Colors.white.withOpacity(0.95), strokeColor: inkColor, seed: 500), + child: Padding( + padding: const EdgeInsets.all(25.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("FINE PARTITA", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 22, fontWeight: FontWeight.w900, color: inkColor, letterSpacing: 2))), + const SizedBox(height: 20), + dialogContent, + ], + ), + ), + ), + ); + } else { + dialogContent = Container( + padding: const EdgeInsets.all(25.0), + decoration: BoxDecoration( + color: theme.background, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: winnerColor.withOpacity(0.5), width: 2), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("FINE PARTITA", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: theme.text))), + const SizedBox(height: 20), + dialogContent, + ], + ), + ); + } + + return Dialog( + backgroundColor: Colors.transparent, + insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20), + child: dialogContent, + ); + } + + Widget _buildPrimaryButton(String label, Color color, AppThemeType themeType, Color inkColor, VoidCallback onTap) { + if (themeType == AppThemeType.doodle) { + return GestureDetector( + onTap: onTap, + child: CustomPaint( + painter: DoodleBackgroundPainter(fillColor: color, strokeColor: inkColor, seed: label.length * 7), + child: Container( + height: 55, + alignment: Alignment.center, + child: Text(label, style: getSharedTextStyle(themeType, const TextStyle(fontSize: 16, fontWeight: FontWeight.w900, color: Colors.white, letterSpacing: 1.5))), + ), + ), + ); + } + + return ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: color, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 15), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), + elevation: 5, + ), + onPressed: onTap, + child: Text(label, style: getSharedTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 1.5))), + ); + } + + Widget _buildSecondaryButton(String label, AppThemeType themeType, Color inkColor, ThemeColors theme, VoidCallback onTap) { + if (themeType == AppThemeType.doodle) { + return GestureDetector( + onTap: onTap, + child: CustomPaint( + painter: DoodleBackgroundPainter(fillColor: Colors.transparent, strokeColor: inkColor.withOpacity(0.5), seed: label.length * 3), + child: Container( + height: 55, + alignment: Alignment.center, + child: Text(label, style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor, letterSpacing: 1.5))), + ), + ), + ); + } + + return 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: onTap, + child: Text(label, style: getSharedTextStyle(themeType, TextStyle(fontWeight: FontWeight.bold, color: theme.text, fontSize: 14, letterSpacing: 1.5))), + ); } } \ No newline at end of file diff --git a/public/report.html b/public/report.html index 98f1ddb..15df65c 100644 --- a/public/report.html +++ b/public/report.html @@ -3,65 +3,140 @@ - Report Statistiche TetraQ + Report Giocatori - TetraQ