Auto-sync: 20260301_205905

This commit is contained in:
Paolo 2026-03-01 20:59:06 +01:00
parent a3d70e39b2
commit 2d6e86db3c
20 changed files with 1656 additions and 1019 deletions

BIN
.DS_Store vendored

Binary file not shown.

BIN
ios/.DS_Store vendored

Binary file not shown.

View file

@ -473,7 +473,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_TEAM = 2BX6QRR7GG; DEVELOPMENT_TEAM = 2BX6QRR7GG;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@ -482,7 +482,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.0; MARKETING_VERSION = 1.0.2;
PRODUCT_BUNDLE_IDENTIFIER = com.sanza.tetraq; PRODUCT_BUNDLE_IDENTIFIER = com.sanza.tetraq;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@ -658,7 +658,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_TEAM = 2BX6QRR7GG; DEVELOPMENT_TEAM = 2BX6QRR7GG;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@ -667,7 +667,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.0; MARKETING_VERSION = 1.0.2;
PRODUCT_BUNDLE_IDENTIFIER = com.sanza.tetraq; PRODUCT_BUNDLE_IDENTIFIER = com.sanza.tetraq;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@ -683,7 +683,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_TEAM = 2BX6QRR7GG; DEVELOPMENT_TEAM = 2BX6QRR7GG;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@ -692,7 +692,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.0; MARKETING_VERSION = 1.0.2;
PRODUCT_BUNDLE_IDENTIFIER = com.sanza.tetraq; PRODUCT_BUNDLE_IDENTIFIER = com.sanza.tetraq;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";

BIN
lib/.DS_Store vendored

Binary file not shown.

View file

@ -1,6 +1,11 @@
import 'package:flutter/material.dart'; // ===========================================================================
// FILE: lib/core/app_colors.dart
// ===========================================================================
enum AppThemeType { minimal, doodle, cyberpunk, wood } import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
enum AppThemeType { minimal, doodle, cyberpunk, wood, arcade, grimorio }
class ThemeColors { class ThemeColors {
final Color background; final Color background;
@ -29,22 +34,24 @@ class AppColors {
playerRed: Color(0xFFD32F2F), playerBlue: Color(0xFF1565C0), text: Color(0xFF37474F), playerRed: Color(0xFFD32F2F), playerBlue: Color(0xFF1565C0), text: Color(0xFF37474F),
); );
// --- TEMA CYBERPUNK AGGIORNATO ---
static const ThemeColors cyberpunk = ThemeColors( static const ThemeColors cyberpunk = ThemeColors(
background: Color(0xFF0A001A), // Sfondo notte profonda background: Color(0xFF0A001A), gridLine: Color(0xFF6200EA),
gridLine: Color(0xFF6200EA), // Viola scuro elettrico (non fa confusione con le mosse) playerRed: Color(0xFFFF007F), playerBlue: Color(0xFF69F0AE), text: Color(0xFFFFFFFF),
playerRed: Color(0xFFFF007F), // Rosa Neon (invariato)
playerBlue: Color(0xFF69F0AE), // Verde Fluo brillante! (Green Accent)
text: Color(0xFFFFFFFF),
); );
// --- TEMA LEGNO POTENZIATO ---
static const ThemeColors wood = ThemeColors( static const ThemeColors wood = ThemeColors(
background: Color(0xFF905D3B), // Marrone caldo e ricco (vero legno) background: Color(0xFF905D3B), gridLine: Color(0xFF4A301E),
gridLine: Color(0xFF4A301E), // Marrone scurissimo per i solchi playerRed: Color(0xFFE53935), playerBlue: Color(0xFF29B6F6), text: Color(0xFFFBE9E7),
playerRed: Color(0xFFE53935), // Rosso acceso per i fiammiferi );
playerBlue: Color(0xFF29B6F6), // Azzurro acceso per i fiammiferi
text: Color(0xFFFBE9E7), // Panna chiaro per contrastare lo scuro static 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: Color(0xFF8D6E63),
playerRed: Color(0xFFE91E63), playerBlue: Color(0xFF4FC3F7), text: Color(0xFFFFF3E0),
); );
static ThemeColors getTheme(AppThemeType type) { static ThemeColors getTheme(AppThemeType type) {
@ -53,6 +60,74 @@ class AppColors {
case AppThemeType.doodle: return doodle; case AppThemeType.doodle: return doodle;
case AppThemeType.cyberpunk: return cyberpunk; case AppThemeType.cyberpunk: return cyberpunk;
case AppThemeType.wood: return wood; case AppThemeType.wood: return wood;
case AppThemeType.arcade: return arcade;
case AppThemeType.grimorio: return grimorio;
} }
} }
} }
class ThemeIcons {
static IconData gold(AppThemeType type) {
switch (type) {
case AppThemeType.minimal: return Icons.star_rounded;
case AppThemeType.doodle: return FontAwesomeIcons.star;
case AppThemeType.wood: return FontAwesomeIcons.gem;
case AppThemeType.cyberpunk: return FontAwesomeIcons.microchip;
case AppThemeType.arcade: return FontAwesomeIcons.coins;
case AppThemeType.grimorio: return FontAwesomeIcons.crown;
}
}
static IconData bomb(AppThemeType type) {
switch (type) {
case AppThemeType.minimal: return Icons.mood_bad_rounded;
case AppThemeType.doodle: return FontAwesomeIcons.virus;
case AppThemeType.wood: return FontAwesomeIcons.fire;
case AppThemeType.cyberpunk: return FontAwesomeIcons.bug;
case AppThemeType.arcade: return FontAwesomeIcons.ghost;
case AppThemeType.grimorio: return FontAwesomeIcons.hatWizard;
}
}
static IconData swap(AppThemeType type) {
switch (type) {
case AppThemeType.minimal: return Icons.sync_rounded;
case AppThemeType.doodle: return FontAwesomeIcons.arrowsRotate;
case AppThemeType.wood: return FontAwesomeIcons.rightLeft;
case AppThemeType.cyberpunk: return FontAwesomeIcons.networkWired;
case AppThemeType.arcade: return FontAwesomeIcons.shuffle;
case AppThemeType.grimorio: return FontAwesomeIcons.hurricane;
}
}
static IconData joker(AppThemeType type) {
switch (type) {
case AppThemeType.minimal: return Icons.sentiment_satisfied_alt;
case AppThemeType.doodle: return FontAwesomeIcons.faceSmileBeam;
case AppThemeType.wood: return FontAwesomeIcons.key;
case AppThemeType.cyberpunk: return FontAwesomeIcons.robot;
case AppThemeType.arcade: return FontAwesomeIcons.gamepad;
case AppThemeType.grimorio: return FontAwesomeIcons.masksTheater;
}
}
static IconData block(AppThemeType type) {
switch (type) {
case AppThemeType.minimal: return Icons.block;
case AppThemeType.doodle: return FontAwesomeIcons.squareXmark;
case AppThemeType.wood: return FontAwesomeIcons.ban;
case AppThemeType.cyberpunk: return FontAwesomeIcons.shieldHalved;
case AppThemeType.arcade: return FontAwesomeIcons.powerOff;
case AppThemeType.grimorio: return FontAwesomeIcons.meteor;
}
}
// --- NUOVE ICONE ---
static IconData ice(AppThemeType type) {
return FontAwesomeIcons.snowflake;
}
static IconData multiplier(AppThemeType type) {
return FontAwesomeIcons.bolt;
}
}

View file

@ -5,7 +5,6 @@
import 'dart:math'; import 'dart:math';
import '../models/game_board.dart'; import '../models/game_board.dart';
// Modificato per tracciare anche l'effetto Swap
class _ClosureResult { class _ClosureResult {
final bool closesSomething; final bool closesSomething;
final int netValue; final int netValue;
@ -25,7 +24,6 @@ class AIEngine {
bool beSmart = random.nextDouble() < smartChance; bool beSmart = random.nextDouble() < smartChance;
// Calcolo punteggi attuali per valutare lo SWAP
int myScore = board.currentPlayer == Player.red ? board.scoreRed : board.scoreBlue; int myScore = board.currentPlayer == Player.red ? board.scoreRed : board.scoreBlue;
int oppScore = board.currentPlayer == Player.red ? board.scoreBlue : board.scoreRed; int oppScore = board.currentPlayer == Player.red ? board.scoreBlue : board.scoreRed;
@ -36,15 +34,12 @@ class AIEngine {
var result = _checkClosure(board, line); var result = _checkClosure(board, line);
if (result.closesSomething) { if (result.closesSomething) {
if (result.causesSwap) { if (result.causesSwap) {
// SE L'IA STA PERDENDO -> Lo Swap è un'ottima mossa!
// SE L'IA STA VINCENDO O PAREGGIANDO -> Lo Swap è una pessima mossa!
if (myScore < oppScore) { if (myScore < oppScore) {
goodClosingMoves.add(line); goodClosingMoves.add(line);
} else { } else {
badClosingMoves.add(line); badClosingMoves.add(line);
} }
} else { } else {
// Normale valutazione dei punti
if (result.netValue >= 0) { if (result.netValue >= 0) {
goodClosingMoves.add(line); goodClosingMoves.add(line);
} else { } else {
@ -112,7 +107,19 @@ class AIEngine {
if (linesCount == 4) { if (linesCount == 4) {
closesSomething = true; closesSomething = true;
netValue += box.value;
// 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; if (box.type == BoxType.swap) causesSwap = true;
} }
} }
@ -132,17 +139,30 @@ class AIEngine {
if (box.right.owner != Player.none) currentLinesCount++; if (box.right.owner != Player.none) currentLinesCount++;
if (currentLinesCount == 2) { if (currentLinesCount == 2) {
// La Bomba è sempre sicura da lasciare all'avversario
if (box.type == BoxType.bomb) { // 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; continue;
} }
// IL TRANELLO PERFETTO: Se stiamo PERDENDO, lasciare un quadrato Swap a 3 lati
// costringerà l'avversario a prenderlo e a ridarci la sua vittoria!
if (box.type == BoxType.swap) { if (box.type == BoxType.swap) {
if (myScore < oppScore) { if (myScore < oppScore) {
continue; // È sicuro e strategico! continue;
} else { } else {
return false; // Se stiamo vincendo non dobbiamo MAI lasciare uno Swap! return false;
} }
} }

View file

@ -7,7 +7,10 @@ import 'dart:math';
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import '../models/game_board.dart'; import '../models/game_board.dart';
export '../models/game_board.dart';
import 'ai_engine.dart'; import 'ai_engine.dart';
import '../services/audio_service.dart'; import '../services/audio_service.dart';
import '../services/storage_service.dart'; import '../services/storage_service.dart';
@ -30,22 +33,29 @@ class GameController extends ChangeNotifier {
Timer? _blitzTimer; Timer? _blitzTimer;
int timeLeft = 15; int timeLeft = 15;
final int maxTime = 15; final int maxTime = 15;
bool isTimeMode = true; bool isTimeMode = true;
String effectText = ''; String effectText = '';
Color effectColor = Colors.transparent; Color effectColor = Colors.transparent;
Timer? _effectTimer; Timer? _effectTimer;
// --- VARIABILI EMOJI E RIVINCITA ---
String? myReaction; String? myReaction;
String? opponentReaction; String? opponentReaction;
Timer? _myReactionTimer; Timer? _myReactionTimer;
Timer? _oppReactionTimer; Timer? _oppReactionTimer;
Timestamp? _lastOpponentReactionTime;
bool rematchRequested = false; bool rematchRequested = false;
bool opponentWantsRematch = false; bool opponentWantsRematch = false;
int lastMatchXP = 0;
Player get myPlayer => isHost ? Player.red : Player.blue; 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; bool get isGameOver => board.isGameOver;
int cpuLevel = 1; int cpuLevel = 1;
@ -69,12 +79,19 @@ class GameController extends ChangeNotifier {
_effectTimer?.cancel(); _effectTimer?.cancel();
effectText = ''; effectText = '';
_hasSavedResult = false; _hasSavedResult = false;
lastMatchXP = 0;
myReaction = null; myReaction = null;
opponentReaction = null; opponentReaction = null;
_lastOpponentReactionTime = null;
rematchRequested = false; rematchRequested = false;
opponentWantsRematch = false; opponentWantsRematch = false;
isSetupPhase = true;
myJokerPlaced = false;
oppJokerPlaced = false;
jokerTurn = Player.red;
this.isVsCPU = vsCPU; this.isVsCPU = vsCPU;
this.isOnline = isOnline; this.isOnline = isOnline;
this.roomCode = roomCode; this.roomCode = roomCode;
@ -85,8 +102,6 @@ class GameController extends ChangeNotifier {
int levelToUse = isOnline ? (currentMatchLevel == 1 ? 2 : currentMatchLevel) : cpuLevel; int levelToUse = isOnline ? (currentMatchLevel == 1 ? 2 : currentMatchLevel) : cpuLevel;
board = GameBoard(radius: radius, level: levelToUse, seed: currentSeed, shape: onlineShape); board = GameBoard(radius: radius, level: levelToUse, seed: currentSeed, shape: onlineShape);
// FIX: Assicuriamoci che a inizio partita il turno sia sempre del Rosso!
board.currentPlayer = Player.red; board.currentPlayer = Player.red;
isCPUThinking = false; isCPUThinking = false;
@ -96,11 +111,68 @@ class GameController extends ChangeNotifier {
_listenToOnlineGame(this.roomCode!); _listenToOnlineGame(this.roomCode!);
} }
_startTimer();
notifyListeners(); notifyListeners();
} }
// --- METODI EMOJI E RIVINCITA --- 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) { void sendReaction(String reaction) {
if (!isOnline || roomCode == null) return; if (!isOnline || roomCode == null) return;
MultiplayerService().sendReaction(roomCode!, isHost, reaction); MultiplayerService().sendReaction(roomCode!, isHost, reaction);
@ -118,23 +190,14 @@ class GameController extends ChangeNotifier {
if (isMe) { if (isMe) {
myReaction = reaction; myReaction = reaction;
_myReactionTimer?.cancel(); _myReactionTimer?.cancel();
// MODIFICA: Timer impostato a 4 secondi _myReactionTimer = Timer(const Duration(seconds: 4), () { myReaction = null; notifyListeners(); });
_myReactionTimer = Timer(const Duration(seconds: 4), () {
myReaction = null;
notifyListeners();
});
} else { } else {
opponentReaction = reaction; opponentReaction = reaction;
_oppReactionTimer?.cancel(); _oppReactionTimer?.cancel();
// MODIFICA: Timer impostato a 4 secondi _oppReactionTimer = Timer(const Duration(seconds: 4), () { opponentReaction = null; notifyListeners(); });
_oppReactionTimer = Timer(const Duration(seconds: 4), () {
opponentReaction = null;
notifyListeners();
});
} }
notifyListeners(); notifyListeners();
} }
// --------------------------------
void triggerSpecialEffect(String text, Color color) { void triggerSpecialEffect(String text, Color color) {
effectText = text; effectText = text;
@ -144,20 +207,53 @@ class GameController extends ChangeNotifier {
_effectTimer = Timer(const Duration(milliseconds: 1200), () { effectText = ''; notifyListeners(); }); _effectTimer = Timer(const Duration(milliseconds: 1200), () { effectText = ''; notifyListeners(); });
} }
void _playEffects(List<Box> newClosed, {required bool isOpponent}) { 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) { if (newClosed.isEmpty) {
AudioService.instance.playLineSfx(_activeTheme); AudioService.instance.playLineSfx(_activeTheme);
if (!isOpponent) HapticFeedback.lightImpact(); if (!isOpponent) HapticFeedback.lightImpact();
} else { } 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 isGold = newClosed.any((b) => b.type == BoxType.gold);
bool isBomb = newClosed.any((b) => b.type == BoxType.bomb); bool isBomb = newClosed.any((b) => b.type == BoxType.bomb);
bool isSwap = newClosed.any((b) => b.type == BoxType.swap); bool isSwap = newClosed.any((b) => b.type == BoxType.swap);
bool isMultiplier = newClosed.any((b) => b.type == BoxType.multiplier);
if (isSwap) { if (isSwap) {
// Usa temporaneamente playBonusSfx per lo swap, o un suono dedicato se lo aggiungerai
AudioService.instance.playBonusSfx(); AudioService.instance.playBonusSfx();
triggerSpecialEffect("SCAMBIO!", Colors.purpleAccent); triggerSpecialEffect("SCAMBIO!", Colors.purpleAccent);
HapticFeedback.heavyImpact(); HapticFeedback.heavyImpact();
} else if (isMultiplier) {
AudioService.instance.playBonusSfx();
triggerSpecialEffect("MOLTIPLICATORE x2!", Colors.yellowAccent);
HapticFeedback.heavyImpact();
} else if (isGold) { } else if (isGold) {
AudioService.instance.playBonusSfx(); triggerSpecialEffect("+2", Colors.amber); HapticFeedback.heavyImpact(); AudioService.instance.playBonusSfx(); triggerSpecialEffect("+2", Colors.amber); HapticFeedback.heavyImpact();
} else if (isBomb) { } else if (isBomb) {
@ -170,40 +266,32 @@ class GameController extends ChangeNotifier {
void _startTimer() { void _startTimer() {
_blitzTimer?.cancel(); _blitzTimer?.cancel();
timeLeft = maxTime; if (isSetupPhase) return;
if (!isTimeMode) { timeLeft = maxTime;
notifyListeners(); if (!isTimeMode) { notifyListeners(); return; }
return;
}
_blitzTimer = Timer.periodic(const Duration(seconds: 1), (timer) { _blitzTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (isGameOver || isCPUThinking) { timer.cancel(); return; } if (isGameOver || isCPUThinking) { timer.cancel(); return; }
if (timeLeft > 0) { if (timeLeft > 0) {
timeLeft--; timeLeft--; notifyListeners();
notifyListeners();
} else { } else {
timer.cancel(); timer.cancel();
if (!isOnline || board.currentPlayer == myPlayer) { if (!isOnline || board.currentPlayer == myPlayer) { _handleTimeOut(); }
_handleTimeOut();
}
} }
}); });
} }
void _handleTimeOut() { void _handleTimeOut() {
if (!isTimeMode) return; if (!isTimeMode || isSetupPhase) return;
if (isOnline) { if (isOnline) {
Line randomMove = AIEngine.getBestMove(board, 5); Line randomMove = AIEngine.getBestMove(board, 5);
handleLineTap(randomMove, _activeTheme, forced: true); handleLineTap(randomMove, _activeTheme, forced: true);
} } else if (isVsCPU && board.currentPlayer == Player.red) {
else if (isVsCPU && board.currentPlayer == Player.red) {
Line randomMove = AIEngine.getBestMove(board, cpuLevel); Line randomMove = AIEngine.getBestMove(board, cpuLevel);
handleLineTap(randomMove, _activeTheme, forced: true); handleLineTap(randomMove, _activeTheme, forced: true);
} } else if (!isVsCPU) {
else if (!isVsCPU) {
Line randomMove = AIEngine.getBestMove(board, 5); Line randomMove = AIEngine.getBestMove(board, 5);
handleLineTap(randomMove, _activeTheme, forced: true); handleLineTap(randomMove, _activeTheme, forced: true);
} }
@ -216,15 +304,12 @@ class GameController extends ChangeNotifier {
_effectTimer?.cancel(); _effectTimer?.cancel();
_myReactionTimer?.cancel(); _myReactionTimer?.cancel();
_oppReactionTimer?.cancel(); _oppReactionTimer?.cancel();
_lastOpponentReactionTime = null;
if (isOnline && roomCode != null) { if (isOnline && roomCode != null) {
FirebaseFirestore.instance.collection('games').doc(roomCode).update({'status': 'abandoned'}).catchError((e) => null); FirebaseFirestore.instance.collection('games').doc(roomCode).update({'status': 'abandoned'}).catchError((e) => null);
} }
isOnline = false; roomCode = null; currentMatchLevel = 1; currentSeed = null;
isOnline = false;
roomCode = null;
currentMatchLevel = 1;
currentSeed = null;
} }
@override @override
@ -233,7 +318,6 @@ class GameController extends ChangeNotifier {
void _listenToOnlineGame(String code) { void _listenToOnlineGame(String code) {
_onlineSubscription = FirebaseFirestore.instance.collection('games').doc(code).snapshots().listen((doc) { _onlineSubscription = FirebaseFirestore.instance.collection('games').doc(code).snapshots().listen((doc) {
if (!doc.exists) return; if (!doc.exists) return;
var data = doc.data() as Map<String, dynamic>; var data = doc.data() as Map<String, dynamic>;
onlineHostName = data['hostName'] ?? "ROSSO"; onlineHostName = data['hostName'] ?? "ROSSO";
@ -243,13 +327,16 @@ class GameController extends ChangeNotifier {
opponentLeft = true; notifyListeners(); return; opponentLeft = true; notifyListeners(); return;
} }
// --- ASCOLTO EMOJI E RIVINCITA ---
String? p1React = data['p1_reaction']; String? p1React = data['p1_reaction'];
Timestamp? p1Time = data['p1_reaction_time'] as Timestamp?;
String? p2React = data['p2_reaction']; String? p2React = data['p2_reaction'];
Timestamp? p2Time = data['p2_reaction_time'] as Timestamp?;
if (isHost && p2React != null && p2React != opponentReaction) { if (isHost && p2React != null && p2Time != null && p2Time != _lastOpponentReactionTime) {
_lastOpponentReactionTime = p2Time;
_showReaction(false, p2React); _showReaction(false, p2React);
} else if (!isHost && p1React != null && p1React != opponentReaction) { } else if (!isHost && p1React != null && p1Time != null && p1Time != _lastOpponentReactionTime) {
_lastOpponentReactionTime = p1Time;
_showReaction(false, p1React); _showReaction(false, p1React);
} }
@ -257,12 +344,10 @@ class GameController extends ChangeNotifier {
bool p2Rematch = data['p2_rematch'] ?? false; bool p2Rematch = data['p2_rematch'] ?? false;
opponentWantsRematch = isHost ? p2Rematch : p1Rematch; opponentWantsRematch = isHost ? p2Rematch : p1Rematch;
// FIX: Rilevamento inizio nuova partita dopo rivincita
// Se il server ha resettato (status: playing, mosse: vuote) e noi avevamo chiesto rivincita
if (data['status'] == 'playing' && (data['moves'] as List).isEmpty && rematchRequested) { if (data['status'] == 'playing' && (data['moves'] as List).isEmpty && rematchRequested) {
currentSeed = data['seed']; currentSeed = data['seed'];
startNewGame(data['radius'], isOnline: true, roomCode: roomCode, isHost: isHost, shape: ArenaShape.values.firstWhere((e) => e.name == data['shape']), timeMode: data['timeMode']); startNewGame(data['radius'], isOnline: true, roomCode: roomCode, isHost: isHost, shape: ArenaShape.values.firstWhere((e) => e.name == data['shape']), timeMode: data['timeMode']);
return; // Evitiamo di processare le mosse vuote return;
} }
if (p1Rematch && p2Rematch && isHost && data['status'] != 'playing') { if (p1Rematch && p2Rematch && isHost && data['status'] != 'playing') {
@ -273,26 +358,34 @@ class GameController extends ChangeNotifier {
ArenaShape newShape = ArenaShape.values[rand.nextInt(ArenaShape.values.length)]; ArenaShape newShape = ArenaShape.values[rand.nextInt(ArenaShape.values.length)];
MultiplayerService().resetMatch(roomCode!, newRadius, newShape.name, newSeed); 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'] ?? []; List<dynamic> moves = data['moves'] ?? [];
int hostLevel = data['matchLevel'] ?? 1; int hostLevel = data['matchLevel'] ?? 1;
int? hostSeed = data['seed']; int? hostSeed = data['seed'];
int hostRadius = data['radius'] ?? board.radius; int hostRadius = data['radius'] ?? board.radius;
String shapeStr = data['shape'] ?? 'classic'; String shapeStr = data['shape'] ?? 'classic';
ArenaShape hostShape = ArenaShape.values.firstWhere((e) => e.name == shapeStr, orElse: () => ArenaShape.classic); ArenaShape hostShape = ArenaShape.values.firstWhere((e) => e.name == shapeStr, orElse: () => ArenaShape.classic);
onlineShape = hostShape; onlineShape = hostShape;
isTimeMode = data['timeMode'] ?? true; isTimeMode = data['timeMode'] ?? true;
// FIX: Non resettare la board se le mosse non sono a 0 e stiamo solo aspettando la rivincita
if (!rematchRequested && (hostLevel > currentMatchLevel || (isOnline && currentSeed == null && hostSeed != null) || (hostSeed != null && hostSeed != currentSeed))) { if (!rematchRequested && (hostLevel > currentMatchLevel || (isOnline && currentSeed == null && hostSeed != null) || (hostSeed != null && hostSeed != currentSeed))) {
currentMatchLevel = hostLevel; currentSeed = hostSeed; currentMatchLevel = hostLevel; currentSeed = hostSeed;
int levelToUse = (currentMatchLevel == 1) ? 2 : currentMatchLevel; int levelToUse = (currentMatchLevel == 1) ? 2 : currentMatchLevel;
board = GameBoard(radius: hostRadius, level: levelToUse, seed: currentSeed, shape: onlineShape); board = GameBoard(radius: hostRadius, level: levelToUse, seed: currentSeed, shape: onlineShape);
board.currentPlayer = Player.red; // FIX Turno Iniziale board.currentPlayer = Player.red;
isCPUThinking = false; notifyListeners(); return; isCPUThinking = false; notifyListeners(); return;
} }
@ -302,7 +395,7 @@ class GameController extends ChangeNotifier {
if (firebaseMovesCount == 0 && localMovesCount > 0 && !rematchRequested) { if (firebaseMovesCount == 0 && localMovesCount > 0 && !rematchRequested) {
int levelToUse = (currentMatchLevel == 1) ? 2 : currentMatchLevel; int levelToUse = (currentMatchLevel == 1) ? 2 : currentMatchLevel;
board = GameBoard(radius: hostRadius, level: levelToUse, seed: currentSeed, shape: onlineShape); board = GameBoard(radius: hostRadius, level: levelToUse, seed: currentSeed, shape: onlineShape);
board.currentPlayer = Player.red; // FIX Turno Iniziale board.currentPlayer = Player.red;
notifyListeners(); return; notifyListeners(); return;
} }
@ -321,25 +414,22 @@ class GameController extends ChangeNotifier {
Player playerFromFirebase = (m['player'] == 'red') ? Player.red : Player.blue; Player playerFromFirebase = (m['player'] == 'red') ? Player.red : Player.blue;
bool isOpponentMove = (playerFromFirebase != myPlayer); bool isOpponentMove = (playerFromFirebase != myPlayer);
List<Box> closedBefore = board.boxes.where((b) => b.owner != Player.none).toList(); 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();
// FIX: Forziamo la mossa usando il giocatore indicato da Firebase
board.playMove(lineToPlay, forcedPlayer: playerFromFirebase); board.playMove(lineToPlay, forcedPlayer: playerFromFirebase);
newMovesApplied = true; newMovesApplied = true;
List<Box> newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList(); List<Box> newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList();
if (isOpponentMove) _playEffects(newClosed, isOpponent: true); 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) { if (newMovesApplied) {
// FIX: Sincronizzazione esplicita del turno basata su Firebase
String expectedTurnStr = data['turn'] ?? 'red'; String expectedTurnStr = data['turn'] ?? 'red';
Player expectedTurn = expectedTurnStr == 'red' ? Player.red : Player.blue; Player expectedTurn = expectedTurnStr == 'red' ? Player.red : Player.blue;
if (!board.isGameOver && board.currentPlayer != expectedTurn) { board.currentPlayer = expectedTurn; }
if (!board.isGameOver && board.currentPlayer != expectedTurn) {
board.currentPlayer = expectedTurn;
}
_startTimer(); _startTimer();
} }
@ -350,17 +440,18 @@ class GameController extends ChangeNotifier {
} }
void handleLineTap(Line line, AppThemeType theme, {bool forced = false}) { void handleLineTap(Line line, AppThemeType theme, {bool forced = false}) {
if ((isCPUThinking || board.isGameOver || opponentLeft) && !forced) return; if ((isSetupPhase || isCPUThinking || board.isGameOver || opponentLeft) && !forced) return;
// Controllo Turno
if (isOnline && board.currentPlayer != myPlayer && !forced) return; if (isOnline && board.currentPlayer != myPlayer && !forced) return;
_activeTheme = theme; _activeTheme = theme;
List<Box> closedBefore = board.boxes.where((b) => b.owner != Player.none).toList(); 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)) { if (board.playMove(line)) {
List<Box> newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList(); List<Box> newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList();
if (!forced) _playEffects(newClosed, isOpponent: false); 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(); _startTimer(); notifyListeners();
@ -369,8 +460,6 @@ class GameController extends ChangeNotifier {
'x1': line.p1.x, 'y1': line.p1.y, 'x2': line.p2.x, 'y2': line.p2.y, 'x1': line.p1.x, 'y1': line.p1.y, 'x2': line.p2.x, 'y2': line.p2.y,
'player': myPlayer == Player.red ? 'red' : 'blue' 'player': myPlayer == Player.red ? 'red' : 'blue'
}; };
// FIX: Invia anche il currentPlayer aggiornato a Firebase per mantenere tutti sincronizzati
String nextTurnStr = board.currentPlayer == Player.red ? 'red' : 'blue'; String nextTurnStr = board.currentPlayer == Player.red ? 'red' : 'blue';
FirebaseFirestore.instance.collection('games').doc(roomCode).update({ FirebaseFirestore.instance.collection('games').doc(roomCode).update({
@ -382,7 +471,6 @@ class GameController extends ChangeNotifier {
_saveMatchResult(); _saveMatchResult();
if (isHost) FirebaseFirestore.instance.collection('games').doc(roomCode).update({'status': 'finished'}); if (isHost) FirebaseFirestore.instance.collection('games').doc(roomCode).update({'status': 'finished'});
} }
} else { } else {
if (board.isGameOver) _saveMatchResult(); if (board.isGameOver) _saveMatchResult();
else if (isVsCPU && board.currentPlayer == Player.blue) _checkCPUTurn(); else if (isVsCPU && board.currentPlayer == Player.blue) _checkCPUTurn();
@ -397,11 +485,15 @@ class GameController extends ChangeNotifier {
if (!board.isGameOver) { if (!board.isGameOver) {
List<Box> closedBefore = board.boxes.where((b) => b.owner != Player.none).toList(); 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); Line bestMove = AIEngine.getBestMove(board, cpuLevel);
board.playMove(bestMove); board.playMove(bestMove);
List<Box> newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList(); List<Box> newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList();
_playEffects(newClosed, isOpponent: true); 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(); isCPUThinking = false; _startTimer(); notifyListeners();
@ -415,22 +507,44 @@ class GameController extends ChangeNotifier {
if (_hasSavedResult) return; if (_hasSavedResult) return;
_hasSavedResult = true; _hasSavedResult = true;
int calculatedXP = 0;
bool isDraw = board.scoreRed == board.scoreBlue;
String myRealName = StorageService.instance.playerName; String myRealName = StorageService.instance.playerName;
if (myRealName.isEmpty) myRealName = "IO"; if (myRealName.isEmpty) myRealName = "IO";
if (isOnline) { if (isOnline) {
bool isWin = isHost ? board.scoreRed > board.scoreBlue : board.scoreBlue > board.scoreRed;
calculatedXP = isWin ? 20 : (isDraw ? 5 : 2);
String oppName = isHost ? onlineGuestName : onlineHostName; String oppName = isHost ? onlineGuestName : onlineHostName;
int myScore = isHost ? board.scoreRed : board.scoreBlue; int myScore = isHost ? board.scoreRed : board.scoreBlue;
int oppScore = isHost ? board.scoreBlue : board.scoreRed; int oppScore = isHost ? board.scoreBlue : board.scoreRed;
StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: oppName, myScore: myScore, oppScore: oppScore, isOnline: true); StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: oppName, myScore: myScore, oppScore: oppScore, isOnline: true);
if (isWin) StorageService.instance.updateQuestProgress(0, 1); // Missione: Vinci Online
} else if (isVsCPU) { } else if (isVsCPU) {
int myScore = board.scoreRed; int cpuScore = board.scoreBlue; int myScore = board.scoreRed; int cpuScore = board.scoreBlue;
if (myScore > cpuScore) StorageService.instance.addWin(); bool isWin = myScore > cpuScore;
else if (cpuScore > myScore) StorageService.instance.addLoss(); calculatedXP = isWin ? (10 + (cpuLevel * 2)) : (isDraw ? 5 : 2);
if (isWin) {
StorageService.instance.addWin();
StorageService.instance.updateQuestProgress(1, 1); // Missione: Vinci vs CPU
} else if (cpuScore > myScore) {
StorageService.instance.addLoss();
}
StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "CPU (Liv. $cpuLevel)", myScore: myScore, oppScore: cpuScore, isOnline: false); StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "CPU (Liv. $cpuLevel)", myScore: myScore, oppScore: cpuScore, isOnline: false);
} else { } else {
calculatedXP = 2;
StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "Ospite (Locale)", myScore: board.scoreRed, oppScore: board.scoreBlue, isOnline: false); StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "Ospite (Locale)", myScore: board.scoreRed, oppScore: board.scoreBlue, isOnline: false);
} }
// Se si sta giocando in una forma speciale (non classica)
if (board.shape != ArenaShape.classic) {
StorageService.instance.updateQuestProgress(2, 1); // Missione: Usa forme speciali
}
lastMatchXP = calculatedXP; StorageService.instance.addXP(calculatedXP); notifyListeners();
} }
void increaseLevelAndRestart() { void increaseLevelAndRestart() {

View file

@ -5,9 +5,7 @@
import 'dart:math'; import 'dart:math';
enum Player { red, blue, none } enum Player { red, blue, none }
enum BoxType { normal, gold, bomb, invisible, swap } enum BoxType { normal, gold, bomb, invisible, swap, ice, multiplier } // Aggiunti ice e multiplier
// --- AGGIUNTO 'chaos' ---
enum ArenaShape { classic, cross, donut, hourglass, chaos } enum ArenaShape { classic, cross, donut, hourglass, chaos }
class Dot { class Dot {
@ -26,6 +24,7 @@ class Line {
final Dot p2; final Dot p2;
Player owner = Player.none; Player owner = Player.none;
bool isPlayable = false; bool isPlayable = false;
bool isIceCracked = false; // NUOVO: Stato per il blocco di ghiaccio
Line(this.p1, this.p2); Line(this.p1, this.p2);
@ -39,6 +38,10 @@ class Box {
late Line top, bottom, left, right; late Line top, bottom, left, right;
BoxType type = BoxType.normal; BoxType type = BoxType.normal;
bool isRevealed = false;
Player? hiddenJokerOwner;
bool isJokerRevealed = false;
Box(this.x, this.y); Box(this.x, this.y);
bool isClosed() { bool isClosed() {
@ -46,10 +49,13 @@ class Box {
return top.owner != Player.none && bottom.owner != Player.none && left.owner != Player.none && right.owner != Player.none; return top.owner != Player.none && bottom.owner != Player.none && left.owner != Player.none && right.owner != Player.none;
} }
int get value { int getCalculatedValue(Player closer) {
if (hiddenJokerOwner != null) {
return (closer == hiddenJokerOwner) ? 2 : -1;
}
if (type == BoxType.gold) return 2; if (type == BoxType.gold) return 2;
if (type == BoxType.bomb) return -1; if (type == BoxType.bomb) return -1;
if (type == BoxType.swap) return 0; if (type == BoxType.swap || type == BoxType.ice || type == BoxType.multiplier) return 0; // Il moltiplicatore e il ghiaccio non danno punti base
return 1; return 1;
} }
} }
@ -60,6 +66,9 @@ class GameBoard {
final int? seed; final int? seed;
final ArenaShape shape; final ArenaShape shape;
late int columns;
late int rows;
List<Dot> dots = []; List<Dot> dots = [];
List<Line> lines = []; List<Line> lines = [];
List<Box> boxes = []; List<Box> boxes = [];
@ -71,93 +80,91 @@ class GameBoard {
Line? lastMove; 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}) { GameBoard({required this.radius, this.level = 1, this.seed, this.shape = ArenaShape.classic}) {
_generateBoard(); _generateBoard();
} }
void _generateBoard() { void _generateBoard() {
int size = radius * 2 + 1;
final random = seed != null ? Random(seed) : Random(); final random = seed != null ? Random(seed) : Random();
// Se è Caos, decidiamo quale algoritmo usare in base al seed
int chaosAlgorithm = random.nextInt(5); 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(); dots.clear();
lines.clear(); lines.clear();
boxes.clear(); boxes.clear();
lastMove = null; lastMove = null;
for (int y = 0; y < size; y++) { for (int y = 0; y < rows; y++) {
for (int x = 0; x < size; x++) { for (int x = 0; x < columns; x++) {
var box = Box(x, y); var box = Box(x, y);
bool isVisible = true;
if (shape != ArenaShape.chaos) {
int dx = (x - radius).abs(); int dx = (x - radius).abs();
int dy = (y - radius).abs(); int dy = (y - radius).abs();
isVisible = (dx + dy) <= radius;
bool isVisible = (dx + dy) <= radius;
if (isVisible) { if (isVisible) {
switch (shape) { switch (shape) {
case ArenaShape.classic:
break;
case ArenaShape.cross: case ArenaShape.cross:
int spessoreBraccio = radius > 3 ? 1 : 0; int spessoreBraccio = radius > 3 ? 1 : 0;
if (dx > spessoreBraccio && dy > spessoreBraccio) isVisible = false; if (dx > spessoreBraccio && dy > spessoreBraccio) isVisible = false; break;
break;
case ArenaShape.donut: case ArenaShape.donut:
int dimensioneBuco = radius > 3 ? 2 : 1; int dimensioneBuco = radius > 3 ? 2 : 1;
if ((dx + dy) <= dimensioneBuco) isVisible = false; if ((dx + dy) <= dimensioneBuco) isVisible = false; break;
break;
case ArenaShape.hourglass: case ArenaShape.hourglass:
if (dx > dy) isVisible = false; if (dx > dy) isVisible = false;
if (x == radius && y == radius) isVisible = true; if (x == radius && y == radius) isVisible = true; break;
break; default: break;
case ArenaShape.chaos: }
// --- GENERATORE PROCEDURALE (IL CAOS) --- }
// Essendo basato su dx e dy, genererà sempre forme simmetriche a 4 vie! } else {
double percentY = y / rows;
if (chaosAlgorithm == 0) { if (chaosAlgorithm == 0) {
// Modello "Rete Frattale": Rimuove blocchi basati su operatori bitwise isVisible = (x % 2 == 0) && (random.nextDouble() > 0.15);
if ((dx & dy) != 0) isVisible = false;
} else if (chaosAlgorithm == 1) { } else if (chaosAlgorithm == 1) {
// Modello "Quattro Pilastri": Svuota lunghe linee ma salva i centri double chance = 0.2 + (percentY * 0.7);
if (dx == 1 || dy == 1) { isVisible = random.nextDouble() < chance;
if ((dx + dy) > 2 && (dx + dy) < radius) isVisible = false;
}
} else if (chaosAlgorithm == 2) { } else if (chaosAlgorithm == 2) {
// Modello "X-Treme": Taglia le diagonali perfette int midY = rows ~/ 2;
if (dx == dy && dx > 0 && dx < radius) isVisible = false; 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) { } else if (chaosAlgorithm == 3) {
// Modello "Scacchiera Nucleare": Alternanza precisa isVisible = (y % 2 == 0) ? (x < columns - 1) : (x > 0);
if (dx % 2 == 1 && dy % 2 == 1) isVisible = false; if (random.nextDouble() > 0.8) isVisible = false;
} else if (chaosAlgorithm == 4) { } else if (chaosAlgorithm == 4) {
// Modello "Anelli Frammentati": Buca gli anelli pari isVisible = random.nextDouble() > 0.45;
if ((dx + dy) % 2 == 0 && (dx + dy) > 0) {
if (dx != 0 && dy != 0) isVisible = false;
}
}
// Assicuriamoci che il punto centrale esista quasi sempre per connettere la mappa
if (dx == 0 && dy == 0) isVisible = true;
break;
} }
if (x == radius && y == rows ~/ 2) isVisible = true;
} }
if (!isVisible) { if (!isVisible) {
box.type = BoxType.invisible; box.type = BoxType.invisible;
} else if (level > 1) { } else if (level > 1) {
double chance = random.nextDouble(); double chance = random.nextDouble();
if (chance < 0.10) { if (chance < 0.08) box.type = BoxType.gold;
box.type = BoxType.gold; else if (chance > 0.92) box.type = BoxType.bomb;
} else if (chance > 0.90) { else if (level >= 5 && chance > 0.88 && chance <= 0.92) box.type = BoxType.swap;
box.type = BoxType.bomb; else if (level >= 10 && chance > 0.83 && chance <= 0.88) box.type = BoxType.ice; // Nuova Scatola Ghiaccio
} else if (level >= 5 && chance > 0.85 && chance <= 0.90) { else if (level >= 15 && chance > 0.78 && chance <= 0.83) box.type = BoxType.multiplier; // Nuova Scatola x2
box.type = BoxType.swap;
}
} }
boxes.add(box); boxes.add(box);
} }
} }
// Costruzione Linee (Identico a prima)
for (var box in boxes) { for (var box in boxes) {
Dot tl = _getOrAddDot(box.x, box.y); Dot tl = _getOrAddDot(box.x, box.y);
Dot tr = _getOrAddDot(box.x + 1, box.y); Dot tr = _getOrAddDot(box.x + 1, box.y);
@ -170,10 +177,8 @@ class GameBoard {
box.right = _getOrAddLine(tr, br); box.right = _getOrAddLine(tr, br);
if (box.type != BoxType.invisible) { if (box.type != BoxType.invisible) {
box.top.isPlayable = true; box.top.isPlayable = true; box.bottom.isPlayable = true;
box.bottom.isPlayable = true; box.left.isPlayable = true; box.right.isPlayable = true;
box.left.isPlayable = true;
box.right.isPlayable = true;
} }
} }
} }
@ -181,15 +186,13 @@ class GameBoard {
Dot _getOrAddDot(int x, int y) { Dot _getOrAddDot(int x, int y) {
for (var dot in dots) { if (dot.x == x && dot.y == y) return dot; } for (var dot in dots) { if (dot.x == x && dot.y == y) return dot; }
var newDot = Dot(x, y); var newDot = Dot(x, y);
dots.add(newDot); dots.add(newDot); return newDot;
return newDot;
} }
Line _getOrAddLine(Dot a, Dot b) { Line _getOrAddLine(Dot a, Dot b) {
for (var line in lines) { if (line.connects(a, b)) return line; } for (var line in lines) { if (line.connects(a, b)) return line; }
var newLine = Line(a, b); var newLine = Line(a, b);
lines.add(newLine); lines.add(newLine); return newLine;
return newLine;
} }
bool playMove(Line lineToPlay, {Player? forcedPlayer}) { bool playMove(Line lineToPlay, {Player? forcedPlayer}) {
@ -198,45 +201,91 @@ class GameBoard {
Player playerMakingMove = forcedPlayer ?? currentPlayer; Player playerMakingMove = forcedPlayer ?? currentPlayer;
Line? actualLine; Line? actualLine;
for (var l in lines) { for (var l in lines) {
if (l.connects(lineToPlay.p1, lineToPlay.p2)) { if (l.connects(lineToPlay.p1, lineToPlay.p2)) { actualLine = l; break; }
actualLine = l; break;
}
} }
if (actualLine == null || actualLine.owner != Player.none || !actualLine.isPlayable) return false; 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; actualLine.owner = playerMakingMove;
lastMove = actualLine; lastMove = actualLine;
bool boxedClosed = false; bool scoredPoint = false;
bool triggeredSwap = false; bool triggeredSwap = false;
for (var box in boxes) { for (var box in boxes) {
if (box.owner == Player.none && box.isClosed()) { if (box.owner == Player.none && box.isClosed()) {
box.owner = playerMakingMove; box.owner = playerMakingMove;
boxedClosed = true; scoredPoint = true;
if (playerMakingMove == Player.red) { scoreRed += box.value; } if (box.hiddenJokerOwner != null) box.isJokerRevealed = true;
else { scoreBlue += box.value; }
if (box.type == BoxType.swap) { 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 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; 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) { if (triggeredSwap) {
int temp = scoreRed; int temp = scoreRed; scoreRed = scoreBlue; scoreBlue = temp;
scoreRed = scoreBlue;
scoreBlue = temp;
} }
if (lines.where((l) => l.isPlayable).every((l) => l.owner != Player.none)) { isGameOver = true; } if (lines.where((l) => l.isPlayable).every((l) => l.owner != Player.none)) { isGameOver = true; }
if (forcedPlayer == null) { if (forcedPlayer == null) {
if (!boxedClosed && !isGameOver) { currentPlayer = (currentPlayer == Player.red) ? Player.blue : Player.red; } if (!scoredPoint && !isGameOver) { currentPlayer = (currentPlayer == Player.red) ? Player.blue : Player.red; }
else if (scoredPoint && !isGameOver) { currentPlayer = playerMakingMove; }
} else { } else {
if (!boxedClosed && !isGameOver) { currentPlayer = (forcedPlayer == Player.red) ? Player.blue : Player.red; } if (!scoredPoint && !isGameOver) { currentPlayer = (forcedPlayer == Player.red) ? Player.blue : Player.red; }
else { currentPlayer = forcedPlayer; } else { currentPlayer = forcedPlayer; }
} }

View file

@ -1,3 +1,7 @@
// ===========================================================================
// FILE: lib/services/audio_service.dart
// ===========================================================================
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:audioplayers/audioplayers.dart'; import 'package:audioplayers/audioplayers.dart';
import '../core/app_colors.dart'; import '../core/app_colors.dart';
@ -18,11 +22,15 @@ class AudioService extends ChangeNotifier {
if (isMuted) return; if (isMuted) return;
String file = ''; String file = '';
switch (theme) { switch (theme) {
case AppThemeType.minimal: file = 'minimal_line.wav'; break; case AppThemeType.minimal:
case AppThemeType.arcade: // Suono secco per l'arcade
file = 'minimal_line.wav'; break;
case AppThemeType.doodle: case AppThemeType.doodle:
case AppThemeType.wood: case AppThemeType.wood:
file = 'doodle_line.wav'; break; file = 'doodle_line.wav'; break;
case AppThemeType.cyberpunk: file = 'cyber_line.wav'; break; case AppThemeType.cyberpunk:
case AppThemeType.grimorio: // Suono etereo per la magia
file = 'cyber_line.wav'; break;
} }
await _sfxPlayer.play(AssetSource('audio/sfx/$file')); await _sfxPlayer.play(AssetSource('audio/sfx/$file'));
} }
@ -31,25 +39,26 @@ class AudioService extends ChangeNotifier {
if (isMuted) return; if (isMuted) return;
String file = ''; String file = '';
switch (theme) { switch (theme) {
case AppThemeType.minimal: file = 'minimal_box.wav'; break; case AppThemeType.minimal:
case AppThemeType.arcade:
file = 'minimal_box.wav'; break;
case AppThemeType.doodle: case AppThemeType.doodle:
case AppThemeType.wood: case AppThemeType.wood:
file = 'doodle_box.wav'; break; file = 'doodle_box.wav'; break;
case AppThemeType.cyberpunk: file = 'cyber_box.wav'; break; case AppThemeType.cyberpunk:
case AppThemeType.grimorio:
file = 'cyber_box.wav'; break;
} }
await _sfxPlayer.play(AssetSource('audio/sfx/$file')); await _sfxPlayer.play(AssetSource('audio/sfx/$file'));
} }
// --- NUOVI EFFETTI SPECIALI ---
void playBonusSfx() async { void playBonusSfx() async {
if (isMuted) return; if (isMuted) return;
// Assicurati di aggiungere questo file nella cartella assets/audio/sfx/
await _sfxPlayer.play(AssetSource('audio/sfx/bonus.wav')); await _sfxPlayer.play(AssetSource('audio/sfx/bonus.wav'));
} }
void playBombSfx() async { void playBombSfx() async {
if (isMuted) return; if (isMuted) return;
// Assicurati di aggiungere questo file nella cartella assets/audio/sfx/
await _sfxPlayer.play(AssetSource('audio/sfx/bomb.wav')); await _sfxPlayer.play(AssetSource('audio/sfx/bomb.wav'));
} }
} }

View file

@ -1,5 +1,10 @@
// ===========================================================================
// FILE: lib/services/storage_service.dart
// ===========================================================================
import 'dart:convert'; import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import '../core/app_colors.dart'; import '../core/app_colors.dart';
class StorageService { class StorageService {
@ -8,13 +13,12 @@ class StorageService {
late SharedPreferences _prefs; late SharedPreferences _prefs;
// Si avvia quando apriamo l'app
Future<void> init() async { Future<void> init() async {
_prefs = await SharedPreferences.getInstance(); _prefs = await SharedPreferences.getInstance();
_checkDailyQuests(); // All'avvio controlliamo se ci sono nuove sfide
} }
// --- IMPOSTAZIONI --- int get savedThemeIndex => _prefs.getInt('theme') ?? AppThemeType.minimal.index;
int get savedThemeIndex => _prefs.getInt('theme') ?? AppThemeType.cyberpunk.index;
Future<void> saveTheme(AppThemeType theme) async => await _prefs.setInt('theme', theme.index); Future<void> saveTheme(AppThemeType theme) async => await _prefs.setInt('theme', theme.index);
int get savedRadius => _prefs.getInt('radius') ?? 2; int get savedRadius => _prefs.getInt('radius') ?? 2;
@ -23,9 +27,21 @@ class StorageService {
bool get isMuted => _prefs.getBool('isMuted') ?? false; bool get isMuted => _prefs.getBool('isMuted') ?? false;
Future<void> saveMuted(bool muted) async => await _prefs.setBool('isMuted', muted); Future<void> saveMuted(bool muted) async => await _prefs.setBool('isMuted', muted);
// --- STATISTICHE VS CPU --- int get totalXP => _prefs.getInt('totalXP') ?? 0;
// Modificato per sincronizzare automaticamente la classifica su Firebase
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; int get wins => _prefs.getInt('wins') ?? 0;
Future<void> addWin() async => await _prefs.setInt('wins', wins + 1); Future<void> addWin() async {
await _prefs.setInt('wins', wins + 1);
syncLeaderboard();
}
int get losses => _prefs.getInt('losses') ?? 0; int get losses => _prefs.getInt('losses') ?? 0;
Future<void> addLoss() async => await _prefs.setInt('losses', losses + 1); Future<void> addLoss() async => await _prefs.setInt('losses', losses + 1);
@ -33,9 +49,66 @@ class StorageService {
int get cpuLevel => _prefs.getInt('cpuLevel') ?? 1; int get cpuLevel => _prefs.getInt('cpuLevel') ?? 1;
Future<void> saveCpuLevel(int level) async => await _prefs.setInt('cpuLevel', level); Future<void> saveCpuLevel(int level) async => await _prefs.setInt('cpuLevel', level);
// --- MULTIPLAYER ---
String get playerName => _prefs.getString('playerName') ?? ''; String get playerName => _prefs.getString('playerName') ?? '';
Future<void> savePlayerName(String name) async => await _prefs.setString('playerName', name); Future<void> savePlayerName(String name) async {
await _prefs.setString('playerName', name);
syncLeaderboard(); // Aggiorna il nome in classifica
}
// --- NUOVO: SINCRONIZZAZIONE CLASSIFICA ONLINE ---
Future<void> syncLeaderboard() async {
if (playerName.isNotEmpty) {
try {
await FirebaseFirestore.instance.collection('leaderboard').doc(playerName).set({
'name': playerName,
'xp': totalXP,
'level': playerLevel,
'wins': wins,
'lastActive': FieldValue.serverTimestamp(),
}, SetOptions(merge: true));
} catch(e) {
// Ignoriamo gli errori se manca la rete, si sincronizzerà dopo
}
}
}
// --- NUOVO: GESTIONE SFIDE GIORNALIERE ---
void _checkDailyQuests() {
String today = DateTime.now().toIso8601String().substring(0, 10);
String lastDate = _prefs.getString('quest_date') ?? '';
if (today != lastDate) {
// Nuovo giorno, nuove sfide!
_prefs.setString('quest_date', today);
// Sfida 1: Gioca partite online
_prefs.setInt('q1_type', 0);
_prefs.setInt('q1_prog', 0);
_prefs.setInt('q1_target', 3);
// Sfida 2: Vinci contro la CPU
_prefs.setInt('q2_type', 1);
_prefs.setInt('q2_prog', 0);
_prefs.setInt('q2_target', 2);
// Sfida 3: Partite con forme speciali (Croce, Caos, ecc)
_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);
}
}
}
}
// --- STORICO PARTITE --- // --- STORICO PARTITE ---
List<Map<String, dynamic>> get matchHistory { List<Map<String, dynamic>> get matchHistory {
@ -43,27 +116,14 @@ class StorageService {
return history.map((e) => jsonDecode(e) as Map<String, dynamic>).toList(); return history.map((e) => jsonDecode(e) as Map<String, dynamic>).toList();
} }
// Salviamo sia il nostro nome che quello dell'avversario
Future<void> saveMatchToHistory({required String myName, required String opponent, required int myScore, required int oppScore, required bool isOnline}) async { 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') ?? []; List<String> history = _prefs.getStringList('matchHistory') ?? [];
Map<String, dynamic> match = { Map<String, dynamic> match = {
'date': DateTime.now().toIso8601String(), 'date': DateTime.now().toIso8601String(),
'myName': myName, 'myName': myName, 'opponent': opponent, 'myScore': myScore, 'oppScore': oppScore, 'isOnline': isOnline,
'opponent': opponent,
'myScore': myScore,
'oppScore': oppScore,
'isOnline': isOnline,
}; };
// Aggiungiamo in cima (il più recente per primo)
history.insert(0, jsonEncode(match)); history.insert(0, jsonEncode(match));
if (history.length > 50) history = history.sublist(0, 50);
// Teniamo solo le ultime 50 partite per non intasare la memoria
if (history.length > 50) {
history = history.sublist(0, 50);
}
await _prefs.setStringList('matchHistory', history); await _prefs.setStringList('matchHistory', history);
} }
} }

View file

@ -4,6 +4,7 @@
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../models/game_board.dart'; import '../../models/game_board.dart';
import '../../core/app_colors.dart'; import '../../core/app_colors.dart';
@ -13,7 +14,23 @@ class BoardPainter extends CustomPainter {
final AppThemeType themeType; final AppThemeType themeType;
final double blinkValue; final double blinkValue;
BoardPainter({required this.board, required this.theme, required this.themeType, this.blinkValue = 0.0}); 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 @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
@ -32,19 +49,28 @@ class BoardPainter extends CustomPainter {
} }
} }
int gridPoints = board.radius * 2 + 2; int gridPoints = board.columns + 1;
double spacing = size.width / gridPoints; double spacing = size.width / gridPoints;
double offset = spacing / 2; double offset = spacing / 2;
Offset getScreenPos(int x, int y) => Offset(x * spacing + offset, y * spacing + offset); Offset getScreenPos(int x, int y) => Offset(x * spacing + offset, y * spacing + offset);
// --- 1. DISEGNO AREE CONQUISTATE E ICONE ---
for (var box in board.boxes) { for (var box in board.boxes) {
if (box.type == BoxType.invisible) continue;
Offset p1 = getScreenPos(box.x, box.y); Offset p1 = getScreenPos(box.x, box.y);
Offset p2 = getScreenPos(box.x + 1, box.y + 1); Offset p2 = getScreenPos(box.x + 1, box.y + 1);
Rect rect = Rect.fromPoints(p1, p2); 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) { if (box.owner != Player.none) {
final boxPaint = Paint() final boxPaint = Paint()
..style = PaintingStyle.fill ..style = PaintingStyle.fill
@ -55,41 +81,65 @@ class BoardPainter extends CustomPainter {
} else if (themeType == AppThemeType.doodle) { } else if (themeType == AppThemeType.doodle) {
Color penColor = box.owner == Player.red ? Colors.redAccent.shade700 : Colors.blueAccent.shade700; Color penColor = box.owner == Player.red ? Colors.redAccent.shade700 : Colors.blueAccent.shade700;
_drawScribbleBox(canvas, rect, penColor); _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 { } else {
canvas.drawRect(rect, boxPaint); canvas.drawRect(rect, boxPaint);
} }
} }
if (box.type == BoxType.gold) { if (box.hiddenJokerOwner != null) {
_drawIconInBox(canvas, rect, Icons.star_rounded, Colors.amber); Color jokerColor = box.hiddenJokerOwner == Player.red ? theme.playerRed : theme.playerBlue;
} else if (box.type == BoxType.bomb) {
_drawIconInBox(canvas, rect, Icons.mood_bad_rounded, themeType == AppThemeType.cyberpunk ? Colors.greenAccent : Colors.deepPurple); if (box.isJokerRevealed) {
} else if (box.type == BoxType.swap) { _drawIconInBox(canvas, rect, ThemeIcons.joker(themeType), jokerColor);
// NUOVA ICONA SWAP: Frecce circolari viola (o cyan) per indicare l'inversione } else {
_drawIconInBox(canvas, rect, Icons.sync_rounded, Colors.purpleAccent); 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 ? 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);
} }
} }
// --- 2. DISEGNO LINEE CON EFFETTO LAMPEGGIAMENTO ---
for (var line in board.lines) { for (var line in board.lines) {
if (!line.isPlayable) continue; if (!line.isPlayable) continue;
Offset p1 = getScreenPos(line.p1.x, line.p1.y); Offset p1 = getScreenPos(line.p1.x, line.p1.y);
Offset p2 = getScreenPos(line.p2.x, line.p2.y); Offset p2 = getScreenPos(line.p2.x, line.p2.y);
bool isLastMove = (line == board.lastMove); // --- DISEGNO DELLA LINEA "INCRINATA" DAL GHIACCIO ---
if (line.isIceCracked) {
_drawCrackedIceLine(canvas, p1, p2, blinkValue);
continue; // Non ha ancora un proprietario, passiamo alla prossima!
}
bool isLastMove = (line == board.lastMove);
Color lineColor = line.owner == Player.none Color lineColor = line.owner == Player.none
? theme.gridLine.withOpacity(0.4) ? theme.gridLine.withOpacity(0.4)
: (line.owner == Player.red ? theme.playerRed : theme.playerBlue); : (line.owner == Player.red ? theme.playerRed : theme.playerBlue);
if (isLastMove && line.owner != Player.none && themeType != AppThemeType.wood && themeType != AppThemeType.cyberpunk) { if (isLastMove && line.owner != Player.none && themeType != AppThemeType.wood && themeType != AppThemeType.cyberpunk && themeType != AppThemeType.arcade && themeType != AppThemeType.grimorio) {
canvas.drawLine(p1, p2, Paint() 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));
..color = Colors.white.withOpacity(blinkValue * 0.5)
..strokeWidth = 16.0
..strokeCap = StrokeCap.round
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6.0)
);
} }
if (themeType == AppThemeType.wood) { if (themeType == AppThemeType.wood) {
@ -97,35 +147,30 @@ class BoardPainter extends CustomPainter {
canvas.drawLine(p1, p2, Paint()..color = const Color(0xFF3E2723).withOpacity(0.3)..strokeWidth = 4.5..strokeCap = StrokeCap.round); canvas.drawLine(p1, p2, Paint()..color = const Color(0xFF3E2723).withOpacity(0.3)..strokeWidth = 4.5..strokeCap = StrokeCap.round);
} else { } else {
Color headColor = lineColor; Color headColor = lineColor;
if (isLastMove) { if (isLastMove) headColor = Color.lerp(headColor, Colors.yellow, blinkValue * 0.8) ?? headColor;
headColor = Color.lerp(headColor, Colors.yellow, blinkValue * 0.8) ?? headColor;
}
_drawRealisticMatch(canvas, p1, p2, headColor, isLastMove: isLastMove, blinkValue: blinkValue); _drawRealisticMatch(canvas, p1, p2, headColor, isLastMove: isLastMove, blinkValue: blinkValue);
} }
} else if (themeType == AppThemeType.cyberpunk) { } else if (themeType == AppThemeType.cyberpunk) {
_drawNeonLine(canvas, p1, p2, lineColor, line.owner != Player.none, isLastMove: isLastMove, blinkValue: blinkValue); _drawNeonLine(canvas, p1, p2, lineColor, line.owner != Player.none, isLastMove: isLastMove, blinkValue: blinkValue);
} else if (themeType == AppThemeType.doodle) { } else if (themeType == AppThemeType.doodle) {
Color doodleColor = line.owner == Player.none ? Colors.black.withOpacity(0.05) : lineColor; Color doodleColor = line.owner == Player.none ? Colors.black.withOpacity(0.05) : lineColor;
if (isLastMove && line.owner != Player.none) { if (isLastMove && line.owner != Player.none) doodleColor = Color.lerp(doodleColor, Colors.black, blinkValue * 0.4) ?? doodleColor;
doodleColor = Color.lerp(doodleColor, Colors.black, blinkValue * 0.4) ?? doodleColor;
}
_drawWobblyLine(canvas, p1, p2, doodleColor, line.owner != Player.none, isLastMove: isLastMove, blinkValue: blinkValue); _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 { } else {
if (isLastMove && line.owner != Player.none) { if (isLastMove && line.owner != Player.none) lineColor = Color.lerp(lineColor, Colors.white, blinkValue * 0.5) ?? lineColor;
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); canvas.drawLine(p1, p2, Paint()..color = lineColor..strokeWidth = isLastMove ? 6.0 + (2.0 * blinkValue) : 6.0..strokeCap = StrokeCap.round);
} }
} }
// --- 3. DISEGNO PUNTINI ---
final dotPaint = Paint()..style = PaintingStyle.fill; final dotPaint = Paint()..style = PaintingStyle.fill;
Set<Dot> activeDots = {}; Set<Dot> activeDots = {};
for (var line in board.lines) { for (var line in board.lines) {
if (line.isPlayable) { if (line.isPlayable) {
activeDots.add(line.p1); activeDots.add(line.p1); activeDots.add(line.p2);
activeDots.add(line.p2);
} }
} }
@ -138,6 +183,13 @@ class BoardPainter extends CustomPainter {
canvas.drawCircle(pos, 3.0, Paint()..color = Colors.white.withOpacity(0.5)); canvas.drawCircle(pos, 3.0, Paint()..color = Colors.white.withOpacity(0.5));
} else if (themeType == AppThemeType.doodle) { } else if (themeType == AppThemeType.doodle) {
canvas.drawRect(Rect.fromCenter(center: pos, width: 4, height: 4), dotPaint..color = Colors.black.withOpacity(0.25)); 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 { } else {
canvas.drawCircle(pos, 5.0, dotPaint..color = theme.text.withOpacity(0.6)); canvas.drawCircle(pos, 5.0, dotPaint..color = theme.text.withOpacity(0.6));
} }
@ -149,189 +201,146 @@ class BoardPainter extends CustomPainter {
textPainter.text = TextSpan( textPainter.text = TextSpan(
text: String.fromCharCode(icon.codePoint), text: String.fromCharCode(icon.codePoint),
style: TextStyle( style: TextStyle(
color: color.withOpacity(0.7), color: themeType == AppThemeType.arcade ? color : color.withOpacity(0.7),
fontSize: rect.width * 0.45, fontSize: rect.width * 0.45,
fontFamily: icon.fontFamily, fontFamily: icon.fontFamily,
package: icon.fontPackage, package: icon.fontPackage,
shadows: [Shadow(color: color.withOpacity(0.6), blurRadius: 10, offset: const Offset(0, 0))] shadows: themeType == AppThemeType.arcade ? [] : [Shadow(color: color.withOpacity(0.6), blurRadius: 10, offset: const Offset(0, 0))]
), ),
); );
textPainter.layout(); textPainter.layout();
textPainter.paint(canvas, Offset(rect.center.dx - textPainter.width / 2, rect.center.dy - textPainter.height / 2)); textPainter.paint(canvas, Offset(rect.center.dx - textPainter.width / 2, rect.center.dy - textPainter.height / 2));
} }
void _drawFlameBox(Canvas canvas, Rect baseRect, bool isRed) { void _drawCrackedIceLine(Canvas canvas, Offset p1, Offset p2, double blink) {
final rand = Random((baseRect.left + baseRect.top).toInt()); Paint crackPaint = Paint()
Offset center = baseRect.center; ..color = Colors.cyanAccent.withOpacity(0.6 + (0.4 * blink))
double w = baseRect.width * 0.35; ..strokeWidth = 3.0
double h = baseRect.height * 0.55; ..style = PaintingStyle.stroke
Offset bottomCenter = Offset(center.dx, center.dy + h * 0.5); ..strokeCap = StrokeCap.round
..maskFilter = const MaskFilter.blur(BlurStyle.solid, 2.0);
Color outerColor = isRed ? Colors.red.shade600.withOpacity(0.85) : Colors.blue.shade700.withOpacity(0.85); // Effetto linea frammentata
Color midColor = isRed ? Colors.orangeAccent : Colors.lightBlueAccent; canvas.drawLine(p1, p2, Paint()..color = Colors.cyan.withOpacity(0.2)..strokeWidth=6.0);
Color coreColor = isRed ? Colors.yellowAccent : Colors.white;
canvas.drawOval( Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy);
Rect.fromCenter(center: bottomCenter, width: w * 1.5, height: w * 0.5), double len = dir.length; Vector2 ndir = dir.normalized(); Vector2 perp = Vector2(-ndir.y, ndir.x);
Paint()
..color = Colors.black.withOpacity(0.4)
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4.0),
);
void drawFlameLayer(double scale, Color color, double tipOffsetX) { Path crack = Path()..moveTo(p1.dx, p1.dy);
Path path = Path(); int zigzags = 6;
double fw = w * scale; for (int i=1; i<zigzags; i++) {
double fh = h * scale; double d = len * (i / zigzags);
Offset basePt = Offset(p1.dx + ndir.x * d, p1.dy + ndir.y * d);
path.moveTo(bottomCenter.dx, bottomCenter.dy); double offset = (i % 2 == 0 ? 3.0 : -3.0);
path.cubicTo( crack.lineTo(basePt.dx + perp.x * offset, basePt.dy + perp.y * offset);
bottomCenter.dx + fw, bottomCenter.dy, }
bottomCenter.dx + fw * 0.8, bottomCenter.dy - fh * 0.6, crack.lineTo(p2.dx, p2.dy);
bottomCenter.dx + tipOffsetX, bottomCenter.dy - fh, canvas.drawPath(crack, crackPaint);
);
path.cubicTo(
bottomCenter.dx - fw * 0.8, bottomCenter.dy - fh * 0.6,
bottomCenter.dx - fw, bottomCenter.dy,
bottomCenter.dx, bottomCenter.dy,
);
canvas.drawPath(
path,
Paint()
..color = color
..style = PaintingStyle.fill
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 1.5),
);
} }
double randomTipX = (rand.nextDouble() - 0.5) * w * 0.8; void _drawArcadeBox(Canvas canvas, Rect rect, Color color) {
drawFlameLayer(1.0, outerColor, randomTipX); double pixelSize = 4.0; Paint paint = Paint()..color = color.withOpacity(0.9)..style = PaintingStyle.fill;
drawFlameLayer(0.65, midColor.withOpacity(0.9), randomTipX * 0.6); for (double y = rect.top; y < rect.bottom; y += pixelSize) {
drawFlameLayer(0.35, coreColor.withOpacity(0.9), randomTipX * 0.2); 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 _drawFlameBox(Canvas canvas, Rect baseRect, bool isRed) {
final rand = Random((baseRect.left + baseRect.top).toInt());
Offset center = baseRect.center; double w = baseRect.width * 0.35; double h = baseRect.height * 0.55; Offset bottomCenter = Offset(center.dx, center.dy + h * 0.5);
Color outerColor = isRed ? Colors.red.shade600.withOpacity(0.85) : Colors.blue.shade700.withOpacity(0.85); Color midColor = isRed ? Colors.orangeAccent : Colors.lightBlueAccent; Color coreColor = isRed ? Colors.yellowAccent : Colors.white;
canvas.drawOval(Rect.fromCenter(center: bottomCenter, width: w * 1.5, height: w * 0.5), Paint()..color = Colors.black.withOpacity(0.4)..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4.0));
void drawFlameLayer(double scale, Color color, double tipOffsetX) {
Path path = Path(); double fw = w * scale; double fh = h * scale;
path.moveTo(bottomCenter.dx, bottomCenter.dy); path.cubicTo(bottomCenter.dx + fw, bottomCenter.dy, bottomCenter.dx + fw * 0.8, bottomCenter.dy - fh * 0.6, bottomCenter.dx + tipOffsetX, bottomCenter.dy - fh); path.cubicTo(bottomCenter.dx - fw * 0.8, bottomCenter.dy - fh * 0.6, bottomCenter.dx - fw, bottomCenter.dy, bottomCenter.dx, bottomCenter.dy);
canvas.drawPath(path, Paint()..color = color..style = PaintingStyle.fill..maskFilter = const MaskFilter.blur(BlurStyle.normal, 1.5));
}
double randomTipX = (rand.nextDouble() - 0.5) * w * 0.8; drawFlameLayer(1.0, outerColor, randomTipX); drawFlameLayer(0.65, midColor.withOpacity(0.9), randomTipX * 0.6); drawFlameLayer(0.35, coreColor.withOpacity(0.9), randomTipX * 0.2);
} }
void _drawScribbleBox(Canvas canvas, Rect baseRect, Color color) { void _drawScribbleBox(Canvas canvas, Rect baseRect, Color color) {
final rand = Random((baseRect.left + baseRect.top).toInt()); final rand = Random((baseRect.left + baseRect.top).toInt());
final paint = Paint() final paint = Paint()..color = color.withOpacity(0.85)..style = PaintingStyle.stroke..strokeWidth = 3.5..strokeCap = StrokeCap.round..strokeJoin = StrokeJoin.round;
..color = color.withOpacity(0.85) final path = Path(); Rect rect = baseRect.deflate(4.0); int numZigs = 15 + rand.nextInt(6); double stepY = rect.height / numZigs;
..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); 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); }
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); canvas.drawPath(path, paint);
} }
void _drawRealisticMatch(Canvas canvas, Offset p1, Offset p2, Color headColor, {bool isLastMove = false, double blinkValue = 0.0}) { void _drawRealisticMatch(Canvas canvas, Offset p1, Offset p2, Color headColor, {bool isLastMove = false, double blinkValue = 0.0}) {
int seed = (p1.dx * 1000 + p1.dy).toInt(); int seed = (p1.dx * 1000 + p1.dy).toInt(); Random rand = Random(seed); Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy).normalized(); double shrink = 8.0; Offset start = Offset(p1.dx + dir.x * shrink, p1.dy + dir.y * shrink); Offset end = Offset(p2.dx - dir.x * shrink, p2.dy - dir.y * shrink); start += Offset(rand.nextDouble() * 4 - 2, rand.nextDouble() * 4 - 2); end += Offset(rand.nextDouble() * 4 - 2, rand.nextDouble() * 4 - 2); bool headAtEnd = rand.nextBool(); Offset headPos = headAtEnd ? end : start; Offset tailPos = headAtEnd ? start : end; Vector2 matchDir = Vector2(headPos.dx - tailPos.dx, headPos.dy - tailPos.dy).normalized();
Random rand = Random(seed);
Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy).normalized();
double shrink = 8.0;
Offset start = Offset(p1.dx + dir.x * shrink, p1.dy + dir.y * shrink);
Offset end = Offset(p2.dx - dir.x * shrink, p2.dy - dir.y * shrink);
start += Offset(rand.nextDouble() * 4 - 2, rand.nextDouble() * 4 - 2);
end += Offset(rand.nextDouble() * 4 - 2, rand.nextDouble() * 4 - 2);
bool headAtEnd = rand.nextBool();
Offset headPos = headAtEnd ? end : start;
Offset tailPos = headAtEnd ? start : end;
Vector2 matchDir = Vector2(headPos.dx - tailPos.dx, headPos.dy - tailPos.dy).normalized();
canvas.drawLine(tailPos + const Offset(4, 4), headPos + const Offset(4, 4), Paint()..color = Colors.black.withOpacity(0.6)..strokeWidth = 7.0..strokeCap = StrokeCap.round); canvas.drawLine(tailPos + const Offset(4, 4), headPos + const Offset(4, 4), Paint()..color = Colors.black.withOpacity(0.6)..strokeWidth = 7.0..strokeCap = StrokeCap.round);
if (isLastMove) { canvas.drawCircle(headPos, 8.0 + (blinkValue * 6.0), Paint()..color = Colors.orangeAccent.withOpacity(0.6 * blinkValue)..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6.0)); }
if (isLastMove) { canvas.drawLine(tailPos, headPos, Paint()..color = const Color(0xFF6D4C41)..strokeWidth = 7.0..strokeCap = StrokeCap.round); canvas.drawLine(tailPos, headPos, Paint()..color = const Color(0xFFEDC498)..strokeWidth = 4.0..strokeCap = StrokeCap.round); Offset burnPos = Offset(headPos.dx - matchDir.x * 8, headPos.dy - matchDir.y * 8); canvas.drawLine(burnPos, headPos, Paint()..color = const Color(0xFF2E1A14)..strokeWidth = 6.0..strokeCap = StrokeCap.round);
canvas.drawCircle(headPos, 8.0 + (blinkValue * 6.0), Paint() canvas.save(); canvas.translate(headPos.dx, headPos.dy); double angle = atan2(matchDir.y, matchDir.x); canvas.rotate(angle); Rect headOval = Rect.fromCenter(center: Offset.zero, width: 18.0, height: 13.0); canvas.drawOval(headOval.shift(const Offset(1, 2)), Paint()..color = Colors.black.withOpacity(0.6)); canvas.drawOval(headOval, Paint()..color = headColor); canvas.restore();
..color = Colors.orangeAccent.withOpacity(0.6 * blinkValue)
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6.0)
);
}
canvas.drawLine(tailPos, headPos, Paint()..color = const Color(0xFF6D4C41)..strokeWidth = 7.0..strokeCap = StrokeCap.round);
canvas.drawLine(tailPos, headPos, Paint()..color = const Color(0xFFEDC498)..strokeWidth = 4.0..strokeCap = StrokeCap.round);
Offset burnPos = Offset(headPos.dx - matchDir.x * 8, headPos.dy - matchDir.y * 8);
canvas.drawLine(burnPos, headPos, Paint()..color = const Color(0xFF2E1A14)..strokeWidth = 6.0..strokeCap = StrokeCap.round);
canvas.save();
canvas.translate(headPos.dx, headPos.dy);
double angle = atan2(matchDir.y, matchDir.x);
canvas.rotate(angle);
Rect headOval = Rect.fromCenter(center: Offset.zero, width: 18.0, height: 13.0);
canvas.drawOval(headOval.shift(const Offset(1, 2)), Paint()..color = Colors.black.withOpacity(0.6));
canvas.drawOval(headOval, Paint()..color = headColor);
canvas.restore();
} }
void _drawNeonLine(Canvas canvas, Offset p1, Offset p2, Color color, bool isConquered, {bool isLastMove = false, double blinkValue = 0.0}) { 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; 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);
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() canvas.drawLine(p1, p2, Paint()..color = coreColor..strokeWidth = mainWidth..strokeCap = StrokeCap.round);
..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}) { 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 random = Random((p1.dx + p1.dy + p2.dx + p2.dy).toInt()); final dx = p2.dx - p1.dx; final dy = p2.dy - p1.dy;
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; 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 basePaint = Paint() 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);
..color = color 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));
..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 @override bool shouldRepaint(covariant BoardPainter oldDelegate) => true;
bool shouldRepaint(covariant BoardPainter oldDelegate) => true;
} }
class Vector2 { class Vector2 {
final double x, y; final double x, y; Vector2(this.x, this.y); double get length => sqrt(x * x + y * y);
Vector2(this.x, this.y); Vector2 normalized() { double l = length; return l == 0 ? Vector2(0, 0) : Vector2(x / l, y / l); }
double get length => sqrt(x * x + y * y);
Vector2 normalized() {
double l = length;
return l == 0 ? Vector2(0, 0) : Vector2(x / l, y / l);
}
} }

View file

@ -6,12 +6,27 @@ import 'dart:ui';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../logic/game_controller.dart'; import '../../logic/game_controller.dart';
import '../../core/theme_manager.dart'; import '../../core/theme_manager.dart';
import '../../core/app_colors.dart'; import '../../core/app_colors.dart';
import 'board_painter.dart'; import 'board_painter.dart';
import 'score_board.dart'; import 'score_board.dart';
import '../../models/game_board.dart'; import 'package:google_fonts/google_fonts.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));
}
return baseStyle;
}
class GameScreen extends StatefulWidget { class GameScreen extends StatefulWidget {
const GameScreen({super.key}); const GameScreen({super.key});
@ -25,20 +40,19 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
bool _gameOverDialogShown = false; bool _gameOverDialogShown = false;
bool _opponentLeftDialogShown = false; bool _opponentLeftDialogShown = false;
// Variabili per coprire il posizionamento del Jolly in Locale
bool _hideJokerMessage = false;
bool _wasSetupPhase = false;
Player _lastJokerTurn = Player.red;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_blinkController = AnimationController( _blinkController = AnimationController(vsync: this, duration: const Duration(milliseconds: 600))..repeat(reverse: true);
vsync: this,
duration: const Duration(milliseconds: 600),
)..repeat(reverse: true);
} }
@override @override
void dispose() { void dispose() { _blinkController.dispose(); super.dispose(); }
_blinkController.dispose();
super.dispose();
}
void _showGameOverDialog(BuildContext context, GameController game, ThemeColors theme, AppThemeType themeType) { void _showGameOverDialog(BuildContext context, GameController game, ThemeColors theme, AppThemeType themeType) {
_gameOverDialogShown = true; _gameOverDialogShown = true;
@ -50,22 +64,21 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
builder: (context, controller, child) { builder: (context, controller, child) {
if (!controller.isGameOver) { if (!controller.isGameOver) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (Navigator.canPop(dialogContext)) Navigator.pop(dialogContext); if (_gameOverDialogShown) {
_gameOverDialogShown = false; _gameOverDialogShown = false;
if (Navigator.canPop(dialogContext)) Navigator.pop(dialogContext);
}
}); });
return const SizedBox(); return const SizedBox.shrink();
} }
int red = controller.board.scoreRed; int red = controller.board.scoreRed; int blue = controller.board.scoreBlue;
int blue = controller.board.scoreBlue;
bool playerBeatCPU = controller.isVsCPU && red > blue; bool playerBeatCPU = controller.isVsCPU && red > blue;
String nameRed = controller.isOnline ? controller.onlineHostName.toUpperCase() : "TU"; String nameRed = controller.isOnline ? controller.onlineHostName.toUpperCase() : "TU";
String nameBlue = controller.isOnline ? controller.onlineGuestName.toUpperCase() : (themeType == AppThemeType.cyberpunk ? "VERDE" : "BLU"); String nameBlue = controller.isOnline ? controller.onlineGuestName.toUpperCase() : (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? "VERDE" : "BLU");
if (controller.isVsCPU) nameBlue = "CPU"; if (controller.isVsCPU) nameBlue = "CPU";
String winnerText = ""; String winnerText = ""; Color winnerColor = theme.text;
Color winnerColor = theme.text;
if (red > blue) { winnerText = "VINCE $nameRed!"; winnerColor = theme.playerRed; } if (red > blue) { winnerText = "VINCE $nameRed!"; winnerColor = theme.playerRed; }
else if (blue > red) { winnerText = "VINCE $nameBlue!"; winnerColor = theme.playerBlue; } else if (blue > red) { winnerText = "VINCE $nameBlue!"; winnerColor = theme.playerBlue; }
else { winnerText = "PAREGGIO!"; winnerColor = theme.text; } else { winnerText = "PAREGGIO!"; winnerColor = theme.text; }
@ -73,11 +86,11 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
return AlertDialog( return AlertDialog(
backgroundColor: theme.background, backgroundColor: theme.background,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: BorderSide(color: winnerColor.withOpacity(0.5), width: 2)), 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)), title: Text("FINE PARTITA", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 22))),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text(winnerText, textAlign: TextAlign.center, style: TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: winnerColor)), Text(winnerText, textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: winnerColor))),
const SizedBox(height: 20), const SizedBox(height: 20),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
@ -85,24 +98,39 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text("$nameRed: $red", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerRed)), Text("$nameRed: $red", style: _getTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerRed))),
Text(" - ", style: TextStyle(fontSize: 18, color: theme.text)), Text(" - ", style: _getTextStyle(themeType, TextStyle(fontSize: 18, color: theme.text))),
Text("$nameBlue: $blue", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerBlue)), 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 ? [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) ...[ if (controller.isVsCPU) ...[
const SizedBox(height: 15), const SizedBox(height: 15),
Text("Difficoltà CPU: Livello ${controller.cpuLevel}", style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: theme.text.withOpacity(0.7))), Text("Difficoltà CPU: Livello ${controller.cpuLevel}", style: _getTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: theme.text.withOpacity(0.7)))),
], ],
if (controller.isOnline) ...[ if (controller.isOnline) ...[
const SizedBox(height: 20), const SizedBox(height: 20),
if (controller.rematchRequested && !controller.opponentWantsRematch) if (controller.rematchRequested && !controller.opponentWantsRematch)
Text("In attesa di $nameBlue...", style: TextStyle(color: Colors.amber, fontWeight: FontWeight.bold, fontStyle: FontStyle.italic)), Text("In attesa di $nameBlue...", style: _getTextStyle(themeType, const TextStyle(color: Colors.amber, fontWeight: FontWeight.bold, fontStyle: FontStyle.italic))),
if (controller.opponentWantsRematch && !controller.rematchRequested) if (controller.opponentWantsRematch && !controller.rematchRequested)
Text("$nameBlue vuole la rivincita!", style: TextStyle(color: Colors.greenAccent, fontWeight: FontWeight.bold)), Text("$nameBlue vuole la rivincita!", style: _getTextStyle(themeType, const TextStyle(color: Colors.greenAccent, fontWeight: FontWeight.bold))),
if (controller.rematchRequested && controller.opponentWantsRematch) if (controller.rematchRequested && controller.opponentWantsRematch)
Text("Avvio nuova partita...", style: TextStyle(color: Colors.green, fontWeight: FontWeight.bold)), Text("Avvio nuova partita...", style: _getTextStyle(themeType, const TextStyle(color: Colors.green, fontWeight: FontWeight.bold))),
] ]
], ],
), ),
@ -115,32 +143,30 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
if (playerBeatCPU) if (playerBeatCPU)
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: winnerColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5), 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(dialogContext); _gameOverDialogShown = false; controller.increaseLevelAndRestart(); }, onPressed: () { controller.increaseLevelAndRestart(); },
child: const Text("PROSSIMO LIVELLO ➔", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), child: Text("PROSSIMO LIVELLO ➔", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16))),
) )
else if (controller.isOnline) else if (controller.isOnline)
ElevatedButton( 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), 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 : () { onPressed: controller.rematchRequested ? null : () { controller.requestRematch(); },
controller.requestRematch(); child: Text(controller.opponentWantsRematch ? "ACCETTA RIVINCITA" : "CHIEDI RIVINCITA", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 1.0))),
},
child: Text(controller.opponentWantsRematch ? "ACCETTA RIVINCITA" : "CHIEDI RIVINCITA", style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 1.0)),
) )
else else
ElevatedButton( 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), 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(dialogContext); _gameOverDialogShown = false; controller.startNewGame(controller.board.radius, vsCPU: controller.isVsCPU); }, onPressed: () { controller.startNewGame(controller.board.radius, vsCPU: controller.isVsCPU, shape: controller.board.shape, timeMode: controller.isTimeMode); },
child: const Text("RIGIOCA", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 2)), child: Text("RIGIOCA", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 2))),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
OutlinedButton( 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))), 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: () { onPressed: () {
if (controller.isOnline) controller.disconnectOnlineGame(); if (controller.isOnline) controller.disconnectOnlineGame();
Navigator.pop(dialogContext); _gameOverDialogShown = false;
Navigator.pop(context); Navigator.pop(dialogContext); Navigator.pop(context);
}, },
child: Text("TORNA AL MENU", style: TextStyle(fontWeight: FontWeight.bold, color: theme.text, fontSize: 14, letterSpacing: 1.5)), child: Text("TORNA AL MENU", style: _getTextStyle(themeType, TextStyle(fontWeight: FontWeight.bold, color: theme.text, fontSize: 14, letterSpacing: 1.5))),
), ),
], ],
) )
@ -151,6 +177,68 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
); );
} }
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 {
// --- TESTI MODALITÀ LOCALE ---
String pName = gameController.jokerTurn == Player.red ? "ROSSO" : (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? "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 ? 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) {
return Container(decoration: BoxDecoration(color: Colors.black.withOpacity(0.9), borderRadius: BorderRadius.circular(20), border: Border.all(color: Colors.yellowAccent, width: 2), boxShadow: [const BoxShadow(color: Colors.yellowAccent, 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.wood) {
return Container(decoration: BoxDecoration(color: const Color(0xFF5D4037), borderRadius: BorderRadius.circular(15), border: Border.all(color: const Color(0xFF3E2723), width: 4), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.6), blurRadius: 15, offset: const Offset(0, 8))]), 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, borderRadius: BorderRadius.circular(20), border: Border.all(color: theme.gridLine, width: 2), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.15), blurRadius: 20, offset: const Offset(0, 10))]), child: content);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final themeManager = context.watch<ThemeManager>(); final themeManager = context.watch<ThemeManager>();
@ -158,6 +246,18 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
final theme = themeManager.currentColors; final theme = themeManager.currentColors;
final gameController = context.watch<GameController>(); final gameController = context.watch<GameController>();
// --- LOGICA CAMBIO TURNO E SCHERMATA JOLLY ---
if (gameController.isSetupPhase && !_wasSetupPhase) {
// È appena iniziata una nuova partita
_hideJokerMessage = false;
_lastJokerTurn = Player.red;
} else if (gameController.isSetupPhase && gameController.jokerTurn != _lastJokerTurn) {
// È cambiato il turno durante il setup (in modalità locale), rifacciamo apparire la copertura
_hideJokerMessage = false;
_lastJokerTurn = gameController.jokerTurn;
}
_wasSetupPhase = gameController.isSetupPhase;
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (gameController.opponentLeft && !_opponentLeftDialogShown) { if (gameController.opponentLeft && !_opponentLeftDialogShown) {
_opponentLeftDialogShown = true; _opponentLeftDialogShown = true;
@ -167,14 +267,14 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
builder: (dialogContext) => AlertDialog( builder: (dialogContext) => AlertDialog(
backgroundColor: theme.background, backgroundColor: theme.background,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: Text("VITTORIA A TAVOLINO!", textAlign: TextAlign.center, style: TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold)), 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: TextStyle(color: theme.text, fontSize: 16)), 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, actionsAlignment: MainAxisAlignment.center,
actions: [ actions: [
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: theme.playerBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), style: ElevatedButton.styleFrom(backgroundColor: theme.playerBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))),
onPressed: () { gameController.disconnectOnlineGame(); Navigator.pop(dialogContext); Navigator.pop(context); }, onPressed: () { gameController.disconnectOnlineGame(); Navigator.pop(dialogContext); Navigator.pop(context); },
child: const Text("MENU PRINCIPALE", style: TextStyle(fontWeight: FontWeight.bold)), child: Text("MENU PRINCIPALE", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold))),
) )
], ],
) )
@ -188,7 +288,7 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
if (themeType == AppThemeType.wood) bgImage = 'assets/images/wood_bg.jpg'; if (themeType == AppThemeType.wood) bgImage = 'assets/images/wood_bg.jpg';
if (themeType == AppThemeType.doodle) bgImage = 'assets/images/doodle_bg.jpg'; if (themeType == AppThemeType.doodle) bgImage = 'assets/images/doodle_bg.jpg';
Color indicatorColor = themeType == AppThemeType.cyberpunk ? Colors.white : Colors.black; Color indicatorColor = themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? Colors.white : Colors.black;
Widget emojiBar = const SizedBox(); Widget emojiBar = const SizedBox();
if (gameController.isOnline && !gameController.isGameOver) { if (gameController.isOnline && !gameController.isGameOver) {
@ -196,7 +296,7 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
emojiBar = Container( emojiBar = Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration( decoration: BoxDecoration(
color: themeType == AppThemeType.cyberpunk ? Colors.black.withOpacity(0.6) : Colors.white.withOpacity(0.8), color: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? Colors.black.withOpacity(0.6) : Colors.white.withOpacity(0.8),
borderRadius: BorderRadius.circular(30), borderRadius: BorderRadius.circular(30),
border: Border.all(color: themeType == AppThemeType.cyberpunk ? theme.playerBlue.withOpacity(0.3) : Colors.black12, width: 2), border: Border.all(color: themeType == AppThemeType.cyberpunk ? theme.playerBlue.withOpacity(0.3) : Colors.black12, width: 2),
), ),
@ -220,26 +320,39 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
child: Center( child: Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(10.0), padding: const EdgeInsets.all(10.0),
child: AspectRatio(
aspectRatio: 1,
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
return GestureDetector( 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: GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTapDown: (details) => _handleTap(details.localPosition, constraints.maxWidth, gameController, themeType), onTapDown: (details) => _handleTap(details.localPosition, actualWidth, actualHeight, gameController, themeType),
child: AnimatedBuilder( child: AnimatedBuilder(
animation: _blinkController, animation: _blinkController,
builder: (context, child) { builder: (context, child) {
return CustomPaint( return CustomPaint(
size: Size(constraints.maxWidth, constraints.maxHeight), size: Size(actualWidth, actualHeight),
painter: BoardPainter(board: gameController.board, theme: theme, themeType: themeType, blinkValue: _blinkController.value), 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,
), ),
); );
} }
), ),
), ),
);
}
),
), ),
), ),
), ),
@ -257,7 +370,7 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(Icons.smart_toy_rounded, size: 16, color: indicatorColor), const SizedBox(width: 8), Icon(Icons.smart_toy_rounded, size: 16, color: indicatorColor), const SizedBox(width: 8),
Text("LIVELLO CPU: ${gameController.cpuLevel}", style: TextStyle(color: indicatorColor, fontWeight: FontWeight.bold, fontSize: 13, letterSpacing: 1.0)), Text("LIVELLO CPU: ${gameController.cpuLevel}", style: _getTextStyle(themeType, TextStyle(color: indicatorColor, fontWeight: FontWeight.bold, fontSize: 11, letterSpacing: 1.0))),
], ],
), ),
) )
@ -267,10 +380,10 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
Container( Container(
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.4), offset: const Offset(0, 4), blurRadius: 5)]), decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.4), offset: const Offset(0, 4), blurRadius: 5)]),
child: TextButton.icon( child: TextButton.icon(
style: TextButton.styleFrom(backgroundColor: bgImage != null ? Colors.black87 : theme.background, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: BorderSide(color: Colors.white.withOpacity(0.1), width: 1))), 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 ? Colors.white : theme.text, size: 20), icon: Icon(Icons.exit_to_app, color: bgImage != null || themeType == AppThemeType.arcade ? Colors.white : theme.text, size: 20),
onPressed: () { gameController.disconnectOnlineGame(); Navigator.pop(context); }, onPressed: () { gameController.disconnectOnlineGame(); Navigator.pop(context); },
label: Text("ESCI", style: TextStyle(color: bgImage != null ? Colors.white : theme.text, fontWeight: FontWeight.bold, fontSize: 14)), label: Text("ESCI", style: _getTextStyle(themeType, TextStyle(color: bgImage != null || themeType == AppThemeType.arcade ? Colors.white : theme.text, fontWeight: FontWeight.bold, fontSize: 12))),
), ),
), ),
], ],
@ -280,19 +393,9 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
), ),
if (gameController.myReaction != null) if (gameController.myReaction != null)
Positioned( Positioned(top: 80, left: gameController.isHost ? 30 : null, right: gameController.isHost ? null : 30, child: _BouncingEmoji(emoji: gameController.myReaction!)),
top: 80,
left: gameController.isHost ? 30 : null,
right: gameController.isHost ? null : 30,
child: _BouncingEmoji(emoji: gameController.myReaction!),
),
if (gameController.opponentReaction != null) if (gameController.opponentReaction != null)
Positioned( Positioned(top: 80, left: !gameController.isHost ? 30 : null, right: !gameController.isHost ? null : 30, child: _BouncingEmoji(emoji: gameController.opponentReaction!)),
top: 80,
left: !gameController.isHost ? 30 : null,
right: !gameController.isHost ? null : 30,
child: _BouncingEmoji(emoji: gameController.opponentReaction!),
),
], ],
), ),
); );
@ -308,26 +411,36 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
decoration: bgImage != null ? BoxDecoration(image: DecorationImage(image: AssetImage(bgImage), fit: BoxFit.cover, colorFilter: themeType == AppThemeType.doodle ? ColorFilter.mode(Colors.white.withOpacity(0.7), BlendMode.lighten) : null)) : null, decoration: bgImage != null ? BoxDecoration(image: DecorationImage(image: AssetImage(bgImage), fit: BoxFit.cover, colorFilter: themeType == AppThemeType.doodle ? ColorFilter.mode(Colors.white.withOpacity(0.7), BlendMode.lighten) : null)) : null,
child: Stack( child: Stack(
children: [ children: [
if (gameController.isTimeMode && !gameController.isCPUThinking && !gameController.isGameOver && gameController.timeLeft > 0 && gameController.timeLeft <= 5) 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)), Positioned.fill(child: BlitzBackgroundEffect(timeLeft: gameController.timeLeft, color: theme.playerRed, themeType: themeType)),
if (gameController.effectText.isNotEmpty) if (gameController.effectText.isNotEmpty)
Positioned.fill(child: SpecialEventBackgroundEffect(text: gameController.effectText, color: gameController.effectColor)), Positioned.fill(child: SpecialEventBackgroundEffect(text: gameController.effectText, color: gameController.effectColor, themeType: themeType)),
Positioned.fill(child: gameContent), Positioned.fill(child: gameContent),
// ========================================== // --- SCHERMATA COPRENTE PER IL PASSAGGIO DEL TELEFONO IN LOCALE ---
// EFFETTI VISIVI (VFX) DI FINE PARTITA if (gameController.isSetupPhase && !_hideJokerMessage)
// ==========================================
if (gameController.isGameOver && gameController.board.scoreRed != gameController.board.scoreBlue)
Positioned.fill( Positioned.fill(
child: IgnorePointer( child: Container(
child: WinnerVFXOverlay( // Il colore di sfondo riempie tutto lo schermo per non far sbirciare la griglia
winnerColor: gameController.board.scoreRed > gameController.board.scoreBlue ? theme.playerRed : theme.playerBlue, color: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade
themeType: themeType, ? Colors.black
: 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))),
], ],
), ),
), ),
@ -336,29 +449,24 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
); );
} }
void _handleTap(Offset tapPos, double size, GameController controller, AppThemeType themeType) { void _handleTap(Offset tapPos, double width, double height, GameController controller, AppThemeType themeType) {
final board = controller.board; final board = controller.board;
if (board.isGameOver) return; if (board.isGameOver) return;
int cols = board.columns + 1; double spacing = width / cols; double offset = spacing / 2;
int gridPoints = board.radius * 2 + 2; if (controller.isSetupPhase) {
double spacing = size / gridPoints; int bx = ((tapPos.dx - offset) / spacing).floor(); int by = ((tapPos.dy - offset) / spacing).floor();
double offset = spacing / 2; 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; }
} }
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); } if (closestLine != null) { controller.handleLineTap(closestLine, themeType); }
} }
@ -371,30 +479,16 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
} }
} }
// ===========================================================================
// CLASSI PER IL MOTORE PARTICELLARE (VFX) DI FINE PARTITA
// ===========================================================================
class _Particle { class _Particle {
double x, y; double x, y, vx, vy, size, angle, spin;
double vx, vy; Color color; int type;
Color color;
double size;
double angle;
double spin;
int type; // 0=cerchio, 1=quadrato, 2=triangolo
_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}); _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 { class WinnerVFXOverlay extends StatefulWidget {
final Color winnerColor; final Color winnerColor; final AppThemeType themeType;
final AppThemeType themeType;
const WinnerVFXOverlay({super.key, required this.winnerColor, required this.themeType}); const WinnerVFXOverlay({super.key, required this.winnerColor, required this.themeType});
@override State<WinnerVFXOverlay> createState() => _WinnerVFXOverlayState();
@override
State<WinnerVFXOverlay> createState() => _WinnerVFXOverlayState();
} }
class _WinnerVFXOverlayState extends State<WinnerVFXOverlay> with SingleTickerProviderStateMixin { class _WinnerVFXOverlayState extends State<WinnerVFXOverlay> with SingleTickerProviderStateMixin {
@ -406,291 +500,125 @@ class _WinnerVFXOverlayState extends State<WinnerVFXOverlay> with SingleTickerPr
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// L'animazione gira a 60fps per 4 secondi e poi si ferma _vfxController = AnimationController(vsync: this, duration: const Duration(seconds: 4))..addListener(() { _updateParticles(); })..forward();
_vfxController = AnimationController(vsync: this, duration: const Duration(seconds: 4))
..addListener(() {
_updateParticles();
})
..forward();
} }
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
if (!_initialized) { if (!_initialized) { _initParticles(MediaQuery.of(context).size); _initialized = true; }
_initParticles(MediaQuery.of(context).size);
_initialized = true;
}
} }
void _initParticles(Size screenSize) { void _initParticles(Size screenSize) {
int particleCount = widget.themeType == AppThemeType.cyberpunk ? 150 : 100; int particleCount = widget.themeType == AppThemeType.cyberpunk ? 150 : 100;
if (widget.themeType == AppThemeType.arcade) particleCount = 80;
if (widget.themeType == AppThemeType.grimorio) particleCount = 120;
// Lista di colori da mixare (colore vincitore + bianco + colori a tema)
List<Color> palette = [widget.winnerColor, widget.winnerColor.withOpacity(0.7), Colors.white]; List<Color> palette = [widget.winnerColor, widget.winnerColor.withOpacity(0.7), Colors.white];
if (widget.themeType == AppThemeType.cyberpunk) { if (widget.themeType == AppThemeType.cyberpunk) { palette.add(Colors.cyanAccent); palette.add(Colors.yellowAccent); }
palette.add(Colors.cyanAccent); else if (widget.themeType == AppThemeType.doodle) { palette.add(const Color(0xFF00008B)); palette.add(Colors.redAccent); }
palette.add(Colors.yellowAccent); else if (widget.themeType == AppThemeType.wood) { palette = [Colors.orangeAccent, Colors.yellow, Colors.red, Colors.white]; }
} else if (widget.themeType == AppThemeType.doodle) { else if (widget.themeType == AppThemeType.arcade) { palette = [widget.winnerColor, Colors.white, Colors.greenAccent]; }
palette.add(const Color(0xFF00008B)); // Inchiostro biro else if (widget.themeType == AppThemeType.grimorio) { palette = [widget.winnerColor, Colors.deepPurpleAccent, Colors.white]; }
palette.add(Colors.redAccent);
} else if (widget.themeType == AppThemeType.wood) {
palette = [Colors.orangeAccent, Colors.yellow, Colors.red, Colors.white];
}
for (int i = 0; i < particleCount; i++) { for (int i = 0; i < particleCount; i++) {
// Esplosione dal centro verso l'esterno
double speed = _rand.nextDouble() * 20 + 5; double speed = _rand.nextDouble() * 20 + 5;
double theta = _rand.nextDouble() * 2 * math.pi; 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)));
_particles.add(_Particle(
x: screenSize.width / 2,
y: screenSize.height / 2,
vx: speed * math.cos(theta),
vy: speed * math.sin(theta) - 5, // Leggera spinta verso l'alto
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() { void _updateParticles() {
setState(() { setState(() {
for (var p in _particles) { for (var p in _particles) {
p.x += p.vx; p.x += p.vx; p.y += p.vy;
p.y += p.vy; if (widget.themeType == AppThemeType.cyberpunk) { p.vy += 0.1; p.vx *= 0.98; p.vy *= 0.98; }
else if (widget.themeType == AppThemeType.wood) { p.vy -= 0.2; p.x += math.sin(p.y * 0.05) * 2; }
// Gravità e attrito else if (widget.themeType == AppThemeType.arcade) { p.vy += 0.3; p.spin = 0; p.angle = 0; }
if (widget.themeType == AppThemeType.cyberpunk) { else if (widget.themeType == AppThemeType.grimorio) { p.vy -= 0.1; p.x += math.sin(p.y * 0.02) * 1.5; p.size *= 0.995; }
p.vy += 0.1; // Gravità bassa (fluttuano di più) else { p.vy += 0.5; }
p.vx *= 0.98; // Attrito p.angle += p.spin; p.size *= 0.99;
p.vy *= 0.98;
} else if (widget.themeType == AppThemeType.wood) {
p.vy -= 0.2; // Vanno verso l'alto come fumo/scintille!
p.x += math.sin(p.y * 0.05) * 2; // Tremolio
} else {
p.vy += 0.5; // Gravità standard (coriandoli cadono)
}
p.angle += p.spin;
p.size *= 0.99; // Si rimpiccioliscono nel tempo
} }
}); });
} }
@override @override void dispose() { _vfxController.dispose(); super.dispose(); }
void dispose() { @override Widget build(BuildContext context) { return CustomPaint(painter: _VFXPainter(particles: _particles, themeType: widget.themeType), child: Container()); }
_vfxController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: _VFXPainter(particles: _particles, themeType: widget.themeType),
child: Container(),
);
}
} }
class _VFXPainter extends CustomPainter { class _VFXPainter extends CustomPainter {
final List<_Particle> particles; final List<_Particle> particles; final AppThemeType themeType;
final AppThemeType themeType;
_VFXPainter({required this.particles, required this.themeType}); _VFXPainter({required this.particles, required this.themeType});
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
for (var p in particles) { for (var p in particles) {
if (p.size < 0.5) continue; if (p.size < 0.5) continue;
final paint = Paint()..color = p.color..style = PaintingStyle.fill;
final paint = Paint() if (themeType == AppThemeType.cyberpunk) { paint.maskFilter = const MaskFilter.blur(BlurStyle.solid, 4.0); }
..color = p.color canvas.save(); canvas.translate(p.x, p.y); canvas.rotate(p.angle);
..style = PaintingStyle.fill;
// Glow per il Cyberpunk
if (themeType == AppThemeType.cyberpunk) {
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) { if (themeType == AppThemeType.doodle) {
// Stile schizzato paint.style = PaintingStyle.stroke; paint.strokeWidth = 2.0;
paint.style = PaintingStyle.stroke; 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); }
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.wood) { } else if (themeType == AppThemeType.wood) {
// Scintille rotonde e sfuocate paint.maskFilter = const MaskFilter.blur(BlurStyle.normal, 3.0); canvas.drawCircle(Offset.zero, p.size, paint);
paint.maskFilter = const MaskFilter.blur(BlurStyle.normal, 3.0); } 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, paint);
canvas.drawCircle(Offset.zero, p.size * 0.3, Paint()..color=Colors.white..style=PaintingStyle.fill);
} else { } else {
// Forme standard per Minimal e Cyberpunk if (p.type == 0) { canvas.drawCircle(Offset.zero, p.size, paint); }
if (p.type == 0) { else if (p.type == 1) { canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: p.size * 2, height: p.size * 2), paint); }
canvas.drawCircle(Offset.zero, p.size, paint); else { var path = Path()..moveTo(0, -p.size)..lineTo(p.size, p.size)..lineTo(-p.size, p.size)..close(); canvas.drawPath(path, 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(); canvas.restore();
} }
} }
@override bool shouldRepaint(covariant _VFXPainter oldDelegate) => true;
@override
bool shouldRepaint(covariant _VFXPainter oldDelegate) => true;
} }
// ===========================================================================
// WIDGET INTERNI ESISTENTENTI
// ===========================================================================
class _BouncingEmoji extends StatefulWidget { class _BouncingEmoji extends StatefulWidget {
final String emoji; final String emoji; const _BouncingEmoji({required this.emoji});
const _BouncingEmoji({required this.emoji}); @override State<_BouncingEmoji> createState() => _BouncingEmojiState();
@override
State<_BouncingEmoji> createState() => _BouncingEmojiState();
} }
class _BouncingEmojiState extends State<_BouncingEmoji> with SingleTickerProviderStateMixin { class _BouncingEmojiState extends State<_BouncingEmoji> with SingleTickerProviderStateMixin {
late AnimationController _ctrl; late AnimationController _ctrl; late Animation<double> _anim;
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 @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))))); }
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 { class FullScreenGridPainter extends CustomPainter {
final Color gridColor; final Color gridColor; FullScreenGridPainter(this.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;
@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 { class BlitzBackgroundEffect extends StatefulWidget {
final int timeLeft; final int timeLeft; final Color color; final AppThemeType themeType;
final Color color; const BlitzBackgroundEffect({super.key, required this.timeLeft, required this.color, required this.themeType});
const BlitzBackgroundEffect({super.key, required this.timeLeft, required this.color}); @override State<BlitzBackgroundEffect> createState() => _BlitzBackgroundEffectState();
@override
State<BlitzBackgroundEffect> createState() => _BlitzBackgroundEffectState();
} }
class _BlitzBackgroundEffectState extends State<BlitzBackgroundEffect> with SingleTickerProviderStateMixin { class _BlitzBackgroundEffectState extends State<BlitzBackgroundEffect> with SingleTickerProviderStateMixin {
late AnimationController _controller; late AnimationController _controller;
@override @override void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 400))..repeat(reverse: true); }
void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 400))..repeat(reverse: true); } @override void dispose() { _controller.dispose(); super.dispose(); }
@override @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)))))); }); }
void dispose() { _controller.dispose(); super.dispose(); }
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Container(
color: widget.color.withOpacity(0.12 * _controller.value),
child: Center(
child: ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: 2.0, sigmaY: 2.0),
child: Text('${widget.timeLeft}', style: TextStyle(fontSize: 300, fontWeight: FontWeight.w900, color: widget.color.withOpacity(0.35 + (0.3 * _controller.value)), height: 1.0)),
),
),
);
},
);
}
} }
class SpecialEventBackgroundEffect extends StatefulWidget { class SpecialEventBackgroundEffect extends StatefulWidget {
final String text; final String text; final Color color; final AppThemeType themeType;
final Color color; const SpecialEventBackgroundEffect({super.key, required this.text, required this.color, required this.themeType});
const SpecialEventBackgroundEffect({super.key, required this.text, required this.color}); @override State<SpecialEventBackgroundEffect> createState() => _SpecialEventBackgroundEffectState();
@override
State<SpecialEventBackgroundEffect> createState() => _SpecialEventBackgroundEffectState();
} }
class _SpecialEventBackgroundEffectState extends State<SpecialEventBackgroundEffect> with SingleTickerProviderStateMixin { class _SpecialEventBackgroundEffectState extends State<SpecialEventBackgroundEffect> with SingleTickerProviderStateMixin {
late AnimationController _controller; late AnimationController _controller; late Animation<double> _scaleAnimation; late Animation<double> _opacityAnimation;
late Animation<double> _scaleAnimation; @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)); }
late Animation<double> _opacityAnimation; @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 @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))))))); }); }
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 1000))..forward();
_scaleAnimation = Tween<double>(begin: 0.5, end: 1.5).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic));
_opacityAnimation = Tween<double>(begin: 0.9, end: 0.0).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn));
}
@override
void didUpdateWidget(covariant SpecialEventBackgroundEffect oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.text != widget.text) { _controller.reset(); _controller.forward(); }
}
@override
void dispose() { _controller.dispose(); super.dispose(); }
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Center(
child: Transform.scale(
scale: _scaleAnimation.value,
child: Opacity(
opacity: _opacityAnimation.value,
child: ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0),
child: Text(widget.text, style: TextStyle(fontSize: 250, fontWeight: FontWeight.w900, color: widget.color, height: 1.0)),
),
),
),
);
},
);
}
} }

