Auto-sync: 20260320_220000

This commit is contained in:
Paolo 2026-03-20 22:00:01 +01:00
parent d68068117a
commit 931cfa0d5a
7 changed files with 371 additions and 100 deletions

View file

@ -37,9 +37,11 @@ class GameController extends ChangeNotifier {
bool _hasSavedResult = false; bool _hasSavedResult = false;
Timer? _blitzTimer; Timer? _blitzTimer;
int timeLeft = 15; int timeLeft = 10;
final int maxTime = 15; int maxTime = 10;
bool isTimeMode = true; String timeModeSetting = 'fixed'; // 'fixed', 'relax', 'dynamic'
bool get isTimeMode => timeModeSetting != 'relax';
int consecutiveRematches = 0; // Contatore per la modalità Dinamica
String effectText = ''; String effectText = '';
Color effectColor = Colors.transparent; Color effectColor = Colors.transparent;
@ -56,8 +58,6 @@ class GameController extends ChangeNotifier {
bool opponentWantsRematch = false; bool opponentWantsRematch = false;
int lastMatchXP = 0; int lastMatchXP = 0;
// --- NUOVA ROADMAP DINAMICA DEGLI SBLOCCHI ---
// Aggiungi qui le tue future meccaniche! Il popup le leggerà in automatico.
static const Map<int, List<Map<String, dynamic>>> rewardsRoadmap = { static const Map<int, List<Map<String, dynamic>>> rewardsRoadmap = {
2: [{'title': 'Bomba & Oro', 'desc': 'Appaiono le caselle speciali: Oro (+2) e Bomba (-1)!', 'icon': Icons.stars, 'color': Colors.amber}], 2: [{'title': 'Bomba & Oro', 'desc': 'Appaiono le caselle speciali: Oro (+2) e Bomba (-1)!', 'icon': Icons.stars, 'color': Colors.amber}],
3: [ 3: [
@ -81,7 +81,7 @@ class GameController extends ChangeNotifier {
bool hasLeveledUp = false; bool hasLeveledUp = false;
int newlyReachedLevel = 1; int newlyReachedLevel = 1;
List<Map<String, dynamic>> unlockedRewards = []; // Ora è una lista di mappe dinamiche List<Map<String, dynamic>> unlockedRewards = [];
bool isSetupPhase = true; bool isSetupPhase = true;
bool myJokerPlaced = false; bool myJokerPlaced = false;
@ -124,7 +124,7 @@ class GameController extends ChangeNotifier {
return CpuMatchSetup(chosenRadius, chosenShape); return CpuMatchSetup(chosenRadius, chosenShape);
} }
void startNewGame(int radius, {bool vsCPU = false, bool isOnline = false, String? roomCode, bool isHost = false, ArenaShape shape = ArenaShape.classic, bool timeMode = true}) { void startNewGame(int radius, {bool vsCPU = false, bool isOnline = false, String? roomCode, bool isHost = false, ArenaShape shape = ArenaShape.classic, String timeMode = 'fixed', bool isRematch = false}) {
_onlineSubscription?.cancel(); _onlineSubscription?.cancel();
_onlineSubscription = null; _onlineSubscription = null;
_blitzTimer?.cancel(); _blitzTimer?.cancel();
@ -151,7 +151,29 @@ class GameController extends ChangeNotifier {
this.isOnline = isOnline; this.isOnline = isOnline;
this.roomCode = roomCode; this.roomCode = roomCode;
this.isHost = isHost; this.isHost = isHost;
this.isTimeMode = timeMode;
if (!isRematch) consecutiveRematches = 0;
this.timeModeSetting = timeMode;
// --- LOGICA TIMER ---
if (this.isVsCPU) {
// La CPU usa sempre la sua formula basata sul Livello Profilo
int pLevel = StorageService.instance.playerLevel;
int calculatedTime = 15 - ((pLevel - 1) * 12 / 14).round();
maxTime = calculatedTime.clamp(3, 15);
} else {
// Multiplayer e Locale
if (timeModeSetting == 'dynamic') {
// Parte da 10s e toglie 2s per ogni rivincita (Minimo 2s)
maxTime = max(2, 10 - (consecutiveRematches * 2));
} else if (timeModeSetting == 'relax') {
maxTime = 0; // Il timer non scatterà
} else {
maxTime = 10; // Fisso 10s
}
}
timeLeft = maxTime;
// -------------------
int finalRadius = radius; int finalRadius = radius;
ArenaShape finalShape = shape; ArenaShape finalShape = shape;
@ -330,10 +352,9 @@ class GameController extends ChangeNotifier {
void _startTimer() { void _startTimer() {
_blitzTimer?.cancel(); _blitzTimer?.cancel();
if (isSetupPhase) return; if (isSetupPhase || !isTimeMode) return;
timeLeft = maxTime; timeLeft = maxTime;
if (!isTimeMode) { notifyListeners(); return; }
_blitzTimer = Timer.periodic(const Duration(seconds: 1), (timer) { _blitzTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (isGameOver || isCPUThinking) { timer.cancel(); return; } if (isGameOver || isCPUThinking) { timer.cancel(); return; }
@ -407,9 +428,14 @@ class GameController extends ChangeNotifier {
bool p2Rematch = data['p2_rematch'] ?? false; bool p2Rematch = data['p2_rematch'] ?? false;
opponentWantsRematch = isHost ? p2Rematch : p1Rematch; opponentWantsRematch = isHost ? p2Rematch : p1Rematch;
// === LA RIVINCITA INCREMENTA IL CONTATORE DELLA MODALITA' DINAMICA ===
if (data['status'] == 'playing' && (data['moves'] as List).isEmpty && rematchRequested) { if (data['status'] == 'playing' && (data['moves'] as List).isEmpty && rematchRequested) {
currentSeed = data['seed']; currentSeed = data['seed'];
startNewGame(data['radius'], isOnline: true, roomCode: roomCode, isHost: isHost, shape: ArenaShape.values.firstWhere((e) => e.name == data['shape']), timeMode: data['timeMode']); consecutiveRematches++;
String tMode = data['timeMode'] is String ? data['timeMode'] : (data['timeMode'] == true ? 'fixed' : 'relax');
startNewGame(data['radius'], isOnline: true, roomCode: roomCode, isHost: isHost, shape: ArenaShape.values.firstWhere((e) => e.name == data['shape']), timeMode: tMode, isRematch: true);
return; return;
} }
@ -442,7 +468,9 @@ class GameController extends ChangeNotifier {
String shapeStr = data['shape'] ?? 'classic'; String shapeStr = data['shape'] ?? 'classic';
ArenaShape hostShape = ArenaShape.values.firstWhere((e) => e.name == shapeStr, orElse: () => ArenaShape.classic); ArenaShape hostShape = ArenaShape.values.firstWhere((e) => e.name == shapeStr, orElse: () => ArenaShape.classic);
onlineShape = hostShape; onlineShape = hostShape;
isTimeMode = data['timeMode'] ?? true;
String hostTimeMode = data['timeMode'] is String ? data['timeMode'] : (data['timeMode'] == true ? 'fixed' : 'relax');
timeModeSetting = hostTimeMode;
if (!rematchRequested && (hostLevel > currentMatchLevel || (isOnline && currentSeed == null && hostSeed != null) || (hostSeed != null && hostSeed != currentSeed))) { if (!rematchRequested && (hostLevel > currentMatchLevel || (isOnline && currentSeed == null && hostSeed != null) || (hostSeed != null && hostSeed != currentSeed))) {
currentMatchLevel = hostLevel; currentSeed = hostSeed; currentMatchLevel = hostLevel; currentSeed = hostSeed;
@ -566,7 +594,6 @@ class GameController extends ChangeNotifier {
} }
} }
// --- LOGICA DI ESTRAZIONE SBLOCCHI DINAMICA ---
List<Map<String, dynamic>> _getUnlocks(int oldLevel, int newLevel) { List<Map<String, dynamic>> _getUnlocks(int oldLevel, int newLevel) {
List<Map<String, dynamic>> unlocks = []; List<Map<String, dynamic>> unlocks = [];
for(int i = oldLevel + 1; i <= newLevel; i++) { for(int i = oldLevel + 1; i <= newLevel; i++) {
@ -634,6 +661,6 @@ class GameController extends ChangeNotifier {
void increaseLevelAndRestart() { void increaseLevelAndRestart() {
cpuLevel++; StorageService.instance.saveCpuLevel(cpuLevel); cpuLevel++; StorageService.instance.saveCpuLevel(cpuLevel);
startNewGame(board.radius, vsCPU: true, shape: board.shape, timeMode: isTimeMode); startNewGame(board.radius, vsCPU: true, shape: board.shape, timeMode: timeModeSetting);
} }
} }

View file

@ -15,12 +15,8 @@ import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth/firebase_auth.dart';
import 'firebase_options.dart'; import 'firebase_options.dart';
import 'package:firebase_app_check/firebase_app_check.dart'; import 'package:firebase_app_check/firebase_app_check.dart';
// --- NUOVI IMPORT PER GLI AGGIORNAMENTI ---
import 'package:upgrader/upgrader.dart'; import 'package:upgrader/upgrader.dart';
import 'package:in_app_update/in_app_update.dart'; import 'package:in_app_update/in_app_update.dart';
// --- IMPORT PER IL SUPPORTO MULTILINGUA ---
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:tetraq/l10n/app_localizations.dart'; import 'package:tetraq/l10n/app_localizations.dart';
@ -69,15 +65,11 @@ class TetraQApp extends StatelessWidget {
useMaterial3: true, useMaterial3: true,
), ),
// --- BIVIO DELLE LINGUE ATTIVATO! ---
// Flutter si occuperà di caricare automaticamente tutte le lingue
// che hai generato tramite lo script.
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
// ------------------------------------
// Avvolgiamo la HomeScreen nel nostro nuovo gestore di aggiornamenti! // Nessun modificatore const richiesto qui.
home: const UpdateWrapper(child: HomeScreen()), home: UpdateWrapper(child: HomeScreen()),
); );
} }
} }
@ -97,7 +89,6 @@ class _UpdateWrapperState extends State<UpdateWrapper> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Controlla gli aggiornamenti in background solo se siamo su Android
if (!kIsWeb && Platform.isAndroid) { if (!kIsWeb && Platform.isAndroid) {
_checkForAndroidUpdate(); _checkForAndroidUpdate();
} }
@ -107,12 +98,10 @@ class _UpdateWrapperState extends State<UpdateWrapper> {
try { try {
final info = await InAppUpdate.checkForUpdate(); final info = await InAppUpdate.checkForUpdate();
if (info.updateAvailability == UpdateAvailability.updateAvailable) { if (info.updateAvailability == UpdateAvailability.updateAvailable) {
// Se possibile, fai scaricare l'aggiornamento in background mentre l'utente gioca
if (info.flexibleUpdateAllowed) { if (info.flexibleUpdateAllowed) {
await InAppUpdate.startFlexibleUpdate(); await InAppUpdate.startFlexibleUpdate();
await InAppUpdate.completeFlexibleUpdate(); // Chiede il riavvio rapido dell'app await InAppUpdate.completeFlexibleUpdate();
} }
// Se l'aggiornamento è impostato come critico dalla console di Google Play
else if (info.immediateUpdateAllowed) { else if (info.immediateUpdateAllowed) {
await InAppUpdate.performImmediateUpdate(); await InAppUpdate.performImmediateUpdate();
} }
@ -124,22 +113,17 @@ class _UpdateWrapperState extends State<UpdateWrapper> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Su iOS e macOS usiamo "upgrader" che si occupa di mostrare il pop-up nativo
if (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) { if (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) {
return UpgradeAlert( return UpgradeAlert(
dialogStyle: (Platform.isIOS || Platform.isMacOS) dialogStyle: (Platform.isIOS || Platform.isMacOS)
? UpgradeDialogStyle.cupertino ? UpgradeDialogStyle.cupertino
: UpgradeDialogStyle.material, : UpgradeDialogStyle.material,
showIgnore: false, // <-- Spostato qui showIgnore: false,
showLater: true, // <-- Spostato qui showLater: true,
upgrader: Upgrader( upgrader: Upgrader(),
// debugDisplayAlways: true, // <--- Scommenta questa riga se vuoi testare la UI del pop-up sul Mac ora!
),
child: widget.child, child: widget.child,
); );
} }
// Su Android restituiamo la UI normale (l'aggiornamento è gestito nel background da initState)
return widget.child; return widget.child;
} }
} }

View file

@ -15,7 +15,8 @@ class MultiplayerService {
CollectionReference get _gamesCollection => _firestore.collection('games'); CollectionReference get _gamesCollection => _firestore.collection('games');
CollectionReference get _invitesCollection => _firestore.collection('invites'); CollectionReference get _invitesCollection => _firestore.collection('invites');
Future<String> createGameRoom(int boardRadius, String hostName, String shapeName, bool isTimeMode, {bool isPublic = true}) async { // --- MODIFICA QUI: bool isTimeMode è diventato String timeMode ---
Future<String> createGameRoom(int boardRadius, String hostName, String shapeName, String timeMode, {bool isPublic = true}) async {
String roomCode = _generateRoomCode(); String roomCode = _generateRoomCode();
int randomSeed = Random().nextInt(1000000); int randomSeed = Random().nextInt(1000000);
@ -31,7 +32,7 @@ class MultiplayerService {
'hostUid': _auth.currentUser?.uid, 'hostUid': _auth.currentUser?.uid,
'guestName': '', 'guestName': '',
'shape': shapeName, 'shape': shapeName,
'timeMode': isTimeMode, 'timeMode': timeMode, // Salva la stringa ('fixed', 'relax' o 'dynamic')
'isPublic': isPublic, 'isPublic': isPublic,
'p1_reaction': null, 'p1_reaction': null,
'p2_reaction': null, 'p2_reaction': null,

View file

@ -164,7 +164,7 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
else else
ElevatedButton( 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), style: ElevatedButton.styleFrom(backgroundColor: winnerColor == theme.text ? theme.playerBlue : winnerColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5),
onPressed: () { controller.startNewGame(controller.board.radius, vsCPU: controller.isVsCPU, shape: controller.board.shape, timeMode: controller.isTimeMode); }, onPressed: () { controller.startNewGame(controller.board.radius, vsCPU: controller.isVsCPU, shape: controller.board.shape, timeMode: controller.timeModeSetting); },
child: Text("RIGIOCA", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 2))), child: Text("RIGIOCA", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 2))),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),

View file

@ -33,7 +33,7 @@ class HomeModals {
showDialog( showDialog(
context: context, context: context,
barrierDismissible: false, // Impedisce di chiudere tappando fuori barrierDismissible: false,
barrierColor: Colors.black.withOpacity(0.8), barrierColor: Colors.black.withOpacity(0.8),
builder: (dialogContext) { builder: (dialogContext) {
final themeManager = dialogContext.watch<ThemeManager>(); final themeManager = dialogContext.watch<ThemeManager>();
@ -206,7 +206,6 @@ class HomeModals {
if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) dialogContent = AnimatedCyberBorder(child: dialogContent); if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) dialogContent = AnimatedCyberBorder(child: dialogContent);
// LA PROTEZIONE ANTI-BACK DI ANDROID: Impedisce l'uscita non autorizzata
return PopScope( return PopScope(
canPop: false, canPop: false,
child: Dialog(backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.all(20), child: dialogContent) child: Dialog(backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.all(20), child: dialogContent)
@ -217,8 +216,224 @@ class HomeModals {
); );
} }
// --- SELETTORE DEL TEMPO A 3 OPZIONI ---
static Widget _buildTimeOption(String label, String sub, String value, String current, ThemeColors theme, AppThemeType type, VoidCallback onTap) {
bool isSel = value == current;
return Expanded(
child: GestureDetector(
onTap: onTap,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
height: 50,
decoration: BoxDecoration(
color: isSel ? Colors.orange.shade600 : (type == AppThemeType.doodle ? Colors.white : theme.text.withOpacity(0.05)),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: isSel ? Colors.orange.shade800 : (type == AppThemeType.doodle ? const Color(0xFF111122) : Colors.white24), width: isSel ? 2 : 1.5),
boxShadow: isSel && type != AppThemeType.doodle ? [BoxShadow(color: Colors.orange.withOpacity(0.5), blurRadius: 8)] : [],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(label, style: getSharedTextStyle(type, TextStyle(color: isSel ? Colors.white : (type == AppThemeType.doodle ? const Color(0xFF111122) : theme.text), fontWeight: FontWeight.w900, fontSize: 13))),
if (sub.isNotEmpty) Text(sub, style: getSharedTextStyle(type, TextStyle(color: isSel ? Colors.white70 : (type == AppThemeType.doodle ? Colors.black54 : theme.text.withOpacity(0.5)), fontWeight: FontWeight.bold, fontSize: 8))),
],
),
),
),
);
}
static void showChallengeSetupDialog(BuildContext context, String targetName, Function(int radius, ArenaShape shape, String timeMode) onStart) {
int localRadius = 4; ArenaShape localShape = ArenaShape.classic; String localTimeMode = 'fixed';
bool isChaosUnlocked = StorageService.instance.playerLevel >= 7;
showDialog(
context: context, barrierColor: Colors.black.withOpacity(0.8),
builder: (ctx) {
final themeManager = ctx.watch<ThemeManager>();
final theme = themeManager.currentColors; final themeType = themeManager.currentThemeType;
Color inkColor = const Color(0xFF111122);
return StatefulBuilder(
builder: (context, setStateDialog) {
Widget dialogContent = themeType == AppThemeType.doodle
? Transform.rotate(
angle: 0.015,
child: CustomPaint(
painter: DoodleBackgroundPainter(fillColor: Colors.white.withOpacity(0.95), strokeColor: inkColor, seed: 200),
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(25.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("SFIDA $targetName", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: theme.playerRed, letterSpacing: 2))),
const SizedBox(height: 10),
Text("IMPOSTAZIONI STANZA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))),
const SizedBox(height: 25),
Text("FORMA ARENA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 15),
Wrap(
spacing: 12, runSpacing: 12, alignment: WrapAlignment.center,
children: [
NeonShapeButton(icon: Icons.diamond_outlined, label: 'Rombo', isSelected: localShape == ArenaShape.classic, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.classic)),
NeonShapeButton(icon: Icons.add, label: 'Croce', isSelected: localShape == ArenaShape.cross, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.cross)),
NeonShapeButton(icon: Icons.donut_large, label: 'Buco', isSelected: localShape == ArenaShape.donut, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.donut)),
NeonShapeButton(icon: Icons.hourglass_bottom, label: 'Clessidra', isSelected: localShape == ArenaShape.hourglass, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.hourglass)),
NeonShapeButton(icon: Icons.all_inclusive, label: 'Caos', isSelected: localShape == ArenaShape.chaos, theme: theme, themeType: themeType, isSpecial: true, isLocked: !isChaosUnlocked, onTap: () => setStateDialog(() => localShape = ArenaShape.chaos)),
],
),
const SizedBox(height: 25), Divider(color: inkColor.withOpacity(0.3), thickness: 2.5), const SizedBox(height: 20),
Text("GRANDEZZA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
NeonSizeButton(label: 'S', isSelected: localRadius == 3, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 3)),
NeonSizeButton(label: 'M', isSelected: localRadius == 4, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 4)),
NeonSizeButton(label: 'L', isSelected: localRadius == 5, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 5)),
NeonSizeButton(label: 'MAX', isSelected: localRadius == 6, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 6)),
],
),
const SizedBox(height: 25), Divider(color: inkColor.withOpacity(0.3), thickness: 2.5), const SizedBox(height: 20),
Text("TEMPO E OPZIONI", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 10),
Row(
children: [
_buildTimeOption('10s', 'FISSO', 'fixed', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'fixed')),
_buildTimeOption('RELAX', 'INFINITO', 'relax', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'relax')),
_buildTimeOption('DINAMICO', '-2s A PARTITA', 'dynamic', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'dynamic')),
],
),
const SizedBox(height: 35),
Row(
children: [
Expanded(
child: GestureDetector(
onTap: () {
Navigator.pop(ctx);
onStart(localRadius, localShape, localTimeMode);
},
child: CustomPaint(painter: DoodleBackgroundPainter(fillColor: theme.playerRed, strokeColor: inkColor, seed: 300), child: Container(height: 55, alignment: Alignment.center, child: Text("AVVIA", style: getSharedTextStyle(themeType, const TextStyle(fontSize: 18, fontWeight: FontWeight.w900, letterSpacing: 2.0, color: Colors.white))))),
),
),
const SizedBox(width: 15),
Expanded(
child: GestureDetector(
onTap: () => Navigator.pop(ctx),
child: CustomPaint(painter: DoodleBackgroundPainter(fillColor: Colors.grey.shade400, strokeColor: inkColor, seed: 301), child: Container(height: 55, alignment: Alignment.center, child: Text("ANNULLA", style: getSharedTextStyle(themeType, const TextStyle(fontSize: 18, fontWeight: FontWeight.w900, letterSpacing: 2.0, color: Colors.white))))),
),
),
],
)
],
),
),
),
),
)
: Container(
decoration: BoxDecoration(
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [theme.background.withOpacity(0.95), theme.background.withOpacity(0.8)]),
borderRadius: BorderRadius.circular(25), border: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? null : Border.all(color: Colors.white.withOpacity(0.15), width: 1.5),
boxShadow: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? [] : [BoxShadow(color: Colors.black.withOpacity(0.5), blurRadius: 20, offset: const Offset(4, 10))],
),
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("SFIDA $targetName", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 24, fontWeight: FontWeight.w900, color: theme.playerRed, letterSpacing: 2))),
const SizedBox(height: 10),
Text("IMPOSTAZIONI STANZA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))),
const SizedBox(height: 20),
Text("FORMA ARENA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10),
Wrap(
spacing: 10, runSpacing: 10, alignment: WrapAlignment.center,
children: [
NeonShapeButton(icon: Icons.diamond_outlined, label: 'Rombo', isSelected: localShape == ArenaShape.classic, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.classic)),
NeonShapeButton(icon: Icons.add, label: 'Croce', isSelected: localShape == ArenaShape.cross, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.cross)),
NeonShapeButton(icon: Icons.donut_large, label: 'Buco', isSelected: localShape == ArenaShape.donut, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.donut)),
NeonShapeButton(icon: Icons.hourglass_bottom, label: 'Clessidra', isSelected: localShape == ArenaShape.hourglass, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.hourglass)),
NeonShapeButton(icon: Icons.all_inclusive, label: 'Caos', isSelected: localShape == ArenaShape.chaos, theme: theme, themeType: themeType, isSpecial: true, isLocked: !isChaosUnlocked, onTap: () => setStateDialog(() => localShape = ArenaShape.chaos)),
],
),
const SizedBox(height: 20), Divider(color: Colors.white.withOpacity(0.05), thickness: 2), const SizedBox(height: 20),
Text("GRANDEZZA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
NeonSizeButton(label: 'S', isSelected: localRadius == 3, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 3)),
NeonSizeButton(label: 'M', isSelected: localRadius == 4, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 4)),
NeonSizeButton(label: 'L', isSelected: localRadius == 5, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 5)),
NeonSizeButton(label: 'MAX', isSelected: localRadius == 6, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 6)),
],
),
const SizedBox(height: 20), Divider(color: Colors.white.withOpacity(0.05), thickness: 2), const SizedBox(height: 20),
Text("TEMPO E OPZIONI", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10),
Row(
children: [
_buildTimeOption('10s', 'FISSO', 'fixed', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'fixed')),
_buildTimeOption('RELAX', 'INFINITO', 'relax', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'relax')),
_buildTimeOption('DINAMICO', '-2s A PARTITA', 'dynamic', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'dynamic')),
],
),
const SizedBox(height: 30),
Row(
children: [
Expanded(
child: SizedBox(
height: 55,
child: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: theme.playerRed, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))),
onPressed: () {
Navigator.pop(ctx);
onStart(localRadius, localShape, localTimeMode);
},
child: const Text("AVVIA", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2)),
),
),
),
const SizedBox(width: 15),
Expanded(
child: SizedBox(
height: 55,
child: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.grey.shade800, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))),
onPressed: () => Navigator.pop(ctx),
child: const Text("ANNULLA", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2)),
),
),
),
],
)
],
),
),
),
);
if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) {
dialogContent = AnimatedCyberBorder(child: dialogContent);
}
return Dialog(backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 20), child: dialogContent);
},
);
}
);
}
static void showMatchSetupDialog(BuildContext context, bool isVsCPU) { static void showMatchSetupDialog(BuildContext context, bool isVsCPU) {
int localRadius = 4; ArenaShape localShape = ArenaShape.classic; bool localTimeMode = true; int localRadius = 4; ArenaShape localShape = ArenaShape.classic; String localTimeMode = 'fixed';
bool isChaosUnlocked = StorageService.instance.playerLevel >= 7; bool isChaosUnlocked = StorageService.instance.playerLevel >= 7;
final loc = AppLocalizations.of(context)!; final loc = AppLocalizations.of(context)!;
@ -249,7 +464,7 @@ class HomeModals {
if (isVsCPU) ...[ if (isVsCPU) ...[
Icon(Icons.smart_toy, size: 50, color: inkColor.withOpacity(0.6)), const SizedBox(height: 10), Icon(Icons.smart_toy, size: 50, color: inkColor.withOpacity(0.6)), const SizedBox(height: 10),
Text("MODALITÀ CAMPAGNA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.w900, color: inkColor))), const SizedBox(height: 10), Text("MODALITÀ CAMPAGNA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.w900, color: inkColor))), const SizedBox(height: 10),
Text("Livello CPU: ${StorageService.instance.cpuLevel}\nForma e dimensioni si adatteranno alla tua bravura!", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 13, color: inkColor.withOpacity(0.8), height: 1.4))), const SizedBox(height: 25), Text("Livello CPU: ${StorageService.instance.cpuLevel}\nForma e tempo si adatteranno alla tua bravura!", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 13, color: inkColor.withOpacity(0.8), height: 1.4))), const SizedBox(height: 25),
Divider(color: inkColor.withOpacity(0.3), thickness: 2.5), const SizedBox(height: 20), Divider(color: inkColor.withOpacity(0.3), thickness: 2.5), const SizedBox(height: 20),
] else ...[ ] else ...[
Text("FORMA ARENA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 15), Text("FORMA ARENA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 15),
@ -276,10 +491,17 @@ class HomeModals {
], ],
), ),
const SizedBox(height: 25), Divider(color: inkColor.withOpacity(0.3), thickness: 2.5), const SizedBox(height: 20), const SizedBox(height: 25), Divider(color: inkColor.withOpacity(0.3), thickness: 2.5), const SizedBox(height: 20),
],
// TEMPO È ORA ESCLUSIVO PER IL MULTIPLAYER
Text("TEMPO", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 10), Text("TEMPO", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 10),
NeonTimeSwitch(isTimeMode: localTimeMode, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localTimeMode = !localTimeMode)), const SizedBox(height: 35), Row(
children: [
_buildTimeOption('10s', 'FISSO', 'fixed', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'fixed')),
_buildTimeOption('RELAX', 'INFINITO', 'relax', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'relax')),
_buildTimeOption('DINAMICO', '-2s A PARTITA', 'dynamic', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'dynamic')),
],
), const SizedBox(height: 35),
],
Transform.rotate( Transform.rotate(
angle: -0.02, angle: -0.02,
@ -313,7 +535,7 @@ class HomeModals {
if (isVsCPU) ...[ if (isVsCPU) ...[
Icon(Icons.smart_toy, size: 50, color: theme.playerBlue), const SizedBox(height: 10), Icon(Icons.smart_toy, size: 50, color: theme.playerBlue), const SizedBox(height: 10),
Text("MODALITÀ CAMPAGNA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 1.5))), const SizedBox(height: 10), Text("MODALITÀ CAMPAGNA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 1.5))), const SizedBox(height: 10),
Text("Livello CPU: ${StorageService.instance.cpuLevel}\nForma e dimensioni si adatteranno alla tua bravura!", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 13, color: theme.text.withOpacity(0.7), height: 1.4))), const SizedBox(height: 20), Text("Livello CPU: ${StorageService.instance.cpuLevel}\nForma e tempo si adatteranno alla tua bravura!", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 13, color: theme.text.withOpacity(0.7), height: 1.4))), const SizedBox(height: 20),
Divider(color: Colors.white.withOpacity(0.05), thickness: 2), const SizedBox(height: 20), Divider(color: Colors.white.withOpacity(0.05), thickness: 2), const SizedBox(height: 20),
] else ...[ ] else ...[
Text("FORMA ARENA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10), Text("FORMA ARENA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10),
@ -340,10 +562,16 @@ class HomeModals {
], ],
), ),
const SizedBox(height: 20), Divider(color: Colors.white.withOpacity(0.05), thickness: 2), const SizedBox(height: 20), const SizedBox(height: 20), Divider(color: Colors.white.withOpacity(0.05), thickness: 2), const SizedBox(height: 20),
],
Text("TEMPO", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10), Text("TEMPO", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10),
NeonTimeSwitch(isTimeMode: localTimeMode, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localTimeMode = !localTimeMode)), const SizedBox(height: 30), Row(
children: [
_buildTimeOption('10s', 'FISSO', 'fixed', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'fixed')),
_buildTimeOption('RELAX', 'INFINITO', 'relax', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'relax')),
_buildTimeOption('DINAMICO', '-2s A PARTITA', 'dynamic', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'dynamic')),
],
), const SizedBox(height: 30),
],
SizedBox( SizedBox(
width: double.infinity, height: 60, width: double.infinity, height: 60,
@ -376,7 +604,7 @@ class HomeModals {
required bool isPublicRoom, required bool isPublicRoom,
required int selectedRadius, required int selectedRadius,
required ArenaShape selectedShape, required ArenaShape selectedShape,
required bool isTimeMode, required String selectedTimeMode,
required MultiplayerService multiplayerService, required MultiplayerService multiplayerService,
required VoidCallback onRoomStarted, required VoidCallback onRoomStarted,
required VoidCallback onCleanup, required VoidCallback onCleanup,
@ -410,9 +638,9 @@ class HomeModals {
child: Column( child: Column(
children: [ children: [
Icon(isPublicRoom ? Icons.podcasts : Icons.share, color: theme.playerBlue, size: 32), const SizedBox(height: 12), Icon(isPublicRoom ? Icons.podcasts : Icons.share, color: theme.playerBlue, size: 32), const SizedBox(height: 12),
Text(isPublicRoom ? "Sei in Bacheca!" : "Invita un amico", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.w900, fontSize: 18))), Text(isPublicRoom ? "Sei in Bacheca!" : "Invito inviato", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.w900, fontSize: 18))),
const SizedBox(height: 8), const SizedBox(height: 8),
Text(isPublicRoom ? "Aspettiamo che uno sfidante si unisca dalla lobby pubblica." : "Condividi il codice. La partita inizierà appena si unirà.", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.8), fontSize: 14, height: 1.5))), Text(isPublicRoom ? "Aspettiamo che uno sfidante si unisca dalla lobby pubblica." : "Attendi che il tuo amico accetti la sfida. Non chiudere questa finestra.", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.8), fontSize: 14, height: 1.5))),
], ],
), ),
), ),
@ -444,8 +672,8 @@ class HomeModals {
onRoomStarted(); onRoomStarted();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.pop(ctx); Navigator.pop(ctx);
context.read<GameController>().startNewGame(selectedRadius, isOnline: true, roomCode: code, isHost: true, shape: selectedShape, timeMode: isTimeMode); context.read<GameController>().startNewGame(selectedRadius, isOnline: true, roomCode: code, isHost: true, shape: selectedShape, timeMode: selectedTimeMode);
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const GameScreen())); Navigator.push(context, MaterialPageRoute(builder: (_) => const GameScreen()));
}); });
} }
} }

