From 1395fc32f677c788e2d1871debea7caa9366ac24 Mon Sep 17 00:00:00 2001 From: Paolo Date: Tue, 24 Mar 2026 12:58:56 +0100 Subject: [PATCH] Auto-sync: 20260324_125426 --- .DS_Store | Bin 14340 -> 14340 bytes ios/Podfile.lock | 4 +- lib/logic/ai_engine.dart | 83 +- lib/models/game_board.dart | 31 +- pubspec.yaml | 2 +- report/TetraQ_24-03-26_12.40.txt | 11302 +++++++++++++++++++++++++++++ report/codice_recap.txt | 54 + 7 files changed, 11423 insertions(+), 53 deletions(-) create mode 100644 report/TetraQ_24-03-26_12.40.txt create mode 100644 report/codice_recap.txt diff --git a/.DS_Store b/.DS_Store index 47aeb830f96170dc2113fe05a2d96dc74286caec..887f61bf35d57d4ac1659d2148cdb1d80f6148df 100644 GIT binary patch delta 1721 zcmeHIO>7%Q6rOML`kGN>W7UVTP??CA8n zdjj)*cQ)7&4Efz03Cz!nLY-$r@5t2Qcp|BvFmJRpty}nezCX*es=jR@bJF7$*&XuU z@rlWO%>1yX&K$0p*)Ba|s3U4Tn$c#}ZPAQsIC$X72Jx-cYnr@wukYyE)Ym_-XL!jj zRoB!y7@O1-HJQ;K($uv1cD<90C8G(o8h%k+vYU6-26OIaneA72(mlFjO;e-`0{8K7 zU$ZRlvpieVDt{`1P=MRk$!yHfj;d0$G_idHV`GQ4q#_wsS{~XNVsbbhjj3@-5sP8n zJxty^tELT2PfEK=;C+r9lgAJ0i3BgwD1n=|$npVe-Q7_mGn7)3yr#>g#kX_cq;V*k zR)>xx)pQBn$zURi9lr1LAH_IB*ImYB@5&fIZMuyPsk8M3%`-0F@B23RQ5R z5nlLl54zBe9&Eu7hB1Os?8hN!n1+El97Pt#@C2U32`u0wp2P4Nyo^_H7O&$SoW}*c ziw|%apW$P$zAmE!0of({|cLyXi*SO9$x?9j8$`O?5hF zp1#XhwL-w=qji?9To3LT*tzR3UA6qPuFMDOHOiXIe2!iqmxxPb0X9=kWqw#7lS;uW`!Wz?+<~_wYV0;v;;FOZc=z z-8Y=LpYf~dbb6e(ede>H>113_KFo!dA8nWbyk^Ah%zNF}#YNMRXVO*F2fhX^E3c@u x3F4}%|Ec?LQWu743pgqYf1`6(mdi|gs9Cgb^@w$cm?7pdBcU_-o8GFjKLC6BkX`@) delta 221 zcmZoEXepQ=Ez!onpuod`0yuy?j){dLvdlmRBM_SbF_105z>vw1!;q3voSc)CKe13* zfeEUB1xT|pBr%jCE6C4bn5?cKx3S2HeX@ZG%VKU04n~>Ha$1)dg~8_K02L=e%mOOi z%p;J*zIl^eB;&@0cveQq$q5P)n^hE^ac(U6%Q~6eK!g?S(v5}hnJ4p`D2jrtRsiAz VAO^Yd01z`wb~M?s`L7B)GXP?5Es6jD diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 45a00d9..3614bbb 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1391,7 +1391,7 @@ PODS: - gRPC-Core/Privacy (= 1.69.0) - gRPC-Core/Interface (1.69.0) - gRPC-Core/Privacy (1.69.0) - - GTMSessionFetcher/Core (5.1.0) + - GTMSessionFetcher/Core (5.2.0) - leveldb-library (1.22.6) - nanopb (3.30910.0): - nanopb/decode (= 3.30910.0) @@ -1501,7 +1501,7 @@ SPEC CHECKSUMS: GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 "gRPC-C++": cc207623316fb041a7a3e774c252cf68a058b9e8 gRPC-Core: 860978b7db482de8b4f5e10677216309b5ff6330 - GTMSessionFetcher: b8ab00db932816e14b0a0664a08cb73dda6d164b + GTMSessionFetcher: 904bdd2a82c635bcd6f44edf94cc8775c5d1d6e6 leveldb-library: cc8b8f8e013647a295ad3f8cd2ddf49a6f19be19 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 diff --git a/lib/logic/ai_engine.dart b/lib/logic/ai_engine.dart index 921e3bf..4e60667 100644 --- a/lib/logic/ai_engine.dart +++ b/lib/logic/ai_engine.dart @@ -9,7 +9,7 @@ class _ClosureResult { final bool closesSomething; final int netValue; final bool causesSwap; - final bool isIceTrap; // NUOVO: Identifica le mosse suicide sul ghiaccio + final bool isIceTrap; _ClosureResult(this.closesSomething, this.netValue, this.causesSwap, this.isIceTrap); } @@ -30,24 +30,30 @@ class AIEngine { int myScore = board.currentPlayer == Player.red ? board.scoreRed : board.scoreBlue; int oppScore = board.currentPlayer == Player.red ? board.scoreBlue : board.scoreRed; + // --- NUOVA LOGICA: GESTIONE INVERSIONE (TACTICAL FEEDING) --- + // Se c'è un numero dispari di caselle Scambio aperte, il gioco è "invertito". + // I punti accumulati andranno in regalo all'avversario! + int swapCount = board.boxes.where((b) => b.type == BoxType.swap && !b.isClosed()).length; + bool isInverted = swapCount % 2 != 0; + List goodClosingMoves = []; List badClosingMoves = []; - List iceTraps = []; // Le mosse da evitare assolutamente + List iceTraps = []; for (var line in availableLines) { - var result = _checkClosure(board, line); + var result = _checkClosure(board, line, isInverted); if (result.isIceTrap) { - iceTraps.add(line); // Segna la linea come trappola e passa alla prossima + iceTraps.add(line); continue; } if (result.closesSomething) { if (result.causesSwap) { if (myScore < oppScore) { - goodClosingMoves.add(line); + goodClosingMoves.add(line); // Se perdiamo, lo scambio è la mossa vincente! } else { - badClosingMoves.add(line); + badClosingMoves.add(line); // Se vinciamo, NON tocchiamo lo scambio! } } else { if (result.netValue >= 0) { @@ -66,10 +72,10 @@ class AIEngine { } } - // --- REGOLA 2: Mosse Sicure (Ora include le esche del ghiaccio!) --- + // --- REGOLA 2: Mosse Sicure --- List safeMoves = []; for (var line in availableLines) { - if (!badClosingMoves.contains(line) && !goodClosingMoves.contains(line) && !iceTraps.contains(line) && _isSafeMove(board, line, myScore, oppScore)) { + if (!badClosingMoves.contains(line) && !goodClosingMoves.contains(line) && !iceTraps.contains(line) && _isSafeMove(board, line, myScore, oppScore, isInverted)) { safeMoves.add(line); } } @@ -92,17 +98,16 @@ class AIEngine { } } - // Ultima spiaggia prima del disastro: qualsiasi cosa tranne bombe e trappole ghiacciate + // Ultima spiaggia List nonTerribleMoves = availableLines.where((l) => !badClosingMoves.contains(l) && !iceTraps.contains(l)).toList(); if (nonTerribleMoves.isNotEmpty) { return nonTerribleMoves[random.nextInt(nonTerribleMoves.length)]; } - // Se l'IA è messa all'angolo ed è costretta a suicidarsi... pesca a caso return availableLines[random.nextInt(availableLines.length)]; } - static _ClosureResult _checkClosure(GameBoard board, Line line) { + static _ClosureResult _checkClosure(GameBoard board, Line line, bool isInverted) { int netValue = 0; bool closesSomething = false; bool causesSwap = false; @@ -120,23 +125,27 @@ class AIEngine { if (linesCount == 4) { if (box.type == BoxType.ice && !line.isIceCracked) { - // L'IA capisce che questa mossa non chiuderà il box, ma le farà perdere il turno. isIceTrap = true; } else { closesSomething = true; - if (box.hiddenJokerOwner == board.currentPlayer) { - netValue += 2; + if (box.type == BoxType.swap) { + causesSwap = true; } else { - if (box.type == BoxType.gold) netValue += 2; - else if (box.type == BoxType.bomb) netValue -= 1; - else if (box.type == BoxType.swap) netValue += 0; - else if (box.type == BoxType.ice) netValue += 0; // Rompere il ghiaccio vale 0 punti, ma fa rigiocare - else if (box.type == BoxType.multiplier) netValue += 1; // Leggero boost per dare priorità al x2 - else netValue += 1; - } + int boxValue = 0; + if (box.hiddenJokerOwner == board.currentPlayer) { + boxValue = 2; + } else { + if (box.type == BoxType.gold) boxValue = 2; + else if (box.type == BoxType.bomb) boxValue = -1; + else if (box.type == BoxType.ice) boxValue = 0; + else if (box.type == BoxType.multiplier) boxValue = 1; + else boxValue = 1; + } - if (box.type == BoxType.swap) causesSwap = true; + // LA MAGIA: Se il gioco è invertito, fare punti positivi viene calcolato come MALUS per l'IA! + netValue += isInverted ? -boxValue : boxValue; + } } } } @@ -144,7 +153,7 @@ class AIEngine { return _ClosureResult(closesSomething, netValue, causesSwap, isIceTrap); } - static bool _isSafeMove(GameBoard board, Line line, int myScore, int oppScore) { + static bool _isSafeMove(GameBoard board, Line line, int myScore, int oppScore, bool isInverted) { for (var box in board.boxes) { if (box.type == BoxType.invisible) continue; @@ -156,37 +165,35 @@ class AIEngine { if (box.right.owner != Player.none) currentLinesCount++; if (currentLinesCount == 2) { - // L'IA valuta cosa succede se lascia questa casella con 3 linee all'avversario int valueForOpponent = 0; if (box.type == BoxType.ice) { - // Il ghiaccio è la trappola perfetta. Lasciarlo con 3 linee spingerà l'avversario a incrinarlo e a perdere il turno. - // L'IA valuta questa mossa come SICURISSIMA! valueForOpponent = -5; + } else if (box.type == BoxType.swap) { + if (myScore < oppScore) { + continue; // Sicuro lasciarlo: se lo prende perde i punti. + } else { + return false; // Pericoloso: se lo prende ci ruba il vantaggio! + } } else if (box.hiddenJokerOwner == board.currentPlayer) { valueForOpponent = -1; } else { if (box.type == BoxType.gold) valueForOpponent = 2; else if (box.type == BoxType.bomb) valueForOpponent = -1; - else if (box.type == BoxType.swap) valueForOpponent = 0; else if (box.type == BoxType.multiplier) valueForOpponent = 1; else valueForOpponent = 1; } - // Se per l'avversario è una trappola (bomba o ghiaccio), lascia pure la mossa libera + // LA MAGIA 2: Se il tabellone è invertito, regalare un punto all'avversario è un'ottima esca! + if (isInverted && box.type != BoxType.swap && box.type != BoxType.ice) { + valueForOpponent = -valueForOpponent; + } + if (valueForOpponent < 0) { - continue; + continue; // Mossa considerata sicura (trappola perfetta) } - if (box.type == BoxType.swap) { - if (myScore < oppScore) { - continue; - } else { - return false; - } - } - - return false; // La mossa regalerebbe punti, quindi NON è sicura + return false; } } } diff --git a/lib/models/game_board.dart b/lib/models/game_board.dart index 1165c4a..01f56da 100644 --- a/lib/models/game_board.dart +++ b/lib/models/game_board.dart @@ -5,7 +5,7 @@ import 'dart:math'; enum Player { red, blue, none } -enum BoxType { normal, gold, bomb, invisible, swap, ice, multiplier } // Aggiunti ice e multiplier +enum BoxType { normal, gold, bomb, invisible, swap, ice, multiplier } enum ArenaShape { classic, cross, donut, hourglass, chaos } class Dot { @@ -24,7 +24,7 @@ class Line { final Dot p2; Player owner = Player.none; bool isPlayable = false; - bool isIceCracked = false; // NUOVO: Stato per il blocco di ghiaccio + bool isIceCracked = false; Line(this.p1, this.p2); @@ -55,7 +55,7 @@ class Box { } if (type == BoxType.gold) return 2; if (type == BoxType.bomb) return -1; - if (type == BoxType.swap || type == BoxType.ice || type == BoxType.multiplier) return 0; // Il moltiplicatore e il ghiaccio non danno punti base + if (type == BoxType.swap || type == BoxType.ice || type == BoxType.multiplier) return 0; return 1; } } @@ -80,7 +80,6 @@ class GameBoard { Line? lastMove; - // Variabili per il Moltiplicatore bool redHasMultiplier = false; bool blueHasMultiplier = false; @@ -158,13 +157,23 @@ class GameBoard { if (chance < 0.08) box.type = BoxType.gold; else if (chance > 0.92) box.type = BoxType.bomb; else if (level >= 5 && chance > 0.88 && chance <= 0.92) box.type = BoxType.swap; - else if (level >= 10 && chance > 0.83 && chance <= 0.88) box.type = BoxType.ice; // Nuova Scatola Ghiaccio - else if (level >= 15 && chance > 0.78 && chance <= 0.83) box.type = BoxType.multiplier; // Nuova Scatola x2 + else if (level >= 10 && chance > 0.83 && chance <= 0.88) box.type = BoxType.ice; + else if (level >= 15 && chance > 0.78 && chance <= 0.83) box.type = BoxType.multiplier; } boxes.add(box); } } + // ========================================================= + // NUOVO BLOCCO: ELIMINAZIONE SCAMBI PARI + // ========================================================= + int swapCount = boxes.where((b) => b.type == BoxType.swap).length; + if (swapCount > 0 && swapCount % 2 == 0) { + Box lastSwap = boxes.lastWhere((b) => b.type == BoxType.swap); + lastSwap.type = BoxType.normal; + } + // ========================================================= + for (var box in boxes) { Dot tl = _getOrAddDot(box.x, box.y); Dot tr = _getOrAddDot(box.x + 1, box.y); @@ -220,14 +229,13 @@ class GameBoard { } if (closesIce && !actualLine.isIceCracked) { - actualLine.isIceCracked = true; // Si incrina ma non si chiude! + actualLine.isIceCracked = true; lastMove = actualLine; if (forcedPlayer == null) currentPlayer = (currentPlayer == Player.red) ? Player.blue : Player.red; else currentPlayer = (forcedPlayer == Player.red) ? Player.blue : Player.red; - return true; // Mossa valida, ma turno finito. + return true; } - // Mossa normale o secondo colpo al ghiaccio actualLine.isIceCracked = false; actualLine.owner = playerMakingMove; lastMove = actualLine; @@ -249,13 +257,12 @@ class GameBoard { if (playerMakingMove == Player.red) redHasMultiplier = true; else blueHasMultiplier = true; } else if (points != 0) { - // Se la scatola chiusa dà punti e il giocatore ha un x2 attivo... if (playerMakingMove == Player.red && redHasMultiplier) { points *= 2; - redHasMultiplier = false; // Si consuma + redHasMultiplier = false; } else if (playerMakingMove == Player.blue && blueHasMultiplier) { points *= 2; - blueHasMultiplier = false; // Si consuma + blueHasMultiplier = false; } } diff --git a/pubspec.yaml b/pubspec.yaml index 0eda164..47358e9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: tetraq description: A new Flutter project. publish_to: 'none' -version: 1.1.7+1 +version: 1.1.8+2 environment: sdk: ^3.10.7 diff --git a/report/TetraQ_24-03-26_12.40.txt b/report/TetraQ_24-03-26_12.40.txt new file mode 100644 index 0000000..c9017a2 --- /dev/null +++ b/report/TetraQ_24-03-26_12.40.txt @@ -0,0 +1,11302 @@ +=== FLUTTER PROJECT BACKUP === + +=== PROJECT STRUCTURE (LIB, ASSETS & PUBLIC) === +assets/.DS_Store +assets/audio/.DS_Store +assets/audio/bgm/8-bit_Prowler.mp3 +assets/audio/bgm/Cyber_Dystopia.mp3 +assets/audio/bgm/Grimorio_Astral.mp3 +assets/audio/bgm/Legno_Canopy.mp3 +assets/audio/bgm/Music_Loop.mp3 +assets/audio/bgm/Quad_Dreams.mp3 +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/arcade.jpg +assets/images/cyber_bg.jpg +assets/images/doodle_bg.jpg +assets/images/doodle_bg_riserva.jpg +assets/images/egizi_bg.jpg +assets/images/grimorio copia.jpg +assets/images/grimorio.jpg +assets/images/icona_big.jpeg +assets/images/music_bg.jpg +assets/images/sfondo_temi.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_de.arb +lib/l10n/app_en.arb +lib/l10n/app_es.arb +lib/l10n/app_fr.arb +lib/l10n/app_it.arb +lib/l10n/app_localizations.dart +lib/l10n/app_localizations_de.dart +lib/l10n/app_localizations_en.dart +lib/l10n/app_localizations_es.dart +lib/l10n/app_localizations_fr.dart +lib/l10n/app_localizations_it.dart +lib/l10n/app_localizations_pt.dart +lib/l10n/app_localizations_ru.dart +lib/l10n/app_localizations_zh.dart +lib/l10n/app_pt.arb +lib/l10n/app_ru.arb +lib/l10n/app_zh.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/.DS_Store +lib/ui/admin/admin_screen.dart +lib/ui/game/board_painter.dart +lib/ui/game/game_screen.dart +lib/ui/game/score_board.dart +lib/ui/home/dialog.dart +lib/ui/home/history_screen.dart +lib/ui/home/home_modals.dart +lib/ui/home/home_screen.dart +lib/ui/multiplayer/lobby_screen.dart +lib/ui/multiplayer/lobby_widgets.dart +lib/ui/settings/settings_screen.dart +lib/widgets/custom_button.dart +lib/widgets/custom_settings_button.dart +lib/widgets/cyber_border.dart +lib/widgets/game_over_dialog.dart +lib/widgets/home_buttons.dart +lib/widgets/music_theme_widgets.dart +lib/widgets/painters.dart +public/404.html +public/index.html +public/report.html + +=== pubspec.yaml === +name: tetraq +description: A new Flutter project. +publish_to: 'none' +version: 1.1.7+1 +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 + firebase_auth: ^6.1.4 # <--- NUOVO: LA CORAZZA DI SICUREZZA! + cloud_firestore: ^6.1.2 + share_plus: ^12.0.1 + app_links: ^7.0.0 + google_fonts: ^8.0.2 + font_awesome_flutter: ^10.12.0 + firebase_app_check: ^0.4.1+5 + package_info_plus: ^9.0.0 + device_info_plus: ^12.3.0 + + # --- NUOVI PACCHETTI PER GLI AGGIORNAMENTI --- + upgrader: ^12.5.0 + in_app_update: ^4.2.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^6.0.0 + flutter_launcher_icons: ^0.13.1 + change_app_package_name: ^1.5.0 + +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/audio/ + + +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 --- + + + + + LSApplicationCategoryType + public.app-category.puzzle-games + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + +--- Entitlements --- + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.client + + com.apple.security.network.server + + keychain-access-groups + + + + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.client + + keychain-access-groups + + + +--- Podfile --- +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end + +=== IOS CONFIG === +--- Info.plist --- + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Tetraq + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + tetraq + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + com.sanza.tetraq + CFBundleURLSchemes + + tetraq + + + + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + +--- Podfile --- +# Uncomment this line to define a global platform for your project + platform :ios, '15.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end + +=== ANDROID CONFIG === +--- AndroidManifest.xml --- + + + + + + + + + + + + + + + + + + + + + + + + + +--- build.gradle / build.gradle.kts --- +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") +} + +// Aggiungiamo esplicitamente gli import richiesti da Kotlin +import java.io.FileInputStream + import java.util.Properties + +// Carichiamo il file con le password +val keystoreProperties = Properties() +val keystorePropertiesFile = rootProject.file("key.properties") +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(FileInputStream(keystorePropertiesFile)) +} + +android { + namespace = "com.amastra.tetraq" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + // Sintassi aggiornata come richiesto dal compilatore Kotlin + kotlin { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) + } + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.amastra.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 + } + + // Aggiunto il blocco per la firma in formato Kotlin DSL + signingConfigs { + create("release") { + keyAlias = keystoreProperties.getProperty("keyAlias") + keyPassword = keystoreProperties.getProperty("keyPassword") + val storeFileString = keystoreProperties.getProperty("storeFile") + if (storeFileString != null) { + storeFile = file(storeFileString) + } + storePassword = keystoreProperties.getProperty("storePassword") + } + } + + buildTypes { + getByName("release") { + // TODO: Add your own signing config for the release build. + // Ora usiamo la chiave di release appena creata + signingConfig = signingConfigs.getByName("release") + } + } +} + +flutter { + source = "../.." +} +=== WEB / FIREBASE (public/) === + +// =========================================================================== +// FILE: public/404.html +// =========================================================================== + + + + + + + Page Not Found + + + + +
+

404

+

Page Not Found

+

The specified file was not found on this website. Please check the URL for mistakes and try again.

+

Why am I seeing this?

+

This page was generated by the Firebase Command-Line Interface. To modify it, edit the 404.html file in your project's configured public directory.

+
+ + + +// =========================================================================== +// FILE: public/index.html +// =========================================================================== + + + + + + + + Gioca a TetraQ! + + + + + + + + +

+ Apertura in corso... 🚀 +

+ + +// =========================================================================== +// FILE: public/report.html +// =========================================================================== + + + + + + + Report Giocatori - TetraQ + + + + + + + + + + +
+ +
+ +
+
+
+

📊 Report Statistiche TetraQ

+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ +
+
+

Totale Giocatori

+

0

+
+
+

Apple iOS

+

0

+
+
+

Google Android

+

0

+
+
+

Desktop / Mac

+

0

+
+
+ + + + + + + + + + + + + + +
Ultimo AccessoGiocatoreStatisticheConnessioneDispositivo
Caricamento dati dal database in corso...
+
+
+ + + + +=== SOURCE CODE (lib/) === + +// =========================================================================== +// FILE: lib/core/app_colors.dart +// =========================================================================== + +// =========================================================================== +// FILE: lib/core/app_colors.dart +// =========================================================================== + +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; + +enum AppThemeType { doodle, cyberpunk, arcade, grimorio, music } + +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 doodle = ThemeColors( + background: Color(0xFFFFF9E6), gridLine: Color(0xFFB0BEC5), + playerRed: Color(0xFFD32F2F), playerBlue: Color(0xFF1565C0), text: Color(0xFF37474F), + ); + + static const ThemeColors cyberpunk = ThemeColors( + background: Color(0xFF0A001A), gridLine: Color(0xFF6200EA), + playerRed: Color(0xFFFF007F), playerBlue: Color(0xFF69F0AE), text: Color(0xFFFFFFFF), + ); + + static const ThemeColors arcade = ThemeColors( + background: Color(0xFF111111), gridLine: Color(0xFF00FF00), + playerRed: Color(0xFFFF004D), playerBlue: Color(0xFF00E5FF), text: Color(0xFFFFFFFF), + ); + + static const ThemeColors grimorio = ThemeColors( + background: Color(0xFF1E112A), gridLine: Colors.black, + playerRed: Color(0xFFE91E63), playerBlue: Color(0xFF4FC3F7), text: Color(0xFFFFF3E0), + ); + + static const ThemeColors music = ThemeColors( + background: Color(0xFF120B29), + gridLine: Color(0xFF6A1B9A), + playerRed: Color(0xFFFF2A6D), + playerBlue: Color(0xFF05D5FF), + text: Color(0xFFE0E0E0), + ); + + static ThemeColors getTheme(AppThemeType type) { + switch (type) { + case AppThemeType.doodle: return doodle; + case AppThemeType.cyberpunk: return cyberpunk; + case AppThemeType.arcade: return arcade; + case AppThemeType.grimorio: return grimorio; + case AppThemeType.music: return music; + } + } +} + +class ThemeIcons { + static IconData gold(AppThemeType type) { + switch (type) { + case AppThemeType.doodle: return FontAwesomeIcons.star; + case AppThemeType.cyberpunk: return FontAwesomeIcons.microchip; + case AppThemeType.arcade: return FontAwesomeIcons.coins; + case AppThemeType.grimorio: return FontAwesomeIcons.crown; + case AppThemeType.music: return FontAwesomeIcons.compactDisc; + } + } + + static IconData bomb(AppThemeType type) { + switch (type) { + case AppThemeType.doodle: return FontAwesomeIcons.virus; + case AppThemeType.cyberpunk: return FontAwesomeIcons.bug; + case AppThemeType.arcade: return FontAwesomeIcons.ghost; + case AppThemeType.grimorio: return FontAwesomeIcons.hatWizard; + case AppThemeType.music: return FontAwesomeIcons.volumeXmark; + } + } + + static IconData swap(AppThemeType type) { + switch (type) { + case AppThemeType.doodle: return FontAwesomeIcons.arrowsRotate; + case AppThemeType.cyberpunk: return FontAwesomeIcons.networkWired; + case AppThemeType.arcade: return FontAwesomeIcons.shuffle; + case AppThemeType.grimorio: return FontAwesomeIcons.hurricane; + case AppThemeType.music: return FontAwesomeIcons.sliders; + } + } + + static IconData joker(AppThemeType type) { + switch (type) { + case AppThemeType.doodle: return FontAwesomeIcons.faceSmileBeam; + case AppThemeType.cyberpunk: return FontAwesomeIcons.robot; + case AppThemeType.arcade: return FontAwesomeIcons.gamepad; + case AppThemeType.grimorio: return FontAwesomeIcons.masksTheater; + case AppThemeType.music: return FontAwesomeIcons.headphones; + } + } + + static IconData block(AppThemeType type) { + switch (type) { + case AppThemeType.doodle: return FontAwesomeIcons.squareXmark; + case AppThemeType.cyberpunk: return FontAwesomeIcons.shieldHalved; + case AppThemeType.arcade: return FontAwesomeIcons.powerOff; + case AppThemeType.grimorio: return FontAwesomeIcons.meteor; + case AppThemeType.music: return FontAwesomeIcons.pause; + } + } + + static IconData ice(AppThemeType type) { + if (type == AppThemeType.music) return FontAwesomeIcons.music; + return FontAwesomeIcons.snowflake; + } + + static IconData multiplier(AppThemeType type) { + if (type == AppThemeType.music) return FontAwesomeIcons.forwardFast; + return FontAwesomeIcons.bolt; + } +} +// =========================================================================== +// 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 +// =========================================================================== + +// =========================================================================== +// FILE: lib/core/theme_manager.dart +// =========================================================================== + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'app_colors.dart'; +import '../services/storage_service.dart'; + +// --- ENUM DEI TEMI AGGIORNATO --- +const Map themeIcons = { + AppThemeType.cyberpunk: Icons.electric_bolt, + AppThemeType.doodle: Icons.brush, + AppThemeType.music: Icons.headset_mic, + AppThemeType.arcade: Icons.videogame_asset, + AppThemeType.grimorio: Icons.auto_stories, +}; + +const Map themeNames = { + AppThemeType.cyberpunk: "Cyberpunk", + AppThemeType.doodle: "Doodle", + AppThemeType.music: "Music", + AppThemeType.arcade: "Arcade", + AppThemeType.grimorio: "Grimorio", +}; + +class ThemeManager with ChangeNotifier { + AppThemeType _currentThemeType = AppThemeType.doodle; + ThemeColors _currentColors = AppColors.getTheme(AppThemeType.doodle); + + AppThemeType get currentThemeType => _currentThemeType; + ThemeColors get currentColors => _currentColors; + + ThemeManager() { + _loadTheme(); + } + + void _loadTheme() async { + String themeStr = StorageService.instance.getTheme(); + AppThemeType loadedType = AppThemeType.values.firstWhere( + (e) => e.toString() == themeStr, + orElse: () => AppThemeType.doodle + ); + _currentThemeType = loadedType; + _currentColors = AppColors.getTheme(loadedType); + _updateSystemUI(); + notifyListeners(); + } + + void setTheme(AppThemeType type) { + _currentThemeType = type; + _currentColors = AppColors.getTheme(type); + StorageService.instance.saveTheme(type.toString()); + _updateSystemUI(); + notifyListeners(); + } + + void _updateSystemUI() { + SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: _currentThemeType == AppThemeType.doodle ? Brightness.dark : Brightness.light, + systemNavigationBarColor: _currentColors.background, + systemNavigationBarIconBrightness: _currentThemeType == AppThemeType.doodle ? Brightness.dark : Brightness.light, + )); + } +} +// =========================================================================== +// 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:ceac21bb06b7a9f07b949b', + messagingSenderId: '705460445314', + projectId: 'tetraq-32a4a', + storageBucket: 'tetraq-32a4a.firebasestorage.app', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyB77j18Jgeb9gBAEwp-uyOQvr4m-RJ_rAE', + appId: '1:705460445314:ios:54d64cb7592954327b949b', + messagingSenderId: '705460445314', + projectId: 'tetraq-32a4a', + storageBucket: 'tetraq-32a4a.firebasestorage.app', + iosBundleId: 'com.amastra.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/l10n/app_localizations.dart +// =========================================================================== + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/intl.dart' as intl; + +import 'app_localizations_de.dart'; +import 'app_localizations_en.dart'; +import 'app_localizations_es.dart'; +import 'app_localizations_fr.dart'; +import 'app_localizations_it.dart'; +import 'app_localizations_pt.dart'; +import 'app_localizations_ru.dart'; +import 'app_localizations_zh.dart'; + +// ignore_for_file: type=lint + +/// Callers can lookup localized strings with an instance of AppLocalizations +/// returned by `AppLocalizations.of(context)`. +/// +/// Applications need to include `AppLocalizations.delegate()` in their app's +/// `localizationDelegates` list, and the locales they support in the app's +/// `supportedLocales` list. For example: +/// +/// ```dart +/// import 'l10n/app_localizations.dart'; +/// +/// return MaterialApp( +/// localizationsDelegates: AppLocalizations.localizationsDelegates, +/// supportedLocales: AppLocalizations.supportedLocales, +/// home: MyApplicationHome(), +/// ); +/// ``` +/// +/// ## Update pubspec.yaml +/// +/// Please make sure to update your pubspec.yaml to include the following +/// packages: +/// +/// ```yaml +/// dependencies: +/// # Internationalization support. +/// flutter_localizations: +/// sdk: flutter +/// intl: any # Use the pinned version from flutter_localizations +/// +/// # Rest of dependencies +/// ``` +/// +/// ## iOS Applications +/// +/// iOS applications define key application metadata, including supported +/// locales, in an Info.plist file that is built into the application bundle. +/// To configure the locales supported by your app, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s Runner folder. +/// +/// Next, select the Information Property List item, select Add Item from the +/// Editor menu, then select Localizations from the pop-up menu. +/// +/// Select and expand the newly-created Localizations item then, for each +/// locale your application supports, add a new item and select the locale +/// you wish to add from the pop-up menu in the Value field. This list should +/// be consistent with the languages listed in the AppLocalizations.supportedLocales +/// property. +abstract class AppLocalizations { + AppLocalizations(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + + final String localeName; + + static AppLocalizations? of(BuildContext context) { + return Localizations.of(context, AppLocalizations); + } + + static const LocalizationsDelegate delegate = + _AppLocalizationsDelegate(); + + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static const List> localizationsDelegates = + >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [ + Locale('de'), + Locale('en'), + Locale('es'), + Locale('fr'), + Locale('it'), + Locale('pt'), + Locale('ru'), + Locale('zh'), + ]; + + /// No description provided for @appTitle. + /// + /// In it, this message translates to: + /// **'TetraQ'** + String get appTitle; + + /// No description provided for @welcomeTitle. + /// + /// In it, this message translates to: + /// **'BENVENUTO IN TETRAQ!'** + String get welcomeTitle; + + /// No description provided for @nameHint. + /// + /// In it, this message translates to: + /// **'NOME'** + String get nameHint; + + /// No description provided for @saveAndPlay. + /// + /// In it, this message translates to: + /// **'SALVA E GIOCA'** + String get saveAndPlay; + + /// No description provided for @onlineTitle. + /// + /// In it, this message translates to: + /// **'ONLINE'** + String get onlineTitle; + + /// No description provided for @onlineSub. + /// + /// In it, this message translates to: + /// **'Sfida il mondo'** + String get onlineSub; + + /// No description provided for @cpuTitle. + /// + /// In it, this message translates to: + /// **'VS CPU'** + String get cpuTitle; + + /// No description provided for @cpuSub. + /// + /// In it, this message translates to: + /// **'Allenati con l\'IA'** + String get cpuSub; + + /// No description provided for @localTitle. + /// + /// In it, this message translates to: + /// **'LOCALE'** + String get localTitle; + + /// No description provided for @localSub. + /// + /// In it, this message translates to: + /// **'Stesso schermo'** + String get localSub; + + /// No description provided for @leaderboardTitle. + /// + /// In it, this message translates to: + /// **'CLASSIFICA'** + String get leaderboardTitle; + + /// No description provided for @questsTitle. + /// + /// In it, this message translates to: + /// **'SFIDE'** + String get questsTitle; + + /// No description provided for @themesTitle. + /// + /// In it, this message translates to: + /// **'TEMI'** + String get themesTitle; + + /// No description provided for @tutorialTitle. + /// + /// In it, this message translates to: + /// **'TUTORIAL'** + String get tutorialTitle; + + /// No description provided for @startGame. + /// + /// In it, this message translates to: + /// **'AVVIA PARTITA'** + String get startGame; + + /// No description provided for @createMatch. + /// + /// In it, this message translates to: + /// **'CREA PARTITA'** + String get createMatch; + + /// No description provided for @joinMatch. + /// + /// In it, this message translates to: + /// **'UNISCITI'** + String get joinMatch; + + /// No description provided for @gameOver. + /// + /// In it, this message translates to: + /// **'FINE PARTITA'** + String get gameOver; + + /// No description provided for @mainMenu. + /// + /// In it, this message translates to: + /// **'TORNA AL MENU'** + String get mainMenu; + + /// No description provided for @exit. + /// + /// In it, this message translates to: + /// **'ESCI'** + String get exit; + + /// No description provided for @roomSettings. + /// + /// In it, this message translates to: + /// **'IMPOSTAZIONI STANZA'** + String get roomSettings; + + /// No description provided for @arenaShape. + /// + /// In it, this message translates to: + /// **'FORMA ARENA'** + String get arenaShape; + + /// No description provided for @arenaSize. + /// + /// In it, this message translates to: + /// **'GRANDEZZA'** + String get arenaSize; + + /// No description provided for @timeAndOptions. + /// + /// In it, this message translates to: + /// **'TEMPO E OPZIONI'** + String get timeAndOptions; + + /// No description provided for @timeLabel. + /// + /// In it, this message translates to: + /// **'TEMPO'** + String get timeLabel; + + /// No description provided for @btnStart. + /// + /// In it, this message translates to: + /// **'AVVIA'** + String get btnStart; + + /// No description provided for @btnCancel. + /// + /// In it, this message translates to: + /// **'ANNULLA'** + String get btnCancel; + + /// No description provided for @wordOr. + /// + /// In it, this message translates to: + /// **'OPPURE'** + String get wordOr; + + /// No description provided for @codeHint. + /// + /// In it, this message translates to: + /// **'CODICE'** + String get codeHint; + + /// No description provided for @publicLobbyTitle. + /// + /// In it, this message translates to: + /// **'LOBBY PUBBLICA'** + String get publicLobbyTitle; + + /// No description provided for @emptyLobbyMsg. + /// + /// In it, this message translates to: + /// **'Nessuna stanza pubblica al momento.\nCreane una tu!'** + String get emptyLobbyMsg; + + /// No description provided for @roomOf. + /// + /// In it, this message translates to: + /// **'Stanza di'** + String get roomOf; + + /// No description provided for @btnEnter. + /// + /// In it, this message translates to: + /// **'ENTRA'** + String get btnEnter; +} + +class _AppLocalizationsDelegate + extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => [ + 'de', + 'en', + 'es', + 'fr', + 'it', + 'pt', + 'ru', + 'zh', + ].contains(locale.languageCode); + + @override + bool shouldReload(_AppLocalizationsDelegate old) => false; +} + +AppLocalizations lookupAppLocalizations(Locale locale) { + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'de': + return AppLocalizationsDe(); + case 'en': + return AppLocalizationsEn(); + case 'es': + return AppLocalizationsEs(); + case 'fr': + return AppLocalizationsFr(); + case 'it': + return AppLocalizationsIt(); + case 'pt': + return AppLocalizationsPt(); + case 'ru': + return AppLocalizationsRu(); + case 'zh': + return AppLocalizationsZh(); + } + + throw FlutterError( + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.', + ); +} + +// =========================================================================== +// FILE: lib/l10n/app_localizations_de.dart +// =========================================================================== + +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for German (`de`). +class AppLocalizationsDe extends AppLocalizations { + AppLocalizationsDe([String locale = 'de']) : super(locale); + + @override + String get appTitle => 'TetraQ'; + + @override + String get welcomeTitle => 'WILLKOMMEN BEI TETRAQ!'; + + @override + String get nameHint => 'NAME'; + + @override + String get saveAndPlay => 'SPEICHERN & SPIELEN'; + + @override + String get onlineTitle => 'ONLINE'; + + @override + String get onlineSub => 'Fordere die Welt heraus'; + + @override + String get cpuTitle => 'VS CPU'; + + @override + String get cpuSub => 'Trainiere mit KI'; + + @override + String get localTitle => 'LOKAL'; + + @override + String get localSub => 'Gleicher Bildschirm'; + + @override + String get leaderboardTitle => 'RANGLISTE'; + + @override + String get questsTitle => 'MISSIONEN'; + + @override + String get themesTitle => 'THEMEN'; + + @override + String get tutorialTitle => 'TUTORIAL'; + + @override + String get startGame => 'SPIEL STARTEN'; + + @override + String get createMatch => 'SPIEL ERSTELLEN'; + + @override + String get joinMatch => 'BEITRETEN'; + + @override + String get gameOver => 'SPIELENDE'; + + @override + String get mainMenu => 'ZURÜCK ZUM MENÜ'; + + @override + String get exit => 'BEENDEN'; + + @override + String get roomSettings => 'IMPOSTAZIONI STANZA'; + + @override + String get arenaShape => 'FORMA ARENA'; + + @override + String get arenaSize => 'GRANDEZZA'; + + @override + String get timeAndOptions => 'TEMPO E OPZIONI'; + + @override + String get timeLabel => 'TEMPO'; + + @override + String get btnStart => 'AVVIA'; + + @override + String get btnCancel => 'ANNULLA'; + + @override + String get wordOr => 'OPPURE'; + + @override + String get codeHint => 'CODICE'; + + @override + String get publicLobbyTitle => 'LOBBY PUBBLICA'; + + @override + String get emptyLobbyMsg => + 'Nessuna stanza pubblica al momento.\nCreane una tu!'; + + @override + String get roomOf => 'Stanza di'; + + @override + String get btnEnter => 'ENTRA'; +} + +// =========================================================================== +// FILE: lib/l10n/app_localizations_en.dart +// =========================================================================== + +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for English (`en`). +class AppLocalizationsEn extends AppLocalizations { + AppLocalizationsEn([String locale = 'en']) : super(locale); + + @override + String get appTitle => 'TetraQ'; + + @override + String get welcomeTitle => 'WELCOME TO TETRAQ!'; + + @override + String get nameHint => 'NAME'; + + @override + String get saveAndPlay => 'SAVE & PLAY'; + + @override + String get onlineTitle => 'ONLINE'; + + @override + String get onlineSub => 'Challenge the world'; + + @override + String get cpuTitle => 'VS CPU'; + + @override + String get cpuSub => 'Train with AI'; + + @override + String get localTitle => 'LOCAL'; + + @override + String get localSub => 'Same screen'; + + @override + String get leaderboardTitle => 'LEADERBOARD'; + + @override + String get questsTitle => 'QUESTS'; + + @override + String get themesTitle => 'THEMES'; + + @override + String get tutorialTitle => 'TUTORIAL'; + + @override + String get startGame => 'START GAME'; + + @override + String get createMatch => 'CREATE MATCH'; + + @override + String get joinMatch => 'JOIN'; + + @override + String get gameOver => 'GAME OVER'; + + @override + String get mainMenu => 'BACK TO MENU'; + + @override + String get exit => 'EXIT'; + + @override + String get roomSettings => 'ROOM SETTINGS'; + + @override + String get arenaShape => 'ARENA SHAPE'; + + @override + String get arenaSize => 'SIZE'; + + @override + String get timeAndOptions => 'TIME & OPTIONS'; + + @override + String get timeLabel => 'TIME'; + + @override + String get btnStart => 'START'; + + @override + String get btnCancel => 'CANCEL'; + + @override + String get wordOr => 'OR'; + + @override + String get codeHint => 'CODE'; + + @override + String get publicLobbyTitle => 'PUBLIC LOBBY'; + + @override + String get emptyLobbyMsg => 'No public rooms right now.\nCreate one!'; + + @override + String get roomOf => 'Room of'; + + @override + String get btnEnter => 'ENTER'; +} + +// =========================================================================== +// FILE: lib/l10n/app_localizations_es.dart +// =========================================================================== + +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Spanish Castilian (`es`). +class AppLocalizationsEs extends AppLocalizations { + AppLocalizationsEs([String locale = 'es']) : super(locale); + + @override + String get appTitle => 'TetraQ'; + + @override + String get welcomeTitle => '¡BIENVENIDO A TETRAQ!'; + + @override + String get nameHint => 'NOMBRE'; + + @override + String get saveAndPlay => 'GUARDAR Y JUGAR'; + + @override + String get onlineTitle => 'ONLINE'; + + @override + String get onlineSub => 'Desafía al mundo'; + + @override + String get cpuTitle => 'VS CPU'; + + @override + String get cpuSub => 'Entrena con IA'; + + @override + String get localTitle => 'LOCAL'; + + @override + String get localSub => 'Misma pantalla'; + + @override + String get leaderboardTitle => 'RANKING'; + + @override + String get questsTitle => 'MISIONES'; + + @override + String get themesTitle => 'TEMAS'; + + @override + String get tutorialTitle => 'TUTORIAL'; + + @override + String get startGame => 'INICIAR JUEGO'; + + @override + String get createMatch => 'CREAR PARTIDA'; + + @override + String get joinMatch => 'UNIRSE'; + + @override + String get gameOver => 'FIN DEL JUEGO'; + + @override + String get mainMenu => 'VOLVER AL MENÚ'; + + @override + String get exit => 'SALIR'; + + @override + String get roomSettings => 'IMPOSTAZIONI STANZA'; + + @override + String get arenaShape => 'FORMA ARENA'; + + @override + String get arenaSize => 'GRANDEZZA'; + + @override + String get timeAndOptions => 'TEMPO E OPZIONI'; + + @override + String get timeLabel => 'TEMPO'; + + @override + String get btnStart => 'AVVIA'; + + @override + String get btnCancel => 'ANNULLA'; + + @override + String get wordOr => 'OPPURE'; + + @override + String get codeHint => 'CODICE'; + + @override + String get publicLobbyTitle => 'LOBBY PUBBLICA'; + + @override + String get emptyLobbyMsg => + 'Nessuna stanza pubblica al momento.\nCreane una tu!'; + + @override + String get roomOf => 'Stanza di'; + + @override + String get btnEnter => 'ENTRA'; +} + +// =========================================================================== +// FILE: lib/l10n/app_localizations_fr.dart +// =========================================================================== + +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for French (`fr`). +class AppLocalizationsFr extends AppLocalizations { + AppLocalizationsFr([String locale = 'fr']) : super(locale); + + @override + String get appTitle => 'TetraQ'; + + @override + String get welcomeTitle => 'BIENVENUE DANS TETRAQ !'; + + @override + String get nameHint => 'NOM'; + + @override + String get saveAndPlay => 'SAUVEGARDER ET JOUER'; + + @override + String get onlineTitle => 'EN LIGNE'; + + @override + String get onlineSub => 'Défiez le monde'; + + @override + String get cpuTitle => 'VS CPU'; + + @override + String get cpuSub => 'Entraînez avec l\'IA'; + + @override + String get localTitle => 'LOCAL'; + + @override + String get localSub => 'Même écran'; + + @override + String get leaderboardTitle => 'CLASSEMENT'; + + @override + String get questsTitle => 'QUÊTES'; + + @override + String get themesTitle => 'THÈMES'; + + @override + String get tutorialTitle => 'TUTORIEL'; + + @override + String get startGame => 'JOUER'; + + @override + String get createMatch => 'CRÉER UN MATCH'; + + @override + String get joinMatch => 'REJOINDRE'; + + @override + String get gameOver => 'FIN DE PARTIE'; + + @override + String get mainMenu => 'RETOUR AU MENU'; + + @override + String get exit => 'QUITTER'; + + @override + String get roomSettings => 'IMPOSTAZIONI STANZA'; + + @override + String get arenaShape => 'FORMA ARENA'; + + @override + String get arenaSize => 'GRANDEZZA'; + + @override + String get timeAndOptions => 'TEMPO E OPZIONI'; + + @override + String get timeLabel => 'TEMPO'; + + @override + String get btnStart => 'AVVIA'; + + @override + String get btnCancel => 'ANNULLA'; + + @override + String get wordOr => 'OPPURE'; + + @override + String get codeHint => 'CODICE'; + + @override + String get publicLobbyTitle => 'LOBBY PUBBLICA'; + + @override + String get emptyLobbyMsg => + 'Nessuna stanza pubblica al momento.\nCreane una tu!'; + + @override + String get roomOf => 'Stanza di'; + + @override + String get btnEnter => 'ENTRA'; +} + +// =========================================================================== +// FILE: lib/l10n/app_localizations_it.dart +// =========================================================================== + +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Italian (`it`). +class AppLocalizationsIt extends AppLocalizations { + AppLocalizationsIt([String locale = 'it']) : super(locale); + + @override + String get appTitle => 'TetraQ'; + + @override + String get welcomeTitle => 'BENVENUTO IN TETRAQ!'; + + @override + String get nameHint => 'NOME'; + + @override + String get saveAndPlay => 'SALVA E GIOCA'; + + @override + String get onlineTitle => 'ONLINE'; + + @override + String get onlineSub => 'Sfida il mondo'; + + @override + String get cpuTitle => 'VS CPU'; + + @override + String get cpuSub => 'Allenati con l\'IA'; + + @override + String get localTitle => 'LOCALE'; + + @override + String get localSub => 'Stesso schermo'; + + @override + String get leaderboardTitle => 'CLASSIFICA'; + + @override + String get questsTitle => 'SFIDE'; + + @override + String get themesTitle => 'TEMI'; + + @override + String get tutorialTitle => 'TUTORIAL'; + + @override + String get startGame => 'AVVIA PARTITA'; + + @override + String get createMatch => 'CREA PARTITA'; + + @override + String get joinMatch => 'UNISCITI'; + + @override + String get gameOver => 'FINE PARTITA'; + + @override + String get mainMenu => 'TORNA AL MENU'; + + @override + String get exit => 'ESCI'; + + @override + String get roomSettings => 'IMPOSTAZIONI STANZA'; + + @override + String get arenaShape => 'FORMA ARENA'; + + @override + String get arenaSize => 'GRANDEZZA'; + + @override + String get timeAndOptions => 'TEMPO E OPZIONI'; + + @override + String get timeLabel => 'TEMPO'; + + @override + String get btnStart => 'AVVIA'; + + @override + String get btnCancel => 'ANNULLA'; + + @override + String get wordOr => 'OPPURE'; + + @override + String get codeHint => 'CODICE'; + + @override + String get publicLobbyTitle => 'LOBBY PUBBLICA'; + + @override + String get emptyLobbyMsg => + 'Nessuna stanza pubblica al momento.\nCreane una tu!'; + + @override + String get roomOf => 'Stanza di'; + + @override + String get btnEnter => 'ENTRA'; +} + +// =========================================================================== +// FILE: lib/l10n/app_localizations_pt.dart +// =========================================================================== + +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Portuguese (`pt`). +class AppLocalizationsPt extends AppLocalizations { + AppLocalizationsPt([String locale = 'pt']) : super(locale); + + @override + String get appTitle => 'TetraQ'; + + @override + String get welcomeTitle => 'BEM-VINDO AO TETRAQ!'; + + @override + String get nameHint => 'NOME'; + + @override + String get saveAndPlay => 'SALVAR E JOGAR'; + + @override + String get onlineTitle => 'ONLINE'; + + @override + String get onlineSub => 'Desafie o mundo'; + + @override + String get cpuTitle => 'VS CPU'; + + @override + String get cpuSub => 'Treine com a IA'; + + @override + String get localTitle => 'LOCAL'; + + @override + String get localSub => 'Mesma tela'; + + @override + String get leaderboardTitle => 'CLASSIFICAÇÃO'; + + @override + String get questsTitle => 'DESAFIOS'; + + @override + String get themesTitle => 'TEMAS'; + + @override + String get tutorialTitle => 'TUTORIAL'; + + @override + String get startGame => 'INICIAR JOGO'; + + @override + String get createMatch => 'CRIAR PARTIDA'; + + @override + String get joinMatch => 'ENTRAR'; + + @override + String get gameOver => 'FIM DE JOGO'; + + @override + String get mainMenu => 'VOLTAR AO MENU'; + + @override + String get exit => 'SAIR'; + + @override + String get roomSettings => 'IMPOSTAZIONI STANZA'; + + @override + String get arenaShape => 'FORMA ARENA'; + + @override + String get arenaSize => 'GRANDEZZA'; + + @override + String get timeAndOptions => 'TEMPO E OPZIONI'; + + @override + String get timeLabel => 'TEMPO'; + + @override + String get btnStart => 'AVVIA'; + + @override + String get btnCancel => 'ANNULLA'; + + @override + String get wordOr => 'OPPURE'; + + @override + String get codeHint => 'CODICE'; + + @override + String get publicLobbyTitle => 'LOBBY PUBBLICA'; + + @override + String get emptyLobbyMsg => + 'Nessuna stanza pubblica al momento.\nCreane una tu!'; + + @override + String get roomOf => 'Stanza di'; + + @override + String get btnEnter => 'ENTRA'; +} + +// =========================================================================== +// FILE: lib/l10n/app_localizations_ru.dart +// =========================================================================== + +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Russian (`ru`). +class AppLocalizationsRu extends AppLocalizations { + AppLocalizationsRu([String locale = 'ru']) : super(locale); + + @override + String get appTitle => 'TetraQ'; + + @override + String get welcomeTitle => 'ДОБРО ПОЖАЛОВАТЬ В TETRAQ!'; + + @override + String get nameHint => 'ИМЯ'; + + @override + String get saveAndPlay => 'СОХРАНИТЬ И ИГРАТЬ'; + + @override + String get onlineTitle => 'ОНЛАЙН'; + + @override + String get onlineSub => 'Брось вызов миру'; + + @override + String get cpuTitle => 'VS ИИ'; + + @override + String get cpuSub => 'Тренировка с ИИ'; + + @override + String get localTitle => 'ЛОКАЛЬНО'; + + @override + String get localSub => 'Один экран'; + + @override + String get leaderboardTitle => 'РЕЙТИНГ'; + + @override + String get questsTitle => 'ЗАДАНИЯ'; + + @override + String get themesTitle => 'ТЕМЫ'; + + @override + String get tutorialTitle => 'ОБУЧЕНИЕ'; + + @override + String get startGame => 'НАЧАТЬ ИГРУ'; + + @override + String get createMatch => 'СОЗДАТЬ ИГРУ'; + + @override + String get joinMatch => 'ПРИСОЕДИНИТЬСЯ'; + + @override + String get gameOver => 'ИГРА ОКОНЧЕНА'; + + @override + String get mainMenu => 'В ГЛАВНОЕ МЕНЮ'; + + @override + String get exit => 'ВЫХОД'; + + @override + String get roomSettings => 'IMPOSTAZIONI STANZA'; + + @override + String get arenaShape => 'FORMA ARENA'; + + @override + String get arenaSize => 'GRANDEZZA'; + + @override + String get timeAndOptions => 'TEMPO E OPZIONI'; + + @override + String get timeLabel => 'TEMPO'; + + @override + String get btnStart => 'AVVIA'; + + @override + String get btnCancel => 'ANNULLA'; + + @override + String get wordOr => 'OPPURE'; + + @override + String get codeHint => 'CODICE'; + + @override + String get publicLobbyTitle => 'LOBBY PUBBLICA'; + + @override + String get emptyLobbyMsg => + 'Nessuna stanza pubblica al momento.\nCreane una tu!'; + + @override + String get roomOf => 'Stanza di'; + + @override + String get btnEnter => 'ENTRA'; +} + +// =========================================================================== +// FILE: lib/l10n/app_localizations_zh.dart +// =========================================================================== + +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Chinese (`zh`). +class AppLocalizationsZh extends AppLocalizations { + AppLocalizationsZh([String locale = 'zh']) : super(locale); + + @override + String get appTitle => 'TetraQ'; + + @override + String get welcomeTitle => '欢迎来到 TETRAQ!'; + + @override + String get nameHint => '名字'; + + @override + String get saveAndPlay => '保存并开始'; + + @override + String get onlineTitle => '在线匹配'; + + @override + String get onlineSub => '挑战世界'; + + @override + String get cpuTitle => '人机对战'; + + @override + String get cpuSub => '与AI训练'; + + @override + String get localTitle => '本地游戏'; + + @override + String get localSub => '同屏对战'; + + @override + String get leaderboardTitle => '排行榜'; + + @override + String get questsTitle => '任务'; + + @override + String get themesTitle => '主题'; + + @override + String get tutorialTitle => '教程'; + + @override + String get startGame => '开始游戏'; + + @override + String get createMatch => '创建比赛'; + + @override + String get joinMatch => '加入'; + + @override + String get gameOver => '游戏结束'; + + @override + String get mainMenu => '返回主菜单'; + + @override + String get exit => '退出'; + + @override + String get roomSettings => 'IMPOSTAZIONI STANZA'; + + @override + String get arenaShape => 'FORMA ARENA'; + + @override + String get arenaSize => 'GRANDEZZA'; + + @override + String get timeAndOptions => 'TEMPO E OPZIONI'; + + @override + String get timeLabel => 'TEMPO'; + + @override + String get btnStart => 'AVVIA'; + + @override + String get btnCancel => 'ANNULLA'; + + @override + String get wordOr => 'OPPURE'; + + @override + String get codeHint => 'CODICE'; + + @override + String get publicLobbyTitle => 'LOBBY PUBBLICA'; + + @override + String get emptyLobbyMsg => + 'Nessuna stanza pubblica al momento.\nCreane una tu!'; + + @override + String get roomOf => 'Stanza di'; + + @override + String get btnEnter => 'ENTRA'; +} + +// =========================================================================== +// FILE: lib/logic/ai_engine.dart +// =========================================================================== + +// =========================================================================== +// FILE: lib/logic/ai_engine.dart +// =========================================================================== + +import 'dart:math'; +import '../models/game_board.dart'; + +class _ClosureResult { + final bool closesSomething; + final int netValue; + final bool causesSwap; + final bool isIceTrap; // NUOVO: Identifica le mosse suicide sul ghiaccio + + _ClosureResult(this.closesSomething, this.netValue, this.causesSwap, this.isIceTrap); +} + +class AIEngine { + static Line getBestMove(GameBoard board, int level) { + List availableLines = board.lines.where((l) => l.owner == Player.none && l.isPlayable).toList(); + final random = Random(); + + if (availableLines.isEmpty) return board.lines.first; + + // Più il livello è alto, più l'IA è "intelligente" + double smartChance = 0.50 + ((level - 1) * 0.10); + if (smartChance > 1.0) smartChance = 1.0; + + bool beSmart = random.nextDouble() < smartChance; + + int myScore = board.currentPlayer == Player.red ? board.scoreRed : board.scoreBlue; + int oppScore = board.currentPlayer == Player.red ? board.scoreBlue : board.scoreRed; + + List goodClosingMoves = []; + List badClosingMoves = []; + List iceTraps = []; // Le mosse da evitare assolutamente + + for (var line in availableLines) { + var result = _checkClosure(board, line); + + if (result.isIceTrap) { + iceTraps.add(line); // Segna la linea come trappola e passa alla prossima + continue; + } + + if (result.closesSomething) { + if (result.causesSwap) { + if (myScore < oppScore) { + goodClosingMoves.add(line); + } else { + badClosingMoves.add(line); + } + } else { + if (result.netValue >= 0) { + goodClosingMoves.add(line); + } else { + badClosingMoves.add(line); + } + } + } + } + + // --- REGOLA 1: Chiudere i quadrati vantaggiosi --- + if (goodClosingMoves.isNotEmpty) { + if (beSmart || random.nextDouble() < 0.70) { + return goodClosingMoves[random.nextInt(goodClosingMoves.length)]; + } + } + + // --- REGOLA 2: Mosse Sicure (Ora include le esche del ghiaccio!) --- + List safeMoves = []; + for (var line in availableLines) { + if (!badClosingMoves.contains(line) && !goodClosingMoves.contains(line) && !iceTraps.contains(line) && _isSafeMove(board, line, myScore, oppScore)) { + 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: Scegliere il male minore --- + if (beSmart) { + List riskyButNotTerrible = availableLines.where((l) => !badClosingMoves.contains(l) && !goodClosingMoves.contains(l) && !iceTraps.contains(l)).toList(); + if (riskyButNotTerrible.isNotEmpty) { + return riskyButNotTerrible[random.nextInt(riskyButNotTerrible.length)]; + } + } + + // Ultima spiaggia prima del disastro: qualsiasi cosa tranne bombe e trappole ghiacciate + List nonTerribleMoves = availableLines.where((l) => !badClosingMoves.contains(l) && !iceTraps.contains(l)).toList(); + if (nonTerribleMoves.isNotEmpty) { + return nonTerribleMoves[random.nextInt(nonTerribleMoves.length)]; + } + + // Se l'IA è messa all'angolo ed è costretta a suicidarsi... pesca a caso + return availableLines[random.nextInt(availableLines.length)]; + } + + static _ClosureResult _checkClosure(GameBoard board, Line line) { + int netValue = 0; + bool closesSomething = false; + bool causesSwap = false; + bool isIceTrap = false; + + for (var box in board.boxes) { + 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) { + if (box.type == BoxType.ice && !line.isIceCracked) { + // L'IA capisce che questa mossa non chiuderà il box, ma le farà perdere il turno. + isIceTrap = true; + } else { + closesSomething = true; + + if (box.hiddenJokerOwner == board.currentPlayer) { + netValue += 2; + } else { + if (box.type == BoxType.gold) netValue += 2; + else if (box.type == BoxType.bomb) netValue -= 1; + else if (box.type == BoxType.swap) netValue += 0; + else if (box.type == BoxType.ice) netValue += 0; // Rompere il ghiaccio vale 0 punti, ma fa rigiocare + else if (box.type == BoxType.multiplier) netValue += 1; // Leggero boost per dare priorità al x2 + else netValue += 1; + } + + if (box.type == BoxType.swap) causesSwap = true; + } + } + } + } + return _ClosureResult(closesSomething, netValue, causesSwap, isIceTrap); + } + + static bool _isSafeMove(GameBoard board, Line line, int myScore, int oppScore) { + for (var box in board.boxes) { + 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) { + // L'IA valuta cosa succede se lascia questa casella con 3 linee all'avversario + int valueForOpponent = 0; + + if (box.type == BoxType.ice) { + // Il ghiaccio è la trappola perfetta. Lasciarlo con 3 linee spingerà l'avversario a incrinarlo e a perdere il turno. + // L'IA valuta questa mossa come SICURISSIMA! + valueForOpponent = -5; + } else if (box.hiddenJokerOwner == board.currentPlayer) { + valueForOpponent = -1; + } else { + if (box.type == BoxType.gold) valueForOpponent = 2; + else if (box.type == BoxType.bomb) valueForOpponent = -1; + else if (box.type == BoxType.swap) valueForOpponent = 0; + else if (box.type == BoxType.multiplier) valueForOpponent = 1; + else valueForOpponent = 1; + } + + // Se per l'avversario è una trappola (bomba o ghiaccio), lascia pure la mossa libera + if (valueForOpponent < 0) { + continue; + } + + if (box.type == BoxType.swap) { + if (myScore < oppScore) { + continue; + } else { + return false; + } + } + + return false; // La mossa regalerebbe punti, quindi NON è sicura + } + } + } + return true; + } +} +// =========================================================================== +// FILE: lib/logic/game_controller.dart +// =========================================================================== + +// =========================================================================== +// FILE: lib/logic/game_controller.dart +// =========================================================================== + +import 'dart:async'; +import 'dart:math'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../models/game_board.dart'; +export '../models/game_board.dart'; + +import 'ai_engine.dart'; +import '../services/audio_service.dart'; +import '../services/storage_service.dart'; +import '../services/multiplayer_service.dart'; +import '../core/app_colors.dart'; + +class CpuMatchSetup { + final int radius; + final ArenaShape shape; + CpuMatchSetup(this.radius, this.shape); +} + +class GameController extends ChangeNotifier { + late GameBoard board; + bool isVsCPU = false; + bool isCPUThinking = false; + + bool isOnline = false; + String? roomCode; + bool isHost = false; + StreamSubscription? _onlineSubscription; + + bool opponentLeft = false; + bool _hasSavedResult = false; + + Timer? _blitzTimer; + int timeLeft = 10; + int maxTime = 10; + String timeModeSetting = 'fixed'; // 'fixed', 'relax', 'dynamic' + bool get isTimeMode => timeModeSetting != 'relax'; + int consecutiveRematches = 0; // Contatore per la modalità Dinamica + + String effectText = ''; + Color effectColor = Colors.transparent; + Timer? _effectTimer; + + String? myReaction; + String? opponentReaction; + Timer? _myReactionTimer; + Timer? _oppReactionTimer; + + Timestamp? _lastOpponentReactionTime; + + bool rematchRequested = false; + bool opponentWantsRematch = false; + int lastMatchXP = 0; + + static const Map>> rewardsRoadmap = { + 2: [{'title': 'Bomba & Oro', 'desc': 'Appaiono le caselle speciali: Oro (+2) e Bomba (-1)!', 'icon': Icons.stars, 'color': Colors.amber}], + 3: [ + {'title': 'Tema Cyberpunk', 'desc': 'Sbloccato un nuovo tema visivo nelle impostazioni.', 'icon': Icons.palette, 'color': Colors.tealAccent}, + {'title': 'Arena a Croce', 'desc': 'Sbloccata una nuova forma arena più complessa.', 'icon': Icons.add_box, 'color': Colors.blueAccent} + ], + 5: [{'title': 'Scambio', 'desc': 'Nuova casella! Inverte istantaneamente i punteggi.', 'icon': Icons.swap_horiz, 'color': Colors.purpleAccent}], + 7: [ + {'title': 'Tema 8-Bit', 'desc': 'Sbloccato il nostalgico tema sala giochi.', 'icon': Icons.videogame_asset, 'color': Colors.greenAccent}, + {'title': 'Arene Caos', 'desc': 'Generazione procedurale sbloccata. Nessuna partita sarà uguale!', 'icon': Icons.all_inclusive, 'color': Colors.redAccent} + ], + 10: [ + {'title': 'Tema Grimorio', 'desc': 'Sbloccato il tema della magia antica.', 'icon': Icons.auto_stories, 'color': Colors.deepPurpleAccent}, + {'title': 'Blocco di Ghiaccio', 'desc': 'Nuova meccanica! Il ghiaccio richiede due colpi per rompersi.', 'icon': Icons.ac_unit, 'color': Colors.cyanAccent} + ], + 15: [ + {'title': 'Tema Musica', 'desc': 'Sbloccato il tema a tempo di beat.', 'icon': Icons.headphones, 'color': Colors.pinkAccent}, + {'title': 'Moltiplicatore x2', 'desc': 'Nuova casella! Raddoppia i punti della tua prossima conquista.', 'icon': Icons.bolt, 'color': Colors.yellowAccent} + ], + }; + + bool hasLeveledUp = false; + int newlyReachedLevel = 1; + List> unlockedRewards = []; + + bool isSetupPhase = true; + bool myJokerPlaced = false; + bool oppJokerPlaced = false; + Player jokerTurn = Player.red; + + Player get myPlayer => isOnline ? (isHost ? Player.red : Player.blue) : Player.red; + bool get isGameOver => board.isGameOver; + + int cpuLevel = 1; + int currentMatchLevel = 1; + int? currentSeed; + AppThemeType _activeTheme = AppThemeType.doodle; + + String onlineHostName = "ROSSO"; + String onlineGuestName = "BLU"; + ArenaShape onlineShape = ArenaShape.classic; + + GameController({int radius = 3}) { + cpuLevel = StorageService.instance.cpuLevel; + startNewGame(radius); + } + + CpuMatchSetup _getSetupForCpuLevel(int level) { + final rand = Random(); + if (level == 1) return CpuMatchSetup(3, ArenaShape.classic); + if (level == 2) return CpuMatchSetup(4, ArenaShape.classic); + if (level == 3) return CpuMatchSetup(4, ArenaShape.cross); + if (level == 4) return CpuMatchSetup(4, ArenaShape.donut); + if (level == 5) return CpuMatchSetup(5, ArenaShape.classic); + if (level == 6) return CpuMatchSetup(4, ArenaShape.hourglass); + if (level == 7) return CpuMatchSetup(5, ArenaShape.cross); + if (level == 8) return CpuMatchSetup(5, ArenaShape.donut); + if (level == 9) return CpuMatchSetup(5, ArenaShape.hourglass); + + List hardShapes = [ArenaShape.classic, ArenaShape.cross, ArenaShape.donut, ArenaShape.hourglass, ArenaShape.chaos]; + ArenaShape chosenShape = hardShapes[rand.nextInt(hardShapes.length)]; + + int chosenRadius = (chosenShape == ArenaShape.chaos) ? (rand.nextInt(2) + 4) : (rand.nextInt(2) + 5); + return CpuMatchSetup(chosenRadius, chosenShape); + } + + void startNewGame(int radius, {bool vsCPU = false, bool isOnline = false, String? roomCode, bool isHost = false, ArenaShape shape = ArenaShape.classic, String timeMode = 'fixed', bool isRematch = false}) { + _onlineSubscription?.cancel(); + _onlineSubscription = null; + _blitzTimer?.cancel(); + _effectTimer?.cancel(); + effectText = ''; + _hasSavedResult = false; + lastMatchXP = 0; + + hasLeveledUp = false; + unlockedRewards.clear(); + + myReaction = null; + opponentReaction = null; + _lastOpponentReactionTime = null; + rematchRequested = false; + opponentWantsRematch = false; + + isSetupPhase = true; + myJokerPlaced = false; + oppJokerPlaced = false; + jokerTurn = Player.red; + + this.isVsCPU = vsCPU; + this.isOnline = isOnline; + this.roomCode = roomCode; + this.isHost = isHost; + + if (!isRematch) consecutiveRematches = 0; + this.timeModeSetting = timeMode; + + // --- LOGICA TIMER --- + if (this.isVsCPU) { + int pLevel = StorageService.instance.playerLevel; + int calculatedTime = 15 - ((pLevel - 1) * 12 / 14).round(); + maxTime = calculatedTime.clamp(3, 15); + } else { + if (timeModeSetting == 'dynamic') { + maxTime = max(2, 10 - (consecutiveRematches * 2)); + } else if (timeModeSetting == 'relax') { + maxTime = 0; + } else { + maxTime = 10; + } + } + timeLeft = maxTime; + + int finalRadius = radius; + ArenaShape finalShape = shape; + + if (this.isVsCPU) { + CpuMatchSetup setup = _getSetupForCpuLevel(cpuLevel); + finalRadius = setup.radius; + finalShape = setup.shape; + } + + onlineShape = finalShape; + int levelToUse = isOnline ? (currentMatchLevel == 1 ? 2 : currentMatchLevel) : cpuLevel; + + board = GameBoard(radius: finalRadius, level: levelToUse, seed: currentSeed, shape: finalShape); + board.currentPlayer = Player.red; + + isCPUThinking = false; + opponentLeft = false; + + if (this.isOnline && this.roomCode != null) { + _listenToOnlineGame(this.roomCode!); + } + + notifyListeners(); + } + + void placeJoker(int bx, int by) { + if (!isSetupPhase) return; + + Box? target; + try { target = board.boxes.firstWhere((b) => b.x == bx && b.y == by); } catch(e) {} + + if (target == null || target.type == BoxType.invisible || target.hiddenJokerOwner != null) return; + + AudioService.instance.playLineSfx(_activeTheme); + + if (isOnline) { + if (myJokerPlaced) return; + target.hiddenJokerOwner = myPlayer; + myJokerPlaced = true; + + String prefix = isHost ? 'p1' : 'p2'; + FirebaseFirestore.instance.collection('games').doc(roomCode).update({ + '${prefix}_joker': {'x': bx, 'y': by} + }); + } else { + target.hiddenJokerOwner = jokerTurn; + if (jokerTurn == Player.red) { + jokerTurn = Player.blue; + if (isVsCPU) { + _placeCpuJoker(); + } + } else { + jokerTurn = Player.red; + } + } + notifyListeners(); + _checkSetupComplete(); + } + + void _placeCpuJoker() { + var validBoxes = board.boxes.where((b) => b.type != BoxType.invisible && b.hiddenJokerOwner == null).toList(); + if (validBoxes.isNotEmpty) { + var b = validBoxes[Random().nextInt(validBoxes.length)]; + b.hiddenJokerOwner = Player.blue; + } + jokerTurn = Player.red; + _checkSetupComplete(); + } + + void _checkSetupComplete() { + if (isOnline) { + if (myJokerPlaced && oppJokerPlaced) { + isSetupPhase = false; + _startTimer(); + } + } else { + if (jokerTurn == Player.red) { + isSetupPhase = false; + _startTimer(); + } + } + notifyListeners(); + } + + void sendReaction(String reaction) { + if (!isOnline || roomCode == null) return; + MultiplayerService().sendReaction(roomCode!, isHost, reaction); + _showReaction(true, reaction); + } + + void requestRematch() { + if (!isOnline || roomCode == null) return; + rematchRequested = true; + notifyListeners(); + MultiplayerService().requestRematch(roomCode!, isHost); + } + + void _showReaction(bool isMe, String reaction) { + if (isMe) { + myReaction = reaction; + _myReactionTimer?.cancel(); + _myReactionTimer = Timer(const Duration(seconds: 4), () { myReaction = null; notifyListeners(); }); + } else { + opponentReaction = reaction; + _oppReactionTimer?.cancel(); + _oppReactionTimer = Timer(const Duration(seconds: 4), () { opponentReaction = null; notifyListeners(); }); + } + 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 newClosed, {List newGhosts = const [], required bool isOpponent}) { + if (newGhosts.isNotEmpty) { + AudioService.instance.playBombSfx(); + triggerSpecialEffect("TRAPPOLA!", Colors.grey.shade400); + HapticFeedback.heavyImpact(); + return; + } + + bool isIceCracked = board.lastMove?.isIceCracked ?? false; + if (isIceCracked) { + AudioService.instance.playLineSfx(_activeTheme); + triggerSpecialEffect("GHIACCIO INCRINATO!", Colors.cyanAccent); + HapticFeedback.mediumImpact(); + return; + } + + if (newClosed.isEmpty) { + AudioService.instance.playLineSfx(_activeTheme); + if (!isOpponent) HapticFeedback.lightImpact(); + } else { + for (var b in newClosed) { + if (b.isJokerRevealed) { + if (b.owner == b.hiddenJokerOwner) { + AudioService.instance.playBonusSfx(); + triggerSpecialEffect("JOLLY! +2", Colors.greenAccent); + } else { + AudioService.instance.playBombSfx(); + triggerSpecialEffect("JOLLY! -1", Colors.redAccent); + } + HapticFeedback.heavyImpact(); + return; + } + } + + bool isGold = newClosed.any((b) => b.type == BoxType.gold); + bool isBomb = newClosed.any((b) => b.type == BoxType.bomb); + bool isSwap = newClosed.any((b) => b.type == BoxType.swap); + bool isMultiplier = newClosed.any((b) => b.type == BoxType.multiplier); + + if (isSwap) { + AudioService.instance.playBonusSfx(); + triggerSpecialEffect("SCAMBIO!", Colors.purpleAccent); + HapticFeedback.heavyImpact(); + } else if (isMultiplier) { + AudioService.instance.playBonusSfx(); + triggerSpecialEffect("MOLTIPLICATORE x2!", Colors.yellowAccent); + HapticFeedback.heavyImpact(); + } else if (isGold) { + AudioService.instance.playBonusSfx(); triggerSpecialEffect("+2", Colors.amber); HapticFeedback.heavyImpact(); + } else if (isBomb) { + AudioService.instance.playBombSfx(); triggerSpecialEffect("-1", Colors.redAccent); HapticFeedback.heavyImpact(); + } else { + AudioService.instance.playBoxSfx(_activeTheme); HapticFeedback.heavyImpact(); + } + } + } + + void _startTimer() { + _blitzTimer?.cancel(); + if (isSetupPhase || !isTimeMode) return; + + timeLeft = maxTime; + + _blitzTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (isGameOver || isCPUThinking) { timer.cancel(); return; } + if (timeLeft > 0) { + timeLeft--; notifyListeners(); + } else { + timer.cancel(); + if (!isOnline || board.currentPlayer == myPlayer) { _handleTimeOut(); } + } + }); + } + + void _handleTimeOut() { + if (!isTimeMode || isSetupPhase) return; + + if (isOnline && board.currentPlayer != myPlayer) return; + + List availableLines = board.lines.where((l) => l.owner == Player.none && l.isPlayable).toList(); + if (availableLines.isEmpty) return; + + final random = Random(); + Line randomMove = availableLines[random.nextInt(availableLines.length)]; + + handleLineTap(randomMove, _activeTheme, forced: true); + } + + void disconnectOnlineGame() { + _onlineSubscription?.cancel(); + _onlineSubscription = null; + _blitzTimer?.cancel(); + _effectTimer?.cancel(); + _myReactionTimer?.cancel(); + _oppReactionTimer?.cancel(); + _lastOpponentReactionTime = null; + + if (isOnline && roomCode != null) { + FirebaseFirestore.instance.collection('games').doc(roomCode).update({'status': 'abandoned'}).catchError((e) => null); + } + isOnline = false; roomCode = null; currentMatchLevel = 1; currentSeed = null; + } + + @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; + + onlineHostName = data['hostName'] ?? "ROSSO"; + onlineGuestName = (data['guestName'] != null && data['guestName'] != '') ? data['guestName'] : "BLU"; + + // 1. GESTIONE ABBANDONO + if (data['status'] == 'abandoned' && !board.isGameOver && !opponentLeft) { + opponentLeft = true; notifyListeners(); return; + } + + // 2. GESTIONE REAZIONI + String? p1React = data['p1_reaction']; + Timestamp? p1Time = data['p1_reaction_time'] as Timestamp?; + String? p2React = data['p2_reaction']; + Timestamp? p2Time = data['p2_reaction_time'] as Timestamp?; + + if (isHost && p2React != null && p2Time != null && p2Time != _lastOpponentReactionTime) { + _lastOpponentReactionTime = p2Time; + _showReaction(false, p2React); + } else if (!isHost && p1React != null && p1Time != null && p1Time != _lastOpponentReactionTime) { + _lastOpponentReactionTime = p1Time; + _showReaction(false, p1React); + } + + // 3. LOGICA RIVINCITA MIGLIORATA + bool p1Rematch = data['p1_rematch'] ?? false; + bool p2Rematch = data['p2_rematch'] ?? false; + opponentWantsRematch = isHost ? p2Rematch : p1Rematch; + + // SOLO L'HOST si occupa di chiamare resetMatch sul server + if (isHost && p1Rematch && p2Rematch && data['status'] != 'playing') { + currentMatchLevel++; + int newSeed = DateTime.now().millisecondsSinceEpoch % 1000000; + final rand = Random(); + int newRadius = rand.nextInt(4) + 3; + ArenaShape newShape = ArenaShape.values[rand.nextInt(ArenaShape.values.length)]; + + // Questo cambierà lo status in 'playing' e svuoterà l'array moves. + MultiplayerService().resetMatch(roomCode!, newRadius, newShape.name, newSeed); + return; // L'host aspetterà il prossimo trigger dal server con il nuovo seed. + } + + int? hostSeed = data['seed']; + int hostRadius = data['radius'] ?? board.radius; + String shapeStr = data['shape'] ?? 'classic'; + ArenaShape hostShape = ArenaShape.values.firstWhere((e) => e.name == shapeStr, orElse: () => ArenaShape.classic); + String hostTimeMode = data['timeMode'] is String ? data['timeMode'] : (data['timeMode'] == true ? 'fixed' : 'relax'); + + // TUTTI (Host e Guest) ripartono SOLO quando vedono il reset effettivo (nuovo seed e status 'playing') + if (rematchRequested && data['status'] == 'playing' && hostSeed != null && hostSeed != currentSeed) { + currentSeed = hostSeed; + consecutiveRematches++; + startNewGame(hostRadius, isOnline: true, roomCode: roomCode, isHost: isHost, shape: hostShape, timeMode: hostTimeMode, isRematch: true); + return; + } + + // 4. GESTIONE FASE INIZIALE (JOLLY) + if (isSetupPhase) { + if (!isHost && data['p1_joker'] != null && !oppJokerPlaced) { + int jx = data['p1_joker']['x']; int jy = data['p1_joker']['y']; + board.boxes.firstWhere((b) => b.x == jx && b.y == jy).hiddenJokerOwner = Player.red; + oppJokerPlaced = true; _checkSetupComplete(); + } + if (isHost && data['p2_joker'] != null && !oppJokerPlaced) { + int jx = data['p2_joker']['x']; int jy = data['p2_joker']['y']; + board.boxes.firstWhere((b) => b.x == jx && b.y == jy).hiddenJokerOwner = Player.blue; + oppJokerPlaced = true; _checkSetupComplete(); + } + } + + // 5. AGGIORNAMENTO LIVELLO / SEED (se non in rivincita) + int hostLevel = data['matchLevel'] ?? 1; + onlineShape = hostShape; + timeModeSetting = hostTimeMode; + + if (!rematchRequested && (hostLevel > currentMatchLevel || (isOnline && currentSeed == null && hostSeed != null) || (hostSeed != null && hostSeed != currentSeed))) { + currentMatchLevel = hostLevel; currentSeed = hostSeed; + int levelToUse = (currentMatchLevel == 1) ? 2 : currentMatchLevel; + board = GameBoard(radius: hostRadius, level: levelToUse, seed: currentSeed, shape: onlineShape); + board.currentPlayer = Player.red; + isCPUThinking = false; notifyListeners(); return; + } + + // 6. GESTIONE MOSSE + List moves = data['moves'] ?? []; + int firebaseMovesCount = moves.length; + int localMovesCount = board.lines.where((l) => l.owner != Player.none).length; + + // Resilienza: se il locale ha mosse e il server no (e non stiamo aspettando una rivincita), pulisci. + if (firebaseMovesCount == 0 && localMovesCount > 0 && !rematchRequested) { + int levelToUse = (currentMatchLevel == 1) ? 2 : currentMatchLevel; + board = GameBoard(radius: hostRadius, level: levelToUse, seed: currentSeed, shape: onlineShape); + board.currentPlayer = Player.red; + notifyListeners(); return; + } + + // Applica mosse remote + if (firebaseMovesCount > localMovesCount) { + bool newMovesApplied = false; + + 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 closedBefore = board.boxes.where((b) => b.owner != Player.none).toList(); + List ghostsBefore = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed).toList(); + + board.playMove(lineToPlay, forcedPlayer: playerFromFirebase); + newMovesApplied = true; + + List newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList(); + List newGhosts = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed && !ghostsBefore.contains(b)).toList(); + + if (isOpponentMove) _playEffects(newClosed, newGhosts: newGhosts, isOpponent: true); + } + } + + if (newMovesApplied) { + String expectedTurnStr = data['turn'] ?? 'red'; + Player expectedTurn = expectedTurnStr == 'red' ? Player.red : Player.blue; + if (!board.isGameOver && board.currentPlayer != expectedTurn) { board.currentPlayer = expectedTurn; } + _startTimer(); + } + + if (board.isGameOver) _saveMatchResult(); + notifyListeners(); + } + }); + } + + void handleLineTap(Line line, AppThemeType theme, {bool forced = false}) { + if ((isSetupPhase || isCPUThinking || board.isGameOver || opponentLeft) && !forced) return; + if (isOnline && board.currentPlayer != myPlayer && !forced) return; + + _activeTheme = theme; + List closedBefore = board.boxes.where((b) => b.owner != Player.none).toList(); + List ghostsBefore = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed).toList(); + + if (board.playMove(line)) { + List newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList(); + List newGhosts = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed && !ghostsBefore.contains(b)).toList(); + + if (!forced) _playEffects(newClosed, newGhosts: newGhosts, isOpponent: false); + + _startTimer(); notifyListeners(); + + if (isOnline && roomCode != null) { + Map moveData = { + 'id': DateTime.now().millisecondsSinceEpoch, + 'x1': line.p1.x, 'y1': line.p1.y, 'x2': line.p2.x, 'y2': line.p2.y, + 'player': myPlayer == Player.red ? 'red' : 'blue' + }; + String nextTurnStr = board.currentPlayer == Player.red ? 'red' : 'blue'; + + FirebaseFirestore.instance.collection('games').doc(roomCode).update({ + 'moves': FieldValue.arrayUnion([moveData]), + 'turn': nextTurnStr + }).catchError((e) => debugPrint("Errore: $e")); + + if (board.isGameOver) { + _saveMatchResult(); + if (isHost) FirebaseFirestore.instance.collection('games').doc(roomCode).update({'status': 'finished'}); + } + } else { + if (board.isGameOver) _saveMatchResult(); + else if (isVsCPU && board.currentPlayer == Player.blue) _checkCPUTurn(); + } + } + } + + 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 closedBefore = board.boxes.where((b) => b.owner != Player.none).toList(); + List ghostsBefore = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed).toList(); + + Line bestMove = AIEngine.getBestMove(board, cpuLevel); + board.playMove(bestMove); + + List newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList(); + List newGhosts = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed && !ghostsBefore.contains(b)).toList(); + + _playEffects(newClosed, newGhosts: newGhosts, isOpponent: true); + + isCPUThinking = false; _startTimer(); notifyListeners(); + + if (board.isGameOver) _saveMatchResult(); + else _checkCPUTurn(); + } + } + } + + List> _getUnlocks(int oldLevel, int newLevel) { + List> unlocks = []; + for(int i = oldLevel + 1; i <= newLevel; i++) { + if (rewardsRoadmap.containsKey(i)) { + unlocks.addAll(rewardsRoadmap[i]!); + } + } + return unlocks; + } + + Future _saveMatchResult() async { + if (_hasSavedResult) return; + _hasSavedResult = true; + + int calculatedXP = 0; + bool isDraw = board.scoreRed == board.scoreBlue; + String myRealName = StorageService.instance.playerName; + if (myRealName.isEmpty) myRealName = "IO"; + + int oldLevel = StorageService.instance.playerLevel; + + if (isOnline) { + bool isWin = isHost ? board.scoreRed > board.scoreBlue : board.scoreBlue > board.scoreRed; + calculatedXP = isWin ? 20 : (isDraw ? 5 : 2); + String oppName = isHost ? onlineGuestName : onlineHostName; + int myScore = isHost ? board.scoreRed : board.scoreBlue; + int oppScore = isHost ? board.scoreBlue : board.scoreRed; + await StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: oppName, myScore: myScore, oppScore: oppScore, isOnline: true); + + if (isWin) await StorageService.instance.updateQuestProgress(0, 1); + + } else if (isVsCPU) { + int myScore = board.scoreRed; int cpuScore = board.scoreBlue; + bool isWin = myScore > cpuScore; + calculatedXP = isWin ? (10 + (cpuLevel * 2)) : (isDraw ? 5 : 2); + + if (isWin) { + await StorageService.instance.addWin(); + await StorageService.instance.updateQuestProgress(1, 1); + } else if (cpuScore > myScore) { + await StorageService.instance.addLoss(); + } + await StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "CPU (Liv. $cpuLevel)", myScore: myScore, oppScore: cpuScore, isOnline: false); + } else { + calculatedXP = 2; + await StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "Ospite (Locale)", myScore: board.scoreRed, oppScore: board.scoreBlue, isOnline: false); + } + + if (board.shape != ArenaShape.classic) { + await StorageService.instance.updateQuestProgress(2, 1); + } + + lastMatchXP = calculatedXP; + await StorageService.instance.addXP(calculatedXP); + + int newLevel = StorageService.instance.playerLevel; + if (newLevel > oldLevel) { + hasLeveledUp = true; + newlyReachedLevel = newLevel; + unlockedRewards = _getUnlocks(oldLevel, newLevel); + } + + notifyListeners(); + } + + void increaseLevelAndRestart() { + cpuLevel++; StorageService.instance.saveCpuLevel(cpuLevel); + startNewGame(board.radius, vsCPU: true, shape: board.shape, timeMode: timeModeSetting); + } +} +// =========================================================================== +// FILE: lib/main.dart +// =========================================================================== + +// =========================================================================== +// FILE: lib/main.dart +// =========================================================================== + +import 'dart:io' show Platform; +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.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'; +import 'services/audio_service.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'firebase_options.dart'; +import 'package:firebase_app_check/firebase_app_check.dart'; +import 'package:upgrader/upgrader.dart'; +import 'package:in_app_update/in_app_update.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:tetraq/l10n/app_localizations.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + + await FirebaseAppCheck.instance.activate( + androidProvider: kDebugMode ? AndroidProvider.debug : AndroidProvider.playIntegrity, + appleProvider: kDebugMode ? AppleProvider.debug : AppleProvider.deviceCheck, + ); + + try { + // --- BUG FIX: Creiamo l'account anonimo SOLO se non c'è una sessione attiva --- + // In questo modo, una volta fatto il login, non verrai più buttato fuori al riavvio! + if (FirebaseAuth.instance.currentUser == null) { + await FirebaseAuth.instance.signInAnonymously(); + } + } catch (e) { + debugPrint("Errore Auth: $e"); + } + + await StorageService.instance.init(); + await AudioService.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, + ), + + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + + home: UpdateWrapper(child: HomeScreen()), + ); + } +} + +// =========================================================================== +// WIDGET WRAPPER PER LA GESTIONE DEGLI AGGIORNAMENTI IBRIDI (iOS/Android) +// =========================================================================== +class UpdateWrapper extends StatefulWidget { + final Widget child; + const UpdateWrapper({super.key, required this.child}); + + @override + State createState() => _UpdateWrapperState(); +} + +class _UpdateWrapperState extends State { + @override + void initState() { + super.initState(); + if (!kIsWeb && Platform.isAndroid) { + _checkForAndroidUpdate(); + } + } + + Future _checkForAndroidUpdate() async { + try { + final info = await InAppUpdate.checkForUpdate(); + if (info.updateAvailability == UpdateAvailability.updateAvailable) { + if (info.flexibleUpdateAllowed) { + await InAppUpdate.startFlexibleUpdate(); + await InAppUpdate.completeFlexibleUpdate(); + } + else if (info.immediateUpdateAllowed) { + await InAppUpdate.performImmediateUpdate(); + } + } + } catch (e) { + debugPrint("Errore in_app_update Android: $e"); + } + } + + @override + Widget build(BuildContext context) { + if (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) { + return UpgradeAlert( + dialogStyle: (Platform.isIOS || Platform.isMacOS) + ? UpgradeDialogStyle.cupertino + : UpgradeDialogStyle.material, + showIgnore: false, + showLater: true, + upgrader: Upgrader(), + child: widget.child, + ); + } + return widget.child; + } +} +// =========================================================================== +// FILE: lib/models/game_board.dart +// =========================================================================== + +// =========================================================================== +// FILE: lib/models/game_board.dart +// =========================================================================== + +import 'dart:math'; + +enum Player { red, blue, none } +enum BoxType { normal, gold, bomb, invisible, swap, ice, multiplier } // Aggiunti ice e multiplier +enum ArenaShape { classic, cross, donut, hourglass, chaos } + +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; + bool isIceCracked = false; // NUOVO: Stato per il blocco di ghiaccio + + 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; + + bool isRevealed = false; + Player? hiddenJokerOwner; + bool isJokerRevealed = false; + + 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 getCalculatedValue(Player closer) { + if (hiddenJokerOwner != null) { + return (closer == hiddenJokerOwner) ? 2 : -1; + } + if (type == BoxType.gold) return 2; + if (type == BoxType.bomb) return -1; + if (type == BoxType.swap || type == BoxType.ice || type == BoxType.multiplier) return 0; // Il moltiplicatore e il ghiaccio non danno punti base + return 1; + } +} + +class GameBoard { + final int radius; + final int level; + final int? seed; + final ArenaShape shape; + + late int columns; + late int rows; + + List dots = []; + List lines = []; + List boxes = []; + + Player currentPlayer = Player.red; + int scoreRed = 0; + int scoreBlue = 0; + bool isGameOver = false; + + Line? lastMove; + + // Variabili per il Moltiplicatore + bool redHasMultiplier = false; + bool blueHasMultiplier = false; + + GameBoard({required this.radius, this.level = 1, this.seed, this.shape = ArenaShape.classic}) { + _generateBoard(); + } + + void _generateBoard() { + final random = seed != null ? Random(seed) : Random(); + int chaosAlgorithm = random.nextInt(5); + + if (shape == ArenaShape.chaos) { + columns = radius * 2 + 1; + rows = (radius * 3) + 2; + } else { + columns = radius * 2 + 1; + rows = radius * 2 + 1; + } + + dots.clear(); + lines.clear(); + boxes.clear(); + lastMove = null; + + for (int y = 0; y < rows; y++) { + for (int x = 0; x < columns; x++) { + var box = Box(x, y); + bool isVisible = true; + + if (shape != ArenaShape.chaos) { + int dx = (x - radius).abs(); + int dy = (y - radius).abs(); + isVisible = (dx + dy) <= radius; + + if (isVisible) { + switch (shape) { + case ArenaShape.cross: + int spessoreBraccio = radius > 3 ? 1 : 0; + if (dx > spessoreBraccio && dy > spessoreBraccio) isVisible = false; break; + case ArenaShape.donut: + int dimensioneBuco = radius > 3 ? 2 : 1; + if ((dx + dy) <= dimensioneBuco) isVisible = false; break; + case ArenaShape.hourglass: + if (dx > dy) isVisible = false; + if (x == radius && y == radius) isVisible = true; break; + default: break; + } + } + } else { + double percentY = y / rows; + if (chaosAlgorithm == 0) { + isVisible = (x % 2 == 0) && (random.nextDouble() > 0.15); + } else if (chaosAlgorithm == 1) { + double chance = 0.2 + (percentY * 0.7); + isVisible = random.nextDouble() < chance; + } else if (chaosAlgorithm == 2) { + int midY = rows ~/ 2; + int distFromCenterY = (y - midY).abs(); + int allowedWidth = (distFromCenterY / midY * radius).ceil() + 1; + int dx = (x - radius).abs(); + isVisible = dx <= allowedWidth && random.nextDouble() > 0.1; + } else if (chaosAlgorithm == 3) { + isVisible = (y % 2 == 0) ? (x < columns - 1) : (x > 0); + if (random.nextDouble() > 0.8) isVisible = false; + } else if (chaosAlgorithm == 4) { + isVisible = random.nextDouble() > 0.45; + } + if (x == radius && y == rows ~/ 2) isVisible = true; + } + + if (!isVisible) { + box.type = BoxType.invisible; + } else if (level > 1) { + double chance = random.nextDouble(); + if (chance < 0.08) box.type = BoxType.gold; + else if (chance > 0.92) box.type = BoxType.bomb; + else if (level >= 5 && chance > 0.88 && chance <= 0.92) box.type = BoxType.swap; + else if (level >= 10 && chance > 0.83 && chance <= 0.88) box.type = BoxType.ice; // Nuova Scatola Ghiaccio + else if (level >= 15 && chance > 0.78 && chance <= 0.83) box.type = BoxType.multiplier; // Nuova Scatola x2 + } + boxes.add(box); + } + } + + 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); + + 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; + + // --- LOGICA BLOCCO DI GHIACCIO --- + bool closesIce = false; + for (var box in boxes) { + if (box.type == BoxType.ice && box.owner == Player.none) { + int linesCount = 0; + if (box.top.owner != Player.none || box.top == actualLine) linesCount++; + if (box.bottom.owner != Player.none || box.bottom == actualLine) linesCount++; + if (box.left.owner != Player.none || box.left == actualLine) linesCount++; + if (box.right.owner != Player.none || box.right == actualLine) linesCount++; + if (linesCount == 4) closesIce = true; + } + } + + if (closesIce && !actualLine.isIceCracked) { + actualLine.isIceCracked = true; // Si incrina ma non si chiude! + lastMove = actualLine; + if (forcedPlayer == null) currentPlayer = (currentPlayer == Player.red) ? Player.blue : Player.red; + else currentPlayer = (forcedPlayer == Player.red) ? Player.blue : Player.red; + return true; // Mossa valida, ma turno finito. + } + + // Mossa normale o secondo colpo al ghiaccio + actualLine.isIceCracked = false; + actualLine.owner = playerMakingMove; + lastMove = actualLine; + + bool scoredPoint = false; + bool triggeredSwap = false; + + for (var box in boxes) { + if (box.owner == Player.none && box.isClosed()) { + box.owner = playerMakingMove; + scoredPoint = true; + + if (box.hiddenJokerOwner != null) box.isJokerRevealed = true; + + int points = box.getCalculatedValue(playerMakingMove); + + // --- LOGICA MOLTIPLICATORE x2 --- + if (box.type == BoxType.multiplier) { + if (playerMakingMove == Player.red) redHasMultiplier = true; + else blueHasMultiplier = true; + } else if (points != 0) { + // Se la scatola chiusa dà punti e il giocatore ha un x2 attivo... + if (playerMakingMove == Player.red && redHasMultiplier) { + points *= 2; + redHasMultiplier = false; // Si consuma + } else if (playerMakingMove == Player.blue && blueHasMultiplier) { + points *= 2; + blueHasMultiplier = false; // Si consuma + } + } + + if (playerMakingMove == Player.red) { scoreRed += points; } + else { scoreBlue += points; } + + if (box.type == BoxType.swap && box.hiddenJokerOwner == null) { + triggeredSwap = true; + } + } + + if (box.type == BoxType.invisible && !box.isRevealed) { + if (box.top.owner != Player.none && box.bottom.owner != Player.none && + box.left.owner != Player.none && box.right.owner != Player.none) { + box.isRevealed = true; + } + } + } + + if (triggeredSwap) { + int temp = scoreRed; scoreRed = scoreBlue; scoreBlue = temp; + } + + if (lines.where((l) => l.isPlayable).every((l) => l.owner != Player.none)) { isGameOver = true; } + + if (forcedPlayer == null) { + if (!scoredPoint && !isGameOver) { currentPlayer = (currentPlayer == Player.red) ? Player.blue : Player.red; } + else if (scoredPoint && !isGameOver) { currentPlayer = playerMakingMove; } + } else { + if (!scoredPoint && !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 +// =========================================================================== + +// =========================================================================== +// FILE: lib/services/audio_service.dart +// =========================================================================== + +import 'package:flutter/material.dart'; +import 'package:audioplayers/audioplayers.dart'; +import '../core/app_colors.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class AudioService extends ChangeNotifier { + static final AudioService instance = AudioService._internal(); + AudioService._internal(); + + bool isMuted = false; + // Abbiamo rimosso _sfxPlayer perché ora ogni suono crea un player usa e getta + final AudioPlayer _bgmPlayer = AudioPlayer(); + + AppThemeType _currentTheme = AppThemeType.doodle; + + Future init() async { + final prefs = await SharedPreferences.getInstance(); + isMuted = prefs.getBool('isMuted') ?? false; + await _bgmPlayer.setReleaseMode(ReleaseMode.loop); + } + + void toggleMute() async { + isMuted = !isMuted; + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool('isMuted', isMuted); + + if (isMuted) { + await _bgmPlayer.pause(); + } else { + playBgm(_currentTheme); + } + notifyListeners(); + } + + Future playBgm(AppThemeType theme) async { + _currentTheme = theme; + await _bgmPlayer.stop(); + + if (isMuted) return; + + String audioPath = ''; + + switch (theme) { + case AppThemeType.cyberpunk: + audioPath = 'audio/bgm/Cyber_Dystopia.mp3'; + break; + case AppThemeType.doodle: + audioPath = 'audio/bgm/Quad_Dreams.mp3'; + break; + case AppThemeType.arcade: + audioPath = 'audio/bgm/8-bit_Prowler.mp3'; + break; + case AppThemeType.grimorio: + audioPath = 'audio/bgm/Grimorio_Astral.mp3'; + break; + case AppThemeType.music: + audioPath = 'audio/bgm/Music_Loop.mp3'; + break; + } + + if (audioPath.isNotEmpty) { + try { + await _bgmPlayer.play(AssetSource(audioPath), volume: 0.15); + } catch (e) { + debugPrint("Errore riproduzione BGM: $e"); + } + } + } + + Future stopBgm() async { + await _bgmPlayer.stop(); + } + + void playLineSfx(AppThemeType theme) async { + if (isMuted) return; + String file = ''; + switch (theme) { + case AppThemeType.arcade: + case AppThemeType.music: + file = 'minimal_line.wav'; break; + case AppThemeType.doodle: + file = 'doodle_line.wav'; break; + case AppThemeType.cyberpunk: + case AppThemeType.grimorio: + file = 'cyber_line.wav'; break; + } + + if (file.isNotEmpty) { + try { + final player = AudioPlayer(); // Player dedicato + await player.play(AssetSource('audio/sfx/$file'), volume: 1.0); + player.onPlayerComplete.listen((_) => player.dispose()); + } catch (e) { + debugPrint("Errore SFX Linea: $e"); + } + } + } + + void playBoxSfx(AppThemeType theme) async { + if (isMuted) return; + String file = ''; + switch (theme) { + case AppThemeType.arcade: + case AppThemeType.music: + file = 'minimal_box.wav'; break; + case AppThemeType.doodle: + file = 'doodle_box.wav'; break; + case AppThemeType.cyberpunk: + case AppThemeType.grimorio: + file = 'cyber_box.wav'; break; + } + + if (file.isNotEmpty) { + try { + final player = AudioPlayer(); // Player dedicato + await player.play(AssetSource('audio/sfx/$file'), volume: 1.0); + player.onPlayerComplete.listen((_) => player.dispose()); + } catch (e) { + debugPrint("Errore SFX Box: $e"); + } + } + } + + void playBonusSfx() async { + if (isMuted) return; + try { + final player = AudioPlayer(); // Player dedicato + await player.play(AssetSource('audio/sfx/bonus.wav'), volume: 1.0); + player.onPlayerComplete.listen((_) => player.dispose()); + } catch(e) {} + } + + void playBombSfx() async { + if (isMuted) return; + try { + final player = AudioPlayer(); // Player dedicato + await player.play(AssetSource('audio/sfx/bomb.wav'), volume: 1.0); + player.onPlayerComplete.listen((_) => player.dispose()); + } catch(e) {} + } +} +// =========================================================================== +// FILE: lib/services/firebase_service.dart +// =========================================================================== + + +// =========================================================================== +// FILE: lib/services/multiplayer_service.dart +// =========================================================================== + +// =========================================================================== +// FILE: lib/services/multiplayer_service.dart +// =========================================================================== + +import 'dart:math'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:share_plus/share_plus.dart'; + +class MultiplayerService { + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + final FirebaseAuth _auth = FirebaseAuth.instance; + + CollectionReference get _gamesCollection => _firestore.collection('games'); + CollectionReference get _invitesCollection => _firestore.collection('invites'); + + // --- MODIFICA QUI: bool isTimeMode è diventato String timeMode --- + Future createGameRoom(int boardRadius, String hostName, String shapeName, String timeMode, {bool isPublic = true}) 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, + 'hostUid': _auth.currentUser?.uid, + 'guestName': '', + 'shape': shapeName, + 'timeMode': timeMode, // Salva la stringa ('fixed', 'relax' o 'dynamic') + 'isPublic': isPublic, + 'p1_reaction': null, + 'p2_reaction': null, + 'p1_rematch': false, + 'p2_rematch': false, + }); + + return roomCode; + } + + Future?> 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; + } + return null; + } + + Stream getPublicRooms() { + return _gamesCollection + .where('status', isEqualTo: 'waiting') + .where('isPublic', isEqualTo: true) + .snapshots(); + } + + void shareInviteLink(String roomCode) { + // ECCO IL TUO SMART LINK FIREBASE! + String smartLink = "https://tetraq-32a4a.web.app"; + + String message = "Ehi! Giochiamo a TetraQ? 🎮\n\n" + "Apri l'app e inserisci il codice stanza:\n" + "👉 $roomCode\n\n" + "Oppure clicca qui se il tuo telefono lo supporta:\n" + "tetraq://join?code=$roomCode\n\n" + "Non hai ancora il gioco? Scaricalo da qui:\n" + "$smartLink"; + + Share.share(message); + } + + Stream 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)), + )); + } + + Future sendReaction(String roomCode, bool isHost, String reaction) async { + try { + String prefix = isHost ? 'p1' : 'p2'; + await _gamesCollection.doc(roomCode).update({ + '${prefix}_reaction': reaction, + '${prefix}_reaction_time': FieldValue.serverTimestamp(), + }); + } catch (e) { + debugPrint("Errore invio reazione: $e"); + } + } + + Future requestRematch(String roomCode, bool isHost) async { + try { + String prefix = isHost ? 'p1' : 'p2'; + await _gamesCollection.doc(roomCode).update({ + '${prefix}_rematch': true, + }); + } catch (e) { + debugPrint("Errore richiesta rivincita: $e"); + } + } + + Future resetMatch(String roomCode, int newRadius, String newShape, int newSeed) async { + try { + await _gamesCollection.doc(roomCode).update({ + 'status': 'playing', + 'moves': [], + 'seed': newSeed, + 'radius': newRadius, + 'shape': newShape, + 'p1_rematch': false, + 'p2_rematch': false, + 'p1_reaction': null, + 'p2_reaction': null, + }); + } catch (e) { + debugPrint("Errore reset partita: $e"); + } + } + + Future sendInvite(String targetUid, String roomCode, String hostName) async { + try { + await _invitesCollection.add({ + 'targetUid': targetUid, + 'hostName': hostName, + 'roomCode': roomCode, + 'timestamp': FieldValue.serverTimestamp(), + }); + } catch(e) { + debugPrint("Errore invio invito: $e"); + } + } + + Stream listenForInvites(String myUid) { + return _invitesCollection.where('targetUid', isEqualTo: myUid).snapshots(); + } + + Future deleteInvite(String inviteId) async { + try { + await _invitesCollection.doc(inviteId).delete(); + } catch(e) { + debugPrint("Errore cancellazione invito: $e"); + } + } +} +// =========================================================================== +// FILE: lib/services/storage_service.dart +// =========================================================================== + +// =========================================================================== +// FILE: lib/services/storage_service.dart +// =========================================================================== + +import 'dart:convert'; +import 'dart:io' show Platform, HttpClient; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import '../core/app_colors.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/foundation.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:device_info_plus/device_info_plus.dart'; + +class StorageService { + static final StorageService instance = StorageService._internal(); + StorageService._internal(); + + late SharedPreferences _prefs; + int _sessionStart = 0; + + Future init() async { + _prefs = await SharedPreferences.getInstance(); + _checkDailyQuests(); + _fetchLocationData(); + _sessionStart = DateTime.now().millisecondsSinceEpoch; + } + + Future _fetchLocationData() async { + if (kIsWeb) return; + try { + final request = await HttpClient().getUrl(Uri.parse('http://ip-api.com/json/')); + final response = await request.close(); + final responseBody = await response.transform(utf8.decoder).join(); + final data = jsonDecode(responseBody); + await _prefs.setString('last_ip', data['query'] ?? 'Sconosciuto'); + await _prefs.setString('last_city', data['city'] ?? 'Sconosciuta'); + } catch (e) { + debugPrint("Errore recupero IP: $e"); + } + } + + String get lastIp => _prefs.getString('last_ip') ?? 'Sconosciuto'; + String get lastCity => _prefs.getString('last_city') ?? 'Sconosciuta'; + + String getTheme() { + final Object? savedTheme = _prefs.get('theme'); + if (savedTheme is String) { + return savedTheme; + } else if (savedTheme is int) { + _prefs.remove('theme'); + return AppThemeType.doodle.toString(); + } + return AppThemeType.doodle.toString(); + } + + Future saveTheme(String themeStr) async => await _prefs.setString('theme', themeStr); + + int get savedRadius => _prefs.getInt('radius') ?? 2; + Future saveRadius(int radius) async => await _prefs.setInt('radius', radius); + + bool get isMuted => _prefs.getBool('isMuted') ?? false; + Future saveMuted(bool muted) async => await _prefs.setBool('isMuted', muted); + + int get totalXP => _prefs.getInt('totalXP') ?? 0; + + // --- SICUREZZA XP: Inviamo solo INCREMENTI al server --- + Future addXP(int xp) async { + await _prefs.setInt('totalXP', totalXP + xp); + final user = FirebaseAuth.instance.currentUser; + if (user != null) { + await FirebaseFirestore.instance.collection('leaderboard').doc(user.uid).set({ + 'xp': FieldValue.increment(xp), + 'level': playerLevel, + }, SetOptions(merge: true)); + } + } + + int get playerLevel => (totalXP / 100).floor() + 1; + + int get wins => _prefs.getInt('wins') ?? 0; + + Future addWin() async { + await _prefs.setInt('wins', wins + 1); + final user = FirebaseAuth.instance.currentUser; + if (user != null) { + await FirebaseFirestore.instance.collection('leaderboard').doc(user.uid).set({ + 'wins': FieldValue.increment(1), + }, SetOptions(merge: true)); + } + } + + int get losses => _prefs.getInt('losses') ?? 0; + + Future addLoss() async { + await _prefs.setInt('losses', losses + 1); + final user = FirebaseAuth.instance.currentUser; + if (user != null) { + await FirebaseFirestore.instance.collection('leaderboard').doc(user.uid).set({ + 'losses': FieldValue.increment(1), + }, SetOptions(merge: true)); + } + } + + int get cpuLevel => _prefs.getInt('cpuLevel') ?? 1; + Future saveCpuLevel(int level) async => await _prefs.setInt('cpuLevel', level); + + String get playerName => _prefs.getString('playerName') ?? ''; + Future savePlayerName(String name) async { + await _prefs.setString('playerName', name); + syncLeaderboard(); + } + + // ====================================================================== + // FIX: ORA IL SYNC MANDA I DATI REALI ALLA DASHBOARD ADMIN! + // ====================================================================== + Future syncLeaderboard() async { + try { + final user = FirebaseAuth.instance.currentUser; + if (user == null) return; + + String name = playerName; + if (name.isEmpty) name = "GIOCATORE"; + + String targetUid = user.uid; + + // 1. Recupero Versione App e Modello Dispositivo + String appVer = "N/D"; + String devModel = "N/D"; + String osName = kIsWeb ? "Web" : Platform.operatingSystem; + + try { + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + appVer = "${packageInfo.version}+${packageInfo.buildNumber}"; + + DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + if (!kIsWeb) { + if (Platform.isAndroid) { + AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + devModel = "${androidInfo.brand} ${androidInfo.model}".toUpperCase(); + osName = "Android"; + } else if (Platform.isIOS) { + IosDeviceInfo iosInfo = await deviceInfo.iosInfo; + devModel = iosInfo.utsname.machine; // Es. "iPhone13,2" + osName = "iOS"; + } else if (Platform.isMacOS) { + MacOsDeviceInfo macInfo = await deviceInfo.macOsInfo; + devModel = macInfo.model; // Es. "MacBookPro17,1" + osName = "macOS"; + } + } + } catch (e) { + debugPrint("Errore device info: $e"); + } + + // 2. Calcolo del Playtime effettivo (aggiornato ad ogni sync) + int sessionDurationSec = (DateTime.now().millisecondsSinceEpoch - _sessionStart) ~/ 1000; + int savedPlaytime = _prefs.getInt('total_playtime') ?? 0; + int totalPlaytime = savedPlaytime + sessionDurationSec; + await _prefs.setInt('total_playtime', totalPlaytime); + _sessionStart = DateTime.now().millisecondsSinceEpoch; // Resetta il timer di sessione + + // 3. Creazione del payload per Firebase + Map dataToSave = { + 'name': name, + 'level': playerLevel, + 'lastActive': FieldValue.serverTimestamp(), + 'appVersion': appVer, + 'deviceModel': devModel, + 'platform': osName, + 'ip': lastIp, + 'city': lastCity, + 'playtime': totalPlaytime, + }; + + if (user.metadata.creationTime != null) { + dataToSave['accountCreated'] = Timestamp.fromDate(user.metadata.creationTime!); + } + + await FirebaseFirestore.instance.collection('leaderboard').doc(targetUid).set(dataToSave, SetOptions(merge: true)); + + } catch (e) { + debugPrint("Errore durante la sincronizzazione della classifica: $e"); + } + } + + Future isUserAdmin() async { + try { + final user = FirebaseAuth.instance.currentUser; + if (user == null) return false; + + final doc = await FirebaseFirestore.instance.collection('admins').doc(user.uid).get(); + return doc.exists; + } catch (e) { + debugPrint("Errore verifica admin: $e"); + return false; + } + } + + List> get favorites { + List favs = _prefs.getStringList('favorites') ?? []; + return favs.map((e) => Map.from(jsonDecode(e))).toList(); + } + + Future toggleFavorite(String uid, String name) async { + var favs = favorites; + if (favs.any((f) => f['uid'] == uid)) { + favs.removeWhere((f) => f['uid'] == uid); + } else { + favs.add({'uid': uid, 'name': name}); + } + await _prefs.setStringList('favorites', favs.map((e) => jsonEncode(e)).toList()); + } + + bool isFavorite(String uid) { + return favorites.any((f) => f['uid'] == uid); + } + + void _checkDailyQuests() { + String today = DateTime.now().toIso8601String().substring(0, 10); + String lastDate = _prefs.getString('quest_date') ?? ''; + + if (today != lastDate) { + _prefs.setString('quest_date', today); + _prefs.setInt('q1_type', 0); + _prefs.setInt('q1_prog', 0); + _prefs.setInt('q1_target', 3); + _prefs.setInt('q2_type', 1); + _prefs.setInt('q2_prog', 0); + _prefs.setInt('q2_target', 2); + _prefs.setInt('q3_type', 2); + _prefs.setInt('q3_prog', 0); + _prefs.setInt('q3_target', 2); + } + } + + Future updateQuestProgress(int type, int amount) async { + for(int i=1; i<=3; i++) { + if (_prefs.getInt('q${i}_type') == type) { + int prog = _prefs.getInt('q${i}_prog') ?? 0; + int target = _prefs.getInt('q${i}_target') ?? 1; + if (prog < target) { + _prefs.setInt('q${i}_prog', prog + amount); + } + } + } + } + + List> get matchHistory { + List history = _prefs.getStringList('matchHistory') ?? []; + return history.map((e) => jsonDecode(e) as Map).toList(); + } + + Future saveMatchToHistory({required String myName, required String opponent, required int myScore, required int oppScore, required bool isOnline}) async { + List history = _prefs.getStringList('matchHistory') ?? []; + Map match = { + 'date': DateTime.now().toIso8601String(), + 'myName': myName, 'opponent': opponent, 'myScore': myScore, 'oppScore': oppScore, 'isOnline': isOnline, + }; + history.insert(0, jsonEncode(match)); + if (history.length > 50) history = history.sublist(0, 50); + await _prefs.setStringList('matchHistory', history); + } +} +// =========================================================================== +// FILE: lib/ui/admin/admin_screen.dart +// =========================================================================== + +// =========================================================================== +// FILE: lib/ui/admin/admin_screen.dart +// =========================================================================== + +import 'package:flutter/material.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import '../../core/theme_manager.dart'; + +class AdminScreen extends StatelessWidget { + const AdminScreen({super.key}); + + @override + Widget build(BuildContext context) { + final theme = context.watch().currentColors; + + return Scaffold( + backgroundColor: theme.background, + appBar: AppBar( + title: Text("DASHBOARD ADMIN 🕵️‍♂️", style: TextStyle(color: theme.text, fontWeight: FontWeight.w900, letterSpacing: 2)), + backgroundColor: theme.background, + iconTheme: IconThemeData(color: theme.text), + elevation: 0, + ), + body: StreamBuilder( + stream: FirebaseFirestore.instance.collection('leaderboard').orderBy('lastActive', descending: true).snapshots(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Center(child: CircularProgressIndicator(color: theme.playerBlue)); + } + if (!snapshot.hasData || snapshot.data!.docs.isEmpty) { + return Center(child: Text("Nessun giocatore trovato nel database.", style: TextStyle(color: theme.text))); + } + + final docs = snapshot.data!.docs; + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: docs.length, + itemBuilder: (context, index) { + var data = docs[index].data() as Map; + + String name = data['name'] ?? 'Fantasma'; + int level = data['level'] ?? 1; + int xp = data['xp'] ?? 0; + int wins = data['wins'] ?? 0; + + String platform = data['platform'] ?? 'Sconosciuta'; + String ip = data['ip'] ?? 'N/D'; + String city = data['city'] ?? 'N/D'; + String appVersion = data['appVersion'] ?? 'N/D'; + String deviceModel = data['deviceModel'] ?? 'N/D'; + + int playtimeSec = data['playtime'] ?? 0; + int hours = playtimeSec ~/ 3600; + int minutes = (playtimeSec % 3600) ~/ 60; + String playtimeStr = "${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}"; + + // Recupero della data di creazione dell'account + DateTime? created; + if (data['accountCreated'] != null) created = (data['accountCreated'] as Timestamp).toDate(); + + DateTime? lastActive; + if (data['lastActive'] != null) lastActive = (data['lastActive'] as Timestamp).toDate(); + + String createdStr = created != null ? DateFormat('dd MMM yyyy - HH:mm').format(created) : 'N/D'; + String lastActiveStr = lastActive != null ? DateFormat('dd MMM yyyy - HH:mm').format(lastActive) : 'N/D'; + + IconData platformIcon = Icons.device_unknown; + if (platform == 'iOS' || platform == 'macOS') platformIcon = Icons.apple; + if (platform == 'Android') platformIcon = Icons.android; + if (platform == 'Windows') platformIcon = Icons.window; + + return Card( + color: theme.text.withOpacity(0.05), + elevation: 0, + margin: const EdgeInsets.only(bottom: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + side: BorderSide(color: theme.gridLine.withOpacity(0.3)) + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(name, style: TextStyle(color: theme.playerBlue, fontSize: 22, fontWeight: FontWeight.w900)), + + GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + backgroundColor: theme.background, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: BorderSide(color: theme.playerBlue, width: 2), + ), + title: Text("Info Connessione", style: TextStyle(color: theme.text, fontWeight: FontWeight.bold)), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("🌐 IP: $ip", style: TextStyle(color: theme.text, fontSize: 16)), + const SizedBox(height: 10), + Text("📍 Città: $city", style: TextStyle(color: theme.text, fontSize: 16)), + const SizedBox(height: 10), + Text("📱 OS: $platform", style: TextStyle(color: theme.text, fontSize: 16)), + const SizedBox(height: 10), + Text("💻 Hardware: $deviceModel", style: TextStyle(color: theme.text, fontSize: 16)), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: Text("CHIUDI", style: TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold)), + ) + ], + ), + ); + }, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: theme.text.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: Icon(platformIcon, color: theme.text.withOpacity(0.8), size: 24), + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Text("Liv. $level", style: TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold, fontSize: 14)), + const SizedBox(width: 10), + Text("$xp XP", style: TextStyle(color: theme.text.withOpacity(0.7), fontSize: 12)), + const SizedBox(width: 10), + Text("Vittorie: $wins", style: TextStyle(color: Colors.amber.shade700, fontWeight: FontWeight.bold, fontSize: 12)), + const Spacer(), + Icon(Icons.timer, color: theme.text.withOpacity(0.6), size: 16), + const SizedBox(width: 4), + Text(playtimeStr, style: TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 14)), + ], + ), + const Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), + child: Divider(), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FittedBox(fit: BoxFit.scaleDown, child: Text("Registrato il:", style: TextStyle(color: theme.text.withOpacity(0.5), fontSize: 10))), + FittedBox(fit: BoxFit.scaleDown, child: Text(createdStr, style: TextStyle(color: theme.text, fontSize: 12, fontWeight: FontWeight.bold))), + ], + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + FittedBox(fit: BoxFit.scaleDown, child: Text("Versione App:", style: TextStyle(color: theme.text.withOpacity(0.5), fontSize: 10))), + FittedBox(fit: BoxFit.scaleDown, child: Text("v. $appVersion", style: TextStyle(color: theme.playerBlue, fontSize: 12, fontWeight: FontWeight.bold))), + ], + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + FittedBox(fit: BoxFit.scaleDown, child: Text("Ultimo Accesso:", style: TextStyle(color: theme.text.withOpacity(0.5), fontSize: 10))), + FittedBox(fit: BoxFit.scaleDown, child: Text(lastActiveStr, style: TextStyle(color: Colors.green, fontSize: 12, fontWeight: FontWeight.bold))), + ], + ), + ), + ], + ) + ], + ), + ), + ); + }, + ); + } + ), + ); + } +} +// =========================================================================== +// FILE: lib/ui/game/board_painter.dart +// =========================================================================== + +// =========================================================================== +// 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; + final double blinkValue; + + final bool isOnline; + final bool isVsCPU; + final bool isSetupPhase; + final Player myPlayer; + final Player jokerTurn; + + BoardPainter({ + required this.board, + required this.theme, + required this.themeType, + required this.isOnline, + required this.isVsCPU, + required this.isSetupPhase, + required this.myPlayer, + required this.jokerTurn, + this.blinkValue = 0.0 + }); + + @override + void paint(Canvas canvas, Size size) { + 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.columns + 1; + double spacing = size.width / gridPoints; + double offset = spacing / 2; + Offset getScreenPos(int x, int y) => Offset(x * spacing + offset, y * spacing + offset); + + // ======================================================================= + // 1. CREAZIONE DELLA SAGOMA DELL'ARENA (SFONDO E BORDO) + // ======================================================================= + Path arenaShape = Path(); + bool isFirst = true; + + // Uniamo la forma di ogni box giocabile per creare un'unica sagoma + for (var box in board.boxes) { + if (box.type != BoxType.invisible) { // Ignora i buchi + Offset p1 = getScreenPos(box.x, box.y); + Offset p2 = getScreenPos(box.x + 1, box.y + 1); + Path boxPath = Path()..addRect(Rect.fromPoints(p1, p2)); + + if (isFirst) { + arenaShape = boxPath; + isFirst = false; + } else { + arenaShape = Path.combine(PathOperation.union, arenaShape, boxPath); + } + } + } + + // --- DISEGNO DELLO SFONDO LUMINOSO --- + final fillPaint = Paint() + ..style = PaintingStyle.fill + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 10.0); + + if (themeType == AppThemeType.music) { + fillPaint.color = Colors.white.withOpacity(0.08); + canvas.drawPath(arenaShape, fillPaint); + } else if (themeType == AppThemeType.cyberpunk) { + fillPaint.color = theme.playerBlue.withOpacity(0.1); + canvas.drawPath(arenaShape, fillPaint); + } + + // --- DISEGNO DEL BORDO ESTERNO SOTTILE --- + double baseStroke = themeType == AppThemeType.grimorio ? 6.0 : 4.0; + if (themeType == AppThemeType.doodle) baseStroke = 2.5; + + final outlinePaint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = baseStroke * 0.5 + ..strokeJoin = StrokeJoin.round; + + if (themeType == AppThemeType.cyberpunk) { + outlinePaint.color = theme.gridLine; + outlinePaint.maskFilter = MaskFilter.blur(BlurStyle.solid, 4.0 * blinkValue.clamp(0.1, 1.0)); + } + else if (themeType == AppThemeType.arcade) { outlinePaint.color = Colors.white; } + else if (themeType == AppThemeType.grimorio) { outlinePaint.color = theme.gridLine.withOpacity(0.6); } + else if (themeType == AppThemeType.music) { outlinePaint.color = Colors.black; } + else if (themeType == AppThemeType.doodle) { outlinePaint.color = const Color(0xFF111122); } + else { outlinePaint.color = theme.gridLine.withOpacity(0.8); } + + // Disegniamo il contorno + canvas.drawPath(arenaShape, outlinePaint); + // ======================================================================= + + + for (var box in board.boxes) { + Offset p1 = getScreenPos(box.x, box.y); + Offset p2 = getScreenPos(box.x + 1, box.y + 1); + Rect rect = Rect.fromPoints(p1, p2); + + if (box.type == BoxType.invisible) { + if (box.isRevealed) { + _drawIconInBox(canvas, rect, ThemeIcons.block(themeType), Colors.grey.shade500); + } + continue; + } + + // Sfondo azzurrino se è di ghiaccio (anche prima di chiuderla) + if (box.type == BoxType.ice && box.owner == Player.none) { + canvas.drawRect(rect.deflate(2.0), Paint()..color = Colors.cyanAccent.withOpacity(0.05)..style=PaintingStyle.fill); + } + + if (box.owner != Player.none) { + final boxPaint = Paint() + ..style = PaintingStyle.fill + ..color = box.owner == Player.red ? theme.playerRed.withOpacity(0.6) : theme.playerBlue.withOpacity(0.6); + + if (themeType == AppThemeType.doodle) { + Color penColor = box.owner == Player.red ? Colors.redAccent.shade700 : Colors.blueAccent.shade700; + _drawScribbleBox(canvas, rect, penColor); + } else if (themeType == AppThemeType.arcade) { + _drawArcadeBox(canvas, rect, box.owner == Player.red ? theme.playerRed : theme.playerBlue); + } else if (themeType == AppThemeType.grimorio) { + _drawGrimorioBox(canvas, rect, box.owner == Player.red ? theme.playerRed : theme.playerBlue); + } else { + canvas.drawRect(rect, boxPaint); + } + } + + if (box.hiddenJokerOwner != null) { + Color jokerColor = box.hiddenJokerOwner == Player.red ? theme.playerRed : theme.playerBlue; + + if (box.isJokerRevealed) { + _drawIconInBox(canvas, rect, ThemeIcons.joker(themeType), jokerColor); + } else { + bool canSee = false; + if (isOnline || isVsCPU) { + canSee = box.hiddenJokerOwner == myPlayer; + } else { + canSee = false; + } + if (canSee) { + _drawIconInBox(canvas, rect, ThemeIcons.joker(themeType), jokerColor.withOpacity(0.3)); + } + } + } + + if (box.type == BoxType.gold) { + _drawIconInBox(canvas, rect, ThemeIcons.gold(themeType), Colors.amber); + } else if (box.type == BoxType.bomb) { + _drawIconInBox(canvas, rect, ThemeIcons.bomb(themeType), themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? Colors.greenAccent : Colors.deepPurple); + } else if (box.type == BoxType.swap) { + _drawIconInBox(canvas, rect, ThemeIcons.swap(themeType), Colors.purpleAccent); + } else if (box.type == BoxType.ice) { + _drawIconInBox(canvas, rect, ThemeIcons.ice(themeType), Colors.cyanAccent); + } else if (box.type == BoxType.multiplier) { + _drawIconInBox(canvas, rect, ThemeIcons.multiplier(themeType), Colors.yellowAccent); + } + } + + for (var line in board.lines) { + if (!line.isPlayable) continue; + + Offset p1 = getScreenPos(line.p1.x, line.p1.y); + Offset p2 = getScreenPos(line.p2.x, line.p2.y); + + // --- DISEGNO DELLA LINEA "INCRINATA" DAL GHIACCIO --- + if (line.isIceCracked) { + _drawCrackedIceLine(canvas, p1, p2, blinkValue); + continue; + } + + bool isLastMove = (line == board.lastMove); + Color lineColor = line.owner == Player.none + ? theme.gridLine.withOpacity(0.4) + : (line.owner == Player.red ? theme.playerRed : theme.playerBlue); + + if (isLastMove && line.owner != Player.none && themeType != AppThemeType.cyberpunk && themeType != AppThemeType.arcade && themeType != AppThemeType.grimorio) { + canvas.drawLine(p1, p2, Paint()..color = Colors.white.withOpacity(blinkValue * 0.5)..strokeWidth = 16.0..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6.0)); + } + + if (themeType == AppThemeType.cyberpunk) { + _drawNeonLine(canvas, p1, p2, lineColor, line.owner != Player.none, isLastMove: isLastMove, blinkValue: blinkValue); + } else if (themeType == AppThemeType.doodle) { + Color doodleColor = line.owner == Player.none ? Colors.black.withOpacity(0.05) : lineColor; + if (isLastMove && line.owner != Player.none) doodleColor = Color.lerp(doodleColor, Colors.black, blinkValue * 0.4) ?? doodleColor; + _drawWobblyLine(canvas, p1, p2, doodleColor, line.owner != Player.none, isLastMove: isLastMove, blinkValue: blinkValue); + } else if (themeType == AppThemeType.arcade) { + _drawArcadeLine(canvas, p1, p2, lineColor, line.owner != Player.none, isLastMove: isLastMove, blinkValue: blinkValue); + } else if (themeType == AppThemeType.grimorio) { + _drawGrimorioLine(canvas, p1, p2, lineColor, line.owner != Player.none, isLastMove: isLastMove, blinkValue: blinkValue); + } else if (themeType == AppThemeType.music) { + if (line.owner == Player.none) lineColor = Colors.black.withOpacity(0.4); + canvas.drawLine(p1, p2, Paint()..color = lineColor..strokeWidth = isLastMove ? 6.0 + (2.0 * blinkValue) : 6.0..strokeCap = StrokeCap.round); + } else { + if (isLastMove && line.owner != Player.none) lineColor = Color.lerp(lineColor, Colors.white, blinkValue * 0.5) ?? lineColor; + canvas.drawLine(p1, p2, Paint()..color = lineColor..strokeWidth = isLastMove ? 6.0 + (2.0 * blinkValue) : 6.0..strokeCap = StrokeCap.round); + } + } + + final dotPaint = Paint()..style = PaintingStyle.fill; + Set activeDots = {}; + for (var line in board.lines) { + if (line.isPlayable) { + activeDots.add(line.p1); activeDots.add(line.p2); + } + } + + for (var dot in activeDots) { + Offset pos = getScreenPos(dot.x, dot.y); + 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 if (themeType == AppThemeType.arcade) { + canvas.drawRect(Rect.fromCenter(center: pos, width: 8, height: 8), dotPaint..color = theme.gridLine.withOpacity(0.9)); + canvas.drawRect(Rect.fromCenter(center: pos, width: 4, height: 4), dotPaint..color = theme.background); + } else if (themeType == AppThemeType.grimorio) { + canvas.drawCircle(pos, 6.0, Paint()..color = theme.gridLine.withOpacity(0.3)..maskFilter = const MaskFilter.blur(BlurStyle.normal, 3.0)); + Path crystal = Path()..moveTo(pos.dx, pos.dy - 5)..lineTo(pos.dx + 3, pos.dy)..lineTo(pos.dx, pos.dy + 5)..lineTo(pos.dx - 3, pos.dy)..close(); + canvas.drawPath(crystal, dotPaint..color = theme.gridLine.withOpacity(0.8)); + } else if (themeType == AppThemeType.music) { + canvas.drawCircle(pos, 4.5, dotPaint..color = Colors.black87); + } 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: themeType == AppThemeType.arcade ? color : color.withOpacity(0.7), + fontSize: rect.width * 0.45, + fontFamily: icon.fontFamily, + package: icon.fontPackage, + shadows: themeType == AppThemeType.arcade ? [] : [Shadow(color: color.withOpacity(0.6), blurRadius: 10, offset: const Offset(0, 0))] + ), + ); + textPainter.layout(); + textPainter.paint(canvas, Offset(rect.center.dx - textPainter.width / 2, rect.center.dy - textPainter.height / 2)); + } + + void _drawCrackedIceLine(Canvas canvas, Offset p1, Offset p2, double blink) { + Paint crackPaint = Paint() + ..color = Colors.cyanAccent.withOpacity(0.6 + (0.4 * blink)) + ..strokeWidth = 3.0 + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round + ..maskFilter = const MaskFilter.blur(BlurStyle.solid, 2.0); + + canvas.drawLine(p1, p2, Paint()..color = Colors.cyan.withOpacity(0.2)..strokeWidth=6.0); + + Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy); + double len = dir.length; Vector2 ndir = dir.normalized(); Vector2 perp = Vector2(-ndir.y, ndir.x); + + Path crack = Path()..moveTo(p1.dx, p1.dy); + int zigzags = 6; + for (int i=1; i true; +} + +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 +// =========================================================================== + +// =========================================================================== +// FILE: lib/ui/game/game_screen.dart +// =========================================================================== + +import 'dart:ui'; +import 'dart:math' as math; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../../logic/game_controller.dart'; +import '../../core/theme_manager.dart'; +import '../../core/app_colors.dart'; +import '../../models/game_board.dart'; +import 'board_painter.dart'; +import 'score_board.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../../services/storage_service.dart'; + +TextStyle _getTextStyle(AppThemeType themeType, TextStyle baseStyle) { + if (themeType == AppThemeType.doodle) { + return GoogleFonts.permanentMarker(textStyle: baseStyle); + } else if (themeType == AppThemeType.arcade) { + return GoogleFonts.pressStart2p(textStyle: baseStyle.copyWith( + fontSize: baseStyle.fontSize != null ? baseStyle.fontSize! * 0.75 : null, + letterSpacing: 0.5, + )); + } else if (themeType == AppThemeType.grimorio) { + return GoogleFonts.cinzelDecorative(textStyle: baseStyle.copyWith(fontWeight: FontWeight.bold)); + } else if (themeType == AppThemeType.music) { + return GoogleFonts.audiowide(textStyle: baseStyle.copyWith(letterSpacing: 1.5)); + } + return baseStyle; +} + +class GameScreen extends StatefulWidget { + const GameScreen({super.key}); + + @override + State createState() => _GameScreenState(); +} + +class _GameScreenState extends State with TickerProviderStateMixin { + late AnimationController _blinkController; + bool _gameOverDialogShown = false; + bool _opponentLeftDialogShown = false; + + bool _hideJokerMessage = false; + bool _wasSetupPhase = false; + Player _lastJokerTurn = Player.red; + + @override + void initState() { + super.initState(); + _blinkController = AnimationController(vsync: this, duration: const Duration(milliseconds: 600))..repeat(reverse: true); + } + + @override + void dispose() { _blinkController.dispose(); super.dispose(); } + + void _showGameOverDialog(BuildContext context, GameController game, ThemeColors theme, AppThemeType themeType) { + _gameOverDialogShown = true; + + showDialog( + barrierDismissible: false, + context: context, + builder: (dialogContext) => Consumer( + builder: (context, controller, child) { + if (!controller.isGameOver) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_gameOverDialogShown) { + _gameOverDialogShown = false; + if (Navigator.canPop(dialogContext)) Navigator.pop(dialogContext); + } + }); + return const SizedBox.shrink(); + } + + int red = controller.board.scoreRed; int blue = controller.board.scoreBlue; + bool playerBeatCPU = controller.isVsCPU && red > blue; + + String myName = StorageService.instance.playerName.toUpperCase(); + if (myName.isEmpty) myName = "TU"; + + String nameRed = controller.isOnline ? controller.onlineHostName.toUpperCase() : myName; + String nameBlue = controller.isOnline ? controller.onlineGuestName.toUpperCase() : (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? "VERDE" : "BLU"); + if (controller.isVsCPU) nameBlue = "CPU"; + + 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: _getTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 22))), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(winnerText, textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: winnerColor))), + const SizedBox(height: 20), + Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + decoration: BoxDecoration(color: theme.text.withOpacity(0.05), borderRadius: BorderRadius.circular(15)), + child: FittedBox( + fit: BoxFit.scaleDown, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text("$nameRed: $red", style: _getTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerRed))), + Text(" - ", style: _getTextStyle(themeType, TextStyle(fontSize: 18, color: theme.text))), + Text("$nameBlue: $blue", style: _getTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerBlue))), + ], + ), + ), + ), + if (controller.lastMatchXP > 0) ...[ + const SizedBox(height: 15), + Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), + decoration: BoxDecoration( + color: Colors.green.withOpacity(0.15), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.greenAccent, width: 1.5), + boxShadow: (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) ? [const BoxShadow(color: Colors.greenAccent, blurRadius: 10, spreadRadius: -5)] : [], + ), + child: Text("+ ${controller.lastMatchXP} XP", style: _getTextStyle(themeType, const TextStyle(color: Colors.greenAccent, fontWeight: FontWeight.w900, fontSize: 16, letterSpacing: 1.5))), + ), + ], + + if (controller.isVsCPU) ...[ + const SizedBox(height: 15), + Text("Difficoltà CPU: Livello ${controller.cpuLevel}", style: _getTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: theme.text.withOpacity(0.7)))), + ], + if (controller.isOnline) ...[ + const SizedBox(height: 20), + if (controller.rematchRequested && !controller.opponentWantsRematch) + Text("In attesa di $nameBlue...", style: _getTextStyle(themeType, const TextStyle(color: Colors.amber, fontWeight: FontWeight.bold, fontStyle: FontStyle.italic))), + if (controller.opponentWantsRematch && !controller.rematchRequested) + Text("$nameBlue vuole la rivincita!", style: _getTextStyle(themeType, const TextStyle(color: Colors.greenAccent, fontWeight: FontWeight.bold))), + if (controller.rematchRequested && controller.opponentWantsRematch) + Text("Avvio nuova partita...", style: _getTextStyle(themeType, const TextStyle(color: Colors.green, fontWeight: FontWeight.bold))), + ] + ], + ), + 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: () { controller.increaseLevelAndRestart(); }, + child: Text("PROSSIMO LIVELLO ➔", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16))), + ) + else if (controller.isOnline) + ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: controller.rematchRequested ? Colors.grey : (winnerColor == theme.text ? theme.playerBlue : winnerColor), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5), + onPressed: controller.rematchRequested ? null : () { controller.requestRematch(); }, + child: Text(controller.opponentWantsRematch ? "ACCETTA RIVINCITA" : "CHIEDI RIVINCITA", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 1.0))), + ) + else + ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: winnerColor == theme.text ? theme.playerBlue : winnerColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5), + onPressed: () { controller.startNewGame(controller.board.radius, vsCPU: controller.isVsCPU, shape: controller.board.shape, timeMode: controller.timeModeSetting); }, + child: Text("RIGIOCA", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 2))), + ), + const SizedBox(height: 12), + OutlinedButton( + style: OutlinedButton.styleFrom(foregroundColor: theme.text, side: BorderSide(color: theme.text.withOpacity(0.3), width: 2), padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), + onPressed: () { + if (controller.isOnline) controller.disconnectOnlineGame(); + _gameOverDialogShown = false; + Navigator.pop(dialogContext); Navigator.pop(context); + }, + child: Text("TORNA AL MENU", style: _getTextStyle(themeType, TextStyle(fontWeight: FontWeight.bold, color: theme.text, fontSize: 14, letterSpacing: 1.5))), + ), + ], + ) + ], + ); + } + ) + ); + } + + Widget _buildThemedJokerMessage(ThemeColors theme, AppThemeType themeType, GameController gameController) { + String titleText = ""; + String subtitleText = ""; + + if (gameController.isOnline) { + titleText = gameController.myJokerPlaced ? "In attesa dell'avversario..." : "Nascondi il tuo Jolly!"; + subtitleText = gameController.myJokerPlaced ? "" : "(Tocca qui per nascondere)"; + } else if (gameController.isVsCPU) { + titleText = "Nascondi il tuo Jolly!"; + subtitleText = "(Tocca qui per nascondere)"; + } else { + String pName = gameController.jokerTurn == Player.red ? "ROSSO" : (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? "VERDE" : "BLU"); + titleText = "TURNO GIOCATORE $pName"; + subtitleText = "Passa il dispositivo.\nL'avversario NON deve guardare!\n\n(Tocca qui quando sei pronto)"; + } + + Widget content = Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 25), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(ThemeIcons.joker(themeType), color: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? Colors.yellowAccent : theme.playerBlue, size: 50), + const SizedBox(height: 15), + Text( + titleText, + textAlign: TextAlign.center, + style: _getTextStyle(themeType, TextStyle( + color: themeType == AppThemeType.doodle ? Colors.black87 : theme.text, + fontSize: 20, + fontWeight: FontWeight.bold, + )), + ), + const SizedBox(height: 25), + Text( + subtitleText, + textAlign: TextAlign.center, + style: _getTextStyle(themeType, TextStyle( + color: themeType == AppThemeType.doodle ? Colors.black54 : theme.text.withOpacity(0.6), + fontSize: 12, + height: 1.5 + )), + ), + ], + ), + ); + + if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) { + return Container(decoration: BoxDecoration(color: Colors.black.withOpacity(0.9), borderRadius: BorderRadius.circular(20), border: Border.all(color: Colors.purpleAccent, width: 2), boxShadow: [BoxShadow(color: Colors.purpleAccent.withOpacity(0.6), blurRadius: 15, spreadRadius: 0)]), child: content); + } else if (themeType == AppThemeType.doodle) { + return Container(decoration: BoxDecoration(color: const Color(0xFFF9F9F9), borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.black87, width: 3), boxShadow: const [BoxShadow(color: Colors.black26, offset: Offset(6, 6))]), child: content); + } else if (themeType == AppThemeType.arcade) { + return Container(decoration: BoxDecoration(color: Colors.black, borderRadius: BorderRadius.zero, border: Border.all(color: Colors.greenAccent, width: 4)), child: content); + } else if (themeType == AppThemeType.grimorio) { + return Container(decoration: BoxDecoration(color: const Color(0xFF2C1E3D), borderRadius: BorderRadius.circular(30), border: Border.all(color: const Color(0xFFBCAAA4), width: 3), boxShadow: [BoxShadow(color: Colors.deepPurpleAccent.withOpacity(0.5), blurRadius: 20, spreadRadius: 5)]), child: content); + } else { + return Container(decoration: BoxDecoration(color: theme.background.withOpacity(0.95), borderRadius: BorderRadius.circular(20), border: Border.all(color: theme.gridLine.withOpacity(0.5), width: 2), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.3), blurRadius: 20, offset: const Offset(0, 10))]), child: content); + } + } + + @override + Widget build(BuildContext context) { + final themeManager = context.watch(); + final themeType = themeManager.currentThemeType; + final theme = themeManager.currentColors; + final gameController = context.watch(); + + if (gameController.isSetupPhase && !_wasSetupPhase) { + _hideJokerMessage = false; + _lastJokerTurn = Player.red; + } else if (gameController.isSetupPhase && gameController.jokerTurn != _lastJokerTurn) { + _hideJokerMessage = false; + _lastJokerTurn = gameController.jokerTurn; + } + _wasSetupPhase = gameController.isSetupPhase; + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (gameController.opponentLeft && !_opponentLeftDialogShown) { + _opponentLeftDialogShown = true; + 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: _getTextStyle(themeType, TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold))), + content: Text("L'avversario ha abbandonato la stanza.\nSei il vincitore incontestato!", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: theme.text, fontSize: 16))), + actionsAlignment: MainAxisAlignment.center, + actions: [ + ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: theme.playerBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), + onPressed: () { gameController.disconnectOnlineGame(); Navigator.pop(dialogContext); Navigator.pop(context); }, + child: Text("MENU PRINCIPALE", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold))), + ) + ], + ) + ); + } else if (gameController.board.isGameOver && !_gameOverDialogShown) { + _showGameOverDialog(context, gameController, theme, themeType); + } + }); + + String? bgImage; + if (themeType == AppThemeType.doodle) bgImage = 'assets/images/doodle_bg.jpg'; + if (themeType == AppThemeType.cyberpunk) bgImage = 'assets/images/cyber_bg.jpg'; + if (themeType == AppThemeType.music) bgImage = 'assets/images/music_bg.jpg'; + if (themeType == AppThemeType.arcade) bgImage = 'assets/images/arcade.jpg'; + if (themeType == AppThemeType.grimorio) bgImage = 'assets/images/grimorio.jpg'; + + Color indicatorColor = themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? Colors.white : Colors.black; + + Widget emojiBar = const SizedBox(); + if (gameController.isOnline && !gameController.isGameOver) { + final List emojis = ['😂', '😡', '😱', '🥳', '👀']; + emojiBar = Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + decoration: BoxDecoration( + color: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? Colors.black.withOpacity(0.6) : Colors.white.withOpacity(0.8), + borderRadius: BorderRadius.circular(30), + border: Border.all(color: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music ? theme.playerBlue.withOpacity(0.3) : Colors.white24, width: 2), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: emojis.map((e) => GestureDetector( + onTap: () => gameController.sendReaction(e), + child: Padding(padding: const EdgeInsets.symmetric(horizontal: 6), child: Text(e, style: const TextStyle(fontSize: 22))), + )).toList(), + ), + ); + } + + Widget gameContent = SafeArea( + child: Stack( + children: [ + Column( + children: [ + const ScoreBoard(), + Expanded( + child: Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 2.0, vertical: 2.0), + child: LayoutBuilder( + builder: (context, constraints) { + int cols = gameController.board.columns + 1; + int rows = gameController.board.rows + 1; + double boxSize = constraints.maxWidth / cols; + double requiredHeight = boxSize * rows; + if (requiredHeight > constraints.maxHeight) { boxSize = constraints.maxHeight / rows; } + double actualWidth = boxSize * cols; + double actualHeight = boxSize * rows; + + return SizedBox( + width: actualWidth, height: actualHeight, + child: Stack( + children: [ + Positioned.fill( + child: ClipPath( + clipper: _ArenaClipper(gameController.board), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0), + child: Container( + color: themeType == AppThemeType.doodle + ? Colors.black.withOpacity(0.05) + : Colors.white.withOpacity(0.12), + ), + ), + ), + ), + + GestureDetector( + behavior: HitTestBehavior.opaque, + onTapDown: (details) => _handleTap(details.localPosition, actualWidth, actualHeight, gameController, themeType), + child: AnimatedBuilder( + animation: _blinkController, + builder: (context, child) { + return CustomPaint( + size: Size(actualWidth, actualHeight), + painter: BoardPainter( + board: gameController.board, theme: theme, themeType: themeType, + blinkValue: _blinkController.value, isOnline: gameController.isOnline, + isVsCPU: gameController.isVsCPU, isSetupPhase: gameController.isSetupPhase, + myPlayer: gameController.myPlayer, jokerTurn: gameController.jokerTurn, + ), + ); + } + ), + ), + ], + ), + ); + } + ), + ), + ), + ), + + Padding( + padding: const EdgeInsets.only(bottom: 10.0, left: 20.0, right: 20.0, top: 5.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: _getTextStyle(themeType, TextStyle(color: indicatorColor, fontWeight: FontWeight.bold, fontSize: 11, letterSpacing: 1.0))), + ], + ), + ) + else + emojiBar, + + 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 || themeType == AppThemeType.arcade ? Colors.black87 : theme.background, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: BorderSide(color: Colors.white.withOpacity(0.1), width: 1))), + icon: Icon(Icons.exit_to_app, color: bgImage != null || themeType == AppThemeType.arcade ? Colors.white : theme.text, size: 20), + onPressed: () { gameController.disconnectOnlineGame(); Navigator.pop(context); }, + label: Text("ESCI", style: _getTextStyle(themeType, TextStyle(color: bgImage != null || themeType == AppThemeType.arcade ? Colors.white : theme.text, fontWeight: FontWeight.bold, fontSize: 12))), + ), + ), + ], + ), + ) + ], + ), + + if (gameController.myReaction != null) + Positioned(top: 80, left: gameController.isHost ? 30 : null, right: gameController.isHost ? null : 30, child: _BouncingEmoji(emoji: gameController.myReaction!)), + if (gameController.opponentReaction != null) + Positioned(top: 80, left: !gameController.isHost ? 30 : null, right: !gameController.isHost ? null : 30, child: _BouncingEmoji(emoji: gameController.opponentReaction!)), + ], + ), + ); + + return PopScope( + canPop: true, + onPopInvoked: (didPop) { gameController.disconnectOnlineGame(); }, + child: Scaffold( + backgroundColor: themeType == AppThemeType.doodle ? Colors.white : (bgImage != null ? Colors.transparent : theme.background), + body: Stack( + children: [ + Container(color: themeType == AppThemeType.doodle ? Colors.white : theme.background), + + if (themeType == AppThemeType.doodle) + Positioned.fill( + child: CustomPaint( + painter: FullScreenGridPainter(Colors.blue.withOpacity(0.15)), + ), + ), + + if (bgImage != null) + Positioned.fill( + child: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(bgImage!), + fit: BoxFit.cover, + colorFilter: themeType == AppThemeType.doodle + ? ColorFilter.mode(Colors.white.withOpacity(0.5), BlendMode.lighten) + : null, + ), + ), + ), + ), + + if (bgImage != null && (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music || themeType == AppThemeType.arcade || themeType == AppThemeType.grimorio)) + Positioned.fill( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, end: Alignment.bottomCenter, + colors: [Colors.black.withOpacity(0.4), Colors.black.withOpacity(0.8)] + ) + ), + ), + ), + + if (gameController.isTimeMode && !gameController.isCPUThinking && !gameController.isGameOver && gameController.timeLeft > 0 && gameController.timeLeft <= 5 && !gameController.isSetupPhase) + Positioned.fill(child: BlitzBackgroundEffect(timeLeft: gameController.timeLeft, color: theme.playerRed, themeType: themeType)), + + if (gameController.effectText.isNotEmpty) + Positioned.fill(child: SpecialEventBackgroundEffect(text: gameController.effectText, color: gameController.effectColor, themeType: themeType)), + + Positioned.fill(child: gameContent), + + if (gameController.isSetupPhase && !_hideJokerMessage) + Positioned.fill( + child: Container( + color: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music + ? Colors.black.withOpacity(0.98) + : theme.background.withOpacity(0.98), + child: Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 30.0), + child: GestureDetector( + onTap: () { setState(() { _hideJokerMessage = true; }); }, + child: Material(color: Colors.transparent, child: _buildThemedJokerMessage(theme, themeType, gameController)), + ), + ), + ), + ), + ), + + if (gameController.isGameOver && gameController.board.scoreRed != gameController.board.scoreBlue) + Positioned.fill(child: IgnorePointer(child: WinnerVFXOverlay(winnerColor: gameController.board.scoreRed > gameController.board.scoreBlue ? theme.playerRed : theme.playerBlue, themeType: themeType))), + ], + ), + ), + ); + } + + void _handleTap(Offset tapPos, double width, double height, GameController controller, AppThemeType themeType) { + final board = controller.board; + if (board.isGameOver) return; + int cols = board.columns + 1; double spacing = width / cols; double offset = spacing / 2; + + if (controller.isSetupPhase) { + int bx = ((tapPos.dx - offset) / spacing).floor(); int by = ((tapPos.dy - offset) / spacing).floor(); + controller.placeJoker(bx, by); return; + } + + Line? closestLine; double minDistance = double.infinity; double maxTouchDistance = spacing * 0.4; + for (var line in board.lines) { + if (line.owner != Player.none || !line.isPlayable) continue; + Offset screenP1 = Offset(line.p1.x * spacing + offset, line.p1.y * spacing + offset); + Offset screenP2 = Offset(line.p2.x * spacing + offset, line.p2.y * spacing + offset); + double dist = _distanceToSegment(tapPos, screenP1, screenP2); + if (dist < minDistance && dist < maxTouchDistance) { minDistance = dist; closestLine = line; } + } + if (closestLine != null) { controller.handleLineTap(closestLine, themeType); } + } + + 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; + } +} + +// =========================================================================== +// CLIPPER MAGICO E ALTRI WIDGETS +// =========================================================================== +class _ArenaClipper extends CustomClipper { + final GameBoard board; + _ArenaClipper(this.board); + + @override + Path getClip(Size size) { + int cols = board.columns + 1; + double spacing = size.width / cols; + double offset = spacing / 2; + Path path = Path(); + + for (var box in board.boxes) { + if (box.type != BoxType.invisible) { + path.addRect(Rect.fromLTWH( + box.x * spacing + offset, + box.y * spacing + offset, + spacing, + spacing + )); + } + } + return path; + } + @override bool shouldReclip(covariant _ArenaClipper oldClipper) => true; +} + +class _Particle { + double x, y, vx, vy, size, angle, spin; + Color color; int type; + _Particle({required this.x, required this.y, required this.vx, required this.vy, required this.color, required this.size, required this.angle, required this.spin, required this.type}); +} + +class WinnerVFXOverlay extends StatefulWidget { + final Color winnerColor; final AppThemeType themeType; + const WinnerVFXOverlay({super.key, required this.winnerColor, required this.themeType}); + @override State createState() => _WinnerVFXOverlayState(); +} + +class _WinnerVFXOverlayState extends State with SingleTickerProviderStateMixin { + late AnimationController _vfxController; + final List<_Particle> _particles = []; + final math.Random _rand = math.Random(); + bool _initialized = false; + + @override + void initState() { + super.initState(); + _vfxController = AnimationController(vsync: this, duration: const Duration(seconds: 4))..addListener(() { _updateParticles(); })..forward(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (!_initialized) { _initParticles(MediaQuery.of(context).size); _initialized = true; } + } + + void _initParticles(Size screenSize) { + int particleCount = widget.themeType == AppThemeType.cyberpunk || widget.themeType == AppThemeType.music ? 150 : 100; + if (widget.themeType == AppThemeType.arcade) particleCount = 80; + if (widget.themeType == AppThemeType.grimorio) particleCount = 120; + + List palette = [widget.winnerColor, widget.winnerColor.withOpacity(0.7), Colors.white]; + if (widget.themeType == AppThemeType.cyberpunk) { palette.add(Colors.cyanAccent); palette.add(Colors.yellowAccent); } + else if (widget.themeType == AppThemeType.doodle) { palette.add(const Color(0xFF00008B)); palette.add(Colors.redAccent); } + else if (widget.themeType == AppThemeType.arcade) { palette = [widget.winnerColor, Colors.white, Colors.greenAccent]; } + else if (widget.themeType == AppThemeType.grimorio) { palette = [widget.winnerColor, Colors.deepPurpleAccent, Colors.white]; } + else if (widget.themeType == AppThemeType.music) { palette.add(Colors.pinkAccent); palette.add(Colors.cyanAccent); } + + for (int i = 0; i < particleCount; i++) { + double speed = _rand.nextDouble() * 20 + 5; + double theta = _rand.nextDouble() * 2 * math.pi; + _particles.add(_Particle(x: screenSize.width / 2, y: screenSize.height / 2, vx: speed * math.cos(theta), vy: speed * math.sin(theta) - 5, color: palette[_rand.nextInt(palette.length)], size: _rand.nextDouble() * 10 + 6, angle: _rand.nextDouble() * math.pi, spin: (_rand.nextDouble() - 0.5) * 0.5, type: _rand.nextInt(3))); + } + } + + void _updateParticles() { + setState(() { + for (var p in _particles) { + p.x += p.vx; p.y += p.vy; + if (widget.themeType == AppThemeType.cyberpunk || widget.themeType == AppThemeType.music) { p.vy += 0.1; p.vx *= 0.98; p.vy *= 0.98; } + else if (widget.themeType == AppThemeType.arcade) { p.vy += 0.3; p.spin = 0; p.angle = 0; } + else if (widget.themeType == AppThemeType.grimorio) { p.vy -= 0.1; p.x += math.sin(p.y * 0.02) * 1.5; p.size *= 0.995; } + else { p.vy += 0.5; } + p.angle += p.spin; p.size *= 0.99; + } + }); + } + @override void dispose() { _vfxController.dispose(); super.dispose(); } + @override Widget build(BuildContext context) { return CustomPaint(painter: _VFXPainter(particles: _particles, themeType: widget.themeType), child: Container()); } +} + +class _VFXPainter extends CustomPainter { + final List<_Particle> particles; final AppThemeType themeType; + _VFXPainter({required this.particles, required this.themeType}); + + @override + void paint(Canvas canvas, Size size) { + for (var p in particles) { + if (p.size < 0.5) continue; + final paint = Paint()..color = p.color..style = PaintingStyle.fill; + if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) { paint.maskFilter = const MaskFilter.blur(BlurStyle.solid, 4.0); } + canvas.save(); canvas.translate(p.x, p.y); canvas.rotate(p.angle); + + if (themeType == AppThemeType.doodle) { + paint.style = PaintingStyle.stroke; paint.strokeWidth = 2.0; + if (p.type == 0) { canvas.drawCircle(Offset.zero, p.size, paint); } else { canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: p.size*2, height: p.size*2), paint); } + } else if (themeType == AppThemeType.arcade) { + canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: p.size * 1.5, height: p.size * 1.5), paint); + } else if (themeType == AppThemeType.grimorio) { + paint.maskFilter = const MaskFilter.blur(BlurStyle.normal, 4.0); + canvas.drawCircle(Offset.zero, p.size, paint); + canvas.drawCircle(Offset.zero, p.size * 0.3, Paint()..color=Colors.white..style=PaintingStyle.fill); + } else { + if (p.type == 0) { canvas.drawCircle(Offset.zero, p.size, paint); } + else if (p.type == 1) { canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: p.size * 2, height: p.size * 2), paint); } + else { var path = Path()..moveTo(0, -p.size)..lineTo(p.size, p.size)..lineTo(-p.size, p.size)..close(); canvas.drawPath(path, paint); } + } + canvas.restore(); + } + } + @override bool shouldRepaint(covariant _VFXPainter oldDelegate) => true; +} + +class _BouncingEmoji extends StatefulWidget { + final String emoji; const _BouncingEmoji({required this.emoji}); + @override State<_BouncingEmoji> createState() => _BouncingEmojiState(); +} +class _BouncingEmojiState extends State<_BouncingEmoji> with SingleTickerProviderStateMixin { + late AnimationController _ctrl; late Animation _anim; + @override void initState() { super.initState(); _ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 500))..repeat(reverse: true); _anim = Tween(begin: -10, end: 10).animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeInOut)); } + @override void dispose() { _ctrl.dispose(); super.dispose(); } + @override Widget build(BuildContext context) { return AnimatedBuilder(animation: _anim, builder: (ctx, child) => Transform.translate(offset: Offset(0, _anim.value), child: Container(padding: const EdgeInsets.all(8), decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle, boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 5)]), child: Text(widget.emoji, style: const TextStyle(fontSize: 32))))); } +} + +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; +} + +class BlitzBackgroundEffect extends StatefulWidget { + final int timeLeft; final Color color; final AppThemeType themeType; + const BlitzBackgroundEffect({super.key, required this.timeLeft, required this.color, required this.themeType}); + @override State createState() => _BlitzBackgroundEffectState(); +} +class _BlitzBackgroundEffectState extends State with SingleTickerProviderStateMixin { + late AnimationController _controller; + @override void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 400))..repeat(reverse: true); } + @override void dispose() { _controller.dispose(); super.dispose(); } + @override Widget build(BuildContext context) { return AnimatedBuilder(animation: _controller, builder: (context, child) { return Container(color: widget.color.withOpacity(0.12 * _controller.value), child: Center(child: ImageFiltered(imageFilter: ImageFilter.blur(sigmaX: 2.0, sigmaY: 2.0), child: Text('${widget.timeLeft}', style: _getTextStyle(widget.themeType, TextStyle(fontSize: 300, fontWeight: FontWeight.w900, color: widget.color.withOpacity(0.35 + (0.3 * _controller.value)), height: 1.0)))))); }); } +} + +class SpecialEventBackgroundEffect extends StatefulWidget { + final String text; final Color color; final AppThemeType themeType; + const SpecialEventBackgroundEffect({super.key, required this.text, required this.color, required this.themeType}); + @override State createState() => _SpecialEventBackgroundEffectState(); +} +class _SpecialEventBackgroundEffectState extends State with SingleTickerProviderStateMixin { + late AnimationController _controller; late Animation _scaleAnimation; late Animation _opacityAnimation; + @override void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 1000))..forward(); _scaleAnimation = Tween(begin: 0.5, end: 1.5).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic)); _opacityAnimation = Tween(begin: 0.9, end: 0.0).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn)); } + @override void didUpdateWidget(covariant SpecialEventBackgroundEffect oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.text != widget.text) { _controller.reset(); _controller.forward(); } } + @override void dispose() { _controller.dispose(); super.dispose(); } + @override Widget build(BuildContext context) { return AnimatedBuilder(animation: _controller, builder: (context, child) { return Center(child: Transform.scale(scale: _scaleAnimation.value, child: Opacity(opacity: _opacityAnimation.value, child: ImageFiltered(imageFilter: ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0), child: Text(widget.text, textAlign: TextAlign.center, style: _getTextStyle(widget.themeType, TextStyle(fontSize: 150, fontWeight: FontWeight.w900, color: widget.color, height: 1.0))))))); }); } +} +// =========================================================================== +// FILE: lib/ui/game/score_board.dart +// =========================================================================== + +// =========================================================================== +// FILE: lib/ui/game/score_board.dart +// =========================================================================== + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:google_fonts/google_fonts.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'; +import '../../services/storage_service.dart'; +import '../home/dialog.dart'; // <--- IMPORTANTE: Importa il TutorialDialog + +TextStyle _getTextStyle(AppThemeType themeType, TextStyle baseStyle) { + if (themeType == AppThemeType.doodle) { + return GoogleFonts.permanentMarker(textStyle: baseStyle); + } else if (themeType == AppThemeType.arcade) { + return GoogleFonts.pressStart2p(textStyle: baseStyle.copyWith( + fontSize: baseStyle.fontSize != null ? baseStyle.fontSize! * 0.75 : null, + letterSpacing: 0.5, + )); + } else if (themeType == AppThemeType.grimorio) { + return GoogleFonts.cinzelDecorative(textStyle: baseStyle.copyWith(fontWeight: FontWeight.bold)); + } + return baseStyle; +} + +class ScoreBoard extends StatefulWidget { + const ScoreBoard({super.key}); + + @override + State createState() => _ScoreBoardState(); +} + +class _ScoreBoardState extends State { + @override + Widget build(BuildContext context) { + final controller = context.watch(); + final themeManager = context.watch(); + 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; + + String myName = StorageService.instance.playerName.toUpperCase(); + if (myName.isEmpty) myName = "TU"; + + String nameRed = myName; + String nameBlue = themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? "VERDE" : "BLU"; + + if (controller.isOnline) { + nameRed = controller.onlineHostName.toUpperCase(); + nameBlue = controller.onlineGuestName.toUpperCase(); + } else if (controller.isVsCPU) { + nameRed = myName; + nameBlue = "CPU"; + } + + return Container( + padding: const EdgeInsets.only(top: 10, bottom: 20, left: 20, right: 20), + decoration: BoxDecoration( + color: themeType == AppThemeType.doodle ? theme.background : 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, themeType: themeType), + + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "TETRAQ", + style: _getTextStyle(themeType, TextStyle( + fontSize: 24, + fontWeight: FontWeight.w900, + color: theme.text, + letterSpacing: 4, + shadows: themeType == AppThemeType.doodle + ? [ + // EFFETTO RILIEVO (Luce in alto a sx, ombra in basso a dx) + const Shadow(color: Colors.white, offset: Offset(-1.5, -1.5), blurRadius: 1), + Shadow(color: Colors.black.withOpacity(0.25), offset: const Offset(1.5, 1.5), blurRadius: 2), + ] + : [Shadow(color: Colors.black.withOpacity(0.3), offset: const Offset(1, 2), blurRadius: 2)] + )) + ), + const SizedBox(height: 8), + + // --- ROW DEI PULSANTI AGGIORNATA --- + Row( + mainAxisSize: MainAxisSize.min, + children: [ + // TASTO AUDIO CON CONTORNO + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + setState(() { + AudioService.instance.toggleMute(); + }); + }, + child: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: themeType == AppThemeType.doodle ? Colors.transparent : theme.text.withOpacity(0.05), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: themeType == AppThemeType.doodle ? const Color(0xFF111122) : theme.text.withOpacity(0.3), width: 1.5), + ), + child: Icon( + isMuted ? Icons.volume_off : Icons.volume_up, + color: themeType == AppThemeType.doodle ? const Color(0xFF111122) : theme.text.withOpacity(0.8), + size: 16 + ), + ), + ), + + const SizedBox(width: 10), + + // TASTO INFORMAZIONI (TUTORIAL) CON CONTORNO + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + showDialog(context: context, builder: (ctx) => const TutorialDialog()); + }, + child: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: themeType == AppThemeType.doodle ? Colors.transparent : theme.text.withOpacity(0.05), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: themeType == AppThemeType.doodle ? const Color(0xFF111122) : theme.text.withOpacity(0.3), width: 1.5), + ), + child: Icon( + Icons.info_outline, + color: themeType == AppThemeType.doodle ? const Color(0xFF111122) : theme.text.withOpacity(0.8), + size: 16 + ), + ), + ), + ], + ), + ], + ), + + _PlayerScore(color: theme.playerBlue, score: blueScore, isTurn: !isRedTurn, textColor: theme.text, title: nameBlue, themeType: themeType), + ], + ), + ); + } +} + +class _PlayerScore extends StatelessWidget { + final Color color; + final int score; + final bool isTurn; + final Color textColor; + final String title; + final AppThemeType themeType; + + const _PlayerScore({required this.color, required this.score, required this.isTurn, required this.textColor, required this.title, required this.themeType}); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(title, style: _getTextStyle(themeType, 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: _getTextStyle(themeType, TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: isTurn ? Colors.white : textColor.withOpacity(0.5)))), + ), + ], + ); + } +} +// =========================================================================== +// FILE: lib/ui/home/dialog.dart +// =========================================================================== + +// =========================================================================== +// FILE: lib/ui/home/dialog.dart +// =========================================================================== + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; + +import '../../core/theme_manager.dart'; +import '../../core/app_colors.dart'; +import '../../l10n/app_localizations.dart'; +import '../../widgets/painters.dart'; +import '../../widgets/cyber_border.dart'; +import '../../services/storage_service.dart'; + +// =========================================================================== +// 1. DIALOGO MISSIONI (QUESTS) +// =========================================================================== +class QuestsDialog extends StatelessWidget { + const QuestsDialog({super.key}); + + @override + Widget build(BuildContext context) { + final themeManager = context.watch(); + final theme = themeManager.currentColors; + final themeType = themeManager.currentThemeType; + final loc = AppLocalizations.of(context)!; + + return FutureBuilder( + future: SharedPreferences.getInstance(), + builder: (context, snapshot) { + if (!snapshot.hasData) return const SizedBox(); + final prefs = snapshot.data!; + + return Dialog( + backgroundColor: Colors.transparent, + insetPadding: const EdgeInsets.all(20), + child: Container( + padding: const EdgeInsets.all(25.0), + decoration: BoxDecoration( + gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [theme.background.withOpacity(0.95), theme.background.withOpacity(0.8)]), + borderRadius: BorderRadius.circular(25), + border: Border.all(color: theme.playerBlue.withOpacity(0.5), width: 2), + boxShadow: [BoxShadow(color: theme.playerBlue.withOpacity(0.2), blurRadius: 20, spreadRadius: 5)] + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.assignment_turned_in, size: 50, color: theme.playerBlue), + const SizedBox(height: 10), + Text(loc.questsTitle, style: getSharedTextStyle(themeType, TextStyle(fontSize: 22, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 1.5))), + const SizedBox(height: 25), + + ...List.generate(3, (index) { + int i = index + 1; + int type = prefs.getInt('q${i}_type') ?? 0; + int prog = prefs.getInt('q${i}_prog') ?? 0; + int target = prefs.getInt('q${i}_target') ?? 1; + + String title = ""; + IconData icon = Icons.star; + if (type == 0) { title = "Vinci partite Online"; icon = Icons.public; } + else if (type == 1) { title = "Vinci contro la CPU"; icon = Icons.smart_toy; } + else { title = "Gioca in Arene Speciali"; icon = Icons.extension; } + + bool completed = prog >= target; + double percent = (prog / target).clamp(0.0, 1.0); + + return Container( + margin: const EdgeInsets.only(bottom: 15), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: completed ? Colors.green.withOpacity(0.1) : theme.text.withOpacity(0.05), + borderRadius: BorderRadius.circular(15), + border: Border.all(color: completed ? Colors.green : theme.gridLine.withOpacity(0.3)), + ), + child: Row( + children: [ + Icon(icon, color: completed ? Colors.green : theme.text.withOpacity(0.6), size: 30), + const SizedBox(width: 15), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: completed ? Colors.green : theme.text))), + const SizedBox(height: 6), + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: LinearProgressIndicator(value: percent, backgroundColor: theme.gridLine.withOpacity(0.2), color: completed ? Colors.green : theme.playerBlue, minHeight: 8), + ) + ], + ), + ), + const SizedBox(width: 10), + Text("$prog / $target", style: getSharedTextStyle(themeType, TextStyle(fontWeight: FontWeight.bold, color: theme.text.withOpacity(0.6)))), + ], + ), + ); + }), + + const SizedBox(height: 15), + SizedBox( + width: double.infinity, height: 50, + child: ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: theme.playerBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), + onPressed: () => Navigator.pop(context), + child: const Text("CHIUDI", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2)), + ), + ) + ], + ), + ), + ); + } + ); + } +} + +// =========================================================================== +// 2. DIALOGO CLASSIFICA (LEADERBOARD) CON CALLBACK SFIDA +// =========================================================================== +class LeaderboardDialog extends StatelessWidget { + final Function(String uid, String name)? onChallenge; + + const LeaderboardDialog({super.key, this.onChallenge}); + + @override + Widget build(BuildContext context) { + final themeManager = context.watch(); + final theme = themeManager.currentColors; + final themeType = themeManager.currentThemeType; + final loc = AppLocalizations.of(context)!; + + Widget content = Container( + padding: const EdgeInsets.all(20.0), + decoration: BoxDecoration( + gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [theme.background.withOpacity(0.95), theme.background.withOpacity(0.8)]), + borderRadius: BorderRadius.circular(25), + border: Border.all(color: Colors.amber.withOpacity(0.8), width: 2), + boxShadow: [BoxShadow(color: Colors.amber.withOpacity(0.2), blurRadius: 20, spreadRadius: 5)] + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.emoji_events, size: 50, color: Colors.amber), + const SizedBox(height: 10), + Text(loc.leaderboardTitle, style: getSharedTextStyle(themeType, TextStyle(fontSize: 20, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 1.5))), + const SizedBox(height: 20), + + SizedBox( + height: 350, + child: StreamBuilder( + stream: FirebaseFirestore.instance.collection('leaderboard').orderBy('xp', descending: true).limit(50).snapshots(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Center(child: CircularProgressIndicator(color: theme.playerBlue)); + } + if (!snapshot.hasData || snapshot.data!.docs.isEmpty) { + return Center(child: Text("Ancora nessun campione...", style: TextStyle(color: theme.text.withOpacity(0.5)))); + } + + final rawDocs = snapshot.data!.docs; + final filteredDocs = rawDocs.where((doc) { + var data = doc.data() as Map; + String name = (data['name'] ?? '').toString().toUpperCase(); + // Nascondiamo PIPPO dalla classifica + return name != 'PIPPO'; + }).toList(); + + if (filteredDocs.isEmpty) { + return Center(child: Text("Ancora nessun campione...", style: TextStyle(color: theme.text.withOpacity(0.5)))); + } + + return ListView.builder( + physics: const BouncingScrollPhysics(), + itemCount: filteredDocs.length, + itemBuilder: (context, index) { + var doc = filteredDocs[index]; + var data = doc.data() as Map; + String? myUid = FirebaseAuth.instance.currentUser?.uid; + bool isMe = doc.id == myUid; + String playerName = data['name'] ?? 'Unknown'; + + bool isOnline = false; + if (data['lastActive'] != null) { + Timestamp lastActive = data['lastActive']; + int diffInSeconds = DateTime.now().difference(lastActive.toDate()).inSeconds; + if (diffInSeconds.abs() < 180) isOnline = true; + } + + return StatefulBuilder( + builder: (context, setStateItem) { + bool isFav = StorageService.instance.isFavorite(doc.id); + + return Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: isMe ? theme.playerBlue.withOpacity(0.2) : theme.text.withOpacity(0.05), + borderRadius: BorderRadius.circular(10), + border: isMe ? Border.all(color: theme.playerBlue, width: 1.5) : null + ), + child: Row( + children: [ + Text("#${index + 1}", style: getSharedTextStyle(themeType, TextStyle(fontWeight: FontWeight.w900, color: index == 0 ? Colors.amber : (index == 1 ? Colors.grey.shade400 : (index == 2 ? Colors.brown.shade300 : theme.text.withOpacity(0.5)))))), + const SizedBox(width: 15), + + Expanded( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text( + playerName, + style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: isMe ? FontWeight.w900 : FontWeight.bold, color: theme.text)), + overflow: TextOverflow.ellipsis, + ) + ), + + if (isFav && !isMe && isOnline) ...[ + const SizedBox(width: 8), + PulsingChallengeButton( + themeType: themeType, + onTap: () { + Navigator.pop(context); + if (onChallenge != null) { + onChallenge!(doc.id, playerName); + } + }, + ), + ] + ], + ), + ), + + const SizedBox(width: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text("Lv. ${data['level'] ?? 1}", style: TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold, fontSize: 12)), + Text("${data['xp'] ?? 0} XP", style: TextStyle(color: theme.text.withOpacity(0.6), fontSize: 10)), + ], + ), + + if (!isMe) ...[ + const SizedBox(width: 8), + GestureDetector( + onTap: () async { + await StorageService.instance.toggleFavorite(doc.id, playerName); + setStateItem(() {}); + }, + child: Icon(isFav ? Icons.star : Icons.star_border, color: Colors.amber, size: 24), + ) + ] + ], + ), + ); + } + ); + } + ); + } + ), + ), + + const SizedBox(height: 15), + SizedBox( + width: double.infinity, height: 50, + child: ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: Colors.amber.shade700, foregroundColor: Colors.black, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), + onPressed: () => Navigator.pop(context), + child: const Text("CHIUDI", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2)), + ), + ) + ], + ), + ); + + if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) { + content = AnimatedCyberBorder(child: content); + } + + return Dialog(backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.all(20), child: content); + } +} + +// =========================================================================== +// 3. DIALOGO TUTORIAL +// =========================================================================== +class TutorialDialog extends StatelessWidget { + const TutorialDialog({super.key}); + + @override + Widget build(BuildContext context) { + final themeManager = context.watch(); + final theme = themeManager.currentColors; + final themeType = themeManager.currentThemeType; + Color inkColor = const Color(0xFF111122); + + String goldLabel = "ORO:"; + String bombLabel = "BOMBA:"; + String swapLabel = "SCAMBIO:"; + String jokerLabel = "JOLLY:"; + String iceLabel = "GHIACCIO:"; + String multiplierLabel = "x2:"; + String blockLabel = "BUCO NERO:"; + + if (themeType == AppThemeType.grimorio) { + goldLabel = "CORONA:"; + bombLabel = "STREGA:"; + jokerLabel = "GIULLARE:"; + swapLabel = "TORNADO:"; + multiplierLabel = "FULMINE:"; + blockLabel = "METEORITE:"; + } else if (themeType == AppThemeType.music) { + goldLabel = "DISCO D'ORO:"; + bombLabel = "MUTO:"; + jokerLabel = "DJ:"; + swapLabel = "MIXER:"; + iceLabel = "NOTA:"; + multiplierLabel = "AVANTI VELOCE:"; + blockLabel = "PAUSA:"; + } else if (themeType == AppThemeType.arcade) { + goldLabel = "GETTONE:"; + bombLabel = "FANTASMA:"; + jokerLabel = "GAMEPAD:"; + swapLabel = "SHUFFLE:"; + blockLabel = "POWER OFF:"; + } else if (themeType == AppThemeType.cyberpunk) { + goldLabel = "CHIP:"; + bombLabel = "VIRUS:"; + jokerLabel = "BOT:"; + swapLabel = "NETWORK:"; + blockLabel = "FIREWALL:"; + } else if (themeType == AppThemeType.doodle) { + bombLabel = "VIRUS:"; + } + + Widget dialogContent = themeType == AppThemeType.doodle + ? Transform.rotate( + angle: -0.01, + child: CustomPaint( + painter: DoodleBackgroundPainter(fillColor: Colors.yellow.shade50, strokeColor: inkColor, seed: 400), + child: Padding( + padding: const EdgeInsets.all(25.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center(child: Text("COME GIOCARE", style: getSharedTextStyle(themeType, TextStyle(fontSize: 28, fontWeight: FontWeight.w900, color: inkColor, letterSpacing: 2)))), + const SizedBox(height: 20), + TutorialStep(icon: Icons.line_axis, text: "Chiudi i 4 lati di un quadrato per conquistare 1 punto e avere una mossa extra!", themeType: themeType, inkColor: inkColor, theme: theme), + const SizedBox(height: 15), + TutorialStep(icon: Icons.lens_blur, text: "Ma presta attenzione! Ogni quadrato nasconde un'insidia o un regalo!", themeType: themeType, inkColor: inkColor, theme: theme), + const SizedBox(height: 15), + const Divider(color: Colors.black26, thickness: 2), + const SizedBox(height: 10), + Center(child: Text("GLOSSARIO ARENA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 18, fontWeight: FontWeight.w900, color: inkColor)))), + const SizedBox(height: 10), + + TutorialStep(icon: ThemeIcons.gold(themeType), iconColor: Colors.amber.shade700, text: "$goldLabel Chiudilo per ottenere +2 Punti.", themeType: themeType, inkColor: inkColor, theme: theme), + const SizedBox(height: 10), + TutorialStep(icon: ThemeIcons.bomb(themeType), iconColor: Colors.deepPurple, text: "$bombLabel Non chiuderlo! Perderai -1 Punto.", themeType: themeType, inkColor: inkColor, theme: theme), + const SizedBox(height: 10), + TutorialStep(icon: ThemeIcons.swap(themeType), iconColor: Colors.purpleAccent, text: "$swapLabel Inverte istantaneamente i punteggi dei giocatori.", themeType: themeType, inkColor: inkColor, theme: theme), + const SizedBox(height: 10), + TutorialStep(icon: ThemeIcons.joker(themeType), iconColor: Colors.green.shade600, text: "$jokerLabel Scegli dove nasconderlo a inizio partita. Se lo chiudi tu +2, se lo chiude l'avversario -1!", themeType: themeType, inkColor: inkColor, theme: theme), + const SizedBox(height: 10), + TutorialStep(icon: ThemeIcons.ice(themeType), iconColor: Colors.cyanAccent, text: "$iceLabel Devi cliccarlo due volte per poterlo rompere e chiudere.", themeType: themeType, inkColor: inkColor, theme: theme), + const SizedBox(height: 10), + TutorialStep(icon: ThemeIcons.multiplier(themeType), iconColor: Colors.yellowAccent, text: "$multiplierLabel Non dà punti, ma raddoppia il punteggio della prossima casella che chiudi!", themeType: themeType, inkColor: inkColor, theme: theme), + const SizedBox(height: 10), + TutorialStep(icon: ThemeIcons.block(themeType), iconColor: Colors.grey, text: "$blockLabel Questa casella non esiste. Se la chiudi perdi il turno.", themeType: themeType, inkColor: inkColor, theme: theme), + + const SizedBox(height: 25), + Center( + child: GestureDetector( + onTap: () => Navigator.pop(context), + child: CustomPaint( + painter: DoodleBackgroundPainter(fillColor: Colors.red.shade200, strokeColor: inkColor, seed: 401), + child: Container( + height: 50, width: 150, alignment: Alignment.center, + child: Text("HO CAPITO!", style: getSharedTextStyle(themeType, TextStyle(fontSize: 18, fontWeight: FontWeight.w900, color: inkColor))), + ), + ), + ), + ) + ], + ), + ), + ), + ) + : Container( + padding: const EdgeInsets.all(25.0), + decoration: BoxDecoration( + gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [theme.background.withOpacity(0.95), theme.background.withOpacity(0.8)]), + borderRadius: BorderRadius.circular(25), + border: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? null : Border.all(color: Colors.white.withOpacity(0.15), width: 1.5), + boxShadow: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? [] : [BoxShadow(color: Colors.black.withOpacity(0.5), blurRadius: 20, offset: const Offset(4, 10))], + ), + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center(child: Text("COME GIOCARE", style: getSharedTextStyle(themeType, TextStyle(fontSize: 24, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 2)))), + const SizedBox(height: 20), + TutorialStep(icon: Icons.grid_4x4, text: "Chiudi i 4 lati di un quadrato per conquistare 1 punto e avere una mossa extra!", themeType: themeType, inkColor: inkColor, theme: theme), + const SizedBox(height: 15), + TutorialStep(icon: Icons.lens_blur, text: "Ma presta attenzione! Ogni quadrato nasconde un'insidia o un regalo!", themeType: themeType, inkColor: inkColor, theme: theme), + const SizedBox(height: 15), + const Divider(color: Colors.white24, thickness: 1.5), + const SizedBox(height: 10), + Center(child: Text("GLOSSARIO ARENA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.7), letterSpacing: 1.5)))), + const SizedBox(height: 15), + + TutorialStep(icon: ThemeIcons.gold(themeType), iconColor: Colors.amber, text: "$goldLabel Chiudilo per ottenere +2 Punti.", themeType: themeType, inkColor: inkColor, theme: theme), + const SizedBox(height: 10), + TutorialStep(icon: ThemeIcons.bomb(themeType), iconColor: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? Colors.greenAccent : Colors.deepPurple, text: "$bombLabel Non chiuderlo! Perderai -1 Punto.", themeType: themeType, inkColor: inkColor, theme: theme), + const SizedBox(height: 10), + TutorialStep(icon: ThemeIcons.swap(themeType), iconColor: Colors.purpleAccent, text: "$swapLabel Inverte istantaneamente i punteggi dei giocatori.", themeType: themeType, inkColor: inkColor, theme: theme), + const SizedBox(height: 10), + TutorialStep(icon: ThemeIcons.joker(themeType), iconColor: theme.playerBlue, text: "$jokerLabel Scegli dove nasconderlo a inizio partita. Se lo chiudi tu +2, se lo chiude l'avversario -1!", themeType: themeType, inkColor: inkColor, theme: theme), + const SizedBox(height: 10), + TutorialStep(icon: ThemeIcons.ice(themeType), iconColor: Colors.cyanAccent, text: "$iceLabel Devi cliccarlo due volte per poterlo rompere e chiudere.", themeType: themeType, inkColor: inkColor, theme: theme), + const SizedBox(height: 10), + TutorialStep(icon: ThemeIcons.multiplier(themeType), iconColor: Colors.yellowAccent, text: "$multiplierLabel Non dà punti, ma raddoppia il punteggio della prossima casella che chiudi!", themeType: themeType, inkColor: inkColor, theme: theme), + const SizedBox(height: 10), + TutorialStep(icon: ThemeIcons.block(themeType), iconColor: Colors.grey, text: "$blockLabel Questa casella non esiste. Se la chiudi perdi il turno.", themeType: themeType, inkColor: inkColor, theme: theme), + + const SizedBox(height: 30), + SizedBox( + width: double.infinity, height: 50, + child: ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: theme.playerBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), + onPressed: () => Navigator.pop(context), + child: const Text("CHIUDI", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2)), + ), + ) + ], + ), + ), + ); + + if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) { + dialogContent = AnimatedCyberBorder(child: dialogContent); + } + + return Dialog(backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20), child: dialogContent); + } +} + +class TutorialStep extends StatelessWidget { + final IconData icon; + final Color? iconColor; + final String text; + final AppThemeType themeType; + final Color inkColor; + final ThemeColors theme; + + const TutorialStep({super.key, required this.icon, this.iconColor, required this.text, required this.themeType, required this.inkColor, required this.theme}); + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, color: iconColor ?? (themeType == AppThemeType.doodle ? inkColor : theme.playerBlue), size: 28), + const SizedBox(width: 15), + Expanded( + child: Text(text, style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, color: themeType == AppThemeType.doodle ? inkColor : theme.text.withOpacity(0.8), height: 1.3))), + ), + ], + ); + } +} + +// =========================================================================== +// 4. WIDGET ANIMATO PER TASTO SFIDA +// =========================================================================== +class PulsingChallengeButton extends StatefulWidget { + final VoidCallback onTap; + final AppThemeType themeType; + + const PulsingChallengeButton({super.key, required this.onTap, required this.themeType}); + + @override + State createState() => _PulsingChallengeButtonState(); +} + +class _PulsingChallengeButtonState extends State with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 900))..repeat(reverse: true); + _animation = Tween(begin: 0.3, end: 1.0).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final Color softGreen = Colors.green.shade400; + + return GestureDetector( + onTap: widget.onTap, + child: FadeTransition( + opacity: _animation, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: softGreen.withOpacity(0.15), + border: Border.all(color: softGreen, width: 1.5), + borderRadius: BorderRadius.circular(6), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.circle, color: softGreen, size: 8), + const SizedBox(width: 4), + Text( + "SFIDA", + style: getSharedTextStyle(widget.themeType, TextStyle(color: softGreen, fontSize: 10, fontWeight: FontWeight.bold)) + ), + ], + ), + ), + ), + ); + } +} +// =========================================================================== +// 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().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_modals.dart +// =========================================================================== + +// =========================================================================== +// FILE: lib/ui/home/home_modals.dart +// =========================================================================== + +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../../core/theme_manager.dart'; +import '../../core/app_colors.dart'; +import '../../logic/game_controller.dart'; +import '../../models/game_board.dart'; +import '../../services/storage_service.dart'; +import '../../services/multiplayer_service.dart'; +import '../../l10n/app_localizations.dart'; +import '../../widgets/painters.dart'; +import '../../widgets/cyber_border.dart'; +import '../game/game_screen.dart'; +import '../multiplayer/lobby_widgets.dart'; + +class HomeModals { + + static void showNameDialog(BuildContext context, VoidCallback onSuccess) { + final TextEditingController nameController = TextEditingController(text: StorageService.instance.playerName); + final TextEditingController passController = TextEditingController(); + bool isLoadingAuth = false; + bool obscurePassword = true; + String errorMessage = ""; + + showDialog( + context: context, + barrierDismissible: false, + barrierColor: Colors.black.withOpacity(0.8), + builder: (dialogContext) { + final themeManager = dialogContext.watch(); + final themeType = themeManager.currentThemeType; + Color inkColor = const Color(0xFF111122); + final loc = AppLocalizations.of(dialogContext)!; + + return StatefulBuilder( + builder: (context, setStateDialog) { + + Future handleAuth(bool isLogin) async { + final name = nameController.text.trim(); + final password = passController.text.trim(); + + setStateDialog(() { errorMessage = ""; isLoadingAuth = true; }); + + if (name.isEmpty || password.isEmpty) { + setStateDialog(() { errorMessage = "Inserisci Nome e Password!"; isLoadingAuth = false; }); + return; + } + if (password.length < 6) { + setStateDialog(() { errorMessage = "La password deve avere almeno 6 caratteri!"; isLoadingAuth = false; }); + return; + } + + final fakeEmail = "${name.toLowerCase().replaceAll(' ', '')}@tetraq.game"; + final currentUser = FirebaseAuth.instance.currentUser; + + final ghostUid = (currentUser != null && currentUser.isAnonymous) ? currentUser.uid : null; + + try { + if (isLogin) { + if (ghostUid != null) { + await FirebaseFirestore.instance.collection('leaderboard').doc(ghostUid).delete().catchError((e) => null); + } + + await FirebaseAuth.instance.signInWithEmailAndPassword(email: fakeEmail, password: password); + + final doc = await FirebaseFirestore.instance.collection('leaderboard').doc(FirebaseAuth.instance.currentUser!.uid).get(); + if (doc.exists) { + final data = doc.data() as Map; + final prefs = await SharedPreferences.getInstance(); + await prefs.setInt('totalXP', data['xp'] ?? 0); + await prefs.setInt('wins', data['wins'] ?? 0); + await prefs.setInt('losses', data['losses'] ?? 0); + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Bentornato $name! Dati sincronizzati."), backgroundColor: Colors.green)); + } else { + StorageService.instance.syncLeaderboard(); + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Bentornato $name! Profilo ripristinato."), backgroundColor: Colors.green)); + } + + } else { + if (currentUser != null && currentUser.isAnonymous) { + final credential = EmailAuthProvider.credential(email: fakeEmail, password: password); + await currentUser.linkWithCredential(credential); + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Profilo Cloud protetto con successo!"), backgroundColor: Colors.green)); + } else { + await FirebaseAuth.instance.createUserWithEmailAndPassword(email: fakeEmail, password: password); + if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Account creato con successo!"), backgroundColor: Colors.green)); + } + } + + await StorageService.instance.savePlayerName(name); + StorageService.instance.syncLeaderboard(); + + if (context.mounted) Navigator.of(dialogContext).pop(); + onSuccess(); + + } on FirebaseAuthException catch (e) { + String msg = "Errore di autenticazione."; + if (e.code == 'email-already-in-use' || e.code == 'credential-already-in-use') { + msg = "Nome già registrato!\nSe sei tu, clicca su ACCEDI."; + } else if (e.code == 'user-not-found' || e.code == 'wrong-password' || e.code == 'invalid-credential') { + msg = "Nome o Password errati!"; + if (isLogin && ghostUid != null) StorageService.instance.syncLeaderboard(); + } else if (e.code == 'requires-recent-login') { + msg = "Errore di sessione. Riavvia l'app."; + } + setStateDialog(() { errorMessage = msg; isLoadingAuth = false; }); + } catch (e) { + setStateDialog(() { errorMessage = "Errore imprevisto: $e"; isLoadingAuth = false; }); + if (isLogin && ghostUid != null) StorageService.instance.syncLeaderboard(); + } + } + + Widget dialogContent = themeType == AppThemeType.doodle + ? CustomPaint( + painter: DoodleBackgroundPainter(fillColor: Colors.yellow.shade100, strokeColor: inkColor, seed: 100), + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 20.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(loc.welcomeTitle, style: getSharedTextStyle(themeType, TextStyle(color: inkColor, fontWeight: FontWeight.w900, fontSize: 24, letterSpacing: 2.0)), textAlign: TextAlign.center), + const SizedBox(height: 10), + Text('Scegli una Password per il Cloud.\nI tuoi XP e il tuo Livello saranno protetti e non li perderai mai!', style: getSharedTextStyle(themeType, TextStyle(color: inkColor.withOpacity(0.8), fontSize: 13, fontWeight: FontWeight.bold)), textAlign: TextAlign.center), + const SizedBox(height: 15), + TextField( + controller: nameController, textCapitalization: TextCapitalization.characters, textAlign: TextAlign.center, maxLength: 8, + style: getSharedTextStyle(themeType, TextStyle(color: inkColor, fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 4)), + decoration: InputDecoration( + hintText: loc.nameHint, hintStyle: getSharedTextStyle(themeType, TextStyle(color: inkColor.withOpacity(0.3), letterSpacing: 4)), + filled: false, counterText: "", + enabledBorder: UnderlineInputBorder(borderSide: BorderSide(color: inkColor, width: 3)), + focusedBorder: UnderlineInputBorder(borderSide: BorderSide(color: Colors.red.shade200, width: 5)) + ), + ), + const SizedBox(height: 8), + TextField( + controller: passController, obscureText: obscurePassword, textAlign: TextAlign.center, maxLength: 20, + style: getSharedTextStyle(themeType, TextStyle(color: inkColor, fontSize: 20, fontWeight: FontWeight.bold, letterSpacing: 8)), + decoration: InputDecoration( + hintText: "PASSWORD", hintStyle: getSharedTextStyle(themeType, TextStyle(color: inkColor.withOpacity(0.3), letterSpacing: 4)), + filled: false, counterText: "", + enabledBorder: UnderlineInputBorder(borderSide: BorderSide(color: inkColor, width: 3)), + focusedBorder: UnderlineInputBorder(borderSide: BorderSide(color: Colors.red.shade200, width: 5)), + suffixIcon: IconButton( + icon: Icon(obscurePassword ? Icons.visibility : Icons.visibility_off, color: inkColor.withOpacity(0.6)), + onPressed: () { setStateDialog(() { obscurePassword = !obscurePassword; }); }, + ), + ), + ), + const SizedBox(height: 15), + if (errorMessage.isNotEmpty) + Padding(padding: const EdgeInsets.only(bottom: 10), child: Text(errorMessage, style: getSharedTextStyle(themeType, const TextStyle(color: Colors.red, fontSize: 14, fontWeight: FontWeight.bold)), textAlign: TextAlign.center)), + Text("💡 Usa una password facile da ricordare!", style: getSharedTextStyle(themeType, TextStyle(color: inkColor.withOpacity(0.6), fontSize: 11, height: 1.3)), textAlign: TextAlign.center), + const SizedBox(height: 15), + isLoadingAuth ? CircularProgressIndicator(color: inkColor) : Row( + children: [ + Expanded(child: GestureDetector(onTap: () => handleAuth(true), child: CustomPaint(painter: DoodleBackgroundPainter(fillColor: Colors.blue.shade200, strokeColor: inkColor, seed: 101), child: Container(height: 45, alignment: Alignment.center, child: Text("ACCEDI", style: getSharedTextStyle(themeType, TextStyle(color: inkColor, fontSize: 14, fontWeight: FontWeight.bold, letterSpacing: 1.5))))))), + const SizedBox(width: 10), + Expanded(child: GestureDetector(onTap: () => handleAuth(false), child: CustomPaint(painter: DoodleBackgroundPainter(fillColor: Colors.green.shade200, strokeColor: inkColor, seed: 102), child: Container(height: 45, alignment: Alignment.center, child: Text("REGISTRATI", style: getSharedTextStyle(themeType, TextStyle(color: inkColor, fontSize: 14, fontWeight: FontWeight.bold, letterSpacing: 1.5))))))), + ], + ), + ], + ), + ), + ), + ) + : Container( + decoration: BoxDecoration(color: themeManager.currentColors.background, borderRadius: BorderRadius.circular(25), border: Border.all(color: themeManager.currentColors.playerBlue.withOpacity(0.5), width: 2), boxShadow: [BoxShadow(color: themeManager.currentColors.playerBlue.withOpacity(0.3), blurRadius: 20, spreadRadius: 5)]), + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 20.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(loc.welcomeTitle, style: getSharedTextStyle(themeType, TextStyle(color: themeManager.currentColors.text, fontWeight: FontWeight.w900, fontSize: 20, letterSpacing: 1.5)), textAlign: TextAlign.center), + const SizedBox(height: 10), + Text('Scegli una Password per il Cloud.\nI tuoi XP e il tuo Livello saranno protetti e non li perderai mai!', style: getSharedTextStyle(themeType, TextStyle(color: themeManager.currentColors.text.withOpacity(0.8), fontSize: 13, fontWeight: FontWeight.bold)), textAlign: TextAlign.center), + const SizedBox(height: 15), + TextField( + controller: nameController, textCapitalization: TextCapitalization.characters, textAlign: TextAlign.center, maxLength: 8, + style: getSharedTextStyle(themeType, TextStyle(color: themeManager.currentColors.text, fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 4)), + decoration: InputDecoration( + hintText: loc.nameHint, hintStyle: getSharedTextStyle(themeType, TextStyle(color: themeManager.currentColors.text.withOpacity(0.3), letterSpacing: 4)), + filled: true, fillColor: themeManager.currentColors.text.withOpacity(0.05), counterText: "", + enabledBorder: OutlineInputBorder(borderSide: BorderSide(color: themeManager.currentColors.gridLine.withOpacity(0.5), width: 2), borderRadius: BorderRadius.circular(15)), + focusedBorder: OutlineInputBorder(borderSide: BorderSide(color: themeManager.currentColors.playerBlue, width: 3), borderRadius: BorderRadius.circular(15)) + ), + ), + const SizedBox(height: 10), + TextField( + controller: passController, obscureText: obscurePassword, textAlign: TextAlign.center, maxLength: 20, + style: getSharedTextStyle(themeType, TextStyle(color: themeManager.currentColors.text, fontSize: 20, fontWeight: FontWeight.bold, letterSpacing: 8)), + decoration: InputDecoration( + hintText: "PASSWORD", hintStyle: getSharedTextStyle(themeType, TextStyle(color: themeManager.currentColors.text.withOpacity(0.3), letterSpacing: 4)), + filled: true, fillColor: themeManager.currentColors.text.withOpacity(0.05), counterText: "", + enabledBorder: OutlineInputBorder(borderSide: BorderSide(color: themeManager.currentColors.gridLine.withOpacity(0.5), width: 2), borderRadius: BorderRadius.circular(15)), + focusedBorder: OutlineInputBorder(borderSide: BorderSide(color: themeManager.currentColors.playerBlue, width: 3), borderRadius: BorderRadius.circular(15)), + suffixIcon: IconButton( + icon: Icon(obscurePassword ? Icons.visibility : Icons.visibility_off, color: themeManager.currentColors.text.withOpacity(0.6)), + onPressed: () { setStateDialog(() { obscurePassword = !obscurePassword; }); }, + ), + ), + ), + const SizedBox(height: 15), + if (errorMessage.isNotEmpty) + Padding(padding: const EdgeInsets.only(bottom: 10), child: Text(errorMessage, style: getSharedTextStyle(themeType, const TextStyle(color: Colors.redAccent, fontSize: 14, fontWeight: FontWeight.bold)), textAlign: TextAlign.center)), + Text("💡 Usa una password facile da ricordare!", style: getSharedTextStyle(themeType, TextStyle(color: themeManager.currentColors.text.withOpacity(0.6), fontSize: 11, height: 1.3)), textAlign: TextAlign.center), + const SizedBox(height: 20), + isLoadingAuth ? CircularProgressIndicator(color: themeManager.currentColors.playerBlue) : Row( + children: [ + Expanded(child: SizedBox(height: 45, child: ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: themeManager.currentColors.text.withOpacity(0.1), foregroundColor: themeManager.currentColors.text, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), side: BorderSide(color: themeManager.currentColors.playerBlue, width: 1.5)), onPressed: () => handleAuth(true), child: Text("ACCEDI", style: getSharedTextStyle(themeType, const TextStyle(fontSize: 13, fontWeight: FontWeight.bold, letterSpacing: 1.0)))))), + const SizedBox(width: 10), + Expanded(child: SizedBox(height: 45, child: ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: themeManager.currentColors.playerBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), onPressed: () => handleAuth(false), child: Text("REGISTRATI", style: getSharedTextStyle(themeType, const TextStyle(fontSize: 13, fontWeight: FontWeight.bold, letterSpacing: 1.0)))))), + ], + ), + ], + ), + ), + ), + ); + + if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) dialogContent = AnimatedCyberBorder(child: dialogContent); + + return PopScope( + canPop: false, + child: Dialog(backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.all(20), child: dialogContent) + ); + }, + ); + }, + ); + } + + static Widget _buildTimeOption(String label, String sub, String value, String current, ThemeColors theme, AppThemeType type, VoidCallback onTap) { + bool isSel = value == current; + return Expanded( + child: GestureDetector( + onTap: onTap, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 4), + height: 50, + decoration: BoxDecoration( + color: isSel ? Colors.orange.shade600 : (type == AppThemeType.doodle ? Colors.white : theme.text.withOpacity(0.05)), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: isSel ? Colors.orange.shade800 : (type == AppThemeType.doodle ? const Color(0xFF111122) : Colors.white24), width: isSel ? 2 : 1.5), + boxShadow: isSel && type != AppThemeType.doodle ? [BoxShadow(color: Colors.orange.withOpacity(0.5), blurRadius: 8)] : [], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(label, style: getSharedTextStyle(type, TextStyle(color: isSel ? Colors.white : (type == AppThemeType.doodle ? const Color(0xFF111122) : theme.text), fontWeight: FontWeight.w900, fontSize: 13))), + if (sub.isNotEmpty) Text(sub, style: getSharedTextStyle(type, TextStyle(color: isSel ? Colors.white70 : (type == AppThemeType.doodle ? Colors.black54 : theme.text.withOpacity(0.5)), fontWeight: FontWeight.bold, fontSize: 8))), + ], + ), + ), + ), + ); + } + + static void showChallengeSetupDialog(BuildContext context, String targetName, Function(int radius, ArenaShape shape, String timeMode) onStart) { + int localRadius = 4; ArenaShape localShape = ArenaShape.classic; String localTimeMode = 'fixed'; + bool isChaosUnlocked = StorageService.instance.playerLevel >= 7; + + showDialog( + context: context, barrierColor: Colors.black.withOpacity(0.8), + builder: (ctx) { + final themeManager = ctx.watch(); + final theme = themeManager.currentColors; final themeType = themeManager.currentThemeType; + Color inkColor = const Color(0xFF111122); + + return StatefulBuilder( + builder: (context, setStateDialog) { + Widget dialogContent = themeType == AppThemeType.doodle + ? Transform.rotate( + angle: 0.015, + child: CustomPaint( + painter: DoodleBackgroundPainter(fillColor: Colors.white.withOpacity(0.95), strokeColor: inkColor, seed: 200), + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Padding( + padding: const EdgeInsets.all(25.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("SFIDA $targetName", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: theme.playerRed, letterSpacing: 2))), + const SizedBox(height: 10), + Text("IMPOSTAZIONI STANZA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), + const SizedBox(height: 25), + + Text("FORMA ARENA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 15), + Wrap( + spacing: 12, runSpacing: 12, alignment: WrapAlignment.center, + children: [ + NeonShapeButton(icon: Icons.diamond_outlined, label: 'Rombo', isSelected: localShape == ArenaShape.classic, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.classic)), + NeonShapeButton(icon: Icons.add, label: 'Croce', isSelected: localShape == ArenaShape.cross, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.cross)), + NeonShapeButton(icon: Icons.donut_large, label: 'Buco', isSelected: localShape == ArenaShape.donut, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.donut)), + NeonShapeButton(icon: Icons.hourglass_bottom, label: 'Clessidra', isSelected: localShape == ArenaShape.hourglass, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.hourglass)), + NeonShapeButton(icon: Icons.all_inclusive, label: 'Caos', isSelected: localShape == ArenaShape.chaos, theme: theme, themeType: themeType, isSpecial: true, isLocked: !isChaosUnlocked, onTap: () => setStateDialog(() => localShape = ArenaShape.chaos)), + ], + ), + const SizedBox(height: 25), Divider(color: inkColor.withOpacity(0.3), thickness: 2.5), const SizedBox(height: 20), + + Text("GRANDEZZA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 15), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + NeonSizeButton(label: 'S', isSelected: localRadius == 3, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 3)), + NeonSizeButton(label: 'M', isSelected: localRadius == 4, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 4)), + NeonSizeButton(label: 'L', isSelected: localRadius == 5, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 5)), + NeonSizeButton(label: 'MAX', isSelected: localRadius == 6, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 6)), + ], + ), + const SizedBox(height: 25), Divider(color: inkColor.withOpacity(0.3), thickness: 2.5), const SizedBox(height: 20), + + Text("TEMPO E OPZIONI", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 10), + Row( + children: [ + _buildTimeOption('10s', 'FISSO', 'fixed', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'fixed')), + _buildTimeOption('RELAX', 'INFINITO', 'relax', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'relax')), + _buildTimeOption('DINAMICO', '-2s A PARTITA', 'dynamic', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'dynamic')), + ], + ), + const SizedBox(height: 35), + + Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () { + Navigator.pop(ctx); + onStart(localRadius, localShape, localTimeMode); + }, + child: CustomPaint(painter: DoodleBackgroundPainter(fillColor: theme.playerRed, strokeColor: inkColor, seed: 300), child: Container(height: 55, alignment: Alignment.center, child: Text("AVVIA", style: getSharedTextStyle(themeType, const TextStyle(fontSize: 18, fontWeight: FontWeight.w900, letterSpacing: 2.0, color: Colors.white))))), + ), + ), + const SizedBox(width: 15), + Expanded( + child: GestureDetector( + onTap: () => Navigator.pop(ctx), + child: CustomPaint(painter: DoodleBackgroundPainter(fillColor: Colors.grey.shade400, strokeColor: inkColor, seed: 301), child: Container(height: 55, alignment: Alignment.center, child: Text("ANNULLA", style: getSharedTextStyle(themeType, const TextStyle(fontSize: 18, fontWeight: FontWeight.w900, letterSpacing: 2.0, color: Colors.white))))), + ), + ), + ], + ) + ], + ), + ), + ), + ), + ) + : Container( + decoration: BoxDecoration( + gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [theme.background.withOpacity(0.95), theme.background.withOpacity(0.8)]), + borderRadius: BorderRadius.circular(25), border: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? null : Border.all(color: Colors.white.withOpacity(0.15), width: 1.5), + boxShadow: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? [] : [BoxShadow(color: Colors.black.withOpacity(0.5), blurRadius: 20, offset: const Offset(4, 10))], + ), + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("SFIDA $targetName", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 24, fontWeight: FontWeight.w900, color: theme.playerRed, letterSpacing: 2))), + const SizedBox(height: 10), + Text("IMPOSTAZIONI STANZA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), + const SizedBox(height: 20), + + Text("FORMA ARENA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10), + Wrap( + spacing: 10, runSpacing: 10, alignment: WrapAlignment.center, + children: [ + NeonShapeButton(icon: Icons.diamond_outlined, label: 'Rombo', isSelected: localShape == ArenaShape.classic, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.classic)), + NeonShapeButton(icon: Icons.add, label: 'Croce', isSelected: localShape == ArenaShape.cross, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.cross)), + NeonShapeButton(icon: Icons.donut_large, label: 'Buco', isSelected: localShape == ArenaShape.donut, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.donut)), + NeonShapeButton(icon: Icons.hourglass_bottom, label: 'Clessidra', isSelected: localShape == ArenaShape.hourglass, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.hourglass)), + NeonShapeButton(icon: Icons.all_inclusive, label: 'Caos', isSelected: localShape == ArenaShape.chaos, theme: theme, themeType: themeType, isSpecial: true, isLocked: !isChaosUnlocked, onTap: () => setStateDialog(() => localShape = ArenaShape.chaos)), + ], + ), + const SizedBox(height: 20), Divider(color: Colors.white.withOpacity(0.05), thickness: 2), const SizedBox(height: 20), + + Text("GRANDEZZA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + NeonSizeButton(label: 'S', isSelected: localRadius == 3, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 3)), + NeonSizeButton(label: 'M', isSelected: localRadius == 4, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 4)), + NeonSizeButton(label: 'L', isSelected: localRadius == 5, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 5)), + NeonSizeButton(label: 'MAX', isSelected: localRadius == 6, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 6)), + ], + ), + const SizedBox(height: 20), Divider(color: Colors.white.withOpacity(0.05), thickness: 2), const SizedBox(height: 20), + + Text("TEMPO E OPZIONI", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10), + Row( + children: [ + _buildTimeOption('10s', 'FISSO', 'fixed', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'fixed')), + _buildTimeOption('RELAX', 'INFINITO', 'relax', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'relax')), + _buildTimeOption('DINAMICO', '-2s A PARTITA', 'dynamic', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'dynamic')), + ], + ), + const SizedBox(height: 30), + + Row( + children: [ + Expanded( + child: SizedBox( + height: 55, + child: ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: theme.playerRed, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), + onPressed: () { + Navigator.pop(ctx); + onStart(localRadius, localShape, localTimeMode); + }, + child: const Text("AVVIA", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2)), + ), + ), + ), + const SizedBox(width: 15), + Expanded( + child: SizedBox( + height: 55, + child: ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: Colors.grey.shade800, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), + onPressed: () => Navigator.pop(ctx), + child: const Text("ANNULLA", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2)), + ), + ), + ), + ], + ) + ], + ), + ), + ), + ); + + if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) { + dialogContent = AnimatedCyberBorder(child: dialogContent); + } + + return Dialog(backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 20), child: dialogContent); + }, + ); + } + ); + } + + static void showMatchSetupDialog(BuildContext context, bool isVsCPU) { + int localRadius = 4; ArenaShape localShape = ArenaShape.classic; String localTimeMode = 'fixed'; + bool isChaosUnlocked = StorageService.instance.playerLevel >= 7; + final loc = AppLocalizations.of(context)!; + + showDialog( + context: context, barrierColor: Colors.black.withOpacity(0.8), + builder: (ctx) { + final themeManager = ctx.watch(); + final theme = themeManager.currentColors; final themeType = themeManager.currentThemeType; + Color inkColor = const Color(0xFF111122); + + return StatefulBuilder( + builder: (context, setStateDialog) { + Widget dialogContent = themeType == AppThemeType.doodle + ? Transform.rotate( + angle: 0.015, + child: CustomPaint( + painter: DoodleBackgroundPainter(fillColor: Colors.white.withOpacity(0.95), strokeColor: inkColor, seed: 200), + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Padding( + padding: const EdgeInsets.all(25.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row(children: [ SizedBox(width: 40, child: IconButton(padding: EdgeInsets.zero, alignment: Alignment.centerLeft, icon: Icon(Icons.arrow_back_ios_new, color: inkColor, size: 26), onPressed: () => Navigator.pop(ctx))), Expanded(child: Text(isVsCPU ? loc.cpuTitle : loc.localTitle, textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: inkColor, letterSpacing: 2)))), const SizedBox(width: 40) ]), + const SizedBox(height: 25), + + if (isVsCPU) ...[ + Icon(Icons.smart_toy, size: 50, color: inkColor.withOpacity(0.6)), const SizedBox(height: 10), + Text("MODALITÀ CAMPAGNA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.w900, color: inkColor))), const SizedBox(height: 10), + Text("Livello CPU: ${StorageService.instance.cpuLevel}\nForma e tempo si adatteranno alla tua bravura!", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 13, color: inkColor.withOpacity(0.8), height: 1.4))), const SizedBox(height: 25), + Divider(color: inkColor.withOpacity(0.3), thickness: 2.5), const SizedBox(height: 20), + ] else ...[ + Text("FORMA ARENA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 15), + Wrap( + spacing: 12, runSpacing: 12, alignment: WrapAlignment.center, + children: [ + NeonShapeButton(icon: Icons.diamond_outlined, label: 'Rombo', isSelected: localShape == ArenaShape.classic, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.classic)), + NeonShapeButton(icon: Icons.add, label: 'Croce', isSelected: localShape == ArenaShape.cross, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.cross)), + NeonShapeButton(icon: Icons.donut_large, label: 'Buco', isSelected: localShape == ArenaShape.donut, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.donut)), + NeonShapeButton(icon: Icons.hourglass_bottom, label: 'Clessidra', isSelected: localShape == ArenaShape.hourglass, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.hourglass)), + NeonShapeButton(icon: Icons.all_inclusive, label: 'Caos', isSelected: localShape == ArenaShape.chaos, theme: theme, themeType: themeType, isSpecial: true, isLocked: !isChaosUnlocked, onTap: () => setStateDialog(() => localShape = ArenaShape.chaos)), + ], + ), + const SizedBox(height: 25), Divider(color: inkColor.withOpacity(0.3), thickness: 2.5), const SizedBox(height: 20), + + Text("GRANDEZZA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 15), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + NeonSizeButton(label: 'S', isSelected: localRadius == 3, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 3)), + NeonSizeButton(label: 'M', isSelected: localRadius == 4, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 4)), + NeonSizeButton(label: 'L', isSelected: localRadius == 5, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 5)), + NeonSizeButton(label: 'MAX', isSelected: localRadius == 6, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 6)), + ], + ), + const SizedBox(height: 25), Divider(color: inkColor.withOpacity(0.3), thickness: 2.5), const SizedBox(height: 20), + + Text("TEMPO", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor.withOpacity(0.6), letterSpacing: 1.5))), const SizedBox(height: 10), + Row( + children: [ + _buildTimeOption('10s', 'FISSO', 'fixed', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'fixed')), + _buildTimeOption('RELAX', 'INFINITO', 'relax', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'relax')), + _buildTimeOption('DINAMICO', '-2s A PARTITA', 'dynamic', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'dynamic')), + ], + ), const SizedBox(height: 35), + ], + + Transform.rotate( + angle: -0.02, + child: GestureDetector( + onTap: () { Navigator.pop(ctx); context.read().startNewGame(localRadius, vsCPU: isVsCPU, shape: localShape, timeMode: localTimeMode); Navigator.push(context, MaterialPageRoute(builder: (_) => const GameScreen())); }, + child: CustomPaint(painter: DoodleBackgroundPainter(fillColor: Colors.green.shade200, strokeColor: inkColor, seed: 300), child: Container(height: 65, width: double.infinity, alignment: Alignment.center, child: Text(loc.startGame, style: getSharedTextStyle(themeType, TextStyle(fontSize: 22, fontWeight: FontWeight.w900, letterSpacing: 3.0, color: inkColor))))), + ), + ) + ], + ), + ), + ), + ), + ) + : Container( + decoration: BoxDecoration( + gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [theme.background.withOpacity(0.95), theme.background.withOpacity(0.8)]), + borderRadius: BorderRadius.circular(25), border: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? null : Border.all(color: Colors.white.withOpacity(0.15), width: 1.5), + boxShadow: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade || themeType == AppThemeType.music ? [] : [BoxShadow(color: Colors.black.withOpacity(0.5), blurRadius: 20, offset: const Offset(4, 10))], + ), + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row(children: [ SizedBox(width: 40, child: IconButton(padding: EdgeInsets.zero, alignment: Alignment.centerLeft, icon: Icon(Icons.arrow_back_ios_new, color: theme.text, size: 26), onPressed: () => Navigator.pop(ctx))), Expanded(child: Text(isVsCPU ? loc.cpuTitle : loc.localTitle, textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 24, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 2)))), const SizedBox(width: 40) ]), + const SizedBox(height: 20), + + if (isVsCPU) ...[ + Icon(Icons.smart_toy, size: 50, color: theme.playerBlue), const SizedBox(height: 10), + Text("MODALITÀ CAMPAGNA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 1.5))), const SizedBox(height: 10), + Text("Livello CPU: ${StorageService.instance.cpuLevel}\nForma e tempo si adatteranno alla tua bravura!", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 13, color: theme.text.withOpacity(0.7), height: 1.4))), const SizedBox(height: 20), + Divider(color: Colors.white.withOpacity(0.05), thickness: 2), const SizedBox(height: 20), + ] else ...[ + Text("FORMA ARENA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10), + Wrap( + spacing: 10, runSpacing: 10, alignment: WrapAlignment.center, + children: [ + NeonShapeButton(icon: Icons.diamond_outlined, label: 'Rombo', isSelected: localShape == ArenaShape.classic, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.classic)), + NeonShapeButton(icon: Icons.add, label: 'Croce', isSelected: localShape == ArenaShape.cross, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.cross)), + NeonShapeButton(icon: Icons.donut_large, label: 'Buco', isSelected: localShape == ArenaShape.donut, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.donut)), + NeonShapeButton(icon: Icons.hourglass_bottom, label: 'Clessidra', isSelected: localShape == ArenaShape.hourglass, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.hourglass)), + NeonShapeButton(icon: Icons.all_inclusive, label: 'Caos', isSelected: localShape == ArenaShape.chaos, theme: theme, themeType: themeType, isSpecial: true, isLocked: !isChaosUnlocked, onTap: () => setStateDialog(() => localShape = ArenaShape.chaos)), + ], + ), + const SizedBox(height: 20), Divider(color: Colors.white.withOpacity(0.05), thickness: 2), const SizedBox(height: 20), + + Text("GRANDEZZA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + NeonSizeButton(label: 'S', isSelected: localRadius == 3, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 3)), + NeonSizeButton(label: 'M', isSelected: localRadius == 4, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 4)), + NeonSizeButton(label: 'L', isSelected: localRadius == 5, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 5)), + NeonSizeButton(label: 'MAX', isSelected: localRadius == 6, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localRadius = 6)), + ], + ), + const SizedBox(height: 20), Divider(color: Colors.white.withOpacity(0.05), thickness: 2), const SizedBox(height: 20), + + Text("TEMPO", style: getSharedTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: theme.text.withOpacity(0.5), letterSpacing: 1.5))), const SizedBox(height: 10), + Row( + children: [ + _buildTimeOption('10s', 'FISSO', 'fixed', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'fixed')), + _buildTimeOption('RELAX', 'INFINITO', 'relax', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'relax')), + _buildTimeOption('DINAMICO', '-2s A PARTITA', 'dynamic', localTimeMode, theme, themeType, () => setStateDialog(() => localTimeMode = 'dynamic')), + ], + ), const SizedBox(height: 30), + ], + + SizedBox( + width: double.infinity, height: 60, + child: ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: isVsCPU ? Colors.purple.shade400 : theme.playerRed, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), + onPressed: () { Navigator.pop(ctx); context.read().startNewGame(localRadius, vsCPU: isVsCPU, shape: localShape, timeMode: localTimeMode); Navigator.push(context, MaterialPageRoute(builder: (_) => const GameScreen())); }, + child: Text(loc.startGame, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w900, letterSpacing: 2)), + ), + ) + ], + ), + ), + ), + ); + + if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) { + dialogContent = AnimatedCyberBorder(child: dialogContent); + } + + return Dialog(backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 20), child: dialogContent); + }, + ); + } + ); + } + + static void showWaitingDialog({ + required BuildContext context, + required String code, + required bool isPublicRoom, + required int selectedRadius, + required ArenaShape selectedShape, + required String selectedTimeMode, + required MultiplayerService multiplayerService, + required VoidCallback onRoomStarted, + required VoidCallback onCleanup, + }) { + showDialog( + context: context, + barrierDismissible: false, + builder: (dialogContext) { + final theme = dialogContext.watch().currentColors; + final themeType = dialogContext.read().currentThemeType; + + Widget dialogContent = Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator(color: theme.playerRed), const SizedBox(height: 25), + Text("CODICE STANZA", style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.text.withOpacity(0.6), letterSpacing: 2))), + Text(code, style: getSharedTextStyle(themeType, TextStyle(fontSize: 40, fontWeight: FontWeight.w900, color: theme.playerRed, letterSpacing: 8, shadows: themeType == AppThemeType.doodle ? [] : [Shadow(color: theme.playerRed.withOpacity(0.5), blurRadius: 10)]))), + const SizedBox(height: 25), + Transform.rotate( + angle: themeType == AppThemeType.doodle ? 0.02 : 0, + child: Container( + padding: const EdgeInsets.all(18), + decoration: BoxDecoration( + color: themeType == AppThemeType.doodle ? Colors.white : theme.text.withOpacity(0.05), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: themeType == AppThemeType.doodle ? theme.text : theme.playerBlue.withOpacity(0.3), width: themeType == AppThemeType.doodle ? 2 : 1.5), + boxShadow: themeType == AppThemeType.doodle + ? [BoxShadow(color: theme.text.withOpacity(0.8), offset: const Offset(4, 4))] + : [BoxShadow(color: theme.playerBlue.withOpacity(0.1), blurRadius: 10)] + ), + child: Column( + children: [ + Icon(isPublicRoom ? Icons.podcasts : Icons.share, color: theme.playerBlue, size: 32), const SizedBox(height: 12), + Text(isPublicRoom ? "Sei in Bacheca!" : "Invito inviato", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.w900, fontSize: 18))), + const SizedBox(height: 8), + Text(isPublicRoom ? "Aspettiamo che uno sfidante si unisca dalla lobby pubblica." : "Attendi che il tuo amico accetti la sfida. Non chiudere questa finestra.", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.8), fontSize: 14, height: 1.5))), + ], + ), + ), + ), + ], + ); + + if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) { + dialogContent = AnimatedCyberBorder(child: dialogContent); + } else { + dialogContent = Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: themeType == AppThemeType.doodle ? Colors.white.withOpacity(0.95) : theme.background, + borderRadius: BorderRadius.circular(25), + border: Border.all(color: themeType == AppThemeType.doodle ? theme.text : theme.gridLine.withOpacity(0.5), width: 2), + boxShadow: themeType == AppThemeType.doodle ? [BoxShadow(color: theme.text.withOpacity(0.6), offset: const Offset(8, 8))] : [] + ), + child: dialogContent + ); + } + + return StreamBuilder( + stream: multiplayerService.listenToRoom(code), + builder: (ctx, snapshot) { + if (snapshot.hasData && snapshot.data!.exists) { + var data = snapshot.data!.data() as Map; + if (data['status'] == 'playing') { + onRoomStarted(); + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.pop(ctx); + context.read().startNewGame(selectedRadius, isOnline: true, roomCode: code, isHost: true, shape: selectedShape, timeMode: selectedTimeMode); + Navigator.push(context, MaterialPageRoute(builder: (_) => const GameScreen())); + }); + } + } + + return PopScope( + canPop: false, + onPopInvoked: (didPop) { + if (didPop) return; + onCleanup(); + Navigator.pop(ctx); + }, + child: Dialog( + backgroundColor: Colors.transparent, + insetPadding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + dialogContent, + const SizedBox(height: 20), + TextButton( + onPressed: () { + onCleanup(); + Navigator.pop(ctx); + }, + child: Text("ANNULLA", style: getSharedTextStyle(themeType, TextStyle(color: Colors.red, fontWeight: FontWeight.w900, fontSize: 20, letterSpacing: 2.0, shadows: themeType == AppThemeType.doodle ? [] : [const Shadow(color: Colors.black, blurRadius: 2)]))), + ), + ], + ), + ), + ); + }, + ); + } + ); + } + + static void showJoinPromptDialog(BuildContext context, String roomCode, Function(String) onConfirm) { + showDialog( + context: context, + builder: (context) { + final themeManager = context.watch(); + final theme = themeManager.currentColors; + final themeType = themeManager.currentThemeType; + return AlertDialog( + backgroundColor: themeType == AppThemeType.doodle ? Colors.white : theme.background, + shape: themeType == AppThemeType.doodle ? RoundedRectangleBorder(borderRadius: BorderRadius.circular(15), side: BorderSide(color: theme.text, width: 2)) : null, + title: Text("Invito Trovato!", style: getSharedTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.bold))), + content: Text("Vuoi unirti alla stanza $roomCode?", style: getSharedTextStyle(themeType, TextStyle(color: theme.text))), + actions: [ + TextButton(onPressed: () => Navigator.pop(context), child: Text("No", style: getSharedTextStyle(themeType, const TextStyle(color: Colors.red)))), + ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: themeType == AppThemeType.doodle ? Colors.transparent : theme.playerBlue, elevation: 0, side: themeType == AppThemeType.doodle ? BorderSide(color: theme.text, width: 1.5) : BorderSide.none), + onPressed: () { + Navigator.of(context).pop(); + onConfirm(roomCode); + }, + child: Text(AppLocalizations.of(context)!.joinMatch, style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? theme.text : Colors.white, fontWeight: FontWeight.bold))), + ), + ], + ); + } + ); + } + + static void showFavoritesDialog(BuildContext context, Function(String, String) onInvite) { + final favs = StorageService.instance.favorites; + + showDialog( + context: context, + builder: (ctx) { + final themeManager = ctx.watch(); + final theme = themeManager.currentColors; + final themeType = themeManager.currentThemeType; + + return AlertDialog( + backgroundColor: theme.background, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + title: Text("I TUOI PREFERITI", style: getLobbyTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.bold))), + content: Container( + width: double.maxFinite, + height: 300, + decoration: BoxDecoration( + border: Border.all(color: theme.playerRed, width: 2), + borderRadius: BorderRadius.circular(10) + ), + child: favs.isEmpty + ? Center(child: Padding( + padding: const EdgeInsets.all(20.0), + child: Text("Non hai ancora aggiunto nessun preferito dalla Classifica!", textAlign: TextAlign.center, style: getLobbyTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.6)))), + )) + : ListView.builder( + itemCount: favs.length, + itemBuilder: (c, i) { + return ListTile( + title: Text(favs[i]['name']!, style: getLobbyTextStyle(themeType, TextStyle(color: theme.text, fontSize: 18, fontWeight: FontWeight.bold))), + trailing: ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: theme.playerBlue, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))), + onPressed: () { + Navigator.pop(ctx); + onInvite(favs[i]['uid']!, favs[i]['name']!); + }, + child: Text("SFIDA", style: getLobbyTextStyle(themeType, const TextStyle(color: Colors.white, fontWeight: FontWeight.bold))), + ), + ); + }, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: Text("CHIUDI", style: getLobbyTextStyle(themeType, TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold))), + ), + ], + ); + } + ); + } +} +// =========================================================================== +// FILE: lib/ui/home/home_screen.dart +// =========================================================================== + +// =========================================================================== +// FILE: lib/ui/home/home_screen.dart +// =========================================================================== + +import 'dart:ui'; +import 'dart:math'; +import 'dart:io' show Platform; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/foundation.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'dart:async'; +import 'package:app_links/app_links.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:upgrader/upgrader.dart'; +import 'package:in_app_update/in_app_update.dart'; + +import '../../logic/game_controller.dart'; +import '../../core/theme_manager.dart'; +import '../../core/app_colors.dart'; +import '../../services/storage_service.dart'; +import '../../services/audio_service.dart'; +import '../../services/multiplayer_service.dart'; +import '../multiplayer/lobby_screen.dart'; +import '../admin/admin_screen.dart'; +import '../settings/settings_screen.dart'; +import '../game/game_screen.dart'; +import 'package:tetraq/l10n/app_localizations.dart'; + +import '../../widgets/painters.dart'; +import '../../widgets/cyber_border.dart'; +import '../../widgets/music_theme_widgets.dart'; +import '../../widgets/home_buttons.dart'; +import 'dialog.dart'; +import 'home_modals.dart'; + +class HomeScreen extends StatefulWidget { + const HomeScreen({super.key}); + + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State with WidgetsBindingObserver { + + int _debugTapCount = 0; + late AppLinks _appLinks; + StreamSubscription? _linkSubscription; + StreamSubscription? _favoritesSubscription; + StreamSubscription? _invitesSubscription; + + Map _lastOnlineNotifications = {}; + + final int _selectedRadius = 4; + final ArenaShape _selectedShape = ArenaShape.classic; + final bool _isPublicRoom = true; + + bool _isLoading = false; + String? _myRoomCode; + bool _roomStarted = false; + + String _appVersion = ''; + bool _updateAvailable = false; + + final MultiplayerService _multiplayerService = MultiplayerService(); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (StorageService.instance.playerName.isEmpty) { + HomeModals.showNameDialog(context, () { + StorageService.instance.syncLeaderboard(); + _listenToInvites(); + setState(() {}); + }); + } else { + StorageService.instance.syncLeaderboard(); + _listenToInvites(); + } + _checkThemeSafety(); + }); + _checkClipboardForInvite(); + _initDeepLinks(); + _listenToFavoritesOnline(); + _loadAppVersion(); + _checkStoreForUpdate(); + } + + Future _loadAppVersion() async { + try { + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + if (mounted) { + setState(() { + _appVersion = "v. ${packageInfo.version}"; + }); + } + } catch (e) { + debugPrint("Errore lettura versione: $e"); + } + } + + Future _checkStoreForUpdate() async { + + if (kIsWeb) return; + try { + if (Platform.isAndroid) { + final info = await InAppUpdate.checkForUpdate(); + if (info.updateAvailability == UpdateAvailability.updateAvailable) { + if (mounted) setState(() => _updateAvailable = true); + } + } else if (Platform.isIOS || Platform.isMacOS) { + final upgrader = Upgrader(); + await upgrader.initialize(); + if (upgrader.isUpdateAvailable()) { + if (mounted) setState(() => _updateAvailable = true); + } + } + } catch (e) { + debugPrint("Errore controllo aggiornamenti: $e"); + } + + } + + void _triggerUpdate() async { + if (kIsWeb) return; + if (Platform.isAndroid) { + try { + final info = await InAppUpdate.checkForUpdate(); + if (info.updateAvailability == UpdateAvailability.updateAvailable) { + await InAppUpdate.performImmediateUpdate(); + } + } catch(e) { + Upgrader().sendUserToAppStore(); + } + } else { + Upgrader().sendUserToAppStore(); + } + } + + void _checkThemeSafety() { + String themeStr = StorageService.instance.getTheme(); + bool exists = AppThemeType.values.any((e) => e.toString() == themeStr); + if (!exists) { + context.read().setTheme(AppThemeType.doodle); + } + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + _cleanupGhostRoom(); + _linkSubscription?.cancel(); + _favoritesSubscription?.cancel(); + _invitesSubscription?.cancel(); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + _checkClipboardForInvite(); + _listenToFavoritesOnline(); + } else if (state == AppLifecycleState.detached) { + _cleanupGhostRoom(); + } + } + + void _cleanupGhostRoom() { + if (_myRoomCode != null && !_roomStarted) { + FirebaseFirestore.instance.collection('games').doc(_myRoomCode).delete(); + _myRoomCode = null; + } + } + + Future _initDeepLinks() async { + _appLinks = AppLinks(); + try { + final initialUri = await _appLinks.getInitialLink(); + if (initialUri != null) _handleDeepLink(initialUri); + } catch (e) { debugPrint("Errore lettura link iniziale: $e"); } + _linkSubscription = _appLinks.uriLinkStream.listen((uri) { _handleDeepLink(uri); }, onError: (err) { debugPrint("Errore stream link: $err"); }); + } + + void _handleDeepLink(Uri uri) { + if (uri.scheme == 'tetraq' && uri.host == 'join') { + String? code = uri.queryParameters['code']; + if (code != null && code.length == 5) { + Future.delayed(const Duration(milliseconds: 500), () { + if (mounted) HomeModals.showJoinPromptDialog(context, code.toUpperCase(), _joinRoomByCode); + }); + } + } + } + + Future _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) { + HomeModals.showJoinPromptDialog(context, roomCode, _joinRoomByCode); + } + } + } + } catch (e) { debugPrint("Errore lettura appunti: $e"); } + } + + void _listenToFavoritesOnline() { + _favoritesSubscription?.cancel(); + final favs = StorageService.instance.favorites; + if (favs.isEmpty) return; + + List favUids = favs.map((f) => f['uid']!).toList(); + if (favUids.length > 10) favUids = favUids.sublist(0, 10); + + _favoritesSubscription = FirebaseFirestore.instance + .collection('leaderboard') + .where(FieldPath.documentId, whereIn: favUids) + .snapshots() + .listen((snapshot) { + if (!mounted) return; + + for (var change in snapshot.docChanges) { + if (change.type == DocumentChangeType.modified || change.type == DocumentChangeType.added) { + var data = change.doc.data(); + if (data != null && data['lastActive'] != null) { + Timestamp lastActive = data['lastActive']; + int diffInSeconds = DateTime.now().difference(lastActive.toDate()).inSeconds; + + if (diffInSeconds.abs() < 180) { + String name = data['name'] ?? 'Un amico'; + if (ModalRoute.of(context)?.isCurrent == true) { + _showFavoriteOnlinePopup(name); + } + } + } + } + } + }); + } + + void _showFavoriteOnlinePopup(String name) { + if (!mounted) return; + + if (_lastOnlineNotifications.containsKey(name)) { + if (DateTime.now().difference(_lastOnlineNotifications[name]!).inMinutes < 1) return; + } + _lastOnlineNotifications[name] = DateTime.now(); + + final overlay = Overlay.of(context); + late OverlayEntry entry; + bool removed = false; + + entry = OverlayEntry( + builder: (context) => Positioned( + top: MediaQuery.of(context).padding.top + 85, + left: 20, + right: 20, + child: FavoriteOnlinePopup( + name: name, + onDismiss: () { + if (!removed) { + removed = true; + entry.remove(); + } + }, + ), + ), + ); + overlay.insert(entry); + } + + void _listenToInvites() { + final user = FirebaseAuth.instance.currentUser; + if (user == null) return; + + _invitesSubscription?.cancel(); + _invitesSubscription = FirebaseFirestore.instance + .collection('invites') + .where('toUid', isEqualTo: user.uid) + .snapshots() + .listen((snapshot) { + if (!mounted) return; + + for (var change in snapshot.docChanges) { + if (change.type == DocumentChangeType.added) { + var data = change.doc.data(); + if (data != null) { + String code = data['roomCode']; + String from = data['fromName']; + String inviteId = change.doc.id; + + Timestamp? ts = data['timestamp']; + if (ts != null) { + if (DateTime.now().difference(ts.toDate()).inMinutes > 2) { + FirebaseFirestore.instance.collection('invites').doc(inviteId).delete(); + continue; + } + } + + if (ModalRoute.of(context)?.isCurrent == true) { + _showInvitePopup(from, code, inviteId); + } + } + } + } + }); + } + + void _showInvitePopup(String fromName, String roomCode, String inviteId) { + final themeType = context.read().currentThemeType; + final theme = context.read().currentColors; + + showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => AlertDialog( + backgroundColor: themeType == AppThemeType.doodle ? Colors.white : theme.background, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: BorderSide(color: theme.playerRed, width: 2)), + title: Row( + children: [ + Icon(Icons.warning_amber_rounded, color: theme.playerRed), + const SizedBox(width: 10), + Text("SFIDA IN ARRIVO!", style: getSharedTextStyle(themeType, TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold, fontSize: 18))), + ], + ), + content: Text("$fromName ti ha sfidato a duello!\nAccetti la sfida?", style: getSharedTextStyle(themeType, TextStyle(color: theme.text, fontSize: 16))), + actions: [ + TextButton( + onPressed: () { + FirebaseFirestore.instance.collection('invites').doc(inviteId).delete(); + Navigator.pop(ctx); + }, + child: Text("RIFIUTA", style: getSharedTextStyle(themeType, const TextStyle(color: Colors.grey, fontWeight: FontWeight.bold))), + ), + ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: theme.playerBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))), + onPressed: () { + FirebaseFirestore.instance.collection('invites').doc(inviteId).delete(); + Navigator.pop(ctx); + _joinRoomByCode(roomCode); + }, + child: Text("ACCETTA!", style: getSharedTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold))), + ), + ], + ) + ); + } + + void _startDirectChallengeFlow(String targetUid, String targetName) { + HomeModals.showChallengeSetupDialog( + context, + targetName, + (int radius, ArenaShape shape, String timeMode) { + _executeSendChallenge(targetUid, targetName, radius, shape, timeMode); + } + ); + } + + Future _executeSendChallenge(String targetUid, String targetName, int radius, ArenaShape shape, String timeMode) async { + setState(() => _isLoading = true); + + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + final rnd = Random(); + String roomCode = String.fromCharCodes(Iterable.generate(5, (_) => chars.codeUnitAt(rnd.nextInt(chars.length)))); + + try { + int gameSeed = rnd.nextInt(9999999); + + await FirebaseFirestore.instance.collection('games').doc(roomCode).set({ + 'status': 'waiting', + 'hostName': StorageService.instance.playerName, + 'hostUid': FirebaseAuth.instance.currentUser?.uid, + 'radius': radius, + 'shape': shape.name, + 'timeMode': timeMode, + 'isPublic': false, + 'createdAt': FieldValue.serverTimestamp(), + 'players': [FirebaseAuth.instance.currentUser?.uid], + 'turn': 0, + 'moves': [], + 'seed': gameSeed, + }); + + await FirebaseFirestore.instance.collection('invites').add({ + 'toUid': targetUid, + 'fromName': StorageService.instance.playerName, + 'roomCode': roomCode, + 'timestamp': FieldValue.serverTimestamp(), + }); + + setState(() => _isLoading = false); + + if (mounted) { + HomeModals.showWaitingDialog( + context: context, + code: roomCode, + isPublicRoom: false, + selectedRadius: radius, + selectedShape: shape, + selectedTimeMode: timeMode, + multiplayerService: _multiplayerService, + onRoomStarted: () {}, + onCleanup: () { + FirebaseFirestore.instance.collection('games').doc(roomCode).delete(); + } + ); + } + } catch (e) { + setState(() => _isLoading = false); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Errore: $e", style: const TextStyle(color: Colors.white)), backgroundColor: Colors.red)); + } + } + } + + Future _joinRoomByCode(String code) async { + if (_isLoading) return; + FocusScope.of(context).unfocus(); + setState(() => _isLoading = true); + + try { + String playerName = StorageService.instance.playerName; + if (playerName.isEmpty) playerName = "GUEST"; + + Map? roomData = await _multiplayerService.joinGameRoom(code, playerName); + + if (!mounted) return; + setState(() => _isLoading = false); + + if (roomData != null) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("La sfida inizierà a breve..."), backgroundColor: Colors.green)); + + int hostRadius = roomData['radius'] ?? 4; + String shapeStr = roomData['shape'] ?? 'classic'; + ArenaShape hostShape = ArenaShape.values.firstWhere((e) => e.name == shapeStr, orElse: () => ArenaShape.classic); + + String hostTimeMode = roomData['timeMode'] is String ? roomData['timeMode'] : (roomData['timeMode'] == true ? 'fixed' : 'relax'); + + context.read().startNewGame(hostRadius, isOnline: true, roomCode: code, isHost: false, shape: hostShape, timeMode: hostTimeMode); + Navigator.push(context, MaterialPageRoute(builder: (_) => const GameScreen())); + } else { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Stanza non trovata, piena o partita già iniziata.", style: TextStyle(color: Colors.white)), backgroundColor: Colors.red)); + } + } catch (e) { + if (mounted) { + setState(() => _isLoading = false); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Errore di connessione: $e", style: const TextStyle(color: Colors.white)), backgroundColor: Colors.red)); + } + } + } + + BoxDecoration _glassBoxDecoration(ThemeColors theme, AppThemeType themeType) { + return BoxDecoration( + color: themeType == AppThemeType.doodle ? Colors.white : null, + gradient: themeType == AppThemeType.doodle ? null : LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.white.withOpacity(0.25), + Colors.white.withOpacity(0.05), + ], + ), + borderRadius: BorderRadius.circular(25), + border: Border.all( + color: themeType == AppThemeType.doodle ? theme.text : Colors.white.withOpacity(0.3), + width: themeType == AppThemeType.doodle ? 2 : 1.5, + ), + boxShadow: themeType == AppThemeType.doodle + ? [BoxShadow(color: theme.text.withOpacity(0.8), offset: const Offset(4, 4), blurRadius: 0)] + : [BoxShadow(color: Colors.black.withOpacity(0.2), blurRadius: 10)], + ); + } + + Widget _buildTopBar(BuildContext context, ThemeColors theme, AppThemeType themeType, String playerName, int playerLevel) { + Color inkColor = const Color(0xFF111122); + + return Padding( + padding: const EdgeInsets.only(top: 5.0, left: 15.0, right: 15.0, bottom: 10.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: () => HomeModals.showNameDialog(context, () => setState(() {})), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: _glassBoxDecoration(theme, themeType), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + CircleAvatar( + radius: 18, + backgroundColor: theme.playerBlue.withOpacity(0.2), + child: Icon(Icons.person, color: theme.playerBlue, size: 20), + ), + const SizedBox(width: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + playerName.toUpperCase(), + style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor : theme.text, fontWeight: FontWeight.bold, fontSize: 16)), + overflow: TextOverflow.visible, + softWrap: false, + ), + Text( + "LIV. $playerLevel", + style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.8) : theme.playerBlue, fontWeight: FontWeight.bold, fontSize: 11)), + overflow: TextOverflow.visible, + softWrap: false, + ), + ], + ), + ], + ), + ), + ), + + // --- BOX STATISTICHE --- + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: _glassBoxDecoration(theme, themeType), + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(themeType == AppThemeType.music ? FontAwesomeIcons.microphone : Icons.emoji_events, color: Colors.amber.shade600, size: 16), + const SizedBox(width: 6), + + Text( + "${StorageService.instance.wins}", + style: getSharedTextStyle(themeType, TextStyle( + color: themeType == AppThemeType.doodle ? inkColor : theme.text, + fontWeight: FontWeight.w900, + fontSize: 16, + )), + overflow: TextOverflow.visible, + softWrap: false, + ), + + const SizedBox(width: 10), + Icon(themeType == AppThemeType.music ? FontAwesomeIcons.compactDisc : Icons.sentiment_very_dissatisfied, color: theme.playerRed.withOpacity(0.8), size: 16), + const SizedBox(width: 6), + + Text( + "${StorageService.instance.losses}", + style: getSharedTextStyle(themeType, TextStyle( + color: themeType == AppThemeType.doodle ? inkColor : theme.text, + fontWeight: FontWeight.w900, + fontSize: 16, + )), + overflow: TextOverflow.visible, + softWrap: false, + ), + + const SizedBox(width: 10), + Container(width: 1, height: 20, color: (themeType == AppThemeType.doodle ? inkColor : Colors.white).withOpacity(0.2)), + const SizedBox(width: 10), + + AnimatedBuilder( + animation: AudioService.instance, + builder: (context, child) { + bool isMuted = AudioService.instance.isMuted; + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + AudioService.instance.toggleMute(); + }, + child: Icon( + isMuted ? Icons.volume_off : Icons.volume_up, + color: isMuted ? theme.playerRed : (themeType == AppThemeType.doodle ? inkColor : theme.text), + size: 20, + ), + ); + } + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildCyberCard(Widget card, AppThemeType themeType) { + if (themeType == AppThemeType.cyberpunk) return AnimatedCyberBorder(child: card); + return card; + } + + @override + Widget build(BuildContext context) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); + SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]); + + final themeManager = context.watch(); + final themeType = themeManager.currentThemeType; + final theme = themeManager.currentColors; + Color inkColor = const Color(0xFF111122); + final loc = AppLocalizations.of(context)!; + + String? bgImage; + if (themeType == AppThemeType.doodle) bgImage = 'assets/images/doodle_bg.jpg'; + if (themeType == AppThemeType.cyberpunk) bgImage = 'assets/images/cyber_bg.jpg'; + if (themeType == AppThemeType.music) bgImage = 'assets/images/music_bg.jpg'; + if (themeType == AppThemeType.arcade) bgImage = 'assets/images/arcade.jpg'; + if (themeType == AppThemeType.grimorio) bgImage = 'assets/images/grimorio.jpg'; + + String playerName = StorageService.instance.playerName; + if (playerName.isEmpty) playerName = "GUEST"; + int playerLevel = StorageService.instance.playerLevel; + + final double screenHeight = MediaQuery.of(context).size.height; + final double vScale = (screenHeight / 920.0).clamp(0.50, 1.0); + + Widget uiContent = SafeArea( + child: Column( + children: [ + _buildTopBar(context, theme, themeType, playerName, playerLevel), + + Expanded( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox(height: 20 * vScale), + Center( + child: Transform.rotate( + angle: themeType == AppThemeType.doodle ? -0.04 : 0, + child: GestureDetector( + onTap: () async { + _debugTapCount++; + + // CHEAT LOCALE VIVO SOLO IN DEBUG MODE (ORA CON PIPPO!) + if (kDebugMode && playerName.toUpperCase() == 'PIPPO' && _debugTapCount == 5) { + StorageService.instance.addXP(2000); + setState(() {}); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("🛠 DEBUG MODE: +20 Livelli!", style: getSharedTextStyle(themeType, const TextStyle(color: Colors.white, fontWeight: FontWeight.bold))), backgroundColor: Colors.purpleAccent, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))) + ); + } + // ACCESSO DASHBOARD + else if (_debugTapCount >= 7) { + _debugTapCount = 0; + + if (kDebugMode && playerName.toUpperCase() == 'PIPPO') { + Navigator.push(context, MaterialPageRoute(builder: (_) => const AdminScreen())); + } else { + bool isAdmin = await StorageService.instance.isUserAdmin(); + + if (isAdmin && mounted) { + Navigator.push(context, MaterialPageRoute(builder: (_) => const AdminScreen())); + } else if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Accesso Negato: Non sei un Amministratore 🛑", style: getSharedTextStyle(themeType, const TextStyle(color: Colors.white, fontWeight: FontWeight.bold))), + backgroundColor: Colors.redAccent, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), + ) + ); + } + } + } + }, + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + loc.appTitle.toUpperCase(), + style: getSharedTextStyle(themeType, TextStyle( + fontSize: 65 * vScale, + fontWeight: FontWeight.w900, + color: themeType == AppThemeType.doodle ? inkColor : theme.text, + letterSpacing: 10 * vScale, + shadows: themeType == AppThemeType.doodle + ? [const Shadow(color: Colors.white, offset: Offset(2.5, 2.5), blurRadius: 2), const Shadow(color: Colors.white, offset: Offset(-2.5, -2.5), blurRadius: 2)] + : [Shadow(color: Colors.black.withOpacity(0.8), offset: const Offset(3, 4), blurRadius: 8), Shadow(color: theme.playerBlue.withOpacity(0.4), offset: const Offset(0, 0), blurRadius: 20)] + )), + overflow: TextOverflow.visible, + softWrap: false, + ), + ), + ), + ), + ), + SizedBox(height: 40 * vScale), + + if (themeType == AppThemeType.music) ...[ + MusicCassetteCard(title: loc.onlineTitle, subtitle: loc.onlineSub, neonColor: Colors.blueAccent, angle: -0.04, leftIcon: FontAwesomeIcons.sliders, rightIcon: FontAwesomeIcons.globe, themeType: themeType, onTap: () { Navigator.push(context, MaterialPageRoute(builder: (_) => LobbyScreen())); }), + SizedBox(height: 12 * vScale), + MusicCassetteCard(title: loc.cpuTitle, subtitle: loc.cpuSub, neonColor: Colors.purpleAccent, angle: 0.03, leftIcon: FontAwesomeIcons.desktop, rightIcon: FontAwesomeIcons.music, themeType: themeType, onTap: () => HomeModals.showMatchSetupDialog(context, true)), + SizedBox(height: 12 * vScale), + MusicCassetteCard(title: loc.localTitle, subtitle: loc.localSub, neonColor: Colors.deepPurpleAccent, angle: -0.02, leftIcon: FontAwesomeIcons.headphones, rightIcon: FontAwesomeIcons.headphones, themeType: themeType, onTap: () => HomeModals.showMatchSetupDialog(context, false)), + SizedBox(height: 30 * vScale), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: MusicKnobCard(title: loc.leaderboardTitle, icon: FontAwesomeIcons.compactDisc, iconColor: Colors.amber, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => LeaderboardDialog(onChallenge: _startDirectChallengeFlow)))), + Expanded(child: MusicKnobCard(title: loc.questsTitle, icon: FontAwesomeIcons.microphoneLines, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => const QuestsDialog()))), + Expanded(child: MusicKnobCard(title: loc.themesTitle, icon: FontAwesomeIcons.palette, themeType: themeType, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => SettingsScreen())))), + Expanded(child: MusicKnobCard(title: loc.tutorialTitle, icon: FontAwesomeIcons.bookOpen, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => const TutorialDialog()))), + ], + ), + ] else ...[ + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildCyberCard(FeatureCard(title: loc.onlineTitle, subtitle: loc.onlineSub, icon: Icons.public, color: Colors.lightBlue.shade200, theme: theme, themeType: themeType, isFeatured: true, onTap: () { Navigator.push(context, MaterialPageRoute(builder: (_) => LobbyScreen())); }), themeType), + SizedBox(height: 12 * vScale), + _buildCyberCard(FeatureCard(title: loc.cpuTitle, subtitle: loc.cpuSub, icon: Icons.smart_toy, color: Colors.purple.shade200, theme: theme, themeType: themeType, onTap: () => HomeModals.showMatchSetupDialog(context, true)), themeType), + SizedBox(height: 12 * vScale), + _buildCyberCard(FeatureCard(title: loc.localTitle, subtitle: loc.localSub, icon: Icons.people_alt, color: Colors.red.shade200, theme: theme, themeType: themeType, onTap: () => HomeModals.showMatchSetupDialog(context, false)), themeType), + SizedBox(height: 12 * vScale), + + Row( + children: [ + Expanded(child: _buildCyberCard(FeatureCard(title: loc.leaderboardTitle, subtitle: "Top 50 Globale", icon: Icons.leaderboard, color: Colors.amber.shade200, theme: theme, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => LeaderboardDialog(onChallenge: _startDirectChallengeFlow)), compact: true), themeType)), + const SizedBox(width: 12), + Expanded(child: _buildCyberCard(FeatureCard(title: loc.questsTitle, subtitle: "Missioni", icon: Icons.assignment_turned_in, color: Colors.green.shade200, theme: theme, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => const QuestsDialog()), compact: true), themeType)), + ], + ), + + SizedBox(height: 12 * vScale), + + Row( + children: [ + Expanded(child: _buildCyberCard(FeatureCard(title: loc.themesTitle, subtitle: "Personalizza", icon: Icons.palette, color: Colors.teal.shade200, theme: theme, themeType: themeType, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => SettingsScreen())), compact: true), themeType)), + const SizedBox(width: 12), + Expanded(child: _buildCyberCard(FeatureCard(title: loc.tutorialTitle, subtitle: "Come giocare", icon: Icons.school, color: Colors.indigo.shade200, theme: theme, themeType: themeType, onTap: () => showDialog(context: context, builder: (ctx) => const TutorialDialog()), compact: true), themeType)), + ], + ), + ], + ), + ], + SizedBox(height: 40 * vScale), + ], + ), + ), + ), + ), + ], + ), + ); + + return Scaffold( + backgroundColor: bgImage != null ? Colors.transparent : theme.background, + extendBodyBehindAppBar: true, + body: Stack( + children: [ + Container(color: themeType == AppThemeType.doodle ? Colors.white : theme.background), + + if (themeType == AppThemeType.doodle) + Positioned.fill( + child: CustomPaint( + painter: FullScreenGridPainter(Colors.blue.withOpacity(0.15)), + ), + ), + + if (bgImage != null) + Positioned.fill( + child: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(bgImage!), + fit: BoxFit.cover, + colorFilter: themeType == AppThemeType.doodle + ? ColorFilter.mode(Colors.white.withOpacity(0.5), BlendMode.lighten) + : null, + ), + ), + ), + ), + + if (bgImage != null && (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music || themeType == AppThemeType.arcade || themeType == AppThemeType.grimorio)) + Positioned.fill( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, end: Alignment.bottomCenter, + colors: [Colors.black.withOpacity(0.4), Colors.black.withOpacity(0.8)] + ) + ), + ), + ), + + if (themeType == AppThemeType.music) + Positioned.fill( + child: IgnorePointer( + child: CustomPaint( + painter: AudioCablesPainter(), + ), + ), + ), + + Positioned.fill(child: uiContent), + + // --- NUMERO DI VERSIONE APP E BADGE AGGIORNAMENTO --- + if (_appVersion.isNotEmpty) + Positioned( + bottom: MediaQuery.of(context).padding.bottom + 10, + left: 20, + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Opacity( + opacity: 0.6, + child: Text( + _appVersion, + style: getSharedTextStyle(themeType, TextStyle( + color: themeType == AppThemeType.doodle ? inkColor : theme.text, + fontSize: 12, + fontWeight: FontWeight.bold, + letterSpacing: 1.0, + )), + ), + ), + if (_updateAvailable) ...[ + const SizedBox(width: 15), + _PulsingUpdateBadge( + themeType: themeType, + theme: theme, + onTap: _triggerUpdate, + ), + ] + ], + ), + ), + ], + ), + ); + } +} + +// --- NUOVO WIDGET: BADGE AGGIORNAMENTO PULSANTE --- +class _PulsingUpdateBadge extends StatefulWidget { + final AppThemeType themeType; + final ThemeColors theme; + final VoidCallback onTap; + + const _PulsingUpdateBadge({ + required this.themeType, + required this.theme, + required this.onTap, + }); + + @override + State<_PulsingUpdateBadge> createState() => _PulsingUpdateBadgeState(); +} + +class _PulsingUpdateBadgeState extends State<_PulsingUpdateBadge> with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 800))..repeat(reverse: true); + _scaleAnimation = Tween(begin: 0.95, end: 1.05).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + Color badgeColor = widget.themeType == AppThemeType.doodle ? Colors.red.shade700 : widget.theme.playerRed; + Color textColor = widget.themeType == AppThemeType.doodle ? Colors.white : widget.theme.playerRed; + Color bgColor = widget.themeType == AppThemeType.doodle ? badgeColor : badgeColor.withOpacity(0.15); + + return GestureDetector( + onTap: widget.onTap, + child: ScaleTransition( + scale: _scaleAnimation, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: badgeColor, width: 1.5), + boxShadow: widget.themeType == AppThemeType.doodle + ? [const BoxShadow(color: Colors.black26, offset: Offset(2, 2))] + : [BoxShadow(color: badgeColor.withOpacity(0.4), blurRadius: 8)], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.system_update_alt, color: textColor, size: 14), + const SizedBox(width: 6), + Text( + "AGGIORNAMENTO DISPONIBILE", + style: getSharedTextStyle(widget.themeType, TextStyle( + color: textColor, + fontSize: 10, + fontWeight: FontWeight.w900, + )), + ), + ], + ), + ), + ), + ); + } +} +// ---------------------------------------------------- + +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; +} + +class FavoriteOnlinePopup extends StatefulWidget { + final String name; + final VoidCallback onDismiss; + + const FavoriteOnlinePopup({super.key, required this.name, required this.onDismiss}); + + @override + State createState() => _FavoriteOnlinePopupState(); +} + +class _FavoriteOnlinePopupState extends State with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _offsetAnimation; + + @override + void initState() { + super.initState(); + _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 400)); + _offsetAnimation = Tween(begin: const Offset(0.0, -1.5), end: Offset.zero) + .animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutBack)); + + _controller.forward(); + + Future.delayed(const Duration(seconds: 3), () { + if (mounted) { + _controller.reverse().then((_) => widget.onDismiss()); + } + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final themeManager = context.watch(); + final themeType = themeManager.currentThemeType; + final theme = themeManager.currentColors; + Color inkColor = const Color(0xFF111122); + + return SlideTransition( + position: _offsetAnimation, + child: Material( + color: Colors.transparent, + elevation: 100, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: themeType == AppThemeType.doodle ? Colors.white : theme.background, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: themeType == AppThemeType.doodle ? inkColor : theme.playerBlue, + width: 2 + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 10, + offset: const Offset(0, 5) + ) + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.circle, color: Colors.greenAccent, size: 14), + const SizedBox(width: 10), + Text( + "${widget.name} è online!", + style: getSharedTextStyle( + themeType, + TextStyle( + color: themeType == AppThemeType.doodle ? inkColor : theme.text, + fontWeight: FontWeight.bold, + fontSize: 15 + ) + ), + ), + ], + ), + ), + ), + ); + } +} +// =========================================================================== +// FILE: lib/ui/multiplayer/lobby_screen.dart +// =========================================================================== + +// =========================================================================== +// FILE: lib/ui/multiplayer/lobby_screen.dart +// =========================================================================== + +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:tetraq/l10n/app_localizations.dart'; + +import '../../logic/game_controller.dart'; +import '../../models/game_board.dart'; +import '../../core/theme_manager.dart'; +import '../../core/app_colors.dart'; +import '../../services/multiplayer_service.dart'; +import '../../services/storage_service.dart'; +import '../game/game_screen.dart'; +import '../../widgets/cyber_border.dart'; +import '../../widgets/painters.dart'; // <--- ECCO L'IMPORT MANCANTE! +import 'lobby_widgets.dart'; + +class LobbyScreen extends StatefulWidget { + final String? initialRoomCode; + + const LobbyScreen({super.key, this.initialRoomCode}); + + @override + State createState() => _LobbyScreenState(); +} + +class _LobbyScreenState extends State with WidgetsBindingObserver { + final MultiplayerService _multiplayerService = MultiplayerService(); + late TextEditingController _codeController; + + bool _isLoading = false; + String? _myRoomCode; + String _playerName = ''; + + bool _isCreatingRoom = false; + + int _selectedRadius = 4; + ArenaShape _selectedShape = ArenaShape.classic; + + String _timeModeSetting = 'fixed'; + + bool _isPublicRoom = true; + bool _roomStarted = false; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + _codeController = TextEditingController(); + _playerName = StorageService.instance.playerName; + + if (widget.initialRoomCode != null && widget.initialRoomCode!.isNotEmpty) { + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { _codeController.text = widget.initialRoomCode!; }); + }); + } + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + _cleanupGhostRoom(); + _codeController.dispose(); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.detached) { + _cleanupGhostRoom(); + } + } + + void _cleanupGhostRoom() { + if (_myRoomCode != null && !_roomStarted) { + FirebaseFirestore.instance.collection('games').doc(_myRoomCode).delete(); + _myRoomCode = null; + } + } + + Future _createRoom() async { + if (_isLoading) return; + setState(() => _isLoading = true); + + try { + String code = await _multiplayerService.createGameRoom( + _selectedRadius, _playerName, _selectedShape.name, _timeModeSetting, isPublic: _isPublicRoom + ); + + if (!mounted) return; + setState(() { _myRoomCode = code; _isLoading = false; _roomStarted = false; }); + + if (!_isPublicRoom) { + _multiplayerService.shareInviteLink(code); + } + _showWaitingDialog(code); + } catch (e) { + if (mounted) { setState(() => _isLoading = false); _showError("Errore durante la creazione della partita."); } + } + } + + Future _createRoomAndInvite(String targetUid, String targetName) async { + if (_isLoading) return; + setState(() => _isLoading = true); + + try { + String code = await _multiplayerService.createGameRoom( + _selectedRadius, _playerName, _selectedShape.name, _timeModeSetting, isPublic: _isPublicRoom + ); + + await _multiplayerService.sendInvite(targetUid, code, _playerName); + + if (!mounted) return; + setState(() { _myRoomCode = code; _isLoading = false; _roomStarted = false; }); + + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Sfida inviata a $targetName!"), backgroundColor: Colors.green)); + _showWaitingDialog(code); + + } catch (e) { + if (mounted) { setState(() => _isLoading = false); _showError("Errore durante la creazione della partita."); } + } + } + + Future _joinRoomByCode(String code) async { + if (_isLoading) return; + FocusScope.of(context).unfocus(); + + code = code.trim().toUpperCase(); + if (code.isEmpty || code.length != 5) { _showError("Inserisci un codice valido di 5 caratteri."); return; } + + setState(() => _isLoading = true); + + try { + Map? 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'] ?? 4; + String shapeStr = roomData['shape'] ?? 'classic'; + ArenaShape hostShape = ArenaShape.values.firstWhere((e) => e.name == shapeStr, orElse: () => ArenaShape.classic); + + String hostTimeMode = roomData['timeMode'] is String ? roomData['timeMode'] : (roomData['timeMode'] == true ? 'fixed' : 'relax'); + + context.read().startNewGame(hostRadius, isOnline: true, roomCode: code, isHost: false, shape: hostShape, timeMode: hostTimeMode); + 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 _showFavoritesDialogForCreation() { + final favs = StorageService.instance.favorites; + + showDialog( + context: context, + builder: (ctx) { + final themeManager = ctx.watch(); + final theme = themeManager.currentColors; + final themeType = themeManager.currentThemeType; + + return AlertDialog( + backgroundColor: theme.background, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + title: Text("I TUOI PREFERITI", style: getLobbyTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.bold))), + content: Container( + width: double.maxFinite, + height: 300, + decoration: BoxDecoration( + border: Border.all(color: theme.playerRed, width: 2), + borderRadius: BorderRadius.circular(10) + ), + child: favs.isEmpty + ? Center(child: Padding( + padding: const EdgeInsets.all(20.0), + child: Text("Non hai ancora aggiunto nessun preferito dalla Classifica!", textAlign: TextAlign.center, style: getLobbyTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.6)))), + )) + : ListView.builder( + itemCount: favs.length, + itemBuilder: (c, i) { + return ListTile( + title: Text(favs[i]['name']!, style: getLobbyTextStyle(themeType, TextStyle(color: theme.text, fontSize: 18, fontWeight: FontWeight.bold))), + trailing: ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: theme.playerBlue, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))), + onPressed: () { + Navigator.pop(ctx); + _createRoomAndInvite(favs[i]['uid']!, favs[i]['name']!); + }, + child: Text("SFIDA", style: getLobbyTextStyle(themeType, const TextStyle(color: Colors.white, fontWeight: FontWeight.bold))), + ), + ); + }, + ), + ), + actions: [ + TextButton(onPressed: () => Navigator.pop(ctx), child: Text("CHIUDI", style: getLobbyTextStyle(themeType, TextStyle(color: theme.playerRed)))) + ], + ); + } + ); + } + + void _showWaitingDialog(String code) { + showDialog( + context: context, + barrierDismissible: false, + builder: (dialogContext) { + final theme = dialogContext.watch().currentColors; + final themeType = dialogContext.read().currentThemeType; + final loc = AppLocalizations.of(context)!; + + Widget dialogContent = Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator(color: theme.playerRed), const SizedBox(height: 25), + Text(loc.codeHint, style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.text.withOpacity(0.6), letterSpacing: 2))), + Text(code, style: getSharedTextStyle(themeType, TextStyle(fontSize: 40, fontWeight: FontWeight.w900, color: theme.playerRed, letterSpacing: 8, shadows: themeType == AppThemeType.doodle ? [] : [Shadow(color: theme.playerRed.withOpacity(0.5), blurRadius: 10)]))), + const SizedBox(height: 25), + Transform.rotate( + angle: themeType == AppThemeType.doodle ? 0.02 : 0, + child: Container( + padding: const EdgeInsets.all(18), + decoration: BoxDecoration( + color: themeType == AppThemeType.doodle ? Colors.white : theme.text.withOpacity(0.05), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: themeType == AppThemeType.doodle ? theme.text : theme.playerBlue.withOpacity(0.3), width: themeType == AppThemeType.doodle ? 2 : 1.5), + boxShadow: themeType == AppThemeType.doodle + ? [BoxShadow(color: theme.text.withOpacity(0.8), offset: const Offset(4, 4))] + : [BoxShadow(color: theme.playerBlue.withOpacity(0.1), blurRadius: 10)] + ), + child: Column( + children: [ + Icon(_isPublicRoom ? Icons.podcasts : Icons.share, color: theme.playerBlue, size: 32), const SizedBox(height: 12), + Text(_isPublicRoom ? "Sei in Bacheca!" : "Invito inviato", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.w900, fontSize: 18))), + const SizedBox(height: 8), + Text(_isPublicRoom ? "Aspettiamo che uno sfidante si unisca dalla lobby pubblica." : "Attendi che il tuo amico accetti la sfida. Non chiudere questa finestra.", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.8), fontSize: 14, height: 1.5))), + ], + ), + ), + ), + ], + ); + + if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) { + dialogContent = AnimatedCyberBorder(child: dialogContent); + } else { + dialogContent = Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: themeType == AppThemeType.doodle ? Colors.white.withOpacity(0.95) : theme.background, + borderRadius: BorderRadius.circular(25), + border: Border.all(color: themeType == AppThemeType.doodle ? theme.text : theme.gridLine.withOpacity(0.5), width: 2), + boxShadow: themeType == AppThemeType.doodle ? [BoxShadow(color: theme.text.withOpacity(0.6), offset: const Offset(8, 8))] : [] + ), + child: dialogContent + ); + } + + return StreamBuilder( + stream: _multiplayerService.listenToRoom(code), + builder: (ctx, snapshot) { + if (snapshot.hasData && snapshot.data!.exists) { + var data = snapshot.data!.data() as Map; + if (data['status'] == 'playing') { + _roomStarted = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.pop(ctx); + context.read().startNewGame(_selectedRadius, isOnline: true, roomCode: code, isHost: true, shape: _selectedShape, timeMode: _timeModeSetting); + Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const GameScreen())); + }); + } + } + + return PopScope( + canPop: false, + onPopInvoked: (didPop) { + if (didPop) return; + _cleanupGhostRoom(); + Navigator.pop(ctx); + }, + child: Dialog( + backgroundColor: Colors.transparent, + insetPadding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + dialogContent, + const SizedBox(height: 20), + TextButton( + onPressed: () { + _cleanupGhostRoom(); + Navigator.pop(ctx); + }, + child: Text(loc.btnCancel.toUpperCase(), style: getSharedTextStyle(themeType, TextStyle(color: Colors.red, fontWeight: FontWeight.w900, fontSize: 20, letterSpacing: 2.0, shadows: themeType == AppThemeType.doodle ? [] : [const Shadow(color: Colors.black, blurRadius: 2)]))), + ), + ], + ), + ), + ); + }, + ); + } + ); + } + + Widget _buildTimeOption(String label, String sub, String value, ThemeColors theme, AppThemeType type) { + bool isSel = value == _timeModeSetting; + return Expanded( + child: GestureDetector( + onTap: () => setState(() => _timeModeSetting = value), + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 4), + height: 50, + decoration: BoxDecoration( + color: isSel ? Colors.orange.shade600 : (type == AppThemeType.doodle ? Colors.white : theme.text.withOpacity(0.05)), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: isSel ? Colors.orange.shade800 : (type == AppThemeType.doodle ? const Color(0xFF111122) : Colors.white24), width: isSel ? 2 : 1.5), + boxShadow: isSel && type != AppThemeType.doodle ? [BoxShadow(color: Colors.orange.withOpacity(0.5), blurRadius: 8)] : [], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(label, style: getLobbyTextStyle(type, TextStyle(color: isSel ? Colors.white : (type == AppThemeType.doodle ? const Color(0xFF111122) : theme.text), fontWeight: FontWeight.w900, fontSize: 13))), + if (sub.isNotEmpty) Text(sub, style: getLobbyTextStyle(type, TextStyle(color: isSel ? Colors.white70 : (type == AppThemeType.doodle ? Colors.black54 : theme.text.withOpacity(0.5)), fontWeight: FontWeight.bold, fontSize: 8))), + ], + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + final themeManager = context.watch(); + final themeType = themeManager.currentThemeType; + final theme = themeManager.currentColors; + final loc = AppLocalizations.of(context)!; + + String? bgImage; + if (themeType == AppThemeType.doodle) bgImage = 'assets/images/doodle_bg.jpg'; + if (themeType == AppThemeType.cyberpunk) bgImage = 'assets/images/cyber_bg.jpg'; + if (themeType == AppThemeType.music) bgImage = 'assets/images/music_bg.jpg'; + if (themeType == AppThemeType.arcade) bgImage = 'assets/images/arcade.jpg'; + if (themeType == AppThemeType.grimorio) bgImage = 'assets/images/grimorio.jpg'; + + bool isChaosUnlocked = StorageService.instance.playerLevel >= 7; + + Color panelBackgroundColor = Colors.transparent; + if (themeType == AppThemeType.cyberpunk) { + panelBackgroundColor = Colors.black.withOpacity(0.1); + } else if (themeType == AppThemeType.doodle) { + panelBackgroundColor = Colors.white.withOpacity(0.5); + } else if (themeType == AppThemeType.grimorio) { + panelBackgroundColor = Colors.white.withOpacity(0.2); + } else if (themeType == AppThemeType.arcade) { + panelBackgroundColor = Colors.black.withOpacity(0.4); + } + + Widget hostPanel = Transform.rotate( + angle: themeType == AppThemeType.doodle ? 0.01 : 0, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 15), + decoration: BoxDecoration( + color: panelBackgroundColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(themeType == AppThemeType.doodle ? 5 : 20), + topRight: const Radius.circular(20), + bottomLeft: const Radius.circular(20), + bottomRight: Radius.circular(themeType == AppThemeType.doodle ? 5 : 20), + ), + border: themeType == AppThemeType.cyberpunk ? null : Border.all(color: themeType == AppThemeType.doodle ? theme.text.withOpacity(0.5) : Colors.white.withOpacity(0.15), width: themeType == AppThemeType.doodle ? 2 : 1.5), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center(child: Text(loc.roomSettings, textAlign: TextAlign.center, style: getLobbyTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.6), letterSpacing: 2.0)))), + const SizedBox(height: 10), + + Text(loc.arenaShape, style: getLobbyTextStyle(themeType, TextStyle(fontSize: 10, fontWeight: FontWeight.w900, color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.5), letterSpacing: 1.5))), + const SizedBox(height: 6), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded(child: NeonShapeButton(icon: Icons.diamond_outlined, label: 'Rombo', isSelected: _selectedShape == ArenaShape.classic, theme: theme, themeType: themeType, onTap: () => setState(() => _selectedShape = ArenaShape.classic))), + const SizedBox(width: 4), + Expanded(child: NeonShapeButton(icon: Icons.add, label: 'Croce', isSelected: _selectedShape == ArenaShape.cross, theme: theme, themeType: themeType, onTap: () => setState(() => _selectedShape = ArenaShape.cross))), + const SizedBox(width: 4), + Expanded(child: NeonShapeButton(icon: Icons.donut_large, label: 'Buco', isSelected: _selectedShape == ArenaShape.donut, theme: theme, themeType: themeType, onTap: () => setState(() => _selectedShape = ArenaShape.donut))), + const SizedBox(width: 4), + Expanded(child: NeonShapeButton(icon: Icons.hourglass_bottom, label: 'Clessidra', isSelected: _selectedShape == ArenaShape.hourglass, theme: theme, themeType: themeType, onTap: () => setState(() => _selectedShape = ArenaShape.hourglass))), + const SizedBox(width: 4), + Expanded(child: NeonShapeButton(icon: Icons.all_inclusive, label: 'Caos', isSelected: _selectedShape == ArenaShape.chaos, theme: theme, themeType: themeType, isSpecial: true, isLocked: !isChaosUnlocked, onTap: () => setState(() => _selectedShape = ArenaShape.chaos))), + ], + ), + + const SizedBox(height: 12), + Divider(color: themeType == AppThemeType.doodle ? theme.text.withOpacity(0.5) : Colors.white.withOpacity(0.05), thickness: themeType == AppThemeType.doodle ? 2.5 : 1.5), + const SizedBox(height: 12), + + Text(loc.arenaSize, style: getLobbyTextStyle(themeType, TextStyle(fontSize: 10, fontWeight: FontWeight.w900, color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.5), letterSpacing: 1.5))), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + NeonSizeButton(label: 'S', isSelected: _selectedRadius == 3, theme: theme, themeType: themeType, onTap: () => setState(() => _selectedRadius = 3)), + NeonSizeButton(label: 'M', isSelected: _selectedRadius == 4, theme: theme, themeType: themeType, onTap: () => setState(() => _selectedRadius = 4)), + NeonSizeButton(label: 'L', isSelected: _selectedRadius == 5, theme: theme, themeType: themeType, onTap: () => setState(() => _selectedRadius = 5)), + NeonSizeButton(label: 'MAX', isSelected: _selectedRadius == 6, theme: theme, themeType: themeType, onTap: () => setState(() => _selectedRadius = 6)), + ], + ), + + const SizedBox(height: 12), + Divider(color: themeType == AppThemeType.doodle ? theme.text.withOpacity(0.5) : Colors.white.withOpacity(0.05), thickness: themeType == AppThemeType.doodle ? 2.5 : 1.5), + const SizedBox(height: 12), + + Text(loc.timeAndOptions, style: getLobbyTextStyle(themeType, TextStyle(fontSize: 10, fontWeight: FontWeight.w900, color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.5), letterSpacing: 1.5))), + const SizedBox(height: 8), + + Row( + children: [ + _buildTimeOption('10s', 'FISSO', 'fixed', theme, themeType), + _buildTimeOption('RELAX', 'INFINITO', 'relax', theme, themeType), + _buildTimeOption('DINAMICO', '-2s', 'dynamic', theme, themeType), + ], + ), + const SizedBox(height: 10), + + Row( + children: [ + Expanded(child: NeonPrivacySwitch(isPublic: _isPublicRoom, theme: theme, themeType: themeType, onTap: () => setState(() => _isPublicRoom = !_isPublicRoom))), + const SizedBox(width: 8), + Expanded(child: NeonInviteFavoriteButton(theme: theme, themeType: themeType, onTap: _showFavoritesDialogForCreation)), + ], + ) + ], + ), + ), + ); + + if (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) { + hostPanel = AnimatedCyberBorder(child: hostPanel); + } + + Widget uiContent = SafeArea( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: EdgeInsets.only(left: 20.0, right: 20.0, top: 10.0, bottom: MediaQuery.of(context).padding.bottom + 60.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + IconButton( + icon: Icon(Icons.arrow_back_ios_new, color: theme.text), + onPressed: () => Navigator.pop(context), + ), + Expanded( + child: Text(loc.onlineTitle.toUpperCase(), textAlign: TextAlign.center, style: getLobbyTextStyle(themeType, TextStyle(fontSize: 20, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 2))), + ), + const SizedBox(width: 48), + ], + ), + const SizedBox(height: 20), + + AnimatedSize( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + alignment: Alignment.topCenter, + child: _isCreatingRoom + ? Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + hostPanel, + const SizedBox(height: 15), + Row( + children: [ + Expanded( + child: NeonActionButton(label: loc.btnStart.toUpperCase(), color: theme.playerRed, onTap: _createRoom, theme: theme, themeType: themeType), + ), + const SizedBox(width: 10), + Expanded( + child: NeonActionButton(label: loc.btnCancel.toUpperCase(), color: Colors.grey.shade600, onTap: () => setState(() => _isCreatingRoom = false), theme: theme, themeType: themeType), + ), + ], + ), + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + NeonActionButton(label: loc.createMatch.toUpperCase(), color: theme.playerRed, onTap: () { FocusScope.of(context).unfocus(); setState(() => _isCreatingRoom = true); }, theme: theme, themeType: themeType), + const SizedBox(height: 20), + Row( + children: [ + Expanded(child: Divider(color: theme.text.withOpacity(0.4), thickness: themeType == AppThemeType.doodle ? 2 : 1.0)), + Padding(padding: const EdgeInsets.symmetric(horizontal: 10), child: Text(loc.wordOr.toUpperCase(), style: getLobbyTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.5), fontWeight: FontWeight.bold, letterSpacing: 2.0, fontSize: 13)))), + Expanded(child: Divider(color: theme.text.withOpacity(0.4), thickness: themeType == AppThemeType.doodle ? 2 : 1.0)), + ], + ), + const SizedBox(height: 20), + + Transform.rotate( + angle: themeType == AppThemeType.doodle ? 0.02 : 0, + child: Container( + decoration: themeType == AppThemeType.doodle ? BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.only(topLeft: Radius.circular(20), bottomRight: Radius.circular(20), topRight: Radius.circular(5), bottomLeft: Radius.circular(5)), + border: Border.all(color: theme.text, width: 2.5), + boxShadow: [BoxShadow(color: theme.text.withOpacity(0.8), offset: const Offset(5, 5), blurRadius: 0)], + ) : BoxDecoration( + boxShadow: [BoxShadow(color: theme.playerBlue.withOpacity(0.15), blurRadius: 15, spreadRadius: 1)] + ), + child: TextField( + controller: _codeController, textCapitalization: TextCapitalization.characters, textAlign: TextAlign.center, maxLength: 5, + style: getLobbyTextStyle(themeType, TextStyle(fontSize: 28, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 12, shadows: themeType == AppThemeType.doodle ? [] : [Shadow(color: theme.playerBlue.withOpacity(0.5), blurRadius: 8)])), + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric(vertical: 12), + hintText: loc.codeHint.toUpperCase(), hintStyle: getLobbyTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.3), letterSpacing: 10, fontSize: 20)), counterText: "", + filled: themeType != AppThemeType.doodle, + fillColor: themeType == AppThemeType.cyberpunk ? Colors.black.withOpacity(0.85) : theme.text.withOpacity(0.05), + enabledBorder: themeType == AppThemeType.doodle ? InputBorder.none : OutlineInputBorder(borderSide: BorderSide(color: theme.gridLine.withOpacity(0.5), width: 2.0), borderRadius: BorderRadius.circular(15)), + focusedBorder: themeType == AppThemeType.doodle ? InputBorder.none : OutlineInputBorder(borderSide: BorderSide(color: theme.playerBlue, width: 3.0), borderRadius: BorderRadius.circular(15)), + ), + ), + ), + ), + const SizedBox(height: 15), + NeonActionButton(label: loc.joinMatch.toUpperCase(), color: theme.playerBlue, onTap: () => _joinRoomByCode(_codeController.text), theme: theme, themeType: themeType), + ], + ), + ), + + const SizedBox(height: 25), + Row( + children: [ + Expanded(child: Divider(color: theme.text.withOpacity(0.4), thickness: themeType == AppThemeType.doodle ? 2 : 1.0)), + Padding(padding: const EdgeInsets.symmetric(horizontal: 10), child: Text(loc.publicLobbyTitle.toUpperCase(), style: getLobbyTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.5), fontWeight: FontWeight.bold, letterSpacing: 2.0, fontSize: 13)))), + Expanded(child: Divider(color: theme.text.withOpacity(0.4), thickness: themeType == AppThemeType.doodle ? 2 : 1.0)), + ], + ), + const SizedBox(height: 15), + + StreamBuilder( + stream: _multiplayerService.getPublicRooms(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Padding(padding: const EdgeInsets.all(20), child: Center(child: CircularProgressIndicator(color: theme.playerBlue))); + } + + if (!snapshot.hasData || snapshot.data!.docs.isEmpty) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 40.0), + child: Center(child: Text(loc.emptyLobbyMsg, textAlign: TextAlign.center, style: getLobbyTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.6), height: 1.5, fontSize: 16)))), + ); + } + + DateTime now = DateTime.now(); + String? myUid = FirebaseAuth.instance.currentUser?.uid; + + var docs = snapshot.data!.docs.where((doc) { + var data = doc.data() as Map; + if (data['isPublic'] != true) return false; + if (data['hostUid'] != null && data['hostUid'] == myUid) return false; + + Timestamp? createdAt = data['createdAt'] as Timestamp?; + if (createdAt != null) { + int ageInMinutes = now.difference(createdAt.toDate()).inMinutes; + if (ageInMinutes > 15) { + FirebaseFirestore.instance.collection('games').doc(doc.id).delete(); + return false; + } + } + return true; + }).toList(); + + if (docs.isEmpty) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 40.0), + child: Center(child: Text(loc.emptyLobbyMsg, textAlign: TextAlign.center, style: getLobbyTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.6), height: 1.5, fontSize: 16)))), + ); + } + + docs.sort((a, b) { + Timestamp? tA = (a.data() as Map)['createdAt'] as Timestamp?; + Timestamp? tB = (b.data() as Map)['createdAt'] as Timestamp?; + if (tA == null || tB == null) return 0; + return tB.compareTo(tA); + }); + + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + itemCount: docs.length, + itemBuilder: (context, index) { + var doc = docs[index]; + var data = doc.data() as Map; + String host = data['hostName'] ?? 'Guest'; + int r = data['radius'] ?? 4; + String shapeStr = data['shape'] ?? 'classic'; + + String tMode = data['timeMode'] is String ? data['timeMode'] : (data['timeMode'] == true ? 'fixed' : 'relax'); + String prettyTime = "10s"; + if (tMode == 'relax') prettyTime = "Relax"; + else if (tMode == 'dynamic') prettyTime = "Dinamico"; + + String prettyShape = "Rombo"; + if (shapeStr == 'cross') prettyShape = "Croce"; + else if (shapeStr == 'donut') prettyShape = "Buco"; + else if (shapeStr == 'hourglass') prettyShape = "Clessidra"; + else if (shapeStr == 'chaos') prettyShape = "Caos"; + + return Transform.rotate( + angle: themeType == AppThemeType.doodle ? (index % 2 == 0 ? 0.01 : -0.01) : 0, + child: Container( + margin: const EdgeInsets.only(bottom: 15), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: themeType == AppThemeType.doodle ? Colors.white : theme.text.withOpacity(0.05), + borderRadius: BorderRadius.circular(15), + border: Border.all(color: themeType == AppThemeType.doodle ? theme.text : theme.playerBlue.withOpacity(0.3), width: themeType == AppThemeType.doodle ? 2 : 1), + boxShadow: themeType == AppThemeType.doodle ? [BoxShadow(color: theme.text.withOpacity(0.6), offset: const Offset(3, 4))] : [], + ), + child: Row( + children: [ + CircleAvatar(radius: 25, backgroundColor: theme.playerRed.withOpacity(0.2), child: Icon(Icons.person, color: theme.playerRed, size: 28)), + const SizedBox(width: 15), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("${loc.roomOf} $host", style: getLobbyTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 18))), + const SizedBox(height: 6), + Text("Raggio: $r • $prettyShape • $prettyTime", style: getLobbyTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.6), fontSize: 12))), + ], + ), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + backgroundColor: theme.playerBlue, foregroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + elevation: themeType == AppThemeType.doodle ? 0 : 2, + side: themeType == AppThemeType.doodle ? BorderSide(color: theme.text, width: 2) : BorderSide.none, + ), + onPressed: () => _joinRoomByCode(doc.id), + child: Text(loc.btnEnter.toUpperCase(), style: getLobbyTextStyle(themeType, const TextStyle(fontWeight: FontWeight.w900, letterSpacing: 1.0))), + ) + ], + ), + ), + ); + } + ); + } + ), + ], + ), + ), + ); + + return Scaffold( + backgroundColor: bgImage != null ? Colors.transparent : theme.background, + extendBodyBehindAppBar: true, + body: Stack( + children: [ + Container(color: themeType == AppThemeType.doodle ? Colors.white : theme.background), + + if (themeType == AppThemeType.doodle) + Positioned.fill( + child: CustomPaint( + painter: FullScreenGridPainter(Colors.blue.withOpacity(0.15)), + ), + ), + + if (bgImage != null) + Positioned.fill( + child: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(bgImage!), + fit: BoxFit.cover, + colorFilter: themeType == AppThemeType.doodle + ? ColorFilter.mode(Colors.white.withOpacity(0.5), BlendMode.lighten) + : null, + ), + ), + ), + ), + + if (bgImage != null && (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music || themeType == AppThemeType.arcade || themeType == AppThemeType.grimorio)) + Positioned.fill( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, end: Alignment.bottomCenter, + colors: [Colors.black.withOpacity(0.4), Colors.black.withOpacity(0.8)] + ) + ), + ), + ), + + if (themeType == AppThemeType.music) + Positioned.fill( + child: IgnorePointer( + child: CustomPaint( + painter: AudioCablesPainter(), + ), + ), + ), + + Positioned.fill(child: uiContent), + ], + ), + ); + } +} + +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; +} +// =========================================================================== +// FILE: lib/ui/multiplayer/lobby_widgets.dart +// =========================================================================== + +// =========================================================================== +// FILE: lib/ui/multiplayer/lobby_widgets.dart +// =========================================================================== + +import 'package:flutter/material.dart'; +import 'dart:math' as math; +import 'package:google_fonts/google_fonts.dart'; + +import '../../core/theme_manager.dart'; +import '../../core/app_colors.dart'; + +TextStyle getLobbyTextStyle(AppThemeType themeType, TextStyle baseStyle) { + if (themeType == AppThemeType.doodle) { + return GoogleFonts.permanentMarker(textStyle: baseStyle); + } else if (themeType == AppThemeType.arcade) { + return GoogleFonts.pressStart2p(textStyle: baseStyle.copyWith( + fontSize: baseStyle.fontSize != null ? baseStyle.fontSize! * 0.75 : null, + letterSpacing: 0.5, + )); + } else if (themeType == AppThemeType.grimorio) { + return GoogleFonts.cinzelDecorative(textStyle: baseStyle.copyWith(fontWeight: FontWeight.bold)); + } else if (themeType == AppThemeType.music) { + return GoogleFonts.audiowide(textStyle: baseStyle.copyWith(letterSpacing: 1.5)); + } + return baseStyle; +} + +class NeonShapeButton extends StatelessWidget { + final IconData icon; + final String label; + final bool isSelected; + final ThemeColors theme; + final AppThemeType themeType; + final VoidCallback onTap; + final bool isLocked; + final bool isSpecial; + + const NeonShapeButton({ + super.key, required this.icon, required this.label, required this.isSelected, + required this.theme, required this.themeType, required this.onTap, + this.isLocked = false, this.isSpecial = false + }); + + Color _getDoodleColor() { + switch (label) { + case 'Rombo': return Colors.blue.shade700; + case 'Croce': return Colors.teal.shade700; + case 'Buco': return Colors.pink.shade600; + case 'Clessidra': return Colors.deepPurple.shade600; + case 'Caos': return Colors.blueGrey.shade800; + default: return Colors.blue.shade700; + } + } + + @override + Widget build(BuildContext context) { + if (themeType == AppThemeType.doodle) { + Color doodleColor = isLocked ? Colors.grey : _getDoodleColor(); + double tilt = (label.length % 2 == 0) ? -0.03 : 0.04; + + return Transform.rotate( + angle: tilt, + child: GestureDetector( + onTap: isLocked ? null : onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 6), + transform: Matrix4.translationValues(0, isSelected ? 3 : 0, 0), + decoration: BoxDecoration( + color: isSelected ? doodleColor : Colors.white, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(15), topRight: Radius.circular(8), + bottomLeft: Radius.circular(6), bottomRight: Radius.circular(18), + ), + border: Border.all(color: isSelected ? theme.text : doodleColor.withOpacity(0.5), width: isSelected ? 2.5 : 1.5), + boxShadow: isSelected + ? [BoxShadow(color: theme.text.withOpacity(0.8), offset: const Offset(3, 4), blurRadius: 0)] + : [BoxShadow(color: doodleColor.withOpacity(0.2), offset: const Offset(2, 2), blurRadius: 0)], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(isLocked ? Icons.lock : icon, color: isSelected ? Colors.white : doodleColor, size: 20), + const SizedBox(height: 2), + FittedBox(fit: BoxFit.scaleDown, child: Text(isLocked ? "Liv. 7" : label, style: getLobbyTextStyle(themeType, TextStyle(color: isSelected ? Colors.white : doodleColor, fontSize: 9, fontWeight: FontWeight.w900, letterSpacing: 0.2)))), + ], + ), + ), + ), + ); + } + + Color mainColor = isSpecial && !isLocked ? Colors.purpleAccent : theme.playerBlue; + return GestureDetector( + onTap: isLocked ? null : onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 250), + curve: Curves.easeOutCubic, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + transform: Matrix4.translationValues(0, isSelected ? 2 : 0, 0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: isLocked + ? [Colors.grey.withOpacity(0.1), Colors.black.withOpacity(0.2)] + : isSelected + ? [mainColor.withOpacity(0.3), mainColor.withOpacity(0.1)] + : [theme.text.withOpacity(0.1), theme.text.withOpacity(0.02)], + ), + border: Border.all( + color: isLocked ? Colors.transparent : (isSelected ? mainColor : Colors.white.withOpacity(0.1)), + width: isSelected ? 2 : 1, + ), + boxShadow: isLocked ? [] : isSelected + ? [BoxShadow(color: mainColor.withOpacity(0.5), blurRadius: 15, spreadRadius: 1, offset: const Offset(0, 0))] + : [ + BoxShadow(color: Colors.black.withOpacity(0.4), blurRadius: 6, offset: const Offset(2, 4)), + BoxShadow(color: Colors.white.withOpacity(0.05), blurRadius: 2, offset: const Offset(-1, -1)), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(isLocked ? Icons.lock : icon, color: isLocked ? Colors.grey.withOpacity(0.5) : (isSelected ? Colors.white : theme.text.withOpacity(0.6)), size: 20), + const SizedBox(height: 4), + FittedBox(fit: BoxFit.scaleDown, child: Text(isLocked ? "Liv. 7" : label, style: getLobbyTextStyle(themeType, TextStyle(color: isLocked ? Colors.grey.withOpacity(0.5) : (isSelected ? Colors.white : theme.text.withOpacity(0.6)), fontSize: 9, fontWeight: isSelected ? FontWeight.w900 : FontWeight.bold)))), + ], + ), + ), + ); + } +} + +class NeonSizeButton extends StatelessWidget { + final String label; + final bool isSelected; + final ThemeColors theme; + final AppThemeType themeType; + final VoidCallback onTap; + + const NeonSizeButton({super.key, required this.label, required this.isSelected, required this.theme, required this.themeType, required this.onTap}); + + @override + Widget build(BuildContext context) { + if (themeType == AppThemeType.doodle) { + Color doodleColor = label == 'MAX' ? Colors.red.shade700 : Colors.blueGrey.shade600; + double tilt = (label == 'M' || label == 'MAX') ? 0.05 : -0.04; + + return Transform.rotate( + angle: tilt, + child: GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 42, height: 40, + transform: Matrix4.translationValues(0, isSelected ? 3 : 0, 0), + decoration: BoxDecoration( + color: isSelected ? doodleColor : Colors.white, + borderRadius: const BorderRadius.all(Radius.elliptical(25, 20)), + border: Border.all(color: isSelected ? theme.text : doodleColor.withOpacity(0.5), width: 2), + boxShadow: isSelected + ? [BoxShadow(color: theme.text.withOpacity(0.8), offset: const Offset(3, 4), blurRadius: 0)] + : [BoxShadow(color: doodleColor.withOpacity(0.2), offset: const Offset(2, 2), blurRadius: 0)], + ), + child: Center( + child: FittedBox(fit: BoxFit.scaleDown, child: Text(label, style: getLobbyTextStyle(themeType, TextStyle(color: isSelected ? Colors.white : doodleColor, fontSize: 13, fontWeight: FontWeight.w900)))), + ), + ), + ), + ); + } + + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 250), + curve: Curves.easeOutCubic, + width: 42, height: 42, + transform: Matrix4.translationValues(0, isSelected ? 2 : 0, 0), + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: isSelected + ? [theme.playerRed.withOpacity(0.3), theme.playerRed.withOpacity(0.1)] + : [theme.text.withOpacity(0.1), theme.text.withOpacity(0.02)], + ), + border: Border.all(color: isSelected ? theme.playerRed : Colors.white.withOpacity(0.1), width: isSelected ? 2 : 1), + boxShadow: isSelected + ? [BoxShadow(color: theme.playerRed.withOpacity(0.5), blurRadius: 15, spreadRadius: 1)] + : [ + BoxShadow(color: Colors.black.withOpacity(0.4), blurRadius: 6, offset: const Offset(2, 4)), + BoxShadow(color: Colors.white.withOpacity(0.05), blurRadius: 2, offset: const Offset(-1, -1)), + ], + ), + child: Center( + child: FittedBox(fit: BoxFit.scaleDown, child: Text(label, style: getLobbyTextStyle(themeType, TextStyle(color: isSelected ? Colors.white : theme.text.withOpacity(0.6), fontSize: 12, fontWeight: isSelected ? FontWeight.w900 : FontWeight.bold)))), + ), + ), + ); + } +} + +class NeonTimeSwitch extends StatelessWidget { + final bool isTimeMode; + final ThemeColors theme; + final AppThemeType themeType; + final VoidCallback onTap; + + const NeonTimeSwitch({super.key, required this.isTimeMode, required this.theme, required this.themeType, required this.onTap}); + + @override + Widget build(BuildContext context) { + if (themeType == AppThemeType.doodle) { + Color doodleColor = Colors.orange.shade700; + return Transform.rotate( + angle: -0.015, + child: GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + transform: Matrix4.translationValues(0, isTimeMode ? 3 : 0, 0), + decoration: BoxDecoration( + color: isTimeMode ? doodleColor : Colors.white, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(8), topRight: Radius.circular(15), + bottomLeft: Radius.circular(15), bottomRight: Radius.circular(6), + ), + border: Border.all(color: isTimeMode ? theme.text : doodleColor.withOpacity(0.5), width: 2.5), + boxShadow: isTimeMode + ? [BoxShadow(color: theme.text.withOpacity(0.8), offset: const Offset(4, 5), blurRadius: 0)] + : [BoxShadow(color: doodleColor.withOpacity(0.2), offset: const Offset(2, 2), blurRadius: 0)], + ), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(isTimeMode ? Icons.timer : Icons.timer_off, color: isTimeMode ? Colors.white : doodleColor, size: 20), + const SizedBox(width: 8), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(isTimeMode ? 'A TEMPO' : 'RELAX', style: getLobbyTextStyle(themeType, TextStyle(color: isTimeMode ? Colors.white : doodleColor, fontWeight: FontWeight.w900, fontSize: 12, letterSpacing: 1.0)))), + FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(isTimeMode ? '15s a mossa' : 'Senza limiti', style: getLobbyTextStyle(themeType, TextStyle(color: isTimeMode ? Colors.white : doodleColor.withOpacity(0.8), fontSize: 9, fontWeight: FontWeight.bold)))), + ], + ), + ), + ], + ), + ), + ), + ); + } + + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: isTimeMode + ? [Colors.amber.withOpacity(0.25), Colors.amber.withOpacity(0.05)] + : [theme.text.withOpacity(0.1), theme.text.withOpacity(0.02)], + ), + border: Border.all(color: isTimeMode ? Colors.amber : Colors.white.withOpacity(0.1), width: isTimeMode ? 2 : 1), + boxShadow: isTimeMode + ? [BoxShadow(color: Colors.amber.withOpacity(0.3), blurRadius: 15, spreadRadius: 2)] + : [ + BoxShadow(color: Colors.black.withOpacity(0.4), blurRadius: 6, offset: const Offset(2, 4)), + BoxShadow(color: Colors.white.withOpacity(0.05), blurRadius: 2, offset: const Offset(-1, -1)), + ], + ), + 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: 20), + const SizedBox(width: 8), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(isTimeMode ? 'A TEMPO' : 'RELAX', style: getLobbyTextStyle(themeType, TextStyle(color: isTimeMode ? Colors.white : theme.text.withOpacity(0.5), fontWeight: FontWeight.w900, fontSize: 11, letterSpacing: 1.5)))), + FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(isTimeMode ? '15s a mossa' : 'Senza limiti', style: getLobbyTextStyle(themeType, TextStyle(color: isTimeMode ? Colors.amber.shade200 : theme.text.withOpacity(0.4), fontSize: 9, fontWeight: FontWeight.bold)))), + ], + ), + ), + ], + ), + ), + ); + } +} + +class NeonPrivacySwitch extends StatelessWidget { + final bool isPublic; + final ThemeColors theme; + final AppThemeType themeType; + final VoidCallback onTap; + + const NeonPrivacySwitch({super.key, required this.isPublic, required this.theme, required this.themeType, required this.onTap}); + + @override + Widget build(BuildContext context) { + if (themeType == AppThemeType.doodle) { + Color doodleColor = isPublic ? Colors.green.shade600 : Colors.red.shade600; + return Transform.rotate( + angle: 0.015, + child: GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + transform: Matrix4.translationValues(0, isPublic ? 3 : 0, 0), + decoration: BoxDecoration( + color: isPublic ? doodleColor : Colors.white, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(15), topRight: Radius.circular(8), + bottomLeft: Radius.circular(6), bottomRight: Radius.circular(15), + ), + border: Border.all(color: isPublic ? theme.text : doodleColor.withOpacity(0.5), width: 2.5), + boxShadow: [BoxShadow(color: isPublic ? theme.text.withOpacity(0.8) : doodleColor.withOpacity(0.2), offset: const Offset(4, 5), blurRadius: 0)], + ), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(isPublic ? Icons.public : Icons.lock, color: isPublic ? Colors.white : doodleColor, size: 20), + const SizedBox(width: 8), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(isPublic ? 'STANZA PUBBLICA' : 'STANZA PRIVATA', style: getLobbyTextStyle(themeType, TextStyle(color: isPublic ? Colors.white : doodleColor, fontWeight: FontWeight.w900, fontSize: 10, letterSpacing: 1.0)))), + FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(isPublic ? 'In bacheca' : 'Invita con codice', style: getLobbyTextStyle(themeType, TextStyle(color: isPublic ? Colors.white : doodleColor.withOpacity(0.8), fontSize: 9, fontWeight: FontWeight.bold)))), + ], + ), + ), + ], + ), + ), + ), + ); + } + + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: isPublic + ? [Colors.greenAccent.withOpacity(0.25), Colors.greenAccent.withOpacity(0.05)] + : [theme.playerRed.withOpacity(0.25), theme.playerRed.withOpacity(0.05)], + ), + border: Border.all(color: isPublic ? Colors.greenAccent : theme.playerRed, width: isPublic ? 2 : 1), + boxShadow: isPublic + ? [BoxShadow(color: Colors.greenAccent.withOpacity(0.3), blurRadius: 15, spreadRadius: 2)] + : [BoxShadow(color: Colors.black.withOpacity(0.4), blurRadius: 6, offset: const Offset(2, 4))], + ), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(isPublic ? Icons.public : Icons.lock, color: isPublic ? Colors.greenAccent : theme.playerRed, size: 20), + const SizedBox(width: 8), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(isPublic ? 'STANZA PUBBLICA' : 'STANZA PRIVATA', style: getLobbyTextStyle(themeType, TextStyle(color: isPublic ? Colors.white : theme.text.withOpacity(0.8), fontWeight: FontWeight.w900, fontSize: 10, letterSpacing: 1.0)))), + FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(isPublic ? 'Tutti ti vedono' : 'Solo con Codice', style: getLobbyTextStyle(themeType, TextStyle(color: isPublic ? Colors.greenAccent.shade200 : theme.playerRed.withOpacity(0.7), fontSize: 9, fontWeight: FontWeight.bold)))), + ], + ), + ), + ], + ), + ), + ); + } +} + +class NeonInviteFavoriteButton extends StatelessWidget { + final ThemeColors theme; + final AppThemeType themeType; + final VoidCallback onTap; + + const NeonInviteFavoriteButton({super.key, required this.theme, required this.themeType, required this.onTap}); + + @override + Widget build(BuildContext context) { + if (themeType == AppThemeType.doodle) { + Color doodleColor = Colors.pink.shade600; + return Transform.rotate( + angle: -0.015, + child: GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(8), topRight: Radius.circular(15), + bottomLeft: Radius.circular(15), bottomRight: Radius.circular(6), + ), + border: Border.all(color: doodleColor.withOpacity(0.5), width: 2.5), + boxShadow: [BoxShadow(color: doodleColor.withOpacity(0.2), offset: const Offset(4, 5), blurRadius: 0)], + ), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.favorite, color: doodleColor, size: 20), + const SizedBox(width: 8), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text('PREFERITI', style: getLobbyTextStyle(themeType, TextStyle(color: doodleColor, fontWeight: FontWeight.w900, fontSize: 12, letterSpacing: 1.0)))), + FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text('Invita amico', style: getLobbyTextStyle(themeType, TextStyle(color: doodleColor.withOpacity(0.8), fontSize: 9, fontWeight: FontWeight.bold)))), + ], + ), + ), + ], + ), + ), + ), + ); + } + + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Colors.pinkAccent.withOpacity(0.25), Colors.pinkAccent.withOpacity(0.05)], + ), + border: Border.all(color: Colors.pinkAccent, width: 1.5), + boxShadow: [BoxShadow(color: Colors.pinkAccent.withOpacity(0.3), blurRadius: 15, spreadRadius: 2)], + ), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.favorite, color: Colors.pinkAccent, size: 20), + const SizedBox(width: 8), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text('PREFERITI', style: getLobbyTextStyle(themeType, const TextStyle(color: Colors.white, fontWeight: FontWeight.w900, fontSize: 11, letterSpacing: 1.5)))), + FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text('Invita amico', style: getLobbyTextStyle(themeType, TextStyle(color: Colors.pinkAccent.shade200, fontSize: 9, fontWeight: FontWeight.bold)))), + ], + ), + ), + ], + ), + ), + ); + } +} + +class NeonActionButton extends StatelessWidget { + final String label; + final Color color; + final VoidCallback onTap; + final ThemeColors theme; + final AppThemeType themeType; + + const NeonActionButton({super.key, required this.label, required this.color, required this.onTap, required this.theme, required this.themeType}); + + @override + Widget build(BuildContext context) { + if (themeType == AppThemeType.doodle) { + double tilt = (label == "UNISCITI" || label == "ANNULLA") ? -0.015 : 0.02; + return Transform.rotate( + angle: tilt, + child: GestureDetector( + onTap: onTap, + child: Container( + height: 50, + decoration: BoxDecoration( + color: color, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(10), topRight: Radius.circular(20), + bottomLeft: Radius.circular(25), bottomRight: Radius.circular(10), + ), + border: Border.all(color: theme.text, width: 3.0), + boxShadow: [BoxShadow(color: theme.text.withOpacity(0.9), offset: const Offset(4, 4), blurRadius: 0)], + ), + child: Center( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Text(label, style: getLobbyTextStyle(themeType, const TextStyle(fontSize: 20, fontWeight: FontWeight.w900, letterSpacing: 3.0, color: Colors.white))), + ), + ), + ), + ), + ), + ); + } + + return GestureDetector( + onTap: onTap, + child: Container( + height: 50, + decoration: BoxDecoration( + gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [color.withOpacity(0.9), color.withOpacity(0.6)]), + borderRadius: BorderRadius.circular(15), + border: Border.all(color: Colors.white.withOpacity(0.3), width: 1.5), + boxShadow: [ + BoxShadow(color: Colors.black.withOpacity(0.5), offset: const Offset(4, 8), blurRadius: 12), + BoxShadow(color: color.withOpacity(0.3), offset: const Offset(0, 0), blurRadius: 15, spreadRadius: 1), + ], + ), + child: Center( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Text(label, style: getLobbyTextStyle(themeType, const TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2.0, color: Colors.white, shadows: [Shadow(color: Colors.black, blurRadius: 2, offset: Offset(1, 1))]))), + ), + ), + ), + ), + ); + } +} +// =========================================================================== +// FILE: lib/ui/settings/settings_screen.dart +// =========================================================================== + +// =========================================================================== +// FILE: lib/ui/settings/settings_screen.dart +// =========================================================================== + +import 'dart:ui'; // <--- IMPORTANTE: Aggiunto per ImageFilter.blur +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../../core/theme_manager.dart'; +import '../../core/app_colors.dart'; +import '../../services/storage_service.dart'; +import '../../widgets/painters.dart'; + +class SettingsScreen extends StatefulWidget { + const SettingsScreen({super.key}); + + @override + State createState() => _SettingsScreenState(); +} + +class _SettingsScreenState extends State { + @override + Widget build(BuildContext context) { + final themeManager = context.watch(); + final theme = themeManager.currentColors; + final themeType = themeManager.currentThemeType; + + int playerLevel = StorageService.instance.playerLevel; + + final double screenHeight = MediaQuery.of(context).size.height; + final double vScale = (screenHeight / 920.0).clamp(0.7, 1.2); + + return Scaffold( + backgroundColor: theme.background, + extendBodyBehindAppBar: true, + appBar: AppBar( + toolbarHeight: 80 * vScale, + title: Container( + padding: EdgeInsets.symmetric(horizontal: 24 * vScale, vertical: 10 * vScale), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.white.withOpacity(0.3), + Colors.white.withOpacity(0.05), + ], + ), + borderRadius: BorderRadius.circular(15), + border: Border.all(color: Colors.white.withOpacity(0.5), width: 1.5), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 10, + offset: const Offset(0, 4), + ) + ], + ), + child: Text( + "SELEZIONA TEMA", + style: getSharedTextStyle(themeType, TextStyle( + fontWeight: FontWeight.w900, + color: Colors.white, + letterSpacing: 2.0, + fontSize: 20 * vScale, + shadows: [Shadow(color: Colors.black.withOpacity(0.8), blurRadius: 5, offset: const Offset(2, 2))] + )) + ), + ), + centerTitle: true, + backgroundColor: Colors.transparent, + elevation: 0, + iconTheme: IconThemeData(color: Colors.white, size: 28 * vScale), + ), + body: Stack( + children: [ + Container(color: themeType == AppThemeType.doodle ? Colors.white : theme.background), + + Positioned.fill( + child: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: const AssetImage('assets/images/sfondo_temi.jpg'), + fit: BoxFit.cover, + colorFilter: ColorFilter.mode(Colors.black.withOpacity(0.6), BlendMode.darken), + ), + ), + ), + ), + + ListView( + padding: EdgeInsets.only(top: 120 * vScale, left: 20 * vScale, right: 20 * vScale, bottom: 40 * vScale), + physics: const BouncingScrollPhysics(), + children: [ + _ThemeCard( + title: "Quaderno", + subtitle: "Sfondo a quadretti, tratto a penna", + type: AppThemeType.doodle, + previewColors: AppColors.doodle, + requiredLevel: 1, + currentLevel: playerLevel, + vScale: vScale, + ), + SizedBox(height: 25 * vScale), + _ThemeCard( + title: "Cyberpunk", + subtitle: "Nero profondo, luci al neon", + type: AppThemeType.cyberpunk, + previewColors: AppColors.cyberpunk, + requiredLevel: 3, + currentLevel: playerLevel, + vScale: vScale, + ), + SizedBox(height: 25 * vScale), + _ThemeCard( + title: "8-Bit Arcade", + subtitle: "Sale giochi, fosfori verdi e pixel", + type: AppThemeType.arcade, + previewColors: AppColors.arcade, + requiredLevel: 7, + currentLevel: playerLevel, + vScale: vScale, + ), + SizedBox(height: 25 * vScale), + _ThemeCard( + title: "Grimorio", + subtitle: "Incantesimi antichi, rune magiche", + type: AppThemeType.grimorio, + previewColors: AppColors.grimorio, + requiredLevel: 10, + currentLevel: playerLevel, + vScale: vScale, + ), + SizedBox(height: 25 * vScale), + _ThemeCard( + title: "Musica", + subtitle: "Vinili, cassette e vibrazioni sonore", + type: AppThemeType.music, + previewColors: AppColors.music, + requiredLevel: 15, + currentLevel: playerLevel, + vScale: vScale, + ), + SizedBox(height: 40 * vScale), + ], + ), + ], + ), + ); + } +} + +class _ThemeCard extends StatelessWidget { + final String title; + final String subtitle; + final AppThemeType type; + final ThemeColors previewColors; + final int requiredLevel; + final int currentLevel; + final double vScale; + + const _ThemeCard({ + required this.title, + required this.subtitle, + required this.type, + required this.previewColors, + required this.requiredLevel, + required this.currentLevel, + required this.vScale, + }); + + @override + Widget build(BuildContext context) { + final themeManager = context.watch(); + bool isSelected = themeManager.currentThemeType == type; + bool isLocked = currentLevel < requiredLevel; + + String? bgImage; + if (type == AppThemeType.doodle) bgImage = 'assets/images/doodle_bg.jpg'; + if (type == AppThemeType.cyberpunk) bgImage = 'assets/images/cyber_bg.jpg'; + if (type == AppThemeType.music) bgImage = 'assets/images/music_bg.jpg'; + if (type == AppThemeType.arcade) bgImage = 'assets/images/arcade.jpg'; + if (type == AppThemeType.grimorio) bgImage = 'assets/images/grimorio.jpg'; + + Border border; + List shadows = [ + BoxShadow(color: Colors.black.withOpacity(0.8), offset: const Offset(0, 10), blurRadius: 15) + ]; + + if (type == AppThemeType.doodle) { + border = Border.all(color: isSelected ? previewColors.playerBlue : const Color(0xFF111122).withOpacity(0.8), width: isSelected ? 4 : 2); + if (isSelected) shadows.add(const BoxShadow(color: Color(0xFF111122), offset: Offset(4, 5))); + } else if (type == AppThemeType.cyberpunk || type == AppThemeType.music) { + border = Border.all(color: isSelected ? previewColors.playerBlue : previewColors.gridLine.withOpacity(0.8), width: isSelected ? 3 : 1.5); + if (isSelected) shadows.add(BoxShadow(color: previewColors.playerBlue.withOpacity(0.8), blurRadius: 25, spreadRadius: 3)); + } else if (type == AppThemeType.arcade) { + border = Border.all(color: isSelected ? previewColors.gridLine : Colors.white54, width: isSelected ? 4 : 2); + if (isSelected) shadows.add(BoxShadow(color: previewColors.gridLine.withOpacity(0.5), offset: const Offset(4, 4))); + } else { + border = Border.all(color: isSelected ? Colors.amber : previewColors.gridLine.withOpacity(0.8), width: isSelected ? 3 : 1.5); + if (isSelected) shadows.add(BoxShadow(color: Colors.amber.withOpacity(0.6), blurRadius: 20)); + } + + return GestureDetector( + onTap: () { + if (isLocked) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Gioca per raggiungere il Liv. $requiredLevel e sbloccare questo tema!", style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.white)), + backgroundColor: Colors.redAccent, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + duration: const Duration(seconds: 2), + ) + ); + return; + } + themeManager.setTheme(type); + Navigator.pop(context); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + height: 140 * vScale, + padding: EdgeInsets.symmetric(horizontal: 20 * vScale, vertical: 15 * vScale), + decoration: BoxDecoration( + color: isLocked ? Colors.black87 : previewColors.background, + borderRadius: BorderRadius.circular(20), + border: border, + boxShadow: shadows, + image: bgImage != null ? DecorationImage( + image: AssetImage(bgImage!), + fit: BoxFit.cover, + colorFilter: type == AppThemeType.doodle + ? ColorFilter.mode(Colors.white.withOpacity(isLocked ? 0.9 : 0.4), BlendMode.lighten) + : ColorFilter.mode(Colors.black.withOpacity(isLocked ? 0.85 : 0.5), BlendMode.darken), + ) : null, + ), + child: Stack( + alignment: Alignment.center, + children: [ + Opacity( + opacity: isLocked ? 0.3 : 1.0, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + // --- CORNICE EFFETTO VETRO (GLASSMORPHISM) --- + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 6.0, sigmaY: 6.0), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 15 * vScale, vertical: 10 * vScale), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.white.withOpacity(0.5), // Più visibile in alto a sx + Colors.white.withOpacity(0.1), // Quasi trasparente in basso a dx + ], + ), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.white.withOpacity(0.3), width: 1.5), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text( + title, + style: getSharedTextStyle( + type, + TextStyle( + fontSize: (type == AppThemeType.arcade ? 15 : 22) * vScale, + fontWeight: FontWeight.w900, + color: const Color(0xFF111122), + letterSpacing: type == AppThemeType.music ? 1.5 : 0.5, + ) + ) + ), + ), + SizedBox(height: 4 * vScale), + FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text( + subtitle, + style: getSharedTextStyle( + type, + TextStyle( + fontSize: (type == AppThemeType.arcade ? 8 : 12) * vScale, + color: const Color(0xFF333344), + fontWeight: FontWeight.bold, + ) + ) + ), + ), + ], + ), + ), + ), + ), + ), + SizedBox(width: 15 * vScale), + Container( + width: 28 * vScale, height: 28 * vScale, + decoration: BoxDecoration( + color: previewColors.playerRed, + shape: type == AppThemeType.arcade ? BoxShape.rectangle : BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.5), blurRadius: 5, offset: const Offset(1, 2))] + ) + ), + SizedBox(width: 12 * vScale), + Container( + width: 28 * vScale, height: 28 * vScale, + decoration: BoxDecoration( + color: previewColors.playerBlue, + shape: type == AppThemeType.arcade ? BoxShape.rectangle : BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.5), blurRadius: 5, offset: const Offset(1, 2))] + ) + ), + ], + ), + ), + if (isLocked) + Container( + padding: EdgeInsets.symmetric(horizontal: 16 * vScale, vertical: 10 * vScale), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.95), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: previewColors.playerRed.withOpacity(0.8), width: 2), + boxShadow: [BoxShadow(color: previewColors.playerRed.withOpacity(0.5), blurRadius: 20)], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.lock_rounded, color: Colors.white, size: 20 * vScale), + SizedBox(width: 8 * vScale), + Text( + "LIV. $requiredLevel", + style: getSharedTextStyle(type, TextStyle(color: Colors.white, fontWeight: FontWeight.w900, fontSize: 16 * vScale, letterSpacing: 2)) + ), + ], + ), + ), + ], + ), + ), + ); + } +} +// =========================================================================== +// FILE: lib/widgets/custom_button.dart +// =========================================================================== + + +// =========================================================================== +// FILE: lib/widgets/custom_settings_button.dart +// =========================================================================== + +// =========================================================================== +// FILE: lib/widgets/custom_settings_button.dart +// =========================================================================== + +import 'package:flutter/material.dart'; +import '../core/app_colors.dart'; +import 'painters.dart'; // Importiamo i painter per i doodle e il font + +class NeonShapeButton extends StatelessWidget { + final IconData icon; final String label; final bool isSelected; + final ThemeColors theme; final AppThemeType themeType; final VoidCallback onTap; + final bool isLocked; final bool isSpecial; + + const NeonShapeButton({super.key, required this.icon, required this.label, required this.isSelected, required this.theme, required this.themeType, required this.onTap, this.isLocked = false, this.isSpecial = false}); + + Color _getDoodleColor() { + switch (label) { + case 'Rombo': return Colors.lightBlue.shade200; + case 'Croce': return Colors.green.shade200; + case 'Buco': return Colors.pink.shade200; + case 'Clessidra': return Colors.purple.shade200; + case 'Caos': return Colors.grey.shade300; + default: return Colors.lightBlue.shade200; + } + } + + @override + Widget build(BuildContext context) { + if (themeType == AppThemeType.doodle) { + Color doodleColor = isLocked ? Colors.grey : _getDoodleColor(); + Color inkColor = const Color(0xFF111122); + double tilt = (label.length % 2 == 0) ? -0.05 : 0.04; + + return Transform.rotate( + angle: tilt, + child: GestureDetector( + onTap: isLocked ? null : onTap, + child: CustomPaint( + painter: DoodleBackgroundPainter(fillColor: isSelected ? doodleColor : Colors.white.withOpacity(0.8), strokeColor: inkColor, seed: label.length * 3), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(isLocked ? Icons.lock : icon, color: inkColor, size: 24), + const SizedBox(height: 2), + Text(isLocked ? "Liv. 10" : label, style: getSharedTextStyle(themeType, TextStyle(color: inkColor, fontSize: 11, fontWeight: FontWeight.w900, letterSpacing: 0.5))), + ], + ), + ), + ), + ), + ); + } + + Color mainColor = isSpecial && !isLocked ? Colors.purpleAccent : theme.playerBlue; + return GestureDetector( + onTap: isLocked ? null : onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 250), curve: Curves.easeOutCubic, padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), transform: Matrix4.translationValues(0, isSelected ? 2 : 0, 0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), border: Border.all(color: isLocked ? Colors.transparent : (isSelected ? mainColor : Colors.white.withOpacity(0.1)), width: isSelected ? 2 : 1), + gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: isLocked ? [Colors.grey.withOpacity(0.1), Colors.black.withOpacity(0.2)] : isSelected ? [mainColor.withOpacity(0.3), mainColor.withOpacity(0.1)] : [theme.text.withOpacity(0.1), theme.text.withOpacity(0.02)]), + boxShadow: isLocked ? [] : isSelected ? [BoxShadow(color: mainColor.withOpacity(0.5), blurRadius: 15, spreadRadius: 1, offset: const Offset(0, 0))] : [BoxShadow(color: Colors.black.withOpacity(0.4), blurRadius: 6, offset: const Offset(2, 4)), BoxShadow(color: Colors.white.withOpacity(0.05), blurRadius: 2, offset: const Offset(-1, -1))], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(isLocked ? Icons.lock : icon, color: isLocked ? Colors.grey.withOpacity(0.5) : (isSelected ? Colors.white : theme.text.withOpacity(0.6)), size: 24), + const SizedBox(height: 6), + Text(isLocked ? "Liv. 10" : label, style: getSharedTextStyle(themeType, TextStyle(color: isLocked ? Colors.grey.withOpacity(0.5) : (isSelected ? Colors.white : theme.text.withOpacity(0.6)), fontSize: 11, fontWeight: isSelected ? FontWeight.w900 : FontWeight.bold))), + ], + ), + ), + ); + } +} + +class NeonSizeButton extends StatelessWidget { + final String label; final bool isSelected; final ThemeColors theme; final AppThemeType themeType; final VoidCallback onTap; + const NeonSizeButton({super.key, required this.label, required this.isSelected, required this.theme, required this.themeType, required this.onTap}); + + @override + Widget build(BuildContext context) { + if (themeType == AppThemeType.doodle) { + Color doodleColor = label == 'MAX' ? Colors.red.shade200 : Colors.cyan.shade100; Color inkColor = const Color(0xFF111122); double tilt = (label == 'M' || label == 'MAX') ? 0.05 : -0.04; + return Transform.rotate( + angle: tilt, + child: GestureDetector( + onTap: onTap, + child: CustomPaint(painter: DoodleBackgroundPainter(fillColor: isSelected ? doodleColor : Colors.white.withOpacity(0.8), strokeColor: inkColor, seed: label.codeUnitAt(0), isCircle: true), child: SizedBox(width: 50, height: 50, child: Center(child: Text(label, style: getSharedTextStyle(themeType, TextStyle(color: inkColor, fontSize: 18, fontWeight: FontWeight.w900)))))), + ), + ); + } + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 250), curve: Curves.easeOutCubic, width: 50, height: 50, transform: Matrix4.translationValues(0, isSelected ? 2 : 0, 0), + decoration: BoxDecoration( + shape: BoxShape.circle, border: Border.all(color: isSelected ? theme.playerRed : Colors.white.withOpacity(0.1), width: isSelected ? 2 : 1), + gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: isSelected ? [theme.playerRed.withOpacity(0.3), theme.playerRed.withOpacity(0.1)] : [theme.text.withOpacity(0.1), theme.text.withOpacity(0.02)]), + boxShadow: isSelected ? [BoxShadow(color: theme.playerRed.withOpacity(0.5), blurRadius: 15, spreadRadius: 1)] : [BoxShadow(color: Colors.black.withOpacity(0.4), blurRadius: 6, offset: const Offset(2, 4)), BoxShadow(color: Colors.white.withOpacity(0.05), blurRadius: 2, offset: const Offset(-1, -1))], + ), + child: Center(child: Text(label, style: getSharedTextStyle(themeType, TextStyle(color: isSelected ? Colors.white : theme.text.withOpacity(0.6), fontSize: 14, fontWeight: isSelected ? FontWeight.w900 : FontWeight.bold)))), + ), + ); + } +} + +class NeonTimeSwitch extends StatelessWidget { + final bool isTimeMode; final ThemeColors theme; final AppThemeType themeType; final VoidCallback onTap; + const NeonTimeSwitch({super.key, required this.isTimeMode, required this.theme, required this.themeType, required this.onTap}); + + @override + Widget build(BuildContext context) { + if (themeType == AppThemeType.doodle) { + Color doodleColor = Colors.orange.shade200; Color inkColor = const Color(0xFF111122); + return Transform.rotate( + angle: -0.015, + child: GestureDetector( + onTap: onTap, + child: CustomPaint( + painter: DoodleBackgroundPainter(fillColor: isTimeMode ? doodleColor : Colors.white.withOpacity(0.8), strokeColor: inkColor, seed: 42), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + child: Row( + mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(isTimeMode ? Icons.timer : Icons.timer_off, color: inkColor, size: 28), const SizedBox(width: 12), + Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [Text(isTimeMode ? 'A TEMPO' : 'RELAX', style: getSharedTextStyle(themeType, TextStyle(color: inkColor, fontWeight: FontWeight.w900, fontSize: 16, letterSpacing: 2.0))), Text(isTimeMode ? '15 sec a mossa' : 'Nessun limite', style: getSharedTextStyle(themeType, TextStyle(color: inkColor.withOpacity(0.8), fontSize: 13, fontWeight: FontWeight.bold)))]), + ], + ), + ), + ), + ), + ); + } + 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), border: Border.all(color: isTimeMode ? Colors.amber : Colors.white.withOpacity(0.1), width: isTimeMode ? 2 : 1), + gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: isTimeMode ? [Colors.amber.withOpacity(0.25), Colors.amber.withOpacity(0.05)] : [theme.text.withOpacity(0.1), theme.text.withOpacity(0.02)]), + boxShadow: isTimeMode ? [BoxShadow(color: Colors.amber.withOpacity(0.3), blurRadius: 15, spreadRadius: 2)] : [BoxShadow(color: Colors.black.withOpacity(0.4), blurRadius: 6, offset: const Offset(2, 4)), BoxShadow(color: Colors.white.withOpacity(0.05), blurRadius: 2, offset: const Offset(-1, -1))], + ), + 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: getSharedTextStyle(themeType, TextStyle(color: isTimeMode ? Colors.white : theme.text.withOpacity(0.5), fontWeight: FontWeight.w900, fontSize: 14, letterSpacing: 1.5))), Text(isTimeMode ? '15 sec a mossa' : 'Nessun limite', style: getSharedTextStyle(themeType, TextStyle(color: isTimeMode ? Colors.amber.shade200 : theme.text.withOpacity(0.4), fontSize: 11, fontWeight: FontWeight.bold)))]), + ], + ), + ), + ); + } +} + +class NeonPrivacySwitch extends StatelessWidget { + final bool isPublic; + final ThemeColors theme; + final AppThemeType themeType; + final VoidCallback onTap; + + const NeonPrivacySwitch({super.key, required this.isPublic, required this.theme, required this.themeType, required this.onTap}); + + @override + Widget build(BuildContext context) { + if (themeType == AppThemeType.doodle) { + Color doodleColor = isPublic ? Colors.green.shade600 : Colors.red.shade600; + return Transform.rotate( + angle: 0.015, + child: GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + transform: Matrix4.translationValues(0, isPublic ? 3 : 0, 0), + decoration: BoxDecoration( + color: isPublic ? doodleColor : Colors.white, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(15), topRight: Radius.circular(8), + bottomLeft: Radius.circular(6), bottomRight: Radius.circular(15), + ), + border: Border.all(color: isPublic ? theme.text : doodleColor.withOpacity(0.5), width: 2.5), + boxShadow: [BoxShadow(color: isPublic ? theme.text.withOpacity(0.8) : doodleColor.withOpacity(0.2), offset: const Offset(4, 5), blurRadius: 0)], + ), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(isPublic ? Icons.public : Icons.lock, color: isPublic ? Colors.white : doodleColor, size: 20), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(isPublic ? 'PUBBLICA' : 'PRIVATA', style: getSharedTextStyle(themeType, TextStyle(color: isPublic ? Colors.white : doodleColor, fontWeight: FontWeight.w900, fontSize: 12, letterSpacing: 1.0))), + Text(isPublic ? 'In Bacheca' : 'Solo Codice', style: getSharedTextStyle(themeType, TextStyle(color: isPublic ? Colors.white : doodleColor.withOpacity(0.8), fontSize: 9, fontWeight: FontWeight.bold))), + ], + ), + ], + ), + ), + ), + ); + } + + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: isPublic + ? [Colors.greenAccent.withOpacity(0.25), Colors.greenAccent.withOpacity(0.05)] + : [theme.playerRed.withOpacity(0.25), theme.playerRed.withOpacity(0.05)], + ), + border: Border.all(color: isPublic ? Colors.greenAccent : theme.playerRed, width: isPublic ? 2 : 1), + boxShadow: isPublic + ? [BoxShadow(color: Colors.greenAccent.withOpacity(0.3), blurRadius: 15, spreadRadius: 2)] + : [BoxShadow(color: Colors.black.withOpacity(0.4), blurRadius: 6, offset: const Offset(2, 4))], + ), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(isPublic ? Icons.public : Icons.lock, color: isPublic ? Colors.greenAccent : theme.playerRed, size: 20), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(isPublic ? 'PUBBLICA' : 'PRIVATA', style: getSharedTextStyle(themeType, TextStyle(color: isPublic ? Colors.white : theme.text.withOpacity(0.8), fontWeight: FontWeight.w900, fontSize: 11, letterSpacing: 1.5))), + Text(isPublic ? 'Tutti ti vedono' : 'Solo con Codice', style: getSharedTextStyle(themeType, TextStyle(color: isPublic ? Colors.greenAccent.shade200 : theme.playerRed.withOpacity(0.7), fontSize: 9, fontWeight: FontWeight.bold))), + ], + ), + ], + ), + ), + ); + } +} + +class NeonActionButton extends StatelessWidget { + final String label; + final Color color; + final VoidCallback onTap; + final ThemeColors theme; + final AppThemeType themeType; + + const NeonActionButton({super.key, required this.label, required this.color, required this.onTap, required this.theme, required this.themeType}); + + @override + Widget build(BuildContext context) { + if (themeType == AppThemeType.doodle) { + double tilt = (label == "UNISCITI" || label == "ANNULLA") ? -0.015 : 0.02; + return Transform.rotate( + angle: tilt, + child: GestureDetector( + onTap: onTap, + child: Container( + height: 50, + decoration: BoxDecoration( + color: color, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(10), topRight: Radius.circular(20), + bottomLeft: Radius.circular(25), bottomRight: Radius.circular(10), + ), + border: Border.all(color: theme.text, width: 3.0), + boxShadow: [BoxShadow(color: theme.text.withOpacity(0.9), offset: const Offset(4, 4), blurRadius: 0)], + ), + child: Center( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Text(label, style: getSharedTextStyle(themeType, TextStyle(fontSize: 20, fontWeight: FontWeight.w900, letterSpacing: 3.0, color: Colors.white))), + ), + ), + ), + ), + ), + ); + } + + return GestureDetector( + onTap: onTap, + child: Container( + height: 50, + decoration: BoxDecoration( + gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [color.withOpacity(0.9), color.withOpacity(0.6)]), + borderRadius: BorderRadius.circular(15), + border: Border.all(color: Colors.white.withOpacity(0.3), width: 1.5), + boxShadow: [ + BoxShadow(color: Colors.black.withOpacity(0.5), offset: const Offset(4, 8), blurRadius: 12), + BoxShadow(color: color.withOpacity(0.3), offset: const Offset(0, 0), blurRadius: 15, spreadRadius: 1), + ], + ), + child: Center( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Text(label, style: getSharedTextStyle(themeType, const TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2.0, color: Colors.white, shadows: [Shadow(color: Colors.black, blurRadius: 2, offset: Offset(1, 1))]))), + ), + ), + ), + ), + ); + } +} +// =========================================================================== +// FILE: lib/widgets/cyber_border.dart +// =========================================================================== + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../core/theme_manager.dart'; // Import aggiornato +import 'dart:math' as math; + +class AnimatedCyberBorder extends StatefulWidget { + final Widget child; + const AnimatedCyberBorder({super.key, required this.child}); + @override + State createState() => _AnimatedCyberBorderState(); +} + +class _AnimatedCyberBorderState extends State 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().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(15), 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(15)); + final Paint paint = Paint() + ..shader = SweepGradient(colors: [color1, color2, color1, color2, color1], stops: const [0.0, 0.25, 0.5, 0.75, 1.0], transform: GradientRotation(animationValue * 2 * math.pi)).createShader(rect) + ..style = PaintingStyle.stroke + ..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/widgets/game_over_dialog.dart +// =========================================================================== + +// =========================================================================== +// 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'; +import '../services/storage_service.dart'; +import 'painters.dart'; + +class GameOverDialog extends StatelessWidget { + const GameOverDialog({super.key}); + + @override + Widget build(BuildContext context) { + final game = context.read(); + final themeManager = context.read(); + final theme = themeManager.currentColors; + final themeType = themeManager.currentThemeType; + Color inkColor = const Color(0xFF111122); + + int red = game.board.scoreRed; + int blue = game.board.scoreBlue; + + bool playerBeatCPU = game.isVsCPU && red > blue; + + String myName = StorageService.instance.playerName.toUpperCase(); + if (myName.isEmpty) myName = "TU"; + + // --- LOGICA NOMI --- + String nameRed = myName; + String nameBlue = themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? "VERDE" : "BLU"; + + if (game.isOnline) { + nameRed = game.onlineHostName.toUpperCase(); + nameBlue = game.onlineGuestName.toUpperCase(); + } else if (game.isVsCPU) { + nameRed = myName; + 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 = themeType == AppThemeType.doodle ? inkColor : theme.text; + } + + Widget dialogContent = Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(winnerText, textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: winnerColor))), + const SizedBox(height: 20), + Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + decoration: BoxDecoration( + color: themeType == AppThemeType.doodle ? Colors.transparent : theme.text.withOpacity(0.05), + borderRadius: BorderRadius.circular(15), + border: themeType == AppThemeType.doodle ? Border.all(color: inkColor.withOpacity(0.3), width: 1.5) : null, + ), + child: FittedBox( + fit: BoxFit.scaleDown, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text("$nameRed: $red", style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerRed))), + Text(" - ", style: getSharedTextStyle(themeType, TextStyle(fontSize: 18, color: themeType == AppThemeType.doodle ? inkColor : theme.text))), + Text("$nameBlue: $blue", style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerBlue))), + ], + ), + ), + ), + + if (game.lastMatchXP > 0) ...[ + const SizedBox(height: 15), + Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), + decoration: BoxDecoration( + color: Colors.green.withOpacity(0.15), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: themeType == AppThemeType.doodle ? Colors.green.shade700 : Colors.greenAccent, width: 1.5), + boxShadow: (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) ? [const BoxShadow(color: Colors.greenAccent, blurRadius: 10, spreadRadius: -5)] : [], + ), + child: Text("+ ${game.lastMatchXP} XP", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.green.shade700 : Colors.greenAccent, fontWeight: FontWeight.w900, fontSize: 16, letterSpacing: 1.5))), + ), + ], + + if (game.isVsCPU) ...[ + const SizedBox(height: 15), + Text("Difficoltà CPU: Livello ${game.cpuLevel}", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.7) : theme.text.withOpacity(0.7)))), + ], + + if (game.isOnline) ...[ + const SizedBox(height: 20), + if (game.rematchRequested && !game.opponentWantsRematch) + Text("In attesa di $nameBlue...", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.orange.shade700 : Colors.amber, fontWeight: FontWeight.bold, fontStyle: FontStyle.italic))), + if (game.opponentWantsRematch && !game.rematchRequested) + Text("$nameBlue vuole la rivincita!", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.green.shade700 : Colors.greenAccent, fontWeight: FontWeight.bold))), + if (game.rematchRequested && game.opponentWantsRematch) + Text("Avvio nuova partita...", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.green.shade800 : Colors.green, fontWeight: FontWeight.bold))), + ], + + // --- SEZIONE LEVEL UP E ROADMAP DINAMICA --- + if (game.hasLeveledUp && game.unlockedRewards.isNotEmpty) ...[ + const SizedBox(height: 30), + Divider(color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.3) : theme.text.withOpacity(0.2)), + const SizedBox(height: 15), + Container( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 20), + decoration: BoxDecoration( + color: themeType == AppThemeType.doodle ? Colors.amber.withOpacity(0.1) : Colors.amber.withOpacity(0.2), + borderRadius: BorderRadius.circular(30), + border: Border.all(color: themeType == AppThemeType.doodle ? Colors.amber.shade700 : Colors.amber, width: 2) + ), + child: Text("🎉 LIVELLO ${game.newlyReachedLevel}! 🎉", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.amber.shade700 : Colors.amber, fontWeight: FontWeight.w900, fontSize: 18))), + ), + const SizedBox(height: 15), + + ...game.unlockedRewards.map((reward) { + Color rewardColor = themeType == AppThemeType.doodle ? (reward['color'] as Color).withOpacity(0.8) : reward['color']; + return Container( + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: rewardColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: rewardColor.withOpacity(0.5), width: 1.5), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: rewardColor.withOpacity(0.2), + shape: BoxShape.circle, + ), + child: Icon(reward['icon'], color: rewardColor, size: 28), + ), + const SizedBox(width: 15), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(reward['title'], style: getSharedTextStyle(themeType, TextStyle(color: rewardColor, fontWeight: FontWeight.w900, fontSize: 16))), + const SizedBox(height: 4), + Text(reward['desc'], style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.9) : theme.text.withOpacity(0.9), fontSize: 12, height: 1.3))), + ], + ) + ) + ] + ), + ); + }), + ], + + const SizedBox(height: 30), + + // --- BOTTONI AZIONE --- + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (playerBeatCPU) + _buildPrimaryButton( + "PROSSIMO LIVELLO ➔", + winnerColor, + themeType, + inkColor, + () { + Navigator.pop(context); + game.increaseLevelAndRestart(); + }, + ) + else if (game.isOnline) + _buildPrimaryButton( + game.opponentWantsRematch ? "ACCETTA RIVINCITA" : "CHIEDI RIVINCITA", + game.rematchRequested ? Colors.grey : (winnerColor == (themeType == AppThemeType.doodle ? inkColor : theme.text) ? theme.playerBlue : winnerColor), + themeType, + inkColor, + game.rematchRequested ? () {} : () => game.requestRematch(), + ) + else + _buildPrimaryButton( + "RIGIOCA", + winnerColor == (themeType == AppThemeType.doodle ? inkColor : theme.text) ? theme.playerBlue : winnerColor, + themeType, + inkColor, + () { + Navigator.pop(context); + game.startNewGame(game.board.radius, vsCPU: game.isVsCPU); + }, + ), + + const SizedBox(height: 12), + + _buildSecondaryButton( + "TORNA AL MENU", + themeType, + inkColor, + theme, + () { + if (game.isOnline) { + game.disconnectOnlineGame(); + } + Navigator.pop(context); + Navigator.pop(context); + }, + ), + ], + ) + ], + ); + + if (themeType == AppThemeType.doodle) { + dialogContent = Transform.rotate( + angle: 0.015, + child: CustomPaint( + painter: DoodleBackgroundPainter(fillColor: Colors.white.withOpacity(0.95), strokeColor: inkColor, seed: 500), + child: Padding( + padding: const EdgeInsets.all(25.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("FINE PARTITA", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 22, fontWeight: FontWeight.w900, color: inkColor, letterSpacing: 2))), + const SizedBox(height: 20), + dialogContent, + ], + ), + ), + ), + ); + } else { + dialogContent = Container( + padding: const EdgeInsets.all(25.0), + decoration: BoxDecoration( + color: theme.background, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: winnerColor.withOpacity(0.5), width: 2), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("FINE PARTITA", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: theme.text))), + const SizedBox(height: 20), + dialogContent, + ], + ), + ); + } + + return Dialog( + backgroundColor: Colors.transparent, + insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20), + child: dialogContent, + ); + } + + Widget _buildPrimaryButton(String label, Color color, AppThemeType themeType, Color inkColor, VoidCallback onTap) { + if (themeType == AppThemeType.doodle) { + return GestureDetector( + onTap: onTap, + child: CustomPaint( + painter: DoodleBackgroundPainter(fillColor: color, strokeColor: inkColor, seed: label.length * 7), + child: Container( + height: 55, + alignment: Alignment.center, + child: Text(label, style: getSharedTextStyle(themeType, const TextStyle(fontSize: 16, fontWeight: FontWeight.w900, color: Colors.white, letterSpacing: 1.5))), + ), + ), + ); + } + + return ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: color, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 15), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), + elevation: 5, + ), + onPressed: onTap, + child: Text(label, style: getSharedTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 1.5))), + ); + } + + Widget _buildSecondaryButton(String label, AppThemeType themeType, Color inkColor, ThemeColors theme, VoidCallback onTap) { + if (themeType == AppThemeType.doodle) { + return GestureDetector( + onTap: onTap, + child: CustomPaint( + painter: DoodleBackgroundPainter(fillColor: Colors.transparent, strokeColor: inkColor.withOpacity(0.5), seed: label.length * 3), + child: Container( + height: 55, + alignment: Alignment.center, + child: Text(label, style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor, letterSpacing: 1.5))), + ), + ), + ); + } + + return 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: onTap, + child: Text(label, style: getSharedTextStyle(themeType, TextStyle(fontWeight: FontWeight.bold, color: theme.text, fontSize: 14, letterSpacing: 1.5))), + ); + } +} +// =========================================================================== +// FILE: lib/widgets/home_buttons.dart +// =========================================================================== + +import 'package:flutter/material.dart'; +import '../core/app_colors.dart'; // Import aggiornato +import 'painters.dart'; + +class FeatureCard extends StatelessWidget { + final String title; + final String subtitle; + final IconData icon; + final Color color; + final ThemeColors theme; + final AppThemeType themeType; + final VoidCallback onTap; + final bool isFeatured; + final bool compact; + + const FeatureCard({super.key, required this.title, required this.subtitle, required this.icon, required this.color, required this.theme, required this.themeType, required this.onTap, this.isFeatured = false, this.compact = false}); + + @override + Widget build(BuildContext context) { + if (themeType == AppThemeType.doodle) { + double tilt = (title.length % 2 == 0) ? -0.015 : 0.02; + Color inkColor = const Color(0xFF111122); + + return Transform.rotate( + angle: tilt, + child: GestureDetector( + onTap: onTap, + child: CustomPaint( + painter: DoodleBackgroundPainter(fillColor: color, strokeColor: inkColor, seed: title.length * 5), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: compact ? 12.0 : 22.0, vertical: compact ? 12.0 : 16.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(icon, color: inkColor, size: compact ? 24 : 32), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(title, style: getSharedTextStyle(themeType, TextStyle(color: inkColor, fontSize: compact ? 16 : 24, fontWeight: FontWeight.w900)))), + if (!compact) ...[ const SizedBox(height: 2), Text(subtitle, style: getSharedTextStyle(themeType, TextStyle(color: inkColor.withOpacity(0.8), fontSize: 14, fontWeight: FontWeight.bold))) ] + ], + ), + ), + if (!compact) Icon(Icons.chevron_right_rounded, color: inkColor.withOpacity(0.6), size: 32), + ], + ), + ), + ), + ), + ); + } + + return GestureDetector( + onTap: onTap, + child: Container( + padding: EdgeInsets.symmetric(horizontal: compact ? 12.0 : 20.0, vertical: compact ? 10.0 : 14.0), + decoration: BoxDecoration( + gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: isFeatured ? [color.withOpacity(0.9), color.withOpacity(0.6)] : [color.withOpacity(0.25), color.withOpacity(0.05)]), + borderRadius: BorderRadius.circular(15), border: Border.all(color: color.withOpacity(isFeatured ? 0.5 : 0.2), width: 1.5), + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.6), offset: const Offset(0, 8), blurRadius: 15), BoxShadow(color: color.withOpacity(isFeatured ? 0.3 : 0.05), offset: const Offset(-1, -1), blurRadius: 5)] + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + padding: EdgeInsets.all(compact ? 6 : 10), + decoration: BoxDecoration(gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Colors.white.withOpacity(0.3), Colors.white.withOpacity(0.05)]), shape: BoxShape.circle, border: Border.all(color: Colors.white.withOpacity(0.2)), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.2), blurRadius: 5, offset: const Offset(2, 4))]), + child: Icon(icon, color: isFeatured ? Colors.white : color, size: compact ? 20 : 26), + ), + SizedBox(width: compact ? 10 : 20), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(title, style: getSharedTextStyle(themeType, TextStyle(color: isFeatured ? Colors.white : theme.text, fontSize: compact ? 14 : 18, fontWeight: FontWeight.w900, shadows: [Shadow(color: Colors.black.withOpacity(0.5), offset: const Offset(1, 2), blurRadius: 2)])))), + if (!compact) ...[ const SizedBox(height: 2), Text(subtitle, style: getSharedTextStyle(themeType, TextStyle(color: isFeatured ? Colors.white.withOpacity(0.8) : theme.text.withOpacity(0.6), fontSize: 12, fontWeight: FontWeight.bold))) ] + ], + ), + ), + if (!compact) Icon(Icons.chevron_right_rounded, color: isFeatured ? Colors.white.withOpacity(0.7) : color.withOpacity(0.5), size: 30), + ], + ), + ), + ); + } +} +// =========================================================================== +// FILE: lib/widgets/music_theme_widgets.dart +// =========================================================================== + +// =========================================================================== +// FILE: lib/widgets/music_theme_widgets.dart +// =========================================================================== + +import 'package:flutter/material.dart'; +import '../core/app_colors.dart'; +import 'painters.dart'; + +class MusicCassetteCard extends StatelessWidget { + final String title; + final String subtitle; + final Color neonColor; + final double angle; + final IconData leftIcon; + final IconData rightIcon; + final VoidCallback onTap; + final AppThemeType themeType; + + const MusicCassetteCard({ + super.key, + required this.title, + required this.subtitle, + required this.neonColor, + required this.angle, + required this.leftIcon, + required this.rightIcon, + required this.onTap, + required this.themeType + }); + + @override + Widget build(BuildContext context) { + // Calcoliamo la scala in base all'altezza dello schermo per strizzare la cassetta + final double screenHeight = MediaQuery.of(context).size.height; + final double vScale = (screenHeight / 850.0).clamp(0.65, 1.0); + + return Transform.rotate( + angle: angle, + child: GestureDetector( + onTap: onTap, + child: Container( + height: 125 * vScale, // Altezza dinamica! + margin: EdgeInsets.symmetric(vertical: 8 * vScale, horizontal: 10), + padding: EdgeInsets.all(12 * vScale), + decoration: BoxDecoration( + color: const Color(0xFF22222A), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.black87, width: 2), + boxShadow: [ + BoxShadow(color: neonColor.withOpacity(0.5), blurRadius: 25, spreadRadius: 2), + const BoxShadow(color: Colors.black54, offset: Offset(5, 10), blurRadius: 15) + ] + ), + child: Column( + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + color: neonColor.withOpacity(0.15), + borderRadius: BorderRadius.circular(4), + border: Border.all(color: neonColor.withOpacity(0.5), width: 1.5) + ), + child: Row( + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 12 * vScale), + child: Icon(leftIcon, color: neonColor, size: 28 * vScale) + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text(title, style: getSharedTextStyle(themeType, TextStyle(color: Colors.white, fontSize: 20 * vScale, fontWeight: FontWeight.w900, shadows: [Shadow(color: neonColor, blurRadius: 10)]))) + ) + ), + Flexible( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text(subtitle, style: getSharedTextStyle(themeType, TextStyle(color: Colors.white70, fontSize: 11 * vScale, fontWeight: FontWeight.bold))) + ) + ), + ], + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 12 * vScale), + child: Icon(rightIcon, color: neonColor, size: 28 * vScale) + ), + ], + ), + ), + ), + SizedBox(height: 10 * vScale), + Container( + height: 35 * vScale, + width: 180 * vScale, + decoration: BoxDecoration( + color: const Color(0xFF0D0D12), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.white24, width: 1) + ), + child: Stack( + alignment: Alignment.center, + children: [ + Container(height: 2, width: 120 * vScale, color: const Color(0xFF333333)), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ _buildSpool(vScale), _buildSpool(vScale) ] + ), + ], + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildSpool(double vScale) { + return Container( + width: 26 * vScale, + height: 26 * vScale, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white70, + border: Border.all(color: Colors.black87, width: 5 * vScale) + ), + child: Center( + child: Container( + width: 6 * vScale, + height: 6 * vScale, + decoration: const BoxDecoration(shape: BoxShape.circle, color: Colors.black) + ) + ), + ); + } +} + +class MusicKnobCard extends StatelessWidget { + final String title; + final IconData icon; + final VoidCallback onTap; + final AppThemeType themeType; + final Color? iconColor; + + const MusicKnobCard({ + super.key, + required this.title, + required this.icon, + required this.onTap, + required this.themeType, + this.iconColor + }); + + @override + Widget build(BuildContext context) { + // Adattiamo anche le manopole in base all'altezza dello schermo + final double screenHeight = MediaQuery.of(context).size.height; + final double vScale = (screenHeight / 850.0).clamp(0.65, 1.0); + + return GestureDetector( + onTap: onTap, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 65 * vScale, + height: 65 * vScale, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: const Color(0xFF222222), + border: Border.all(color: const Color(0xFF111111), width: 2), + boxShadow: const [ + BoxShadow(color: Colors.black87, blurRadius: 10, offset: Offset(2, 6)), + BoxShadow(color: Colors.white12, blurRadius: 2, offset: Offset(-1, -1)) + ], + ), + child: Padding( + padding: EdgeInsets.all(6.0 * vScale), + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.black54, width: 1), + gradient: const SweepGradient(colors: [Color(0xFF555555), Color(0xFFAAAAAA), Color(0xFF555555), Color(0xFF222222), Color(0xFF555555)]), + ), + child: Padding( + padding: EdgeInsets.all(4.0 * vScale), + child: Container( + decoration: const BoxDecoration(shape: BoxShape.circle, color: Color(0xFF1A1A1A)), + child: Center(child: Icon(icon, color: iconColor ?? Colors.white70, size: 20 * vScale)), + ), + ), + ), + ), + ), + SizedBox(height: 10 * vScale), + FittedBox( + fit: BoxFit.scaleDown, + child: Text(title, style: getSharedTextStyle(themeType, TextStyle(color: Colors.white70, fontSize: 11 * vScale, fontWeight: FontWeight.bold, letterSpacing: 1.0))) + ), + ], + ), + ); + } +} +// =========================================================================== +// FILE: lib/widgets/painters.dart +// =========================================================================== + +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'dart:math' as math; +import '../core/app_colors.dart'; // Import aggiornato + +TextStyle getSharedTextStyle(AppThemeType themeType, TextStyle baseStyle) { + if (themeType == AppThemeType.doodle) { + return GoogleFonts.permanentMarker(textStyle: baseStyle); + } else if (themeType == AppThemeType.arcade) { + return GoogleFonts.pressStart2p(textStyle: baseStyle.copyWith(fontSize: baseStyle.fontSize != null ? baseStyle.fontSize! * 0.75 : null, letterSpacing: 0.5)); + } else if (themeType == AppThemeType.grimorio) { + return GoogleFonts.cinzelDecorative(textStyle: baseStyle.copyWith(fontWeight: FontWeight.bold)); + } else if (themeType == AppThemeType.music) { + return GoogleFonts.audiowide(textStyle: baseStyle.copyWith(letterSpacing: 1.5)); + } + return baseStyle; +} + +class DoodleBackgroundPainter extends CustomPainter { + final Color fillColor; final Color strokeColor; final int seed; final bool isCircle; + DoodleBackgroundPainter({required this.fillColor, required this.strokeColor, required this.seed, this.isCircle = false}); + + @override + void paint(Canvas canvas, Size size) { + final math.Random random = math.Random(seed); + double wobble() => random.nextDouble() * 6 - 3; + final Paint fillPaint = Paint()..color = fillColor..style = PaintingStyle.fill; + final Paint strokePaint = Paint()..color = strokeColor..strokeWidth = 2.5..style = PaintingStyle.stroke..strokeCap = StrokeCap.round..strokeJoin = StrokeJoin.round; + + if (isCircle) { + final Rect rect = Rect.fromLTWH(wobble(), wobble(), size.width + wobble(), size.height + wobble()); + canvas.save(); canvas.translate(wobble(), wobble()); canvas.drawOval(rect, fillPaint); canvas.restore(); + canvas.drawOval(rect, strokePaint); + canvas.save(); canvas.translate(random.nextDouble() * 4 - 2, random.nextDouble() * 4 - 2); canvas.drawOval(rect, strokePaint..strokeWidth = 1.0..color = strokeColor.withOpacity(0.6)); canvas.restore(); + } else { + final Path path = Path()..moveTo(wobble(), wobble())..lineTo(size.width + wobble(), wobble())..lineTo(size.width + wobble(), size.height + wobble())..lineTo(wobble(), size.height + wobble())..close(); + final Path fillPath = Path()..moveTo(wobble() * 1.5, wobble() * 1.5)..lineTo(size.width + wobble() * 1.5, wobble() * 1.5)..lineTo(size.width + wobble() * 1.5, size.height + wobble() * 1.5)..lineTo(wobble() * 1.5, size.height + wobble() * 1.5)..close(); + canvas.drawPath(fillPath, fillPaint); + canvas.drawPath(path, strokePaint); + canvas.save(); canvas.translate(random.nextDouble() * 3 - 1.5, random.nextDouble() * 3 - 1.5); canvas.drawPath(path, strokePaint..strokeWidth = 1.0..color = strokeColor.withOpacity(0.6)); canvas.restore(); + } + } + @override bool shouldRepaint(covariant DoodleBackgroundPainter oldDelegate) => oldDelegate.fillColor != fillColor || oldDelegate.strokeColor != strokeColor; +} + +class AudioCablesPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = const Color(0xFF151515)..style = PaintingStyle.stroke..strokeWidth = 8.0..strokeCap = StrokeCap.round; + final highlight = Paint()..color = const Color(0xFF3A3A3A)..style = PaintingStyle.stroke..strokeWidth = 2.0..strokeCap = StrokeCap.round; + void drawCable(Path path) { canvas.drawPath(path, paint); canvas.drawPath(path, highlight); } + + Path c1 = Path()..moveTo(-20, size.height * 0.2)..quadraticBezierTo(100, size.height * 0.25, 50, size.height * 0.4)..quadraticBezierTo(0, size.height * 0.5, -20, size.height * 0.55); drawCable(c1); + Path c2 = Path()..moveTo(size.width + 20, size.height * 0.4)..quadraticBezierTo(size.width - 100, size.height * 0.5, size.width - 50, size.height * 0.7)..quadraticBezierTo(size.width, size.height * 0.8, size.width + 20, size.height * 0.85); drawCable(c2); + Path c3 = Path()..moveTo(size.width * 0.2, size.height + 20)..quadraticBezierTo(size.width * 0.3, size.height - 80, size.width * 0.5, size.height - 60)..quadraticBezierTo(size.width * 0.7, size.height - 40, size.width * 0.8, size.height + 20); drawCable(c3); + + _drawJack(canvas, Offset(80, size.height * 0.38), -0.5); + _drawJack(canvas, Offset(size.width - 60, size.height * 0.68), 0.8); + } + + void _drawJack(Canvas canvas, Offset pos, double angle) { + canvas.save(); canvas.translate(pos.dx, pos.dy); canvas.rotate(angle); + canvas.drawRect(const Rect.fromLTWH(-15, -4, 15, 8), Paint()..color = const Color(0xFF151515)); + canvas.drawRRect(RRect.fromRectAndRadius(const Rect.fromLTWH(0, -6, 25, 12), const Radius.circular(2)), Paint()..color = const Color(0xFF222222)); + canvas.drawRRect(RRect.fromRectAndRadius(const Rect.fromLTWH(2, -4, 21, 8), const Radius.circular(2)), Paint()..color = const Color(0xFF444444)); + canvas.drawRect(const Rect.fromLTWH(25, -2, 15, 4), Paint()..color = const Color(0xFFCCCCCC)); + canvas.drawRect(const Rect.fromLTWH(40, -1.5, 5, 3), Paint()..color = const Color(0xFFAAAAAA)); + canvas.drawLine(const Offset(30, -2), const Offset(30, 2), Paint()..color = Colors.black..strokeWidth = 1.5); + canvas.drawLine(const Offset(35, -2), const Offset(35, 2), Paint()..color = Colors.black..strokeWidth = 1.5); + canvas.restore(); + } + @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} \ No newline at end of file diff --git a/report/codice_recap.txt b/report/codice_recap.txt new file mode 100644 index 0000000..0b4cfd8 --- /dev/null +++ b/report/codice_recap.txt @@ -0,0 +1,54 @@ +PROJECT_NAME=$(basename "$PWD") +TIMESTAMP=$(date +"%d-%m-%y_%H.%M") +OUTPUT_FILE="./report/${PROJECT_NAME}_${TIMESTAMP}.txt" + +mkdir -p ./report && { + echo "=== FLUTTER PROJECT BACKUP ===" + echo "" + echo "=== PROJECT STRUCTURE (LIB, ASSETS & PUBLIC) ===" + find lib assets public -type f 2>/dev/null | sort + echo "" + echo "=== pubspec.yaml ===" + cat pubspec.yaml 2>/dev/null + echo "" + echo "=== MAC OS CONFIG ===" + echo "--- Info.plist ---" + cat macos/Runner/Info.plist 2>/dev/null + echo "--- Entitlements ---" + cat macos/Runner/*.entitlements 2>/dev/null + echo "--- Podfile ---" + cat macos/Podfile 2>/dev/null + echo "" + echo "=== IOS CONFIG ===" + echo "--- Info.plist ---" + cat ios/Runner/Info.plist 2>/dev/null + echo "--- Podfile ---" + cat ios/Podfile 2>/dev/null + echo "" + echo "=== ANDROID CONFIG ===" + echo "--- AndroidManifest.xml ---" + cat android/app/src/main/AndroidManifest.xml 2>/dev/null + echo "--- build.gradle / build.gradle.kts ---" + cat android/app/build.gradle 2>/dev/null + cat android/app/build.gradle.kts 2>/dev/null + echo "" + echo "=== WEB / FIREBASE (public/) ===" + find public -type f \( -name "*.html" -o -name "*.js" -o -name "*.css" -o -name "*.json" \) 2>/dev/null | sort | while read -r file; do + echo "" + echo "// ===========================================================================" + echo "// FILE: $file" + echo "// ===========================================================================" + echo "" + cat "$file" + done + echo "" + echo "=== SOURCE CODE (lib/) ===" + find lib -type f -name "*.dart" 2>/dev/null | sort | while read -r file; do + echo "" + echo "// ===========================================================================" + echo "// FILE: $file" + echo "// ===========================================================================" + echo "" + cat "$file" + done +} > "$OUTPUT_FILE" && echo "Backup completato! Artefatto salvato in: $OUTPUT_FILE" \ No newline at end of file