9060 lines
No EOL
397 KiB
Text
9060 lines
No EOL
397 KiB
Text
=== FLUTTER PROJECT BACKUP ===
|
||
|
||
=== PROJECT STRUCTURE (LIB & ASSETS) ===
|
||
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_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
|
||
|
||
=== pubspec.yaml ===
|
||
name: tetraq
|
||
description: A new Flutter project.
|
||
publish_to: 'none'
|
||
version: 1.1.5+7
|
||
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
|
||
|
||
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 ---
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||
<plist version="1.0">
|
||
<dict>
|
||
<key>LSApplicationCategoryType</key>
|
||
<string>public.app-category.puzzle-games</string>
|
||
<key>CFBundleDevelopmentRegion</key>
|
||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||
<key>CFBundleExecutable</key>
|
||
<string>$(EXECUTABLE_NAME)</string>
|
||
<key>CFBundleIconFile</key>
|
||
<string></string>
|
||
<key>CFBundleIdentifier</key>
|
||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||
<key>CFBundleInfoDictionaryVersion</key>
|
||
<string>6.0</string>
|
||
<key>CFBundleName</key>
|
||
<string>$(PRODUCT_NAME)</string>
|
||
<key>CFBundlePackageType</key>
|
||
<string>APPL</string>
|
||
<key>CFBundleShortVersionString</key>
|
||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||
<key>CFBundleVersion</key>
|
||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||
<key>LSMinimumSystemVersion</key>
|
||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||
<key>NSHumanReadableCopyright</key>
|
||
<string>$(PRODUCT_COPYRIGHT)</string>
|
||
<key>NSMainNibFile</key>
|
||
<string>MainMenu</string>
|
||
<key>NSPrincipalClass</key>
|
||
<string>NSApplication</string>
|
||
</dict>
|
||
</plist>--- Entitlements ---
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||
<plist version="1.0">
|
||
<dict>
|
||
<key>com.apple.security.app-sandbox</key>
|
||
<true/>
|
||
<key>com.apple.security.cs.allow-jit</key>
|
||
<true/>
|
||
<key>com.apple.security.network.client</key>
|
||
<true/>
|
||
<key>com.apple.security.network.server</key>
|
||
<true/>
|
||
<key>keychain-access-groups</key>
|
||
<array/>
|
||
</dict>
|
||
</plist>
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||
<plist version="1.0">
|
||
<dict>
|
||
<key>com.apple.security.app-sandbox</key>
|
||
<true/>
|
||
<key>com.apple.security.cs.allow-jit</key>
|
||
<true/>
|
||
<key>com.apple.security.network.client</key>
|
||
<true/>
|
||
<key>keychain-access-groups</key>
|
||
<array/>
|
||
</dict>
|
||
</plist>
|
||
--- 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 ---
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||
<plist version="1.0">
|
||
<dict>
|
||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||
<true/>
|
||
<key>CFBundleDevelopmentRegion</key>
|
||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||
<key>CFBundleDisplayName</key>
|
||
<string>Tetraq</string>
|
||
<key>CFBundleExecutable</key>
|
||
<string>$(EXECUTABLE_NAME)</string>
|
||
<key>CFBundleIdentifier</key>
|
||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||
<key>CFBundleInfoDictionaryVersion</key>
|
||
<string>6.0</string>
|
||
<key>CFBundleName</key>
|
||
<string>tetraq</string>
|
||
<key>CFBundlePackageType</key>
|
||
<string>APPL</string>
|
||
<key>CFBundleShortVersionString</key>
|
||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||
<key>CFBundleSignature</key>
|
||
<string>????</string>
|
||
<key>CFBundleURLTypes</key>
|
||
<array>
|
||
<dict>
|
||
<key>CFBundleTypeRole</key>
|
||
<string>Editor</string>
|
||
<key>CFBundleURLName</key>
|
||
<string>com.sanza.tetraq</string>
|
||
<key>CFBundleURLSchemes</key>
|
||
<array>
|
||
<string>tetraq</string>
|
||
</array>
|
||
</dict>
|
||
</array>
|
||
<key>CFBundleVersion</key>
|
||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||
<key>LSRequiresIPhoneOS</key>
|
||
<true/>
|
||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||
<true/>
|
||
<key>UILaunchStoryboardName</key>
|
||
<string>LaunchScreen</string>
|
||
<key>UIMainStoryboardFile</key>
|
||
<string>Main</string>
|
||
<key>UISupportedInterfaceOrientations</key>
|
||
<array>
|
||
<string>UIInterfaceOrientationPortrait</string>
|
||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||
</array>
|
||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||
<array>
|
||
<string>UIInterfaceOrientationPortrait</string>
|
||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||
</array>
|
||
</dict>
|
||
</plist>
|
||
--- 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 ---
|
||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||
|
||
<uses-permission android:name="android.permission.INTERNET" />
|
||
|
||
<application
|
||
android:label="tetraq"
|
||
android:name="${applicationName}"
|
||
android:icon="@mipmap/ic_launcher">
|
||
<activity
|
||
android:name=".MainActivity"
|
||
android:exported="true"
|
||
android:launchMode="singleTop"
|
||
android:taskAffinity=""
|
||
android:theme="@style/LaunchTheme"
|
||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||
android:hardwareAccelerated="true"
|
||
android:windowSoftInputMode="adjustResize">
|
||
<meta-data
|
||
android:name="io.flutter.embedding.android.NormalTheme"
|
||
android:resource="@style/NormalTheme"
|
||
/>
|
||
<intent-filter>
|
||
<action android:name="android.intent.action.MAIN"/>
|
||
<category android:name="android.intent.category.LAUNCHER"/>
|
||
</intent-filter>
|
||
<intent-filter>
|
||
<action android:name="android.intent.action.VIEW" />
|
||
<category android:name="android.intent.category.DEFAULT" />
|
||
<category android:name="android.intent.category.BROWSABLE" />
|
||
<data android:scheme="tetraq" android:host="join" />
|
||
</intent-filter>
|
||
</activity>
|
||
<meta-data
|
||
android:name="flutterEmbedding"
|
||
android:value="2" />
|
||
</application>
|
||
<queries>
|
||
<intent>
|
||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||
<data android:mimeType="text/plain"/>
|
||
</intent>
|
||
</queries>
|
||
</manifest>--- 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 = "../.."
|
||
}
|
||
=== 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<AppThemeType, IconData> 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<AppThemeType, String> 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<AppLocalizations>(context, AppLocalizations);
|
||
}
|
||
|
||
static const LocalizationsDelegate<AppLocalizations> 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<LocalizationsDelegate<dynamic>> localizationsDelegates =
|
||
<LocalizationsDelegate<dynamic>>[
|
||
delegate,
|
||
GlobalMaterialLocalizations.delegate,
|
||
GlobalCupertinoLocalizations.delegate,
|
||
GlobalWidgetsLocalizations.delegate,
|
||
];
|
||
|
||
/// A list of this localizations delegate's supported locales.
|
||
static const List<Locale> supportedLocales = <Locale>[
|
||
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;
|
||
}
|
||
|
||
class _AppLocalizationsDelegate
|
||
extends LocalizationsDelegate<AppLocalizations> {
|
||
const _AppLocalizationsDelegate();
|
||
|
||
@override
|
||
Future<AppLocalizations> load(Locale locale) {
|
||
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
|
||
}
|
||
|
||
@override
|
||
bool isSupported(Locale locale) => <String>[
|
||
'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';
|
||
}
|
||
|
||
// ===========================================================================
|
||
// 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';
|
||
}
|
||
|
||
// ===========================================================================
|
||
// 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';
|
||
}
|
||
|
||
// ===========================================================================
|
||
// 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';
|
||
}
|
||
|
||
// ===========================================================================
|
||
// 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';
|
||
}
|
||
|
||
// ===========================================================================
|
||
// 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';
|
||
}
|
||
|
||
// ===========================================================================
|
||
// 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 => 'ВЫХОД';
|
||
}
|
||
|
||
// ===========================================================================
|
||
// 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 => '退出';
|
||
}
|
||
|
||
// ===========================================================================
|
||
// 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;
|
||
_ClosureResult(this.closesSomething, this.netValue, this.causesSwap);
|
||
}
|
||
|
||
class AIEngine {
|
||
static Line getBestMove(GameBoard board, int level) {
|
||
List<Line> availableLines = board.lines.where((l) => l.owner == Player.none && l.isPlayable).toList();
|
||
final random = Random();
|
||
|
||
if (availableLines.isEmpty) return board.lines.first;
|
||
|
||
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<Line> goodClosingMoves = [];
|
||
List<Line> badClosingMoves = [];
|
||
|
||
for (var line in availableLines) {
|
||
var result = _checkClosure(board, line);
|
||
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 ---
|
||
List<Line> safeMoves = [];
|
||
for (var line in availableLines) {
|
||
if (!badClosingMoves.contains(line) && !goodClosingMoves.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<Line> riskyButNotTerrible = availableLines.where((l) => !badClosingMoves.contains(l) && !goodClosingMoves.contains(l)).toList();
|
||
if (riskyButNotTerrible.isNotEmpty) {
|
||
return riskyButNotTerrible[random.nextInt(riskyButNotTerrible.length)];
|
||
}
|
||
}
|
||
|
||
List<Line> nonTerribleMoves = availableLines.where((l) => !badClosingMoves.contains(l)).toList();
|
||
if (nonTerribleMoves.isNotEmpty) {
|
||
return nonTerribleMoves[random.nextInt(nonTerribleMoves.length)];
|
||
}
|
||
|
||
return availableLines[random.nextInt(availableLines.length)];
|
||
}
|
||
|
||
static _ClosureResult _checkClosure(GameBoard board, Line line) {
|
||
int netValue = 0;
|
||
bool closesSomething = false;
|
||
bool causesSwap = 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) {
|
||
closesSomething = true;
|
||
|
||
// FIX: Togliamo la "vista a raggi X" all'Intelligenza Artificiale!
|
||
if (box.hiddenJokerOwner == board.currentPlayer) {
|
||
// L'IA conosce il suo Jolly, sa che vale +2 e cercherà di chiuderlo
|
||
netValue += 2;
|
||
} else {
|
||
// Se c'è il Jolly del giocatore, l'IA NON DEVE SAPERLO e valuta la casella normalmente!
|
||
if (box.type == BoxType.gold) netValue += 2;
|
||
else if (box.type == BoxType.bomb) netValue -= 1;
|
||
else if (box.type == BoxType.swap) netValue += 0;
|
||
else netValue += 1;
|
||
}
|
||
|
||
if (box.type == BoxType.swap) causesSwap = true;
|
||
}
|
||
}
|
||
}
|
||
return _ClosureResult(closesSomething, netValue, causesSwap);
|
||
}
|
||
|
||
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) {
|
||
|
||
// Nuova logica di sicurezza: cosa succede se l'IA lascia questa scatola all'avversario?
|
||
int valueForOpponent = 0;
|
||
if (box.hiddenJokerOwner == board.currentPlayer) {
|
||
// Se l'avversario la chiude, becca la trappola dell'IA (-1).
|
||
// Quindi PER L'IA È SICURISSIMO LASCIARE QUESTA CASELLA APERTA!
|
||
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 valueForOpponent = 1;
|
||
}
|
||
|
||
// Se per l'avversario vale -1 (bomba normale o trappola dell'IA), lasciamogliela!
|
||
if (valueForOpponent < 0) {
|
||
continue;
|
||
}
|
||
|
||
if (box.type == BoxType.swap) {
|
||
if (myScore < oppScore) {
|
||
continue;
|
||
} else {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
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<DocumentSnapshot>? _onlineSubscription;
|
||
|
||
bool opponentLeft = false;
|
||
bool _hasSavedResult = false;
|
||
|
||
Timer? _blitzTimer;
|
||
int timeLeft = 15;
|
||
final int maxTime = 15;
|
||
bool isTimeMode = true;
|
||
|
||
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;
|
||
|
||
bool hasLeveledUp = false;
|
||
int newlyReachedLevel = 1;
|
||
List<String> unlockedFeatures = [];
|
||
|
||
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<ArenaShape> 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, bool timeMode = true}) {
|
||
_onlineSubscription?.cancel();
|
||
_onlineSubscription = null;
|
||
_blitzTimer?.cancel();
|
||
_effectTimer?.cancel();
|
||
effectText = '';
|
||
_hasSavedResult = false;
|
||
lastMatchXP = 0;
|
||
|
||
hasLeveledUp = false;
|
||
unlockedFeatures.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;
|
||
this.isTimeMode = timeMode;
|
||
|
||
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<Box> newClosed, {List<Box> 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) return;
|
||
|
||
timeLeft = maxTime;
|
||
if (!isTimeMode) { notifyListeners(); return; }
|
||
|
||
_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<Line> 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<String, dynamic>;
|
||
|
||
onlineHostName = data['hostName'] ?? "ROSSO";
|
||
onlineGuestName = (data['guestName'] != null && data['guestName'] != '') ? data['guestName'] : "BLU";
|
||
|
||
if (data['status'] == 'abandoned' && !board.isGameOver && !opponentLeft) {
|
||
opponentLeft = true; notifyListeners(); return;
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
bool p1Rematch = data['p1_rematch'] ?? false;
|
||
bool p2Rematch = data['p2_rematch'] ?? false;
|
||
opponentWantsRematch = isHost ? p2Rematch : p1Rematch;
|
||
|
||
if (data['status'] == 'playing' && (data['moves'] as List).isEmpty && rematchRequested) {
|
||
currentSeed = data['seed'];
|
||
startNewGame(data['radius'], isOnline: true, roomCode: roomCode, isHost: isHost, shape: ArenaShape.values.firstWhere((e) => e.name == data['shape']), timeMode: data['timeMode']);
|
||
return;
|
||
}
|
||
|
||
if (p1Rematch && p2Rematch && isHost && 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)];
|
||
MultiplayerService().resetMatch(roomCode!, newRadius, newShape.name, newSeed);
|
||
}
|
||
|
||
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();
|
||
}
|
||
}
|
||
|
||
List<dynamic> moves = data['moves'] ?? [];
|
||
int hostLevel = data['matchLevel'] ?? 1;
|
||
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);
|
||
onlineShape = hostShape;
|
||
isTimeMode = data['timeMode'] ?? true;
|
||
|
||
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;
|
||
}
|
||
|
||
int firebaseMovesCount = moves.length;
|
||
int localMovesCount = board.lines.where((l) => l.owner != Player.none).length;
|
||
|
||
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;
|
||
}
|
||
|
||
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<Box> closedBefore = board.boxes.where((b) => b.owner != Player.none).toList();
|
||
List<Box> ghostsBefore = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed).toList();
|
||
|
||
board.playMove(lineToPlay, forcedPlayer: playerFromFirebase);
|
||
newMovesApplied = true;
|
||
|
||
List<Box> newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList();
|
||
List<Box> 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<Box> closedBefore = board.boxes.where((b) => b.owner != Player.none).toList();
|
||
List<Box> ghostsBefore = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed).toList();
|
||
|
||
if (board.playMove(line)) {
|
||
List<Box> newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList();
|
||
List<Box> 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<String, dynamic> moveData = {
|
||
'x1': line.p1.x, 'y1': line.p1.y, 'x2': line.p2.x, 'y2': line.p2.y,
|
||
'player': myPlayer == Player.red ? 'red' : 'blue'
|
||
};
|
||
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<Box> closedBefore = board.boxes.where((b) => b.owner != Player.none).toList();
|
||
List<Box> ghostsBefore = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed).toList();
|
||
|
||
Line bestMove = AIEngine.getBestMove(board, cpuLevel);
|
||
board.playMove(bestMove);
|
||
|
||
List<Box> newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList();
|
||
List<Box> 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();
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- NUOVI LIVELLI DI SBLOCCO ---
|
||
List<String> _getUnlocks(int oldLevel, int newLevel) {
|
||
List<String> unlocks = [];
|
||
for(int i = oldLevel + 1; i <= newLevel; i++) {
|
||
if (i == 3) unlocks.add("Tema: Cyberpunk");
|
||
if (i == 7) {
|
||
unlocks.add("Tema: 8-Bit Arcade");
|
||
unlocks.add("Forma Arena: Caos");
|
||
}
|
||
if (i == 10) unlocks.add("Tema: Grimorio");
|
||
if (i == 15) unlocks.add("Tema: Musica");
|
||
}
|
||
return unlocks;
|
||
}
|
||
|
||
void _saveMatchResult() {
|
||
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;
|
||
StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: oppName, myScore: myScore, oppScore: oppScore, isOnline: true);
|
||
|
||
if (isWin) 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) {
|
||
StorageService.instance.addWin();
|
||
StorageService.instance.updateQuestProgress(1, 1);
|
||
} else if (cpuScore > myScore) {
|
||
StorageService.instance.addLoss();
|
||
}
|
||
StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "CPU (Liv. $cpuLevel)", myScore: myScore, oppScore: cpuScore, isOnline: false);
|
||
} else {
|
||
calculatedXP = 2;
|
||
StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "Ospite (Locale)", myScore: board.scoreRed, oppScore: board.scoreBlue, isOnline: false);
|
||
}
|
||
|
||
if (board.shape != ArenaShape.classic) {
|
||
StorageService.instance.updateQuestProgress(2, 1);
|
||
}
|
||
|
||
lastMatchXP = calculatedXP;
|
||
StorageService.instance.addXP(calculatedXP);
|
||
|
||
int newLevel = StorageService.instance.playerLevel;
|
||
if (newLevel > oldLevel) {
|
||
hasLeveledUp = true;
|
||
newlyReachedLevel = newLevel;
|
||
unlockedFeatures = _getUnlocks(oldLevel, newLevel);
|
||
}
|
||
|
||
notifyListeners();
|
||
}
|
||
|
||
void increaseLevelAndRestart() {
|
||
cpuLevel++; StorageService.instance.saveCpuLevel(cpuLevel);
|
||
startNewGame(board.radius, vsCPU: true, shape: board.shape, timeMode: isTimeMode);
|
||
}
|
||
}
|
||
// ===========================================================================
|
||
// FILE: lib/main.dart
|
||
// ===========================================================================
|
||
|
||
// ===========================================================================
|
||
// FILE: lib/main.dart
|
||
// ===========================================================================
|
||
|
||
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 PER IL SUPPORTO MULTILINGUA ---
|
||
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 {
|
||
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,
|
||
),
|
||
|
||
// --- BIVIO DELLE LINGUE ATTIVATO! ---
|
||
// Flutter si occuperà di caricare automaticamente tutte le lingue
|
||
// che hai generato tramite lo script.
|
||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||
supportedLocales: AppLocalizations.supportedLocales,
|
||
// ------------------------------------
|
||
|
||
home: const HomeScreen(),
|
||
);
|
||
}
|
||
}
|
||
// ===========================================================================
|
||
// 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<Dot> dots = [];
|
||
List<Line> lines = [];
|
||
List<Box> 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;
|
||
final AudioPlayer _sfxPlayer = AudioPlayer();
|
||
final AudioPlayer _bgmPlayer = AudioPlayer();
|
||
|
||
AppThemeType _currentTheme = AppThemeType.doodle;
|
||
|
||
Future<void> 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();
|
||
await _sfxPlayer.stop();
|
||
} else {
|
||
playBgm(_currentTheme);
|
||
}
|
||
notifyListeners();
|
||
}
|
||
|
||
Future<void> 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<void> 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 {
|
||
await _sfxPlayer.play(AssetSource('audio/sfx/$file'), volume: 1.0);
|
||
} catch (e) {
|
||
debugPrint("Errore SFX Linea: $file");
|
||
}
|
||
}
|
||
}
|
||
|
||
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 {
|
||
await _sfxPlayer.play(AssetSource('audio/sfx/$file'), volume: 1.0);
|
||
} catch (e) {
|
||
debugPrint("Errore SFX Box: $file");
|
||
}
|
||
}
|
||
}
|
||
|
||
void playBonusSfx() async {
|
||
if (isMuted) return;
|
||
try {
|
||
await _sfxPlayer.play(AssetSource('audio/sfx/bonus.wav'), volume: 1.0);
|
||
} catch(e) {}
|
||
}
|
||
|
||
void playBombSfx() async {
|
||
if (isMuted) return;
|
||
try {
|
||
await _sfxPlayer.play(AssetSource('audio/sfx/bomb.wav'), volume: 1.0);
|
||
} 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');
|
||
|
||
Future<String> createGameRoom(int boardRadius, String hostName, String shapeName, bool isTimeMode, {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': isTimeMode,
|
||
'isPublic': isPublic,
|
||
'p1_reaction': null,
|
||
'p2_reaction': null,
|
||
'p1_rematch': false,
|
||
'p2_rematch': false,
|
||
});
|
||
|
||
return roomCode;
|
||
}
|
||
|
||
Future<Map<String, dynamic>?> joinGameRoom(String roomCode, String guestName) async {
|
||
DocumentSnapshot doc = await _gamesCollection.doc(roomCode).get();
|
||
|
||
if (doc.exists && doc['status'] == 'waiting') {
|
||
await _gamesCollection.doc(roomCode).update({
|
||
'status': 'playing',
|
||
'players': FieldValue.arrayUnion(['guest']),
|
||
'guestName': guestName,
|
||
});
|
||
return doc.data() as Map<String, dynamic>;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
Stream<QuerySnapshot> 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<DocumentSnapshot> listenToRoom(String roomCode) {
|
||
return _gamesCollection.doc(roomCode).snapshots();
|
||
}
|
||
|
||
String _generateRoomCode() {
|
||
const chars = 'ACDEFGHJKLMNPQRSTUVWXYZ2345679';
|
||
final random = Random();
|
||
return String.fromCharCodes(Iterable.generate(
|
||
5, (_) => chars.codeUnitAt(random.nextInt(chars.length)),
|
||
));
|
||
}
|
||
|
||
Future<void> 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<void> 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<void> 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<void> 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<QuerySnapshot> listenForInvites(String myUid) {
|
||
return _invitesCollection.where('targetUid', isEqualTo: myUid).snapshots();
|
||
}
|
||
|
||
Future<void> 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<void> init() async {
|
||
_prefs = await SharedPreferences.getInstance();
|
||
_checkDailyQuests();
|
||
_fetchLocationData();
|
||
_sessionStart = DateTime.now().millisecondsSinceEpoch;
|
||
}
|
||
|
||
Future<void> _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';
|
||
|
||
// --- METODI TEMA AGGIORNATI CON GESTIONE MIGRAZIONE SICURA ---
|
||
String getTheme() {
|
||
final Object? savedTheme = _prefs.get('theme');
|
||
|
||
if (savedTheme is String) {
|
||
return savedTheme;
|
||
} else if (savedTheme is int) {
|
||
// Trovato un vecchio salvataggio in formato intero (causa del crash).
|
||
// Puliamo la memoria per evitare futuri problemi.
|
||
_prefs.remove('theme');
|
||
return AppThemeType.doodle.toString();
|
||
}
|
||
|
||
return AppThemeType.doodle.toString();
|
||
}
|
||
|
||
Future<void> saveTheme(String themeStr) async => await _prefs.setString('theme', themeStr);
|
||
|
||
int get savedRadius => _prefs.getInt('radius') ?? 2;
|
||
Future<void> saveRadius(int radius) async => await _prefs.setInt('radius', radius);
|
||
|
||
bool get isMuted => _prefs.getBool('isMuted') ?? false;
|
||
Future<void> saveMuted(bool muted) async => await _prefs.setBool('isMuted', muted);
|
||
|
||
int get totalXP => _prefs.getInt('totalXP') ?? 0;
|
||
|
||
Future<void> addXP(int xp) async {
|
||
await _prefs.setInt('totalXP', totalXP + xp);
|
||
syncLeaderboard();
|
||
}
|
||
|
||
int get playerLevel => (totalXP / 100).floor() + 1;
|
||
|
||
int get wins => _prefs.getInt('wins') ?? 0;
|
||
Future<void> addWin() async {
|
||
await _prefs.setInt('wins', wins + 1);
|
||
syncLeaderboard();
|
||
}
|
||
|
||
int get losses => _prefs.getInt('losses') ?? 0;
|
||
Future<void> addLoss() async => await _prefs.setInt('losses', losses + 1);
|
||
|
||
int get cpuLevel => _prefs.getInt('cpuLevel') ?? 1;
|
||
Future<void> saveCpuLevel(int level) async => await _prefs.setInt('cpuLevel', level);
|
||
|
||
String get playerName => _prefs.getString('playerName') ?? '';
|
||
Future<void> savePlayerName(String name) async {
|
||
await _prefs.setString('playerName', name);
|
||
syncLeaderboard();
|
||
}
|
||
|
||
Future<void> syncLeaderboard() async {
|
||
if (playerName.isNotEmpty) {
|
||
try {
|
||
final user = FirebaseAuth.instance.currentUser;
|
||
|
||
if (user != null) {
|
||
String currentPlatform = "Sconosciuta";
|
||
String appVersion = "N/D";
|
||
String deviceModel = "Sconosciuto";
|
||
|
||
if (!kIsWeb) {
|
||
if (Platform.isAndroid) currentPlatform = "Android";
|
||
else if (Platform.isIOS) currentPlatform = "iOS";
|
||
else if (Platform.isMacOS) currentPlatform = "macOS";
|
||
else if (Platform.isWindows) currentPlatform = "Windows";
|
||
|
||
try {
|
||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||
appVersion = "${packageInfo.version}+${packageInfo.buildNumber}";
|
||
} catch(e) {
|
||
debugPrint("Errore lettura versione: $e");
|
||
}
|
||
|
||
try {
|
||
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||
if (Platform.isAndroid) {
|
||
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
|
||
deviceModel = "${androidInfo.manufacturer} ${androidInfo.model}";
|
||
} else if (Platform.isIOS) {
|
||
IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
|
||
deviceModel = iosInfo.utsname.machine;
|
||
} else if (Platform.isMacOS) {
|
||
MacOsDeviceInfo macInfo = await deviceInfo.macOsInfo;
|
||
deviceModel = macInfo.model;
|
||
}
|
||
} catch(e) {
|
||
debugPrint("Errore lettura hardware: $e");
|
||
}
|
||
}
|
||
|
||
if (_sessionStart != 0) {
|
||
int now = DateTime.now().millisecondsSinceEpoch;
|
||
int sessionSeconds = (now - _sessionStart) ~/ 1000;
|
||
await _prefs.setInt('totalPlaytime', (_prefs.getInt('totalPlaytime') ?? 0) + sessionSeconds);
|
||
_sessionStart = now;
|
||
}
|
||
int totalPlaytime = _prefs.getInt('totalPlaytime') ?? 0;
|
||
|
||
await FirebaseFirestore.instance.collection('leaderboard').doc(user.uid).set({
|
||
'name': playerName,
|
||
'xp': totalXP,
|
||
'level': playerLevel,
|
||
'wins': wins,
|
||
'lastActive': FieldValue.serverTimestamp(),
|
||
'platform': currentPlatform,
|
||
'ip': lastIp,
|
||
'city': lastCity,
|
||
'playtime': totalPlaytime,
|
||
'appVersion': appVersion,
|
||
'deviceModel': deviceModel,
|
||
}, SetOptions(merge: true));
|
||
}
|
||
} catch(e) {
|
||
debugPrint("Errore sinc. classifica: $e");
|
||
}
|
||
}
|
||
}
|
||
|
||
List<Map<String, String>> get favorites {
|
||
List<String> favs = _prefs.getStringList('favorites') ?? [];
|
||
return favs.map((e) => Map<String, String>.from(jsonDecode(e))).toList();
|
||
}
|
||
|
||
Future<void> 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<void> 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<Map<String, dynamic>> get matchHistory {
|
||
List<String> history = _prefs.getStringList('matchHistory') ?? [];
|
||
return history.map((e) => jsonDecode(e) as Map<String, dynamic>).toList();
|
||
}
|
||
|
||
Future<void> saveMatchToHistory({required String myName, required String opponent, required int myScore, required int oppScore, required bool isOnline}) async {
|
||
List<String> history = _prefs.getStringList('matchHistory') ?? [];
|
||
Map<String, dynamic> match = {
|
||
'date': DateTime.now().toIso8601String(),
|
||
'myName': myName, 'opponent': opponent, 'myScore': myScore, 'oppScore': oppScore, 'isOnline': isOnline,
|
||
};
|
||
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<ThemeManager>().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<QuerySnapshot>(
|
||
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, dynamic>;
|
||
|
||
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')}";
|
||
|
||
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').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(),
|
||
),
|
||
// QUI È DOVE AVVENIVA IL CRASH! Ora usiamo Expanded e FittedBox
|
||
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<Dot> activeDots = {};
|
||
for (var line in board.lines) {
|
||
if (line.isPlayable) {
|
||
activeDots.add(line.p1); activeDots.add(line.p2);
|
||
}
|
||
}
|
||
|
||
for (var dot in activeDots) {
|
||
Offset pos = getScreenPos(dot.x, dot.y);
|
||
if (themeType == AppThemeType.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<zigzags; i++) {
|
||
double d = len * (i / zigzags);
|
||
Offset basePt = Offset(p1.dx + ndir.x * d, p1.dy + ndir.y * d);
|
||
double offset = (i % 2 == 0 ? 3.0 : -3.0);
|
||
crack.lineTo(basePt.dx + perp.x * offset, basePt.dy + perp.y * offset);
|
||
}
|
||
crack.lineTo(p2.dx, p2.dy);
|
||
canvas.drawPath(crack, crackPaint);
|
||
}
|
||
|
||
void _drawArcadeBox(Canvas canvas, Rect rect, Color color) {
|
||
double pixelSize = 4.0; Paint paint = Paint()..color = color.withOpacity(0.9)..style = PaintingStyle.fill;
|
||
for (double y = rect.top; y < rect.bottom; y += pixelSize) {
|
||
for (double x = rect.left; x < rect.right; x += pixelSize) {
|
||
int xi = ((x - rect.left) / pixelSize).floor(); int yi = ((y - rect.top) / pixelSize).floor();
|
||
if ((xi + yi) % 2 == 0) canvas.drawRect(Rect.fromLTWH(x, y, pixelSize, pixelSize), paint);
|
||
}
|
||
}
|
||
canvas.drawRect(rect.deflate(2.0), Paint()..color = Colors.white.withOpacity(0.4)..style = PaintingStyle.stroke..strokeWidth = 2.0);
|
||
}
|
||
|
||
void _drawGrimorioBox(Canvas canvas, Rect rect, Color color) {
|
||
canvas.drawRect(rect, Paint()..color = color.withOpacity(0.15)..style=PaintingStyle.fill);
|
||
Offset c = rect.center; double r = rect.width * 0.35;
|
||
Paint linePaint = Paint()..color = color.withOpacity(0.8)..style = PaintingStyle.stroke..strokeWidth = 1.5..maskFilter = const MaskFilter.blur(BlurStyle.solid, 1.0);
|
||
canvas.drawCircle(c, r, linePaint); canvas.drawCircle(c, r * 0.8, linePaint..strokeWidth = 0.5);
|
||
Path p = Path();
|
||
for(int i=0; i<3; i++) {
|
||
double a = -pi/2 + i * 2*pi/3; Offset pt = Offset(c.dx + r*cos(a), c.dy + r*sin(a));
|
||
if(i==0) p.moveTo(pt.dx, pt.dy); else p.lineTo(pt.dx, pt.dy);
|
||
}
|
||
p.close(); canvas.drawPath(p, linePaint..strokeWidth = 1.0);
|
||
}
|
||
|
||
void _drawArcadeLine(Canvas canvas, Offset p1, Offset p2, Color color, bool isConquered, {bool isLastMove = false, double blinkValue = 0.0}) {
|
||
double pixelSize = 6.0; Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy); double len = dir.length; Vector2 ndir = dir.normalized();
|
||
Paint paint = Paint()..color = isConquered ? color : color.withOpacity(0.15)..style = PaintingStyle.fill;
|
||
Paint highlight = Paint()..color = Colors.white.withOpacity(0.6)..style = PaintingStyle.fill;
|
||
for(double d = 0; d <= len; d += pixelSize + 1.0) {
|
||
Offset pt = Offset(p1.dx + ndir.x * d, p1.dy + ndir.y * d);
|
||
canvas.drawRect(Rect.fromCenter(center: pt, width: pixelSize, height: pixelSize), paint);
|
||
if (isConquered && (d / (pixelSize+1.0)).floor() % 3 == 0) canvas.drawRect(Rect.fromCenter(center: pt - const Offset(1,1), width: pixelSize*0.4, height: pixelSize*0.4), highlight);
|
||
}
|
||
if (isLastMove && isConquered) canvas.drawRect(Rect.fromPoints(p1, p2).inflate(4.0), Paint()..color = Colors.white.withOpacity(blinkValue*0.4)..style=PaintingStyle.stroke..strokeWidth=2.0);
|
||
}
|
||
|
||
void _drawGrimorioLine(Canvas canvas, Offset p1, Offset p2, Color color, bool isConquered, {bool isLastMove = false, double blinkValue = 0.0}) {
|
||
if (!isConquered) { canvas.drawLine(p1, p2, Paint()..color = color.withOpacity(0.15)..strokeWidth = 2.0..strokeCap = StrokeCap.round); return; }
|
||
canvas.drawLine(p1, p2, Paint()..color = color.withOpacity(0.6)..strokeWidth = 5.0..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4.0));
|
||
canvas.drawLine(p1, p2, Paint()..color = Colors.white.withOpacity(0.7)..strokeWidth = 1.5..strokeCap = StrokeCap.round);
|
||
int seed = (p1.dx * 1000 + p1.dy).toInt(); Random rand = Random(seed);
|
||
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 thread1 = Path(); Path thread2 = Path(); int segments = 15; double step = len / segments;
|
||
double phaseOffset = (isLastMove ? blinkValue * pi * 4 : 0) + rand.nextDouble()*pi;
|
||
for(int i = 0; i <= segments; i++) {
|
||
double d = i * step; Offset basePt = Offset(p1.dx + ndir.x * d, p1.dy + ndir.y * d);
|
||
double amplitude = 3.5; double wave1 = sin(d * 0.15 + phaseOffset) * amplitude; double wave2 = cos(d * 0.15 + phaseOffset) * amplitude;
|
||
Offset pt1 = basePt + Offset(perp.x * wave1, perp.y * wave1); Offset pt2 = basePt + Offset(perp.x * wave2, perp.y * wave2);
|
||
if (i == 0) { thread1.moveTo(pt1.dx, pt1.dy); thread2.moveTo(pt2.dx, pt2.dy); } else { thread1.lineTo(pt1.dx, pt1.dy); thread2.lineTo(pt2.dx, pt2.dy); }
|
||
}
|
||
Paint threadPaint = Paint()..color = color.withOpacity(0.9)..style = PaintingStyle.stroke..strokeWidth = 1.5..maskFilter = const MaskFilter.blur(BlurStyle.solid, 1.0);
|
||
canvas.drawPath(thread1, threadPaint); canvas.drawPath(thread2, threadPaint..color = Colors.white.withOpacity(0.5));
|
||
}
|
||
|
||
void _drawScribbleBox(Canvas canvas, Rect baseRect, Color color) {
|
||
final rand = Random((baseRect.left + baseRect.top).toInt());
|
||
final paint = Paint()..color = color.withOpacity(0.85)..style = PaintingStyle.stroke..strokeWidth = 3.5..strokeCap = StrokeCap.round..strokeJoin = StrokeJoin.round;
|
||
final path = Path(); Rect rect = baseRect.deflate(4.0); int numZigs = 15 + rand.nextInt(6); double stepY = rect.height / numZigs;
|
||
path.moveTo(rect.left + rand.nextDouble() * 5, rect.top + rand.nextDouble() * 5);
|
||
for (int i = 1; i <= numZigs; i++) { double targetX = (i % 2 != 0) ? rect.right + (rand.nextDouble() * 4 - 2) : rect.left + (rand.nextDouble() * 4 - 2); double targetY = rect.top + stepY * i + (rand.nextDouble() - 0.5) * 3; double ctrlX = rect.center.dx + (rand.nextDouble() - 0.5) * 20; double ctrlY = targetY - stepY / 2; path.quadraticBezierTo(ctrlX, ctrlY, targetX, targetY); }
|
||
canvas.drawPath(path, paint);
|
||
}
|
||
|
||
void _drawNeonLine(Canvas canvas, Offset p1, Offset p2, Color color, bool isConquered, {bool isLastMove = false, double blinkValue = 0.0}) {
|
||
double mainWidth = isConquered ? (isLastMove ? 6.0 + (blinkValue * 3.0) : 6.0) : 3.0; Color coreColor = isConquered ? (isLastMove ? Color.lerp(Colors.white, color, 1.0 - blinkValue)! : Colors.white.withOpacity(0.9)) : color.withOpacity(0.6);
|
||
canvas.drawLine(p1, p2, Paint()..color = color.withOpacity(isConquered ? (isLastMove ? 0.4 + (0.4 * blinkValue) : 0.4) : 0.2)..strokeWidth = mainWidth * 4..strokeCap = StrokeCap.round..maskFilter = MaskFilter.blur(BlurStyle.normal, isConquered ? 12.0 : 6.0));
|
||
if (isConquered) { canvas.drawLine(p1, p2, Paint()..color = color.withOpacity(isLastMove ? 0.7 + (0.3 * blinkValue) : 0.7)..strokeWidth = mainWidth * 2..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6.0)); }
|
||
canvas.drawLine(p1, p2, Paint()..color = coreColor..strokeWidth = mainWidth..strokeCap = StrokeCap.round);
|
||
}
|
||
|
||
void _drawWobblyLine(Canvas canvas, Offset p1, Offset p2, Color color, bool isConquered, {bool isLastMove = false, double blinkValue = 0.0}) {
|
||
final random = Random((p1.dx + p1.dy + p2.dx + p2.dy).toInt()); final dx = p2.dx - p1.dx; final dy = p2.dy - p1.dy;
|
||
double strokeW = isConquered ? (isLastMove ? 4.5 + (2.0 * blinkValue) : 4.5) : 2.0;
|
||
final basePaint = Paint()..color = color..strokeWidth = strokeW..style = PaintingStyle.stroke..strokeCap = StrokeCap.round;
|
||
final mid1 = Offset(p1.dx + dx / 2 + (random.nextDouble() - 0.5) * 8, p1.dy + dy / 2 + (random.nextDouble() - 0.5) * 8); canvas.drawPath(Path()..moveTo(p1.dx, p1.dy)..quadraticBezierTo(mid1.dx, mid1.dy, p2.dx, p2.dy), basePaint);
|
||
final mid2 = Offset(p1.dx + dx / 2 + (random.nextDouble() - 0.5) * 6, p1.dy + dy / 2 + (random.nextDouble() - 0.5) * 6); canvas.drawPath(Path()..moveTo(p1.dx, p1.dy)..quadraticBezierTo(mid2.dx, mid2.dy, p2.dx, p2.dy), basePaint..strokeWidth = strokeW * 0.5..color = color.withOpacity(0.8));
|
||
}
|
||
|
||
@override bool shouldRepaint(covariant BoardPainter oldDelegate) => true;
|
||
}
|
||
|
||
class Vector2 {
|
||
final double x, y; Vector2(this.x, this.y); double get length => sqrt(x * x + y * y);
|
||
Vector2 normalized() { double l = length; return l == 0 ? Vector2(0, 0) : Vector2(x / l, y / l); }
|
||
}
|
||
// ===========================================================================
|
||
// FILE: lib/ui/game/game_screen.dart
|
||
// ===========================================================================
|
||
|
||
// ===========================================================================
|
||
// 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<GameScreen> createState() => _GameScreenState();
|
||
}
|
||
|
||
class _GameScreenState extends State<GameScreen> 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<GameController>(
|
||
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.isTimeMode); },
|
||
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<ThemeManager>();
|
||
final themeType = themeManager.currentThemeType;
|
||
final theme = themeManager.currentColors;
|
||
final gameController = context.watch<GameController>();
|
||
|
||
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<String> 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<Path> {
|
||
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<WinnerVFXOverlay> createState() => _WinnerVFXOverlayState();
|
||
}
|
||
|
||
class _WinnerVFXOverlayState extends State<WinnerVFXOverlay> 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<Color> 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<double> _anim;
|
||
@override void initState() { super.initState(); _ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 500))..repeat(reverse: true); _anim = Tween<double>(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<BlitzBackgroundEffect> createState() => _BlitzBackgroundEffectState();
|
||
}
|
||
class _BlitzBackgroundEffectState extends State<BlitzBackgroundEffect> with SingleTickerProviderStateMixin {
|
||
late AnimationController _controller;
|
||
@override void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 400))..repeat(reverse: true); }
|
||
@override void dispose() { _controller.dispose(); super.dispose(); }
|
||
@override Widget build(BuildContext context) { return AnimatedBuilder(animation: _controller, builder: (context, child) { return Container(color: widget.color.withOpacity(0.12 * _controller.value), child: Center(child: ImageFiltered(imageFilter: ImageFilter.blur(sigmaX: 2.0, sigmaY: 2.0), child: Text('${widget.timeLeft}', style: _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<SpecialEventBackgroundEffect> createState() => _SpecialEventBackgroundEffectState();
|
||
}
|
||
class _SpecialEventBackgroundEffectState extends State<SpecialEventBackgroundEffect> with SingleTickerProviderStateMixin {
|
||
late AnimationController _controller; late Animation<double> _scaleAnimation; late Animation<double> _opacityAnimation;
|
||
@override void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 1000))..forward(); _scaleAnimation = Tween<double>(begin: 0.5, end: 1.5).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic)); _opacityAnimation = Tween<double>(begin: 0.9, end: 0.0).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn)); }
|
||
@override void didUpdateWidget(covariant SpecialEventBackgroundEffect oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.text != widget.text) { _controller.reset(); _controller.forward(); } }
|
||
@override void dispose() { _controller.dispose(); super.dispose(); }
|
||
@override Widget build(BuildContext context) { return AnimatedBuilder(animation: _controller, builder: (context, child) { return Center(child: Transform.scale(scale: _scaleAnimation.value, child: Opacity(opacity: _opacityAnimation.value, child: ImageFiltered(imageFilter: ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0), child: Text(widget.text, 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<ScoreBoard> createState() => _ScoreBoardState();
|
||
}
|
||
|
||
class _ScoreBoardState extends State<ScoreBoard> {
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final controller = context.watch<GameController>();
|
||
final themeManager = context.watch<ThemeManager>();
|
||
final theme = themeManager.currentColors;
|
||
final themeType = themeManager.currentThemeType;
|
||
|
||
int redScore = controller.board.scoreRed;
|
||
int blueScore = controller.board.scoreBlue;
|
||
|
||
bool isRedTurn = controller.board.currentPlayer == Player.red;
|
||
bool isMuted = AudioService.instance.isMuted;
|
||
|
||
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<ThemeManager>();
|
||
final theme = themeManager.currentColors;
|
||
final themeType = themeManager.currentThemeType;
|
||
final loc = AppLocalizations.of(context)!;
|
||
|
||
return FutureBuilder<SharedPreferences>(
|
||
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)
|
||
// ===========================================================================
|
||
class LeaderboardDialog extends StatelessWidget {
|
||
const LeaderboardDialog({super.key});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final themeManager = context.watch<ThemeManager>();
|
||
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<QuerySnapshot>(
|
||
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, dynamic>;
|
||
String name = (data['name'] ?? '').toString().toUpperCase();
|
||
return name != 'PAOLO';
|
||
}).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, dynamic>;
|
||
String? myUid = FirebaseAuth.instance.currentUser?.uid;
|
||
bool isMe = doc.id == myUid;
|
||
String playerName = data['name'] ?? 'Unknown';
|
||
|
||
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: Text(playerName, style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: isMe ? FontWeight.w900 : FontWeight.bold, color: theme.text)))),
|
||
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<ThemeManager>();
|
||
final theme = themeManager.currentColors;
|
||
final themeType = themeManager.currentThemeType;
|
||
Color inkColor = const Color(0xFF111122);
|
||
|
||
// ETICHETTE DINAMICHE PER I POTENZIAMENTI
|
||
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))),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
// ===========================================================================
|
||
// FILE: lib/ui/home/history_screen.dart
|
||
// ===========================================================================
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'package:provider/provider.dart';
|
||
import 'package:intl/intl.dart';
|
||
import '../../core/theme_manager.dart';
|
||
import '../../services/storage_service.dart';
|
||
|
||
class HistoryScreen extends StatelessWidget {
|
||
const HistoryScreen({super.key});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = context.watch<ThemeManager>().currentColors;
|
||
final history = StorageService.instance.matchHistory;
|
||
|
||
return Scaffold(
|
||
backgroundColor: theme.background,
|
||
appBar: AppBar(
|
||
title: Text("STORICO PARTITE", style: TextStyle(fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 2)),
|
||
backgroundColor: Colors.transparent,
|
||
elevation: 0,
|
||
iconTheme: IconThemeData(color: theme.text),
|
||
),
|
||
body: history.isEmpty
|
||
? Center(
|
||
child: Text(
|
||
"Nessuna partita giocata.\nScendi in campo!",
|
||
textAlign: TextAlign.center,
|
||
style: TextStyle(color: theme.text.withOpacity(0.5), fontSize: 18, fontWeight: FontWeight.bold),
|
||
),
|
||
)
|
||
: ListView.builder(
|
||
padding: const EdgeInsets.all(20),
|
||
itemCount: history.length,
|
||
itemBuilder: (context, index) {
|
||
final match = history[index];
|
||
|
||
DateTime date = DateTime.parse(match['date']);
|
||
String formattedDate = DateFormat('dd MMM yyyy - HH:mm').format(date);
|
||
|
||
// Leggiamo entrambi i nomi
|
||
String myName = match['myName'] ?? "IO"; // Usa 'IO' se è una partita vecchia
|
||
String opponent = match['opponent'];
|
||
|
||
int myScore = match['myScore'];
|
||
int oppScore = match['oppScore'];
|
||
bool isOnline = match['isOnline'];
|
||
|
||
bool isWin = myScore > oppScore;
|
||
bool isDraw = myScore == oppScore;
|
||
|
||
Color resultColor = isWin ? Colors.green : (isDraw ? Colors.grey : theme.playerRed);
|
||
String resultText = isWin ? "VITTORIA" : (isDraw ? "PAREGGIO" : "SCONFITTA");
|
||
|
||
return Container(
|
||
margin: const EdgeInsets.only(bottom: 15),
|
||
padding: const EdgeInsets.all(16),
|
||
decoration: BoxDecoration(
|
||
color: theme.text.withOpacity(0.05),
|
||
borderRadius: BorderRadius.circular(20),
|
||
border: Border.all(color: resultColor.withOpacity(0.5), width: 2),
|
||
boxShadow: [
|
||
BoxShadow(color: Colors.black.withOpacity(0.2), offset: const Offset(0, 4), blurRadius: 6),
|
||
],
|
||
),
|
||
child: Row(
|
||
children: [
|
||
// Icona Tipo di Partita
|
||
Container(
|
||
padding: const EdgeInsets.all(12),
|
||
decoration: BoxDecoration(
|
||
color: resultColor.withOpacity(0.1),
|
||
shape: BoxShape.circle,
|
||
),
|
||
child: Icon(
|
||
isOnline ? Icons.public : (opponent.contains("CPU") ? Icons.smart_toy : Icons.people_alt),
|
||
color: resultColor,
|
||
size: 28,
|
||
),
|
||
),
|
||
const SizedBox(width: 15),
|
||
|
||
// Dati Partita (Ora con i nomi chiari)
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(resultText, style: TextStyle(color: resultColor, fontWeight: FontWeight.w900, fontSize: 16, letterSpacing: 1.5)),
|
||
const SizedBox(height: 5),
|
||
// NOMI GIOCATORI
|
||
RichText(
|
||
text: TextSpan(
|
||
children: [
|
||
TextSpan(text: myName, style: TextStyle(color: theme.playerBlue, fontWeight: FontWeight.bold, fontSize: 15)),
|
||
TextSpan(text: " vs ", style: TextStyle(color: theme.text.withOpacity(0.5), fontStyle: FontStyle.italic, fontSize: 12)),
|
||
TextSpan(text: opponent, style: TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold, fontSize: 15)),
|
||
]
|
||
)
|
||
),
|
||
const SizedBox(height: 5),
|
||
Text(formattedDate, style: TextStyle(color: theme.text.withOpacity(0.5), fontSize: 12)),
|
||
],
|
||
),
|
||
),
|
||
|
||
// Punteggio
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||
decoration: BoxDecoration(
|
||
color: theme.background,
|
||
borderRadius: BorderRadius.circular(15),
|
||
border: Border.all(color: theme.gridLine.withOpacity(0.3)),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Text("$myScore", style: TextStyle(fontSize: 22, fontWeight: FontWeight.w900, color: theme.playerBlue)),
|
||
Text(" - ", style: TextStyle(fontSize: 18, color: theme.text.withOpacity(0.5))),
|
||
Text("$oppScore", style: TextStyle(fontSize: 22, fontWeight: FontWeight.w900, color: theme.playerRed)),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
}
|
||
// ===========================================================================
|
||
// FILE: lib/ui/home/home_screen.dart
|
||
// ===========================================================================
|
||
|
||
// ===========================================================================
|
||
// FILE: lib/ui/home/home_screen.dart
|
||
// ===========================================================================
|
||
|
||
import 'dart:ui';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:provider/provider.dart';
|
||
import 'package:flutter/services.dart';
|
||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||
import 'dart:async';
|
||
import 'package:app_links/app_links.dart';
|
||
|
||
import 'package:firebase_auth/firebase_auth.dart';
|
||
import 'package:shared_preferences/shared_preferences.dart';
|
||
|
||
import '../../logic/game_controller.dart';
|
||
import '../../core/theme_manager.dart';
|
||
import '../../core/app_colors.dart';
|
||
import '../game/game_screen.dart';
|
||
import '../settings/settings_screen.dart';
|
||
import '../../services/storage_service.dart';
|
||
import '../../services/audio_service.dart';
|
||
import '../../services/multiplayer_service.dart';
|
||
import '../multiplayer/lobby_screen.dart';
|
||
import 'history_screen.dart';
|
||
import '../admin/admin_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 '../../widgets/custom_settings_button.dart';
|
||
import 'dialog.dart';
|
||
|
||
class HomeScreen extends StatefulWidget {
|
||
const HomeScreen({super.key});
|
||
|
||
@override
|
||
State<HomeScreen> createState() => _HomeScreenState();
|
||
}
|
||
|
||
class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
||
|
||
int _debugTapCount = 0;
|
||
late AppLinks _appLinks;
|
||
StreamSubscription<Uri>? _linkSubscription;
|
||
bool _isCreatingRoom = false;
|
||
|
||
int _selectedRadius = 4;
|
||
ArenaShape _selectedShape = ArenaShape.classic;
|
||
bool _isTimeMode = true;
|
||
bool _isPublicRoom = true;
|
||
bool _isLoading = false;
|
||
String? _myRoomCode;
|
||
bool _roomStarted = false;
|
||
|
||
final MultiplayerService _multiplayerService = MultiplayerService();
|
||
final TextEditingController _codeController = TextEditingController();
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
WidgetsBinding.instance.addObserver(this);
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
_checkPlayerName();
|
||
StorageService.instance.syncLeaderboard();
|
||
_checkThemeSafety();
|
||
});
|
||
_checkClipboardForInvite();
|
||
_initDeepLinks();
|
||
}
|
||
|
||
void _checkThemeSafety() {
|
||
String themeStr = StorageService.instance.getTheme();
|
||
bool exists = AppThemeType.values.any((e) => e.toString() == themeStr);
|
||
if (!exists) {
|
||
context.read<ThemeManager>().setTheme(AppThemeType.doodle);
|
||
}
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
WidgetsBinding.instance.removeObserver(this);
|
||
_cleanupGhostRoom();
|
||
_linkSubscription?.cancel();
|
||
_codeController.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||
if (state == AppLifecycleState.resumed) {
|
||
_checkClipboardForInvite();
|
||
} else if (state == AppLifecycleState.paused || state == AppLifecycleState.detached) {
|
||
_cleanupGhostRoom();
|
||
}
|
||
}
|
||
|
||
void _cleanupGhostRoom() {
|
||
if (_myRoomCode != null && !_roomStarted) {
|
||
FirebaseFirestore.instance.collection('games').doc(_myRoomCode).delete();
|
||
_myRoomCode = null;
|
||
}
|
||
}
|
||
|
||
void _checkPlayerName() {
|
||
if (StorageService.instance.playerName.isEmpty) { _showNameDialog(); }
|
||
}
|
||
|
||
Future<void> _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) _promptJoinRoom(code.toUpperCase());
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
Future<void> _checkClipboardForInvite() async {
|
||
try {
|
||
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
|
||
String? text = data?.text;
|
||
if (text != null && text.contains("TetraQ") && text.contains("codice:")) {
|
||
RegExp regExp = RegExp(r'codice:\s*([A-Z0-9]{5})', caseSensitive: false);
|
||
Match? match = regExp.firstMatch(text);
|
||
if (match != null) {
|
||
String roomCode = match.group(1)!.toUpperCase();
|
||
await Clipboard.setData(const ClipboardData(text: ''));
|
||
if (mounted && ModalRoute.of(context)?.isCurrent == true) { _promptJoinRoom(roomCode); }
|
||
}
|
||
}
|
||
} catch (e) { debugPrint("Errore lettura appunti: $e"); }
|
||
}
|
||
|
||
Future<void> _createRoom() async {
|
||
if (_isLoading) return;
|
||
setState(() => _isLoading = true);
|
||
|
||
try {
|
||
String playerName = StorageService.instance.playerName;
|
||
if (playerName.isEmpty) playerName = "HOST";
|
||
|
||
String code = await _multiplayerService.createGameRoom(
|
||
_selectedRadius, playerName, _selectedShape.name, _isTimeMode, 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<void> _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 {
|
||
String playerName = StorageService.instance.playerName;
|
||
if (playerName.isEmpty) playerName = "GUEST";
|
||
|
||
Map<String, dynamic>? roomData = await _multiplayerService.joinGameRoom(code, playerName);
|
||
|
||
if (!mounted) return;
|
||
setState(() => _isLoading = false);
|
||
|
||
if (roomData != null) {
|
||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Stanza trovata! Partita in avvio..."), backgroundColor: Colors.green));
|
||
|
||
int hostRadius = roomData['radius'] ?? 4;
|
||
String shapeStr = roomData['shape'] ?? 'classic';
|
||
ArenaShape hostShape = ArenaShape.values.firstWhere((e) => e.name == shapeStr, orElse: () => ArenaShape.classic);
|
||
bool hostTimeMode = roomData['timeMode'] ?? true;
|
||
|
||
context.read<GameController>().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 _showWaitingDialog(String code) {
|
||
showDialog(
|
||
context: context,
|
||
barrierDismissible: false,
|
||
builder: (context) {
|
||
final theme = context.watch<ThemeManager>().currentColors;
|
||
final themeType = context.read<ThemeManager>().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!" : "Invita un amico", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.w900, fontSize: 18))),
|
||
const SizedBox(height: 8),
|
||
Text(_isPublicRoom ? "Aspettiamo che uno sfidante si unisca dalla lobby pubblica." : "Condividi il codice. La partita inizierà appena si unirà.", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.8), fontSize: 14, height: 1.5))),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
|
||
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<DocumentSnapshot>(
|
||
stream: _multiplayerService.listenToRoom(code),
|
||
builder: (context, snapshot) {
|
||
if (snapshot.hasData && snapshot.data!.exists) {
|
||
var data = snapshot.data!.data() as Map<String, dynamic>;
|
||
if (data['status'] == 'playing') {
|
||
_roomStarted = true;
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
Navigator.pop(context);
|
||
context.read<GameController>().startNewGame(_selectedRadius, isOnline: true, roomCode: code, isHost: true, shape: _selectedShape, timeMode: _isTimeMode);
|
||
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const GameScreen()));
|
||
});
|
||
}
|
||
}
|
||
|
||
return PopScope(
|
||
canPop: false,
|
||
onPopInvoked: (didPop) {
|
||
if (didPop) return;
|
||
_cleanupGhostRoom();
|
||
Navigator.pop(context);
|
||
},
|
||
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(context);
|
||
},
|
||
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)]))),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
);
|
||
}
|
||
|
||
void _promptJoinRoom(String roomCode) {
|
||
showDialog(
|
||
context: context,
|
||
builder: (context) {
|
||
final theme = context.watch<ThemeManager>().currentColors;
|
||
final themeType = context.read<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();
|
||
_joinRoomByCode(roomCode);
|
||
},
|
||
child: Text(AppLocalizations.of(context)!.joinMatch, style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? theme.text : Colors.white, fontWeight: FontWeight.bold))),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
);
|
||
}
|
||
|
||
void _showNameDialog() {
|
||
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: (context) {
|
||
final themeManager = context.watch<ThemeManager>();
|
||
final theme = themeManager.currentColors; final themeType = themeManager.currentThemeType;
|
||
Color inkColor = const Color(0xFF111122); final loc = AppLocalizations.of(context)!;
|
||
|
||
return StatefulBuilder(
|
||
builder: (context, setStateDialog) {
|
||
|
||
Future<void> 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";
|
||
|
||
try {
|
||
if (isLogin) {
|
||
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<String, dynamic>;
|
||
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 (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Bentornato $name! Dati sincronizzati."), backgroundColor: Colors.green));
|
||
} else {
|
||
await FirebaseAuth.instance.createUserWithEmailAndPassword(email: fakeEmail, password: password);
|
||
if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Account creato con successo!"), backgroundColor: Colors.green));
|
||
}
|
||
|
||
await StorageService.instance.savePlayerName(name);
|
||
if (mounted) Navigator.of(context).pop();
|
||
setState(() {});
|
||
|
||
} on FirebaseAuthException catch (e) {
|
||
String msg = "Errore di autenticazione.";
|
||
if (e.code == 'email-already-in-use') {
|
||
msg = "Nome occupato!\nSe è il tuo account, clicca su ACCEDI.";
|
||
} else if (e.code == 'user-not-found' || e.code == 'wrong-password' || e.code == 'invalid-credential') {
|
||
msg = "Nome o Password errati!";
|
||
}
|
||
setStateDialog(() {
|
||
_errorMessage = msg;
|
||
isLoadingAuth = false;
|
||
});
|
||
} catch (e) {
|
||
setStateDialog(() {
|
||
_errorMessage = "Errore imprevisto: $e";
|
||
isLoadingAuth = false;
|
||
});
|
||
}
|
||
}
|
||
|
||
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 Nome e Password.\nTi serviranno per recuperare gli XP!', style: getSharedTextStyle(themeType, TextStyle(color: inkColor.withOpacity(0.8), fontSize: 13)), 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("💡 Nota: Non serve una vera email. 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: theme.background, borderRadius: BorderRadius.circular(25), border: Border.all(color: theme.playerBlue.withOpacity(0.5), width: 2), boxShadow: [BoxShadow(color: theme.playerBlue.withOpacity(0.3), blurRadius: 20, spreadRadius: 5)]),
|
||
child: 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: theme.text, fontWeight: FontWeight.w900, fontSize: 20, letterSpacing: 1.5)), textAlign: TextAlign.center),
|
||
const SizedBox(height: 10),
|
||
Text('Scegli Nome e Password.\nTi serviranno per recuperare gli XP!', style: getSharedTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.8), fontSize: 13)), textAlign: TextAlign.center),
|
||
const SizedBox(height: 15),
|
||
TextField(
|
||
controller: nameController, textCapitalization: TextCapitalization.characters, textAlign: TextAlign.center, maxLength: 8,
|
||
style: getSharedTextStyle(themeType, TextStyle(color: theme.text, fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 4)),
|
||
decoration: InputDecoration(
|
||
hintText: loc.nameHint,
|
||
hintStyle: getSharedTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.3), letterSpacing: 4)),
|
||
filled: true, fillColor: theme.text.withOpacity(0.05), counterText: "",
|
||
enabledBorder: OutlineInputBorder(borderSide: BorderSide(color: theme.gridLine.withOpacity(0.5), width: 2), borderRadius: BorderRadius.circular(15)),
|
||
focusedBorder: OutlineInputBorder(borderSide: BorderSide(color: theme.playerBlue, width: 3), borderRadius: BorderRadius.circular(15))
|
||
),
|
||
),
|
||
const SizedBox(height: 10),
|
||
TextField(
|
||
controller: passController, obscureText: _obscurePassword, textAlign: TextAlign.center, maxLength: 20,
|
||
style: getSharedTextStyle(themeType, TextStyle(color: theme.text, fontSize: 20, fontWeight: FontWeight.bold, letterSpacing: 8)),
|
||
decoration: InputDecoration(
|
||
hintText: "PASSWORD",
|
||
hintStyle: getSharedTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.3), letterSpacing: 4)),
|
||
filled: true, fillColor: theme.text.withOpacity(0.05), counterText: "",
|
||
enabledBorder: OutlineInputBorder(borderSide: BorderSide(color: theme.gridLine.withOpacity(0.5), width: 2), borderRadius: BorderRadius.circular(15)),
|
||
focusedBorder: OutlineInputBorder(borderSide: BorderSide(color: theme.playerBlue, width: 3), borderRadius: BorderRadius.circular(15)),
|
||
suffixIcon: IconButton(
|
||
icon: Icon(_obscurePassword ? Icons.visibility : Icons.visibility_off, color: theme.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("💡 Nota: Non serve una vera email. Usa una password facile da ricordare!", style: getSharedTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.6), fontSize: 11, height: 1.3)), textAlign: TextAlign.center),
|
||
const SizedBox(height: 20),
|
||
isLoadingAuth
|
||
? CircularProgressIndicator(color: theme.playerBlue)
|
||
: Row(
|
||
children: [
|
||
Expanded(
|
||
child: SizedBox(
|
||
height: 45,
|
||
child: ElevatedButton(
|
||
style: ElevatedButton.styleFrom(backgroundColor: theme.text.withOpacity(0.1), foregroundColor: theme.text, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), side: BorderSide(color: theme.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: theme.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 Dialog(backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.all(20), child: dialogContent);
|
||
},
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
void _showMatchSetupDialog(bool isVsCPU) {
|
||
int localRadius = 4; ArenaShape localShape = ArenaShape.classic; bool localTimeMode = true;
|
||
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<ThemeManager>();
|
||
final theme = themeManager.currentColors; final themeType = themeManager.currentThemeType;
|
||
Color inkColor = const Color(0xFF111122);
|
||
|
||
return StatefulBuilder(
|
||
builder: (context, setStateDialog) {
|
||
Widget dialogContent = themeType == AppThemeType.doodle
|
||
? Transform.rotate(
|
||
angle: 0.015,
|
||
child: CustomPaint(
|
||
painter: DoodleBackgroundPainter(fillColor: Colors.white.withOpacity(0.95), strokeColor: inkColor, seed: 200),
|
||
child: SingleChildScrollView(
|
||
physics: const BouncingScrollPhysics(),
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(25.0),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
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 dimensioni si adatteranno alla tua bravura!", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 13, color: inkColor.withOpacity(0.8), height: 1.4))), const SizedBox(height: 25),
|
||
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),
|
||
NeonTimeSwitch(isTimeMode: localTimeMode, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localTimeMode = !localTimeMode)), const SizedBox(height: 35),
|
||
|
||
Transform.rotate(
|
||
angle: -0.02,
|
||
child: GestureDetector(
|
||
onTap: () { Navigator.pop(ctx); context.read<GameController>().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 dimensioni si adatteranno alla tua bravura!", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 13, color: theme.text.withOpacity(0.7), height: 1.4))), const SizedBox(height: 20),
|
||
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),
|
||
NeonTimeSwitch(isTimeMode: localTimeMode, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localTimeMode = !localTimeMode)), 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<GameController>().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);
|
||
},
|
||
);
|
||
}
|
||
);
|
||
}
|
||
|
||
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: _showNameDialog,
|
||
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))),
|
||
Text("LIV. $playerLevel", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.8) : theme.playerBlue, fontWeight: FontWeight.bold, fontSize: 11))),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||
decoration: _glassBoxDecoration(theme, themeType),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
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))),
|
||
const SizedBox(width: 12),
|
||
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))),
|
||
|
||
const SizedBox(width: 12),
|
||
Container(width: 1, height: 20, color: (themeType == AppThemeType.doodle ? inkColor : Colors.white).withOpacity(0.2)),
|
||
const SizedBox(width: 12),
|
||
|
||
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<ThemeManager>();
|
||
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: () {
|
||
if (playerName.toUpperCase() == 'PAOLO') {
|
||
_debugTapCount++;
|
||
if (_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)))
|
||
);
|
||
} else if (_debugTapCount >= 7) {
|
||
_debugTapCount = 0;
|
||
Navigator.push(context, MaterialPageRoute(builder: (_) => const AdminScreen()));
|
||
}
|
||
}
|
||
},
|
||
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)]
|
||
))
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
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: (_) => const 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: () => _showMatchSetupDialog(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: () => _showMatchSetupDialog(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) => const LeaderboardDialog()))),
|
||
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: (_) => const 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: (_) => const 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: () => _showMatchSetupDialog(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: () => _showMatchSetupDialog(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) => const LeaderboardDialog()), 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: (_) => const 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 (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 (themeType == AppThemeType.doodle)
|
||
Positioned.fill(
|
||
child: CustomPaint(
|
||
painter: FullScreenGridPainter(Colors.blue.withOpacity(0.15)),
|
||
),
|
||
),
|
||
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),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
// ===========================================================================
|
||
// 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 '../../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/painters.dart';
|
||
import '../../widgets/cyber_border.dart'; // <--- ECCO L'IMPORT MANCANTE!
|
||
import 'lobby_widgets.dart';
|
||
|
||
class LobbyScreen extends StatefulWidget {
|
||
final String? initialRoomCode;
|
||
|
||
const LobbyScreen({super.key, this.initialRoomCode});
|
||
|
||
@override
|
||
State<LobbyScreen> createState() => _LobbyScreenState();
|
||
}
|
||
|
||
class _LobbyScreenState extends State<LobbyScreen> 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;
|
||
bool _isTimeMode = true;
|
||
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.paused || state == AppLifecycleState.detached) {
|
||
_cleanupGhostRoom();
|
||
}
|
||
}
|
||
|
||
void _cleanupGhostRoom() {
|
||
if (_myRoomCode != null && !_roomStarted) {
|
||
FirebaseFirestore.instance.collection('games').doc(_myRoomCode).delete();
|
||
_myRoomCode = null;
|
||
}
|
||
}
|
||
|
||
Future<void> _createRoom() async {
|
||
if (_isLoading) return;
|
||
setState(() => _isLoading = true);
|
||
|
||
try {
|
||
String code = await _multiplayerService.createGameRoom(
|
||
_selectedRadius, _playerName, _selectedShape.name, _isTimeMode, 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<void> _createRoomAndInvite(String targetUid, String targetName) async {
|
||
if (_isLoading) return;
|
||
setState(() => _isLoading = true);
|
||
|
||
try {
|
||
String code = await _multiplayerService.createGameRoom(
|
||
_selectedRadius, _playerName, _selectedShape.name, _isTimeMode, 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<void> _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<String, dynamic>? roomData = await _multiplayerService.joinGameRoom(code, _playerName);
|
||
|
||
if (!mounted) return;
|
||
setState(() => _isLoading = false);
|
||
|
||
if (roomData != null) {
|
||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Stanza trovata! Partita in avvio..."), backgroundColor: Colors.green));
|
||
|
||
int hostRadius = roomData['radius'] ?? 4;
|
||
String shapeStr = roomData['shape'] ?? 'classic';
|
||
ArenaShape hostShape = ArenaShape.values.firstWhere((e) => e.name == shapeStr, orElse: () => ArenaShape.classic);
|
||
bool hostTimeMode = roomData['timeMode'] ?? true;
|
||
|
||
context.read<GameController>().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<ThemeManager>();
|
||
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: (context) {
|
||
final theme = context.watch<ThemeManager>().currentColors;
|
||
final themeType = context.read<ThemeManager>().currentThemeType;
|
||
|
||
Widget dialogContent = Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
CircularProgressIndicator(color: theme.playerRed), const SizedBox(height: 25),
|
||
Text("CODICE STANZA", style: getLobbyTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.text.withOpacity(0.6), letterSpacing: 2))),
|
||
Text(code, style: getLobbyTextStyle(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!" : "Condividi link", textAlign: TextAlign.center, style: getLobbyTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.w900, fontSize: 18))),
|
||
const SizedBox(height: 8),
|
||
Text(_isPublicRoom ? "Aspettiamo che uno sfidante si unisca dalla lobby pubblica." : "Condividi il codice. La partita inizierà appena si unirà.", textAlign: TextAlign.center, style: getLobbyTextStyle(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<DocumentSnapshot>(
|
||
stream: _multiplayerService.listenToRoom(code),
|
||
builder: (context, snapshot) {
|
||
if (snapshot.hasData && snapshot.data!.exists) {
|
||
var data = snapshot.data!.data() as Map<String, dynamic>;
|
||
if (data['status'] == 'playing') {
|
||
_roomStarted = true;
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
Navigator.pop(context);
|
||
context.read<GameController>().startNewGame(_selectedRadius, isOnline: true, roomCode: code, isHost: true, shape: _selectedShape, timeMode: _isTimeMode);
|
||
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const GameScreen()));
|
||
});
|
||
}
|
||
}
|
||
|
||
return PopScope(
|
||
canPop: false,
|
||
onPopInvoked: (didPop) {
|
||
if (didPop) return;
|
||
_cleanupGhostRoom();
|
||
Navigator.pop(context);
|
||
},
|
||
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(context);
|
||
},
|
||
child: Text("ANNULLA", style: getLobbyTextStyle(themeType, TextStyle(color: Colors.red, fontWeight: FontWeight.w900, fontSize: 20, letterSpacing: 2.0, shadows: themeType == AppThemeType.doodle ? [] : [const Shadow(color: Colors.black, blurRadius: 2)]))),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final themeManager = context.watch<ThemeManager>();
|
||
final themeType = themeManager.currentThemeType;
|
||
final theme = themeManager.currentColors;
|
||
|
||
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;
|
||
|
||
Widget hostPanel = Transform.rotate(
|
||
angle: themeType == AppThemeType.doodle ? 0.01 : 0,
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 15),
|
||
decoration: BoxDecoration(
|
||
color: themeType == AppThemeType.cyberpunk ? Colors.black.withOpacity(0.85) : (themeType == AppThemeType.doodle ? Colors.white.withOpacity(0.5) : Colors.transparent),
|
||
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("IMPOSTAZIONI STANZA", 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("FORMA ARENA", 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("GRANDEZZA", 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("TEMPO E OPZIONI", 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: [
|
||
Expanded(child: NeonTimeSwitch(isTimeMode: _isTimeMode, theme: theme, themeType: themeType, onTap: () => setState(() => _isTimeMode = !_isTimeMode))),
|
||
],
|
||
),
|
||
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("MULTIPLAYER", 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: "AVVIA", color: theme.playerRed, onTap: _createRoom, theme: theme, themeType: themeType),
|
||
),
|
||
const SizedBox(width: 10),
|
||
Expanded(
|
||
child: NeonActionButton(label: "ANNULLA", color: Colors.grey.shade600, onTap: () => setState(() => _isCreatingRoom = false), theme: theme, themeType: themeType),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
)
|
||
: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
NeonActionButton(label: "CREA PARTITA", 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("OPPURE", 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: "CODICE", 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: "UNISCITI", 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("LOBBY PUBBLICA", 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<QuerySnapshot>(
|
||
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("Nessuna stanza pubblica al momento.\nCreane una tu!", 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<String, dynamic>;
|
||
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("Nessuna stanza pubblica al momento.\nCreane una tu!", 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<String, dynamic>)['createdAt'] as Timestamp?;
|
||
Timestamp? tB = (b.data() as Map<String, dynamic>)['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, dynamic>;
|
||
String host = data['hostName'] ?? 'Sconosciuto';
|
||
int r = data['radius'] ?? 4;
|
||
String shapeStr = data['shape'] ?? 'classic';
|
||
bool time = data['timeMode'] ?? true;
|
||
|
||
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("Stanza di $host", style: getLobbyTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 18))),
|
||
const SizedBox(height: 6),
|
||
Text("Raggio: $r • $prettyShape • ${time ? 'A Tempo' : 'Relax'}", style: getLobbyTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.6), fontSize: 12))),
|
||
],
|
||
),
|
||
),
|
||
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("ENTRA", 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),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
// ===========================================================================
|
||
// 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<SettingsScreen> createState() => _SettingsScreenState();
|
||
}
|
||
|
||
class _SettingsScreenState extends State<SettingsScreen> {
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final themeManager = context.watch<ThemeManager>();
|
||
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<ThemeManager>();
|
||
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<BoxShadow> 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<AnimatedCyberBorder> createState() => _AnimatedCyberBorderState();
|
||
}
|
||
|
||
class _AnimatedCyberBorderState extends State<AnimatedCyberBorder> with SingleTickerProviderStateMixin {
|
||
late AnimationController _controller;
|
||
@override
|
||
void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: const Duration(seconds: 3))..repeat(); }
|
||
@override
|
||
void dispose() { _controller.dispose(); super.dispose(); }
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = context.watch<ThemeManager>().currentColors;
|
||
return AnimatedBuilder(
|
||
animation: _controller,
|
||
builder: (context, child) {
|
||
return CustomPaint(
|
||
painter: CyberBorderPainter(animationValue: _controller.value, color1: theme.playerBlue, color2: theme.playerRed),
|
||
child: Container(
|
||
decoration: BoxDecoration(color: theme.background.withOpacity(0.9), borderRadius: BorderRadius.circular(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';
|
||
|
||
class GameOverDialog extends StatelessWidget {
|
||
const GameOverDialog({super.key});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final game = context.read<GameController>();
|
||
final themeManager = context.read<ThemeManager>();
|
||
final theme = themeManager.currentColors;
|
||
final themeType = themeManager.currentThemeType;
|
||
|
||
int red = game.board.scoreRed;
|
||
int blue = game.board.scoreBlue;
|
||
|
||
bool playerBeatCPU = game.isVsCPU && red > blue;
|
||
|
||
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 = theme.text;
|
||
}
|
||
|
||
return AlertDialog(
|
||
backgroundColor: theme.background,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(20),
|
||
side: BorderSide(color: winnerColor.withOpacity(0.5), width: 2),
|
||
),
|
||
title: Text("FINE PARTITA", textAlign: TextAlign.center, style: TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 22)),
|
||
content: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Text(winnerText, textAlign: TextAlign.center, style: TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: winnerColor)),
|
||
const SizedBox(height: 20),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||
decoration: BoxDecoration(
|
||
color: theme.text.withOpacity(0.05),
|
||
borderRadius: BorderRadius.circular(15),
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Text("$nameRed: $red", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerRed)),
|
||
Text(" - ", style: TextStyle(fontSize: 18, color: theme.text)),
|
||
Text("$nameBlue: $blue", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerBlue)),
|
||
],
|
||
),
|
||
),
|
||
|
||
if (game.isVsCPU) ...[
|
||
const SizedBox(height: 15),
|
||
Text("Difficoltà CPU: Livello ${game.cpuLevel}", style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: theme.text.withOpacity(0.7))),
|
||
]
|
||
],
|
||
),
|
||
actionsPadding: const EdgeInsets.only(left: 20, right: 20, bottom: 20, top: 10),
|
||
actionsAlignment: MainAxisAlignment.center,
|
||
actions: [
|
||
Column(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
if (playerBeatCPU)
|
||
ElevatedButton(
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: winnerColor,
|
||
foregroundColor: Colors.white,
|
||
padding: const EdgeInsets.symmetric(vertical: 15),
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
||
elevation: 5,
|
||
),
|
||
onPressed: () {
|
||
Navigator.pop(context);
|
||
game.increaseLevelAndRestart();
|
||
},
|
||
child: const Text("PROSSIMO LIVELLO ➔", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||
)
|
||
else if (game.isOnline)
|
||
ElevatedButton(
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: winnerColor == theme.text ? theme.playerBlue : winnerColor,
|
||
foregroundColor: Colors.white,
|
||
padding: const EdgeInsets.symmetric(vertical: 15),
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
||
elevation: 5,
|
||
),
|
||
onPressed: () {
|
||
Navigator.pop(context);
|
||
if (game.board.isGameOver) {
|
||
game.requestRematch();
|
||
}
|
||
},
|
||
child: const Text("RIGIOCA ONLINE", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 1.5)),
|
||
)
|
||
else
|
||
ElevatedButton(
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: winnerColor == theme.text ? theme.playerBlue : winnerColor,
|
||
foregroundColor: Colors.white,
|
||
padding: const EdgeInsets.symmetric(vertical: 15),
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
||
elevation: 5,
|
||
),
|
||
onPressed: () {
|
||
Navigator.pop(context);
|
||
game.startNewGame(game.board.radius, vsCPU: game.isVsCPU);
|
||
},
|
||
child: const Text("RIGIOCA", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 2)),
|
||
),
|
||
|
||
const SizedBox(height: 12),
|
||
|
||
OutlinedButton(
|
||
style: OutlinedButton.styleFrom(
|
||
foregroundColor: theme.text,
|
||
side: BorderSide(color: theme.text.withOpacity(0.3), width: 2),
|
||
padding: const EdgeInsets.symmetric(vertical: 15),
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
||
),
|
||
onPressed: () {
|
||
if (game.isOnline) {
|
||
game.disconnectOnlineGame();
|
||
}
|
||
Navigator.pop(context);
|
||
Navigator.pop(context);
|
||
},
|
||
child: Text("TORNA AL MENU", style: TextStyle(fontWeight: FontWeight.bold, color: theme.text, fontSize: 14, letterSpacing: 1.5)),
|
||
),
|
||
],
|
||
)
|
||
],
|
||
);
|
||
}
|
||
}
|
||
// ===========================================================================
|
||
// 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;
|
||
} |