View file

@ -3,7 +3,7 @@
// =========================================================================== // ===========================================================================
import 'dart:ui'; import 'dart:ui';
import 'dart:math'; // Aggiunto per generare il codice della stanza randomico import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -45,13 +45,12 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
late AppLinks _appLinks; late AppLinks _appLinks;
StreamSubscription<Uri>? _linkSubscription; StreamSubscription<Uri>? _linkSubscription;
StreamSubscription<QuerySnapshot>? _favoritesSubscription; StreamSubscription<QuerySnapshot>? _favoritesSubscription;
StreamSubscription<QuerySnapshot>? _invitesSubscription; // <--- Nuovo Listener per gli inviti in arrivo StreamSubscription<QuerySnapshot>? _invitesSubscription;
Map<String, DateTime> _lastOnlineNotifications = {}; Map<String, DateTime> _lastOnlineNotifications = {};
final int _selectedRadius = 4; final int _selectedRadius = 4;
final ArenaShape _selectedShape = ArenaShape.classic; final ArenaShape _selectedShape = ArenaShape.classic;
final bool _isTimeMode = true;
final bool _isPublicRoom = true; final bool _isPublicRoom = true;
bool _isLoading = false; bool _isLoading = false;
@ -68,12 +67,12 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
if (FirebaseAuth.instance.currentUser == null) { if (FirebaseAuth.instance.currentUser == null) {
HomeModals.showNameDialog(context, () { HomeModals.showNameDialog(context, () {
StorageService.instance.syncLeaderboard(); StorageService.instance.syncLeaderboard();
_listenToInvites(); // <--- Ascoltiamo gli inviti appena loggati _listenToInvites();
setState(() {}); setState(() {});
}); });
} else { } else {
StorageService.instance.syncLeaderboard(); StorageService.instance.syncLeaderboard();
_listenToInvites(); // <--- Ascoltiamo gli inviti se eravamo già loggati _listenToInvites();
} }
_checkThemeSafety(); _checkThemeSafety();
}); });
@ -96,7 +95,7 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
_cleanupGhostRoom(); _cleanupGhostRoom();
_linkSubscription?.cancel(); _linkSubscription?.cancel();
_favoritesSubscription?.cancel(); _favoritesSubscription?.cancel();
_invitesSubscription?.cancel(); // <--- Chiusura Listener _invitesSubscription?.cancel();
super.dispose(); super.dispose();
} }
@ -218,9 +217,6 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
overlay.insert(entry); overlay.insert(entry);
} }
// =========================================================================
// SISTEMA INVITI DIRETTO TRAMITE FIRESTORE
// =========================================================================
void _listenToInvites() { void _listenToInvites() {
final user = FirebaseAuth.instance.currentUser; final user = FirebaseAuth.instance.currentUser;
if (user == null) return; if (user == null) return;
@ -241,7 +237,6 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
String from = data['fromName']; String from = data['fromName'];
String inviteId = change.doc.id; String inviteId = change.doc.id;
// Filtro sicurezza: Evita di mostrare inviti fantasma vecchi di oltre 2 minuti
Timestamp? ts = data['timestamp']; Timestamp? ts = data['timestamp'];
if (ts != null) { if (ts != null) {
if (DateTime.now().difference(ts.toDate()).inMinutes > 2) { if (DateTime.now().difference(ts.toDate()).inMinutes > 2) {
@ -296,35 +291,41 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
); );
} }
Future<void> _sendChallenge(String targetUid, String targetName) async { void _startDirectChallengeFlow(String targetUid, String targetName) {
HomeModals.showChallengeSetupDialog(
context,
targetName,
(int radius, ArenaShape shape, String timeMode) {
_executeSendChallenge(targetUid, targetName, radius, shape, timeMode);
}
);
}
Future<void> _executeSendChallenge(String targetUid, String targetName, int radius, ArenaShape shape, String timeMode) async {
setState(() => _isLoading = true); setState(() => _isLoading = true);
// Generiamo un codice stanza casuale univoco
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
final rnd = Random(); final rnd = Random();
String roomCode = String.fromCharCodes(Iterable.generate(5, (_) => chars.codeUnitAt(rnd.nextInt(chars.length)))); String roomCode = String.fromCharCodes(Iterable.generate(5, (_) => chars.codeUnitAt(rnd.nextInt(chars.length))));
try { try {
// 1. IL SEGRETO DELLA SINCRONIZZAZIONE: Generiamo un "Seme" (Seed) comune!
int gameSeed = rnd.nextInt(9999999); int gameSeed = rnd.nextInt(9999999);
// Creiamo la stanza privata con tutti i crismi (e il seed!)
await FirebaseFirestore.instance.collection('games').doc(roomCode).set({ await FirebaseFirestore.instance.collection('games').doc(roomCode).set({
'status': 'waiting', 'status': 'waiting',
'hostName': StorageService.instance.playerName, 'hostName': StorageService.instance.playerName,
'hostUid': FirebaseAuth.instance.currentUser?.uid, 'hostUid': FirebaseAuth.instance.currentUser?.uid,
'radius': 4, 'radius': radius,
'shape': 'classic', 'shape': shape.name,
'timeMode': true, 'timeMode': timeMode,
'isPublic': false, // È una stanza privata 'isPublic': false,
'createdAt': FieldValue.serverTimestamp(), 'createdAt': FieldValue.serverTimestamp(),
'players': [FirebaseAuth.instance.currentUser?.uid], 'players': [FirebaseAuth.instance.currentUser?.uid],
'turn': 0, 'turn': 0,
'moves': [], 'moves': [],
'seed': gameSeed, // <--- ECCO IL PEZZO MANCANTE CHE GARANTISCE GRIGLIE IDENTICHE! 'seed': gameSeed,
}); });
// 2. Inviamo l'invito al nostro avversario
await FirebaseFirestore.instance.collection('invites').add({ await FirebaseFirestore.instance.collection('invites').add({
'toUid': targetUid, 'toUid': targetUid,
'fromName': StorageService.instance.playerName, 'fromName': StorageService.instance.playerName,
@ -334,19 +335,17 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
setState(() => _isLoading = false); setState(() => _isLoading = false);
// 3. Apriamo il radar d'attesa (che ascolta quando lui accetta)
if (mounted) { if (mounted) {
HomeModals.showWaitingDialog( HomeModals.showWaitingDialog(
context: context, context: context,
code: roomCode, code: roomCode,
isPublicRoom: false, isPublicRoom: false,
selectedRadius: 4, selectedRadius: radius,
selectedShape: ArenaShape.classic, selectedShape: shape,
isTimeMode: true, selectedTimeMode: timeMode,
multiplayerService: _multiplayerService, multiplayerService: _multiplayerService,
onRoomStarted: () {}, onRoomStarted: () {},
onCleanup: () { onCleanup: () {
// Se noi annulliamo, cancelliamo la stanza
FirebaseFirestore.instance.collection('games').doc(roomCode).delete(); FirebaseFirestore.instance.collection('games').doc(roomCode).delete();
} }
); );
@ -358,7 +357,6 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
} }
} }
} }
// =========================================================================
Future<void> _joinRoomByCode(String code) async { Future<void> _joinRoomByCode(String code) async {
if (_isLoading) return; if (_isLoading) return;
@ -375,15 +373,16 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
setState(() => _isLoading = false); setState(() => _isLoading = false);
if (roomData != null) { if (roomData != null) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Stanza trovata! Partita in avvio..."), backgroundColor: Colors.green)); ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("La sfida inizierà a breve..."), backgroundColor: Colors.green));
int hostRadius = roomData['radius'] ?? 4; int hostRadius = roomData['radius'] ?? 4;
String shapeStr = roomData['shape'] ?? 'classic'; String shapeStr = roomData['shape'] ?? 'classic';
ArenaShape hostShape = ArenaShape.values.firstWhere((e) => e.name == shapeStr, orElse: () => ArenaShape.classic); ArenaShape hostShape = ArenaShape.values.firstWhere((e) => e.name == shapeStr, orElse: () => ArenaShape.classic);
bool hostTimeMode = roomData['timeMode'] ?? true;
String hostTimeMode = roomData['timeMode'] is String ? roomData['timeMode'] : (roomData['timeMode'] == true ? 'fixed' : 'relax');
context.read<GameController>().startNewGame(hostRadius, isOnline: true, roomCode: code, isHost: false, shape: hostShape, timeMode: hostTimeMode); context.read<GameController>().startNewGame(hostRadius, isOnline: true, roomCode: code, isHost: false, shape: hostShape, timeMode: hostTimeMode);
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const GameScreen())); Navigator.push(context, MaterialPageRoute(builder: (_) => const GameScreen()));
} else { } else {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Stanza non trovata, piena o partita già iniziata.", style: TextStyle(color: Colors.white)), backgroundColor: Colors.red)); ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Stanza non trovata, piena o partita già iniziata.", style: TextStyle(color: Colors.white)), backgroundColor: Colors.red));
} }
@ -624,7 +623,7 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded(child: MusicKnobCard(title: loc.leaderboardTitle, icon: FontAwesomeIcons.compactDisc, iconColor: Colors.amber, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => LeaderboardDialog(onChallenge: _sendChallenge)))), Expanded(child: MusicKnobCard(title: loc.leaderboardTitle, icon: FontAwesomeIcons.compactDisc, iconColor: Colors.amber, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => LeaderboardDialog(onChallenge: _startDirectChallengeFlow)))),
Expanded(child: MusicKnobCard(title: loc.questsTitle, icon: FontAwesomeIcons.microphoneLines, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => const QuestsDialog()))), Expanded(child: MusicKnobCard(title: loc.questsTitle, icon: FontAwesomeIcons.microphoneLines, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => const QuestsDialog()))),
Expanded(child: MusicKnobCard(title: loc.themesTitle, icon: FontAwesomeIcons.palette, themeType: themeType, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => SettingsScreen())))), Expanded(child: MusicKnobCard(title: loc.themesTitle, icon: FontAwesomeIcons.palette, themeType: themeType, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => SettingsScreen())))),
Expanded(child: MusicKnobCard(title: loc.tutorialTitle, icon: FontAwesomeIcons.bookOpen, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => const TutorialDialog()))), Expanded(child: MusicKnobCard(title: loc.tutorialTitle, icon: FontAwesomeIcons.bookOpen, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => const TutorialDialog()))),
@ -643,7 +642,7 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
Row( Row(
children: [ children: [
Expanded(child: _buildCyberCard(FeatureCard(title: loc.leaderboardTitle, subtitle: "Top 50 Globale", icon: Icons.leaderboard, color: Colors.amber.shade200, theme: theme, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => LeaderboardDialog(onChallenge: _sendChallenge)), compact: true), themeType)), Expanded(child: _buildCyberCard(FeatureCard(title: loc.leaderboardTitle, subtitle: "Top 50 Globale", icon: Icons.leaderboard, color: Colors.amber.shade200, theme: theme, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => LeaderboardDialog(onChallenge: _startDirectChallengeFlow)), compact: true), themeType)),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded(child: _buildCyberCard(FeatureCard(title: loc.questsTitle, subtitle: "Missioni", icon: Icons.assignment_turned_in, color: Colors.green.shade200, theme: theme, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => const QuestsDialog()), compact: true), themeType)), Expanded(child: _buildCyberCard(FeatureCard(title: loc.questsTitle, subtitle: "Missioni", icon: Icons.assignment_turned_in, color: Colors.green.shade200, theme: theme, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => const QuestsDialog()), compact: true), themeType)),
], ],
@ -744,7 +743,6 @@ class FullScreenGridPainter extends CustomPainter {
bool shouldRepaint(covariant CustomPainter oldDelegate) => false; bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
} }
// --- WIDGET POPUP AMICO ONLINE (Ripristinato in coda!) ---
class FavoriteOnlinePopup extends StatefulWidget { class FavoriteOnlinePopup extends StatefulWidget {
final String name; final String name;
final VoidCallback onDismiss; final VoidCallback onDismiss;
@ -768,7 +766,6 @@ class _FavoriteOnlinePopupState extends State<FavoriteOnlinePopup> with SingleTi
_controller.forward(); _controller.forward();
// Chiude il popup automaticamente dopo 3 secondi
Future.delayed(const Duration(seconds: 3), () { Future.delayed(const Duration(seconds: 3), () {
if (mounted) { if (mounted) {
_controller.reverse().then((_) => widget.onDismiss()); _controller.reverse().then((_) => widget.onDismiss());

View file

@ -16,7 +16,7 @@ import '../../services/multiplayer_service.dart';
import '../../services/storage_service.dart'; import '../../services/storage_service.dart';
import '../game/game_screen.dart'; import '../game/game_screen.dart';
import '../../widgets/painters.dart'; import '../../widgets/painters.dart';
import '../../widgets/cyber_border.dart'; // <--- ECCO L'IMPORT MANCANTE! import '../../widgets/cyber_border.dart';
import 'lobby_widgets.dart'; import 'lobby_widgets.dart';
class LobbyScreen extends StatefulWidget { class LobbyScreen extends StatefulWidget {
@ -40,7 +40,9 @@ class _LobbyScreenState extends State<LobbyScreen> with WidgetsBindingObserver {
int _selectedRadius = 4; int _selectedRadius = 4;
ArenaShape _selectedShape = ArenaShape.classic; ArenaShape _selectedShape = ArenaShape.classic;
bool _isTimeMode = true;
String _timeModeSetting = 'fixed';
bool _isPublicRoom = true; bool _isPublicRoom = true;
bool _roomStarted = false; bool _roomStarted = false;
@ -87,7 +89,7 @@ class _LobbyScreenState extends State<LobbyScreen> with WidgetsBindingObserver {
try { try {
String code = await _multiplayerService.createGameRoom( String code = await _multiplayerService.createGameRoom(
_selectedRadius, _playerName, _selectedShape.name, _isTimeMode, isPublic: _isPublicRoom _selectedRadius, _playerName, _selectedShape.name, _timeModeSetting, isPublic: _isPublicRoom
); );
if (!mounted) return; if (!mounted) return;
@ -108,7 +110,7 @@ class _LobbyScreenState extends State<LobbyScreen> with WidgetsBindingObserver {
try { try {
String code = await _multiplayerService.createGameRoom( String code = await _multiplayerService.createGameRoom(
_selectedRadius, _playerName, _selectedShape.name, _isTimeMode, isPublic: _isPublicRoom _selectedRadius, _playerName, _selectedShape.name, _timeModeSetting, isPublic: _isPublicRoom
); );
await _multiplayerService.sendInvite(targetUid, code, _playerName); await _multiplayerService.sendInvite(targetUid, code, _playerName);
@ -145,7 +147,8 @@ class _LobbyScreenState extends State<LobbyScreen> with WidgetsBindingObserver {
int hostRadius = roomData['radius'] ?? 4; int hostRadius = roomData['radius'] ?? 4;
String shapeStr = roomData['shape'] ?? 'classic'; String shapeStr = roomData['shape'] ?? 'classic';
ArenaShape hostShape = ArenaShape.values.firstWhere((e) => e.name == shapeStr, orElse: () => ArenaShape.classic); ArenaShape hostShape = ArenaShape.values.firstWhere((e) => e.name == shapeStr, orElse: () => ArenaShape.classic);
bool hostTimeMode = roomData['timeMode'] ?? true;
String hostTimeMode = roomData['timeMode'] is String ? roomData['timeMode'] : (roomData['timeMode'] == true ? 'fixed' : 'relax');
context.read<GameController>().startNewGame(hostRadius, isOnline: true, roomCode: code, isHost: false, shape: hostShape, timeMode: hostTimeMode); context.read<GameController>().startNewGame(hostRadius, isOnline: true, roomCode: code, isHost: false, shape: hostShape, timeMode: hostTimeMode);
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const GameScreen())); Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const GameScreen()));
@ -276,7 +279,7 @@ class _LobbyScreenState extends State<LobbyScreen> with WidgetsBindingObserver {
_roomStarted = true; _roomStarted = true;
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.pop(context); Navigator.pop(context);
context.read<GameController>().startNewGame(_selectedRadius, isOnline: true, roomCode: code, isHost: true, shape: _selectedShape, timeMode: _isTimeMode); context.read<GameController>().startNewGame(_selectedRadius, isOnline: true, roomCode: code, isHost: true, shape: _selectedShape, timeMode: _timeModeSetting);
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const GameScreen())); Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const GameScreen()));
}); });
} }
@ -314,6 +317,32 @@ class _LobbyScreenState extends State<LobbyScreen> with WidgetsBindingObserver {
); );
} }
Widget _buildTimeOption(String label, String sub, String value, ThemeColors theme, AppThemeType type) {
bool isSel = value == _timeModeSetting;
return Expanded(
child: GestureDetector(
onTap: () => setState(() => _timeModeSetting = value),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
height: 50,
decoration: BoxDecoration(
color: isSel ? Colors.orange.shade600 : (type == AppThemeType.doodle ? Colors.white : theme.text.withOpacity(0.05)),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: isSel ? Colors.orange.shade800 : (type == AppThemeType.doodle ? const Color(0xFF111122) : Colors.white24), width: isSel ? 2 : 1.5),
boxShadow: isSel && type != AppThemeType.doodle ? [BoxShadow(color: Colors.orange.withOpacity(0.5), blurRadius: 8)] : [],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(label, style: getLobbyTextStyle(type, TextStyle(color: isSel ? Colors.white : (type == AppThemeType.doodle ? const Color(0xFF111122) : theme.text), fontWeight: FontWeight.w900, fontSize: 13))),
if (sub.isNotEmpty) Text(sub, style: getLobbyTextStyle(type, TextStyle(color: isSel ? Colors.white70 : (type == AppThemeType.doodle ? Colors.black54 : theme.text.withOpacity(0.5)), fontWeight: FontWeight.bold, fontSize: 8))),
],
),
),
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final themeManager = context.watch<ThemeManager>(); final themeManager = context.watch<ThemeManager>();
@ -329,16 +358,15 @@ class _LobbyScreenState extends State<LobbyScreen> with WidgetsBindingObserver {
bool isChaosUnlocked = StorageService.instance.playerLevel >= 7; bool isChaosUnlocked = StorageService.instance.playerLevel >= 7;
// --- MODIFICA COLORE SFONDO HOST PANEL ---
Color panelBackgroundColor = Colors.transparent; Color panelBackgroundColor = Colors.transparent;
if (themeType == AppThemeType.cyberpunk) { if (themeType == AppThemeType.cyberpunk) {
panelBackgroundColor = Colors.black.withOpacity(0.1); panelBackgroundColor = Colors.black.withOpacity(0.1);
} else if (themeType == AppThemeType.doodle) { } else if (themeType == AppThemeType.doodle) {
panelBackgroundColor = Colors.white.withOpacity(0.5); panelBackgroundColor = Colors.white.withOpacity(0.5);
} else if (themeType == AppThemeType.grimorio) { } else if (themeType == AppThemeType.grimorio) {
panelBackgroundColor = Colors.white.withOpacity(0.2); // Sfumatura bianca leggera per Grimorio panelBackgroundColor = Colors.white.withOpacity(0.2);
} else if (themeType == AppThemeType.arcade) { } else if (themeType == AppThemeType.arcade) {
panelBackgroundColor = Colors.black.withOpacity(0.4); // <-- AGGIUNGI QUESTO PER L'ARCADE panelBackgroundColor = Colors.black.withOpacity(0.4);
} }
@ -405,7 +433,9 @@ class _LobbyScreenState extends State<LobbyScreen> with WidgetsBindingObserver {
Row( Row(
children: [ children: [
Expanded(child: NeonTimeSwitch(isTimeMode: _isTimeMode, theme: theme, themeType: themeType, onTap: () => setState(() => _isTimeMode = !_isTimeMode))), _buildTimeOption('10s', 'FISSO', 'fixed', theme, themeType),
_buildTimeOption('RELAX', 'INFINITO', 'relax', theme, themeType),
_buildTimeOption('DINAMICO', '-2s', 'dynamic', theme, themeType),
], ],
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
@ -583,7 +613,11 @@ class _LobbyScreenState extends State<LobbyScreen> with WidgetsBindingObserver {
String host = data['hostName'] ?? 'Sconosciuto'; String host = data['hostName'] ?? 'Sconosciuto';
int r = data['radius'] ?? 4; int r = data['radius'] ?? 4;
String shapeStr = data['shape'] ?? 'classic'; String shapeStr = data['shape'] ?? 'classic';
bool time = data['timeMode'] ?? true;
String tMode = data['timeMode'] is String ? data['timeMode'] : (data['timeMode'] == true ? 'fixed' : 'relax');
String prettyTime = "10s";
if (tMode == 'relax') prettyTime = "Relax";
else if (tMode == 'dynamic') prettyTime = "Dinamico";
String prettyShape = "Rombo"; String prettyShape = "Rombo";
if (shapeStr == 'cross') prettyShape = "Croce"; if (shapeStr == 'cross') prettyShape = "Croce";
@ -612,7 +646,7 @@ class _LobbyScreenState extends State<LobbyScreen> with WidgetsBindingObserver {
children: [ children: [
Text("Stanza di $host", style: getLobbyTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 18))), Text("Stanza di $host", style: getLobbyTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 18))),
const SizedBox(height: 6), const SizedBox(height: 6),
Text("Raggio: $r$prettyShape${time ? 'A Tempo' : 'Relax'}", style: getLobbyTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.6), fontSize: 12))), Text("Raggio: $r$prettyShape$prettyTime", style: getLobbyTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.6), fontSize: 12))),
], ],
), ),
), ),