tetraq/tetraq_memory.txt
2026-02-27 23:35:54 +01:00

3750 lines
No EOL
139 KiB
Text

=== TETRAQ PROJECT BACKUP ===
=== PROJECT STRUCTURE (LIB & ASSETS) ===
assets/.DS_Store
assets/audio/sfx/cyber_box.wav
assets/audio/sfx/cyber_line.wav
assets/audio/sfx/doodle_box.wav
assets/audio/sfx/doodle_line.wav
assets/audio/sfx/minimal_box.wav
assets/audio/sfx/minimal_line.wav
assets/icon/icona_master.png
assets/images/.DS_Store
assets/images/doodle_bg.jpg
assets/images/doodle_bg1.jpg
assets/images/icona_big.jpeg
assets/images/wood_bg.jpg
lib/.DS_Store
lib/core/app_colors.dart
lib/core/constants.dart
lib/core/theme_manager.dart
lib/firebase_options.dart
lib/l10n/app_en.arb
lib/l10n/app_it.arb
lib/logic/ai_engine.dart
lib/logic/game_controller.dart
lib/main.dart
lib/models/game_board.dart
lib/models/player_info.dart
lib/services/audio_service.dart
lib/services/firebase_service.dart
lib/services/multiplayer_service.dart
lib/services/storage_service.dart
lib/ui/game/board_painter.dart
lib/ui/game/game_screen.dart
lib/ui/game/score_board.dart
lib/ui/home/history_screen.dart
lib/ui/home/home_screen.dart
lib/ui/multiplayer/lobby_screen.dart
lib/ui/settings/settings_screen.dart
lib/widgets/custom_button.dart
lib/widgets/custom_settings_button.dart
lib/widgets/game_over_dialog.dart
=== pubspec.yaml ===
name: tetraq
description: A new Flutter project.
publish_to: 'none'
version: 1.0.0+4
environment:
sdk: ^3.10.7
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
# I nostri "Superpoteri"
provider: ^6.1.2 # Per gestire lo stato (Temi, Punteggi)
shared_preferences: ^2.5.4 # Per salvare opzioni e record sul telefono
audioplayers: ^5.2.1 # Per la musica e gli effetti sonori
intl: ^0.20.2 # Necessario per le traduzioni
flutter_localizations: # Il sistema multilingua ufficiale
sdk: flutter
firebase_core: ^4.4.0
cloud_firestore: ^6.1.2
share_plus: ^12.0.1
app_links: ^7.0.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
flutter_launcher_icons: ^0.13.1
flutter:
uses-material-design: true
# Abilita la generazione automatica delle traduzioni
generate: true
# Dichiariamo le cartelle dove metteremo immagini e suoni
assets:
- assets/images/
- assets/audio/bgm/
- assets/audio/sfx/
- assets/images/
flutter_icons:
android: "ic_launcher"
ios: true
macos:
generate: true
image_path: "assets/icon/icona_master.png"
min_sdk_android: 21 # Serve per compatibilità con Android recenti
=== MAC OS CONFIG: Info.plist ===
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LSApplicationCategoryType</key>
<string>public.app-category.puzzle-games</string>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
<string>$(PRODUCT_COPYRIGHT)</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>
=== MAC OS CONFIG: DebugProfile.entitlements ===
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>
=== IOS CONFIG: Info.plist ===
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Tetraq</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>tetraq</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>
=== ANDROID CONFIG: AndroidManifest.xml ===
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:label="tetraq"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="tetraq" android:host="join" />
</intent-filter>
</activity>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
=== ANDROID CONFIG: build.gradle.kts (app) ===
plugins {
id("com.android.application")
// START: FlutterFire Configuration
id("com.google.gms.google-services")
// END: FlutterFire Configuration
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.sanza.tetraq"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.sanza.tetraq"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}
=== SOURCE CODE (lib/) ===
// ===========================================================================
// FILE: lib/core/app_colors.dart
// ===========================================================================
import 'package:flutter/material.dart';
enum AppThemeType { minimal, doodle, cyberpunk, wood }
class ThemeColors {
final Color background;
final Color gridLine;
final Color playerRed;
final Color playerBlue;
final Color text;
const ThemeColors({
required this.background,
required this.gridLine,
required this.playerRed,
required this.playerBlue,
required this.text,
});
}
class AppColors {
static const ThemeColors minimal = ThemeColors(
background: Color(0xFFF5F7FA), gridLine: Color(0xFFCFD8DC),
playerRed: Color(0xFFE53935), playerBlue: Color(0xFF1E88E5), text: Color(0xFF263238),
);
static const ThemeColors doodle = ThemeColors(
background: Color(0xFFFFF9E6), gridLine: Color(0xFFB0BEC5),
playerRed: Color(0xFFD32F2F), playerBlue: Color(0xFF1565C0), text: Color(0xFF37474F),
);
// --- TEMA CYBERPUNK AGGIORNATO ---
static const ThemeColors cyberpunk = ThemeColors(
background: Color(0xFF0A001A), // Sfondo notte profonda
gridLine: Color(0xFF6200EA), // Viola scuro elettrico (non fa confusione con le mosse)
playerRed: Color(0xFFFF007F), // Rosa Neon (invariato)
playerBlue: Color(0xFF69F0AE), // Verde Fluo brillante! (Green Accent)
text: Color(0xFFFFFFFF),
);
// --- TEMA LEGNO POTENZIATO ---
static const ThemeColors wood = ThemeColors(
background: Color(0xFF905D3B), // Marrone caldo e ricco (vero legno)
gridLine: Color(0xFF4A301E), // Marrone scurissimo per i solchi
playerRed: Color(0xFFE53935), // Rosso acceso per i fiammiferi
playerBlue: Color(0xFF29B6F6), // Azzurro acceso per i fiammiferi
text: Color(0xFFFBE9E7), // Panna chiaro per contrastare lo scuro
);
static ThemeColors getTheme(AppThemeType type) {
switch (type) {
case AppThemeType.minimal: return minimal;
case AppThemeType.doodle: return doodle;
case AppThemeType.cyberpunk: return cyberpunk;
case AppThemeType.wood: return wood;
}
}
}
// ===========================================================================
// FILE: lib/core/constants.dart
// ===========================================================================
class Constants {
// Chiavi per salvare i dati sul telefono
static const String prefThemeKey = 'selected_theme';
static const String prefLanguageKey = 'selected_language';
static const String prefBoardSizeKey = 'board_size';
// Impostazioni della scacchiera a rombo - RAGGI INCREMENTATI
static const int minBoardRadius = 2; // Ex Normale, ora è Piccola
static const int maxBoardRadius = 5; // Formato MAX, enorme
static const int defaultBoardRadius = 3; // Ora il default è più grande
}
// ===========================================================================
// FILE: lib/core/theme_manager.dart
// ===========================================================================
import 'package:flutter/material.dart';
import 'app_colors.dart';
import '../services/storage_service.dart';
class ThemeManager extends ChangeNotifier {
late AppThemeType _currentThemeType;
ThemeManager() {
// Quando l'app parte, legge il tema dalla memoria!
_currentThemeType = AppThemeType.values[StorageService.instance.savedThemeIndex];
}
AppThemeType get currentThemeType => _currentThemeType;
ThemeColors get currentColors => AppColors.getTheme(_currentThemeType);
void setTheme(AppThemeType type) {
_currentThemeType = type;
StorageService.instance.saveTheme(type); // Salva la scelta nel "disco fisso"
notifyListeners();
}
}
// ===========================================================================
// FILE: lib/firebase_options.dart
// ===========================================================================
// File generated by FlutterFire CLI.
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for web - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
return macos;
case TargetPlatform.windows:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for windows - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyBsXO595xVITDPrRnXrW8HPQLOe7Rz4Gg4',
appId: '1:705460445314:android:4d35fef29cfd63727b949b',
messagingSenderId: '705460445314',
projectId: 'tetraq-32a4a',
storageBucket: 'tetraq-32a4a.firebasestorage.app',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyB77j18Jgeb9gBAEwp-uyOQvr4m-RJ_rAE',
appId: '1:705460445314:ios:da11cbca5d1f6bc27b949b',
messagingSenderId: '705460445314',
projectId: 'tetraq-32a4a',
storageBucket: 'tetraq-32a4a.firebasestorage.app',
iosBundleId: 'com.sanza.tetraq',
);
static const FirebaseOptions macos = FirebaseOptions(
apiKey: 'AIzaSyB77j18Jgeb9gBAEwp-uyOQvr4m-RJ_rAE',
appId: '1:705460445314:ios:da11cbca5d1f6bc27b949b',
messagingSenderId: '705460445314',
projectId: 'tetraq-32a4a',
storageBucket: 'tetraq-32a4a.firebasestorage.app',
iosBundleId: 'com.sanza.tetraq',
);
}
// ===========================================================================
// FILE: lib/logic/ai_engine.dart
// ===========================================================================
import 'dart:math';
import '../models/game_board.dart';
class AIEngine {
static Line getBestMove(GameBoard board, int level) {
// IL FIX FONDAMENTALE È QUI:
// Filtriamo le linee in modo che la CPU veda SOLO quelle libere E giocabili!
List<Line> availableLines = board.lines.where((l) => l.owner == Player.none && l.isPlayable).toList();
final random = Random();
// Se per qualche assurdo motivo non ci sono mosse, restituisce la prima linea a caso
// (Non dovrebbe mai succedere, ma ci protegge dai crash)
if (availableLines.isEmpty) return board.lines.first;
// --- FORMULA DI DIFFICOLTÀ ---
double smartChance = 0.50 + ((level - 1) * 0.10);
if (smartChance > 1.0) smartChance = 1.0;
bool beSmart = random.nextDouble() < smartChance;
// --- REGOLA 1: Chiudere i quadrati ---
List<Line> closingMoves = [];
for (var line in availableLines) {
if (_willCloseBox(board, line)) {
closingMoves.add(line);
}
}
if (closingMoves.isNotEmpty) {
if (beSmart || random.nextDouble() < 0.70) {
return closingMoves[random.nextInt(closingMoves.length)];
}
}
// --- REGOLA 2: Mosse Sicure ---
List<Line> safeMoves = [];
for (var line in availableLines) {
if (_isSafeMove(board, line)) {
safeMoves.add(line);
}
}
if (safeMoves.isNotEmpty) {
if (beSmart) {
return safeMoves[random.nextInt(safeMoves.length)];
} else {
if (random.nextDouble() < 0.5) {
return safeMoves[random.nextInt(safeMoves.length)];
}
}
}
// --- REGOLA 3: Mossa a caso ---
return availableLines[random.nextInt(availableLines.length)];
}
static bool _willCloseBox(GameBoard board, Line line) {
for (var box in board.boxes) {
// Ignora i box invisibili!
if (box.type == BoxType.invisible) continue;
if (box.top == line || box.bottom == line || box.left == line || box.right == line) {
int linesCount = 0;
if (box.top.owner != Player.none || box.top == line) linesCount++;
if (box.bottom.owner != Player.none || box.bottom == line) linesCount++;
if (box.left.owner != Player.none || box.left == line) linesCount++;
if (box.right.owner != Player.none || box.right == line) linesCount++;
if (linesCount == 4) return true;
}
}
return false;
}
static bool _isSafeMove(GameBoard board, Line line) {
for (var box in board.boxes) {
// Ignora i box invisibili!
if (box.type == BoxType.invisible) continue;
if (box.top == line || box.bottom == line || box.left == line || box.right == line) {
int currentLinesCount = 0;
if (box.top.owner != Player.none) currentLinesCount++;
if (box.bottom.owner != Player.none) currentLinesCount++;
if (box.left.owner != Player.none) currentLinesCount++;
if (box.right.owner != Player.none) currentLinesCount++;
if (currentLinesCount == 2) return false;
}
}
return true;
}
}
// ===========================================================================
// FILE: lib/logic/game_controller.dart
// ===========================================================================
import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../models/game_board.dart';
import 'ai_engine.dart';
import '../services/audio_service.dart';
import '../services/storage_service.dart';
import '../core/app_colors.dart';
class GameController extends ChangeNotifier {
late GameBoard board;
bool isVsCPU = false;
bool isCPUThinking = false;
bool isOnline = false;
String? roomCode;
bool isHost = false;
StreamSubscription<DocumentSnapshot>? _onlineSubscription;
bool opponentLeft = false;
bool _hasSavedResult = false;
Timer? _blitzTimer;
int timeLeft = 15;
final int maxTime = 15;
String effectText = '';
Color effectColor = Colors.transparent;
Timer? _effectTimer;
Player get myPlayer => isHost ? Player.red : Player.blue;
bool get isGameOver => board.isGameOver;
int cpuLevel = 1;
int currentMatchLevel = 1;
int? currentSeed;
AppThemeType _activeTheme = AppThemeType.cyberpunk;
String onlineHostName = "ROSSO";
String onlineGuestName = "BLU";
ArenaShape onlineShape = ArenaShape.classic;
GameController({int radius = 3}) {
cpuLevel = StorageService.instance.cpuLevel;
startNewGame(radius);
}
void startNewGame(int radius, {bool vsCPU = false, bool isOnline = false, String? roomCode, bool isHost = false, ArenaShape shape = ArenaShape.classic}) {
_onlineSubscription?.cancel();
_onlineSubscription = null;
_blitzTimer?.cancel();
_effectTimer?.cancel();
effectText = '';
_hasSavedResult = false;
this.isVsCPU = vsCPU;
this.isOnline = isOnline;
this.roomCode = roomCode;
this.isHost = isHost;
// Salviamo la forma scelta anche se siamo offline
onlineShape = shape;
int levelToUse = isOnline ? (currentMatchLevel == 1 ? 2 : currentMatchLevel) : cpuLevel;
// PASSIAMO LA FORMA AL MOTORE!
board = GameBoard(radius: radius, level: levelToUse, seed: currentSeed, shape: onlineShape);
isCPUThinking = false;
opponentLeft = false;
if (this.isOnline && this.roomCode != null) {
_listenToOnlineGame(this.roomCode!);
}
_startTimer();
notifyListeners();
}
void triggerSpecialEffect(String text, Color color) {
effectText = text;
effectColor = color;
notifyListeners();
_effectTimer?.cancel();
_effectTimer = Timer(const Duration(milliseconds: 1200), () { effectText = ''; notifyListeners(); });
}
void _playEffects(List<Box> newClosed, {required bool isOpponent}) {
if (newClosed.isEmpty) {
AudioService.instance.playLineSfx(_activeTheme);
if (!isOpponent) HapticFeedback.lightImpact();
} else {
bool isGold = newClosed.any((b) => b.type == BoxType.gold);
bool isBomb = newClosed.any((b) => b.type == BoxType.bomb);
if (isGold) {
AudioService.instance.playBonusSfx(); triggerSpecialEffect("+2", Colors.amber); HapticFeedback.heavyImpact();
} else if (isBomb) {
AudioService.instance.playBombSfx(); triggerSpecialEffect("-1", Colors.redAccent); HapticFeedback.heavyImpact();
} else {
AudioService.instance.playBoxSfx(_activeTheme); HapticFeedback.heavyImpact();
}
}
}
// --- FIX TIMER: SCENDE PER ENTRAMBI, MA SCADE SOLO PER CHI HA IL TURNO ---
void _startTimer() {
_blitzTimer?.cancel();
timeLeft = maxTime;
_blitzTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (isGameOver || isCPUThinking) { timer.cancel(); return; }
if (timeLeft > 0) {
timeLeft--;
notifyListeners();
} else {
timer.cancel();
// IL TIMEOUT FORZA LA MOSSA SOLO SE È IL MIO TURNO
// Evita che il telefono in attesa forzi mosse per conto dell'avversario
if (!isOnline || board.currentPlayer == myPlayer) {
_handleTimeOut();
}
}
});
}
void _handleTimeOut() {
if (isOnline) {
Line randomMove = AIEngine.getBestMove(board, 5);
handleLineTap(randomMove, _activeTheme, forced: true);
}
else if (isVsCPU && board.currentPlayer == Player.red) {
Line randomMove = AIEngine.getBestMove(board, cpuLevel);
handleLineTap(randomMove, _activeTheme, forced: true);
}
else if (!isVsCPU) {
Line randomMove = AIEngine.getBestMove(board, 5);
handleLineTap(randomMove, _activeTheme, forced: true);
}
}
void disconnectOnlineGame() {
_onlineSubscription?.cancel();
_onlineSubscription = null;
_blitzTimer?.cancel();
_effectTimer?.cancel();
if (isOnline && roomCode != null) {
FirebaseFirestore.instance.collection('games').doc(roomCode).update({'status': 'abandoned'}).catchError((e) => null);
}
isOnline = false;
roomCode = null;
currentMatchLevel = 1;
currentSeed = null;
}
void requestRematch() {
if (isOnline && roomCode != null) {
currentMatchLevel++;
int newSeed = DateTime.now().millisecondsSinceEpoch % 1000000;
FirebaseFirestore.instance.collection('games').doc(roomCode).update({
'moves': [],
'matchLevel': currentMatchLevel,
'status': 'playing',
'seed': newSeed,
'shape': onlineShape.name
});
}
}
@override
void dispose() { disconnectOnlineGame(); super.dispose(); }
void _listenToOnlineGame(String code) {
_onlineSubscription = FirebaseFirestore.instance.collection('games').doc(code).snapshots().listen((doc) {
if (!doc.exists) return;
var data = doc.data() as Map<String, dynamic>;
onlineHostName = data['hostName'] ?? "ROSSO";
onlineGuestName = (data['guestName'] != null && data['guestName'] != '') ? data['guestName'] : "BLU";
if (data['status'] == 'abandoned' && !board.isGameOver && !opponentLeft) {
opponentLeft = true; notifyListeners(); return;
}
List<dynamic> moves = data['moves'] ?? [];
int hostLevel = data['matchLevel'] ?? 1;
int? hostSeed = data['seed'];
String shapeStr = data['shape'] ?? 'classic';
ArenaShape hostShape = ArenaShape.values.firstWhere((e) => e.name == shapeStr, orElse: () => ArenaShape.classic);
onlineShape = hostShape;
if (hostLevel > currentMatchLevel || (isOnline && currentSeed == null && hostSeed != null) || (hostSeed != null && hostSeed != currentSeed)) {
currentMatchLevel = hostLevel; currentSeed = hostSeed;
int levelToUse = (currentMatchLevel == 1) ? 2 : currentMatchLevel;
board = GameBoard(radius: board.radius, level: levelToUse, seed: currentSeed, shape: onlineShape);
isCPUThinking = false; notifyListeners(); return;
}
int firebaseMovesCount = moves.length;
int localMovesCount = board.lines.where((l) => l.owner != Player.none).length;
if (firebaseMovesCount == 0 && localMovesCount > 0) {
int levelToUse = (currentMatchLevel == 1) ? 2 : currentMatchLevel;
board = GameBoard(radius: board.radius, level: levelToUse, seed: currentSeed, shape: onlineShape);
notifyListeners(); return;
}
if (firebaseMovesCount > localMovesCount) {
bool newMovesApplied = false; // <-- FIX: Ci segniamo se abbiamo ricevuto mosse
for (int i = localMovesCount; i < firebaseMovesCount; i++) {
var m = moves[i]; Line? lineToPlay;
for (var line in board.lines) {
if ((line.p1.x == m['x1'] && line.p1.y == m['y1'] && line.p2.x == m['x2'] && line.p2.y == m['y2']) ||
(line.p1.x == m['x2'] && line.p1.y == m['y2'] && line.p2.x == m['x1'] && line.p2.y == m['y1'])) {
lineToPlay = line; break;
}
}
if (lineToPlay != null && lineToPlay.owner == Player.none) {
Player playerFromFirebase = (m['player'] == 'red') ? Player.red : Player.blue;
bool isOpponentMove = (playerFromFirebase != myPlayer);
List<Box> closedBefore = board.boxes.where((b) => b.owner != Player.none).toList();
board.playMove(lineToPlay, forcedPlayer: playerFromFirebase);
newMovesApplied = true;
List<Box> newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList();
if (isOpponentMove) _playEffects(newClosed, isOpponent: true);
}
}
// --- FIX TIMER: SE L'AVVERSARIO HA MOSSO, RESETTIAMO IL TIMER ---
if (newMovesApplied) {
_startTimer();
}
if (board.isGameOver) _saveMatchResult();
notifyListeners();
}
});
}
void handleLineTap(Line line, AppThemeType theme, {bool forced = false}) {
if ((isCPUThinking || board.isGameOver || opponentLeft) && !forced) return;
if (isOnline && board.currentPlayer != myPlayer && !forced) return;
_activeTheme = theme;
List<Box> closedBefore = board.boxes.where((b) => b.owner != Player.none).toList();
if (board.playMove(line)) {
List<Box> newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList();
if (!forced) _playEffects(newClosed, isOpponent: false);
_startTimer(); notifyListeners();
if (isOnline && roomCode != null) {
Map<String, dynamic> moveData = {
'x1': line.p1.x, 'y1': line.p1.y, 'x2': line.p2.x, 'y2': line.p2.y,
'player': myPlayer == Player.red ? 'red' : 'blue'
};
FirebaseFirestore.instance.collection('games').doc(roomCode).update({
'moves': FieldValue.arrayUnion([moveData])
}).catchError((e) => debugPrint("Errore: $e"));
if (board.isGameOver) _saveMatchResult();
} else {
if (board.isGameOver) _saveMatchResult();
else if (isVsCPU && board.currentPlayer == Player.blue) _checkCPUTurn();
}
}
}
void _checkCPUTurn() async {
if (isVsCPU && board.currentPlayer == Player.blue && !board.isGameOver) {
isCPUThinking = true; _blitzTimer?.cancel(); notifyListeners();
await Future.delayed(const Duration(milliseconds: 600));
if (!board.isGameOver) {
List<Box> closedBefore = board.boxes.where((b) => b.owner != Player.none).toList();
Line bestMove = AIEngine.getBestMove(board, cpuLevel);
board.playMove(bestMove);
List<Box> newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList();
_playEffects(newClosed, isOpponent: true);
isCPUThinking = false; _startTimer(); notifyListeners();
if (board.isGameOver) _saveMatchResult();
else _checkCPUTurn();
}
}
}
void _saveMatchResult() {
if (_hasSavedResult) return;
_hasSavedResult = true;
String myRealName = StorageService.instance.playerName;
if (myRealName.isEmpty) myRealName = "IO";
if (isOnline) {
String oppName = isHost ? onlineGuestName : onlineHostName;
int myScore = isHost ? board.scoreRed : board.scoreBlue;
int oppScore = isHost ? board.scoreBlue : board.scoreRed;
StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: oppName, myScore: myScore, oppScore: oppScore, isOnline: true);
} else if (isVsCPU) {
int myScore = board.scoreRed; int cpuScore = board.scoreBlue;
if (myScore > cpuScore) StorageService.instance.addWin();
else if (cpuScore > myScore) StorageService.instance.addLoss();
StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "CPU (Liv. $cpuLevel)", myScore: myScore, oppScore: cpuScore, isOnline: false);
} else {
StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "Ospite (Locale)", myScore: board.scoreRed, oppScore: board.scoreBlue, isOnline: false);
}
}
void increaseLevelAndRestart() {
cpuLevel++; StorageService.instance.saveCpuLevel(cpuLevel);
startNewGame(board.radius, vsCPU: true, shape: board.shape);
}
}
// ===========================================================================
// FILE: lib/main.dart
// ===========================================================================
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'core/theme_manager.dart';
import 'logic/game_controller.dart';
import 'ui/home/home_screen.dart';
import 'services/storage_service.dart'; // <-- Importiamo il servizio
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
void main() async {
// Assicuriamoci che i motori di Flutter siano pronti
WidgetsFlutterBinding.ensureInitialized();
// 1. Accendiamo Firebase! (Questo ti era sfuggito)
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
// 2. Accendiamo la Memoria Locale!
await StorageService.instance.init();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => ThemeManager()),
ChangeNotifierProvider(create: (_) => GameController()),
],
child: const TetraQApp(),
),
);
}
class TetraQApp extends StatelessWidget {
const TetraQApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'TetraQ',
debugShowCheckedModeBanner: false,
theme: ThemeData(
fontFamily: 'Roboto',
useMaterial3: true,
),
home: const HomeScreen(),
);
}
}
// ===========================================================================
// FILE: lib/models/game_board.dart
// ===========================================================================
import 'dart:math';
enum Player { red, blue, none }
enum BoxType { normal, gold, bomb, invisible }
enum ArenaShape { classic, cross, donut, hourglass }
class Dot {
final int x;
final int y;
Dot(this.x, this.y);
@override
bool operator ==(Object other) => identical(this, other) || other is Dot && runtimeType == other.runtimeType && x == other.x && y == other.y;
@override
int get hashCode => x.hashCode ^ y.hashCode;
}
class Line {
final Dot p1;
final Dot p2;
Player owner = Player.none;
bool isPlayable = false;
Line(this.p1, this.p2);
bool connects(Dot a, Dot b) { return (p1 == a && p2 == b) || (p1 == b && p2 == a); }
}
class Box {
final int x;
final int y;
Player owner = Player.none;
late Line top, bottom, left, right;
BoxType type = BoxType.normal;
Box(this.x, this.y);
bool isClosed() {
if (type == BoxType.invisible) return false;
return top.owner != Player.none && bottom.owner != Player.none && left.owner != Player.none && right.owner != Player.none;
}
int get value {
if (type == BoxType.gold) return 2;
if (type == BoxType.bomb) return -1;
return 1;
}
}
class GameBoard {
final int radius;
final int level;
final int? seed;
final ArenaShape shape;
List<Dot> dots = [];
List<Line> lines = [];
List<Box> boxes = [];
Player currentPlayer = Player.red;
int scoreRed = 0;
int scoreBlue = 0;
bool isGameOver = false;
GameBoard({required this.radius, this.level = 1, this.seed, this.shape = ArenaShape.classic}) {
_generateBoard();
}
void _generateBoard() {
int size = radius * 2 + 1;
final random = seed != null ? Random(seed) : Random();
dots.clear();
lines.clear();
boxes.clear();
for (int y = 0; y < size; y++) {
for (int x = 0; x < size; x++) {
var box = Box(x, y);
// Distanze dal centro della scacchiera
int dx = (x - radius).abs();
int dy = (y - radius).abs();
// 1. Controllo base: Deve appartenere al Rombo primario
bool isVisible = (dx + dy) <= radius;
// 2. LOGICA FORME (Scolpiamo i blocchi dall'interno)
if (isVisible) {
switch (shape) {
case ArenaShape.classic:
break; // Nessun taglio
case ArenaShape.cross:
// Per fare una croce *all'interno* di un rombo, dobbiamo svuotare
// 4 "triangolini" vicini agli assi mediani.
// La logica è: se non sei sull'asse centrale verticale o orizzontale, sparisci.
// Aumentiamo lo spessore del braccio se il raggio è grande.
int spessoreBraccio = radius > 3 ? 1 : 0;
if (dx > spessoreBraccio && dy > spessoreBraccio) {
isVisible = false;
}
break;
case ArenaShape.donut:
// Il Buco Nero (Donut). Svuotiamo il nucleo centrale.
int dimensioneBuco = radius > 3 ? 2 : 1;
if ((dx + dy) <= dimensioneBuco) {
isVisible = false;
}
break;
case ArenaShape.hourglass:
// La Clessidra. Svuotiamo i due lobi laterali (sinistro e destro).
// Vogliamo mantenere solo l'alto e il basso.
if (dx > dy) {
isVisible = false;
}
// Manteniamo una singola scatola di giunzione al centro esatto
if (x == radius && y == radius) {
isVisible = true;
}
break;
}
}
if (!isVisible) {
box.type = BoxType.invisible;
} else if (level > 1) {
double chance = random.nextDouble();
if (chance < 0.10) { box.type = BoxType.gold; }
else if (chance > 0.90) { box.type = BoxType.bomb; }
}
boxes.add(box);
}
}
// --- PULIZIA DEFINITIVA: ASSEGNAZIONE DELLE LINEE ---
for (var box in boxes) {
Dot tl = _getOrAddDot(box.x, box.y);
Dot tr = _getOrAddDot(box.x + 1, box.y);
Dot bl = _getOrAddDot(box.x, box.y + 1);
Dot br = _getOrAddDot(box.x + 1, box.y + 1);
box.top = _getOrAddLine(tl, tr);
box.bottom = _getOrAddLine(bl, br);
box.left = _getOrAddLine(tl, bl);
box.right = _getOrAddLine(tr, br);
// Le linee sono "giocabili" SOLO se appartengono a una scatola VISIBILE
if (box.type != BoxType.invisible) {
box.top.isPlayable = true;
box.bottom.isPlayable = true;
box.left.isPlayable = true;
box.right.isPlayable = true;
}
}
}
Dot _getOrAddDot(int x, int y) {
for (var dot in dots) { if (dot.x == x && dot.y == y) return dot; }
var newDot = Dot(x, y);
dots.add(newDot);
return newDot;
}
Line _getOrAddLine(Dot a, Dot b) {
for (var line in lines) { if (line.connects(a, b)) return line; }
var newLine = Line(a, b);
lines.add(newLine);
return newLine;
}
bool playMove(Line lineToPlay, {Player? forcedPlayer}) {
if (isGameOver) return false;
Player playerMakingMove = forcedPlayer ?? currentPlayer;
Line? actualLine;
for (var l in lines) {
if (l.connects(lineToPlay.p1, lineToPlay.p2)) {
actualLine = l; break;
}
}
if (actualLine == null || actualLine.owner != Player.none || !actualLine.isPlayable) return false;
actualLine.owner = playerMakingMove;
bool boxedClosed = false;
for (var box in boxes) {
if (box.owner == Player.none && box.isClosed()) {
box.owner = playerMakingMove;
boxedClosed = true;
if (playerMakingMove == Player.red) { scoreRed += box.value; }
else { scoreBlue += box.value; }
}
}
if (lines.where((l) => l.isPlayable).every((l) => l.owner != Player.none)) { isGameOver = true; }
if (forcedPlayer == null) {
if (!boxedClosed && !isGameOver) { currentPlayer = (currentPlayer == Player.red) ? Player.blue : Player.red; }
} else {
if (!boxedClosed && !isGameOver) { currentPlayer = (forcedPlayer == Player.red) ? Player.blue : Player.red; }
else { currentPlayer = forcedPlayer; }
}
return true;
}
}
// ===========================================================================
// FILE: lib/models/player_info.dart
// ===========================================================================
// ===========================================================================
// FILE: lib/services/audio_service.dart
// ===========================================================================
import 'package:flutter/material.dart';
import 'package:audioplayers/audioplayers.dart';
import '../core/app_colors.dart';
class AudioService extends ChangeNotifier {
static final AudioService instance = AudioService._internal();
AudioService._internal();
bool isMuted = false;
final AudioPlayer _sfxPlayer = AudioPlayer();
void toggleMute() {
isMuted = !isMuted;
notifyListeners();
}
void playLineSfx(AppThemeType theme) async {
if (isMuted) return;
String file = '';
switch (theme) {
case AppThemeType.minimal: file = 'minimal_line.wav'; break;
case AppThemeType.doodle:
case AppThemeType.wood:
file = 'doodle_line.wav'; break;
case AppThemeType.cyberpunk: file = 'cyber_line.wav'; break;
}
await _sfxPlayer.play(AssetSource('audio/sfx/$file'));
}
void playBoxSfx(AppThemeType theme) async {
if (isMuted) return;
String file = '';
switch (theme) {
case AppThemeType.minimal: file = 'minimal_box.wav'; break;
case AppThemeType.doodle:
case AppThemeType.wood:
file = 'doodle_box.wav'; break;
case AppThemeType.cyberpunk: file = 'cyber_box.wav'; break;
}
await _sfxPlayer.play(AssetSource('audio/sfx/$file'));
}
// --- NUOVI EFFETTI SPECIALI ---
void playBonusSfx() async {
if (isMuted) return;
// Assicurati di aggiungere questo file nella cartella assets/audio/sfx/
await _sfxPlayer.play(AssetSource('audio/sfx/bonus.wav'));
}
void playBombSfx() async {
if (isMuted) return;
// Assicurati di aggiungere questo file nella cartella assets/audio/sfx/
await _sfxPlayer.play(AssetSource('audio/sfx/bomb.wav'));
}
}
// ===========================================================================
// FILE: lib/services/firebase_service.dart
// ===========================================================================
// ===========================================================================
// FILE: lib/services/multiplayer_service.dart
// ===========================================================================
import 'dart:math';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:share_plus/share_plus.dart';
class MultiplayerService {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
CollectionReference get _gamesCollection => _firestore.collection('games');
// AGGIUNTO shapeName
Future<String> createGameRoom(int boardRadius, String hostName, String shapeName) async {
String roomCode = _generateRoomCode();
int randomSeed = Random().nextInt(1000000);
await _gamesCollection.doc(roomCode).set({
'status': 'waiting',
'radius': boardRadius,
'createdAt': FieldValue.serverTimestamp(),
'players': ['host'],
'turn': 'host',
'moves': [],
'seed': randomSeed,
'hostName': hostName,
'guestName': '',
'shape': shapeName, // <-- SALVIAMO LA FORMA DELL'ARENA SU FIREBASE
});
return roomCode;
}
Future<Map<String, dynamic>?> joinGameRoom(String roomCode, String guestName) async {
DocumentSnapshot doc = await _gamesCollection.doc(roomCode).get();
if (doc.exists && doc['status'] == 'waiting') {
await _gamesCollection.doc(roomCode).update({
'status': 'playing',
'players': FieldValue.arrayUnion(['guest']),
'guestName': guestName,
});
return doc.data() as Map<String, dynamic>;
}
return null;
}
void shareInviteLink(String roomCode) {
String message = "Ehi! Giochiamo a TetraQ? 🎮\nCopia questo intero messaggio e apri l'app per entrare direttamente, oppure inserisci manualmente il codice: $roomCode";
Share.share(message);
}
Stream<DocumentSnapshot> listenToRoom(String roomCode) {
return _gamesCollection.doc(roomCode).snapshots();
}
String _generateRoomCode() {
const chars = 'ACDEFGHJKLMNPQRSTUVWXYZ2345679';
final random = Random();
return String.fromCharCodes(Iterable.generate(
5, (_) => chars.codeUnitAt(random.nextInt(chars.length)),
));
}
}
// ===========================================================================
// FILE: lib/services/storage_service.dart
// ===========================================================================
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import '../core/app_colors.dart';
class StorageService {
static final StorageService instance = StorageService._internal();
StorageService._internal();
late SharedPreferences _prefs;
// Si avvia quando apriamo l'app
Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
}
// --- IMPOSTAZIONI ---
int get savedThemeIndex => _prefs.getInt('theme') ?? AppThemeType.cyberpunk.index;
Future<void> saveTheme(AppThemeType theme) async => await _prefs.setInt('theme', theme.index);
int get savedRadius => _prefs.getInt('radius') ?? 2;
Future<void> saveRadius(int radius) async => await _prefs.setInt('radius', radius);
bool get isMuted => _prefs.getBool('isMuted') ?? false;
Future<void> saveMuted(bool muted) async => await _prefs.setBool('isMuted', muted);
// --- STATISTICHE VS CPU ---
int get wins => _prefs.getInt('wins') ?? 0;
Future<void> addWin() async => await _prefs.setInt('wins', wins + 1);
int get losses => _prefs.getInt('losses') ?? 0;
Future<void> addLoss() async => await _prefs.setInt('losses', losses + 1);
int get cpuLevel => _prefs.getInt('cpuLevel') ?? 1;
Future<void> saveCpuLevel(int level) async => await _prefs.setInt('cpuLevel', level);
// --- MULTIPLAYER ---
String get playerName => _prefs.getString('playerName') ?? '';
Future<void> savePlayerName(String name) async => await _prefs.setString('playerName', name);
// --- STORICO PARTITE ---
List<Map<String, dynamic>> get matchHistory {
List<String> history = _prefs.getStringList('matchHistory') ?? [];
return history.map((e) => jsonDecode(e) as Map<String, dynamic>).toList();
}
// Salviamo sia il nostro nome che quello dell'avversario
Future<void> saveMatchToHistory({required String myName, required String opponent, required int myScore, required int oppScore, required bool isOnline}) async {
List<String> history = _prefs.getStringList('matchHistory') ?? [];
Map<String, dynamic> match = {
'date': DateTime.now().toIso8601String(),
'myName': myName,
'opponent': opponent,
'myScore': myScore,
'oppScore': oppScore,
'isOnline': isOnline,
};
// Aggiungiamo in cima (il più recente per primo)
history.insert(0, jsonEncode(match));
// Teniamo solo le ultime 50 partite per non intasare la memoria
if (history.length > 50) {
history = history.sublist(0, 50);
}
await _prefs.setStringList('matchHistory', history);
}
}
// ===========================================================================
// FILE: lib/ui/game/board_painter.dart
// ===========================================================================
import 'dart:math';
import 'package:flutter/material.dart';
import '../../models/game_board.dart';
import '../../core/app_colors.dart';
class BoardPainter extends CustomPainter {
final GameBoard board;
final ThemeColors theme;
final AppThemeType themeType;
BoardPainter({required this.board, required this.theme, required this.themeType});
@override
void paint(Canvas canvas, Size size) {
if (themeType == AppThemeType.doodle) {
final Paint paperGridPaint = Paint()
..color = Colors.grey.withOpacity(0.3)
..strokeWidth = 1.0
..style = PaintingStyle.stroke;
double paperStep = 20.0;
for (double i = 0; i <= size.width; i += paperStep) {
canvas.drawLine(Offset(i, 0), Offset(i, size.height), paperGridPaint);
}
for (double i = 0; i <= size.height; i += paperStep) {
canvas.drawLine(Offset(0, i), Offset(size.width, i), paperGridPaint);
}
}
int gridPoints = board.radius * 2 + 2;
double spacing = size.width / gridPoints;
double offset = spacing / 2;
Offset getScreenPos(int x, int y) => Offset(x * spacing + offset, y * spacing + offset);
// --- 1. DISEGNO AREE CONQUISTATE E ICONE ---
for (var box in board.boxes) {
// Ignoriamo totalmente le scatole invisibili (tagliate via dalle nuove forme)
if (box.type == BoxType.invisible) continue;
Offset p1 = getScreenPos(box.x, box.y);
Offset p2 = getScreenPos(box.x + 1, box.y + 1);
Rect rect = Rect.fromPoints(p1, p2);
if (box.owner != Player.none) {
final boxPaint = Paint()
..style = PaintingStyle.fill
..color = box.owner == Player.red ? theme.playerRed.withOpacity(0.6) : theme.playerBlue.withOpacity(0.6);
if (themeType == AppThemeType.wood) {
_drawFlameBox(canvas, rect, box.owner == Player.red);
} else if (themeType == AppThemeType.doodle) {
Color penColor = box.owner == Player.red ? Colors.redAccent.shade700 : Colors.blueAccent.shade700;
_drawScribbleBox(canvas, rect, penColor);
} else {
canvas.drawRect(rect, boxPaint);
}
}
if (box.type == BoxType.gold) {
_drawIconInBox(canvas, rect, Icons.star_rounded, Colors.amber);
} else if (box.type == BoxType.bomb) {
_drawIconInBox(canvas, rect, Icons.mood_bad_rounded, themeType == AppThemeType.cyberpunk ? Colors.greenAccent : Colors.deepPurple);
}
}
// --- 2. DISEGNO LINEE ---
for (var line in board.lines) {
// FIX FONDAMENTALE PER LE FORME: Non disegnare linee non giocabili!
if (!line.isPlayable) continue;
Offset p1 = getScreenPos(line.p1.x, line.p1.y);
Offset p2 = getScreenPos(line.p2.x, line.p2.y);
Color lineColor = line.owner == Player.none
? theme.gridLine.withOpacity(0.4)
: (line.owner == Player.red ? theme.playerRed : theme.playerBlue);
if (themeType == AppThemeType.wood) {
if (line.owner == Player.none) {
canvas.drawLine(p1, p2, Paint()..color = const Color(0xFF3E2723).withOpacity(0.3)..strokeWidth = 4.5..strokeCap = StrokeCap.round);
} else {
_drawRealisticMatch(canvas, p1, p2, lineColor);
}
} else if (themeType == AppThemeType.cyberpunk) {
_drawNeonLine(canvas, p1, p2, lineColor, line.owner != Player.none);
} else if (themeType == AppThemeType.doodle) {
Color doodleColor = line.owner == Player.none ? Colors.black.withOpacity(0.05) : lineColor;
_drawWobblyLine(canvas, p1, p2, doodleColor, line.owner != Player.none);
} else {
canvas.drawLine(p1, p2, Paint()..color = lineColor..strokeWidth = 6.0..strokeCap = StrokeCap.round);
}
}
// --- 3. DISEGNO PUNTINI (SOLO QUELLI UTILI) ---
final dotPaint = Paint()..style = PaintingStyle.fill;
// Raccogliamo solo i puntini che appartengono a linee giocabili
Set<Dot> activeDots = {};
for (var line in board.lines) {
if (line.isPlayable) {
activeDots.add(line.p1);
activeDots.add(line.p2);
}
}
for (var dot in activeDots) {
Offset pos = getScreenPos(dot.x, dot.y);
if (themeType == AppThemeType.wood) {
canvas.drawCircle(pos, 3.5, dotPaint..color = const Color(0xFF3E2723).withOpacity(0.2));
} else if (themeType == AppThemeType.cyberpunk) {
canvas.drawCircle(pos, 6.0, Paint()..color = theme.gridLine.withOpacity(0.3));
canvas.drawCircle(pos, 3.0, Paint()..color = Colors.white.withOpacity(0.5));
} else if (themeType == AppThemeType.doodle) {
canvas.drawRect(Rect.fromCenter(center: pos, width: 4, height: 4), dotPaint..color = Colors.black.withOpacity(0.25));
} else {
canvas.drawCircle(pos, 5.0, dotPaint..color = theme.text.withOpacity(0.6));
}
}
}
void _drawIconInBox(Canvas canvas, Rect rect, IconData icon, Color color) {
TextPainter textPainter = TextPainter(textDirection: TextDirection.ltr);
textPainter.text = TextSpan(
text: String.fromCharCode(icon.codePoint),
style: TextStyle(
color: color.withOpacity(0.7),
fontSize: rect.width * 0.45,
fontFamily: icon.fontFamily,
package: icon.fontPackage,
shadows: [Shadow(color: color.withOpacity(0.6), blurRadius: 10, offset: const Offset(0, 0))]
),
);
textPainter.layout();
textPainter.paint(canvas, Offset(rect.center.dx - textPainter.width / 2, rect.center.dy - textPainter.height / 2));
}
void _drawFlameBox(Canvas canvas, Rect baseRect, bool isRed) {
final rand = Random((baseRect.left + baseRect.top).toInt());
Offset center = baseRect.center;
double w = baseRect.width * 0.35;
double h = baseRect.height * 0.55;
Offset bottomCenter = Offset(center.dx, center.dy + h * 0.5);
Color outerColor = isRed ? Colors.red.shade600.withOpacity(0.85) : Colors.blue.shade700.withOpacity(0.85);
Color midColor = isRed ? Colors.orangeAccent : Colors.lightBlueAccent;
Color coreColor = isRed ? Colors.yellowAccent : Colors.white;
canvas.drawOval(
Rect.fromCenter(center: bottomCenter, width: w * 1.5, height: w * 0.5),
Paint()
..color = Colors.black.withOpacity(0.4)
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4.0),
);
void drawFlameLayer(double scale, Color color, double tipOffsetX) {
Path path = Path();
double fw = w * scale;
double fh = h * scale;
path.moveTo(bottomCenter.dx, bottomCenter.dy);
path.cubicTo(
bottomCenter.dx + fw, bottomCenter.dy,
bottomCenter.dx + fw * 0.8, bottomCenter.dy - fh * 0.6,
bottomCenter.dx + tipOffsetX, bottomCenter.dy - fh,
);
path.cubicTo(
bottomCenter.dx - fw * 0.8, bottomCenter.dy - fh * 0.6,
bottomCenter.dx - fw, bottomCenter.dy,
bottomCenter.dx, bottomCenter.dy,
);
canvas.drawPath(
path,
Paint()
..color = color
..style = PaintingStyle.fill
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 1.5),
);
}
double randomTipX = (rand.nextDouble() - 0.5) * w * 0.8;
drawFlameLayer(1.0, outerColor, randomTipX);
drawFlameLayer(0.65, midColor.withOpacity(0.9), randomTipX * 0.6);
drawFlameLayer(0.35, coreColor.withOpacity(0.9), randomTipX * 0.2);
}
void _drawScribbleBox(Canvas canvas, Rect baseRect, Color color) {
final rand = Random((baseRect.left + baseRect.top).toInt());
final paint = Paint()
..color = color.withOpacity(0.85)
..style = PaintingStyle.stroke
..strokeWidth = 3.5
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round;
final path = Path();
Rect rect = baseRect.deflate(4.0);
int numZigs = 15 + rand.nextInt(6);
double stepY = rect.height / numZigs;
path.moveTo(rect.left + rand.nextDouble() * 5, rect.top + rand.nextDouble() * 5);
for (int i = 1; i <= numZigs; i++) {
double targetX = (i % 2 != 0) ? rect.right + (rand.nextDouble() * 4 - 2) : rect.left + (rand.nextDouble() * 4 - 2);
double targetY = rect.top + stepY * i + (rand.nextDouble() - 0.5) * 3;
double ctrlX = rect.center.dx + (rand.nextDouble() - 0.5) * 20;
double ctrlY = targetY - stepY / 2;
path.quadraticBezierTo(ctrlX, ctrlY, targetX, targetY);
}
canvas.drawPath(path, paint);
}
void _drawRealisticMatch(Canvas canvas, Offset p1, Offset p2, Color headColor) {
int seed = (p1.dx * 1000 + p1.dy).toInt();
Random rand = Random(seed);
Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy).normalized();
double shrink = 8.0;
Offset start = Offset(p1.dx + dir.x * shrink, p1.dy + dir.y * shrink);
Offset end = Offset(p2.dx - dir.x * shrink, p2.dy - dir.y * shrink);
start += Offset(rand.nextDouble() * 4 - 2, rand.nextDouble() * 4 - 2);
end += Offset(rand.nextDouble() * 4 - 2, rand.nextDouble() * 4 - 2);
bool headAtEnd = rand.nextBool();
Offset headPos = headAtEnd ? end : start;
Offset tailPos = headAtEnd ? start : end;
Vector2 matchDir = Vector2(headPos.dx - tailPos.dx, headPos.dy - tailPos.dy).normalized();
canvas.drawLine(tailPos + const Offset(4, 4), headPos + const Offset(4, 4), Paint()..color = Colors.black.withOpacity(0.6)..strokeWidth = 7.0..strokeCap = StrokeCap.round);
canvas.drawLine(tailPos, headPos, Paint()..color = const Color(0xFF6D4C41)..strokeWidth = 7.0..strokeCap = StrokeCap.round);
canvas.drawLine(tailPos, headPos, Paint()..color = const Color(0xFFEDC498)..strokeWidth = 4.0..strokeCap = StrokeCap.round);
Offset burnPos = Offset(headPos.dx - matchDir.x * 8, headPos.dy - matchDir.y * 8);
canvas.drawLine(burnPos, headPos, Paint()..color = const Color(0xFF2E1A14)..strokeWidth = 6.0..strokeCap = StrokeCap.round);
canvas.save();
canvas.translate(headPos.dx, headPos.dy);
double angle = atan2(matchDir.y, matchDir.x);
canvas.rotate(angle);
Rect headOval = Rect.fromCenter(center: Offset.zero, width: 18.0, height: 13.0);
canvas.drawOval(headOval.shift(const Offset(1, 2)), Paint()..color = Colors.black.withOpacity(0.6));
canvas.drawOval(headOval, Paint()..color = headColor);
canvas.restore();
}
void _drawNeonLine(Canvas canvas, Offset p1, Offset p2, Color color, bool isConquered) {
double mainWidth = isConquered ? 6.0 : 3.0;
canvas.drawLine(p1, p2, Paint()
..color = color.withOpacity(isConquered ? 0.4 : 0.2)
..strokeWidth = mainWidth * 4
..strokeCap = StrokeCap.round
..maskFilter = MaskFilter.blur(BlurStyle.normal, isConquered ? 12.0 : 6.0)
);
if (isConquered) {
canvas.drawLine(p1, p2, Paint()
..color = color.withOpacity(0.7)
..strokeWidth = mainWidth * 2
..strokeCap = StrokeCap.round
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6.0)
);
}
canvas.drawLine(p1, p2, Paint()
..color = isConquered ? Colors.white.withOpacity(0.9) : color.withOpacity(0.6)
..strokeWidth = mainWidth
..strokeCap = StrokeCap.round
);
}
void _drawWobblyLine(Canvas canvas, Offset p1, Offset p2, Color color, bool isConquered) {
final random = Random((p1.dx + p1.dy + p2.dx + p2.dy).toInt());
final dx = p2.dx - p1.dx;
final dy = p2.dy - p1.dy;
double strokeW = isConquered ? 4.5 : 2.0;
final basePaint = Paint()
..color = color
..strokeWidth = strokeW
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
final mid1 = Offset(p1.dx + dx / 2 + (random.nextDouble() - 0.5) * 8, p1.dy + dy / 2 + (random.nextDouble() - 0.5) * 8);
canvas.drawPath(Path()..moveTo(p1.dx, p1.dy)..quadraticBezierTo(mid1.dx, mid1.dy, p2.dx, p2.dy), basePaint);
final mid2 = Offset(p1.dx + dx / 2 + (random.nextDouble() - 0.5) * 6, p1.dy + dy / 2 + (random.nextDouble() - 0.5) * 6);
canvas.drawPath(Path()..moveTo(p1.dx, p1.dy)..quadraticBezierTo(mid2.dx, mid2.dy, p2.dx, p2.dy), basePaint..strokeWidth = strokeW * 0.5..color = color.withOpacity(0.8));
}
@override
bool shouldRepaint(covariant BoardPainter oldDelegate) => true;
}
class Vector2 {
final double x, y;
Vector2(this.x, this.y);
double get length => sqrt(x * x + y * y);
Vector2 normalized() {
double l = length;
return l == 0 ? Vector2(0, 0) : Vector2(x / l, y / l);
}
}
// ===========================================================================
// FILE: lib/ui/game/game_screen.dart
// ===========================================================================
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../logic/game_controller.dart';
import '../../core/theme_manager.dart';
import '../../core/app_colors.dart';
import 'board_painter.dart';
import 'score_board.dart';
import '../../widgets/game_over_dialog.dart';
import '../../models/game_board.dart';
class GameScreen extends StatelessWidget {
const GameScreen({super.key});
@override
Widget build(BuildContext context) {
final themeManager = context.watch<ThemeManager>();
final themeType = themeManager.currentThemeType;
final theme = themeManager.currentColors;
final gameController = context.watch<GameController>();
// --- CONTROLLO EVENTI DI FINE PARTITA O ABBANDONO ---
WidgetsBinding.instance.addPostFrameCallback((_) {
if (gameController.opponentLeft) {
showDialog(
barrierDismissible: false,
context: context,
builder: (dialogContext) => AlertDialog(
backgroundColor: theme.background,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: Text("VITTORIA A TAVOLINO!", textAlign: TextAlign.center, style: TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold)),
content: Text("L'avversario ha abbandonato la stanza.\nSei il vincitore incontestato!", textAlign: TextAlign.center, style: TextStyle(color: theme.text, fontSize: 16)),
actionsAlignment: MainAxisAlignment.center,
actions: [
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: theme.playerBlue,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))
),
onPressed: () {
gameController.disconnectOnlineGame();
Navigator.pop(dialogContext);
Navigator.pop(context);
},
child: const Text("MENU PRINCIPALE", style: TextStyle(fontWeight: FontWeight.bold)),
)
],
)
);
} else if (gameController.board.isGameOver) {
showDialog(barrierDismissible: false, context: context, builder: (_) => const GameOverDialog());
}
});
String? bgImage;
if (themeType == AppThemeType.wood) bgImage = 'assets/images/wood_bg.jpg';
if (themeType == AppThemeType.doodle) bgImage = 'assets/images/doodle_bg.jpg';
Color indicatorColor = themeType == AppThemeType.cyberpunk ? Colors.white : Colors.black;
// --- IL CONTENUTO PRINCIPALE (Plancia e UI) ---
Widget gameContent = SafeArea(
child: Column(
children: [
const ScoreBoard(),
Expanded(
child: Center(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: AspectRatio(
aspectRatio: 1,
child: LayoutBuilder(
builder: (context, constraints) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTapDown: (details) => _handleTap(details.localPosition, constraints.maxWidth, gameController, themeType),
child: CustomPaint(
size: Size(constraints.maxWidth, constraints.maxHeight),
painter: BoardPainter(
board: gameController.board,
theme: theme,
themeType: themeType,
),
),
);
}
),
),
),
),
),
// --- ZONA INFERIORE: INDICATORE A SX, PULSANTE ESCI A DX ---
Padding(
padding: const EdgeInsets.only(bottom: 20.0, left: 20.0, right: 20.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (gameController.isVsCPU)
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: indicatorColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: indicatorColor.withOpacity(0.3)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.smart_toy_rounded, size: 16, color: indicatorColor),
const SizedBox(width: 8),
Text(
"LIVELLO CPU: ${gameController.cpuLevel}",
style: TextStyle(
color: indicatorColor,
fontWeight: FontWeight.bold,
fontSize: 13,
letterSpacing: 1.0,
),
),
],
),
)
else
const SizedBox(),
// --- PULSANTE ESCI ---
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.4),
offset: const Offset(0, 4),
blurRadius: 5,
),
],
),
child: TextButton.icon(
style: TextButton.styleFrom(
backgroundColor: bgImage != null ? Colors.black87 : theme.background,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(color: Colors.white.withOpacity(0.1), width: 1),
)
),
icon: Icon(Icons.exit_to_app, color: bgImage != null ? Colors.white : theme.text, size: 20),
onPressed: () {
gameController.disconnectOnlineGame();
Navigator.pop(context);
},
label: Text("ESCI", style: TextStyle(color: bgImage != null ? Colors.white : theme.text, fontWeight: FontWeight.bold, fontSize: 14)),
),
),
],
),
)
],
),
);
// --- STRUTTURA A LIVELLI (STACK) DELLO SCHERMO ---
return PopScope(
canPop: true,
onPopInvoked: (didPop) {
gameController.disconnectOnlineGame();
},
child: Scaffold(
backgroundColor: bgImage != null ? Colors.transparent : theme.background,
body: CustomPaint(
painter: themeType == AppThemeType.minimal
? FullScreenGridPainter(Colors.black.withOpacity(0.06))
: null,
child: Container(
decoration: bgImage != null
? BoxDecoration(
image: DecorationImage(
image: AssetImage(bgImage),
fit: BoxFit.cover,
colorFilter: themeType == AppThemeType.doodle
? ColorFilter.mode(Colors.white.withOpacity(0.7), BlendMode.lighten)
: null,
),
)
: null,
// ==========================================
// LA MAGIA DELLO SFARFALLIO AVVIENE QUI
// ==========================================
child: Stack(
children: [
// EFFETTO 1: TIMER LAMPEGGIANTE (Se restano 5 secondi)
if (!gameController.isCPUThinking && !gameController.isGameOver && gameController.timeLeft > 0 && gameController.timeLeft <= 5)
Positioned.fill(
child: BlitzBackgroundEffect(
timeLeft: gameController.timeLeft,
color: theme.playerRed, // Rosso Allarme!
),
),
// --- NUOVO EFFETTO 2: POPUP ORO (+2) E BOMBA (-1) ---
if (gameController.effectText.isNotEmpty)
Positioned.fill(
child: SpecialEventBackgroundEffect(
text: gameController.effectText,
color: gameController.effectColor,
),
),
// LA PLANCIA DI GIOCO SOPRA TUTTI GLI EFFETTI
Positioned.fill(
child: gameContent,
),
],
),
),
),
),
);
}
// ==========================================
// NUOVA GESTIONE DEI TOCCHI E DELLE DISTANZE
// ==========================================
void _handleTap(Offset tapPos, double size, GameController controller, AppThemeType themeType) {
final board = controller.board;
if (board.isGameOver) return;
int gridPoints = board.radius * 2 + 2;
double spacing = size / gridPoints;
double offset = spacing / 2;
Line? closestLine;
double minDistance = double.infinity;
double maxTouchDistance = spacing * 0.4;
// Scorriamo TUTTE le linee valide della plancia
for (var line in board.lines) {
// FIX FONDAMENTALE: Ignoriamo le linee invisibili o già prese!
if (line.owner != Player.none || !line.isPlayable) continue;
Offset screenP1 = Offset(line.p1.x * spacing + offset, line.p1.y * spacing + offset);
Offset screenP2 = Offset(line.p2.x * spacing + offset, line.p2.y * spacing + offset);
double dist = _distanceToSegment(tapPos, screenP1, screenP2);
if (dist < minDistance && dist < maxTouchDistance) {
minDistance = dist;
closestLine = line;
}
}
if (closestLine != null) {
controller.handleLineTap(closestLine, themeType);
}
}
// Calcolo della distanza su coordinate schermo reali
double _distanceToSegment(Offset p, Offset a, Offset b) {
double l2 = (a.dx - b.dx) * (a.dx - b.dx) + (a.dy - b.dy) * (a.dy - b.dy);
if (l2 == 0) return (p - a).distance;
double t = (((p.dx - a.dx) * (b.dx - a.dx) + (p.dy - a.dy) * (b.dy - a.dy)) / l2).clamp(0.0, 1.0);
Offset projection = Offset(a.dx + t * (b.dx - a.dx), a.dy + t * (b.dy - a.dy));
return (p - projection).distance;
}
}
// --- CLASSE PER LO SFONDO A QUADRETTI ---
class FullScreenGridPainter extends CustomPainter {
final Color gridColor;
FullScreenGridPainter(this.gridColor);
@override
void paint(Canvas canvas, Size size) {
final Paint paperGridPaint = Paint()
..color = gridColor
..strokeWidth = 1.0
..style = PaintingStyle.stroke;
double paperStep = 20.0;
for (double i = 0; i <= size.width; i += paperStep) {
canvas.drawLine(Offset(i, 0), Offset(i, size.height), paperGridPaint);
}
for (double i = 0; i <= size.height; i += paperStep) {
canvas.drawLine(Offset(0, i), Offset(size.width, i), paperGridPaint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
// ==============================================================
// WIDGET ANIMATO PER IL TIMER (BATTITO CARDIACO / ALLARME)
// ==============================================================
class BlitzBackgroundEffect extends StatefulWidget {
final int timeLeft;
final Color color;
const BlitzBackgroundEffect({super.key, required this.timeLeft, required this.color});
@override
State<BlitzBackgroundEffect> createState() => _BlitzBackgroundEffectState();
}
class _BlitzBackgroundEffectState extends State<BlitzBackgroundEffect> with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 400))..repeat(reverse: true);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Container(
color: widget.color.withOpacity(0.12 * _controller.value),
child: Center(
child: ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: 2.0, sigmaY: 2.0),
child: Text(
'${widget.timeLeft}',
style: TextStyle(
fontSize: 300,
fontWeight: FontWeight.w900,
color: widget.color.withOpacity(0.35 + (0.3 * _controller.value)),
height: 1.0,
),
),
),
),
);
},
);
}
}
// ==============================================================
// WIDGET ANIMATO PER LE CASELLE SPECIALI (+2 / -1)
// ==============================================================
class SpecialEventBackgroundEffect extends StatefulWidget {
final String text;
final Color color;
const SpecialEventBackgroundEffect({super.key, required this.text, required this.color});
@override
State<SpecialEventBackgroundEffect> createState() => _SpecialEventBackgroundEffectState();
}
class _SpecialEventBackgroundEffectState extends State<SpecialEventBackgroundEffect> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
late Animation<double> _opacityAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 1000))..forward();
_scaleAnimation = Tween<double>(begin: 0.5, end: 1.5).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic));
_opacityAnimation = Tween<double>(begin: 0.9, end: 0.0).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn));
}
@override
void didUpdateWidget(covariant SpecialEventBackgroundEffect oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.text != widget.text) {
_controller.reset();
_controller.forward();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Center(
child: Transform.scale(
scale: _scaleAnimation.value,
child: Opacity(
opacity: _opacityAnimation.value,
child: ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0),
child: Text(
widget.text,
style: TextStyle(
fontSize: 250,
fontWeight: FontWeight.w900,
color: widget.color,
height: 1.0,
),
),
),
),
),
);
},
);
}
}
// ===========================================================================
// FILE: lib/ui/game/score_board.dart
// ===========================================================================
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../logic/game_controller.dart';
import '../../models/game_board.dart';
import '../../core/theme_manager.dart';
import '../../services/audio_service.dart';
import '../../core/app_colors.dart';
class ScoreBoard extends StatefulWidget {
const ScoreBoard({super.key});
@override
State<ScoreBoard> createState() => _ScoreBoardState();
}
class _ScoreBoardState extends State<ScoreBoard> {
@override
Widget build(BuildContext context) {
final controller = context.watch<GameController>();
final themeManager = context.watch<ThemeManager>();
final theme = themeManager.currentColors;
final themeType = themeManager.currentThemeType;
int redScore = controller.board.scoreRed;
int blueScore = controller.board.scoreBlue;
bool isRedTurn = controller.board.currentPlayer == Player.red;
bool isMuted = AudioService.instance.isMuted;
// --- LOGICA PER I NOMI ---
String nameRed = "ROSSO";
String nameBlue = themeType == AppThemeType.cyberpunk ? "VERDE" : "BLU";
if (controller.isOnline) {
nameRed = controller.onlineHostName.toUpperCase();
nameBlue = controller.onlineGuestName.toUpperCase();
} else if (controller.isVsCPU) {
nameRed = "TU";
nameBlue = "CPU";
}
return Container(
padding: const EdgeInsets.only(top: 10, bottom: 20, left: 20, right: 20),
decoration: BoxDecoration(
color: theme.background.withOpacity(0.95),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
offset: const Offset(0, 4),
blurRadius: 8,
),
],
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(30),
bottomRight: Radius.circular(30),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_PlayerScore(color: theme.playerRed, score: redScore, isTurn: isRedTurn, textColor: theme.text, title: nameRed),
Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"TETRAQ",
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w900,
color: theme.text,
letterSpacing: 4,
shadows: [Shadow(color: Colors.black.withOpacity(0.3), offset: const Offset(1, 2), blurRadius: 2)]
)
),
IconButton(
icon: Icon(isMuted ? Icons.volume_off : Icons.volume_up, color: theme.text.withOpacity(0.7)),
onPressed: () {
setState(() {
AudioService.instance.toggleMute();
});
},
),
],
),
_PlayerScore(color: theme.playerBlue, score: blueScore, isTurn: !isRedTurn, textColor: theme.text, title: nameBlue),
],
),
);
}
}
class _PlayerScore extends StatelessWidget {
final Color color;
final int score;
final bool isTurn;
final Color textColor;
final String title;
const _PlayerScore({required this.color, required this.score, required this.isTurn, required this.textColor, required this.title});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(title, style: TextStyle(fontWeight: FontWeight.bold, color: isTurn ? color : textColor.withOpacity(0.5), fontSize: 12)),
const SizedBox(height: 5),
AnimatedContainer(
duration: const Duration(milliseconds: 300),
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 10),
decoration: BoxDecoration(
color: color.withOpacity(isTurn ? 1.0 : 0.2),
borderRadius: BorderRadius.circular(15),
border: isTurn ? Border.all(color: Colors.white.withOpacity(0.4), width: 2) : Border.all(color: Colors.transparent, width: 2),
boxShadow: isTurn ? [
BoxShadow(color: color.withOpacity(0.5), offset: const Offset(0, 4), blurRadius: 6)
] : [],
),
child: Text('$score', style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: isTurn ? Colors.white : textColor.withOpacity(0.5))),
),
],
);
}
}
// ===========================================================================
// FILE: lib/ui/home/history_screen.dart
// ===========================================================================
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import '../../core/theme_manager.dart';
import '../../services/storage_service.dart';
class HistoryScreen extends StatelessWidget {
const HistoryScreen({super.key});
@override
Widget build(BuildContext context) {
final theme = context.watch<ThemeManager>().currentColors;
final history = StorageService.instance.matchHistory;
return Scaffold(
backgroundColor: theme.background,
appBar: AppBar(
title: Text("STORICO PARTITE", style: TextStyle(fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 2)),
backgroundColor: Colors.transparent,
elevation: 0,
iconTheme: IconThemeData(color: theme.text),
),
body: history.isEmpty
? Center(
child: Text(
"Nessuna partita giocata.\nScendi in campo!",
textAlign: TextAlign.center,
style: TextStyle(color: theme.text.withOpacity(0.5), fontSize: 18, fontWeight: FontWeight.bold),
),
)
: ListView.builder(
padding: const EdgeInsets.all(20),
itemCount: history.length,
itemBuilder: (context, index) {
final match = history[index];
DateTime date = DateTime.parse(match['date']);
String formattedDate = DateFormat('dd MMM yyyy - HH:mm').format(date);
// Leggiamo entrambi i nomi
String myName = match['myName'] ?? "IO"; // Usa 'IO' se è una partita vecchia
String opponent = match['opponent'];
int myScore = match['myScore'];
int oppScore = match['oppScore'];
bool isOnline = match['isOnline'];
bool isWin = myScore > oppScore;
bool isDraw = myScore == oppScore;
Color resultColor = isWin ? Colors.green : (isDraw ? Colors.grey : theme.playerRed);
String resultText = isWin ? "VITTORIA" : (isDraw ? "PAREGGIO" : "SCONFITTA");
return Container(
margin: const EdgeInsets.only(bottom: 15),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.text.withOpacity(0.05),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: resultColor.withOpacity(0.5), width: 2),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.2), offset: const Offset(0, 4), blurRadius: 6),
],
),
child: Row(
children: [
// Icona Tipo di Partita
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: resultColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
isOnline ? Icons.public : (opponent.contains("CPU") ? Icons.smart_toy : Icons.people_alt),
color: resultColor,
size: 28,
),
),
const SizedBox(width: 15),
// Dati Partita (Ora con i nomi chiari)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(resultText, style: TextStyle(color: resultColor, fontWeight: FontWeight.w900, fontSize: 16, letterSpacing: 1.5)),
const SizedBox(height: 5),
// NOMI GIOCATORI
RichText(
text: TextSpan(
children: [
TextSpan(text: myName, style: TextStyle(color: theme.playerBlue, fontWeight: FontWeight.bold, fontSize: 15)),
TextSpan(text: " vs ", style: TextStyle(color: theme.text.withOpacity(0.5), fontStyle: FontStyle.italic, fontSize: 12)),
TextSpan(text: opponent, style: TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold, fontSize: 15)),
]
)
),
const SizedBox(height: 5),
Text(formattedDate, style: TextStyle(color: theme.text.withOpacity(0.5), fontSize: 12)),
],
),
),
// Punteggio
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: theme.background,
borderRadius: BorderRadius.circular(15),
border: Border.all(color: theme.gridLine.withOpacity(0.3)),
),
child: Row(
children: [
Text("$myScore", style: TextStyle(fontSize: 22, fontWeight: FontWeight.w900, color: theme.playerBlue)),
Text(" - ", style: TextStyle(fontSize: 18, color: theme.text.withOpacity(0.5))),
Text("$oppScore", style: TextStyle(fontSize: 22, fontWeight: FontWeight.w900, color: theme.playerRed)),
],
),
),
],
),
);
},
),
);
}
}
// ===========================================================================
// FILE: lib/ui/home/home_screen.dart
// ===========================================================================
// ===========================================================================
// FILE: lib/ui/home/home_screen.dart
// ===========================================================================
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter/services.dart';
import 'dart:math' as math;
import '../../core/theme_manager.dart';
import '../../core/app_colors.dart';
import '../../models/game_board.dart';
import '../game/game_screen.dart';
import '../settings/settings_screen.dart';
import '../../logic/game_controller.dart';
import '../../services/storage_service.dart';
import '../multiplayer/lobby_screen.dart';
import 'history_screen.dart';
// --- NUOVI WIDGET PERSONALIZZATI ---
class _NeonShapeButton extends StatelessWidget {
final IconData icon;
final String label;
final bool isSelected;
final ThemeColors theme;
final VoidCallback onTap;
const _NeonShapeButton({required this.icon, required this.label, required this.isSelected, required this.theme, required this.onTap});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
color: isSelected ? theme.playerBlue.withOpacity(0.2) : theme.text.withOpacity(0.05),
border: Border.all(
color: isSelected ? theme.playerBlue : theme.gridLine.withOpacity(0.3),
width: isSelected ? 2 : 1,
),
boxShadow: isSelected ? [BoxShadow(color: theme.playerBlue.withOpacity(0.4), blurRadius: 12, spreadRadius: 2)] : [],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: isSelected ? theme.playerBlue : theme.text.withOpacity(0.5), size: 24),
const SizedBox(height: 6),
Text(label, style: TextStyle(color: isSelected ? theme.text : theme.text.withOpacity(0.5), fontSize: 11, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal)),
],
),
),
);
}
}
class _NeonSizeButton extends StatelessWidget {
final String label;
final bool isSelected;
final ThemeColors theme;
final VoidCallback onTap;
const _NeonSizeButton({required this.label, required this.isSelected, required this.theme, required this.onTap});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: 50,
height: 50,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isSelected ? theme.playerRed.withOpacity(0.2) : theme.text.withOpacity(0.05),
border: Border.all(
color: isSelected ? theme.playerRed : theme.gridLine.withOpacity(0.3),
width: isSelected ? 2 : 1,
),
boxShadow: isSelected ? [BoxShadow(color: theme.playerRed.withOpacity(0.4), blurRadius: 10, spreadRadius: 1.5)] : [],
),
child: Center(
child: Text(label, style: TextStyle(color: isSelected ? theme.text : theme.text.withOpacity(0.5), fontSize: 14, fontWeight: isSelected ? FontWeight.w900 : FontWeight.bold)),
),
),
);
}
}
class _NeonTimeSwitch extends StatelessWidget {
final bool isTimeMode;
final ThemeColors theme;
final VoidCallback onTap;
const _NeonTimeSwitch({required this.isTimeMode, required this.theme, required this.onTap});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: isTimeMode ? Colors.amber.withOpacity(0.2) : theme.text.withOpacity(0.05),
border: Border.all(
color: isTimeMode ? Colors.amber : theme.gridLine.withOpacity(0.3),
width: isTimeMode ? 2 : 1,
),
boxShadow: isTimeMode ? [BoxShadow(color: Colors.amber.withOpacity(0.4), blurRadius: 12, spreadRadius: 2)] : [],
),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(isTimeMode ? Icons.timer : Icons.timer_off, color: isTimeMode ? Colors.amber : theme.text.withOpacity(0.5), size: 28),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
isTimeMode ? 'A TEMPO' : 'RELAX',
style: TextStyle(color: isTimeMode ? theme.text : theme.text.withOpacity(0.5), fontWeight: FontWeight.w900, fontSize: 14, letterSpacing: 1.5)
),
Text(
isTimeMode ? '15 sec a mossa' : 'Nessun limite di tempo',
style: TextStyle(color: isTimeMode ? Colors.amber.shade200 : theme.text.withOpacity(0.4), fontSize: 11, fontWeight: FontWeight.bold)
),
],
),
],
),
),
);
}
}
// ---------------------------------------------------------------------------
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
// Il raggio di partenza è 4 (Taglia M) come richiesto
int _selectedRadius = 4;
ArenaShape _selectedShape = ArenaShape.classic;
bool _isTimeMode = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
WidgetsBinding.instance.addPostFrameCallback((_) {
_checkPlayerName();
});
_checkClipboardForInvite();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
_checkClipboardForInvite();
}
}
void _checkPlayerName() {
if (StorageService.instance.playerName.isEmpty) {
_showNameDialog();
}
}
void _showNameDialog() {
final TextEditingController nameController = TextEditingController();
showDialog(
context: context,
barrierDismissible: false,
barrierColor: Colors.black.withOpacity(0.8),
builder: (context) {
final themeManager = context.watch<ThemeManager>();
final theme = themeManager.currentColors;
final themeType = themeManager.currentThemeType;
Widget dialogContent;
if (themeType == AppThemeType.cyberpunk) {
dialogContent = _AnimatedCyberBorder(child: _buildDialogContent(context, theme, nameController));
} else if (themeType == AppThemeType.wood) {
dialogContent = Container(
decoration: BoxDecoration(
color: theme.background,
borderRadius: BorderRadius.circular(25),
border: Border.all(color: const Color(0xFF8B4513), width: 5),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.6), blurRadius: 15, offset: const Offset(0, 8)),
const BoxShadow(color: Color(0xFFD2691E), blurRadius: 0, spreadRadius: 2, offset: Offset(0, 0)),
],
),
padding: const EdgeInsets.all(5),
child: _buildDialogContent(context, theme, nameController),
);
} else if (themeType == AppThemeType.doodle) {
dialogContent = Container(
decoration: BoxDecoration(
color: theme.background,
borderRadius: BorderRadius.circular(25),
border: Border.all(color: theme.text, width: 3, style: BorderStyle.solid),
boxShadow: [
BoxShadow(color: theme.text.withOpacity(0.3), blurRadius: 0, offset: const Offset(4, 4)),
BoxShadow(color: theme.text.withOpacity(0.3), blurRadius: 0, offset: const Offset(-2, -2)),
],
),
padding: const EdgeInsets.all(4),
child: _buildDialogContent(context, theme, nameController),
);
} else {
dialogContent = Container(
decoration: BoxDecoration(
color: theme.background,
borderRadius: BorderRadius.circular(25),
border: Border.all(color: theme.playerBlue.withOpacity(0.5), width: 2),
boxShadow: [
BoxShadow(color: theme.playerBlue.withOpacity(0.3), blurRadius: 20, spreadRadius: 5),
]
),
child: _buildDialogContent(context, theme, nameController),
);
}
return Dialog(backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.all(20), child: dialogContent);
},
);
}
Widget _buildDialogContent(BuildContext context, ThemeColors theme, TextEditingController controller) {
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 30.0, horizontal: 25.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('BENVENUTO IN TETRAQ!', style: TextStyle(color: theme.text, fontWeight: FontWeight.w900, fontSize: 24, letterSpacing: 1.5), textAlign: TextAlign.center),
const SizedBox(height: 20),
Text('Scegli il tuo nome da battaglia per sfidare i tuoi amici online.', style: TextStyle(color: theme.text.withOpacity(0.8), fontSize: 16), textAlign: TextAlign.center),
const SizedBox(height: 40),
TextField(
controller: controller,
textCapitalization: TextCapitalization.characters,
textAlign: TextAlign.center,
maxLength: 5,
style: TextStyle(color: theme.text, fontSize: 28, fontWeight: FontWeight.bold, letterSpacing: 4),
decoration: InputDecoration(
hintText: 'NOME', hintStyle: TextStyle(color: theme.text.withOpacity(0.3), letterSpacing: 4),
filled: true, fillColor: theme.text.withOpacity(0.05), counterText: "",
enabledBorder: OutlineInputBorder(borderSide: BorderSide(color: theme.gridLine.withOpacity(0.5), width: 2), borderRadius: BorderRadius.circular(15)),
focusedBorder: OutlineInputBorder(borderSide: BorderSide(color: theme.playerBlue, width: 3), borderRadius: BorderRadius.circular(15)),
),
),
const SizedBox(height: 40),
SizedBox(
width: double.infinity, height: 55,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: theme.playerBlue, foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
elevation: 8, shadowColor: theme.playerBlue.withOpacity(0.5),
),
onPressed: () {
final name = controller.text.trim();
if (name.isNotEmpty) {
StorageService.instance.savePlayerName(name);
Navigator.of(context).pop();
setState(() {});
}
},
child: const Text('SALVA E GIOCA', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, letterSpacing: 1.5)),
),
),
],
),
),
);
}
Future<void> _checkClipboardForInvite() async {
try {
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
String? text = data?.text;
if (text != null && text.contains("TetraQ") && text.contains("codice:")) {
RegExp regExp = RegExp(r'codice:\s*([A-Z0-9]{5})', caseSensitive: false);
Match? match = regExp.firstMatch(text);
if (match != null) {
String roomCode = match.group(1)!.toUpperCase();
await Clipboard.setData(const ClipboardData(text: ''));
if (mounted && ModalRoute.of(context)?.isCurrent == true) {
_promptJoinRoom(roomCode);
}
}
}
} catch (e) {
debugPrint("Errore lettura appunti: $e");
}
}
void _promptJoinRoom(String roomCode) {
showDialog(
context: context,
builder: (context) {
final theme = context.watch<ThemeManager>().currentColors;
return AlertDialog(
backgroundColor: theme.background,
title: Text("Invito Trovato!", style: TextStyle(color: theme.text, fontWeight: FontWeight.bold)),
content: Text("Vuoi unirti alla stanza $roomCode?", style: TextStyle(color: theme.text)),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text("No", style: TextStyle(color: Colors.red))),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: theme.playerBlue),
onPressed: () {
Navigator.of(context).pop();
Navigator.push(context, MaterialPageRoute(builder: (_) => LobbyScreen(initialRoomCode: roomCode, selectedRadius: _selectedRadius, selectedShape: _selectedShape)));
},
child: const Text("Unisciti", style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
),
],
);
}
);
}
@override
Widget build(BuildContext context) {
// --- RIPRISTINATO: Forza la modalità a schermo intero e l'orientamento verticale ---
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
final themeManager = context.watch<ThemeManager>();
final themeType = themeManager.currentThemeType;
final theme = themeManager.currentColors;
String? bgImage;
if (themeType == AppThemeType.wood) bgImage = 'assets/images/wood_bg.jpg';
if (themeType == AppThemeType.doodle) bgImage = 'assets/images/doodle_bg.jpg';
int wins = StorageService.instance.wins;
int losses = StorageService.instance.losses;
String playerName = StorageService.instance.playerName;
if (playerName.isEmpty) playerName = "GUEST";
Widget uiContent = SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 10.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
CircleAvatar(
backgroundColor: theme.playerBlue.withOpacity(0.2),
radius: 25,
child: Icon(Icons.person, color: theme.playerBlue, size: 30),
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("BENVENUTO,", style: TextStyle(color: theme.text.withOpacity(0.5), fontSize: 10, fontWeight: FontWeight.bold, letterSpacing: 1.2)),
Text(playerName, style: TextStyle(color: theme.text, fontSize: 18, fontWeight: FontWeight.w900, letterSpacing: 1.5)),
],
),
],
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: theme.text.withOpacity(0.05),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: theme.gridLine.withOpacity(0.2)),
),
child: Row(
children: [
Icon(Icons.emoji_events, color: Colors.amber.shade600, size: 18),
const SizedBox(width: 4),
Text("$wins", style: TextStyle(color: theme.text, fontWeight: FontWeight.bold)),
const SizedBox(width: 10),
Icon(Icons.sentiment_very_dissatisfied, color: theme.playerRed.withOpacity(0.8), size: 18),
const SizedBox(width: 4),
Text("$losses", style: TextStyle(color: theme.text, fontWeight: FontWeight.bold)),
],
),
)
],
),
const SizedBox(height: 20),
Center(
child: Column(
children: [
Text(
"TETRAQ",
style: TextStyle(
fontSize: 50,
fontWeight: FontWeight.w900,
color: theme.text,
letterSpacing: 8,
shadows: [Shadow(color: Colors.black.withOpacity(0.3), offset: const Offset(2, 4), blurRadius: 4)]
)
),
Text(
"THE ROMBUS PUZZLE",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: theme.gridLine,
letterSpacing: 3,
)
),
],
),
),
const SizedBox(height: 20),
// --- PANNELLO IMPOSTAZIONI ARENA CON LOGICA GRANDEZZA CORRETTA ---
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.background.withOpacity(0.8),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: theme.gridLine.withOpacity(0.3)),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.2), blurRadius: 10, offset: const Offset(0, 4))],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 1. SEZIONE FORMA
Text("FORMA DELL'ARENA", style: TextStyle(fontSize: 11, 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: _selectedShape == ArenaShape.classic, theme: theme,
onTap: () => setState(() => _selectedShape = ArenaShape.classic),
),
_NeonShapeButton(
icon: Icons.add, label: 'Croce', isSelected: _selectedShape == ArenaShape.cross, theme: theme,
onTap: () => setState(() => _selectedShape = ArenaShape.cross),
),
_NeonShapeButton(
icon: Icons.donut_large, label: 'Buco Nero', isSelected: _selectedShape == ArenaShape.donut, theme: theme,
onTap: () => setState(() => _selectedShape = ArenaShape.donut),
),
_NeonShapeButton(
icon: Icons.hourglass_bottom, label: 'Clessidra', isSelected: _selectedShape == ArenaShape.hourglass, theme: theme,
onTap: () => setState(() => _selectedShape = ArenaShape.hourglass),
),
],
),
const SizedBox(height: 20),
Divider(color: theme.gridLine.withOpacity(0.2), height: 1),
const SizedBox(height: 20),
// 2. SEZIONE TAGLIA (SCALA CORRETTA DA 3 A 6)
Text("GRANDEZZA", style: TextStyle(fontSize: 11, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5)),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_NeonSizeButton(label: 'S', isSelected: _selectedRadius == 3, theme: theme, onTap: () => setState(() => _selectedRadius = 3)),
_NeonSizeButton(label: 'M', isSelected: _selectedRadius == 4, theme: theme, onTap: () => setState(() => _selectedRadius = 4)),
_NeonSizeButton(label: 'L', isSelected: _selectedRadius == 5, theme: theme, onTap: () => setState(() => _selectedRadius = 5)),
_NeonSizeButton(label: 'MAX', isSelected: _selectedRadius == 6, theme: theme, onTap: () => setState(() => _selectedRadius = 6)),
],
),
const SizedBox(height: 20),
Divider(color: theme.gridLine.withOpacity(0.2), height: 1),
const SizedBox(height: 20),
// 3. SEZIONE TEMPO
Text("MODALITÀ DI GIOCO", style: TextStyle(fontSize: 11, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5)),
const SizedBox(height: 10),
_NeonTimeSwitch(
isTimeMode: _isTimeMode,
theme: theme,
onTap: () => setState(() => _isTimeMode = !_isTimeMode),
),
],
),
),
// --------------------------------------------------------------
const SizedBox(height: 20),
Expanded(
child: GridView.count(
crossAxisCount: 2,
crossAxisSpacing: 15,
mainAxisSpacing: 15,
childAspectRatio: 1.1,
physics: const BouncingScrollPhysics(),
children: [
_FeatureCard(
title: "ONLINE", subtitle: "Sfida il mondo", icon: Icons.public, color: theme.playerBlue, theme: theme, isFeatured: true,
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (_) => LobbyScreen(selectedRadius: _selectedRadius, selectedShape: _selectedShape)));
},
),
_FeatureCard(
title: "VS CPU", subtitle: "Allenati", icon: Icons.smart_toy, color: Colors.purple.shade400, theme: theme,
onTap: () async {
context.read<GameController>().startNewGame(_selectedRadius, vsCPU: true, shape: _selectedShape);
await Navigator.push(context, MaterialPageRoute(builder: (_) => const GameScreen()));
setState(() {});
},
),
_FeatureCard(
title: "LOCALE", subtitle: "Stesso schermo", icon: Icons.people_alt, color: theme.playerRed, theme: theme,
onTap: () {
context.read<GameController>().startNewGame(_selectedRadius, vsCPU: false, shape: _selectedShape);
Navigator.push(context, MaterialPageRoute(builder: (_) => const GameScreen()));
},
),
Column(
children: [
Expanded(child: _MiniCard(title: "STORICO", icon: Icons.history, color: Colors.orange.shade400, theme: theme, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const HistoryScreen())))),
const SizedBox(height: 15),
Expanded(child: _MiniCard(title: "TEMI", icon: Icons.palette, color: Colors.teal.shade400, theme: theme, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const SettingsScreen())))),
],
)
],
),
),
],
),
),
);
return Scaffold(
backgroundColor: bgImage != null ? Colors.transparent : theme.background,
body: Container(
decoration: bgImage != null ? BoxDecoration(image: DecorationImage(image: AssetImage(bgImage), fit: BoxFit.cover)) : null,
child: bgImage != null ? BackdropFilter(filter: ImageFilter.blur(sigmaX: 3.5, sigmaY: 3.5), child: Container(color: themeType == AppThemeType.doodle ? Colors.white.withOpacity(0.1) : Colors.transparent, child: uiContent)) : uiContent,
),
);
}
}
class _FeatureCard extends StatelessWidget {
final String title;
final String subtitle;
final IconData icon;
final Color color;
final ThemeColors theme;
final VoidCallback onTap;
final bool isFeatured;
const _FeatureCard({required this.title, required this.subtitle, required this.icon, required this.color, required this.theme, required this.onTap, this.isFeatured = false});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
decoration: BoxDecoration(
color: isFeatured ? color : theme.background,
borderRadius: BorderRadius.circular(25),
border: Border.all(color: isFeatured ? Colors.transparent : color.withOpacity(0.5), width: 2),
boxShadow: [BoxShadow(color: color.withOpacity(isFeatured ? 0.4 : 0.1), offset: const Offset(0, 8), blurRadius: 15)]
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(color: isFeatured ? Colors.white.withOpacity(0.2) : color.withOpacity(0.1), shape: BoxShape.circle),
child: Icon(icon, color: isFeatured ? Colors.white : color, size: 32),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: TextStyle(color: isFeatured ? Colors.white : theme.text, fontSize: 20, fontWeight: FontWeight.w900)),
Text(subtitle, style: TextStyle(color: isFeatured ? Colors.white.withOpacity(0.8) : theme.text.withOpacity(0.5), fontSize: 12, fontWeight: FontWeight.bold)),
],
)
],
),
),
),
);
}
}
class _MiniCard extends StatelessWidget {
final String title;
final IconData icon;
final Color color;
final ThemeColors theme;
final VoidCallback onTap;
const _MiniCard({required this.title, required this.icon, required this.color, required this.theme, required this.onTap});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
decoration: BoxDecoration(
color: theme.background,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: color.withOpacity(0.5), width: 1.5),
boxShadow: [BoxShadow(color: color.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, 5))]
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color, size: 24),
const SizedBox(width: 8),
Text(title, style: TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 14)),
],
),
),
);
}
}
class _AnimatedCyberBorder extends StatefulWidget {
final Widget child;
const _AnimatedCyberBorder({required this.child});
@override
State<_AnimatedCyberBorder> createState() => _AnimatedCyberBorderState();
}
class _AnimatedCyberBorderState extends State<_AnimatedCyberBorder> with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: const Duration(seconds: 3))..repeat(); }
@override
void dispose() { _controller.dispose(); super.dispose(); }
@override
Widget build(BuildContext context) {
final theme = context.watch<ThemeManager>().currentColors;
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return CustomPaint(
painter: _CyberBorderPainter(animationValue: _controller.value, color1: theme.playerBlue, color2: theme.playerRed),
child: Container(
decoration: BoxDecoration(color: theme.background.withOpacity(0.9), borderRadius: BorderRadius.circular(25), boxShadow: [BoxShadow(color: theme.playerBlue.withOpacity(0.3), blurRadius: 25, spreadRadius: 2)]),
padding: const EdgeInsets.all(3),
child: widget.child,
),
);
},
child: widget.child,
);
}
}
class _CyberBorderPainter extends CustomPainter {
final double animationValue;
final Color color1;
final Color color2;
_CyberBorderPainter({required this.animationValue, required this.color1, required this.color2});
@override
void paint(Canvas canvas, Size size) {
final rect = Offset.zero & size;
final RRect rrect = RRect.fromRectAndRadius(rect, const Radius.circular(25));
final Paint paint = Paint()
..shader = SweepGradient(colors: [color1, color2, color1, color2, color1], stops: const [0.0, 0.25, 0.5, 0.75, 1.0], transform: GradientRotation(animationValue * 2 * math.pi)).createShader(rect)
..style = PaintingStyle.stroke
..strokeWidth = 4.0
..maskFilter = const MaskFilter.blur(BlurStyle.solid, 4);
canvas.drawRRect(rrect, paint);
}
@override
bool shouldRepaint(covariant _CyberBorderPainter oldDelegate) => oldDelegate.animationValue != animationValue;
}
// ===========================================================================
// FILE: lib/ui/multiplayer/lobby_screen.dart
// ===========================================================================
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import '../../core/theme_manager.dart';
import '../../models/game_board.dart'; // <-- IMPORTANTE
import '../../services/multiplayer_service.dart';
import '../../services/storage_service.dart';
import '../game/game_screen.dart';
import '../../logic/game_controller.dart';
class LobbyScreen extends StatefulWidget {
final String? initialRoomCode;
final int selectedRadius;
final ArenaShape selectedShape; // <-- RICEVIAMO LA FORMA
const LobbyScreen({super.key, this.initialRoomCode, this.selectedRadius = 2, this.selectedShape = ArenaShape.classic});
@override
State<LobbyScreen> createState() => _LobbyScreenState();
}
class _LobbyScreenState extends State<LobbyScreen> {
final MultiplayerService _multiplayerService = MultiplayerService();
late TextEditingController _codeController;
bool _isLoading = false;
String? _myRoomCode;
String _playerName = '';
@override
void initState() {
super.initState();
_codeController = TextEditingController();
_playerName = StorageService.instance.playerName;
if (widget.initialRoomCode != null && widget.initialRoomCode!.isNotEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() { _codeController.text = widget.initialRoomCode!; });
});
}
}
@override
void dispose() { _codeController.dispose(); super.dispose(); }
Future<void> _createRoom() async {
if (_isLoading) return;
setState(() => _isLoading = true);
try {
int radius = widget.selectedRadius;
// PASSIAMO ANCHE IL NOME DELLA FORMA!
String code = await _multiplayerService.createGameRoom(radius, _playerName, widget.selectedShape.name);
if (!mounted) return;
setState(() { _myRoomCode = code; _isLoading = false; });
_multiplayerService.shareInviteLink(code);
_showWaitingDialog(code);
} catch (e) {
if (mounted) { setState(() => _isLoading = false); _showError("Errore durante la creazione della partita."); }
}
}
Future<void> _joinRoom() async {
if (_isLoading) return;
FocusScope.of(context).unfocus();
String code = _codeController.text.trim().toUpperCase();
if (code.isEmpty || code.length != 5) { _showError("Inserisci un codice valido di 5 caratteri."); return; }
setState(() => _isLoading = true);
try {
Map<String, dynamic>? roomData = await _multiplayerService.joinGameRoom(code, _playerName);
if (!mounted) return;
setState(() => _isLoading = false);
if (roomData != null) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Stanza trovata! Partita in avvio..."), backgroundColor: Colors.green));
int hostRadius = roomData['radius'] ?? 2;
// LEGGIAMO LA FORMA DELL'HOST
String shapeStr = roomData['shape'] ?? 'classic';
ArenaShape hostShape = ArenaShape.values.firstWhere((e) => e.name == shapeStr, orElse: () => ArenaShape.classic);
context.read<GameController>().startNewGame(hostRadius, isOnline: true, roomCode: code, isHost: false, shape: hostShape);
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const GameScreen()));
} else {
_showError("Stanza non trovata, piena o partita già iniziata.");
}
} catch (e) {
if (mounted) { setState(() => _isLoading = false); _showError("Errore di connessione: $e"); }
}
}
void _showError(String message) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message, style: const TextStyle(color: Colors.white)), backgroundColor: Colors.red)); }
void _showWaitingDialog(String code) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) {
final theme = context.watch<ThemeManager>().currentColors;
return StreamBuilder<DocumentSnapshot>(
stream: _multiplayerService.listenToRoom(code),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data!.exists) {
var data = snapshot.data!.data() as Map<String, dynamic>;
if (data['status'] == 'playing') {
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.pop(context);
int radius = widget.selectedRadius;
// AVVIAMO IL GIOCO CON LA FORMA SCELTA DALL'HOST!
context.read<GameController>().startNewGame(radius, isOnline: true, roomCode: code, isHost: true, shape: widget.selectedShape);
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const GameScreen()));
});
}
}
return AlertDialog(
backgroundColor: theme.background,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: Text("In attesa...", textAlign: TextAlign.center, style: TextStyle(color: theme.text, fontWeight: FontWeight.bold)),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(), const SizedBox(height: 25),
Text("CODICE STANZA", style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: theme.text.withOpacity(0.6), letterSpacing: 2)),
Text(code, style: TextStyle(fontSize: 40, fontWeight: FontWeight.w900, color: theme.playerRed, letterSpacing: 8)),
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(color: theme.text.withOpacity(0.05), borderRadius: BorderRadius.circular(15), border: Border.all(color: theme.playerBlue.withOpacity(0.3), width: 1.5)),
child: Column(
children: [
Icon(Icons.auto_awesome, color: theme.playerBlue, size: 28), const SizedBox(height: 10),
Text("Condividi l'invito con un amico.", textAlign: TextAlign.center, style: TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 14)),
const SizedBox(height: 5),
Text("Gli basterà copiare il messaggio e aprire l'app.", textAlign: TextAlign.center, style: TextStyle(color: theme.text.withOpacity(0.8), fontSize: 13, height: 1.4)),
],
),
),
],
),
actionsAlignment: MainAxisAlignment.center,
actions: [
TextButton(
onPressed: () { FirebaseFirestore.instance.collection('games').doc(code).delete(); Navigator.pop(context); },
child: const Text("ANNULLA", style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 1.5)),
),
],
);
},
);
}
);
}
@override
Widget build(BuildContext context) {
final theme = context.watch<ThemeManager>().currentColors;
return Scaffold(
backgroundColor: theme.background,
appBar: AppBar(title: Text("Multiplayer Online", style: TextStyle(color: theme.text, fontWeight: FontWeight.bold)), backgroundColor: Colors.transparent, elevation: 0, iconTheme: IconThemeData(color: theme.text)),
body: _isLoading ? Center(child: CircularProgressIndicator(color: theme.playerRed)) : Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Icon(Icons.wifi, size: 80, color: theme.playerBlue), const SizedBox(height: 10),
Text("Giocatore: $_playerName", textAlign: TextAlign.center, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: theme.playerRed)),
const SizedBox(height: 40),
SizedBox(height: 60, child: ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: theme.playerRed, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), onPressed: _createRoom, child: const Text("CREA PARTITA", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, letterSpacing: 2)))),
const SizedBox(height: 40),
TextField(
controller: _codeController, textCapitalization: TextCapitalization.characters, textAlign: TextAlign.center, maxLength: 5,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: theme.text, letterSpacing: 8),
decoration: InputDecoration(hintText: "CODICE", hintStyle: TextStyle(color: theme.text.withOpacity(0.3)), enabledBorder: OutlineInputBorder(borderSide: BorderSide(color: theme.text.withOpacity(0.3), width: 2), borderRadius: BorderRadius.circular(15)), focusedBorder: OutlineInputBorder(borderSide: BorderSide(color: theme.playerBlue, width: 3), borderRadius: BorderRadius.circular(15))),
),
const SizedBox(height: 16),
SizedBox(height: 60, child: ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: theme.playerBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), onPressed: _joinRoom, child: const Text("UNISCITI", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, letterSpacing: 2)))),
],
),
),
),
);
}
}
// ===========================================================================
// FILE: lib/ui/settings/settings_screen.dart
// ===========================================================================
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../core/theme_manager.dart';
import '../../core/app_colors.dart';
class SettingsScreen extends StatelessWidget {
const SettingsScreen({super.key});
@override
Widget build(BuildContext context) {
final themeManager = context.watch<ThemeManager>();
final theme = themeManager.currentColors;
return Scaffold(
backgroundColor: theme.background,
appBar: AppBar(
title: Text("SELEZIONA TEMA", style: TextStyle(fontWeight: FontWeight.bold, color: theme.text)),
backgroundColor: Colors.transparent,
elevation: 0,
iconTheme: IconThemeData(color: theme.text),
),
body: ListView(
padding: const EdgeInsets.all(20),
children: [
_ThemeCard(
title: "Minimal",
subtitle: "Linee pulite, sfondo chiaro",
type: AppThemeType.minimal,
previewColors: AppColors.minimal,
),
const SizedBox(height: 15),
_ThemeCard(
title: "Quaderno (Doodle)",
subtitle: "Sfondo a quadretti, tratto a penna",
type: AppThemeType.doodle,
previewColors: AppColors.doodle,
),
const SizedBox(height: 15),
_ThemeCard(
title: "Cyberpunk",
subtitle: "Nero profondo, luci al neon",
type: AppThemeType.cyberpunk,
previewColors: AppColors.cyberpunk,
),
const SizedBox(height: 15),
// --- NUOVA CARD TEMA LEGNO ---
_ThemeCard(
title: "Legno & Fiammiferi",
subtitle: "Tavolo di legno, linee come fiammiferi",
type: AppThemeType.wood,
previewColors: AppColors.wood,
),
// -----------------------------
],
),
);
}
}
class _ThemeCard extends StatelessWidget {
final String title;
final String subtitle;
final AppThemeType type;
final ThemeColors previewColors;
const _ThemeCard({required this.title, required this.subtitle, required this.type, required this.previewColors});
@override
Widget build(BuildContext context) {
final themeManager = context.watch<ThemeManager>();
bool isSelected = themeManager.currentThemeType == type;
return GestureDetector(
onTap: () => themeManager.setTheme(type),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: previewColors.background,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isSelected ? previewColors.playerBlue : previewColors.gridLine.withOpacity(0.5),
width: isSelected ? 4 : 2,
),
boxShadow: isSelected ? [BoxShadow(color: previewColors.playerBlue.withOpacity(0.4), blurRadius: 10, spreadRadius: 2)] : [],
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: previewColors.text)),
Text(subtitle, style: TextStyle(fontSize: 14, color: previewColors.text.withOpacity(0.7))),
],
),
),
Container(width: 20, height: 20, decoration: BoxDecoration(color: previewColors.playerRed, shape: BoxShape.circle)),
const SizedBox(width: 10),
Container(width: 20, height: 20, decoration: BoxDecoration(color: previewColors.playerBlue, shape: BoxShape.circle)),
],
),
),
);
}
}
// ===========================================================================
// FILE: lib/widgets/custom_button.dart
// ===========================================================================
// ===========================================================================
// FILE: lib/widgets/custom_settings_button.dart
// ===========================================================================
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
// Widget per i pulsanti di selezione della forma dell'arena
class NeonShapeButton extends StatelessWidget {
final IconData icon;
final String label;
final bool isSelected;
final VoidCallback onTap;
final ShapeBorder shape; // La forma geometrica del pulsante
const NeonShapeButton({
super.key,
required this.icon,
required this.label,
required this.isSelected,
required this.onTap,
this.shape = const RoundedRectangleBorder( // Forma di default
borderRadius: BorderRadius.all(Radius.circular(12.0))),
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: ShapeDecoration(
shape: shape,
color: isSelected
? AppColors.neonGreen.withOpacity(0.2) // Sfondo luminoso se selezionato
: AppColors.surface.withOpacity(0.5), // Sfondo più scuro se non selezionato
shadows: isSelected
? [ // Bagliore intenso se selezionato
BoxShadow(
color: AppColors.neonGreen.withOpacity(0.6),
blurRadius: 12.0,
spreadRadius: 2.0,
),
]
: [],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
color: isSelected ? AppColors.neonGreen : AppColors.textSecondary,
size: 28,
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
color: isSelected ? AppColors.textPrimary : AppColors.textSecondary,
fontSize: 12,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
],
),
),
);
}
}
// Widget per i pulsanti di selezione della taglia dell'arena
class NeonSizeButton extends StatelessWidget {
final String label;
final bool isSelected;
final VoidCallback onTap;
const NeonSizeButton({
super.key,
required this.label,
required this.isSelected,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: 50,
height: 50,
decoration: BoxDecoration(
shape: BoxShape.circle, // Forma circolare
color: isSelected
? AppColors.neonBlue.withOpacity(0.2)
: AppColors.surface.withOpacity(0.5),
border: Border.all(
color: isSelected ? AppColors.neonBlue : AppColors.surfaceLight,
width: 2.0,
),
shadows: isSelected
? [
BoxShadow(
color: AppColors.neonBlue.withOpacity(0.6),
blurRadius: 10.0,
spreadRadius: 1.5,
),
]
: [],
),
child: Center(
child: Text(
label,
style: TextStyle(
color: isSelected ? AppColors.textPrimary : AppColors.textSecondary,
fontSize: 16,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
),
),
);
}
}
// Widget per l'interruttore della modalità tempo (Clessidra)
class NeonTimeSwitch extends StatelessWidget {
final bool isTimeMode;
final VoidCallback onTap;
const NeonTimeSwitch({
super.key,
required this.isTimeMode,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30.0), // Forma arrotondata per lo switch
color: isTimeMode
? AppColors.neonGreen.withOpacity(0.2)
: AppColors.surface.withOpacity(0.5),
border: Border.all(
color: isTimeMode ? AppColors.neonGreen : AppColors.surfaceLight,
width: 2.0,
),
shadows: isTimeMode
? [
BoxShadow(
color: AppColors.neonGreen.withOpacity(0.6),
blurRadius: 12.0,
spreadRadius: 2.0,
),
]
: [],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.hourglass_empty, // Icona clessidra
color: isTimeMode ? AppColors.neonGreen : AppColors.textSecondary,
),
const SizedBox(width: 8),
Text(
isTimeMode ? 'A TEMPO' : 'SENZA TEMPO',
style: TextStyle(
color: isTimeMode ? AppColors.textPrimary : AppColors.textSecondary,
fontWeight: isTimeMode ? FontWeight.bold : FontWeight.normal,
),
),
],
),
),
);
}
}
// ===========================================================================
// FILE: lib/widgets/game_over_dialog.dart
// ===========================================================================
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../logic/game_controller.dart';
import '../core/theme_manager.dart';
import '../core/app_colors.dart';
class GameOverDialog extends StatelessWidget {
const GameOverDialog({super.key});
@override
Widget build(BuildContext context) {
final game = context.read<GameController>();
final themeManager = context.read<ThemeManager>();
final theme = themeManager.currentColors;
final themeType = themeManager.currentThemeType;
int red = game.board.scoreRed;
int blue = game.board.scoreBlue;
bool playerBeatCPU = game.isVsCPU && red > blue;
// --- LOGICA NOMI ---
String nameRed = "ROSSO";
String nameBlue = themeType == AppThemeType.cyberpunk ? "VERDE" : "BLU";
if (game.isOnline) {
nameRed = game.onlineHostName.toUpperCase();
nameBlue = game.onlineGuestName.toUpperCase();
} else if (game.isVsCPU) {
nameRed = "TU";
nameBlue = "CPU";
}
// --- DETERMINA IL VINCITORE ---
String winnerText = "";
Color winnerColor = theme.text;
if (red > blue) {
winnerText = "VINCE $nameRed!";
winnerColor = theme.playerRed;
} else if (blue > red) {
winnerText = "VINCE $nameBlue!";
winnerColor = theme.playerBlue;
} else {
winnerText = "PAREGGIO!";
winnerColor = theme.text;
}
return AlertDialog(
backgroundColor: theme.background,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(color: winnerColor.withOpacity(0.5), width: 2),
),
title: Text("FINE PARTITA", textAlign: TextAlign.center, style: TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 22)),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(winnerText, textAlign: TextAlign.center, style: TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: winnerColor)),
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
decoration: BoxDecoration(
color: theme.text.withOpacity(0.05),
borderRadius: BorderRadius.circular(15),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text("$nameRed: $red", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerRed)),
Text(" - ", style: TextStyle(fontSize: 18, color: theme.text)),
Text("$nameBlue: $blue", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerBlue)),
],
),
),
if (game.isVsCPU) ...[
const SizedBox(height: 15),
Text("Difficoltà CPU: Livello ${game.cpuLevel}", style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: theme.text.withOpacity(0.7))),
]
],
),
actionsPadding: const EdgeInsets.only(left: 20, right: 20, bottom: 20, top: 10),
actionsAlignment: MainAxisAlignment.center,
actions: [
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (playerBeatCPU)
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: winnerColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
elevation: 5,
),
onPressed: () {
Navigator.pop(context);
game.increaseLevelAndRestart();
},
child: const Text("PROSSIMO LIVELLO ➔", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
)
else if (game.isOnline)
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: winnerColor == theme.text ? theme.playerBlue : winnerColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
elevation: 5,
),
onPressed: () {
Navigator.pop(context);
if (game.board.isGameOver) {
game.requestRematch();
}
},
child: const Text("RIGIOCA ONLINE", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 1.5)),
)
else
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: winnerColor == theme.text ? theme.playerBlue : winnerColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
elevation: 5,
),
onPressed: () {
Navigator.pop(context);
game.startNewGame(game.board.radius, vsCPU: game.isVsCPU);
},
child: const Text("RIGIOCA", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 2)),
),
const SizedBox(height: 12),
OutlinedButton(
style: OutlinedButton.styleFrom(
foregroundColor: theme.text,
side: BorderSide(color: theme.text.withOpacity(0.3), width: 2),
padding: const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
),
onPressed: () {
if (game.isOnline) {
game.disconnectOnlineGame();
}
Navigator.pop(context);
Navigator.pop(context);
},
child: Text("TORNA AL MENU", style: TextStyle(fontWeight: FontWeight.bold, color: theme.text, fontSize: 14, letterSpacing: 1.5)),
),
],
)
],
);
}
}