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

View file

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

View file

@ -15,7 +15,8 @@ class MultiplayerService {
CollectionReference get _gamesCollection => _firestore.collection('games');
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();
int randomSeed = Random().nextInt(1000000);
@ -31,7 +32,7 @@ class MultiplayerService {
'hostUid': _auth.currentUser?.uid,
'guestName': '',
'shape': shapeName,
'timeMode': isTimeMode,
'timeMode': timeMode, // Salva la stringa ('fixed', 'relax' o 'dynamic')
'isPublic': isPublic,
'p1_reaction': null,
'p2_reaction': null,

View file

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

View file

@ -33,7 +33,7 @@ class HomeModals {
showDialog(
context: context,
barrierDismissible: false, // Impedisce di chiudere tappando fuori
barrierDismissible: false,
barrierColor: Colors.black.withOpacity(0.8),
builder: (dialogContext) {
final themeManager = dialogContext.watch<ThemeManager>();
@ -206,7 +206,6 @@ class HomeModals {
if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) dialogContent = AnimatedCyberBorder(child: dialogContent);
// LA PROTEZIONE ANTI-BACK DI ANDROID: Impedisce l'uscita non autorizzata
return PopScope(
canPop: false,
child: Dialog(backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.all(20), child: dialogContent)
@ -217,8 +216,224 @@ class HomeModals {
);
}
// --- SELETTORE DEL TEMPO A 3 OPZIONI ---
static Widget _buildTimeOption(String label, String sub, String value, String current, ThemeColors theme, AppThemeType type, VoidCallback onTap) {
bool isSel = value == current;
return Expanded(
child: GestureDetector(
onTap: onTap,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
height: 50,
decoration: BoxDecoration(
color: isSel ? Colors.orange.shade600 : (type == AppThemeType.doodle ? Colors.white : theme.text.withOpacity(0.05)),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: isSel ? Colors.orange.shade800 : (type == AppThemeType.doodle ? const Color(0xFF111122) : Colors.white24), width: isSel ? 2 : 1.5),
boxShadow: isSel && type != AppThemeType.doodle ? [BoxShadow(color: Colors.orange.withOpacity(0.5), blurRadius: 8)] : [],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(label, style: getSharedTextStyle(type, TextStyle(color: isSel ? Colors.white : (type == AppThemeType.doodle ? const Color(0xFF111122) : theme.text), fontWeight: FontWeight.w900, fontSize: 13))),
if (sub.isNotEmpty) Text(sub, style: getSharedTextStyle(type, TextStyle(color: isSel ? Colors.white70 : (type == AppThemeType.doodle ? Colors.black54 : theme.text.withOpacity(0.5)), fontWeight: FontWeight.bold, fontSize: 8))),
],
),
),
),
);
}
static void showChallengeSetupDialog(BuildContext context, String targetName, Function(int radius, ArenaShape shape, String timeMode) onStart) {
int localRadius = 4; ArenaShape localShape = ArenaShape.classic; String localTimeMode = 'fixed';
bool isChaosUnlocked = StorageService.instance.playerLevel >= 7;
showDialog(
context: context, barrierColor: Colors.black.withOpacity(0.8),
builder: (ctx) {
final themeManager = ctx.watch<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) {
int localRadius = 4; ArenaShape localShape = ArenaShape.classic; bool localTimeMode = true;
int localRadius = 4; ArenaShape localShape = ArenaShape.classic; String localTimeMode = 'fixed';
bool isChaosUnlocked = StorageService.instance.playerLevel >= 7;
final loc = AppLocalizations.of(context)!;
@ -249,7 +464,7 @@ class HomeModals {
if (isVsCPU) ...[
Icon(Icons.smart_toy, size: 50, color: inkColor.withOpacity(0.6)), const SizedBox(height: 10),
Text("MODALITÀ CAMPAGNA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.w900, color: inkColor))), const SizedBox(height: 10),
Text("Livello CPU: ${StorageService.instance.cpuLevel}\nForma e dimensioni si adatteranno alla tua bravura!", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 13, color: inkColor.withOpacity(0.8), height: 1.4))), const SizedBox(height: 25),
Text("Livello CPU: ${StorageService.instance.cpuLevel}\nForma e tempo si adatteranno alla tua bravura!", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 13, color: inkColor.withOpacity(0.8), height: 1.4))), const SizedBox(height: 25),
Divider(color: inkColor.withOpacity(0.3), thickness: 2.5), const SizedBox(height: 20),
] else ...[
Text("FORMA ARENA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 15),
@ -276,10 +491,17 @@ class HomeModals {
],
),
const SizedBox(height: 25), Divider(color: inkColor.withOpacity(0.3), thickness: 2.5), const SizedBox(height: 20),
],
// 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),
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(
angle: -0.02,
@ -313,7 +535,7 @@ class HomeModals {
if (isVsCPU) ...[
Icon(Icons.smart_toy, size: 50, color: theme.playerBlue), const SizedBox(height: 10),
Text("MODALITÀ CAMPAGNA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 1.5))), const SizedBox(height: 10),
Text("Livello CPU: ${StorageService.instance.cpuLevel}\nForma e dimensioni si adatteranno alla tua bravura!", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 13, color: theme.text.withOpacity(0.7), height: 1.4))), const SizedBox(height: 20),
Text("Livello CPU: ${StorageService.instance.cpuLevel}\nForma e tempo si adatteranno alla tua bravura!", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 13, color: theme.text.withOpacity(0.7), height: 1.4))), const SizedBox(height: 20),
Divider(color: Colors.white.withOpacity(0.05), thickness: 2), const SizedBox(height: 20),
] else ...[
Text("FORMA ARENA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10),
@ -340,10 +562,16 @@ class HomeModals {
],
),
const SizedBox(height: 20), Divider(color: Colors.white.withOpacity(0.05), thickness: 2), const SizedBox(height: 20),
],
Text("TEMPO", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10),
NeonTimeSwitch(isTimeMode: localTimeMode, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localTimeMode = !localTimeMode)), const SizedBox(height: 30),
Row(
children: [
_buildTimeOption('10s', 'FISSO', 'fixed', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'fixed')),
_buildTimeOption('RELAX', 'INFINITO', 'relax', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'relax')),
_buildTimeOption('DINAMICO', '-2s A PARTITA', 'dynamic', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'dynamic')),
],
), const SizedBox(height: 30),
],
SizedBox(
width: double.infinity, height: 60,
@ -376,7 +604,7 @@ class HomeModals {
required bool isPublicRoom,
required int selectedRadius,
required ArenaShape selectedShape,
required bool isTimeMode,
required String selectedTimeMode,
required MultiplayerService multiplayerService,
required VoidCallback onRoomStarted,
required VoidCallback onCleanup,
@ -410,9 +638,9 @@ class HomeModals {
child: Column(
children: [
Icon(isPublicRoom ? Icons.podcasts : Icons.share, color: theme.playerBlue, size: 32), const SizedBox(height: 12),
Text(isPublicRoom ? "Sei in Bacheca!" : "Invita un amico", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.w900, fontSize: 18))),
Text(isPublicRoom ? "Sei in Bacheca!" : "Invito inviato", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.w900, fontSize: 18))),
const SizedBox(height: 8),
Text(isPublicRoom ? "Aspettiamo che uno sfidante si unisca dalla lobby pubblica." : "Condividi il codice. La partita inizierà appena si unirà.", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.8), fontSize: 14, height: 1.5))),
Text(isPublicRoom ? "Aspettiamo che uno sfidante si unisca dalla lobby pubblica." : "Attendi che il tuo amico accetti la sfida. Non chiudere questa finestra.", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.8), fontSize: 14, height: 1.5))),
],
),
),
@ -444,8 +672,8 @@ class HomeModals {
onRoomStarted();
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.pop(ctx);
context.read<GameController>().startNewGame(selectedRadius, isOnline: true, roomCode: code, isHost: true, shape: selectedShape, timeMode: isTimeMode);
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const GameScreen()));
context.read<GameController>().startNewGame(selectedRadius, isOnline: true, roomCode: code, isHost: true, shape: selectedShape, timeMode: selectedTimeMode);
Navigator.push(context, MaterialPageRoute(builder: (_) => const GameScreen()));
});
}
}

View file

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

View file

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