diff --git a/.DS_Store b/.DS_Store index e44f658..3087572 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/ios/.DS_Store b/ios/.DS_Store index 298d050..4d2c3b2 100644 Binary files a/ios/.DS_Store and b/ios/.DS_Store differ diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index aebc1e5..aabb61d 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -473,7 +473,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = 2BX6QRR7GG; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -482,7 +482,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.0; + MARKETING_VERSION = 1.0.2; PRODUCT_BUNDLE_IDENTIFIER = com.sanza.tetraq; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -658,7 +658,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = 2BX6QRR7GG; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -667,7 +667,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.0; + MARKETING_VERSION = 1.0.2; PRODUCT_BUNDLE_IDENTIFIER = com.sanza.tetraq; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -683,7 +683,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = 2BX6QRR7GG; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -692,7 +692,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.0; + MARKETING_VERSION = 1.0.2; PRODUCT_BUNDLE_IDENTIFIER = com.sanza.tetraq; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; diff --git a/lib/.DS_Store b/lib/.DS_Store index 78db19d..82ae50d 100644 Binary files a/lib/.DS_Store and b/lib/.DS_Store differ diff --git a/lib/core/app_colors.dart b/lib/core/app_colors.dart index ee0cf34..b0e6603 100644 --- a/lib/core/app_colors.dart +++ b/lib/core/app_colors.dart @@ -1,6 +1,11 @@ -import 'package:flutter/material.dart'; +// =========================================================================== +// FILE: lib/core/app_colors.dart +// =========================================================================== -enum AppThemeType { minimal, doodle, cyberpunk, wood } +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; + +enum AppThemeType { minimal, doodle, cyberpunk, wood, arcade, grimorio } class ThemeColors { final Color background; @@ -29,22 +34,24 @@ class AppColors { playerRed: Color(0xFFD32F2F), playerBlue: Color(0xFF1565C0), text: Color(0xFF37474F), ); - // --- TEMA CYBERPUNK AGGIORNATO --- static const ThemeColors cyberpunk = ThemeColors( - background: Color(0xFF0A001A), // Sfondo notte profonda - gridLine: Color(0xFF6200EA), // Viola scuro elettrico (non fa confusione con le mosse) - playerRed: Color(0xFFFF007F), // Rosa Neon (invariato) - playerBlue: Color(0xFF69F0AE), // Verde Fluo brillante! (Green Accent) - text: Color(0xFFFFFFFF), + background: Color(0xFF0A001A), gridLine: Color(0xFF6200EA), + playerRed: Color(0xFFFF007F), playerBlue: Color(0xFF69F0AE), text: Color(0xFFFFFFFF), ); - // --- TEMA LEGNO POTENZIATO --- static const ThemeColors wood = ThemeColors( - background: Color(0xFF905D3B), // Marrone caldo e ricco (vero legno) - gridLine: Color(0xFF4A301E), // Marrone scurissimo per i solchi - playerRed: Color(0xFFE53935), // Rosso acceso per i fiammiferi - playerBlue: Color(0xFF29B6F6), // Azzurro acceso per i fiammiferi - text: Color(0xFFFBE9E7), // Panna chiaro per contrastare lo scuro + background: Color(0xFF905D3B), gridLine: Color(0xFF4A301E), + playerRed: Color(0xFFE53935), playerBlue: Color(0xFF29B6F6), text: Color(0xFFFBE9E7), + ); + + static const ThemeColors arcade = ThemeColors( + background: Color(0xFF111111), gridLine: Color(0xFF00FF00), + playerRed: Color(0xFFFF004D), playerBlue: Color(0xFF00E5FF), text: Color(0xFFFFFFFF), + ); + + static const ThemeColors grimorio = ThemeColors( + background: Color(0xFF1E112A), gridLine: Color(0xFF8D6E63), + playerRed: Color(0xFFE91E63), playerBlue: Color(0xFF4FC3F7), text: Color(0xFFFFF3E0), ); static ThemeColors getTheme(AppThemeType type) { @@ -53,6 +60,74 @@ class AppColors { case AppThemeType.doodle: return doodle; case AppThemeType.cyberpunk: return cyberpunk; case AppThemeType.wood: return wood; + case AppThemeType.arcade: return arcade; + case AppThemeType.grimorio: return grimorio; } } +} + +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; + } + } + + 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; + } + } + + 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; + } + } + + 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; + } + } + + 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; + } + } + + // --- NUOVE ICONE --- + static IconData ice(AppThemeType type) { + return FontAwesomeIcons.snowflake; + } + + static IconData multiplier(AppThemeType type) { + return FontAwesomeIcons.bolt; + } } \ No newline at end of file diff --git a/lib/logic/ai_engine.dart b/lib/logic/ai_engine.dart index ac31673..816ff35 100644 --- a/lib/logic/ai_engine.dart +++ b/lib/logic/ai_engine.dart @@ -5,7 +5,6 @@ import 'dart:math'; import '../models/game_board.dart'; -// Modificato per tracciare anche l'effetto Swap class _ClosureResult { final bool closesSomething; final int netValue; @@ -25,7 +24,6 @@ class AIEngine { bool beSmart = random.nextDouble() < smartChance; - // Calcolo punteggi attuali per valutare lo SWAP int myScore = board.currentPlayer == Player.red ? board.scoreRed : board.scoreBlue; int oppScore = board.currentPlayer == Player.red ? board.scoreBlue : board.scoreRed; @@ -36,15 +34,12 @@ class AIEngine { var result = _checkClosure(board, line); if (result.closesSomething) { if (result.causesSwap) { - // SE L'IA STA PERDENDO -> Lo Swap è un'ottima mossa! - // SE L'IA STA VINCENDO O PAREGGIANDO -> Lo Swap è una pessima mossa! if (myScore < oppScore) { goodClosingMoves.add(line); } else { badClosingMoves.add(line); } } else { - // Normale valutazione dei punti if (result.netValue >= 0) { goodClosingMoves.add(line); } else { @@ -112,7 +107,19 @@ class AIEngine { if (linesCount == 4) { closesSomething = true; - netValue += box.value; + + // 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; + } 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; + } + if (box.type == BoxType.swap) causesSwap = true; } } @@ -132,17 +139,30 @@ class AIEngine { if (box.right.owner != Player.none) currentLinesCount++; if (currentLinesCount == 2) { - // La Bomba è sempre sicura da lasciare all'avversario - if (box.type == BoxType.bomb) { + + // Nuova logica di sicurezza: cosa succede se l'IA lascia questa scatola 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! + 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 valueForOpponent = 1; + } + + // Se per l'avversario vale -1 (bomba normale o trappola dell'IA), lasciamogliela! + if (valueForOpponent < 0) { continue; } - // IL TRANELLO PERFETTO: Se stiamo PERDENDO, lasciare un quadrato Swap a 3 lati - // costringerà l'avversario a prenderlo e a ridarci la sua vittoria! + if (box.type == BoxType.swap) { if (myScore < oppScore) { - continue; // È sicuro e strategico! + continue; } else { - return false; // Se stiamo vincendo non dobbiamo MAI lasciare uno Swap! + return false; } } diff --git a/lib/logic/game_controller.dart b/lib/logic/game_controller.dart index 9d54460..7300283 100644 --- a/lib/logic/game_controller.dart +++ b/lib/logic/game_controller.dart @@ -7,7 +7,10 @@ import 'dart:math'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; + import '../models/game_board.dart'; +export '../models/game_board.dart'; + import 'ai_engine.dart'; import '../services/audio_service.dart'; import '../services/storage_service.dart'; @@ -30,22 +33,29 @@ class GameController extends ChangeNotifier { Timer? _blitzTimer; int timeLeft = 15; final int maxTime = 15; - bool isTimeMode = true; String effectText = ''; Color effectColor = Colors.transparent; Timer? _effectTimer; - // --- VARIABILI EMOJI E RIVINCITA --- String? myReaction; String? opponentReaction; Timer? _myReactionTimer; Timer? _oppReactionTimer; + + Timestamp? _lastOpponentReactionTime; + bool rematchRequested = false; bool opponentWantsRematch = false; + int lastMatchXP = 0; - Player get myPlayer => isHost ? Player.red : Player.blue; + bool isSetupPhase = true; + bool myJokerPlaced = false; + bool oppJokerPlaced = false; + Player jokerTurn = Player.red; + + Player get myPlayer => isOnline ? (isHost ? Player.red : Player.blue) : Player.red; bool get isGameOver => board.isGameOver; int cpuLevel = 1; @@ -69,12 +79,19 @@ class GameController extends ChangeNotifier { _effectTimer?.cancel(); effectText = ''; _hasSavedResult = false; + lastMatchXP = 0; myReaction = null; opponentReaction = null; + _lastOpponentReactionTime = null; rematchRequested = false; opponentWantsRematch = false; + isSetupPhase = true; + myJokerPlaced = false; + oppJokerPlaced = false; + jokerTurn = Player.red; + this.isVsCPU = vsCPU; this.isOnline = isOnline; this.roomCode = roomCode; @@ -85,8 +102,6 @@ class GameController extends ChangeNotifier { int levelToUse = isOnline ? (currentMatchLevel == 1 ? 2 : currentMatchLevel) : cpuLevel; board = GameBoard(radius: radius, level: levelToUse, seed: currentSeed, shape: onlineShape); - - // FIX: Assicuriamoci che a inizio partita il turno sia sempre del Rosso! board.currentPlayer = Player.red; isCPUThinking = false; @@ -96,11 +111,68 @@ class GameController extends ChangeNotifier { _listenToOnlineGame(this.roomCode!); } - _startTimer(); notifyListeners(); } - // --- METODI EMOJI E RIVINCITA --- + void placeJoker(int bx, int by) { + if (!isSetupPhase) return; + + Box? target; + 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; + + AudioService.instance.playLineSfx(_activeTheme); + + if (isOnline) { + if (myJokerPlaced) return; + target.hiddenJokerOwner = myPlayer; + myJokerPlaced = true; + + String prefix = isHost ? 'p1' : 'p2'; + FirebaseFirestore.instance.collection('games').doc(roomCode).update({ + '${prefix}_joker': {'x': bx, 'y': by} + }); + } else { + target.hiddenJokerOwner = jokerTurn; + if (jokerTurn == Player.red) { + jokerTurn = Player.blue; + if (isVsCPU) { + _placeCpuJoker(); + } + } else { + jokerTurn = Player.red; + } + } + notifyListeners(); + _checkSetupComplete(); + } + + void _placeCpuJoker() { + var validBoxes = board.boxes.where((b) => b.type != BoxType.invisible && b.hiddenJokerOwner == null).toList(); + if (validBoxes.isNotEmpty) { + var b = validBoxes[Random().nextInt(validBoxes.length)]; + b.hiddenJokerOwner = Player.blue; + } + jokerTurn = Player.red; + _checkSetupComplete(); + } + + void _checkSetupComplete() { + if (isOnline) { + if (myJokerPlaced && oppJokerPlaced) { + isSetupPhase = false; + _startTimer(); + } + } else { + if (jokerTurn == Player.red) { + isSetupPhase = false; + _startTimer(); + } + } + notifyListeners(); + } + void sendReaction(String reaction) { if (!isOnline || roomCode == null) return; MultiplayerService().sendReaction(roomCode!, isHost, reaction); @@ -118,23 +190,14 @@ class GameController extends ChangeNotifier { if (isMe) { myReaction = reaction; _myReactionTimer?.cancel(); - // MODIFICA: Timer impostato a 4 secondi - _myReactionTimer = Timer(const Duration(seconds: 4), () { - myReaction = null; - notifyListeners(); - }); + _myReactionTimer = Timer(const Duration(seconds: 4), () { myReaction = null; notifyListeners(); }); } else { opponentReaction = reaction; _oppReactionTimer?.cancel(); - // MODIFICA: Timer impostato a 4 secondi - _oppReactionTimer = Timer(const Duration(seconds: 4), () { - opponentReaction = null; - notifyListeners(); - }); + _oppReactionTimer = Timer(const Duration(seconds: 4), () { opponentReaction = null; notifyListeners(); }); } notifyListeners(); } - // -------------------------------- void triggerSpecialEffect(String text, Color color) { effectText = text; @@ -144,20 +207,53 @@ class GameController extends ChangeNotifier { _effectTimer = Timer(const Duration(milliseconds: 1200), () { effectText = ''; notifyListeners(); }); } - void _playEffects(List newClosed, {required bool isOpponent}) { + void _playEffects(List newClosed, {List newGhosts = const [], required bool isOpponent}) { + if (newGhosts.isNotEmpty) { + AudioService.instance.playBombSfx(); + triggerSpecialEffect("TRAPPOLA!", Colors.grey.shade400); + HapticFeedback.heavyImpact(); + return; + } + + bool isIceCracked = board.lastMove?.isIceCracked ?? false; + if (isIceCracked) { + AudioService.instance.playLineSfx(_activeTheme); + triggerSpecialEffect("GHIACCIO INCRINATO!", Colors.cyanAccent); + HapticFeedback.mediumImpact(); + return; + } + if (newClosed.isEmpty) { AudioService.instance.playLineSfx(_activeTheme); if (!isOpponent) HapticFeedback.lightImpact(); } else { + for (var b in newClosed) { + if (b.isJokerRevealed) { + if (b.owner == b.hiddenJokerOwner) { + AudioService.instance.playBonusSfx(); + triggerSpecialEffect("JOLLY! +2", Colors.greenAccent); + } else { + AudioService.instance.playBombSfx(); + triggerSpecialEffect("JOLLY! -1", Colors.redAccent); + } + HapticFeedback.heavyImpact(); + return; + } + } + bool isGold = newClosed.any((b) => b.type == BoxType.gold); bool isBomb = newClosed.any((b) => b.type == BoxType.bomb); bool isSwap = newClosed.any((b) => b.type == BoxType.swap); + bool isMultiplier = newClosed.any((b) => b.type == BoxType.multiplier); if (isSwap) { - // Usa temporaneamente playBonusSfx per lo swap, o un suono dedicato se lo aggiungerai AudioService.instance.playBonusSfx(); triggerSpecialEffect("SCAMBIO!", Colors.purpleAccent); HapticFeedback.heavyImpact(); + } else if (isMultiplier) { + AudioService.instance.playBonusSfx(); + triggerSpecialEffect("MOLTIPLICATORE x2!", Colors.yellowAccent); + HapticFeedback.heavyImpact(); } else if (isGold) { AudioService.instance.playBonusSfx(); triggerSpecialEffect("+2", Colors.amber); HapticFeedback.heavyImpact(); } else if (isBomb) { @@ -170,40 +266,32 @@ class GameController extends ChangeNotifier { void _startTimer() { _blitzTimer?.cancel(); - timeLeft = maxTime; + if (isSetupPhase) return; - if (!isTimeMode) { - notifyListeners(); - return; - } + timeLeft = maxTime; + if (!isTimeMode) { notifyListeners(); return; } _blitzTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (isGameOver || isCPUThinking) { timer.cancel(); return; } - if (timeLeft > 0) { - timeLeft--; - notifyListeners(); + timeLeft--; notifyListeners(); } else { timer.cancel(); - if (!isOnline || board.currentPlayer == myPlayer) { - _handleTimeOut(); - } + if (!isOnline || board.currentPlayer == myPlayer) { _handleTimeOut(); } } }); } void _handleTimeOut() { - if (!isTimeMode) return; + if (!isTimeMode || isSetupPhase) return; if (isOnline) { Line randomMove = AIEngine.getBestMove(board, 5); handleLineTap(randomMove, _activeTheme, forced: true); - } - else if (isVsCPU && board.currentPlayer == Player.red) { + } else if (isVsCPU && board.currentPlayer == Player.red) { Line randomMove = AIEngine.getBestMove(board, cpuLevel); handleLineTap(randomMove, _activeTheme, forced: true); - } - else if (!isVsCPU) { + } else if (!isVsCPU) { Line randomMove = AIEngine.getBestMove(board, 5); handleLineTap(randomMove, _activeTheme, forced: true); } @@ -216,15 +304,12 @@ class GameController extends ChangeNotifier { _effectTimer?.cancel(); _myReactionTimer?.cancel(); _oppReactionTimer?.cancel(); + _lastOpponentReactionTime = null; if (isOnline && roomCode != null) { FirebaseFirestore.instance.collection('games').doc(roomCode).update({'status': 'abandoned'}).catchError((e) => null); } - - isOnline = false; - roomCode = null; - currentMatchLevel = 1; - currentSeed = null; + isOnline = false; roomCode = null; currentMatchLevel = 1; currentSeed = null; } @override @@ -233,7 +318,6 @@ class GameController extends ChangeNotifier { void _listenToOnlineGame(String code) { _onlineSubscription = FirebaseFirestore.instance.collection('games').doc(code).snapshots().listen((doc) { if (!doc.exists) return; - var data = doc.data() as Map; onlineHostName = data['hostName'] ?? "ROSSO"; @@ -243,13 +327,16 @@ class GameController extends ChangeNotifier { opponentLeft = true; notifyListeners(); return; } - // --- ASCOLTO EMOJI E RIVINCITA --- String? p1React = data['p1_reaction']; + Timestamp? p1Time = data['p1_reaction_time'] as Timestamp?; String? p2React = data['p2_reaction']; + Timestamp? p2Time = data['p2_reaction_time'] as Timestamp?; - if (isHost && p2React != null && p2React != opponentReaction) { + if (isHost && p2React != null && p2Time != null && p2Time != _lastOpponentReactionTime) { + _lastOpponentReactionTime = p2Time; _showReaction(false, p2React); - } else if (!isHost && p1React != null && p1React != opponentReaction) { + } else if (!isHost && p1React != null && p1Time != null && p1Time != _lastOpponentReactionTime) { + _lastOpponentReactionTime = p1Time; _showReaction(false, p1React); } @@ -257,12 +344,10 @@ class GameController extends ChangeNotifier { bool p2Rematch = data['p2_rematch'] ?? false; opponentWantsRematch = isHost ? p2Rematch : p1Rematch; - // FIX: Rilevamento inizio nuova partita dopo rivincita - // Se il server ha resettato (status: playing, mosse: vuote) e noi avevamo chiesto rivincita 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']); - return; // Evitiamo di processare le mosse vuote + return; } if (p1Rematch && p2Rematch && isHost && data['status'] != 'playing') { @@ -273,26 +358,34 @@ class GameController extends ChangeNotifier { ArenaShape newShape = ArenaShape.values[rand.nextInt(ArenaShape.values.length)]; MultiplayerService().resetMatch(roomCode!, newRadius, newShape.name, newSeed); } - // --------------------------------- + + if (isSetupPhase) { + if (!isHost && data['p1_joker'] != null && !oppJokerPlaced) { + 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']; + board.boxes.firstWhere((b) => b.x == jx && b.y == jy).hiddenJokerOwner = Player.blue; + oppJokerPlaced = true; _checkSetupComplete(); + } + } List moves = data['moves'] ?? []; int hostLevel = data['matchLevel'] ?? 1; int? hostSeed = data['seed']; int hostRadius = data['radius'] ?? board.radius; - String shapeStr = data['shape'] ?? 'classic'; ArenaShape hostShape = ArenaShape.values.firstWhere((e) => e.name == shapeStr, orElse: () => ArenaShape.classic); onlineShape = hostShape; - isTimeMode = data['timeMode'] ?? true; - // FIX: Non resettare la board se le mosse non sono a 0 e stiamo solo aspettando la rivincita if (!rematchRequested && (hostLevel > currentMatchLevel || (isOnline && currentSeed == null && hostSeed != null) || (hostSeed != null && hostSeed != currentSeed))) { currentMatchLevel = hostLevel; currentSeed = hostSeed; int levelToUse = (currentMatchLevel == 1) ? 2 : currentMatchLevel; - board = GameBoard(radius: hostRadius, level: levelToUse, seed: currentSeed, shape: onlineShape); - board.currentPlayer = Player.red; // FIX Turno Iniziale + board.currentPlayer = Player.red; isCPUThinking = false; notifyListeners(); return; } @@ -302,7 +395,7 @@ class GameController extends ChangeNotifier { if (firebaseMovesCount == 0 && localMovesCount > 0 && !rematchRequested) { int levelToUse = (currentMatchLevel == 1) ? 2 : currentMatchLevel; board = GameBoard(radius: hostRadius, level: levelToUse, seed: currentSeed, shape: onlineShape); - board.currentPlayer = Player.red; // FIX Turno Iniziale + board.currentPlayer = Player.red; notifyListeners(); return; } @@ -321,25 +414,22 @@ class GameController extends ChangeNotifier { Player playerFromFirebase = (m['player'] == 'red') ? Player.red : Player.blue; bool isOpponentMove = (playerFromFirebase != myPlayer); List closedBefore = board.boxes.where((b) => b.owner != Player.none).toList(); + List ghostsBefore = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed).toList(); - // FIX: Forziamo la mossa usando il giocatore indicato da Firebase board.playMove(lineToPlay, forcedPlayer: playerFromFirebase); newMovesApplied = true; List newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList(); - if (isOpponentMove) _playEffects(newClosed, isOpponent: true); + List newGhosts = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed && !ghostsBefore.contains(b)).toList(); + + if (isOpponentMove) _playEffects(newClosed, newGhosts: newGhosts, isOpponent: true); } } if (newMovesApplied) { - // FIX: Sincronizzazione esplicita del turno basata su Firebase String expectedTurnStr = data['turn'] ?? 'red'; Player expectedTurn = expectedTurnStr == 'red' ? Player.red : Player.blue; - - if (!board.isGameOver && board.currentPlayer != expectedTurn) { - board.currentPlayer = expectedTurn; - } - + if (!board.isGameOver && board.currentPlayer != expectedTurn) { board.currentPlayer = expectedTurn; } _startTimer(); } @@ -350,17 +440,18 @@ class GameController extends ChangeNotifier { } void handleLineTap(Line line, AppThemeType theme, {bool forced = false}) { - if ((isCPUThinking || board.isGameOver || opponentLeft) && !forced) return; - - // Controllo Turno + if ((isSetupPhase || isCPUThinking || board.isGameOver || opponentLeft) && !forced) return; if (isOnline && board.currentPlayer != myPlayer && !forced) return; _activeTheme = theme; List closedBefore = board.boxes.where((b) => b.owner != Player.none).toList(); + List ghostsBefore = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed).toList(); if (board.playMove(line)) { List newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList(); - if (!forced) _playEffects(newClosed, isOpponent: false); + List newGhosts = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed && !ghostsBefore.contains(b)).toList(); + + if (!forced) _playEffects(newClosed, newGhosts: newGhosts, isOpponent: false); _startTimer(); notifyListeners(); @@ -369,8 +460,6 @@ class GameController extends ChangeNotifier { 'x1': line.p1.x, 'y1': line.p1.y, 'x2': line.p2.x, 'y2': line.p2.y, 'player': myPlayer == Player.red ? 'red' : 'blue' }; - - // FIX: Invia anche il currentPlayer aggiornato a Firebase per mantenere tutti sincronizzati String nextTurnStr = board.currentPlayer == Player.red ? 'red' : 'blue'; FirebaseFirestore.instance.collection('games').doc(roomCode).update({ @@ -382,7 +471,6 @@ class GameController extends ChangeNotifier { _saveMatchResult(); if (isHost) FirebaseFirestore.instance.collection('games').doc(roomCode).update({'status': 'finished'}); } - } else { if (board.isGameOver) _saveMatchResult(); else if (isVsCPU && board.currentPlayer == Player.blue) _checkCPUTurn(); @@ -397,11 +485,15 @@ class GameController extends ChangeNotifier { if (!board.isGameOver) { List closedBefore = board.boxes.where((b) => b.owner != Player.none).toList(); + List ghostsBefore = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed).toList(); + Line bestMove = AIEngine.getBestMove(board, cpuLevel); board.playMove(bestMove); List newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList(); - _playEffects(newClosed, isOpponent: true); + List newGhosts = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed && !ghostsBefore.contains(b)).toList(); + + _playEffects(newClosed, newGhosts: newGhosts, isOpponent: true); isCPUThinking = false; _startTimer(); notifyListeners(); @@ -415,22 +507,44 @@ class GameController extends ChangeNotifier { if (_hasSavedResult) return; _hasSavedResult = true; + int calculatedXP = 0; + bool isDraw = board.scoreRed == board.scoreBlue; String myRealName = StorageService.instance.playerName; if (myRealName.isEmpty) myRealName = "IO"; if (isOnline) { + bool isWin = isHost ? board.scoreRed > board.scoreBlue : board.scoreBlue > board.scoreRed; + calculatedXP = isWin ? 20 : (isDraw ? 5 : 2); 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); + + if (isWin) StorageService.instance.updateQuestProgress(0, 1); // Missione: Vinci Online + } else if (isVsCPU) { int myScore = board.scoreRed; int cpuScore = board.scoreBlue; - if (myScore > cpuScore) StorageService.instance.addWin(); - else if (cpuScore > myScore) StorageService.instance.addLoss(); + bool isWin = myScore > cpuScore; + calculatedXP = isWin ? (10 + (cpuLevel * 2)) : (isDraw ? 5 : 2); + + if (isWin) { + StorageService.instance.addWin(); + StorageService.instance.updateQuestProgress(1, 1); // Missione: Vinci vs CPU + } else if (cpuScore > myScore) { + StorageService.instance.addLoss(); + } 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); } + + // Se si sta giocando in una forma speciale (non classica) + if (board.shape != ArenaShape.classic) { + StorageService.instance.updateQuestProgress(2, 1); // Missione: Usa forme speciali + } + + lastMatchXP = calculatedXP; StorageService.instance.addXP(calculatedXP); notifyListeners(); } void increaseLevelAndRestart() { diff --git a/lib/models/game_board.dart b/lib/models/game_board.dart index ed2c081..1165c4a 100644 --- a/lib/models/game_board.dart +++ b/lib/models/game_board.dart @@ -5,9 +5,7 @@ import 'dart:math'; enum Player { red, blue, none } -enum BoxType { normal, gold, bomb, invisible, swap } - -// --- AGGIUNTO 'chaos' --- +enum BoxType { normal, gold, bomb, invisible, swap, ice, multiplier } // Aggiunti ice e multiplier enum ArenaShape { classic, cross, donut, hourglass, chaos } class Dot { @@ -26,6 +24,7 @@ class Line { final Dot p2; Player owner = Player.none; bool isPlayable = false; + bool isIceCracked = false; // NUOVO: Stato per il blocco di ghiaccio Line(this.p1, this.p2); @@ -39,6 +38,10 @@ class Box { late Line top, bottom, left, right; BoxType type = BoxType.normal; + bool isRevealed = false; + Player? hiddenJokerOwner; + bool isJokerRevealed = false; + Box(this.x, this.y); bool isClosed() { @@ -46,10 +49,13 @@ class Box { return top.owner != Player.none && bottom.owner != Player.none && left.owner != Player.none && right.owner != Player.none; } - int get value { + int getCalculatedValue(Player closer) { + if (hiddenJokerOwner != null) { + return (closer == hiddenJokerOwner) ? 2 : -1; + } if (type == BoxType.gold) return 2; if (type == BoxType.bomb) return -1; - if (type == BoxType.swap) return 0; + if (type == BoxType.swap || type == BoxType.ice || type == BoxType.multiplier) return 0; // Il moltiplicatore e il ghiaccio non danno punti base return 1; } } @@ -60,6 +66,9 @@ class GameBoard { final int? seed; final ArenaShape shape; + late int columns; + late int rows; + List dots = []; List lines = []; List boxes = []; @@ -71,93 +80,91 @@ class GameBoard { Line? lastMove; + // Variabili per il Moltiplicatore + bool redHasMultiplier = false; + bool blueHasMultiplier = false; + GameBoard({required this.radius, this.level = 1, this.seed, this.shape = ArenaShape.classic}) { _generateBoard(); } void _generateBoard() { - int size = radius * 2 + 1; final random = seed != null ? Random(seed) : Random(); - - // Se è Caos, decidiamo quale algoritmo usare in base al seed int chaosAlgorithm = random.nextInt(5); + if (shape == ArenaShape.chaos) { + columns = radius * 2 + 1; + rows = (radius * 3) + 2; + } else { + columns = radius * 2 + 1; + rows = radius * 2 + 1; + } + dots.clear(); lines.clear(); boxes.clear(); lastMove = null; - for (int y = 0; y < size; y++) { - for (int x = 0; x < size; x++) { + for (int y = 0; y < rows; y++) { + for (int x = 0; x < columns; x++) { var box = Box(x, y); + bool isVisible = true; - int dx = (x - radius).abs(); - int dy = (y - radius).abs(); + if (shape != ArenaShape.chaos) { + int dx = (x - radius).abs(); + int dy = (y - radius).abs(); + isVisible = (dx + dy) <= radius; - bool isVisible = (dx + dy) <= radius; - - if (isVisible) { - switch (shape) { - case ArenaShape.classic: - break; - 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; - case ArenaShape.chaos: - // --- GENERATORE PROCEDURALE (IL CAOS) --- - // Essendo basato su dx e dy, genererà sempre forme simmetriche a 4 vie! - if (chaosAlgorithm == 0) { - // Modello "Rete Frattale": Rimuove blocchi basati su operatori bitwise - if ((dx & dy) != 0) isVisible = false; - } else if (chaosAlgorithm == 1) { - // Modello "Quattro Pilastri": Svuota lunghe linee ma salva i centri - if (dx == 1 || dy == 1) { - if ((dx + dy) > 2 && (dx + dy) < radius) isVisible = false; - } - } else if (chaosAlgorithm == 2) { - // Modello "X-Treme": Taglia le diagonali perfette - if (dx == dy && dx > 0 && dx < radius) isVisible = false; - } else if (chaosAlgorithm == 3) { - // Modello "Scacchiera Nucleare": Alternanza precisa - if (dx % 2 == 1 && dy % 2 == 1) isVisible = false; - } else if (chaosAlgorithm == 4) { - // Modello "Anelli Frammentati": Buca gli anelli pari - if ((dx + dy) % 2 == 0 && (dx + dy) > 0) { - if (dx != 0 && dy != 0) isVisible = false; - } - } - // Assicuriamoci che il punto centrale esista quasi sempre per connettere la mappa - if (dx == 0 && dy == 0) isVisible = true; - break; + 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 (!isVisible) { box.type = BoxType.invisible; } else if (level > 1) { double chance = random.nextDouble(); - if (chance < 0.10) { - box.type = BoxType.gold; - } else if (chance > 0.90) { - box.type = BoxType.bomb; - } else if (level >= 5 && chance > 0.85 && chance <= 0.90) { - box.type = BoxType.swap; - } + 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); } } - // Costruzione Linee (Identico a prima) for (var box in boxes) { Dot tl = _getOrAddDot(box.x, box.y); Dot tr = _getOrAddDot(box.x + 1, box.y); @@ -170,10 +177,8 @@ class GameBoard { 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; + box.top.isPlayable = true; box.bottom.isPlayable = true; + box.left.isPlayable = true; box.right.isPlayable = true; } } } @@ -181,15 +186,13 @@ class GameBoard { 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; + 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; + lines.add(newLine); return newLine; } bool playMove(Line lineToPlay, {Player? forcedPlayer}) { @@ -198,45 +201,91 @@ class GameBoard { Player playerMakingMove = forcedPlayer ?? currentPlayer; Line? actualLine; for (var l in lines) { - if (l.connects(lineToPlay.p1, lineToPlay.p2)) { - actualLine = l; break; - } + if (l.connects(lineToPlay.p1, lineToPlay.p2)) { actualLine = l; break; } } if (actualLine == null || actualLine.owner != Player.none || !actualLine.isPlayable) return false; + // --- LOGICA BLOCCO DI GHIACCIO --- + bool closesIce = false; + for (var box in boxes) { + 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 (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. + } + + // Mossa normale o secondo colpo al ghiaccio + actualLine.isIceCracked = false; actualLine.owner = playerMakingMove; lastMove = actualLine; - bool boxedClosed = false; + bool scoredPoint = false; bool triggeredSwap = false; for (var box in boxes) { if (box.owner == Player.none && box.isClosed()) { box.owner = playerMakingMove; - boxedClosed = true; + scoredPoint = true; - if (playerMakingMove == Player.red) { scoreRed += box.value; } - else { scoreBlue += box.value; } + if (box.hiddenJokerOwner != null) box.isJokerRevealed = true; - if (box.type == BoxType.swap) { + 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 dà 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; + 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 (!boxedClosed && !isGameOver) { currentPlayer = (currentPlayer == Player.red) ? Player.blue : Player.red; } + if (!scoredPoint && !isGameOver) { currentPlayer = (currentPlayer == Player.red) ? Player.blue : Player.red; } + else if (scoredPoint && !isGameOver) { currentPlayer = playerMakingMove; } } else { - if (!boxedClosed && !isGameOver) { currentPlayer = (forcedPlayer == Player.red) ? Player.blue : Player.red; } + if (!scoredPoint && !isGameOver) { currentPlayer = (forcedPlayer == Player.red) ? Player.blue : Player.red; } else { currentPlayer = forcedPlayer; } } diff --git a/lib/services/audio_service.dart b/lib/services/audio_service.dart index 7475c84..982e2bf 100644 --- a/lib/services/audio_service.dart +++ b/lib/services/audio_service.dart @@ -1,3 +1,7 @@ +// =========================================================================== +// FILE: lib/services/audio_service.dart +// =========================================================================== + import 'package:flutter/material.dart'; import 'package:audioplayers/audioplayers.dart'; import '../core/app_colors.dart'; @@ -18,11 +22,15 @@ class AudioService extends ChangeNotifier { if (isMuted) return; String file = ''; switch (theme) { - case AppThemeType.minimal: file = 'minimal_line.wav'; break; + case AppThemeType.minimal: + case AppThemeType.arcade: // Suono secco per l'arcade + file = 'minimal_line.wav'; break; case AppThemeType.doodle: case AppThemeType.wood: file = 'doodle_line.wav'; break; - case AppThemeType.cyberpunk: file = 'cyber_line.wav'; break; + case AppThemeType.cyberpunk: + case AppThemeType.grimorio: // Suono etereo per la magia + file = 'cyber_line.wav'; break; } await _sfxPlayer.play(AssetSource('audio/sfx/$file')); } @@ -31,25 +39,26 @@ class AudioService extends ChangeNotifier { if (isMuted) return; String file = ''; switch (theme) { - case AppThemeType.minimal: file = 'minimal_box.wav'; break; + case AppThemeType.minimal: + case AppThemeType.arcade: + file = 'minimal_box.wav'; break; case AppThemeType.doodle: case AppThemeType.wood: file = 'doodle_box.wav'; break; - case AppThemeType.cyberpunk: file = 'cyber_box.wav'; break; + case AppThemeType.cyberpunk: + case AppThemeType.grimorio: + file = 'cyber_box.wav'; break; } await _sfxPlayer.play(AssetSource('audio/sfx/$file')); } - // --- NUOVI EFFETTI SPECIALI --- void playBonusSfx() async { if (isMuted) return; - // Assicurati di aggiungere questo file nella cartella assets/audio/sfx/ await _sfxPlayer.play(AssetSource('audio/sfx/bonus.wav')); } void playBombSfx() async { if (isMuted) return; - // Assicurati di aggiungere questo file nella cartella assets/audio/sfx/ await _sfxPlayer.play(AssetSource('audio/sfx/bomb.wav')); } } \ No newline at end of file diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart index 61b4529..f53c2e7 100644 --- a/lib/services/storage_service.dart +++ b/lib/services/storage_service.dart @@ -1,5 +1,10 @@ +// =========================================================================== +// FILE: lib/services/storage_service.dart +// =========================================================================== + import 'dart:convert'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; import '../core/app_colors.dart'; class StorageService { @@ -8,13 +13,12 @@ class StorageService { late SharedPreferences _prefs; - // Si avvia quando apriamo l'app Future init() async { _prefs = await SharedPreferences.getInstance(); + _checkDailyQuests(); // All'avvio controlliamo se ci sono nuove sfide } - // --- IMPOSTAZIONI --- - int get savedThemeIndex => _prefs.getInt('theme') ?? AppThemeType.cyberpunk.index; + int get savedThemeIndex => _prefs.getInt('theme') ?? AppThemeType.minimal.index; Future saveTheme(AppThemeType theme) async => await _prefs.setInt('theme', theme.index); int get savedRadius => _prefs.getInt('radius') ?? 2; @@ -23,9 +27,21 @@ class StorageService { bool get isMuted => _prefs.getBool('isMuted') ?? false; Future saveMuted(bool muted) async => await _prefs.setBool('isMuted', muted); - // --- STATISTICHE VS CPU --- + int get totalXP => _prefs.getInt('totalXP') ?? 0; + + // Modificato per sincronizzare automaticamente la classifica su Firebase + Future addXP(int xp) async { + await _prefs.setInt('totalXP', totalXP + xp); + syncLeaderboard(); + } + + int get playerLevel => (totalXP / 100).floor() + 1; + int get wins => _prefs.getInt('wins') ?? 0; - Future addWin() async => await _prefs.setInt('wins', wins + 1); + Future addWin() async { + await _prefs.setInt('wins', wins + 1); + syncLeaderboard(); + } int get losses => _prefs.getInt('losses') ?? 0; Future addLoss() async => await _prefs.setInt('losses', losses + 1); @@ -33,9 +49,66 @@ class StorageService { int get cpuLevel => _prefs.getInt('cpuLevel') ?? 1; Future saveCpuLevel(int level) async => await _prefs.setInt('cpuLevel', level); - // --- MULTIPLAYER --- String get playerName => _prefs.getString('playerName') ?? ''; - Future savePlayerName(String name) async => await _prefs.setString('playerName', name); + Future savePlayerName(String name) async { + await _prefs.setString('playerName', name); + syncLeaderboard(); // Aggiorna il nome in classifica + } + + // --- NUOVO: SINCRONIZZAZIONE CLASSIFICA ONLINE --- + Future 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)); + } catch(e) { + // Ignoriamo gli errori se manca la rete, si sincronizzerà dopo + } + } + } + + // --- 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); + } + } + + Future updateQuestProgress(int type, int amount) async { + for(int i=1; i<=3; i++) { + if (_prefs.getInt('q${i}_type') == type) { + int prog = _prefs.getInt('q${i}_prog') ?? 0; + int target = _prefs.getInt('q${i}_target') ?? 1; + if (prog < target) { + _prefs.setInt('q${i}_prog', prog + amount); + } + } + } + } // --- STORICO PARTITE --- List> get matchHistory { @@ -43,27 +116,14 @@ class StorageService { return history.map((e) => jsonDecode(e) as Map).toList(); } - // Salviamo sia il nostro nome che quello dell'avversario Future saveMatchToHistory({required String myName, required String opponent, required int myScore, required int oppScore, required bool isOnline}) async { List history = _prefs.getStringList('matchHistory') ?? []; - Map match = { 'date': DateTime.now().toIso8601String(), - 'myName': myName, - 'opponent': opponent, - 'myScore': myScore, - 'oppScore': oppScore, - 'isOnline': isOnline, + 'myName': myName, 'opponent': opponent, 'myScore': myScore, 'oppScore': oppScore, 'isOnline': isOnline, }; - - // Aggiungiamo in cima (il più recente per primo) history.insert(0, jsonEncode(match)); - - // Teniamo solo le ultime 50 partite per non intasare la memoria - if (history.length > 50) { - history = history.sublist(0, 50); - } - + if (history.length > 50) history = history.sublist(0, 50); await _prefs.setStringList('matchHistory', history); } } \ No newline at end of file diff --git a/lib/ui/game/board_painter.dart b/lib/ui/game/board_painter.dart index ffe9118..ff8f6ce 100644 --- a/lib/ui/game/board_painter.dart +++ b/lib/ui/game/board_painter.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; + import '../../models/game_board.dart'; import '../../core/app_colors.dart'; @@ -13,7 +14,23 @@ class BoardPainter extends CustomPainter { final AppThemeType themeType; final double blinkValue; - BoardPainter({required this.board, required this.theme, required this.themeType, this.blinkValue = 0.0}); + final bool isOnline; + final bool isVsCPU; + final bool isSetupPhase; + final Player myPlayer; + final Player jokerTurn; + + BoardPainter({ + required this.board, + required this.theme, + required this.themeType, + required this.isOnline, + required this.isVsCPU, + required this.isSetupPhase, + required this.myPlayer, + required this.jokerTurn, + this.blinkValue = 0.0 + }); @override void paint(Canvas canvas, Size size) { @@ -32,19 +49,28 @@ class BoardPainter extends CustomPainter { } } - int gridPoints = board.radius * 2 + 2; + 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); - // --- 1. DISEGNO AREE CONQUISTATE E ICONE --- for (var box in board.boxes) { - if (box.type == BoxType.invisible) continue; - 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) { final boxPaint = Paint() ..style = PaintingStyle.fill @@ -55,41 +81,65 @@ class BoardPainter extends CustomPainter { } 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, Icons.star_rounded, Colors.amber); + _drawIconInBox(canvas, rect, ThemeIcons.gold(themeType), Colors.amber); } else if (box.type == BoxType.bomb) { - _drawIconInBox(canvas, rect, Icons.mood_bad_rounded, themeType == AppThemeType.cyberpunk ? Colors.greenAccent : Colors.deepPurple); + _drawIconInBox(canvas, rect, ThemeIcons.bomb(themeType), themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? Colors.greenAccent : Colors.deepPurple); } else if (box.type == BoxType.swap) { - // NUOVA ICONA SWAP: Frecce circolari viola (o cyan) per indicare l'inversione - _drawIconInBox(canvas, rect, Icons.sync_rounded, Colors.purpleAccent); + _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); } } - // --- 2. DISEGNO LINEE CON EFFETTO LAMPEGGIAMENTO --- for (var line in board.lines) { if (!line.isPlayable) continue; Offset p1 = getScreenPos(line.p1.x, line.p1.y); Offset p2 = getScreenPos(line.p2.x, line.p2.y); - bool isLastMove = (line == board.lastMove); + // --- 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) { - 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 (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) { @@ -97,35 +147,30 @@ class BoardPainter extends CustomPainter { 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; - } + 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; - } + 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 (isLastMove && line.owner != Player.none) { - lineColor = Color.lerp(lineColor, Colors.white, blinkValue * 0.5) ?? lineColor; - } + 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); } } - // --- 3. DISEGNO PUNTINI --- final dotPaint = Paint()..style = PaintingStyle.fill; - Set activeDots = {}; for (var line in board.lines) { if (line.isPlayable) { - activeDots.add(line.p1); - activeDots.add(line.p2); + activeDots.add(line.p1); activeDots.add(line.p2); } } @@ -138,6 +183,13 @@ class BoardPainter extends CustomPainter { 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 { canvas.drawCircle(pos, 5.0, dotPaint..color = theme.text.withOpacity(0.6)); } @@ -149,189 +201,146 @@ class BoardPainter extends CustomPainter { textPainter.text = TextSpan( text: String.fromCharCode(icon.codePoint), style: TextStyle( - color: color.withOpacity(0.7), + color: themeType == AppThemeType.arcade ? color : color.withOpacity(0.7), fontSize: rect.width * 0.45, fontFamily: icon.fontFamily, package: icon.fontPackage, - shadows: [Shadow(color: color.withOpacity(0.6), blurRadius: 10, offset: const Offset(0, 0))] + 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); + + // Effetto linea frammentata + canvas.drawLine(p1, p2, Paint()..color = Colors.cyan.withOpacity(0.2)..strokeWidth=6.0); + + 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); + int zigzags = 6; + for (int i=1; i true; + @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); - } + 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); } } \ No newline at end of file diff --git a/lib/ui/game/game_screen.dart b/lib/ui/game/game_screen.dart index f39d17b..c3dc480 100644 --- a/lib/ui/game/game_screen.dart +++ b/lib/ui/game/game_screen.dart @@ -6,12 +6,27 @@ import 'dart:ui'; import 'dart:math' as math; 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 'board_painter.dart'; import 'score_board.dart'; -import '../../models/game_board.dart'; +import 'package:google_fonts/google_fonts.dart'; + +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 GameScreen extends StatefulWidget { const GameScreen({super.key}); @@ -25,20 +40,19 @@ class _GameScreenState extends State with TickerProviderStateMixin { bool _gameOverDialogShown = false; bool _opponentLeftDialogShown = false; + // Variabili per coprire il posizionamento del Jolly in Locale + bool _hideJokerMessage = false; + bool _wasSetupPhase = false; + Player _lastJokerTurn = Player.red; + @override void initState() { super.initState(); - _blinkController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 600), - )..repeat(reverse: true); + _blinkController = AnimationController(vsync: this, duration: const Duration(milliseconds: 600))..repeat(reverse: true); } @override - void dispose() { - _blinkController.dispose(); - super.dispose(); - } + void dispose() { _blinkController.dispose(); super.dispose(); } void _showGameOverDialog(BuildContext context, GameController game, ThemeColors theme, AppThemeType themeType) { _gameOverDialogShown = true; @@ -50,22 +64,21 @@ class _GameScreenState extends State with TickerProviderStateMixin { builder: (context, controller, child) { if (!controller.isGameOver) { WidgetsBinding.instance.addPostFrameCallback((_) { - if (Navigator.canPop(dialogContext)) Navigator.pop(dialogContext); - _gameOverDialogShown = false; + if (_gameOverDialogShown) { + _gameOverDialogShown = false; + if (Navigator.canPop(dialogContext)) Navigator.pop(dialogContext); + } }); - return const SizedBox(); + return const SizedBox.shrink(); } - int red = controller.board.scoreRed; - int blue = controller.board.scoreBlue; + 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 ? "VERDE" : "BLU"); + String nameBlue = controller.isOnline ? controller.onlineGuestName.toUpperCase() : (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? "VERDE" : "BLU"); if (controller.isVsCPU) nameBlue = "CPU"; - String winnerText = ""; - Color winnerColor = theme.text; + String winnerText = ""; Color winnerColor = theme.text; if (red > blue) { winnerText = "VINCE $nameRed!"; winnerColor = theme.playerRed; } else if (blue > red) { winnerText = "VINCE $nameBlue!"; winnerColor = theme.playerBlue; } else { winnerText = "PAREGGIO!"; winnerColor = theme.text; } @@ -73,11 +86,11 @@ class _GameScreenState extends State with TickerProviderStateMixin { 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)), + title: Text("FINE PARTITA", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 22))), content: Column( mainAxisSize: MainAxisSize.min, children: [ - Text(winnerText, textAlign: TextAlign.center, style: TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: winnerColor)), + 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), @@ -85,24 +98,39 @@ class _GameScreenState extends State with TickerProviderStateMixin { 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)), + 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)] : [], + ), + 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: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: theme.text.withOpacity(0.7))), + 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: TextStyle(color: Colors.amber, fontWeight: FontWeight.bold, fontStyle: FontStyle.italic)), + 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: TextStyle(color: Colors.greenAccent, fontWeight: FontWeight.bold)), + 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: TextStyle(color: Colors.green, fontWeight: FontWeight.bold)), + Text("Avvio nuova partita...", style: _getTextStyle(themeType, const TextStyle(color: Colors.green, fontWeight: FontWeight.bold))), ] ], ), @@ -115,32 +143,30 @@ class _GameScreenState extends State with TickerProviderStateMixin { 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: () { Navigator.pop(dialogContext); _gameOverDialogShown = false; controller.increaseLevelAndRestart(); }, - child: const Text("PROSSIMO LIVELLO ➔", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), + 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: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 1.0)), + 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: () { Navigator.pop(dialogContext); _gameOverDialogShown = false; controller.startNewGame(controller.board.radius, vsCPU: controller.isVsCPU); }, - child: const Text("RIGIOCA", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 2)), + 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(); - Navigator.pop(dialogContext); - Navigator.pop(context); + _gameOverDialogShown = false; + Navigator.pop(dialogContext); Navigator.pop(context); }, - child: Text("TORNA AL MENU", style: TextStyle(fontWeight: FontWeight.bold, color: theme.text, fontSize: 14, letterSpacing: 1.5)), + child: Text("TORNA AL MENU", style: _getTextStyle(themeType, TextStyle(fontWeight: FontWeight.bold, color: theme.text, fontSize: 14, letterSpacing: 1.5))), ), ], ) @@ -151,6 +177,68 @@ class _GameScreenState extends State 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 { + // --- TESTI MODALITÀ LOCALE --- + 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)"; + } + + 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 (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); + } + } + @override Widget build(BuildContext context) { final themeManager = context.watch(); @@ -158,6 +246,18 @@ class _GameScreenState extends State with TickerProviderStateMixin { final theme = themeManager.currentColors; final gameController = context.watch(); + // --- LOGICA CAMBIO TURNO E SCHERMATA JOLLY --- + if (gameController.isSetupPhase && !_wasSetupPhase) { + // È appena iniziata una nuova partita + _hideJokerMessage = false; + _lastJokerTurn = Player.red; + } else if (gameController.isSetupPhase && gameController.jokerTurn != _lastJokerTurn) { + // È cambiato il turno durante il setup (in modalità locale), rifacciamo apparire la copertura + _hideJokerMessage = false; + _lastJokerTurn = gameController.jokerTurn; + } + _wasSetupPhase = gameController.isSetupPhase; + WidgetsBinding.instance.addPostFrameCallback((_) { if (gameController.opponentLeft && !_opponentLeftDialogShown) { _opponentLeftDialogShown = true; @@ -167,14 +267,14 @@ class _GameScreenState extends State with TickerProviderStateMixin { builder: (dialogContext) => AlertDialog( backgroundColor: theme.background, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), - title: Text("VITTORIA A TAVOLINO!", textAlign: TextAlign.center, style: TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold)), - content: Text("L'avversario ha abbandonato la stanza.\nSei il vincitore incontestato!", textAlign: TextAlign.center, style: TextStyle(color: theme.text, fontSize: 16)), + 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: const Text("MENU PRINCIPALE", style: TextStyle(fontWeight: FontWeight.bold)), + child: Text("MENU PRINCIPALE", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold))), ) ], ) @@ -188,7 +288,7 @@ class _GameScreenState extends State with TickerProviderStateMixin { if (themeType == AppThemeType.wood) bgImage = 'assets/images/wood_bg.jpg'; if (themeType == AppThemeType.doodle) bgImage = 'assets/images/doodle_bg.jpg'; - Color indicatorColor = themeType == AppThemeType.cyberpunk ? Colors.white : Colors.black; + Color indicatorColor = themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? Colors.white : Colors.black; Widget emojiBar = const SizedBox(); if (gameController.isOnline && !gameController.isGameOver) { @@ -196,7 +296,7 @@ class _GameScreenState extends State with TickerProviderStateMixin { emojiBar = Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), decoration: BoxDecoration( - color: themeType == AppThemeType.cyberpunk ? Colors.black.withOpacity(0.6) : Colors.white.withOpacity(0.8), + 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), ), @@ -220,25 +320,38 @@ class _GameScreenState extends State with TickerProviderStateMixin { child: Center( child: Padding( padding: const EdgeInsets.all(10.0), - child: AspectRatio( - aspectRatio: 1, - child: LayoutBuilder( - builder: (context, constraints) { - return GestureDetector( + child: LayoutBuilder( + builder: (context, constraints) { + int cols = gameController.board.columns + 1; + int rows = gameController.board.rows + 1; + double boxSize = constraints.maxWidth / cols; + double requiredHeight = boxSize * rows; + if (requiredHeight > constraints.maxHeight) { boxSize = constraints.maxHeight / rows; } + double actualWidth = boxSize * cols; + double actualHeight = boxSize * rows; + + return SizedBox( + width: actualWidth, height: actualHeight, + child: GestureDetector( behavior: HitTestBehavior.opaque, - onTapDown: (details) => _handleTap(details.localPosition, constraints.maxWidth, gameController, themeType), + onTapDown: (details) => _handleTap(details.localPosition, actualWidth, actualHeight, gameController, themeType), child: AnimatedBuilder( animation: _blinkController, builder: (context, child) { return CustomPaint( - size: Size(constraints.maxWidth, constraints.maxHeight), - painter: BoardPainter(board: gameController.board, theme: theme, themeType: themeType, blinkValue: _blinkController.value), + 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, + ), ); } ), - ); - } - ), + ), + ); + } ), ), ), @@ -257,7 +370,7 @@ class _GameScreenState extends State with TickerProviderStateMixin { mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.smart_toy_rounded, size: 16, color: indicatorColor), const SizedBox(width: 8), - Text("LIVELLO CPU: ${gameController.cpuLevel}", style: TextStyle(color: indicatorColor, fontWeight: FontWeight.bold, fontSize: 13, letterSpacing: 1.0)), + Text("LIVELLO CPU: ${gameController.cpuLevel}", style: _getTextStyle(themeType, TextStyle(color: indicatorColor, fontWeight: FontWeight.bold, fontSize: 11, letterSpacing: 1.0))), ], ), ) @@ -267,10 +380,10 @@ class _GameScreenState extends State with TickerProviderStateMixin { 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 ? 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 ? Colors.white : theme.text, size: 20), + 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: TextStyle(color: bgImage != null ? Colors.white : theme.text, fontWeight: FontWeight.bold, fontSize: 14)), + label: Text("ESCI", style: _getTextStyle(themeType, TextStyle(color: bgImage != null || themeType == AppThemeType.arcade ? Colors.white : theme.text, fontWeight: FontWeight.bold, fontSize: 12))), ), ), ], @@ -280,19 +393,9 @@ class _GameScreenState extends State with TickerProviderStateMixin { ), if (gameController.myReaction != null) - Positioned( - top: 80, - left: gameController.isHost ? 30 : null, - right: gameController.isHost ? null : 30, - child: _BouncingEmoji(emoji: gameController.myReaction!), - ), + 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!), - ), + Positioned(top: 80, left: !gameController.isHost ? 30 : null, right: !gameController.isHost ? null : 30, child: _BouncingEmoji(emoji: gameController.opponentReaction!)), ], ), ); @@ -308,26 +411,36 @@ class _GameScreenState extends State with TickerProviderStateMixin { 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) - Positioned.fill(child: BlitzBackgroundEffect(timeLeft: gameController.timeLeft, color: theme.playerRed)), + 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)), + Positioned.fill(child: SpecialEventBackgroundEffect(text: gameController.effectText, color: gameController.effectColor, themeType: themeType)), Positioned.fill(child: gameContent), - // ========================================== - // EFFETTI VISIVI (VFX) DI FINE PARTITA - // ========================================== - if (gameController.isGameOver && gameController.board.scoreRed != gameController.board.scoreBlue) + // --- SCHERMATA COPRENTE PER IL PASSAGGIO DEL TELEFONO IN LOCALE --- + if (gameController.isSetupPhase && !_hideJokerMessage) Positioned.fill( - child: IgnorePointer( - child: WinnerVFXOverlay( - winnerColor: gameController.board.scoreRed > gameController.board.scoreBlue ? theme.playerRed : theme.playerBlue, - themeType: themeType, + child: Container( + // Il colore di sfondo riempie tutto lo schermo per non far sbirciare la griglia + 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))), ], ), ), @@ -336,29 +449,24 @@ class _GameScreenState extends State with TickerProviderStateMixin { ); } - void _handleTap(Offset tapPos, double size, GameController controller, AppThemeType themeType) { + void _handleTap(Offset tapPos, double width, double height, GameController controller, AppThemeType themeType) { final board = controller.board; if (board.isGameOver) return; + int cols = board.columns + 1; double spacing = width / cols; double offset = spacing / 2; - int gridPoints = board.radius * 2 + 2; - double spacing = size / gridPoints; - double offset = spacing / 2; - - 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 = 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 (controller.isSetupPhase) { + 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 = spacing * 0.4; + for (var line in board.lines) { + if (line.owner != Player.none || !line.isPlayable) continue; + 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); } } @@ -371,30 +479,16 @@ class _GameScreenState extends State with TickerProviderStateMixin { } } -// =========================================================================== -// CLASSI PER IL MOTORE PARTICELLARE (VFX) DI FINE PARTITA -// =========================================================================== - class _Particle { - double x, y; - double vx, vy; - Color color; - double size; - double angle; - double spin; - int type; // 0=cerchio, 1=quadrato, 2=triangolo - + double x, y, vx, vy, size, angle, spin; + Color color; int type; _Particle({required this.x, required this.y, required this.vx, required this.vy, required this.color, required this.size, required this.angle, required this.spin, required this.type}); } class WinnerVFXOverlay extends StatefulWidget { - final Color winnerColor; - final AppThemeType themeType; - + final Color winnerColor; final AppThemeType themeType; const WinnerVFXOverlay({super.key, required this.winnerColor, required this.themeType}); - - @override - State createState() => _WinnerVFXOverlayState(); + @override State createState() => _WinnerVFXOverlayState(); } class _WinnerVFXOverlayState extends State with SingleTickerProviderStateMixin { @@ -406,291 +500,125 @@ class _WinnerVFXOverlayState extends State with SingleTickerPr @override void initState() { super.initState(); - // L'animazione gira a 60fps per 4 secondi e poi si ferma - _vfxController = AnimationController(vsync: this, duration: const Duration(seconds: 4)) - ..addListener(() { - _updateParticles(); - }) - ..forward(); + _vfxController = AnimationController(vsync: this, duration: const Duration(seconds: 4))..addListener(() { _updateParticles(); })..forward(); } @override void didChangeDependencies() { super.didChangeDependencies(); - if (!_initialized) { - _initParticles(MediaQuery.of(context).size); - _initialized = true; - } + if (!_initialized) { _initParticles(MediaQuery.of(context).size); _initialized = true; } } void _initParticles(Size screenSize) { int particleCount = widget.themeType == AppThemeType.cyberpunk ? 150 : 100; + if (widget.themeType == AppThemeType.arcade) particleCount = 80; + if (widget.themeType == AppThemeType.grimorio) particleCount = 120; - // Lista di colori da mixare (colore vincitore + bianco + colori a tema) List palette = [widget.winnerColor, widget.winnerColor.withOpacity(0.7), Colors.white]; - if (widget.themeType == AppThemeType.cyberpunk) { - palette.add(Colors.cyanAccent); - palette.add(Colors.yellowAccent); - } else if (widget.themeType == AppThemeType.doodle) { - palette.add(const Color(0xFF00008B)); // Inchiostro biro - palette.add(Colors.redAccent); - } else if (widget.themeType == AppThemeType.wood) { - palette = [Colors.orangeAccent, Colors.yellow, Colors.red, Colors.white]; - } + if (widget.themeType == AppThemeType.cyberpunk) { palette.add(Colors.cyanAccent); palette.add(Colors.yellowAccent); } + else if (widget.themeType == AppThemeType.doodle) { palette.add(const Color(0xFF00008B)); palette.add(Colors.redAccent); } + 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]; } for (int i = 0; i < particleCount; i++) { - // Esplosione dal centro verso l'esterno double speed = _rand.nextDouble() * 20 + 5; double theta = _rand.nextDouble() * 2 * math.pi; - - _particles.add(_Particle( - x: screenSize.width / 2, - y: screenSize.height / 2, - vx: speed * math.cos(theta), - vy: speed * math.sin(theta) - 5, // Leggera spinta verso l'alto - color: palette[_rand.nextInt(palette.length)], - size: _rand.nextDouble() * 10 + 6, - angle: _rand.nextDouble() * math.pi, - spin: (_rand.nextDouble() - 0.5) * 0.5, - type: _rand.nextInt(3), - )); + _particles.add(_Particle(x: screenSize.width / 2, y: screenSize.height / 2, vx: speed * math.cos(theta), vy: speed * math.sin(theta) - 5, color: palette[_rand.nextInt(palette.length)], size: _rand.nextDouble() * 10 + 6, angle: _rand.nextDouble() * math.pi, spin: (_rand.nextDouble() - 0.5) * 0.5, type: _rand.nextInt(3))); } } void _updateParticles() { setState(() { for (var p in _particles) { - p.x += p.vx; - p.y += p.vy; - - // Gravità e attrito - if (widget.themeType == AppThemeType.cyberpunk) { - p.vy += 0.1; // Gravità bassa (fluttuano di più) - p.vx *= 0.98; // Attrito - p.vy *= 0.98; - } else if (widget.themeType == AppThemeType.wood) { - p.vy -= 0.2; // Vanno verso l'alto come fumo/scintille! - p.x += math.sin(p.y * 0.05) * 2; // Tremolio - } else { - p.vy += 0.5; // Gravità standard (coriandoli cadono) - } - - p.angle += p.spin; - p.size *= 0.99; // Si rimpiccioliscono nel tempo + 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; } + 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; } + else { p.vy += 0.5; } + p.angle += p.spin; p.size *= 0.99; } }); } - @override - void dispose() { - _vfxController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return CustomPaint( - painter: _VFXPainter(particles: _particles, themeType: widget.themeType), - child: Container(), - ); - } + @override void dispose() { _vfxController.dispose(); super.dispose(); } + @override Widget build(BuildContext context) { return CustomPaint(painter: _VFXPainter(particles: _particles, themeType: widget.themeType), child: Container()); } } class _VFXPainter extends CustomPainter { - final List<_Particle> particles; - final AppThemeType themeType; - + final List<_Particle> particles; final AppThemeType themeType; _VFXPainter({required this.particles, required this.themeType}); @override void paint(Canvas canvas, Size size) { for (var p in particles) { if (p.size < 0.5) continue; - - final paint = Paint() - ..color = p.color - ..style = PaintingStyle.fill; - - // Glow per il Cyberpunk - if (themeType == AppThemeType.cyberpunk) { - paint.maskFilter = const MaskFilter.blur(BlurStyle.solid, 4.0); - } - - canvas.save(); - canvas.translate(p.x, p.y); - canvas.rotate(p.angle); + final paint = Paint()..color = p.color..style = PaintingStyle.fill; + if (themeType == AppThemeType.cyberpunk) { 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) { - // Stile schizzato - paint.style = PaintingStyle.stroke; - paint.strokeWidth = 2.0; - if (p.type == 0) { - canvas.drawCircle(Offset.zero, p.size, paint); - } else { - canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: p.size*2, height: p.size*2), paint); - } + paint.style = PaintingStyle.stroke; paint.strokeWidth = 2.0; + if (p.type == 0) { canvas.drawCircle(Offset.zero, p.size, paint); } else { canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: p.size*2, height: p.size*2), paint); } } else if (themeType == AppThemeType.wood) { - // Scintille rotonde e sfuocate - paint.maskFilter = const MaskFilter.blur(BlurStyle.normal, 3.0); + paint.maskFilter = const MaskFilter.blur(BlurStyle.normal, 3.0); canvas.drawCircle(Offset.zero, p.size, paint); + } else if (themeType == AppThemeType.arcade) { + canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: p.size * 1.5, height: p.size * 1.5), paint); + } else if (themeType == AppThemeType.grimorio) { + paint.maskFilter = const MaskFilter.blur(BlurStyle.normal, 4.0); canvas.drawCircle(Offset.zero, p.size, paint); + canvas.drawCircle(Offset.zero, p.size * 0.3, Paint()..color=Colors.white..style=PaintingStyle.fill); } else { - // Forme standard per Minimal e Cyberpunk - if (p.type == 0) { - canvas.drawCircle(Offset.zero, p.size, paint); - } else if (p.type == 1) { - canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: p.size * 2, height: p.size * 2), paint); - } else { - var path = Path() - ..moveTo(0, -p.size) - ..lineTo(p.size, p.size) - ..lineTo(-p.size, p.size) - ..close(); - canvas.drawPath(path, paint); - } + if (p.type == 0) { canvas.drawCircle(Offset.zero, p.size, paint); } + else if (p.type == 1) { canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: p.size * 2, height: p.size * 2), paint); } + else { var path = Path()..moveTo(0, -p.size)..lineTo(p.size, p.size)..lineTo(-p.size, p.size)..close(); canvas.drawPath(path, paint); } } canvas.restore(); } } - - @override - bool shouldRepaint(covariant _VFXPainter oldDelegate) => true; + @override bool shouldRepaint(covariant _VFXPainter oldDelegate) => true; } -// =========================================================================== -// WIDGET INTERNI ESISTENTENTI -// =========================================================================== - class _BouncingEmoji extends StatefulWidget { - final String emoji; - const _BouncingEmoji({required this.emoji}); - - @override - State<_BouncingEmoji> createState() => _BouncingEmojiState(); + final String emoji; const _BouncingEmoji({required this.emoji}); + @override State<_BouncingEmoji> createState() => _BouncingEmojiState(); } class _BouncingEmojiState extends State<_BouncingEmoji> with SingleTickerProviderStateMixin { - late AnimationController _ctrl; - late Animation _anim; - - @override - void initState() { - super.initState(); - _ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 500))..repeat(reverse: true); - _anim = Tween(begin: -10, end: 10).animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeInOut)); - } - - @override - void dispose() { _ctrl.dispose(); super.dispose(); } - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: _anim, - builder: (ctx, child) => Transform.translate( - offset: Offset(0, _anim.value), - child: Container( - padding: const EdgeInsets.all(8), - decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle, boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 5)]), - child: Text(widget.emoji, style: const TextStyle(fontSize: 32)), - ), - ), - ); - } + late AnimationController _ctrl; late Animation _anim; + @override void initState() { super.initState(); _ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 500))..repeat(reverse: true); _anim = Tween(begin: -10, end: 10).animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeInOut)); } + @override void dispose() { _ctrl.dispose(); super.dispose(); } + @override Widget build(BuildContext context) { return AnimatedBuilder(animation: _anim, builder: (ctx, child) => Transform.translate(offset: Offset(0, _anim.value), child: Container(padding: const EdgeInsets.all(8), decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle, boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 5)]), child: Text(widget.emoji, style: const TextStyle(fontSize: 32))))); } } class FullScreenGridPainter extends CustomPainter { - final Color gridColor; - FullScreenGridPainter(this.gridColor); - - @override - void paint(Canvas canvas, Size size) { - final Paint paperGridPaint = Paint()..color = gridColor..strokeWidth = 1.0..style = PaintingStyle.stroke; - 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); - } - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; + final Color gridColor; FullScreenGridPainter(this.gridColor); + @override void paint(Canvas canvas, Size size) { final Paint paperGridPaint = Paint()..color = gridColor..strokeWidth = 1.0..style = PaintingStyle.stroke; 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); } + @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } class BlitzBackgroundEffect extends StatefulWidget { - final int timeLeft; - final Color color; - const BlitzBackgroundEffect({super.key, required this.timeLeft, required this.color}); - @override - State createState() => _BlitzBackgroundEffectState(); + final int timeLeft; final Color color; final AppThemeType themeType; + const BlitzBackgroundEffect({super.key, required this.timeLeft, required this.color, required this.themeType}); + @override State createState() => _BlitzBackgroundEffectState(); } - class _BlitzBackgroundEffectState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; - @override - void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 400))..repeat(reverse: true); } - @override - void dispose() { _controller.dispose(); super.dispose(); } - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: _controller, - builder: (context, child) { - return Container( - color: widget.color.withOpacity(0.12 * _controller.value), - child: Center( - child: ImageFiltered( - imageFilter: ImageFilter.blur(sigmaX: 2.0, sigmaY: 2.0), - child: Text('${widget.timeLeft}', style: TextStyle(fontSize: 300, fontWeight: FontWeight.w900, color: widget.color.withOpacity(0.35 + (0.3 * _controller.value)), height: 1.0)), - ), - ), - ); - }, - ); - } + @override void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 400))..repeat(reverse: true); } + @override void dispose() { _controller.dispose(); super.dispose(); } + @override Widget build(BuildContext context) { return AnimatedBuilder(animation: _controller, builder: (context, child) { return Container(color: widget.color.withOpacity(0.12 * _controller.value), child: Center(child: ImageFiltered(imageFilter: ImageFilter.blur(sigmaX: 2.0, sigmaY: 2.0), child: Text('${widget.timeLeft}', style: _getTextStyle(widget.themeType, TextStyle(fontSize: 300, fontWeight: FontWeight.w900, color: widget.color.withOpacity(0.35 + (0.3 * _controller.value)), height: 1.0)))))); }); } } class SpecialEventBackgroundEffect extends StatefulWidget { - final String text; - final Color color; - const SpecialEventBackgroundEffect({super.key, required this.text, required this.color}); - @override - State createState() => _SpecialEventBackgroundEffectState(); + final String text; final Color color; final AppThemeType themeType; + const SpecialEventBackgroundEffect({super.key, required this.text, required this.color, required this.themeType}); + @override State createState() => _SpecialEventBackgroundEffectState(); } - class _SpecialEventBackgroundEffectState extends State with SingleTickerProviderStateMixin { - late AnimationController _controller; - late Animation _scaleAnimation; - late Animation _opacityAnimation; - - @override - void initState() { - super.initState(); - _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 1000))..forward(); - _scaleAnimation = Tween(begin: 0.5, end: 1.5).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic)); - _opacityAnimation = Tween(begin: 0.9, end: 0.0).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn)); - } - @override - void didUpdateWidget(covariant SpecialEventBackgroundEffect oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.text != widget.text) { _controller.reset(); _controller.forward(); } - } - @override - void dispose() { _controller.dispose(); super.dispose(); } - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: _controller, - builder: (context, child) { - return Center( - child: Transform.scale( - scale: _scaleAnimation.value, - child: Opacity( - opacity: _opacityAnimation.value, - child: ImageFiltered( - imageFilter: ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0), - child: Text(widget.text, style: TextStyle(fontSize: 250, fontWeight: FontWeight.w900, color: widget.color, height: 1.0)), - ), - ), - ), - ); - }, - ); - } + late AnimationController _controller; late Animation _scaleAnimation; late Animation _opacityAnimation; + @override void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 1000))..forward(); _scaleAnimation = Tween(begin: 0.5, end: 1.5).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic)); _opacityAnimation = Tween(begin: 0.9, end: 0.0).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn)); } + @override void didUpdateWidget(covariant SpecialEventBackgroundEffect oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.text != widget.text) { _controller.reset(); _controller.forward(); } } + @override void dispose() { _controller.dispose(); super.dispose(); } + @override Widget build(BuildContext context) { return AnimatedBuilder(animation: _controller, builder: (context, child) { return Center(child: Transform.scale(scale: _scaleAnimation.value, child: Opacity(opacity: _opacityAnimation.value, child: ImageFiltered(imageFilter: ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0), child: Text(widget.text, textAlign: TextAlign.center, style: _getTextStyle(widget.themeType, TextStyle(fontSize: 150, fontWeight: FontWeight.w900, color: widget.color, height: 1.0))))))); }); } } \ No newline at end of file diff --git a/lib/ui/game/score_board.dart b/lib/ui/game/score_board.dart index e1710b9..39fe3a2 100644 --- a/lib/ui/game/score_board.dart +++ b/lib/ui/game/score_board.dart @@ -1,5 +1,10 @@ +// =========================================================================== +// FILE: lib/ui/game/score_board.dart +// =========================================================================== + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +// Import separati e puliti import '../../logic/game_controller.dart'; import '../../models/game_board.dart'; import '../../core/theme_manager.dart'; @@ -27,7 +32,6 @@ class _ScoreBoardState extends State { bool isRedTurn = controller.board.currentPlayer == Player.red; bool isMuted = AudioService.instance.isMuted; - // --- LOGICA PER I NOMI --- String nameRed = "ROSSO"; String nameBlue = themeType == AppThemeType.cyberpunk ? "VERDE" : "BLU"; diff --git a/lib/ui/home/home_screen.dart b/lib/ui/home/home_screen.dart index 13476e8..d34bb92 100644 --- a/lib/ui/home/home_screen.dart +++ b/lib/ui/home/home_screen.dart @@ -6,30 +6,35 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/foundation.dart'; import 'dart:math' as math; import 'package:google_fonts/google_fonts.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../../logic/game_controller.dart'; import '../../core/theme_manager.dart'; import '../../core/app_colors.dart'; -import '../../models/game_board.dart'; import '../game/game_screen.dart'; import '../settings/settings_screen.dart'; -import '../../logic/game_controller.dart'; import '../../services/storage_service.dart'; import '../multiplayer/lobby_screen.dart'; import 'history_screen.dart'; -// --- HELPER PER IL FONT --- 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; } -// =========================================================================== -// IL NOSTRO "PITTORE" DI SCARABOCCHI -// Genera bordi irregolari, tratti doppi a penna e riempimenti sbavati. -// =========================================================================== class _DoodleBackgroundPainter extends CustomPainter { final Color fillColor; final Color strokeColor; @@ -101,8 +106,6 @@ class _DoodleBackgroundPainter extends CustomPainter { } } -// --- WIDGET PERSONALIZZATI --- - class _NeonShapeButton extends StatelessWidget { final IconData icon; final String label; @@ -163,7 +166,6 @@ class _NeonShapeButton extends StatelessWidget { ); } - // --- STILE STANDARD --- Color mainColor = isSpecial && !isLocked ? Colors.purpleAccent : theme.playerBlue; return GestureDetector( onTap: isLocked ? null : onTap, @@ -375,8 +377,6 @@ class _NeonTimeSwitch extends StatelessWidget { } } -// --------------------------------------------------------------------------- - class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @@ -386,6 +386,8 @@ class HomeScreen extends StatefulWidget { class _HomeScreenState extends State with WidgetsBindingObserver { + int _debugTapCount = 0; + @override void initState() { super.initState(); @@ -580,13 +582,11 @@ class _HomeScreenState extends State with WidgetsBindingObserver { ); } - // --- MENU SETUP PARTITA (Popup per CPU e Locale) --- void _showMatchSetupDialog(bool isVsCPU) { int localRadius = 4; ArenaShape localShape = ArenaShape.classic; bool localTimeMode = true; - int cpuLevel = StorageService.instance.cpuLevel; - bool isChaosUnlocked = cpuLevel >= 10; + bool isChaosUnlocked = StorageService.instance.playerLevel >= 10; showDialog( context: context, @@ -681,8 +681,8 @@ class _HomeScreenState extends State with WidgetsBindingObserver { 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 ? null : Border.all(color: Colors.white.withOpacity(0.15), width: 1.5), - boxShadow: themeType == AppThemeType.cyberpunk ? [] : [BoxShadow(color: Colors.black.withOpacity(0.5), blurRadius: 20, offset: const Offset(4, 10))], + border: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? null : Border.all(color: Colors.white.withOpacity(0.15), width: 1.5), + boxShadow: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? [] : [BoxShadow(color: Colors.black.withOpacity(0.5), blurRadius: 20, offset: const Offset(4, 10))], ), child: SingleChildScrollView( physics: const BouncingScrollPhysics(), @@ -762,7 +762,203 @@ class _HomeScreenState extends State with WidgetsBindingObserver { ); } - // --- MENU TUTORIAL (TESTO AGGIORNATO PER TETRAQ) --- + // --- NUOVA FUNZIONE: MOSTRA LE SFIDE GIORNALIERE --- + Future _showDailyQuestsDialog() async { + final prefs = await SharedPreferences.getInstance(); + + 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 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("SFIDE GIORNALIERE", style: _getTextStyle(themeType, TextStyle(fontSize: 22, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 1.5))), + const SizedBox(height: 25), + + // Generiamo dinamicamente le 3 missioni salvate in memoria + ...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: _getTextStyle(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: _getTextStyle(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(ctx), + child: const Text("CHIUDI", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2)), + ), + ) + ], + ), + ), + ); + } + ); + } + + // --- NUOVA FUNZIONE: MOSTRA LA CLASSIFICA GLOBALE --- + void _showLeaderboardDialog() { + showDialog( + context: context, + barrierColor: Colors.black.withOpacity(0.8), + builder: (ctx) { + final themeManager = ctx.watch(); + final theme = themeManager.currentColors; + final themeType = themeManager.currentThemeType; + + 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: [ + Icon(Icons.emoji_events, size: 50, color: Colors.amber), + const SizedBox(height: 10), + Text("CLASSIFICA MONDIALE", style: _getTextStyle(themeType, TextStyle(fontSize: 20, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 1.5))), + const SizedBox(height: 20), + + // Lista giocatori pescata da Firebase! + SizedBox( + height: 350, + child: StreamBuilder( + 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; + bool isMe = data['name'] == StorageService.instance.playerName; + + 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: _getTextStyle(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: _getTextStyle(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(ctx), + child: const Text("CHIUDI", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2)), + ), + ) + ], + ), + ); + + if (themeType == AppThemeType.cyberpunk) content = _AnimatedCyberBorder(child: content); + return Dialog(backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.all(20), child: content); + } + ); + } + void _showTutorialDialog() { showDialog( context: context, @@ -773,34 +969,57 @@ class _HomeScreenState extends State with WidgetsBindingObserver { 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.shade100, strokeColor: inkColor, seed: 400), + 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: [ - Text("COME GIOCARE", style: _getTextStyle(themeType, TextStyle(fontSize: 28, fontWeight: FontWeight.w900, color: inkColor, letterSpacing: 2))), + Center(child: Text("COME GIOCARE", style: _getTextStyle(themeType, TextStyle(fontSize: 28, fontWeight: FontWeight.w900, color: inkColor, letterSpacing: 2)))), const SizedBox(height: 20), - // TESTO MODIFICATO PER EVIDENZIARE IL POSIZIONAMENTO LIBERO - _TutorialStep(icon: Icons.line_axis, text: "Lo scopo del gioco è chiudere i 4 lati di un quadrato per conquistare un punto.", themeType: themeType, inkColor: inkColor, theme: theme), + _TutorialStep(icon: Icons.line_axis, text: "Chiudi i 4 lati di un quadrato e conquisti 1 punto e avere una mossa extra!", themeType: themeType, inkColor: inkColor, theme: theme), const SizedBox(height: 15), - // TESTO MODIFICATO PER L'EFFETTO TOROIDALE - _TutorialStep(icon: Icons.all_out, text: "durante il gioco troverai dei bonus e dei malus impara a trarne profitto", themeType: themeType, inkColor: inkColor, theme: theme), + _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), - // TESTO MODIFICATO PER LA VITTORIA E LE FORME + const Divider(color: Colors.black26, thickness: 2), + const SizedBox(height: 10), + Center(child: Text("GLOSSARIO ARENA", style: _getTextStyle(themeType, TextStyle(fontSize: 18, fontWeight: FontWeight.w900, color: inkColor)))), + const SizedBox(height: 10), - GestureDetector( - onTap: () => Navigator.pop(ctx), - 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: _getTextStyle(themeType, TextStyle(fontSize: 18, fontWeight: FontWeight.w900, color: inkColor))), + _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(ctx), + 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: _getTextStyle(themeType, TextStyle(fontSize: 18, fontWeight: FontWeight.w900, color: inkColor))), + ), ), ), ) @@ -814,29 +1033,51 @@ class _HomeScreenState extends State with WidgetsBindingObserver { 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 ? null : Border.all(color: Colors.white.withOpacity(0.15), width: 1.5), - boxShadow: themeType == AppThemeType.cyberpunk ? [] : [BoxShadow(color: Colors.black.withOpacity(0.5), blurRadius: 20, offset: const Offset(4, 10))], + border: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? null : Border.all(color: Colors.white.withOpacity(0.15), width: 1.5), + boxShadow: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? [] : [BoxShadow(color: Colors.black.withOpacity(0.5), blurRadius: 20, offset: const Offset(4, 10))], ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text("COME GIOCARE", style: _getTextStyle(themeType, TextStyle(fontSize: 24, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 2))), - const SizedBox(height: 20), - // TESTO MODIFICATO - _TutorialStep(icon: Icons.line_axis, text: "Lo scopo del gioco è chiudere i 4 lati di un quadrato per conquistare un punto.", themeType: themeType, inkColor: inkColor, theme: theme), - const SizedBox(height: 15), - // TESTO MODIFICATO PER L'EFFETTO TOROIDALE - _TutorialStep(icon: Icons.all_out, text: "durante il gioco troverai dei bonus e dei malus impara a trarne profitto", themeType: themeType, inkColor: inkColor, theme: theme), - 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(ctx), - child: const Text("CHIUDI", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2)), - ), - ) - ], + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center(child: Text("COME GIOCARE", style: _getTextStyle(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 e conquisti 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: _getTextStyle(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(ctx), + child: const Text("CHIUDI", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2)), + ), + ) + ], + ), ), ); @@ -873,148 +1114,192 @@ class _HomeScreenState extends State with WidgetsBindingObserver { String playerName = StorageService.instance.playerName; if (playerName.isEmpty) playerName = "GUEST"; + int level = StorageService.instance.playerLevel; + int currentXP = StorageService.instance.totalXP; + double xpProgress = (currentXP % 100) / 100.0; + Widget uiContent = SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 20.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // --- HEADER --- - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - onTap: _showNameDialog, - child: Row( + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - themeType == AppThemeType.doodle - ? CustomPaint( - painter: _DoodleBackgroundPainter(fillColor: Colors.white.withOpacity(0.8), strokeColor: inkColor, seed: 1, isCircle: true), - child: SizedBox(width: 50, height: 50, child: Icon(Icons.person, color: inkColor, size: 30)), - ) - : Container( - decoration: BoxDecoration(shape: BoxShape.circle, boxShadow: [BoxShadow(color: theme.playerBlue.withOpacity(0.3), blurRadius: 10, offset: const Offset(0, 4))]), - child: CircleAvatar(backgroundColor: theme.playerBlue.withOpacity(0.2), radius: 25, child: Icon(Icons.person, color: theme.playerBlue, size: 30)), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + onTap: _showNameDialog, + child: Row( + children: [ + themeType == AppThemeType.doodle + ? CustomPaint( + painter: _DoodleBackgroundPainter(fillColor: Colors.white.withOpacity(0.8), strokeColor: inkColor, seed: 1, isCircle: true), + child: SizedBox(width: 50, height: 50, child: Icon(Icons.person, color: inkColor, size: 30)), + ) + : SizedBox( + width: 50, height: 50, + child: Stack( + fit: StackFit.expand, + children: [ + CircularProgressIndicator(value: xpProgress, color: theme.playerBlue, strokeWidth: 3, backgroundColor: theme.gridLine.withOpacity(0.2)), + Padding( + padding: const EdgeInsets.all(4.0), + child: Container( + decoration: BoxDecoration(shape: BoxShape.circle, boxShadow: [BoxShadow(color: theme.playerBlue.withOpacity(0.3), blurRadius: 10, offset: const Offset(0, 4))]), + child: CircleAvatar(backgroundColor: theme.playerBlue.withOpacity(0.2), child: Icon(Icons.person, color: theme.playerBlue, size: 26)), + ), + ), + ], + ), + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(playerName, style: _getTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor : theme.text, fontSize: 24, fontWeight: FontWeight.w900, letterSpacing: 1.5, shadows: themeType == AppThemeType.doodle ? [] : [Shadow(color: Colors.black.withOpacity(0.5), offset: const Offset(1, 2), blurRadius: 2)]))), + Text("LIV. $level", style: _getTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.8) : theme.playerBlue, fontSize: 14, fontWeight: FontWeight.bold, letterSpacing: 1))), + ], + ), + ], + ), + ), + + GestureDetector( + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const HistoryScreen())), + child: themeType == AppThemeType.doodle + ? Transform.rotate( + angle: 0.04, + child: CustomPaint( + painter: _DoodleBackgroundPainter(fillColor: Colors.yellow.shade100, strokeColor: inkColor, seed: 2), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Icon(Icons.emoji_events, color: inkColor, size: 20), const SizedBox(width: 6), + Text("$wins", style: _getTextStyle(themeType, TextStyle(color: inkColor, fontWeight: FontWeight.w900))), const SizedBox(width: 12), + Icon(Icons.sentiment_very_dissatisfied, color: inkColor, size: 20), const SizedBox(width: 6), + Text("$losses", style: _getTextStyle(themeType, TextStyle(color: inkColor, fontWeight: FontWeight.w900))), + ], + ), + ), + ), + ) + : Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + decoration: BoxDecoration( + gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [theme.text.withOpacity(0.15), theme.text.withOpacity(0.02)]), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.white.withOpacity(0.1), width: 1.5), + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.3), offset: const Offset(2, 4), blurRadius: 8), BoxShadow(color: Colors.white.withOpacity(0.05), offset: const Offset(-1, -1), blurRadius: 2)], + ), + child: Row( + children: [ + Icon(Icons.emoji_events, color: Colors.amber.shade600, size: 20), const SizedBox(width: 6), + Text("$wins", style: _getTextStyle(themeType, const TextStyle(color: Colors.white, fontWeight: FontWeight.w900))), const SizedBox(width: 12), + Icon(Icons.sentiment_very_dissatisfied, color: theme.playerRed.withOpacity(0.8), size: 20), const SizedBox(width: 6), + Text("$losses", style: _getTextStyle(themeType, const TextStyle(color: Colors.white, fontWeight: FontWeight.w900))), + ], + ), + ), + ) + ], ), - const SizedBox(width: 12), - Text(playerName, style: _getTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor : theme.text, fontSize: 26, fontWeight: FontWeight.w900, letterSpacing: 1.5, shadows: themeType == AppThemeType.doodle ? [] : [Shadow(color: Colors.black.withOpacity(0.5), offset: const Offset(1, 2), blurRadius: 2)]))), + + const Spacer(), + + Center( + child: Transform.rotate( + angle: themeType == AppThemeType.doodle ? -0.04 : 0, + // --- IL TRUCCO DELLO SVILUPPATORE PROTETTO --- + child: GestureDetector( + onTap: () { + if (kReleaseMode) return; + _debugTapCount++; + if (_debugTapCount >= 5) { + _debugTapCount = 0; + StorageService.instance.addXP(2000); + setState(() {}); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("🛠 DEBUG MODE: +20 Livelli! Tutto sbloccato.", style: _getTextStyle(themeType, const TextStyle(color: Colors.white, fontWeight: FontWeight.bold))), + backgroundColor: Colors.purpleAccent, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), + ) + ); + } + }, + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + "TETRAQ", + style: _getTextStyle(themeType, TextStyle( + fontSize: 65, + fontWeight: FontWeight.w900, + color: themeType == AppThemeType.doodle ? inkColor : theme.text, + letterSpacing: 10, + shadows: themeType == AppThemeType.doodle || themeType == AppThemeType.arcade ? [] : [ + BoxShadow(color: Colors.black.withOpacity(0.6), offset: const Offset(3, 6), blurRadius: 8), + BoxShadow(color: theme.playerBlue.withOpacity(0.4), offset: const Offset(0, 0), blurRadius: 20), + ] + )) + ), + ), + ), + ), + ), + + const Spacer(), + + // --- NUOVA LISTA BOTTONI CON CLASSIFICHE E SFIDE --- + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildCyberCard(_FeatureCard(title: "ONLINE", subtitle: "Sfida il mondo", icon: Icons.public, color: Colors.lightBlue.shade200, theme: theme, themeType: themeType, isFeatured: true, onTap: () { Navigator.push(context, MaterialPageRoute(builder: (_) => const LobbyScreen())); }), themeType), + const SizedBox(height: 12), + _buildCyberCard(_FeatureCard(title: "VS CPU", subtitle: "Allenati con l'IA", icon: Icons.smart_toy, color: Colors.purple.shade200, theme: theme, themeType: themeType, onTap: () => _showMatchSetupDialog(true)), themeType), + const SizedBox(height: 12), + _buildCyberCard(_FeatureCard(title: "LOCALE", subtitle: "Stesso schermo", icon: Icons.people_alt, color: Colors.red.shade200, theme: theme, themeType: themeType, onTap: () => _showMatchSetupDialog(false)), themeType), + const SizedBox(height: 12), + + // NUOVI BOTTONI PER LA VERSIONE 2.0 + Row( + children: [ + Expanded(child: _buildCyberCard(_FeatureCard(title: "CLASSIFICA", subtitle: "Top 50 Globale", icon: Icons.leaderboard, color: Colors.amber.shade200, theme: theme, themeType: themeType, onTap: _showLeaderboardDialog, compact: true), themeType)), + const SizedBox(width: 12), + Expanded(child: _buildCyberCard(_FeatureCard(title: "SFIDE", subtitle: "Missioni", icon: Icons.assignment_turned_in, color: Colors.green.shade200, theme: theme, themeType: themeType, onTap: _showDailyQuestsDialog, compact: true), themeType)), + ], + ), + + const SizedBox(height: 12), + + Row( + children: [ + Expanded(child: _buildCyberCard(_FeatureCard(title: "TEMI", subtitle: "Personalizza", icon: Icons.palette, color: Colors.teal.shade200, theme: theme, themeType: themeType, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const SettingsScreen())), compact: true), themeType)), + const SizedBox(width: 12), + Expanded(child: _buildCyberCard(_FeatureCard(title: "TUTORIAL", subtitle: "Come giocare", icon: Icons.school, color: Colors.indigo.shade200, theme: theme, themeType: themeType, onTap: _showTutorialDialog, compact: true), themeType)), + ], + ), + ], + ), + const SizedBox(height: 10), ], ), ), - - GestureDetector( - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const HistoryScreen())), - child: themeType == AppThemeType.doodle - ? Transform.rotate( - angle: 0.04, - child: CustomPaint( - painter: _DoodleBackgroundPainter(fillColor: Colors.yellow.shade100, strokeColor: inkColor, seed: 2), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Row( - children: [ - Icon(Icons.emoji_events, color: inkColor, size: 20), const SizedBox(width: 6), - Text("$wins", style: _getTextStyle(themeType, TextStyle(color: inkColor, fontWeight: FontWeight.w900))), const SizedBox(width: 12), - Icon(Icons.sentiment_very_dissatisfied, color: inkColor, size: 20), const SizedBox(width: 6), - Text("$losses", style: _getTextStyle(themeType, TextStyle(color: inkColor, fontWeight: FontWeight.w900))), - ], - ), - ), - ), - ) - : Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), - decoration: BoxDecoration( - gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [theme.text.withOpacity(0.15), theme.text.withOpacity(0.02)]), - borderRadius: BorderRadius.circular(20), - border: Border.all(color: Colors.white.withOpacity(0.1), width: 1.5), - boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.3), offset: const Offset(2, 4), blurRadius: 8), BoxShadow(color: Colors.white.withOpacity(0.05), offset: const Offset(-1, -1), blurRadius: 2)], - ), - child: Row( - children: [ - Icon(Icons.emoji_events, color: Colors.amber.shade600, size: 20), const SizedBox(width: 6), - Text("$wins", style: _getTextStyle(themeType, const TextStyle(color: Colors.white, fontWeight: FontWeight.w900))), const SizedBox(width: 12), - Icon(Icons.sentiment_very_dissatisfied, color: theme.playerRed.withOpacity(0.8), size: 20), const SizedBox(width: 6), - Text("$losses", style: _getTextStyle(themeType, const TextStyle(color: Colors.white, fontWeight: FontWeight.w900))), - ], - ), - ), - ) - ], - ), - - const Spacer(), - - Center( - child: Transform.rotate( - angle: themeType == AppThemeType.doodle ? -0.04 : 0, - child: Text( - "TETRAQ", - style: _getTextStyle(themeType, TextStyle( - fontSize: 65, - fontWeight: FontWeight.w900, - color: themeType == AppThemeType.doodle ? inkColor : theme.text, - letterSpacing: 10, - shadows: themeType == AppThemeType.doodle ? [] : [ - BoxShadow(color: Colors.black.withOpacity(0.6), offset: const Offset(3, 6), blurRadius: 8), - BoxShadow(color: theme.playerBlue.withOpacity(0.4), offset: const Offset(0, 0), blurRadius: 20), - ] - )) - ), ), ), - - const Spacer(), - - // --- MENU PRINCIPALE CON COLORI PASTELLO --- - Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _buildCyberCard( - _FeatureCard( - title: "ONLINE", subtitle: "Sfida il mondo", icon: Icons.public, color: Colors.lightBlue.shade200, theme: theme, themeType: themeType, isFeatured: true, - onTap: () { Navigator.push(context, MaterialPageRoute(builder: (_) => const LobbyScreen())); }, - ), - themeType - ), - const SizedBox(height: 14), - _buildCyberCard( - _FeatureCard( - title: "VS CPU", subtitle: "Allenati con l'IA", icon: Icons.smart_toy, color: Colors.purple.shade200, theme: theme, themeType: themeType, - onTap: () => _showMatchSetupDialog(true), - ), - themeType - ), - const SizedBox(height: 14), - _buildCyberCard( - _FeatureCard( - title: "LOCALE", subtitle: "Stesso schermo", icon: Icons.people_alt, color: Colors.red.shade200, theme: theme, themeType: themeType, - onTap: () => _showMatchSetupDialog(false), - ), - themeType - ), - const SizedBox(height: 14), - // --- NUOVO BOTTONE TUTORIAL --- - _buildCyberCard( - _FeatureCard( - title: "TUTORIAL", subtitle: "Come giocare", icon: Icons.school, color: Colors.indigo.shade200, theme: theme, themeType: themeType, - onTap: _showTutorialDialog, - ), - themeType - ), - const SizedBox(height: 14), - _buildCyberCard( - _FeatureCard( - title: "TEMI", subtitle: "Personalizza", icon: Icons.palette, color: Colors.teal.shade200, theme: theme, themeType: themeType, - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const SettingsScreen())), - ), - themeType - ), - ], - ), - const SizedBox(height: 10), - ], - ), + ); + }, ), ); @@ -1047,32 +1332,31 @@ class _HomeScreenState extends State with WidgetsBindingObserver { } } -// --- HELPER PER IL TESTO DEL TUTORIAL --- class _TutorialStep extends StatelessWidget { final IconData icon; + final Color? iconColor; final String text; final AppThemeType themeType; final Color inkColor; final ThemeColors theme; - const _TutorialStep({required this.icon, required this.text, required this.themeType, required this.inkColor, required this.theme}); + const _TutorialStep({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: themeType == AppThemeType.doodle ? inkColor : theme.playerBlue, size: 28), + Icon(icon, color: iconColor ?? (themeType == AppThemeType.doodle ? inkColor : theme.playerBlue), size: 28), const SizedBox(width: 15), Expanded( - child: Text(text, style: _getTextStyle(themeType, TextStyle(fontSize: 16, color: themeType == AppThemeType.doodle ? inkColor : theme.text.withOpacity(0.8), height: 1.3))), + child: Text(text, style: _getTextStyle(themeType, TextStyle(fontSize: 14, color: themeType == AppThemeType.doodle ? inkColor : theme.text.withOpacity(0.8), height: 1.3))), ), ], ); } } -// --- FEATURE CARD --- class _FeatureCard extends StatelessWidget { final String title; final String subtitle; @@ -1082,8 +1366,9 @@ class _FeatureCard extends StatelessWidget { final AppThemeType themeType; final VoidCallback onTap; final bool isFeatured; + final bool compact; - const _FeatureCard({required this.title, required this.subtitle, required this.icon, required this.color, required this.theme, required this.themeType, required this.onTap, this.isFeatured = false}); + const _FeatureCard({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) { @@ -1102,24 +1387,26 @@ class _FeatureCard extends StatelessWidget { seed: title.length * 5, ), child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 22.0, vertical: 16.0), + 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: 32), - const SizedBox(width: 20), + Icon(icon, color: inkColor, size: compact ? 24 : 32), + const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ - Text(title, style: _getTextStyle(themeType, TextStyle(color: inkColor, fontSize: 24, fontWeight: FontWeight.w900))), - const SizedBox(height: 2), - Text(subtitle, style: _getTextStyle(themeType, TextStyle(color: inkColor.withOpacity(0.8), fontSize: 14, fontWeight: FontWeight.bold))), + Text(title, style: _getTextStyle(themeType, TextStyle(color: inkColor, fontSize: compact ? 16 : 24, fontWeight: FontWeight.w900))), + if (!compact) ...[ + const SizedBox(height: 2), + Text(subtitle, style: _getTextStyle(themeType, TextStyle(color: inkColor.withOpacity(0.8), fontSize: 14, fontWeight: FontWeight.bold))), + ] ], ), ), - Icon(Icons.chevron_right_rounded, color: inkColor.withOpacity(0.6), size: 32), + if (!compact) Icon(Icons.chevron_right_rounded, color: inkColor.withOpacity(0.6), size: 32), ], ), ), @@ -1128,31 +1415,30 @@ class _FeatureCard extends StatelessWidget { ); } - // --- STILE STANDARD --- return GestureDetector( onTap: onTap, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 14.0), + 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)] - : [theme.background.withOpacity(0.9), theme.background.withOpacity(0.5)], + : [color.withOpacity(0.25), color.withOpacity(0.05)], ), - borderRadius: BorderRadius.circular(20), - border: Border.all(color: Colors.white.withOpacity(isFeatured ? 0.3 : 0.1), width: 1.5), + 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: Colors.white.withOpacity(isFeatured ? 0.2 : 0.05), offset: const Offset(-1, -1), blurRadius: 5), + BoxShadow(color: color.withOpacity(isFeatured ? 0.3 : 0.05), offset: const Offset(-1, -1), blurRadius: 5), ] ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( - padding: const EdgeInsets.all(10), + padding: EdgeInsets.all(compact ? 6 : 10), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, @@ -1163,23 +1449,25 @@ class _FeatureCard extends StatelessWidget { 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: 26), + child: Icon(icon, color: isFeatured ? Colors.white : color, size: compact ? 20 : 26), ), - const SizedBox(width: 20), + SizedBox(width: compact ? 10 : 20), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ - Text(title, style: TextStyle(color: isFeatured ? Colors.white : theme.text, fontSize: 18, fontWeight: FontWeight.w900, shadows: [Shadow(color: Colors.black.withOpacity(0.5), offset: const Offset(1, 2), blurRadius: 2)])), - const SizedBox(height: 2), - Text(subtitle, style: TextStyle(color: isFeatured ? Colors.white.withOpacity(0.8) : theme.text.withOpacity(0.5), fontSize: 12, fontWeight: FontWeight.bold)), + Text(title, style: _getTextStyle(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: _getTextStyle(themeType, TextStyle(color: isFeatured ? Colors.white.withOpacity(0.8) : theme.text.withOpacity(0.6), fontSize: 12, fontWeight: FontWeight.bold))), + ] ], ), ), - Icon(Icons.chevron_right_rounded, color: isFeatured ? Colors.white.withOpacity(0.7) : theme.text.withOpacity(0.3), size: 30), + if (!compact) Icon(Icons.chevron_right_rounded, color: isFeatured ? Colors.white.withOpacity(0.7) : color.withOpacity(0.5), size: 30), ], ), ), @@ -1209,7 +1497,7 @@ class _AnimatedCyberBorderState extends State<_AnimatedCyberBorder> with SingleT 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(20), boxShadow: [BoxShadow(color: theme.playerBlue.withOpacity(0.3), blurRadius: 25, spreadRadius: 2)]), + 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, ), @@ -1230,7 +1518,7 @@ class _CyberBorderPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final rect = Offset.zero & size; - final RRect rrect = RRect.fromRectAndRadius(rect, const Radius.circular(20)); + 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 diff --git a/lib/ui/multiplayer/lobby_screen.dart b/lib/ui/multiplayer/lobby_screen.dart index c7d5fc5..b60b35f 100644 --- a/lib/ui/multiplayer/lobby_screen.dart +++ b/lib/ui/multiplayer/lobby_screen.dart @@ -1,31 +1,31 @@ -// =========================================================================== -// 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 'dart:math' as math; +import '../../logic/game_controller.dart'; +import '../../models/game_board.dart'; import '../../core/theme_manager.dart'; import '../../core/app_colors.dart'; -import '../../models/game_board.dart'; import '../../services/multiplayer_service.dart'; import '../../services/storage_service.dart'; import '../game/game_screen.dart'; -import '../../logic/game_controller.dart'; import 'package:google_fonts/google_fonts.dart'; -// --- HELPER PER IL FONT --- 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; } -// --- WIDGET 3D/NEON RIUTILIZZABILI COMPATTATI --- - class _NeonShapeButton extends StatelessWidget { final IconData icon; final String label; @@ -414,8 +414,6 @@ class _CyberBorderPainter extends CustomPainter { bool shouldRepaint(covariant _CyberBorderPainter oldDelegate) => oldDelegate.animationValue != animationValue; } -// --------------------------------------------------------------------------- - class LobbyScreen extends StatefulWidget { final String? initialRoomCode; @@ -606,18 +604,15 @@ class _LobbyScreenState extends State { if (themeType == AppThemeType.doodle) bgImage = 'assets/images/doodle_bg.jpg'; if (themeType == AppThemeType.cyberpunk) bgImage = 'assets/images/cyber_bg.jpg'; - int cpuLevel = StorageService.instance.cpuLevel; - bool isChaosUnlocked = cpuLevel >= 10; + bool isChaosUnlocked = true; Color doodlePenColor = const Color(0xFF00008B); - // --- PANNELLO HOST --- Widget hostPanel = Transform.rotate( angle: themeType == AppThemeType.doodle ? 0.01 : 0, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 15), decoration: BoxDecoration( - // Sfondo ancora più scuro (0.85) per il tema Cyberpunk color: themeType == AppThemeType.cyberpunk ? Colors.black.withOpacity(0.85) : (themeType == AppThemeType.doodle ? Colors.white.withOpacity(0.5) : Colors.transparent), borderRadius: BorderRadius.only( topLeft: Radius.circular(themeType == AppThemeType.doodle ? 5 : 20), @@ -630,7 +625,6 @@ class _LobbyScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Rinomato da IMPOSTAZIONI STANZA a IMPOSTAZIONI GRIGLIA 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)))), const SizedBox(height: 10), @@ -686,7 +680,6 @@ class _LobbyScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // --- INTESTAZIONE COMPATTATA --- Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, @@ -712,8 +705,6 @@ class _LobbyScreenState extends State { ), const SizedBox(height: 20), - - // --- SEZIONE HOST --- hostPanel, const SizedBox(height: 15), _NeonActionButton(label: "CREA PARTITA", color: theme.playerRed, onTap: _createRoom, theme: theme, themeType: themeType), @@ -728,7 +719,6 @@ class _LobbyScreenState extends State { ), const SizedBox(height: 20), - // --- SEZIONE JOIN --- Transform.rotate( angle: themeType == AppThemeType.doodle ? 0.02 : 0, child: Container( @@ -747,7 +737,6 @@ class _LobbyScreenState extends State { 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, - // Scurito anche il campo del codice per Cyberpunk (0.85 di opacità) 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)), @@ -770,7 +759,6 @@ class _LobbyScreenState extends State { appBar: AppBar(backgroundColor: Colors.transparent, elevation: 0, iconTheme: IconThemeData(color: theme.text)), body: Stack( children: [ - // 1. Sfondo Container( decoration: bgImage != null ? BoxDecoration(image: DecorationImage(image: AssetImage(bgImage), fit: BoxFit.cover)) : null, child: bgImage != null && themeType == AppThemeType.cyberpunk @@ -780,7 +768,6 @@ class _LobbyScreenState extends State { : null, ), - // 2. Wi-Fi if (themeType == AppThemeType.doodle) Positioned( top: 150, left: -20, right: -20, @@ -801,7 +788,6 @@ class _LobbyScreenState extends State { ), ), - // 3. UI _isLoading ? Center(child: CircularProgressIndicator(color: theme.playerRed)) : uiContent, ], ), diff --git a/lib/ui/settings/settings_screen.dart b/lib/ui/settings/settings_screen.dart index c7f8249..03a37d2 100644 --- a/lib/ui/settings/settings_screen.dart +++ b/lib/ui/settings/settings_screen.dart @@ -6,15 +6,23 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../core/theme_manager.dart'; import '../../core/app_colors.dart'; +import '../../services/storage_service.dart'; -class SettingsScreen extends StatelessWidget { +class SettingsScreen extends StatefulWidget { const SettingsScreen({super.key}); + @override + State createState() => _SettingsScreenState(); +} + +class _SettingsScreenState extends State { @override Widget build(BuildContext context) { final themeManager = context.watch(); final theme = themeManager.currentColors; + int playerLevel = StorageService.instance.playerLevel; + return Scaffold( backgroundColor: theme.background, appBar: AppBar( @@ -31,20 +39,8 @@ class SettingsScreen extends StatelessWidget { subtitle: "Linee pulite, sfondo chiaro", type: AppThemeType.minimal, previewColors: AppColors.minimal, - ), - const SizedBox(height: 15), - _ThemeCard( - title: "Quaderno (Doodle)", - subtitle: "Sfondo a quadretti, tratto a penna", - type: AppThemeType.doodle, - previewColors: AppColors.doodle, - ), - const SizedBox(height: 15), - _ThemeCard( - title: "Cyberpunk", - subtitle: "Nero profondo, luci al neon", - type: AppThemeType.cyberpunk, - previewColors: AppColors.cyberpunk, + requiredLevel: 1, + currentLevel: playerLevel, ), const SizedBox(height: 15), _ThemeCard( @@ -52,6 +48,44 @@ class SettingsScreen extends StatelessWidget { subtitle: "Tavolo di legno, linee come fiammiferi", type: AppThemeType.wood, previewColors: AppColors.wood, + requiredLevel: 3, + 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", + type: AppThemeType.cyberpunk, + previewColors: AppColors.cyberpunk, + requiredLevel: 7, + currentLevel: playerLevel, + ), + const SizedBox(height: 15), + _ThemeCard( + title: "8-Bit Arcade", + subtitle: "Sale giochi, fosfori verdi e pixel", + type: AppThemeType.arcade, + previewColors: AppColors.arcade, + requiredLevel: 10, + currentLevel: playerLevel, + ), + const SizedBox(height: 15), + _ThemeCard( + title: "Grimorio", + subtitle: "Incantesimi antichi, rune magiche", + type: AppThemeType.grimorio, + previewColors: AppColors.grimorio, + requiredLevel: 15, + currentLevel: playerLevel, ), ], ), @@ -64,47 +98,99 @@ class _ThemeCard extends StatelessWidget { final String subtitle; final AppThemeType type; final ThemeColors previewColors; + final int requiredLevel; + final int currentLevel; - const _ThemeCard({required this.title, required this.subtitle, required this.type, required this.previewColors}); + const _ThemeCard({ + required this.title, + required this.subtitle, + required this.type, + required this.previewColors, + required this.requiredLevel, + required this.currentLevel, + }); @override Widget build(BuildContext context) { final themeManager = context.watch(); bool isSelected = themeManager.currentThemeType == type; + bool isLocked = currentLevel < requiredLevel; return GestureDetector( onTap: () { + if (isLocked) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Gioca per raggiungere il Liv. $requiredLevel e sbloccare questo tema!", style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.white)), + backgroundColor: Colors.redAccent, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + duration: const Duration(seconds: 2), + ) + ); + return; + } + themeManager.setTheme(type); - // --- LA MODIFICA È QUI --- - // Chiude la schermata e torna automaticamente alla Home! Navigator.pop(context); }, child: AnimatedContainer( duration: const Duration(milliseconds: 300), padding: const EdgeInsets.all(20), decoration: BoxDecoration( - color: previewColors.background, + color: isLocked ? previewColors.background.withOpacity(0.4) : previewColors.background, borderRadius: BorderRadius.circular(20), border: Border.all( - color: isSelected ? previewColors.playerBlue : previewColors.gridLine.withOpacity(0.5), + color: isSelected + ? previewColors.playerBlue + : (isLocked ? Colors.grey.withOpacity(0.3) : previewColors.gridLine.withOpacity(0.5)), width: isSelected ? 4 : 2, ), boxShadow: isSelected ? [BoxShadow(color: previewColors.playerBlue.withOpacity(0.4), blurRadius: 10, spreadRadius: 2)] : [], ), - child: Row( + child: Stack( + alignment: Alignment.center, children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + Opacity( + opacity: isLocked ? 0.25 : 1.0, + child: Row( children: [ - Text(title, style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: previewColors.text)), - Text(subtitle, style: TextStyle(fontSize: 14, color: previewColors.text.withOpacity(0.7))), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: previewColors.text)), + Text(subtitle, style: TextStyle(fontSize: 14, color: previewColors.text.withOpacity(0.7))), + ], + ), + ), + Container(width: 20, height: 20, decoration: BoxDecoration(color: previewColors.playerRed, shape: BoxShape.circle)), + 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.playerRed, shape: BoxShape.circle)), - const SizedBox(width: 10), - Container(width: 20, height: 20, decoration: BoxDecoration(color: previewColors.playerBlue, shape: BoxShape.circle)), + if (isLocked) + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.85), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.white.withOpacity(0.2), width: 1.5), + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.5), blurRadius: 10, offset: const Offset(0, 4))], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.lock_rounded, color: Colors.white, size: 20), + const SizedBox(width: 8), + Text( + "LIV. $requiredLevel", + style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w900, fontSize: 16, letterSpacing: 2) + ), + ], + ), + ), ], ), ), diff --git a/macos/.DS_Store b/macos/.DS_Store index 07d2e1e..80dd696 100644 Binary files a/macos/.DS_Store and b/macos/.DS_Store differ diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 5cc1da2..04f7d3b 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -598,7 +598,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = 2BX6QRR7GG; ENABLE_APP_SANDBOX = YES; ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; @@ -618,7 +618,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.0.0; + MARKETING_VERSION = 1.0.2; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; @@ -748,7 +748,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = 2BX6QRR7GG; ENABLE_APP_SANDBOX = YES; ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; @@ -768,7 +768,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.0.0; + MARKETING_VERSION = 1.0.2; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -786,7 +786,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = 2BX6QRR7GG; ENABLE_APP_SANDBOX = YES; ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; @@ -806,7 +806,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.0.0; + MARKETING_VERSION = 1.0.2; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; diff --git a/pubspec.lock b/pubspec.lock index 0a29191..dee2a86 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -317,6 +317,14 @@ packages: description: flutter source: sdk version: "0.0.0" + font_awesome_flutter: + dependency: "direct main" + description: + name: font_awesome_flutter + sha256: b9011df3a1fa02993630b8fb83526368cf2206a711259830325bab2f1d2a4eb0 + url: "https://pub.dev" + source: hosted + version: "10.12.0" glob: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2549489..542892e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: tetraq description: A new Flutter project. publish_to: 'none' -version: 1.0.0+4 +version: 1.0.2+4 environment: sdk: ^3.10.7 @@ -22,6 +22,7 @@ dependencies: share_plus: ^12.0.1 app_links: ^7.0.0 google_fonts: ^8.0.2 + font_awesome_flutter: ^10.12.0 dev_dependencies: flutter_test: