Auto-sync: 20260320_220000
This commit is contained in:
parent
d68068117a
commit
931cfa0d5a
7 changed files with 371 additions and 100 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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))),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
Loading…
Reference in a new issue