View file

@ -1,5 +1,10 @@
// ===========================================================================
// FILE: lib/ui/game/score_board.dart
// ===========================================================================
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
// Import separati e puliti
import '../../logic/game_controller.dart'; import '../../logic/game_controller.dart';
import '../../models/game_board.dart'; import '../../models/game_board.dart';
import '../../core/theme_manager.dart'; import '../../core/theme_manager.dart';
@ -27,7 +32,6 @@ class _ScoreBoardState extends State<ScoreBoard> {
bool isRedTurn = controller.board.currentPlayer == Player.red; bool isRedTurn = controller.board.currentPlayer == Player.red;
bool isMuted = AudioService.instance.isMuted; bool isMuted = AudioService.instance.isMuted;
// --- LOGICA PER I NOMI ---
String nameRed = "ROSSO"; String nameRed = "ROSSO";
String nameBlue = themeType == AppThemeType.cyberpunk ? "VERDE" : "BLU"; String nameBlue = themeType == AppThemeType.cyberpunk ? "VERDE" : "BLU";

View file

@ -6,30 +6,35 @@ import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../logic/game_controller.dart';
import '../../core/theme_manager.dart'; import '../../core/theme_manager.dart';
import '../../core/app_colors.dart'; import '../../core/app_colors.dart';
import '../../models/game_board.dart';
import '../game/game_screen.dart'; import '../game/game_screen.dart';
import '../settings/settings_screen.dart'; import '../settings/settings_screen.dart';
import '../../logic/game_controller.dart';
import '../../services/storage_service.dart'; import '../../services/storage_service.dart';
import '../multiplayer/lobby_screen.dart'; import '../multiplayer/lobby_screen.dart';
import 'history_screen.dart'; import 'history_screen.dart';
// --- HELPER PER IL FONT ---
TextStyle _getTextStyle(AppThemeType themeType, TextStyle baseStyle) { TextStyle _getTextStyle(AppThemeType themeType, TextStyle baseStyle) {
if (themeType == AppThemeType.doodle) { if (themeType == AppThemeType.doodle) {
return GoogleFonts.permanentMarker(textStyle: baseStyle); 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; return baseStyle;
} }
// ===========================================================================
// IL NOSTRO "PITTORE" DI SCARABOCCHI
// Genera bordi irregolari, tratti doppi a penna e riempimenti sbavati.
// ===========================================================================
class _DoodleBackgroundPainter extends CustomPainter { class _DoodleBackgroundPainter extends CustomPainter {
final Color fillColor; final Color fillColor;
final Color strokeColor; final Color strokeColor;
@ -101,8 +106,6 @@ class _DoodleBackgroundPainter extends CustomPainter {
} }
} }
// --- WIDGET PERSONALIZZATI ---
class _NeonShapeButton extends StatelessWidget { class _NeonShapeButton extends StatelessWidget {
final IconData icon; final IconData icon;
final String label; final String label;
@ -163,7 +166,6 @@ class _NeonShapeButton extends StatelessWidget {
); );
} }
// --- STILE STANDARD ---
Color mainColor = isSpecial && !isLocked ? Colors.purpleAccent : theme.playerBlue; Color mainColor = isSpecial && !isLocked ? Colors.purpleAccent : theme.playerBlue;
return GestureDetector( return GestureDetector(
onTap: isLocked ? null : onTap, onTap: isLocked ? null : onTap,
@ -375,8 +377,6 @@ class _NeonTimeSwitch extends StatelessWidget {
} }
} }
// ---------------------------------------------------------------------------
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
const HomeScreen({super.key}); const HomeScreen({super.key});
@ -386,6 +386,8 @@ class HomeScreen extends StatefulWidget {
class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver { class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
int _debugTapCount = 0;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -580,13 +582,11 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
); );
} }
// --- MENU SETUP PARTITA (Popup per CPU e Locale) ---
void _showMatchSetupDialog(bool isVsCPU) { void _showMatchSetupDialog(bool isVsCPU) {
int localRadius = 4; int localRadius = 4;
ArenaShape localShape = ArenaShape.classic; ArenaShape localShape = ArenaShape.classic;
bool localTimeMode = true; bool localTimeMode = true;
int cpuLevel = StorageService.instance.cpuLevel; bool isChaosUnlocked = StorageService.instance.playerLevel >= 10;
bool isChaosUnlocked = cpuLevel >= 10;
showDialog( showDialog(
context: context, context: context,
@ -681,8 +681,8 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [theme.background.withOpacity(0.95), theme.background.withOpacity(0.8)]), gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [theme.background.withOpacity(0.95), theme.background.withOpacity(0.8)]),
borderRadius: BorderRadius.circular(25), borderRadius: BorderRadius.circular(25),
border: themeType == AppThemeType.cyberpunk ? null : Border.all(color: Colors.white.withOpacity(0.15), width: 1.5), border: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? null : Border.all(color: Colors.white.withOpacity(0.15), width: 1.5),
boxShadow: themeType == AppThemeType.cyberpunk ? [] : [BoxShadow(color: Colors.black.withOpacity(0.5), blurRadius: 20, offset: const Offset(4, 10))], boxShadow: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? [] : [BoxShadow(color: Colors.black.withOpacity(0.5), blurRadius: 20, offset: const Offset(4, 10))],
), ),
child: SingleChildScrollView( child: SingleChildScrollView(
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
@ -762,7 +762,203 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
); );
} }
// --- MENU TUTORIAL (TESTO AGGIORNATO PER TETRAQ) --- // --- NUOVA FUNZIONE: MOSTRA LE SFIDE GIORNALIERE ---
Future<void> _showDailyQuestsDialog() async {
final prefs = await SharedPreferences.getInstance();
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 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("SFIDE GIORNALIERE", style: _getTextStyle(themeType, TextStyle(fontSize: 22, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 1.5))),
const SizedBox(height: 25),
// Generiamo dinamicamente le 3 missioni salvate in memoria
...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: _getTextStyle(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: _getTextStyle(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(ctx),
child: const Text("CHIUDI", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2)),
),
)
],
),
),
);
}
);
}
// --- NUOVA FUNZIONE: MOSTRA LA CLASSIFICA GLOBALE ---
void _showLeaderboardDialog() {
showDialog(
context: context,
barrierColor: Colors.black.withOpacity(0.8),
builder: (ctx) {
final themeManager = ctx.watch<ThemeManager>();
final theme = themeManager.currentColors;
final themeType = themeManager.currentThemeType;
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: [
Icon(Icons.emoji_events, size: 50, color: Colors.amber),
const SizedBox(height: 10),
Text("CLASSIFICA MONDIALE", style: _getTextStyle(themeType, TextStyle(fontSize: 20, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 1.5))),
const SizedBox(height: 20),
// Lista giocatori pescata da Firebase!
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 docs = snapshot.data!.docs;
return ListView.builder(
physics: const BouncingScrollPhysics(),
itemCount: docs.length,
itemBuilder: (context, index) {
var data = docs[index].data() as Map<String, dynamic>;
bool isMe = data['name'] == StorageService.instance.playerName;
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: _getTextStyle(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(data['name'] ?? 'Unknown', style: _getTextStyle(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)),
],
)
],
),
);
}
);
}
),
),
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(ctx),
child: const Text("CHIUDI", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w900, letterSpacing: 2)),
),
)
],
),
);
if (themeType == AppThemeType.cyberpunk) content = _AnimatedCyberBorder(child: content);
return Dialog(backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.all(20), child: content);
}
);
}
void _showTutorialDialog() { void _showTutorialDialog() {
showDialog( showDialog(
context: context, context: context,
@ -773,27 +969,49 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
final themeType = themeManager.currentThemeType; final themeType = themeManager.currentThemeType;
Color inkColor = const Color(0xFF111122); Color inkColor = const Color(0xFF111122);
String goldLabel = themeType == AppThemeType.grimorio ? "CORONA:" : "ORO:";
String bombLabel = themeType == AppThemeType.grimorio ? "STREGA:" : "BOMBA:";
String jokerLabel = themeType == AppThemeType.grimorio ? "GIULLARE:" : "JOLLY:";
Widget dialogContent = themeType == AppThemeType.doodle Widget dialogContent = themeType == AppThemeType.doodle
? Transform.rotate( ? Transform.rotate(
angle: -0.01, angle: -0.01,
child: CustomPaint( child: CustomPaint(
painter: _DoodleBackgroundPainter(fillColor: Colors.yellow.shade100, strokeColor: inkColor, seed: 400), painter: _DoodleBackgroundPainter(fillColor: Colors.yellow.shade50, strokeColor: inkColor, seed: 400),
child: Padding( child: Padding(
padding: const EdgeInsets.all(25.0), padding: const EdgeInsets.all(25.0),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text("COME GIOCARE", style: _getTextStyle(themeType, TextStyle(fontSize: 28, fontWeight: FontWeight.w900, color: inkColor, letterSpacing: 2))), Center(child: Text("COME GIOCARE", style: _getTextStyle(themeType, TextStyle(fontSize: 28, fontWeight: FontWeight.w900, color: inkColor, letterSpacing: 2)))),
const SizedBox(height: 20), const SizedBox(height: 20),
// TESTO MODIFICATO PER EVIDENZIARE IL POSIZIONAMENTO LIBERO _TutorialStep(icon: Icons.line_axis, text: "Chiudi i 4 lati di un quadrato e conquisti 1 punto e avere una mossa extra!", themeType: themeType, inkColor: inkColor, theme: theme),
_TutorialStep(icon: Icons.line_axis, text: "Lo scopo del gioco è chiudere i 4 lati di un quadrato per conquistare un punto.", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 15), const SizedBox(height: 15),
// TESTO MODIFICATO PER L'EFFETTO TOROIDALE _TutorialStep(icon: Icons.lens_blur, text: "Ma presta attenzione! ogni quadrato nasconde un insidia o un regalo!", themeType: themeType, inkColor: inkColor, theme: theme),
_TutorialStep(icon: Icons.all_out, text: "durante il gioco troverai dei bonus e dei malus impara a trarne profitto", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 15), const SizedBox(height: 15),
// TESTO MODIFICATO PER LA VITTORIA E LE FORME const Divider(color: Colors.black26, thickness: 2),
const SizedBox(height: 10),
Center(child: Text("GLOSSARIO ARENA", style: _getTextStyle(themeType, TextStyle(fontSize: 18, fontWeight: FontWeight.w900, color: inkColor)))),
const SizedBox(height: 10),
GestureDetector( _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: "SCAMBIO: 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: "GHIACCIO: 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: "x2: 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: "BUCO NERO: 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(ctx), onTap: () => Navigator.pop(ctx),
child: CustomPaint( child: CustomPaint(
painter: _DoodleBackgroundPainter(fillColor: Colors.red.shade200, strokeColor: inkColor, seed: 401), painter: _DoodleBackgroundPainter(fillColor: Colors.red.shade200, strokeColor: inkColor, seed: 401),
@ -803,6 +1021,7 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
child: Text("HO CAPITO!", style: _getTextStyle(themeType, TextStyle(fontSize: 18, fontWeight: FontWeight.w900, color: inkColor))), child: Text("HO CAPITO!", style: _getTextStyle(themeType, TextStyle(fontSize: 18, fontWeight: FontWeight.w900, color: inkColor))),
), ),
), ),
),
) )
], ],
), ),
@ -814,20 +1033,41 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [theme.background.withOpacity(0.95), theme.background.withOpacity(0.8)]), gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [theme.background.withOpacity(0.95), theme.background.withOpacity(0.8)]),
borderRadius: BorderRadius.circular(25), borderRadius: BorderRadius.circular(25),
border: themeType == AppThemeType.cyberpunk ? null : Border.all(color: Colors.white.withOpacity(0.15), width: 1.5), border: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? null : Border.all(color: Colors.white.withOpacity(0.15), width: 1.5),
boxShadow: themeType == AppThemeType.cyberpunk ? [] : [BoxShadow(color: Colors.black.withOpacity(0.5), blurRadius: 20, offset: const Offset(4, 10))], boxShadow: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? [] : [BoxShadow(color: Colors.black.withOpacity(0.5), blurRadius: 20, offset: const Offset(4, 10))],
), ),
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text("COME GIOCARE", style: _getTextStyle(themeType, TextStyle(fontSize: 24, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 2))), Center(child: Text("COME GIOCARE", style: _getTextStyle(themeType, TextStyle(fontSize: 24, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 2)))),
const SizedBox(height: 20), const SizedBox(height: 20),
// TESTO MODIFICATO _TutorialStep(icon: Icons.grid_4x4, text: "Chiudi i 4 lati di un quadrato e conquisti 1 punto e avere una mossa extra!", themeType: themeType, inkColor: inkColor, theme: theme),
_TutorialStep(icon: Icons.line_axis, text: "Lo scopo del gioco è chiudere i 4 lati di un quadrato per conquistare un punto.", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 15), const SizedBox(height: 15),
// TESTO MODIFICATO PER L'EFFETTO TOROIDALE _TutorialStep(icon: Icons.lens_blur, text: "Ma presta attenzione! ogni quadrato nasconde un insidia o un regalo!", themeType: themeType, inkColor: inkColor, theme: theme),
_TutorialStep(icon: Icons.all_out, text: "durante il gioco troverai dei bonus e dei malus impara a trarne profitto", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 15), const SizedBox(height: 15),
const Divider(color: Colors.white24, thickness: 1.5),
const SizedBox(height: 10),
Center(child: Text("GLOSSARIO ARENA", style: _getTextStyle(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: "SCAMBIO: 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: "GHIACCIO: 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: "x2: 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: "BUCO NERO: Questa casella non esiste. Se la chiudi perdi il turno.", themeType: themeType, inkColor: inkColor, theme: theme),
const SizedBox(height: 30),
SizedBox( SizedBox(
width: double.infinity, height: 50, width: double.infinity, height: 50,
child: ElevatedButton( child: ElevatedButton(
@ -838,6 +1078,7 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
) )
], ],
), ),
),
); );
if (themeType == AppThemeType.cyberpunk) dialogContent = _AnimatedCyberBorder(child: dialogContent); if (themeType == AppThemeType.cyberpunk) dialogContent = _AnimatedCyberBorder(child: dialogContent);
@ -873,13 +1114,25 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
String playerName = StorageService.instance.playerName; String playerName = StorageService.instance.playerName;
if (playerName.isEmpty) playerName = "GUEST"; if (playerName.isEmpty) playerName = "GUEST";
int level = StorageService.instance.playerLevel;
int currentXP = StorageService.instance.totalXP;
double xpProgress = (currentXP % 100) / 100.0;
Widget uiContent = SafeArea( Widget uiContent = SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 20.0), padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 20.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
// --- HEADER ---
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
@ -892,12 +1145,31 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
painter: _DoodleBackgroundPainter(fillColor: Colors.white.withOpacity(0.8), strokeColor: inkColor, seed: 1, isCircle: true), painter: _DoodleBackgroundPainter(fillColor: Colors.white.withOpacity(0.8), strokeColor: inkColor, seed: 1, isCircle: true),
child: SizedBox(width: 50, height: 50, child: Icon(Icons.person, color: inkColor, size: 30)), child: SizedBox(width: 50, height: 50, child: Icon(Icons.person, color: inkColor, size: 30)),
) )
: Container( : SizedBox(
width: 50, height: 50,
child: Stack(
fit: StackFit.expand,
children: [
CircularProgressIndicator(value: xpProgress, color: theme.playerBlue, strokeWidth: 3, backgroundColor: theme.gridLine.withOpacity(0.2)),
Padding(
padding: const EdgeInsets.all(4.0),
child: Container(
decoration: BoxDecoration(shape: BoxShape.circle, boxShadow: [BoxShadow(color: theme.playerBlue.withOpacity(0.3), blurRadius: 10, offset: const Offset(0, 4))]), decoration: BoxDecoration(shape: BoxShape.circle, boxShadow: [BoxShadow(color: theme.playerBlue.withOpacity(0.3), blurRadius: 10, offset: const Offset(0, 4))]),
child: CircleAvatar(backgroundColor: theme.playerBlue.withOpacity(0.2), radius: 25, child: Icon(Icons.person, color: theme.playerBlue, size: 30)), child: CircleAvatar(backgroundColor: theme.playerBlue.withOpacity(0.2), child: Icon(Icons.person, color: theme.playerBlue, size: 26)),
),
),
],
),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Text(playerName, style: _getTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor : theme.text, fontSize: 26, fontWeight: FontWeight.w900, letterSpacing: 1.5, shadows: themeType == AppThemeType.doodle ? [] : [Shadow(color: Colors.black.withOpacity(0.5), offset: const Offset(1, 2), blurRadius: 2)]))), Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(playerName, style: _getTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor : theme.text, fontSize: 24, fontWeight: FontWeight.w900, letterSpacing: 1.5, shadows: themeType == AppThemeType.doodle ? [] : [Shadow(color: Colors.black.withOpacity(0.5), offset: const Offset(1, 2), blurRadius: 2)]))),
Text("LIV. $level", style: _getTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.8) : theme.playerBlue, fontSize: 14, fontWeight: FontWeight.bold, letterSpacing: 1))),
],
),
], ],
), ),
), ),
@ -948,6 +1220,27 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
Center( Center(
child: Transform.rotate( child: Transform.rotate(
angle: themeType == AppThemeType.doodle ? -0.04 : 0, angle: themeType == AppThemeType.doodle ? -0.04 : 0,
// --- IL TRUCCO DELLO SVILUPPATORE PROTETTO ---
child: GestureDetector(
onTap: () {
if (kReleaseMode) return;
_debugTapCount++;
if (_debugTapCount >= 5) {
_debugTapCount = 0;
StorageService.instance.addXP(2000);
setState(() {});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("🛠 DEBUG MODE: +20 Livelli! Tutto sbloccato.", style: _getTextStyle(themeType, const TextStyle(color: Colors.white, fontWeight: FontWeight.bold))),
backgroundColor: Colors.purpleAccent,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
)
);
}
},
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text( child: Text(
"TETRAQ", "TETRAQ",
style: _getTextStyle(themeType, TextStyle( style: _getTextStyle(themeType, TextStyle(
@ -955,7 +1248,7 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
color: themeType == AppThemeType.doodle ? inkColor : theme.text, color: themeType == AppThemeType.doodle ? inkColor : theme.text,
letterSpacing: 10, letterSpacing: 10,
shadows: themeType == AppThemeType.doodle ? [] : [ shadows: themeType == AppThemeType.doodle || themeType == AppThemeType.arcade ? [] : [
BoxShadow(color: Colors.black.withOpacity(0.6), offset: const Offset(3, 6), blurRadius: 8), BoxShadow(color: Colors.black.withOpacity(0.6), offset: const Offset(3, 6), blurRadius: 8),
BoxShadow(color: theme.playerBlue.withOpacity(0.4), offset: const Offset(0, 0), blurRadius: 20), BoxShadow(color: theme.playerBlue.withOpacity(0.4), offset: const Offset(0, 0), blurRadius: 20),
] ]
@ -963,52 +1256,39 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
), ),
), ),
), ),
),
),
const Spacer(), const Spacer(),
// --- MENU PRINCIPALE CON COLORI PASTELLO --- // --- NUOVA LISTA BOTTONI CON CLASSIFICHE E SFIDE ---
Column( Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
_buildCyberCard( _buildCyberCard(_FeatureCard(title: "ONLINE", subtitle: "Sfida il mondo", icon: Icons.public, color: Colors.lightBlue.shade200, theme: theme, themeType: themeType, isFeatured: true, onTap: () { Navigator.push(context, MaterialPageRoute(builder: (_) => const LobbyScreen())); }), themeType),
_FeatureCard( const SizedBox(height: 12),
title: "ONLINE", subtitle: "Sfida il mondo", icon: Icons.public, color: Colors.lightBlue.shade200, theme: theme, themeType: themeType, isFeatured: true, _buildCyberCard(_FeatureCard(title: "VS CPU", subtitle: "Allenati con l'IA", icon: Icons.smart_toy, color: Colors.purple.shade200, theme: theme, themeType: themeType, onTap: () => _showMatchSetupDialog(true)), themeType),
onTap: () { Navigator.push(context, MaterialPageRoute(builder: (_) => const LobbyScreen())); }, const SizedBox(height: 12),
_buildCyberCard(_FeatureCard(title: "LOCALE", subtitle: "Stesso schermo", icon: Icons.people_alt, color: Colors.red.shade200, theme: theme, themeType: themeType, onTap: () => _showMatchSetupDialog(false)), themeType),
const SizedBox(height: 12),
// NUOVI BOTTONI PER LA VERSIONE 2.0
Row(
children: [
Expanded(child: _buildCyberCard(_FeatureCard(title: "CLASSIFICA", subtitle: "Top 50 Globale", icon: Icons.leaderboard, color: Colors.amber.shade200, theme: theme, themeType: themeType, onTap: _showLeaderboardDialog, compact: true), themeType)),
const SizedBox(width: 12),
Expanded(child: _buildCyberCard(_FeatureCard(title: "SFIDE", subtitle: "Missioni", icon: Icons.assignment_turned_in, color: Colors.green.shade200, theme: theme, themeType: themeType, onTap: _showDailyQuestsDialog, compact: true), themeType)),
],
), ),
themeType
), const SizedBox(height: 12),
const SizedBox(height: 14),
_buildCyberCard( Row(
_FeatureCard( children: [
title: "VS CPU", subtitle: "Allenati con l'IA", icon: Icons.smart_toy, color: Colors.purple.shade200, theme: theme, themeType: themeType, Expanded(child: _buildCyberCard(_FeatureCard(title: "TEMI", subtitle: "Personalizza", icon: Icons.palette, color: Colors.teal.shade200, theme: theme, themeType: themeType, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const SettingsScreen())), compact: true), themeType)),
onTap: () => _showMatchSetupDialog(true), const SizedBox(width: 12),
), Expanded(child: _buildCyberCard(_FeatureCard(title: "TUTORIAL", subtitle: "Come giocare", icon: Icons.school, color: Colors.indigo.shade200, theme: theme, themeType: themeType, onTap: _showTutorialDialog, compact: true), themeType)),
themeType ],
),
const SizedBox(height: 14),
_buildCyberCard(
_FeatureCard(
title: "LOCALE", subtitle: "Stesso schermo", icon: Icons.people_alt, color: Colors.red.shade200, theme: theme, themeType: themeType,
onTap: () => _showMatchSetupDialog(false),
),
themeType
),
const SizedBox(height: 14),
// --- NUOVO BOTTONE TUTORIAL ---
_buildCyberCard(
_FeatureCard(
title: "TUTORIAL", subtitle: "Come giocare", icon: Icons.school, color: Colors.indigo.shade200, theme: theme, themeType: themeType,
onTap: _showTutorialDialog,
),
themeType
),
const SizedBox(height: 14),
_buildCyberCard(
_FeatureCard(
title: "TEMI", subtitle: "Personalizza", icon: Icons.palette, color: Colors.teal.shade200, theme: theme, themeType: themeType,
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const SettingsScreen())),
),
themeType
), ),
], ],
), ),
@ -1016,6 +1296,11 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
], ],
), ),
), ),
),
),
);
},
),
); );
return Scaffold( return Scaffold(
@ -1047,32 +1332,31 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
} }
} }
// --- HELPER PER IL TESTO DEL TUTORIAL ---
class _TutorialStep extends StatelessWidget { class _TutorialStep extends StatelessWidget {
final IconData icon; final IconData icon;
final Color? iconColor;
final String text; final String text;
final AppThemeType themeType; final AppThemeType themeType;
final Color inkColor; final Color inkColor;
final ThemeColors theme; final ThemeColors theme;
const _TutorialStep({required this.icon, required this.text, required this.themeType, required this.inkColor, required this.theme}); const _TutorialStep({required this.icon, this.iconColor, required this.text, required this.themeType, required this.inkColor, required this.theme});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Icon(icon, color: themeType == AppThemeType.doodle ? inkColor : theme.playerBlue, size: 28), Icon(icon, color: iconColor ?? (themeType == AppThemeType.doodle ? inkColor : theme.playerBlue), size: 28),
const SizedBox(width: 15), const SizedBox(width: 15),
Expanded( Expanded(
child: Text(text, style: _getTextStyle(themeType, TextStyle(fontSize: 16, color: themeType == AppThemeType.doodle ? inkColor : theme.text.withOpacity(0.8), height: 1.3))), child: Text(text, style: _getTextStyle(themeType, TextStyle(fontSize: 14, color: themeType == AppThemeType.doodle ? inkColor : theme.text.withOpacity(0.8), height: 1.3))),
), ),
], ],
); );
} }
} }
// --- FEATURE CARD ---
class _FeatureCard extends StatelessWidget { class _FeatureCard extends StatelessWidget {
final String title; final String title;
final String subtitle; final String subtitle;
@ -1082,8 +1366,9 @@ class _FeatureCard extends StatelessWidget {
final AppThemeType themeType; final AppThemeType themeType;
final VoidCallback onTap; final VoidCallback onTap;
final bool isFeatured; final bool isFeatured;
final bool compact;
const _FeatureCard({required this.title, required this.subtitle, required this.icon, required this.color, required this.theme, required this.themeType, required this.onTap, this.isFeatured = false}); const _FeatureCard({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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -1102,24 +1387,26 @@ class _FeatureCard extends StatelessWidget {
seed: title.length * 5, seed: title.length * 5,
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 22.0, vertical: 16.0), padding: EdgeInsets.symmetric(horizontal: compact ? 12.0 : 22.0, vertical: compact ? 12.0 : 16.0),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Icon(icon, color: inkColor, size: 32), Icon(icon, color: inkColor, size: compact ? 24 : 32),
const SizedBox(width: 20), const SizedBox(width: 12),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text(title, style: _getTextStyle(themeType, TextStyle(color: inkColor, fontSize: 24, fontWeight: FontWeight.w900))), Text(title, style: _getTextStyle(themeType, TextStyle(color: inkColor, fontSize: compact ? 16 : 24, fontWeight: FontWeight.w900))),
if (!compact) ...[
const SizedBox(height: 2), const SizedBox(height: 2),
Text(subtitle, style: _getTextStyle(themeType, TextStyle(color: inkColor.withOpacity(0.8), fontSize: 14, fontWeight: FontWeight.bold))), Text(subtitle, style: _getTextStyle(themeType, TextStyle(color: inkColor.withOpacity(0.8), fontSize: 14, fontWeight: FontWeight.bold))),
]
], ],
), ),
), ),
Icon(Icons.chevron_right_rounded, color: inkColor.withOpacity(0.6), size: 32), if (!compact) Icon(Icons.chevron_right_rounded, color: inkColor.withOpacity(0.6), size: 32),
], ],
), ),
), ),
@ -1128,31 +1415,30 @@ class _FeatureCard extends StatelessWidget {
); );
} }
// --- STILE STANDARD ---
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: onTap,
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 14.0), padding: EdgeInsets.symmetric(horizontal: compact ? 12.0 : 20.0, vertical: compact ? 10.0 : 14.0),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: isFeatured colors: isFeatured
? [color.withOpacity(0.9), color.withOpacity(0.6)] ? [color.withOpacity(0.9), color.withOpacity(0.6)]
: [theme.background.withOpacity(0.9), theme.background.withOpacity(0.5)], : [color.withOpacity(0.25), color.withOpacity(0.05)],
), ),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(15),
border: Border.all(color: Colors.white.withOpacity(isFeatured ? 0.3 : 0.1), width: 1.5), border: Border.all(color: color.withOpacity(isFeatured ? 0.5 : 0.2), width: 1.5),
boxShadow: [ boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.6), offset: const Offset(0, 8), blurRadius: 15), BoxShadow(color: Colors.black.withOpacity(0.6), offset: const Offset(0, 8), blurRadius: 15),
BoxShadow(color: Colors.white.withOpacity(isFeatured ? 0.2 : 0.05), offset: const Offset(-1, -1), blurRadius: 5), BoxShadow(color: color.withOpacity(isFeatured ? 0.3 : 0.05), offset: const Offset(-1, -1), blurRadius: 5),
] ]
), ),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Container( Container(
padding: const EdgeInsets.all(10), padding: EdgeInsets.all(compact ? 6 : 10),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
@ -1163,23 +1449,25 @@ class _FeatureCard extends StatelessWidget {
border: Border.all(color: Colors.white.withOpacity(0.2)), border: Border.all(color: Colors.white.withOpacity(0.2)),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.2), blurRadius: 5, offset: const Offset(2, 4))] boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.2), blurRadius: 5, offset: const Offset(2, 4))]
), ),
child: Icon(icon, color: isFeatured ? Colors.white : color, size: 26), child: Icon(icon, color: isFeatured ? Colors.white : color, size: compact ? 20 : 26),
), ),
const SizedBox(width: 20), SizedBox(width: compact ? 10 : 20),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text(title, style: TextStyle(color: isFeatured ? Colors.white : theme.text, fontSize: 18, fontWeight: FontWeight.w900, shadows: [Shadow(color: Colors.black.withOpacity(0.5), offset: const Offset(1, 2), blurRadius: 2)])), Text(title, style: _getTextStyle(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), const SizedBox(height: 2),
Text(subtitle, style: TextStyle(color: isFeatured ? Colors.white.withOpacity(0.8) : theme.text.withOpacity(0.5), fontSize: 12, fontWeight: FontWeight.bold)), Text(subtitle, style: _getTextStyle(themeType, TextStyle(color: isFeatured ? Colors.white.withOpacity(0.8) : theme.text.withOpacity(0.6), fontSize: 12, fontWeight: FontWeight.bold))),
]
], ],
), ),
), ),
Icon(Icons.chevron_right_rounded, color: isFeatured ? Colors.white.withOpacity(0.7) : theme.text.withOpacity(0.3), size: 30), if (!compact) Icon(Icons.chevron_right_rounded, color: isFeatured ? Colors.white.withOpacity(0.7) : color.withOpacity(0.5), size: 30),
], ],
), ),
), ),
@ -1209,7 +1497,7 @@ class _AnimatedCyberBorderState extends State<_AnimatedCyberBorder> with SingleT
return CustomPaint( return CustomPaint(
painter: _CyberBorderPainter(animationValue: _controller.value, color1: theme.playerBlue, color2: theme.playerRed), painter: _CyberBorderPainter(animationValue: _controller.value, color1: theme.playerBlue, color2: theme.playerRed),
child: Container( child: Container(
decoration: BoxDecoration(color: theme.background.withOpacity(0.9), borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: theme.playerBlue.withOpacity(0.3), blurRadius: 25, spreadRadius: 2)]), 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), padding: const EdgeInsets.all(3),
child: widget.child, child: widget.child,
), ),
@ -1230,7 +1518,7 @@ class _CyberBorderPainter extends CustomPainter {
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
final rect = Offset.zero & size; final rect = Offset.zero & size;
final RRect rrect = RRect.fromRectAndRadius(rect, const Radius.circular(20)); final RRect rrect = RRect.fromRectAndRadius(rect, const Radius.circular(15));
final Paint paint = Paint() 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) ..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 ..style = PaintingStyle.stroke

View file

@ -1,31 +1,31 @@
// ===========================================================================
// FILE: lib/ui/multiplayer/lobby_screen.dart
// ===========================================================================
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'dart:math' as math; import 'dart:math' as math;
import '../../logic/game_controller.dart';
import '../../models/game_board.dart';
import '../../core/theme_manager.dart'; import '../../core/theme_manager.dart';
import '../../core/app_colors.dart'; import '../../core/app_colors.dart';
import '../../models/game_board.dart';
import '../../services/multiplayer_service.dart'; import '../../services/multiplayer_service.dart';
import '../../services/storage_service.dart'; import '../../services/storage_service.dart';
import '../game/game_screen.dart'; import '../game/game_screen.dart';
import '../../logic/game_controller.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
// --- HELPER PER IL FONT ---
TextStyle _getTextStyle(AppThemeType themeType, TextStyle baseStyle) { TextStyle _getTextStyle(AppThemeType themeType, TextStyle baseStyle) {
if (themeType == AppThemeType.doodle) { if (themeType == AppThemeType.doodle) {
return GoogleFonts.permanentMarker(textStyle: baseStyle); 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; return baseStyle;
} }
// --- WIDGET 3D/NEON RIUTILIZZABILI COMPATTATI ---
class _NeonShapeButton extends StatelessWidget { class _NeonShapeButton extends StatelessWidget {
final IconData icon; final IconData icon;
final String label; final String label;
@ -414,8 +414,6 @@ class _CyberBorderPainter extends CustomPainter {
bool shouldRepaint(covariant _CyberBorderPainter oldDelegate) => oldDelegate.animationValue != animationValue; bool shouldRepaint(covariant _CyberBorderPainter oldDelegate) => oldDelegate.animationValue != animationValue;
} }
// ---------------------------------------------------------------------------
class LobbyScreen extends StatefulWidget { class LobbyScreen extends StatefulWidget {
final String? initialRoomCode; final String? initialRoomCode;
@ -606,18 +604,15 @@ class _LobbyScreenState extends State<LobbyScreen> {
if (themeType == AppThemeType.doodle) bgImage = 'assets/images/doodle_bg.jpg'; if (themeType == AppThemeType.doodle) bgImage = 'assets/images/doodle_bg.jpg';
if (themeType == AppThemeType.cyberpunk) bgImage = 'assets/images/cyber_bg.jpg'; if (themeType == AppThemeType.cyberpunk) bgImage = 'assets/images/cyber_bg.jpg';
int cpuLevel = StorageService.instance.cpuLevel; bool isChaosUnlocked = true;
bool isChaosUnlocked = cpuLevel >= 10;
Color doodlePenColor = const Color(0xFF00008B); Color doodlePenColor = const Color(0xFF00008B);
// --- PANNELLO HOST ---
Widget hostPanel = Transform.rotate( Widget hostPanel = Transform.rotate(
angle: themeType == AppThemeType.doodle ? 0.01 : 0, angle: themeType == AppThemeType.doodle ? 0.01 : 0,
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 15), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 15),
decoration: BoxDecoration( decoration: BoxDecoration(
// Sfondo ancora più scuro (0.85) per il tema Cyberpunk
color: themeType == AppThemeType.cyberpunk ? Colors.black.withOpacity(0.85) : (themeType == AppThemeType.doodle ? Colors.white.withOpacity(0.5) : Colors.transparent), color: themeType == AppThemeType.cyberpunk ? Colors.black.withOpacity(0.85) : (themeType == AppThemeType.doodle ? Colors.white.withOpacity(0.5) : Colors.transparent),
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.circular(themeType == AppThemeType.doodle ? 5 : 20), topLeft: Radius.circular(themeType == AppThemeType.doodle ? 5 : 20),
@ -630,7 +625,6 @@ class _LobbyScreenState extends State<LobbyScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Rinomato da IMPOSTAZIONI STANZA a IMPOSTAZIONI GRIGLIA
Center(child: Text("IMPOSTAZIONI GRIGLIA", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(fontSize: 12, fontWeight: FontWeight.w900, color: themeType == AppThemeType.doodle ? theme.text : theme.text.withOpacity(0.6), letterSpacing: 2.0)))), Center(child: Text("IMPOSTAZIONI GRIGLIA", textAlign: TextAlign.center, style: _getTextStyle(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), const SizedBox(height: 10),
@ -686,7 +680,6 @@ class _LobbyScreenState extends State<LobbyScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
// --- INTESTAZIONE COMPATTATA ---
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
@ -712,8 +705,6 @@ class _LobbyScreenState extends State<LobbyScreen> {
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
// --- SEZIONE HOST ---
hostPanel, hostPanel,
const SizedBox(height: 15), const SizedBox(height: 15),
_NeonActionButton(label: "CREA PARTITA", color: theme.playerRed, onTap: _createRoom, theme: theme, themeType: themeType), _NeonActionButton(label: "CREA PARTITA", color: theme.playerRed, onTap: _createRoom, theme: theme, themeType: themeType),
@ -728,7 +719,6 @@ class _LobbyScreenState extends State<LobbyScreen> {
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
// --- SEZIONE JOIN ---
Transform.rotate( Transform.rotate(
angle: themeType == AppThemeType.doodle ? 0.02 : 0, angle: themeType == AppThemeType.doodle ? 0.02 : 0,
child: Container( child: Container(
@ -747,7 +737,6 @@ class _LobbyScreenState extends State<LobbyScreen> {
contentPadding: const EdgeInsets.symmetric(vertical: 12), contentPadding: const EdgeInsets.symmetric(vertical: 12),
hintText: "CODICE", hintStyle: _getTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.3), letterSpacing: 10, fontSize: 20)), counterText: "", hintText: "CODICE", hintStyle: _getTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.3), letterSpacing: 10, fontSize: 20)), counterText: "",
filled: themeType != AppThemeType.doodle, filled: themeType != AppThemeType.doodle,
// Scurito anche il campo del codice per Cyberpunk (0.85 di opacità)
fillColor: themeType == AppThemeType.cyberpunk ? Colors.black.withOpacity(0.85) : theme.text.withOpacity(0.05), 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)), 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)), focusedBorder: themeType == AppThemeType.doodle ? InputBorder.none : OutlineInputBorder(borderSide: BorderSide(color: theme.playerBlue, width: 3.0), borderRadius: BorderRadius.circular(15)),
@ -770,7 +759,6 @@ class _LobbyScreenState extends State<LobbyScreen> {
appBar: AppBar(backgroundColor: Colors.transparent, elevation: 0, iconTheme: IconThemeData(color: theme.text)), appBar: AppBar(backgroundColor: Colors.transparent, elevation: 0, iconTheme: IconThemeData(color: theme.text)),
body: Stack( body: Stack(
children: [ children: [
// 1. Sfondo
Container( Container(
decoration: bgImage != null ? BoxDecoration(image: DecorationImage(image: AssetImage(bgImage), fit: BoxFit.cover)) : null, decoration: bgImage != null ? BoxDecoration(image: DecorationImage(image: AssetImage(bgImage), fit: BoxFit.cover)) : null,
child: bgImage != null && themeType == AppThemeType.cyberpunk child: bgImage != null && themeType == AppThemeType.cyberpunk
@ -780,7 +768,6 @@ class _LobbyScreenState extends State<LobbyScreen> {
: null, : null,
), ),
// 2. Wi-Fi
if (themeType == AppThemeType.doodle) if (themeType == AppThemeType.doodle)
Positioned( Positioned(
top: 150, left: -20, right: -20, top: 150, left: -20, right: -20,
@ -801,7 +788,6 @@ class _LobbyScreenState extends State<LobbyScreen> {
), ),
), ),
// 3. UI
_isLoading ? Center(child: CircularProgressIndicator(color: theme.playerRed)) : uiContent, _isLoading ? Center(child: CircularProgressIndicator(color: theme.playerRed)) : uiContent,
], ],
), ),

View file

@ -6,15 +6,23 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../core/theme_manager.dart'; import '../../core/theme_manager.dart';
import '../../core/app_colors.dart'; import '../../core/app_colors.dart';
import '../../services/storage_service.dart';
class SettingsScreen extends StatelessWidget { class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key}); const SettingsScreen({super.key});
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final themeManager = context.watch<ThemeManager>(); final themeManager = context.watch<ThemeManager>();
final theme = themeManager.currentColors; final theme = themeManager.currentColors;
int playerLevel = StorageService.instance.playerLevel;
return Scaffold( return Scaffold(
backgroundColor: theme.background, backgroundColor: theme.background,
appBar: AppBar( appBar: AppBar(
@ -31,20 +39,8 @@ class SettingsScreen extends StatelessWidget {
subtitle: "Linee pulite, sfondo chiaro", subtitle: "Linee pulite, sfondo chiaro",
type: AppThemeType.minimal, type: AppThemeType.minimal,
previewColors: AppColors.minimal, previewColors: AppColors.minimal,
), requiredLevel: 1,
const SizedBox(height: 15), currentLevel: playerLevel,
_ThemeCard(
title: "Quaderno (Doodle)",
subtitle: "Sfondo a quadretti, tratto a penna",
type: AppThemeType.doodle,
previewColors: AppColors.doodle,
),
const SizedBox(height: 15),
_ThemeCard(
title: "Cyberpunk",
subtitle: "Nero profondo, luci al neon",
type: AppThemeType.cyberpunk,
previewColors: AppColors.cyberpunk,
), ),
const SizedBox(height: 15), const SizedBox(height: 15),
_ThemeCard( _ThemeCard(
@ -52,6 +48,44 @@ class SettingsScreen extends StatelessWidget {
subtitle: "Tavolo di legno, linee come fiammiferi", subtitle: "Tavolo di legno, linee come fiammiferi",
type: AppThemeType.wood, type: AppThemeType.wood,
previewColors: AppColors.wood, previewColors: AppColors.wood,
requiredLevel: 3,
currentLevel: playerLevel,
),
const SizedBox(height: 15),
_ThemeCard(
title: "Quaderno (Doodle)",
subtitle: "Sfondo a quadretti, tratto a penna",
type: AppThemeType.doodle,
previewColors: AppColors.doodle,
requiredLevel: 5,
currentLevel: playerLevel,
),
const SizedBox(height: 15),
_ThemeCard(
title: "Cyberpunk",
subtitle: "Nero profondo, luci al neon",
type: AppThemeType.cyberpunk,
previewColors: AppColors.cyberpunk,
requiredLevel: 7,
currentLevel: playerLevel,
),
const SizedBox(height: 15),
_ThemeCard(
title: "8-Bit Arcade",
subtitle: "Sale giochi, fosfori verdi e pixel",
type: AppThemeType.arcade,
previewColors: AppColors.arcade,
requiredLevel: 10,
currentLevel: playerLevel,
),
const SizedBox(height: 15),
_ThemeCard(
title: "Grimorio",
subtitle: "Incantesimi antichi, rune magiche",
type: AppThemeType.grimorio,
previewColors: AppColors.grimorio,
requiredLevel: 15,
currentLevel: playerLevel,
), ),
], ],
), ),
@ -64,33 +98,61 @@ class _ThemeCard extends StatelessWidget {
final String subtitle; final String subtitle;
final AppThemeType type; final AppThemeType type;
final ThemeColors previewColors; final ThemeColors previewColors;
final int requiredLevel;
final int currentLevel;
const _ThemeCard({required this.title, required this.subtitle, required this.type, required this.previewColors}); const _ThemeCard({
required this.title,
required this.subtitle,
required this.type,
required this.previewColors,
required this.requiredLevel,
required this.currentLevel,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final themeManager = context.watch<ThemeManager>(); final themeManager = context.watch<ThemeManager>();
bool isSelected = themeManager.currentThemeType == type; bool isSelected = themeManager.currentThemeType == type;
bool isLocked = currentLevel < requiredLevel;
return GestureDetector( return GestureDetector(
onTap: () { 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); themeManager.setTheme(type);
// --- LA MODIFICA È QUI ---
// Chiude la schermata e torna automaticamente alla Home!
Navigator.pop(context); Navigator.pop(context);
}, },
child: AnimatedContainer( child: AnimatedContainer(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: previewColors.background, color: isLocked ? previewColors.background.withOpacity(0.4) : previewColors.background,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
border: Border.all( border: Border.all(
color: isSelected ? previewColors.playerBlue : previewColors.gridLine.withOpacity(0.5), color: isSelected
? previewColors.playerBlue
: (isLocked ? Colors.grey.withOpacity(0.3) : previewColors.gridLine.withOpacity(0.5)),
width: isSelected ? 4 : 2, width: isSelected ? 4 : 2,
), ),
boxShadow: isSelected ? [BoxShadow(color: previewColors.playerBlue.withOpacity(0.4), blurRadius: 10, spreadRadius: 2)] : [], boxShadow: isSelected ? [BoxShadow(color: previewColors.playerBlue.withOpacity(0.4), blurRadius: 10, spreadRadius: 2)] : [],
), ),
child: Stack(
alignment: Alignment.center,
children: [
Opacity(
opacity: isLocked ? 0.25 : 1.0,
child: Row( child: Row(
children: [ children: [
Expanded( Expanded(
@ -108,6 +170,30 @@ class _ThemeCard extends StatelessWidget {
], ],
), ),
), ),
if (isLocked)
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.85),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.white.withOpacity(0.2), width: 1.5),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.5), blurRadius: 10, offset: const Offset(0, 4))],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.lock_rounded, color: Colors.white, size: 20),
const SizedBox(width: 8),
Text(
"LIV. $requiredLevel",
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w900, fontSize: 16, letterSpacing: 2)
),
],
),
),
],
),
),
); );
} }
} }

BIN
macos/.DS_Store vendored

Binary file not shown.

View file

@ -598,7 +598,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = 2BX6QRR7GG; DEVELOPMENT_TEAM = 2BX6QRR7GG;
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
@ -618,7 +618,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MARKETING_VERSION = 1.0.0; MARKETING_VERSION = 1.0.2;
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
}; };
@ -748,7 +748,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = 2BX6QRR7GG; DEVELOPMENT_TEAM = 2BX6QRR7GG;
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
@ -768,7 +768,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MARKETING_VERSION = 1.0.0; MARKETING_VERSION = 1.0.2;
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -786,7 +786,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = 2BX6QRR7GG; DEVELOPMENT_TEAM = 2BX6QRR7GG;
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
@ -806,7 +806,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MARKETING_VERSION = 1.0.0; MARKETING_VERSION = 1.0.2;
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
}; };

View file

@ -317,6 +317,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
font_awesome_flutter:
dependency: "direct main"
description:
name: font_awesome_flutter
sha256: b9011df3a1fa02993630b8fb83526368cf2206a711259830325bab2f1d2a4eb0
url: "https://pub.dev"
source: hosted
version: "10.12.0"
glob: glob:
dependency: transitive dependency: transitive
description: description:

View file

@ -1,7 +1,7 @@
name: tetraq name: tetraq
description: A new Flutter project. description: A new Flutter project.
publish_to: 'none' publish_to: 'none'
version: 1.0.0+4 version: 1.0.2+4
environment: environment:
sdk: ^3.10.7 sdk: ^3.10.7
@ -22,6 +22,7 @@ dependencies:
share_plus: ^12.0.1 share_plus: ^12.0.1
app_links: ^7.0.0 app_links: ^7.0.0
google_fonts: ^8.0.2 google_fonts: ^8.0.2
font_awesome_flutter: ^10.12.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: