Auto-sync: 20260301_205905
This commit is contained in:
parent
a3d70e39b2
commit
2d6e86db3c
20 changed files with 1656 additions and 1019 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
BIN
ios/.DS_Store
vendored
BIN
ios/.DS_Store
vendored
Binary file not shown.
|
|
@ -473,7 +473,7 @@
|
|||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
DEVELOPMENT_TEAM = 2BX6QRR7GG;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
|
|
@ -482,7 +482,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
MARKETING_VERSION = 1.0.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.sanza.tetraq;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
|
|
@ -658,7 +658,7 @@
|
|||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
DEVELOPMENT_TEAM = 2BX6QRR7GG;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
|
|
@ -667,7 +667,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
MARKETING_VERSION = 1.0.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.sanza.tetraq;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
|
|
@ -683,7 +683,7 @@
|
|||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
DEVELOPMENT_TEAM = 2BX6QRR7GG;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
|
|
@ -692,7 +692,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
MARKETING_VERSION = 1.0.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.sanza.tetraq;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
|
|
|
|||
BIN
lib/.DS_Store
vendored
BIN
lib/.DS_Store
vendored
Binary file not shown.
|
|
@ -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 {
|
||||
final Color background;
|
||||
|
|
@ -29,22 +34,24 @@ class AppColors {
|
|||
playerRed: Color(0xFFD32F2F), playerBlue: Color(0xFF1565C0), text: Color(0xFF37474F),
|
||||
);
|
||||
|
||||
// --- TEMA CYBERPUNK AGGIORNATO ---
|
||||
static const ThemeColors cyberpunk = ThemeColors(
|
||||
background: Color(0xFF0A001A), // Sfondo notte profonda
|
||||
gridLine: Color(0xFF6200EA), // Viola scuro elettrico (non fa confusione con le mosse)
|
||||
playerRed: Color(0xFFFF007F), // Rosa Neon (invariato)
|
||||
playerBlue: Color(0xFF69F0AE), // Verde Fluo brillante! (Green Accent)
|
||||
text: Color(0xFFFFFFFF),
|
||||
background: Color(0xFF0A001A), gridLine: Color(0xFF6200EA),
|
||||
playerRed: Color(0xFFFF007F), playerBlue: Color(0xFF69F0AE), text: Color(0xFFFFFFFF),
|
||||
);
|
||||
|
||||
// --- TEMA LEGNO POTENZIATO ---
|
||||
static const ThemeColors wood = ThemeColors(
|
||||
background: Color(0xFF905D3B), // Marrone caldo e ricco (vero legno)
|
||||
gridLine: Color(0xFF4A301E), // Marrone scurissimo per i solchi
|
||||
playerRed: Color(0xFFE53935), // Rosso acceso per i fiammiferi
|
||||
playerBlue: Color(0xFF29B6F6), // Azzurro acceso per i fiammiferi
|
||||
text: Color(0xFFFBE9E7), // Panna chiaro per contrastare lo scuro
|
||||
background: Color(0xFF905D3B), gridLine: Color(0xFF4A301E),
|
||||
playerRed: Color(0xFFE53935), playerBlue: Color(0xFF29B6F6), text: Color(0xFFFBE9E7),
|
||||
);
|
||||
|
||||
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) {
|
||||
|
|
@ -53,6 +60,74 @@ class AppColors {
|
|||
case AppThemeType.doodle: return doodle;
|
||||
case AppThemeType.cyberpunk: return cyberpunk;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@
|
|||
import 'dart:math';
|
||||
import '../models/game_board.dart';
|
||||
|
||||
// Modificato per tracciare anche l'effetto Swap
|
||||
class _ClosureResult {
|
||||
final bool closesSomething;
|
||||
final int netValue;
|
||||
|
|
@ -25,7 +24,6 @@ class AIEngine {
|
|||
|
||||
bool beSmart = random.nextDouble() < smartChance;
|
||||
|
||||
// Calcolo punteggi attuali per valutare lo SWAP
|
||||
int myScore = board.currentPlayer == Player.red ? board.scoreRed : board.scoreBlue;
|
||||
int oppScore = board.currentPlayer == Player.red ? board.scoreBlue : board.scoreRed;
|
||||
|
||||
|
|
@ -36,15 +34,12 @@ class AIEngine {
|
|||
var result = _checkClosure(board, line);
|
||||
if (result.closesSomething) {
|
||||
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) {
|
||||
goodClosingMoves.add(line);
|
||||
} else {
|
||||
badClosingMoves.add(line);
|
||||
}
|
||||
} else {
|
||||
// Normale valutazione dei punti
|
||||
if (result.netValue >= 0) {
|
||||
goodClosingMoves.add(line);
|
||||
} else {
|
||||
|
|
@ -112,7 +107,19 @@ class AIEngine {
|
|||
|
||||
if (linesCount == 4) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -132,17 +139,30 @@ class AIEngine {
|
|||
if (box.right.owner != Player.none) currentLinesCount++;
|
||||
|
||||
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;
|
||||
}
|
||||
// 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 (myScore < oppScore) {
|
||||
continue; // È sicuro e strategico!
|
||||
continue;
|
||||
} else {
|
||||
return false; // Se stiamo vincendo non dobbiamo MAI lasciare uno Swap!
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,10 @@ import 'dart:math';
|
|||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../models/game_board.dart';
|
||||
export '../models/game_board.dart';
|
||||
|
||||
import 'ai_engine.dart';
|
||||
import '../services/audio_service.dart';
|
||||
import '../services/storage_service.dart';
|
||||
|
|
@ -30,22 +33,29 @@ class GameController extends ChangeNotifier {
|
|||
Timer? _blitzTimer;
|
||||
int timeLeft = 15;
|
||||
final int maxTime = 15;
|
||||
|
||||
bool isTimeMode = true;
|
||||
|
||||
String effectText = '';
|
||||
Color effectColor = Colors.transparent;
|
||||
Timer? _effectTimer;
|
||||
|
||||
// --- VARIABILI EMOJI E RIVINCITA ---
|
||||
String? myReaction;
|
||||
String? opponentReaction;
|
||||
Timer? _myReactionTimer;
|
||||
Timer? _oppReactionTimer;
|
||||
|
||||
Timestamp? _lastOpponentReactionTime;
|
||||
|
||||
bool rematchRequested = 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;
|
||||
|
||||
int cpuLevel = 1;
|
||||
|
|
@ -69,12 +79,19 @@ class GameController extends ChangeNotifier {
|
|||
_effectTimer?.cancel();
|
||||
effectText = '';
|
||||
_hasSavedResult = false;
|
||||
lastMatchXP = 0;
|
||||
|
||||
myReaction = null;
|
||||
opponentReaction = null;
|
||||
_lastOpponentReactionTime = null;
|
||||
rematchRequested = false;
|
||||
opponentWantsRematch = false;
|
||||
|
||||
isSetupPhase = true;
|
||||
myJokerPlaced = false;
|
||||
oppJokerPlaced = false;
|
||||
jokerTurn = Player.red;
|
||||
|
||||
this.isVsCPU = vsCPU;
|
||||
this.isOnline = isOnline;
|
||||
this.roomCode = roomCode;
|
||||
|
|
@ -85,8 +102,6 @@ class GameController extends ChangeNotifier {
|
|||
int levelToUse = isOnline ? (currentMatchLevel == 1 ? 2 : currentMatchLevel) : cpuLevel;
|
||||
|
||||
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;
|
||||
|
||||
isCPUThinking = false;
|
||||
|
|
@ -96,11 +111,68 @@ class GameController extends ChangeNotifier {
|
|||
_listenToOnlineGame(this.roomCode!);
|
||||
}
|
||||
|
||||
_startTimer();
|
||||
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) {
|
||||
if (!isOnline || roomCode == null) return;
|
||||
MultiplayerService().sendReaction(roomCode!, isHost, reaction);
|
||||
|
|
@ -118,23 +190,14 @@ class GameController extends ChangeNotifier {
|
|||
if (isMe) {
|
||||
myReaction = reaction;
|
||||
_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 {
|
||||
opponentReaction = reaction;
|
||||
_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();
|
||||
}
|
||||
// --------------------------------
|
||||
|
||||
void triggerSpecialEffect(String text, Color color) {
|
||||
effectText = text;
|
||||
|
|
@ -144,20 +207,53 @@ class GameController extends ChangeNotifier {
|
|||
_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) {
|
||||
AudioService.instance.playLineSfx(_activeTheme);
|
||||
if (!isOpponent) HapticFeedback.lightImpact();
|
||||
} else {
|
||||
for (var b in newClosed) {
|
||||
if (b.isJokerRevealed) {
|
||||
if (b.owner == b.hiddenJokerOwner) {
|
||||
AudioService.instance.playBonusSfx();
|
||||
triggerSpecialEffect("JOLLY! +2", Colors.greenAccent);
|
||||
} else {
|
||||
AudioService.instance.playBombSfx();
|
||||
triggerSpecialEffect("JOLLY! -1", Colors.redAccent);
|
||||
}
|
||||
HapticFeedback.heavyImpact();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool isGold = newClosed.any((b) => b.type == BoxType.gold);
|
||||
bool isBomb = newClosed.any((b) => b.type == BoxType.bomb);
|
||||
bool isSwap = newClosed.any((b) => b.type == BoxType.swap);
|
||||
bool isMultiplier = newClosed.any((b) => b.type == BoxType.multiplier);
|
||||
|
||||
if (isSwap) {
|
||||
// Usa temporaneamente playBonusSfx per lo swap, o un suono dedicato se lo aggiungerai
|
||||
AudioService.instance.playBonusSfx();
|
||||
triggerSpecialEffect("SCAMBIO!", Colors.purpleAccent);
|
||||
HapticFeedback.heavyImpact();
|
||||
} else if (isMultiplier) {
|
||||
AudioService.instance.playBonusSfx();
|
||||
triggerSpecialEffect("MOLTIPLICATORE x2!", Colors.yellowAccent);
|
||||
HapticFeedback.heavyImpact();
|
||||
} else if (isGold) {
|
||||
AudioService.instance.playBonusSfx(); triggerSpecialEffect("+2", Colors.amber); HapticFeedback.heavyImpact();
|
||||
} else if (isBomb) {
|
||||
|
|
@ -170,40 +266,32 @@ class GameController extends ChangeNotifier {
|
|||
|
||||
void _startTimer() {
|
||||
_blitzTimer?.cancel();
|
||||
timeLeft = maxTime;
|
||||
if (isSetupPhase) return;
|
||||
|
||||
if (!isTimeMode) {
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
timeLeft = maxTime;
|
||||
if (!isTimeMode) { notifyListeners(); return; }
|
||||
|
||||
_blitzTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (isGameOver || isCPUThinking) { timer.cancel(); return; }
|
||||
|
||||
if (timeLeft > 0) {
|
||||
timeLeft--;
|
||||
notifyListeners();
|
||||
timeLeft--; notifyListeners();
|
||||
} else {
|
||||
timer.cancel();
|
||||
if (!isOnline || board.currentPlayer == myPlayer) {
|
||||
_handleTimeOut();
|
||||
}
|
||||
if (!isOnline || board.currentPlayer == myPlayer) { _handleTimeOut(); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _handleTimeOut() {
|
||||
if (!isTimeMode) return;
|
||||
if (!isTimeMode || isSetupPhase) return;
|
||||
|
||||
if (isOnline) {
|
||||
Line randomMove = AIEngine.getBestMove(board, 5);
|
||||
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);
|
||||
handleLineTap(randomMove, _activeTheme, forced: true);
|
||||
}
|
||||
else if (!isVsCPU) {
|
||||
} else if (!isVsCPU) {
|
||||
Line randomMove = AIEngine.getBestMove(board, 5);
|
||||
handleLineTap(randomMove, _activeTheme, forced: true);
|
||||
}
|
||||
|
|
@ -216,15 +304,12 @@ class GameController extends ChangeNotifier {
|
|||
_effectTimer?.cancel();
|
||||
_myReactionTimer?.cancel();
|
||||
_oppReactionTimer?.cancel();
|
||||
_lastOpponentReactionTime = null;
|
||||
|
||||
if (isOnline && roomCode != null) {
|
||||
FirebaseFirestore.instance.collection('games').doc(roomCode).update({'status': 'abandoned'}).catchError((e) => null);
|
||||
}
|
||||
|
||||
isOnline = false;
|
||||
roomCode = null;
|
||||
currentMatchLevel = 1;
|
||||
currentSeed = null;
|
||||
isOnline = false; roomCode = null; currentMatchLevel = 1; currentSeed = null;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -233,7 +318,6 @@ class GameController extends ChangeNotifier {
|
|||
void _listenToOnlineGame(String code) {
|
||||
_onlineSubscription = FirebaseFirestore.instance.collection('games').doc(code).snapshots().listen((doc) {
|
||||
if (!doc.exists) return;
|
||||
|
||||
var data = doc.data() as Map<String, dynamic>;
|
||||
|
||||
onlineHostName = data['hostName'] ?? "ROSSO";
|
||||
|
|
@ -243,13 +327,16 @@ class GameController extends ChangeNotifier {
|
|||
opponentLeft = true; notifyListeners(); return;
|
||||
}
|
||||
|
||||
// --- ASCOLTO EMOJI E RIVINCITA ---
|
||||
String? p1React = data['p1_reaction'];
|
||||
Timestamp? p1Time = data['p1_reaction_time'] as Timestamp?;
|
||||
String? p2React = data['p2_reaction'];
|
||||
Timestamp? p2Time = data['p2_reaction_time'] as Timestamp?;
|
||||
|
||||
if (isHost && p2React != null && p2React != opponentReaction) {
|
||||
if (isHost && p2React != null && p2Time != null && p2Time != _lastOpponentReactionTime) {
|
||||
_lastOpponentReactionTime = p2Time;
|
||||
_showReaction(false, p2React);
|
||||
} else if (!isHost && p1React != null && p1React != opponentReaction) {
|
||||
} else if (!isHost && p1React != null && p1Time != null && p1Time != _lastOpponentReactionTime) {
|
||||
_lastOpponentReactionTime = p1Time;
|
||||
_showReaction(false, p1React);
|
||||
}
|
||||
|
||||
|
|
@ -257,12 +344,10 @@ class GameController extends ChangeNotifier {
|
|||
bool p2Rematch = data['p2_rematch'] ?? false;
|
||||
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) {
|
||||
currentSeed = data['seed'];
|
||||
startNewGame(data['radius'], isOnline: true, roomCode: roomCode, isHost: isHost, shape: ArenaShape.values.firstWhere((e) => e.name == data['shape']), timeMode: data['timeMode']);
|
||||
return; // Evitiamo di processare le mosse vuote
|
||||
return;
|
||||
}
|
||||
|
||||
if (p1Rematch && p2Rematch && isHost && data['status'] != 'playing') {
|
||||
|
|
@ -273,26 +358,34 @@ class GameController extends ChangeNotifier {
|
|||
ArenaShape newShape = ArenaShape.values[rand.nextInt(ArenaShape.values.length)];
|
||||
MultiplayerService().resetMatch(roomCode!, newRadius, newShape.name, newSeed);
|
||||
}
|
||||
// ---------------------------------
|
||||
|
||||
if (isSetupPhase) {
|
||||
if (!isHost && data['p1_joker'] != null && !oppJokerPlaced) {
|
||||
int jx = data['p1_joker']['x']; int jy = data['p1_joker']['y'];
|
||||
board.boxes.firstWhere((b) => b.x == jx && b.y == jy).hiddenJokerOwner = Player.red;
|
||||
oppJokerPlaced = true; _checkSetupComplete();
|
||||
}
|
||||
if (isHost && data['p2_joker'] != null && !oppJokerPlaced) {
|
||||
int jx = data['p2_joker']['x']; int jy = data['p2_joker']['y'];
|
||||
board.boxes.firstWhere((b) => b.x == jx && b.y == jy).hiddenJokerOwner = Player.blue;
|
||||
oppJokerPlaced = true; _checkSetupComplete();
|
||||
}
|
||||
}
|
||||
|
||||
List<dynamic> moves = data['moves'] ?? [];
|
||||
int hostLevel = data['matchLevel'] ?? 1;
|
||||
int? hostSeed = data['seed'];
|
||||
int hostRadius = data['radius'] ?? board.radius;
|
||||
|
||||
String shapeStr = data['shape'] ?? 'classic';
|
||||
ArenaShape hostShape = ArenaShape.values.firstWhere((e) => e.name == shapeStr, orElse: () => ArenaShape.classic);
|
||||
onlineShape = hostShape;
|
||||
|
||||
isTimeMode = data['timeMode'] ?? true;
|
||||
|
||||
// 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))) {
|
||||
currentMatchLevel = hostLevel; currentSeed = hostSeed;
|
||||
int levelToUse = (currentMatchLevel == 1) ? 2 : currentMatchLevel;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -302,7 +395,7 @@ class GameController extends ChangeNotifier {
|
|||
if (firebaseMovesCount == 0 && localMovesCount > 0 && !rematchRequested) {
|
||||
int levelToUse = (currentMatchLevel == 1) ? 2 : currentMatchLevel;
|
||||
board = GameBoard(radius: hostRadius, level: levelToUse, seed: currentSeed, shape: onlineShape);
|
||||
board.currentPlayer = Player.red; // FIX Turno Iniziale
|
||||
board.currentPlayer = Player.red;
|
||||
notifyListeners(); return;
|
||||
}
|
||||
|
||||
|
|
@ -321,25 +414,22 @@ class GameController extends ChangeNotifier {
|
|||
Player playerFromFirebase = (m['player'] == 'red') ? Player.red : Player.blue;
|
||||
bool isOpponentMove = (playerFromFirebase != myPlayer);
|
||||
List<Box> closedBefore = board.boxes.where((b) => b.owner != Player.none).toList();
|
||||
List<Box> ghostsBefore = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed).toList();
|
||||
|
||||
// FIX: Forziamo la mossa usando il giocatore indicato da Firebase
|
||||
board.playMove(lineToPlay, forcedPlayer: playerFromFirebase);
|
||||
newMovesApplied = true;
|
||||
|
||||
List<Box> newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList();
|
||||
if (isOpponentMove) _playEffects(newClosed, isOpponent: true);
|
||||
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) {
|
||||
// FIX: Sincronizzazione esplicita del turno basata su Firebase
|
||||
String expectedTurnStr = data['turn'] ?? 'red';
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
@ -350,17 +440,18 @@ class GameController extends ChangeNotifier {
|
|||
}
|
||||
|
||||
void handleLineTap(Line line, AppThemeType theme, {bool forced = false}) {
|
||||
if ((isCPUThinking || board.isGameOver || opponentLeft) && !forced) return;
|
||||
|
||||
// Controllo Turno
|
||||
if ((isSetupPhase || isCPUThinking || board.isGameOver || opponentLeft) && !forced) return;
|
||||
if (isOnline && board.currentPlayer != myPlayer && !forced) return;
|
||||
|
||||
_activeTheme = theme;
|
||||
List<Box> closedBefore = board.boxes.where((b) => b.owner != Player.none).toList();
|
||||
List<Box> ghostsBefore = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed).toList();
|
||||
|
||||
if (board.playMove(line)) {
|
||||
List<Box> newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList();
|
||||
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();
|
||||
|
||||
|
|
@ -369,8 +460,6 @@ class GameController extends ChangeNotifier {
|
|||
'x1': line.p1.x, 'y1': line.p1.y, 'x2': line.p2.x, 'y2': line.p2.y,
|
||||
'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';
|
||||
|
||||
FirebaseFirestore.instance.collection('games').doc(roomCode).update({
|
||||
|
|
@ -382,7 +471,6 @@ class GameController extends ChangeNotifier {
|
|||
_saveMatchResult();
|
||||
if (isHost) FirebaseFirestore.instance.collection('games').doc(roomCode).update({'status': 'finished'});
|
||||
}
|
||||
|
||||
} else {
|
||||
if (board.isGameOver) _saveMatchResult();
|
||||
else if (isVsCPU && board.currentPlayer == Player.blue) _checkCPUTurn();
|
||||
|
|
@ -397,11 +485,15 @@ class GameController extends ChangeNotifier {
|
|||
|
||||
if (!board.isGameOver) {
|
||||
List<Box> closedBefore = board.boxes.where((b) => b.owner != Player.none).toList();
|
||||
List<Box> ghostsBefore = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed).toList();
|
||||
|
||||
Line bestMove = AIEngine.getBestMove(board, cpuLevel);
|
||||
board.playMove(bestMove);
|
||||
|
||||
List<Box> newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList();
|
||||
_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();
|
||||
|
||||
|
|
@ -415,22 +507,44 @@ class GameController extends ChangeNotifier {
|
|||
if (_hasSavedResult) return;
|
||||
_hasSavedResult = true;
|
||||
|
||||
int calculatedXP = 0;
|
||||
bool isDraw = board.scoreRed == board.scoreBlue;
|
||||
String myRealName = StorageService.instance.playerName;
|
||||
if (myRealName.isEmpty) myRealName = "IO";
|
||||
|
||||
if (isOnline) {
|
||||
bool isWin = isHost ? board.scoreRed > board.scoreBlue : board.scoreBlue > board.scoreRed;
|
||||
calculatedXP = isWin ? 20 : (isDraw ? 5 : 2);
|
||||
String oppName = isHost ? onlineGuestName : onlineHostName;
|
||||
int myScore = isHost ? board.scoreRed : board.scoreBlue;
|
||||
int oppScore = isHost ? board.scoreBlue : board.scoreRed;
|
||||
StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: oppName, myScore: myScore, oppScore: oppScore, isOnline: true);
|
||||
|
||||
if (isWin) StorageService.instance.updateQuestProgress(0, 1); // Missione: Vinci Online
|
||||
|
||||
} else if (isVsCPU) {
|
||||
int myScore = board.scoreRed; int cpuScore = board.scoreBlue;
|
||||
if (myScore > cpuScore) StorageService.instance.addWin();
|
||||
else if (cpuScore > myScore) StorageService.instance.addLoss();
|
||||
bool isWin = myScore > cpuScore;
|
||||
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);
|
||||
} else {
|
||||
calculatedXP = 2;
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -5,9 +5,7 @@
|
|||
import 'dart:math';
|
||||
|
||||
enum Player { red, blue, none }
|
||||
enum BoxType { normal, gold, bomb, invisible, swap }
|
||||
|
||||
// --- AGGIUNTO 'chaos' ---
|
||||
enum BoxType { normal, gold, bomb, invisible, swap, ice, multiplier } // Aggiunti ice e multiplier
|
||||
enum ArenaShape { classic, cross, donut, hourglass, chaos }
|
||||
|
||||
class Dot {
|
||||
|
|
@ -26,6 +24,7 @@ class Line {
|
|||
final Dot p2;
|
||||
Player owner = Player.none;
|
||||
bool isPlayable = false;
|
||||
bool isIceCracked = false; // NUOVO: Stato per il blocco di ghiaccio
|
||||
|
||||
Line(this.p1, this.p2);
|
||||
|
||||
|
|
@ -39,6 +38,10 @@ class Box {
|
|||
late Line top, bottom, left, right;
|
||||
BoxType type = BoxType.normal;
|
||||
|
||||
bool isRevealed = false;
|
||||
Player? hiddenJokerOwner;
|
||||
bool isJokerRevealed = false;
|
||||
|
||||
Box(this.x, this.y);
|
||||
|
||||
bool isClosed() {
|
||||
|
|
@ -46,10 +49,13 @@ class Box {
|
|||
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.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;
|
||||
}
|
||||
}
|
||||
|
|
@ -60,6 +66,9 @@ class GameBoard {
|
|||
final int? seed;
|
||||
final ArenaShape shape;
|
||||
|
||||
late int columns;
|
||||
late int rows;
|
||||
|
||||
List<Dot> dots = [];
|
||||
List<Line> lines = [];
|
||||
List<Box> boxes = [];
|
||||
|
|
@ -71,93 +80,91 @@ class GameBoard {
|
|||
|
||||
Line? lastMove;
|
||||
|
||||
// Variabili per il Moltiplicatore
|
||||
bool redHasMultiplier = false;
|
||||
bool blueHasMultiplier = false;
|
||||
|
||||
GameBoard({required this.radius, this.level = 1, this.seed, this.shape = ArenaShape.classic}) {
|
||||
_generateBoard();
|
||||
}
|
||||
|
||||
void _generateBoard() {
|
||||
int size = radius * 2 + 1;
|
||||
final random = seed != null ? Random(seed) : Random();
|
||||
|
||||
// Se è Caos, decidiamo quale algoritmo usare in base al seed
|
||||
int chaosAlgorithm = random.nextInt(5);
|
||||
|
||||
if (shape == ArenaShape.chaos) {
|
||||
columns = radius * 2 + 1;
|
||||
rows = (radius * 3) + 2;
|
||||
} else {
|
||||
columns = radius * 2 + 1;
|
||||
rows = radius * 2 + 1;
|
||||
}
|
||||
|
||||
dots.clear();
|
||||
lines.clear();
|
||||
boxes.clear();
|
||||
lastMove = null;
|
||||
|
||||
for (int y = 0; y < size; y++) {
|
||||
for (int x = 0; x < size; x++) {
|
||||
for (int y = 0; y < rows; y++) {
|
||||
for (int x = 0; x < columns; x++) {
|
||||
var box = Box(x, y);
|
||||
bool isVisible = true;
|
||||
|
||||
int dx = (x - radius).abs();
|
||||
int dy = (y - radius).abs();
|
||||
if (shape != ArenaShape.chaos) {
|
||||
int dx = (x - radius).abs();
|
||||
int dy = (y - radius).abs();
|
||||
isVisible = (dx + dy) <= radius;
|
||||
|
||||
bool isVisible = (dx + dy) <= radius;
|
||||
|
||||
if (isVisible) {
|
||||
switch (shape) {
|
||||
case ArenaShape.classic:
|
||||
break;
|
||||
case ArenaShape.cross:
|
||||
int spessoreBraccio = radius > 3 ? 1 : 0;
|
||||
if (dx > spessoreBraccio && dy > spessoreBraccio) isVisible = false;
|
||||
break;
|
||||
case ArenaShape.donut:
|
||||
int dimensioneBuco = radius > 3 ? 2 : 1;
|
||||
if ((dx + dy) <= dimensioneBuco) isVisible = false;
|
||||
break;
|
||||
case ArenaShape.hourglass:
|
||||
if (dx > dy) isVisible = false;
|
||||
if (x == radius && y == radius) isVisible = true;
|
||||
break;
|
||||
case ArenaShape.chaos:
|
||||
// --- GENERATORE PROCEDURALE (IL CAOS) ---
|
||||
// Essendo basato su dx e dy, genererà sempre forme simmetriche a 4 vie!
|
||||
if (chaosAlgorithm == 0) {
|
||||
// Modello "Rete Frattale": Rimuove blocchi basati su operatori bitwise
|
||||
if ((dx & dy) != 0) isVisible = false;
|
||||
} else if (chaosAlgorithm == 1) {
|
||||
// Modello "Quattro Pilastri": Svuota lunghe linee ma salva i centri
|
||||
if (dx == 1 || dy == 1) {
|
||||
if ((dx + dy) > 2 && (dx + dy) < radius) isVisible = false;
|
||||
}
|
||||
} else if (chaosAlgorithm == 2) {
|
||||
// Modello "X-Treme": Taglia le diagonali perfette
|
||||
if (dx == dy && dx > 0 && dx < radius) isVisible = false;
|
||||
} else if (chaosAlgorithm == 3) {
|
||||
// Modello "Scacchiera Nucleare": Alternanza precisa
|
||||
if (dx % 2 == 1 && dy % 2 == 1) isVisible = false;
|
||||
} else if (chaosAlgorithm == 4) {
|
||||
// Modello "Anelli Frammentati": Buca gli anelli pari
|
||||
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 (isVisible) {
|
||||
switch (shape) {
|
||||
case ArenaShape.cross:
|
||||
int spessoreBraccio = radius > 3 ? 1 : 0;
|
||||
if (dx > spessoreBraccio && dy > spessoreBraccio) isVisible = false; break;
|
||||
case ArenaShape.donut:
|
||||
int dimensioneBuco = radius > 3 ? 2 : 1;
|
||||
if ((dx + dy) <= dimensioneBuco) isVisible = false; break;
|
||||
case ArenaShape.hourglass:
|
||||
if (dx > dy) isVisible = false;
|
||||
if (x == radius && y == radius) isVisible = true; break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
double percentY = y / rows;
|
||||
if (chaosAlgorithm == 0) {
|
||||
isVisible = (x % 2 == 0) && (random.nextDouble() > 0.15);
|
||||
} else if (chaosAlgorithm == 1) {
|
||||
double chance = 0.2 + (percentY * 0.7);
|
||||
isVisible = random.nextDouble() < chance;
|
||||
} else if (chaosAlgorithm == 2) {
|
||||
int midY = rows ~/ 2;
|
||||
int distFromCenterY = (y - midY).abs();
|
||||
int allowedWidth = (distFromCenterY / midY * radius).ceil() + 1;
|
||||
int dx = (x - radius).abs();
|
||||
isVisible = dx <= allowedWidth && random.nextDouble() > 0.1;
|
||||
} else if (chaosAlgorithm == 3) {
|
||||
isVisible = (y % 2 == 0) ? (x < columns - 1) : (x > 0);
|
||||
if (random.nextDouble() > 0.8) isVisible = false;
|
||||
} else if (chaosAlgorithm == 4) {
|
||||
isVisible = random.nextDouble() > 0.45;
|
||||
}
|
||||
if (x == radius && y == rows ~/ 2) isVisible = true;
|
||||
}
|
||||
|
||||
if (!isVisible) {
|
||||
box.type = BoxType.invisible;
|
||||
} else if (level > 1) {
|
||||
double chance = random.nextDouble();
|
||||
if (chance < 0.10) {
|
||||
box.type = BoxType.gold;
|
||||
} else if (chance > 0.90) {
|
||||
box.type = BoxType.bomb;
|
||||
} else if (level >= 5 && chance > 0.85 && chance <= 0.90) {
|
||||
box.type = BoxType.swap;
|
||||
}
|
||||
if (chance < 0.08) box.type = BoxType.gold;
|
||||
else if (chance > 0.92) box.type = BoxType.bomb;
|
||||
else if (level >= 5 && chance > 0.88 && chance <= 0.92) box.type = BoxType.swap;
|
||||
else if (level >= 10 && chance > 0.83 && chance <= 0.88) box.type = BoxType.ice; // Nuova Scatola Ghiaccio
|
||||
else if (level >= 15 && chance > 0.78 && chance <= 0.83) box.type = BoxType.multiplier; // Nuova Scatola x2
|
||||
}
|
||||
boxes.add(box);
|
||||
}
|
||||
}
|
||||
|
||||
// Costruzione Linee (Identico a prima)
|
||||
for (var box in boxes) {
|
||||
Dot tl = _getOrAddDot(box.x, box.y);
|
||||
Dot tr = _getOrAddDot(box.x + 1, box.y);
|
||||
|
|
@ -170,10 +177,8 @@ class GameBoard {
|
|||
box.right = _getOrAddLine(tr, br);
|
||||
|
||||
if (box.type != BoxType.invisible) {
|
||||
box.top.isPlayable = true;
|
||||
box.bottom.isPlayable = true;
|
||||
box.left.isPlayable = true;
|
||||
box.right.isPlayable = true;
|
||||
box.top.isPlayable = true; box.bottom.isPlayable = true;
|
||||
box.left.isPlayable = true; box.right.isPlayable = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -181,15 +186,13 @@ class GameBoard {
|
|||
Dot _getOrAddDot(int x, int y) {
|
||||
for (var dot in dots) { if (dot.x == x && dot.y == y) return dot; }
|
||||
var newDot = Dot(x, y);
|
||||
dots.add(newDot);
|
||||
return newDot;
|
||||
dots.add(newDot); return newDot;
|
||||
}
|
||||
|
||||
Line _getOrAddLine(Dot a, Dot b) {
|
||||
for (var line in lines) { if (line.connects(a, b)) return line; }
|
||||
var newLine = Line(a, b);
|
||||
lines.add(newLine);
|
||||
return newLine;
|
||||
lines.add(newLine); return newLine;
|
||||
}
|
||||
|
||||
bool playMove(Line lineToPlay, {Player? forcedPlayer}) {
|
||||
|
|
@ -198,45 +201,91 @@ class GameBoard {
|
|||
Player playerMakingMove = forcedPlayer ?? currentPlayer;
|
||||
Line? actualLine;
|
||||
for (var l in lines) {
|
||||
if (l.connects(lineToPlay.p1, lineToPlay.p2)) {
|
||||
actualLine = l; break;
|
||||
}
|
||||
if (l.connects(lineToPlay.p1, lineToPlay.p2)) { actualLine = l; break; }
|
||||
}
|
||||
|
||||
if (actualLine == null || actualLine.owner != Player.none || !actualLine.isPlayable) return false;
|
||||
|
||||
// --- LOGICA BLOCCO DI GHIACCIO ---
|
||||
bool closesIce = false;
|
||||
for (var box in boxes) {
|
||||
if (box.type == BoxType.ice && box.owner == Player.none) {
|
||||
int linesCount = 0;
|
||||
if (box.top.owner != Player.none || box.top == actualLine) linesCount++;
|
||||
if (box.bottom.owner != Player.none || box.bottom == actualLine) linesCount++;
|
||||
if (box.left.owner != Player.none || box.left == actualLine) linesCount++;
|
||||
if (box.right.owner != Player.none || box.right == actualLine) linesCount++;
|
||||
if (linesCount == 4) closesIce = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (closesIce && !actualLine.isIceCracked) {
|
||||
actualLine.isIceCracked = true; // Si incrina ma non si chiude!
|
||||
lastMove = actualLine;
|
||||
if (forcedPlayer == null) currentPlayer = (currentPlayer == Player.red) ? Player.blue : Player.red;
|
||||
else currentPlayer = (forcedPlayer == Player.red) ? Player.blue : Player.red;
|
||||
return true; // Mossa valida, ma turno finito.
|
||||
}
|
||||
|
||||
// Mossa normale o secondo colpo al ghiaccio
|
||||
actualLine.isIceCracked = false;
|
||||
actualLine.owner = playerMakingMove;
|
||||
lastMove = actualLine;
|
||||
|
||||
bool boxedClosed = false;
|
||||
bool scoredPoint = false;
|
||||
bool triggeredSwap = false;
|
||||
|
||||
for (var box in boxes) {
|
||||
if (box.owner == Player.none && box.isClosed()) {
|
||||
box.owner = playerMakingMove;
|
||||
boxedClosed = true;
|
||||
scoredPoint = true;
|
||||
|
||||
if (playerMakingMove == Player.red) { scoreRed += box.value; }
|
||||
else { scoreBlue += box.value; }
|
||||
if (box.hiddenJokerOwner != null) box.isJokerRevealed = true;
|
||||
|
||||
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 dà punti e il giocatore ha un x2 attivo...
|
||||
if (playerMakingMove == Player.red && redHasMultiplier) {
|
||||
points *= 2;
|
||||
redHasMultiplier = false; // Si consuma
|
||||
} else if (playerMakingMove == Player.blue && blueHasMultiplier) {
|
||||
points *= 2;
|
||||
blueHasMultiplier = false; // Si consuma
|
||||
}
|
||||
}
|
||||
|
||||
if (playerMakingMove == Player.red) { scoreRed += points; }
|
||||
else { scoreBlue += points; }
|
||||
|
||||
if (box.type == BoxType.swap && box.hiddenJokerOwner == null) {
|
||||
triggeredSwap = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (box.type == BoxType.invisible && !box.isRevealed) {
|
||||
if (box.top.owner != Player.none && box.bottom.owner != Player.none &&
|
||||
box.left.owner != Player.none && box.right.owner != Player.none) {
|
||||
box.isRevealed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (triggeredSwap) {
|
||||
int temp = scoreRed;
|
||||
scoreRed = scoreBlue;
|
||||
scoreBlue = temp;
|
||||
int temp = scoreRed; scoreRed = scoreBlue; scoreBlue = temp;
|
||||
}
|
||||
|
||||
if (lines.where((l) => l.isPlayable).every((l) => l.owner != Player.none)) { isGameOver = true; }
|
||||
|
||||
if (forcedPlayer == null) {
|
||||
if (!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 {
|
||||
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; }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
// ===========================================================================
|
||||
// FILE: lib/services/audio_service.dart
|
||||
// ===========================================================================
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import '../core/app_colors.dart';
|
||||
|
|
@ -18,11 +22,15 @@ class AudioService extends ChangeNotifier {
|
|||
if (isMuted) return;
|
||||
String file = '';
|
||||
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.wood:
|
||||
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'));
|
||||
}
|
||||
|
|
@ -31,25 +39,26 @@ class AudioService extends ChangeNotifier {
|
|||
if (isMuted) return;
|
||||
String file = '';
|
||||
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.wood:
|
||||
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'));
|
||||
}
|
||||
|
||||
// --- NUOVI EFFETTI SPECIALI ---
|
||||
void playBonusSfx() async {
|
||||
if (isMuted) return;
|
||||
// Assicurati di aggiungere questo file nella cartella assets/audio/sfx/
|
||||
await _sfxPlayer.play(AssetSource('audio/sfx/bonus.wav'));
|
||||
}
|
||||
|
||||
void playBombSfx() async {
|
||||
if (isMuted) return;
|
||||
// Assicurati di aggiungere questo file nella cartella assets/audio/sfx/
|
||||
await _sfxPlayer.play(AssetSource('audio/sfx/bomb.wav'));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,10 @@
|
|||
// ===========================================================================
|
||||
// FILE: lib/services/storage_service.dart
|
||||
// ===========================================================================
|
||||
|
||||
import 'dart:convert';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import '../core/app_colors.dart';
|
||||
|
||||
class StorageService {
|
||||
|
|
@ -8,13 +13,12 @@ class StorageService {
|
|||
|
||||
late SharedPreferences _prefs;
|
||||
|
||||
// Si avvia quando apriamo l'app
|
||||
Future<void> init() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
_checkDailyQuests(); // All'avvio controlliamo se ci sono nuove sfide
|
||||
}
|
||||
|
||||
// --- IMPOSTAZIONI ---
|
||||
int get savedThemeIndex => _prefs.getInt('theme') ?? AppThemeType.cyberpunk.index;
|
||||
int get savedThemeIndex => _prefs.getInt('theme') ?? AppThemeType.minimal.index;
|
||||
Future<void> saveTheme(AppThemeType theme) async => await _prefs.setInt('theme', theme.index);
|
||||
|
||||
int get savedRadius => _prefs.getInt('radius') ?? 2;
|
||||
|
|
@ -23,9 +27,21 @@ class StorageService {
|
|||
bool get isMuted => _prefs.getBool('isMuted') ?? false;
|
||||
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;
|
||||
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;
|
||||
Future<void> addLoss() async => await _prefs.setInt('losses', losses + 1);
|
||||
|
|
@ -33,9 +49,66 @@ class StorageService {
|
|||
int get cpuLevel => _prefs.getInt('cpuLevel') ?? 1;
|
||||
Future<void> saveCpuLevel(int level) async => await _prefs.setInt('cpuLevel', level);
|
||||
|
||||
// --- MULTIPLAYER ---
|
||||
String get playerName => _prefs.getString('playerName') ?? '';
|
||||
Future<void> savePlayerName(String name) async => await _prefs.setString('playerName', name);
|
||||
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 ---
|
||||
List<Map<String, dynamic>> get matchHistory {
|
||||
|
|
@ -43,27 +116,14 @@ class StorageService {
|
|||
return history.map((e) => jsonDecode(e) as Map<String, dynamic>).toList();
|
||||
}
|
||||
|
||||
// Salviamo sia il nostro nome che quello dell'avversario
|
||||
Future<void> saveMatchToHistory({required String myName, required String opponent, required int myScore, required int oppScore, required bool isOnline}) async {
|
||||
List<String> history = _prefs.getStringList('matchHistory') ?? [];
|
||||
|
||||
Map<String, dynamic> match = {
|
||||
'date': DateTime.now().toIso8601String(),
|
||||
'myName': myName,
|
||||
'opponent': opponent,
|
||||
'myScore': myScore,
|
||||
'oppScore': oppScore,
|
||||
'isOnline': isOnline,
|
||||
'myName': myName, 'opponent': opponent, 'myScore': myScore, 'oppScore': oppScore, 'isOnline': isOnline,
|
||||
};
|
||||
|
||||
// Aggiungiamo in cima (il più recente per primo)
|
||||
history.insert(0, jsonEncode(match));
|
||||
|
||||
// Teniamo solo le ultime 50 partite per non intasare la memoria
|
||||
if (history.length > 50) {
|
||||
history = history.sublist(0, 50);
|
||||
}
|
||||
|
||||
if (history.length > 50) history = history.sublist(0, 50);
|
||||
await _prefs.setStringList('matchHistory', history);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../models/game_board.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
|
||||
|
|
@ -13,7 +14,23 @@ class BoardPainter extends CustomPainter {
|
|||
final AppThemeType themeType;
|
||||
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
|
||||
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 offset = spacing / 2;
|
||||
Offset getScreenPos(int x, int y) => Offset(x * spacing + offset, y * spacing + offset);
|
||||
|
||||
// --- 1. DISEGNO AREE CONQUISTATE E ICONE ---
|
||||
for (var box in board.boxes) {
|
||||
if (box.type == BoxType.invisible) continue;
|
||||
|
||||
Offset p1 = getScreenPos(box.x, box.y);
|
||||
Offset p2 = getScreenPos(box.x + 1, box.y + 1);
|
||||
Rect rect = Rect.fromPoints(p1, p2);
|
||||
|
||||
if (box.type == BoxType.invisible) {
|
||||
if (box.isRevealed) {
|
||||
_drawIconInBox(canvas, rect, ThemeIcons.block(themeType), Colors.grey.shade500);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sfondo azzurrino se è di ghiaccio (anche prima di chiuderla)
|
||||
if (box.type == BoxType.ice && box.owner == Player.none) {
|
||||
canvas.drawRect(rect.deflate(2.0), Paint()..color = Colors.cyanAccent.withOpacity(0.05)..style=PaintingStyle.fill);
|
||||
}
|
||||
|
||||
if (box.owner != Player.none) {
|
||||
final boxPaint = Paint()
|
||||
..style = PaintingStyle.fill
|
||||
|
|
@ -55,41 +81,65 @@ class BoardPainter extends CustomPainter {
|
|||
} else if (themeType == AppThemeType.doodle) {
|
||||
Color penColor = box.owner == Player.red ? Colors.redAccent.shade700 : Colors.blueAccent.shade700;
|
||||
_drawScribbleBox(canvas, rect, penColor);
|
||||
} else if (themeType == AppThemeType.arcade) {
|
||||
_drawArcadeBox(canvas, rect, box.owner == Player.red ? theme.playerRed : theme.playerBlue);
|
||||
} else if (themeType == AppThemeType.grimorio) {
|
||||
_drawGrimorioBox(canvas, rect, box.owner == Player.red ? theme.playerRed : theme.playerBlue);
|
||||
} else {
|
||||
canvas.drawRect(rect, boxPaint);
|
||||
}
|
||||
}
|
||||
|
||||
if (box.hiddenJokerOwner != null) {
|
||||
Color jokerColor = box.hiddenJokerOwner == Player.red ? theme.playerRed : theme.playerBlue;
|
||||
|
||||
if (box.isJokerRevealed) {
|
||||
_drawIconInBox(canvas, rect, ThemeIcons.joker(themeType), jokerColor);
|
||||
} else {
|
||||
bool canSee = false;
|
||||
if (isOnline || isVsCPU) {
|
||||
canSee = box.hiddenJokerOwner == myPlayer;
|
||||
} else {
|
||||
canSee = false;
|
||||
}
|
||||
if (canSee) {
|
||||
_drawIconInBox(canvas, rect, ThemeIcons.joker(themeType), jokerColor.withOpacity(0.3));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (box.type == BoxType.gold) {
|
||||
_drawIconInBox(canvas, rect, Icons.star_rounded, Colors.amber);
|
||||
_drawIconInBox(canvas, rect, ThemeIcons.gold(themeType), Colors.amber);
|
||||
} else if (box.type == BoxType.bomb) {
|
||||
_drawIconInBox(canvas, rect, Icons.mood_bad_rounded, themeType == AppThemeType.cyberpunk ? Colors.greenAccent : Colors.deepPurple);
|
||||
_drawIconInBox(canvas, rect, ThemeIcons.bomb(themeType), themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? Colors.greenAccent : Colors.deepPurple);
|
||||
} else if (box.type == BoxType.swap) {
|
||||
// NUOVA ICONA SWAP: Frecce circolari viola (o cyan) per indicare l'inversione
|
||||
_drawIconInBox(canvas, rect, Icons.sync_rounded, Colors.purpleAccent);
|
||||
_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) {
|
||||
if (!line.isPlayable) continue;
|
||||
|
||||
Offset p1 = getScreenPos(line.p1.x, line.p1.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
|
||||
? theme.gridLine.withOpacity(0.4)
|
||||
: (line.owner == Player.red ? theme.playerRed : theme.playerBlue);
|
||||
|
||||
if (isLastMove && line.owner != Player.none && themeType != AppThemeType.wood && themeType != AppThemeType.cyberpunk) {
|
||||
canvas.drawLine(p1, p2, Paint()
|
||||
..color = Colors.white.withOpacity(blinkValue * 0.5)
|
||||
..strokeWidth = 16.0
|
||||
..strokeCap = StrokeCap.round
|
||||
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6.0)
|
||||
);
|
||||
if (isLastMove && line.owner != Player.none && themeType != AppThemeType.wood && themeType != AppThemeType.cyberpunk && themeType != AppThemeType.arcade && themeType != AppThemeType.grimorio) {
|
||||
canvas.drawLine(p1, p2, Paint()..color = Colors.white.withOpacity(blinkValue * 0.5)..strokeWidth = 16.0..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6.0));
|
||||
}
|
||||
|
||||
if (themeType == AppThemeType.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);
|
||||
} else {
|
||||
Color headColor = lineColor;
|
||||
if (isLastMove) {
|
||||
headColor = Color.lerp(headColor, Colors.yellow, blinkValue * 0.8) ?? headColor;
|
||||
}
|
||||
if (isLastMove) headColor = Color.lerp(headColor, Colors.yellow, blinkValue * 0.8) ?? headColor;
|
||||
_drawRealisticMatch(canvas, p1, p2, headColor, isLastMove: isLastMove, blinkValue: blinkValue);
|
||||
}
|
||||
} else if (themeType == AppThemeType.cyberpunk) {
|
||||
_drawNeonLine(canvas, p1, p2, lineColor, line.owner != Player.none, isLastMove: isLastMove, blinkValue: blinkValue);
|
||||
} else if (themeType == AppThemeType.doodle) {
|
||||
Color doodleColor = line.owner == Player.none ? Colors.black.withOpacity(0.05) : lineColor;
|
||||
if (isLastMove && line.owner != Player.none) {
|
||||
doodleColor = Color.lerp(doodleColor, Colors.black, blinkValue * 0.4) ?? doodleColor;
|
||||
}
|
||||
if (isLastMove && line.owner != Player.none) doodleColor = Color.lerp(doodleColor, Colors.black, blinkValue * 0.4) ?? doodleColor;
|
||||
_drawWobblyLine(canvas, p1, p2, doodleColor, line.owner != Player.none, isLastMove: isLastMove, blinkValue: blinkValue);
|
||||
} else if (themeType == AppThemeType.arcade) {
|
||||
_drawArcadeLine(canvas, p1, p2, lineColor, line.owner != Player.none, isLastMove: isLastMove, blinkValue: blinkValue);
|
||||
} else if (themeType == AppThemeType.grimorio) {
|
||||
_drawGrimorioLine(canvas, p1, p2, lineColor, line.owner != Player.none, isLastMove: isLastMove, blinkValue: blinkValue);
|
||||
} else {
|
||||
if (isLastMove && line.owner != Player.none) {
|
||||
lineColor = Color.lerp(lineColor, Colors.white, blinkValue * 0.5) ?? lineColor;
|
||||
}
|
||||
if (isLastMove && line.owner != Player.none) lineColor = Color.lerp(lineColor, Colors.white, blinkValue * 0.5) ?? lineColor;
|
||||
canvas.drawLine(p1, p2, Paint()..color = lineColor..strokeWidth = isLastMove ? 6.0 + (2.0 * blinkValue) : 6.0..strokeCap = StrokeCap.round);
|
||||
}
|
||||
}
|
||||
|
||||
// --- 3. DISEGNO PUNTINI ---
|
||||
final dotPaint = Paint()..style = PaintingStyle.fill;
|
||||
|
||||
Set<Dot> activeDots = {};
|
||||
for (var line in board.lines) {
|
||||
if (line.isPlayable) {
|
||||
activeDots.add(line.p1);
|
||||
activeDots.add(line.p2);
|
||||
activeDots.add(line.p1); activeDots.add(line.p2);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -138,6 +183,13 @@ class BoardPainter extends CustomPainter {
|
|||
canvas.drawCircle(pos, 3.0, Paint()..color = Colors.white.withOpacity(0.5));
|
||||
} else if (themeType == AppThemeType.doodle) {
|
||||
canvas.drawRect(Rect.fromCenter(center: pos, width: 4, height: 4), dotPaint..color = Colors.black.withOpacity(0.25));
|
||||
} else if (themeType == AppThemeType.arcade) {
|
||||
canvas.drawRect(Rect.fromCenter(center: pos, width: 8, height: 8), dotPaint..color = theme.gridLine.withOpacity(0.9));
|
||||
canvas.drawRect(Rect.fromCenter(center: pos, width: 4, height: 4), dotPaint..color = theme.background);
|
||||
} else if (themeType == AppThemeType.grimorio) {
|
||||
canvas.drawCircle(pos, 6.0, Paint()..color = theme.gridLine.withOpacity(0.3)..maskFilter = const MaskFilter.blur(BlurStyle.normal, 3.0));
|
||||
Path crystal = Path()..moveTo(pos.dx, pos.dy - 5)..lineTo(pos.dx + 3, pos.dy)..lineTo(pos.dx, pos.dy + 5)..lineTo(pos.dx - 3, pos.dy)..close();
|
||||
canvas.drawPath(crystal, dotPaint..color = theme.gridLine.withOpacity(0.8));
|
||||
} else {
|
||||
canvas.drawCircle(pos, 5.0, dotPaint..color = theme.text.withOpacity(0.6));
|
||||
}
|
||||
|
|
@ -149,189 +201,146 @@ class BoardPainter extends CustomPainter {
|
|||
textPainter.text = TextSpan(
|
||||
text: String.fromCharCode(icon.codePoint),
|
||||
style: TextStyle(
|
||||
color: color.withOpacity(0.7),
|
||||
color: themeType == AppThemeType.arcade ? color : color.withOpacity(0.7),
|
||||
fontSize: rect.width * 0.45,
|
||||
fontFamily: icon.fontFamily,
|
||||
package: icon.fontPackage,
|
||||
shadows: [Shadow(color: color.withOpacity(0.6), blurRadius: 10, offset: const Offset(0, 0))]
|
||||
shadows: themeType == AppThemeType.arcade ? [] : [Shadow(color: color.withOpacity(0.6), blurRadius: 10, offset: const Offset(0, 0))]
|
||||
),
|
||||
);
|
||||
textPainter.layout();
|
||||
textPainter.paint(canvas, Offset(rect.center.dx - textPainter.width / 2, rect.center.dy - textPainter.height / 2));
|
||||
}
|
||||
|
||||
void _drawCrackedIceLine(Canvas canvas, Offset p1, Offset p2, double blink) {
|
||||
Paint crackPaint = Paint()
|
||||
..color = Colors.cyanAccent.withOpacity(0.6 + (0.4 * blink))
|
||||
..strokeWidth = 3.0
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeCap = StrokeCap.round
|
||||
..maskFilter = const MaskFilter.blur(BlurStyle.solid, 2.0);
|
||||
|
||||
// Effetto linea frammentata
|
||||
canvas.drawLine(p1, p2, Paint()..color = Colors.cyan.withOpacity(0.2)..strokeWidth=6.0);
|
||||
|
||||
Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy);
|
||||
double len = dir.length; Vector2 ndir = dir.normalized(); Vector2 perp = Vector2(-ndir.y, ndir.x);
|
||||
|
||||
Path crack = Path()..moveTo(p1.dx, p1.dy);
|
||||
int zigzags = 6;
|
||||
for (int i=1; i<zigzags; i++) {
|
||||
double d = len * (i / zigzags);
|
||||
Offset basePt = Offset(p1.dx + ndir.x * d, p1.dy + ndir.y * d);
|
||||
double offset = (i % 2 == 0 ? 3.0 : -3.0);
|
||||
crack.lineTo(basePt.dx + perp.x * offset, basePt.dy + perp.y * offset);
|
||||
}
|
||||
crack.lineTo(p2.dx, p2.dy);
|
||||
canvas.drawPath(crack, crackPaint);
|
||||
}
|
||||
|
||||
void _drawArcadeBox(Canvas canvas, Rect rect, Color color) {
|
||||
double pixelSize = 4.0; Paint paint = Paint()..color = color.withOpacity(0.9)..style = PaintingStyle.fill;
|
||||
for (double y = rect.top; y < rect.bottom; y += pixelSize) {
|
||||
for (double x = rect.left; x < rect.right; x += pixelSize) {
|
||||
int xi = ((x - rect.left) / pixelSize).floor(); int yi = ((y - rect.top) / pixelSize).floor();
|
||||
if ((xi + yi) % 2 == 0) canvas.drawRect(Rect.fromLTWH(x, y, pixelSize, pixelSize), paint);
|
||||
}
|
||||
}
|
||||
canvas.drawRect(rect.deflate(2.0), Paint()..color = Colors.white.withOpacity(0.4)..style = PaintingStyle.stroke..strokeWidth = 2.0);
|
||||
}
|
||||
|
||||
void _drawGrimorioBox(Canvas canvas, Rect rect, Color color) {
|
||||
canvas.drawRect(rect, Paint()..color = color.withOpacity(0.15)..style=PaintingStyle.fill);
|
||||
Offset c = rect.center; double r = rect.width * 0.35;
|
||||
Paint linePaint = Paint()..color = color.withOpacity(0.8)..style = PaintingStyle.stroke..strokeWidth = 1.5..maskFilter = const MaskFilter.blur(BlurStyle.solid, 1.0);
|
||||
canvas.drawCircle(c, r, linePaint); canvas.drawCircle(c, r * 0.8, linePaint..strokeWidth = 0.5);
|
||||
Path p = Path();
|
||||
for(int i=0; i<3; i++) {
|
||||
double a = -pi/2 + i * 2*pi/3; Offset pt = Offset(c.dx + r*cos(a), c.dy + r*sin(a));
|
||||
if(i==0) p.moveTo(pt.dx, pt.dy); else p.lineTo(pt.dx, pt.dy);
|
||||
}
|
||||
p.close(); canvas.drawPath(p, linePaint..strokeWidth = 1.0);
|
||||
}
|
||||
|
||||
void _drawArcadeLine(Canvas canvas, Offset p1, Offset p2, Color color, bool isConquered, {bool isLastMove = false, double blinkValue = 0.0}) {
|
||||
double pixelSize = 6.0; Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy); double len = dir.length; Vector2 ndir = dir.normalized();
|
||||
Paint paint = Paint()..color = isConquered ? color : color.withOpacity(0.15)..style = PaintingStyle.fill;
|
||||
Paint highlight = Paint()..color = Colors.white.withOpacity(0.6)..style = PaintingStyle.fill;
|
||||
for(double d = 0; d <= len; d += pixelSize + 1.0) {
|
||||
Offset pt = Offset(p1.dx + ndir.x * d, p1.dy + ndir.y * d);
|
||||
canvas.drawRect(Rect.fromCenter(center: pt, width: pixelSize, height: pixelSize), paint);
|
||||
if (isConquered && (d / (pixelSize+1.0)).floor() % 3 == 0) canvas.drawRect(Rect.fromCenter(center: pt - const Offset(1,1), width: pixelSize*0.4, height: pixelSize*0.4), highlight);
|
||||
}
|
||||
if (isLastMove && isConquered) canvas.drawRect(Rect.fromPoints(p1, p2).inflate(4.0), Paint()..color = Colors.white.withOpacity(blinkValue*0.4)..style=PaintingStyle.stroke..strokeWidth=2.0);
|
||||
}
|
||||
|
||||
void _drawGrimorioLine(Canvas canvas, Offset p1, Offset p2, Color color, bool isConquered, {bool isLastMove = false, double blinkValue = 0.0}) {
|
||||
if (!isConquered) { canvas.drawLine(p1, p2, Paint()..color = color.withOpacity(0.15)..strokeWidth = 2.0..strokeCap = StrokeCap.round); return; }
|
||||
canvas.drawLine(p1, p2, Paint()..color = color.withOpacity(0.6)..strokeWidth = 5.0..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4.0));
|
||||
canvas.drawLine(p1, p2, Paint()..color = Colors.white.withOpacity(0.7)..strokeWidth = 1.5..strokeCap = StrokeCap.round);
|
||||
int seed = (p1.dx * 1000 + p1.dy).toInt(); Random rand = Random(seed);
|
||||
Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy); double len = dir.length; Vector2 ndir = dir.normalized(); Vector2 perp = Vector2(-ndir.y, ndir.x);
|
||||
Path thread1 = Path(); Path thread2 = Path(); int segments = 15; double step = len / segments;
|
||||
double phaseOffset = (isLastMove ? blinkValue * pi * 4 : 0) + rand.nextDouble()*pi;
|
||||
for(int i = 0; i <= segments; i++) {
|
||||
double d = i * step; Offset basePt = Offset(p1.dx + ndir.x * d, p1.dy + ndir.y * d);
|
||||
double amplitude = 3.5; double wave1 = sin(d * 0.15 + phaseOffset) * amplitude; double wave2 = cos(d * 0.15 + phaseOffset) * amplitude;
|
||||
Offset pt1 = basePt + Offset(perp.x * wave1, perp.y * wave1); Offset pt2 = basePt + Offset(perp.x * wave2, perp.y * wave2);
|
||||
if (i == 0) { thread1.moveTo(pt1.dx, pt1.dy); thread2.moveTo(pt2.dx, pt2.dy); } else { thread1.lineTo(pt1.dx, pt1.dy); thread2.lineTo(pt2.dx, pt2.dy); }
|
||||
}
|
||||
Paint threadPaint = Paint()..color = color.withOpacity(0.9)..style = PaintingStyle.stroke..strokeWidth = 1.5..maskFilter = const MaskFilter.blur(BlurStyle.solid, 1.0);
|
||||
canvas.drawPath(thread1, threadPaint); canvas.drawPath(thread2, threadPaint..color = Colors.white.withOpacity(0.5));
|
||||
}
|
||||
|
||||
void _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),
|
||||
);
|
||||
|
||||
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),
|
||||
);
|
||||
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);
|
||||
double randomTipX = (rand.nextDouble() - 0.5) * w * 0.8; drawFlameLayer(1.0, outerColor, randomTipX); drawFlameLayer(0.65, midColor.withOpacity(0.9), randomTipX * 0.6); drawFlameLayer(0.35, coreColor.withOpacity(0.9), randomTipX * 0.2);
|
||||
}
|
||||
|
||||
void _drawScribbleBox(Canvas canvas, Rect baseRect, Color color) {
|
||||
final rand = Random((baseRect.left + baseRect.top).toInt());
|
||||
final paint = Paint()
|
||||
..color = color.withOpacity(0.85)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 3.5
|
||||
..strokeCap = StrokeCap.round
|
||||
..strokeJoin = StrokeJoin.round;
|
||||
|
||||
final path = Path();
|
||||
Rect rect = baseRect.deflate(4.0);
|
||||
|
||||
int numZigs = 15 + rand.nextInt(6);
|
||||
double stepY = rect.height / numZigs;
|
||||
|
||||
final paint = Paint()..color = color.withOpacity(0.85)..style = PaintingStyle.stroke..strokeWidth = 3.5..strokeCap = StrokeCap.round..strokeJoin = StrokeJoin.round;
|
||||
final path = Path(); Rect rect = baseRect.deflate(4.0); int numZigs = 15 + rand.nextInt(6); double stepY = rect.height / numZigs;
|
||||
path.moveTo(rect.left + rand.nextDouble() * 5, rect.top + rand.nextDouble() * 5);
|
||||
|
||||
for (int i = 1; i <= numZigs; i++) {
|
||||
double targetX = (i % 2 != 0) ? rect.right + (rand.nextDouble() * 4 - 2) : rect.left + (rand.nextDouble() * 4 - 2);
|
||||
double targetY = rect.top + stepY * i + (rand.nextDouble() - 0.5) * 3;
|
||||
double ctrlX = rect.center.dx + (rand.nextDouble() - 0.5) * 20;
|
||||
double ctrlY = targetY - stepY / 2;
|
||||
path.quadraticBezierTo(ctrlX, ctrlY, targetX, targetY);
|
||||
}
|
||||
for (int i = 1; i <= numZigs; i++) { double targetX = (i % 2 != 0) ? rect.right + (rand.nextDouble() * 4 - 2) : rect.left + (rand.nextDouble() * 4 - 2); double targetY = rect.top + stepY * i + (rand.nextDouble() - 0.5) * 3; double ctrlX = rect.center.dx + (rand.nextDouble() - 0.5) * 20; double ctrlY = targetY - stepY / 2; path.quadraticBezierTo(ctrlX, ctrlY, targetX, targetY); }
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
void _drawRealisticMatch(Canvas canvas, Offset p1, Offset p2, Color headColor, {bool isLastMove = false, double blinkValue = 0.0}) {
|
||||
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();
|
||||
|
||||
int seed = (p1.dx * 1000 + p1.dy).toInt(); Random rand = Random(seed); Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy).normalized(); double shrink = 8.0; Offset start = Offset(p1.dx + dir.x * shrink, p1.dy + dir.y * shrink); Offset end = Offset(p2.dx - dir.x * shrink, p2.dy - dir.y * shrink); start += Offset(rand.nextDouble() * 4 - 2, rand.nextDouble() * 4 - 2); end += Offset(rand.nextDouble() * 4 - 2, rand.nextDouble() * 4 - 2); bool headAtEnd = rand.nextBool(); Offset headPos = headAtEnd ? end : start; Offset tailPos = headAtEnd ? start : end; Vector2 matchDir = Vector2(headPos.dx - tailPos.dx, headPos.dy - tailPos.dy).normalized();
|
||||
canvas.drawLine(tailPos + const Offset(4, 4), headPos + const Offset(4, 4), Paint()..color = Colors.black.withOpacity(0.6)..strokeWidth = 7.0..strokeCap = StrokeCap.round);
|
||||
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
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)); }
|
||||
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}) {
|
||||
double mainWidth = isConquered ? (isLastMove ? 6.0 + (blinkValue * 3.0) : 6.0) : 3.0;
|
||||
Color coreColor = isConquered ? (isLastMove ? Color.lerp(Colors.white, color, 1.0 - blinkValue)! : Colors.white.withOpacity(0.9)) : color.withOpacity(0.6);
|
||||
|
||||
canvas.drawLine(p1, p2, Paint()
|
||||
..color = color.withOpacity(isConquered ? (isLastMove ? 0.4 + (0.4 * blinkValue) : 0.4) : 0.2)
|
||||
..strokeWidth = mainWidth * 4
|
||||
..strokeCap = StrokeCap.round
|
||||
..maskFilter = MaskFilter.blur(BlurStyle.normal, isConquered ? 12.0 : 6.0)
|
||||
);
|
||||
|
||||
if (isConquered) {
|
||||
canvas.drawLine(p1, p2, Paint()
|
||||
..color = color.withOpacity(isLastMove ? 0.7 + (0.3 * blinkValue) : 0.7)
|
||||
..strokeWidth = mainWidth * 2
|
||||
..strokeCap = StrokeCap.round
|
||||
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6.0)
|
||||
);
|
||||
}
|
||||
|
||||
canvas.drawLine(p1, p2, Paint()
|
||||
..color = coreColor
|
||||
..strokeWidth = mainWidth
|
||||
..strokeCap = StrokeCap.round
|
||||
);
|
||||
double mainWidth = isConquered ? (isLastMove ? 6.0 + (blinkValue * 3.0) : 6.0) : 3.0; Color coreColor = isConquered ? (isLastMove ? Color.lerp(Colors.white, color, 1.0 - blinkValue)! : Colors.white.withOpacity(0.9)) : color.withOpacity(0.6);
|
||||
canvas.drawLine(p1, p2, Paint()..color = color.withOpacity(isConquered ? (isLastMove ? 0.4 + (0.4 * blinkValue) : 0.4) : 0.2)..strokeWidth = mainWidth * 4..strokeCap = StrokeCap.round..maskFilter = MaskFilter.blur(BlurStyle.normal, isConquered ? 12.0 : 6.0));
|
||||
if (isConquered) { canvas.drawLine(p1, p2, Paint()..color = color.withOpacity(isLastMove ? 0.7 + (0.3 * blinkValue) : 0.7)..strokeWidth = mainWidth * 2..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6.0)); }
|
||||
canvas.drawLine(p1, p2, Paint()..color = coreColor..strokeWidth = mainWidth..strokeCap = StrokeCap.round);
|
||||
}
|
||||
|
||||
void _drawWobblyLine(Canvas canvas, Offset p1, Offset p2, Color color, bool isConquered, {bool isLastMove = false, double blinkValue = 0.0}) {
|
||||
final random = Random((p1.dx + p1.dy + p2.dx + p2.dy).toInt());
|
||||
final dx = p2.dx - p1.dx;
|
||||
final dy = p2.dy - p1.dy;
|
||||
|
||||
final random = Random((p1.dx + p1.dy + p2.dx + p2.dy).toInt()); final dx = p2.dx - p1.dx; final dy = p2.dy - p1.dy;
|
||||
double strokeW = isConquered ? (isLastMove ? 4.5 + (2.0 * blinkValue) : 4.5) : 2.0;
|
||||
|
||||
final basePaint = Paint()
|
||||
..color = color
|
||||
..strokeWidth = strokeW
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeCap = StrokeCap.round;
|
||||
|
||||
final mid1 = Offset(p1.dx + dx / 2 + (random.nextDouble() - 0.5) * 8, p1.dy + dy / 2 + (random.nextDouble() - 0.5) * 8);
|
||||
canvas.drawPath(Path()..moveTo(p1.dx, p1.dy)..quadraticBezierTo(mid1.dx, mid1.dy, p2.dx, p2.dy), basePaint);
|
||||
|
||||
final mid2 = Offset(p1.dx + dx / 2 + (random.nextDouble() - 0.5) * 6, p1.dy + dy / 2 + (random.nextDouble() - 0.5) * 6);
|
||||
canvas.drawPath(Path()..moveTo(p1.dx, p1.dy)..quadraticBezierTo(mid2.dx, mid2.dy, p2.dx, p2.dy), basePaint..strokeWidth = strokeW * 0.5..color = color.withOpacity(0.8));
|
||||
final basePaint = Paint()..color = color..strokeWidth = strokeW..style = PaintingStyle.stroke..strokeCap = StrokeCap.round;
|
||||
final mid1 = Offset(p1.dx + dx / 2 + (random.nextDouble() - 0.5) * 8, p1.dy + dy / 2 + (random.nextDouble() - 0.5) * 8); canvas.drawPath(Path()..moveTo(p1.dx, p1.dy)..quadraticBezierTo(mid1.dx, mid1.dy, p2.dx, p2.dy), basePaint);
|
||||
final mid2 = Offset(p1.dx + dx / 2 + (random.nextDouble() - 0.5) * 6, p1.dy + dy / 2 + (random.nextDouble() - 0.5) * 6); canvas.drawPath(Path()..moveTo(p1.dx, p1.dy)..quadraticBezierTo(mid2.dx, mid2.dy, p2.dx, p2.dy), basePaint..strokeWidth = strokeW * 0.5..color = color.withOpacity(0.8));
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant BoardPainter oldDelegate) => true;
|
||||
@override bool shouldRepaint(covariant BoardPainter oldDelegate) => true;
|
||||
}
|
||||
|
||||
class Vector2 {
|
||||
final double x, y;
|
||||
Vector2(this.x, this.y);
|
||||
double get length => sqrt(x * x + y * y);
|
||||
Vector2 normalized() {
|
||||
double l = length;
|
||||
return l == 0 ? Vector2(0, 0) : Vector2(x / l, y / l);
|
||||
}
|
||||
final double x, y; Vector2(this.x, this.y); double get length => sqrt(x * x + y * y);
|
||||
Vector2 normalized() { double l = length; return l == 0 ? Vector2(0, 0) : Vector2(x / l, y / l); }
|
||||
}
|
||||
|
|
@ -6,12 +6,27 @@ import 'dart:ui';
|
|||
import 'dart:math' as math;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../logic/game_controller.dart';
|
||||
import '../../core/theme_manager.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
import 'board_painter.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 {
|
||||
const GameScreen({super.key});
|
||||
|
|
@ -25,20 +40,19 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
|
|||
bool _gameOverDialogShown = false;
|
||||
bool _opponentLeftDialogShown = false;
|
||||
|
||||
// Variabili per coprire il posizionamento del Jolly in Locale
|
||||
bool _hideJokerMessage = false;
|
||||
bool _wasSetupPhase = false;
|
||||
Player _lastJokerTurn = Player.red;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_blinkController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 600),
|
||||
)..repeat(reverse: true);
|
||||
_blinkController = AnimationController(vsync: this, duration: const Duration(milliseconds: 600))..repeat(reverse: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_blinkController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
void dispose() { _blinkController.dispose(); super.dispose(); }
|
||||
|
||||
void _showGameOverDialog(BuildContext context, GameController game, ThemeColors theme, AppThemeType themeType) {
|
||||
_gameOverDialogShown = true;
|
||||
|
|
@ -50,22 +64,21 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
|
|||
builder: (context, controller, child) {
|
||||
if (!controller.isGameOver) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (Navigator.canPop(dialogContext)) Navigator.pop(dialogContext);
|
||||
_gameOverDialogShown = false;
|
||||
if (_gameOverDialogShown) {
|
||||
_gameOverDialogShown = false;
|
||||
if (Navigator.canPop(dialogContext)) Navigator.pop(dialogContext);
|
||||
}
|
||||
});
|
||||
return const SizedBox();
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
int red = controller.board.scoreRed;
|
||||
int blue = controller.board.scoreBlue;
|
||||
int red = controller.board.scoreRed; int blue = controller.board.scoreBlue;
|
||||
bool playerBeatCPU = controller.isVsCPU && red > blue;
|
||||
|
||||
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";
|
||||
|
||||
String winnerText = "";
|
||||
Color winnerColor = theme.text;
|
||||
String winnerText = ""; Color winnerColor = theme.text;
|
||||
if (red > blue) { winnerText = "VINCE $nameRed!"; winnerColor = theme.playerRed; }
|
||||
else if (blue > red) { winnerText = "VINCE $nameBlue!"; winnerColor = theme.playerBlue; }
|
||||
else { winnerText = "PAREGGIO!"; winnerColor = theme.text; }
|
||||
|
|
@ -73,11 +86,11 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
|
|||
return AlertDialog(
|
||||
backgroundColor: theme.background,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: BorderSide(color: winnerColor.withOpacity(0.5), width: 2)),
|
||||
title: Text("FINE PARTITA", textAlign: TextAlign.center, style: TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 22)),
|
||||
title: Text("FINE PARTITA", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 22))),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(winnerText, textAlign: TextAlign.center, style: 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),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
|
|
@ -85,24 +98,39 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
|
|||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text("$nameRed: $red", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerRed)),
|
||||
Text(" - ", style: TextStyle(fontSize: 18, color: theme.text)),
|
||||
Text("$nameBlue: $blue", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerBlue)),
|
||||
Text("$nameRed: $red", style: _getTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerRed))),
|
||||
Text(" - ", style: _getTextStyle(themeType, TextStyle(fontSize: 18, color: theme.text))),
|
||||
Text("$nameBlue: $blue", style: _getTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerBlue))),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
if (controller.lastMatchXP > 0) ...[
|
||||
const SizedBox(height: 15),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: Colors.greenAccent, width: 1.5),
|
||||
boxShadow: themeType == AppThemeType.cyberpunk ? [const BoxShadow(color: Colors.greenAccent, blurRadius: 10, spreadRadius: -5)] : [],
|
||||
),
|
||||
child: Text("+ ${controller.lastMatchXP} XP", style: _getTextStyle(themeType, const TextStyle(color: Colors.greenAccent, fontWeight: FontWeight.w900, fontSize: 16, letterSpacing: 1.5))),
|
||||
),
|
||||
],
|
||||
|
||||
if (controller.isVsCPU) ...[
|
||||
const SizedBox(height: 15),
|
||||
Text("Difficoltà CPU: Livello ${controller.cpuLevel}", style: 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) ...[
|
||||
const SizedBox(height: 20),
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: winnerColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5),
|
||||
onPressed: () { Navigator.pop(dialogContext); _gameOverDialogShown = false; controller.increaseLevelAndRestart(); },
|
||||
child: const Text("PROSSIMO LIVELLO ➔", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
onPressed: () { controller.increaseLevelAndRestart(); },
|
||||
child: Text("PROSSIMO LIVELLO ➔", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16))),
|
||||
)
|
||||
else if (controller.isOnline)
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: controller.rematchRequested ? Colors.grey : (winnerColor == theme.text ? theme.playerBlue : winnerColor), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5),
|
||||
onPressed: controller.rematchRequested ? null : () {
|
||||
controller.requestRematch();
|
||||
},
|
||||
child: Text(controller.opponentWantsRematch ? "ACCETTA RIVINCITA" : "CHIEDI RIVINCITA", style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 1.0)),
|
||||
onPressed: controller.rematchRequested ? null : () { controller.requestRematch(); },
|
||||
child: Text(controller.opponentWantsRematch ? "ACCETTA RIVINCITA" : "CHIEDI RIVINCITA", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 1.0))),
|
||||
)
|
||||
else
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: winnerColor == theme.text ? theme.playerBlue : winnerColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5),
|
||||
onPressed: () { Navigator.pop(dialogContext); _gameOverDialogShown = false; controller.startNewGame(controller.board.radius, vsCPU: controller.isVsCPU); },
|
||||
child: const Text("RIGIOCA", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 2)),
|
||||
onPressed: () { controller.startNewGame(controller.board.radius, vsCPU: controller.isVsCPU, shape: controller.board.shape, timeMode: controller.isTimeMode); },
|
||||
child: Text("RIGIOCA", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 2))),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(foregroundColor: theme.text, side: BorderSide(color: theme.text.withOpacity(0.3), width: 2), padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))),
|
||||
onPressed: () {
|
||||
if (controller.isOnline) controller.disconnectOnlineGame();
|
||||
Navigator.pop(dialogContext);
|
||||
Navigator.pop(context);
|
||||
_gameOverDialogShown = false;
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final themeManager = context.watch<ThemeManager>();
|
||||
|
|
@ -158,6 +246,18 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
|
|||
final theme = themeManager.currentColors;
|
||||
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((_) {
|
||||
if (gameController.opponentLeft && !_opponentLeftDialogShown) {
|
||||
_opponentLeftDialogShown = true;
|
||||
|
|
@ -167,14 +267,14 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
|
|||
builder: (dialogContext) => AlertDialog(
|
||||
backgroundColor: theme.background,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
title: Text("VITTORIA A TAVOLINO!", textAlign: TextAlign.center, style: TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold)),
|
||||
content: Text("L'avversario ha abbandonato la stanza.\nSei il vincitore incontestato!", textAlign: TextAlign.center, style: TextStyle(color: theme.text, fontSize: 16)),
|
||||
title: Text("VITTORIA A TAVOLINO!", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold))),
|
||||
content: Text("L'avversario ha abbandonato la stanza.\nSei il vincitore incontestato!", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: theme.text, fontSize: 16))),
|
||||
actionsAlignment: MainAxisAlignment.center,
|
||||
actions: [
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: theme.playerBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))),
|
||||
onPressed: () { gameController.disconnectOnlineGame(); Navigator.pop(dialogContext); Navigator.pop(context); },
|
||||
child: 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.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();
|
||||
if (gameController.isOnline && !gameController.isGameOver) {
|
||||
|
|
@ -196,7 +296,7 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
|
|||
emojiBar = Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
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),
|
||||
border: Border.all(color: themeType == AppThemeType.cyberpunk ? theme.playerBlue.withOpacity(0.3) : Colors.black12, width: 2),
|
||||
),
|
||||
|
|
@ -220,25 +320,38 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
|
|||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return GestureDetector(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
int cols = gameController.board.columns + 1;
|
||||
int rows = gameController.board.rows + 1;
|
||||
double boxSize = constraints.maxWidth / cols;
|
||||
double requiredHeight = boxSize * rows;
|
||||
if (requiredHeight > constraints.maxHeight) { boxSize = constraints.maxHeight / rows; }
|
||||
double actualWidth = boxSize * cols;
|
||||
double actualHeight = boxSize * rows;
|
||||
|
||||
return SizedBox(
|
||||
width: actualWidth, height: actualHeight,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTapDown: (details) => _handleTap(details.localPosition, constraints.maxWidth, gameController, themeType),
|
||||
onTapDown: (details) => _handleTap(details.localPosition, actualWidth, actualHeight, gameController, themeType),
|
||||
child: AnimatedBuilder(
|
||||
animation: _blinkController,
|
||||
builder: (context, child) {
|
||||
return CustomPaint(
|
||||
size: Size(constraints.maxWidth, constraints.maxHeight),
|
||||
painter: BoardPainter(board: gameController.board, theme: theme, themeType: themeType, blinkValue: _blinkController.value),
|
||||
size: Size(actualWidth, actualHeight),
|
||||
painter: BoardPainter(
|
||||
board: gameController.board, theme: theme, themeType: themeType,
|
||||
blinkValue: _blinkController.value, isOnline: gameController.isOnline,
|
||||
isVsCPU: gameController.isVsCPU, isSetupPhase: gameController.isSetupPhase,
|
||||
myPlayer: gameController.myPlayer, jokerTurn: gameController.jokerTurn,
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -257,7 +370,7 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.smart_toy_rounded, size: 16, color: indicatorColor), const SizedBox(width: 8),
|
||||
Text("LIVELLO CPU: ${gameController.cpuLevel}", style: TextStyle(color: indicatorColor, fontWeight: FontWeight.bold, fontSize: 13, letterSpacing: 1.0)),
|
||||
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(
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.4), offset: const Offset(0, 4), blurRadius: 5)]),
|
||||
child: TextButton.icon(
|
||||
style: TextButton.styleFrom(backgroundColor: bgImage != null ? Colors.black87 : theme.background, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: BorderSide(color: Colors.white.withOpacity(0.1), width: 1))),
|
||||
icon: Icon(Icons.exit_to_app, color: bgImage != null ? Colors.white : theme.text, size: 20),
|
||||
style: TextButton.styleFrom(backgroundColor: bgImage != null || themeType == AppThemeType.arcade ? Colors.black87 : theme.background, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: BorderSide(color: Colors.white.withOpacity(0.1), width: 1))),
|
||||
icon: Icon(Icons.exit_to_app, color: bgImage != null || themeType == AppThemeType.arcade ? Colors.white : theme.text, size: 20),
|
||||
onPressed: () { gameController.disconnectOnlineGame(); Navigator.pop(context); },
|
||||
label: Text("ESCI", style: 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)
|
||||
Positioned(
|
||||
top: 80,
|
||||
left: gameController.isHost ? 30 : null,
|
||||
right: gameController.isHost ? null : 30,
|
||||
child: _BouncingEmoji(emoji: gameController.myReaction!),
|
||||
),
|
||||
Positioned(top: 80, left: gameController.isHost ? 30 : null, right: gameController.isHost ? null : 30, child: _BouncingEmoji(emoji: gameController.myReaction!)),
|
||||
if (gameController.opponentReaction != null)
|
||||
Positioned(
|
||||
top: 80,
|
||||
left: !gameController.isHost ? 30 : null,
|
||||
right: !gameController.isHost ? null : 30,
|
||||
child: _BouncingEmoji(emoji: gameController.opponentReaction!),
|
||||
),
|
||||
Positioned(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,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (gameController.isTimeMode && !gameController.isCPUThinking && !gameController.isGameOver && gameController.timeLeft > 0 && gameController.timeLeft <= 5)
|
||||
Positioned.fill(child: BlitzBackgroundEffect(timeLeft: gameController.timeLeft, color: theme.playerRed)),
|
||||
if (gameController.isTimeMode && !gameController.isCPUThinking && !gameController.isGameOver && gameController.timeLeft > 0 && gameController.timeLeft <= 5 && !gameController.isSetupPhase)
|
||||
Positioned.fill(child: BlitzBackgroundEffect(timeLeft: gameController.timeLeft, color: theme.playerRed, themeType: themeType)),
|
||||
|
||||
if (gameController.effectText.isNotEmpty)
|
||||
Positioned.fill(child: SpecialEventBackgroundEffect(text: gameController.effectText, color: gameController.effectColor)),
|
||||
Positioned.fill(child: SpecialEventBackgroundEffect(text: gameController.effectText, color: gameController.effectColor, themeType: themeType)),
|
||||
|
||||
Positioned.fill(child: gameContent),
|
||||
|
||||
// ==========================================
|
||||
// EFFETTI VISIVI (VFX) DI FINE PARTITA
|
||||
// ==========================================
|
||||
if (gameController.isGameOver && gameController.board.scoreRed != gameController.board.scoreBlue)
|
||||
// --- SCHERMATA COPRENTE PER IL PASSAGGIO DEL TELEFONO IN LOCALE ---
|
||||
if (gameController.isSetupPhase && !_hideJokerMessage)
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: WinnerVFXOverlay(
|
||||
winnerColor: gameController.board.scoreRed > gameController.board.scoreBlue ? theme.playerRed : theme.playerBlue,
|
||||
themeType: themeType,
|
||||
child: Container(
|
||||
// Il colore di sfondo riempie tutto lo schermo per non far sbirciare la griglia
|
||||
color: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade
|
||||
? 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;
|
||||
if (board.isGameOver) return;
|
||||
int cols = board.columns + 1; double spacing = width / cols; double offset = spacing / 2;
|
||||
|
||||
int gridPoints = board.radius * 2 + 2;
|
||||
double spacing = size / gridPoints;
|
||||
double offset = spacing / 2;
|
||||
|
||||
Line? closestLine;
|
||||
double minDistance = double.infinity;
|
||||
double maxTouchDistance = spacing * 0.4;
|
||||
|
||||
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 (controller.isSetupPhase) {
|
||||
int bx = ((tapPos.dx - offset) / spacing).floor(); int by = ((tapPos.dy - offset) / spacing).floor();
|
||||
controller.placeJoker(bx, by); return;
|
||||
}
|
||||
|
||||
Line? closestLine; double minDistance = double.infinity; double maxTouchDistance = spacing * 0.4;
|
||||
for (var line in board.lines) {
|
||||
if (line.owner != Player.none || !line.isPlayable) continue;
|
||||
Offset screenP1 = Offset(line.p1.x * spacing + offset, line.p1.y * spacing + offset);
|
||||
Offset screenP2 = Offset(line.p2.x * spacing + offset, line.p2.y * spacing + offset);
|
||||
double dist = _distanceToSegment(tapPos, screenP1, screenP2);
|
||||
if (dist < minDistance && dist < maxTouchDistance) { minDistance = dist; closestLine = line; }
|
||||
}
|
||||
if (closestLine != null) { controller.handleLineTap(closestLine, themeType); }
|
||||
}
|
||||
|
||||
|
|
@ -371,30 +479,16 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
|
|||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// CLASSI PER IL MOTORE PARTICELLARE (VFX) DI FINE PARTITA
|
||||
// ===========================================================================
|
||||
|
||||
class _Particle {
|
||||
double x, y;
|
||||
double vx, vy;
|
||||
Color color;
|
||||
double size;
|
||||
double angle;
|
||||
double spin;
|
||||
int type; // 0=cerchio, 1=quadrato, 2=triangolo
|
||||
|
||||
double x, y, vx, vy, size, angle, spin;
|
||||
Color color; int type;
|
||||
_Particle({required this.x, required this.y, required this.vx, required this.vy, required this.color, required this.size, required this.angle, required this.spin, required this.type});
|
||||
}
|
||||
|
||||
class WinnerVFXOverlay extends StatefulWidget {
|
||||
final Color winnerColor;
|
||||
final AppThemeType themeType;
|
||||
|
||||
final Color winnerColor; final AppThemeType 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 {
|
||||
|
|
@ -406,291 +500,125 @@ class _WinnerVFXOverlayState extends State<WinnerVFXOverlay> with SingleTickerPr
|
|||
@override
|
||||
void 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
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (!_initialized) {
|
||||
_initParticles(MediaQuery.of(context).size);
|
||||
_initialized = true;
|
||||
}
|
||||
if (!_initialized) { _initParticles(MediaQuery.of(context).size); _initialized = true; }
|
||||
}
|
||||
|
||||
void _initParticles(Size screenSize) {
|
||||
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];
|
||||
if (widget.themeType == AppThemeType.cyberpunk) {
|
||||
palette.add(Colors.cyanAccent);
|
||||
palette.add(Colors.yellowAccent);
|
||||
} else if (widget.themeType == AppThemeType.doodle) {
|
||||
palette.add(const Color(0xFF00008B)); // Inchiostro biro
|
||||
palette.add(Colors.redAccent);
|
||||
} else if (widget.themeType == AppThemeType.wood) {
|
||||
palette = [Colors.orangeAccent, Colors.yellow, Colors.red, Colors.white];
|
||||
}
|
||||
if (widget.themeType == AppThemeType.cyberpunk) { palette.add(Colors.cyanAccent); palette.add(Colors.yellowAccent); }
|
||||
else if (widget.themeType == AppThemeType.doodle) { palette.add(const Color(0xFF00008B)); palette.add(Colors.redAccent); }
|
||||
else if (widget.themeType == AppThemeType.wood) { palette = [Colors.orangeAccent, Colors.yellow, Colors.red, Colors.white]; }
|
||||
else if (widget.themeType == AppThemeType.arcade) { palette = [widget.winnerColor, Colors.white, Colors.greenAccent]; }
|
||||
else if (widget.themeType == AppThemeType.grimorio) { palette = [widget.winnerColor, Colors.deepPurpleAccent, Colors.white]; }
|
||||
|
||||
for (int i = 0; i < particleCount; i++) {
|
||||
// Esplosione dal centro verso l'esterno
|
||||
double speed = _rand.nextDouble() * 20 + 5;
|
||||
double theta = _rand.nextDouble() * 2 * math.pi;
|
||||
|
||||
_particles.add(_Particle(
|
||||
x: screenSize.width / 2,
|
||||
y: screenSize.height / 2,
|
||||
vx: speed * math.cos(theta),
|
||||
vy: speed * math.sin(theta) - 5, // 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),
|
||||
));
|
||||
_particles.add(_Particle(x: screenSize.width / 2, y: screenSize.height / 2, vx: speed * math.cos(theta), vy: speed * math.sin(theta) - 5, color: palette[_rand.nextInt(palette.length)], size: _rand.nextDouble() * 10 + 6, angle: _rand.nextDouble() * math.pi, spin: (_rand.nextDouble() - 0.5) * 0.5, type: _rand.nextInt(3)));
|
||||
}
|
||||
}
|
||||
|
||||
void _updateParticles() {
|
||||
setState(() {
|
||||
for (var p in _particles) {
|
||||
p.x += p.vx;
|
||||
p.y += p.vy;
|
||||
|
||||
// Gravità e attrito
|
||||
if (widget.themeType == AppThemeType.cyberpunk) {
|
||||
p.vy += 0.1; // Gravità bassa (fluttuano di più)
|
||||
p.vx *= 0.98; // Attrito
|
||||
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
|
||||
p.x += p.vx; 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; }
|
||||
else if (widget.themeType == AppThemeType.arcade) { p.vy += 0.3; p.spin = 0; p.angle = 0; }
|
||||
else if (widget.themeType == AppThemeType.grimorio) { p.vy -= 0.1; p.x += math.sin(p.y * 0.02) * 1.5; p.size *= 0.995; }
|
||||
else { p.vy += 0.5; }
|
||||
p.angle += p.spin; p.size *= 0.99;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_vfxController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomPaint(
|
||||
painter: _VFXPainter(particles: _particles, themeType: widget.themeType),
|
||||
child: Container(),
|
||||
);
|
||||
}
|
||||
@override void dispose() { _vfxController.dispose(); super.dispose(); }
|
||||
@override Widget build(BuildContext context) { return CustomPaint(painter: _VFXPainter(particles: _particles, themeType: widget.themeType), child: Container()); }
|
||||
}
|
||||
|
||||
class _VFXPainter extends CustomPainter {
|
||||
final List<_Particle> particles;
|
||||
final AppThemeType themeType;
|
||||
|
||||
final List<_Particle> particles; final AppThemeType themeType;
|
||||
_VFXPainter({required this.particles, required this.themeType});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
for (var p in particles) {
|
||||
if (p.size < 0.5) continue;
|
||||
|
||||
final paint = Paint()
|
||||
..color = p.color
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
// 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);
|
||||
final paint = Paint()..color = p.color..style = PaintingStyle.fill;
|
||||
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) {
|
||||
// Stile schizzato
|
||||
paint.style = PaintingStyle.stroke;
|
||||
paint.strokeWidth = 2.0;
|
||||
if (p.type == 0) {
|
||||
canvas.drawCircle(Offset.zero, p.size, paint);
|
||||
} else {
|
||||
canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: p.size*2, height: p.size*2), paint);
|
||||
}
|
||||
paint.style = PaintingStyle.stroke; paint.strokeWidth = 2.0;
|
||||
if (p.type == 0) { canvas.drawCircle(Offset.zero, p.size, paint); } else { canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: p.size*2, height: p.size*2), paint); }
|
||||
} else if (themeType == AppThemeType.wood) {
|
||||
// Scintille rotonde e sfuocate
|
||||
paint.maskFilter = const MaskFilter.blur(BlurStyle.normal, 3.0);
|
||||
paint.maskFilter = const MaskFilter.blur(BlurStyle.normal, 3.0); canvas.drawCircle(Offset.zero, p.size, paint);
|
||||
} else if (themeType == AppThemeType.arcade) {
|
||||
canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: p.size * 1.5, height: p.size * 1.5), paint);
|
||||
} else if (themeType == AppThemeType.grimorio) {
|
||||
paint.maskFilter = const MaskFilter.blur(BlurStyle.normal, 4.0);
|
||||
canvas.drawCircle(Offset.zero, p.size, paint);
|
||||
canvas.drawCircle(Offset.zero, p.size * 0.3, Paint()..color=Colors.white..style=PaintingStyle.fill);
|
||||
} else {
|
||||
// Forme standard per Minimal e Cyberpunk
|
||||
if (p.type == 0) {
|
||||
canvas.drawCircle(Offset.zero, p.size, paint);
|
||||
} else if (p.type == 1) {
|
||||
canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: p.size * 2, height: p.size * 2), paint);
|
||||
} else {
|
||||
var path = Path()
|
||||
..moveTo(0, -p.size)
|
||||
..lineTo(p.size, p.size)
|
||||
..lineTo(-p.size, p.size)
|
||||
..close();
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
if (p.type == 0) { canvas.drawCircle(Offset.zero, p.size, paint); }
|
||||
else if (p.type == 1) { canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: p.size * 2, height: p.size * 2), paint); }
|
||||
else { var path = Path()..moveTo(0, -p.size)..lineTo(p.size, p.size)..lineTo(-p.size, p.size)..close(); canvas.drawPath(path, paint); }
|
||||
}
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _VFXPainter oldDelegate) => true;
|
||||
@override bool shouldRepaint(covariant _VFXPainter oldDelegate) => true;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// WIDGET INTERNI ESISTENTENTI
|
||||
// ===========================================================================
|
||||
|
||||
class _BouncingEmoji extends StatefulWidget {
|
||||
final String emoji;
|
||||
const _BouncingEmoji({required this.emoji});
|
||||
|
||||
@override
|
||||
State<_BouncingEmoji> createState() => _BouncingEmojiState();
|
||||
final String emoji; const _BouncingEmoji({required this.emoji});
|
||||
@override State<_BouncingEmoji> createState() => _BouncingEmojiState();
|
||||
}
|
||||
|
||||
class _BouncingEmojiState extends State<_BouncingEmoji> with SingleTickerProviderStateMixin {
|
||||
late AnimationController _ctrl;
|
||||
late Animation<double> _anim;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 500))..repeat(reverse: true);
|
||||
_anim = Tween<double>(begin: -10, end: 10).animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeInOut));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() { _ctrl.dispose(); super.dispose(); }
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _anim,
|
||||
builder: (ctx, child) => Transform.translate(
|
||||
offset: Offset(0, _anim.value),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle, boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 5)]),
|
||||
child: Text(widget.emoji, style: const TextStyle(fontSize: 32)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
late AnimationController _ctrl; late Animation<double> _anim;
|
||||
@override void initState() { super.initState(); _ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 500))..repeat(reverse: true); _anim = Tween<double>(begin: -10, end: 10).animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeInOut)); }
|
||||
@override void dispose() { _ctrl.dispose(); super.dispose(); }
|
||||
@override Widget build(BuildContext context) { return AnimatedBuilder(animation: _anim, builder: (ctx, child) => Transform.translate(offset: Offset(0, _anim.value), child: Container(padding: const EdgeInsets.all(8), decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle, boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 5)]), child: Text(widget.emoji, style: const TextStyle(fontSize: 32))))); }
|
||||
}
|
||||
|
||||
class FullScreenGridPainter extends CustomPainter {
|
||||
final Color gridColor;
|
||||
FullScreenGridPainter(this.gridColor);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final Paint paperGridPaint = Paint()..color = gridColor..strokeWidth = 1.0..style = PaintingStyle.stroke;
|
||||
double paperStep = 20.0;
|
||||
for (double i = 0; i <= size.width; i += paperStep) canvas.drawLine(Offset(i, 0), Offset(i, size.height), paperGridPaint);
|
||||
for (double i = 0; i <= size.height; i += paperStep) canvas.drawLine(Offset(0, i), Offset(size.width, i), paperGridPaint);
|
||||
}
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
final Color gridColor; FullScreenGridPainter(this.gridColor);
|
||||
@override void paint(Canvas canvas, Size size) { final Paint paperGridPaint = Paint()..color = gridColor..strokeWidth = 1.0..style = PaintingStyle.stroke; double paperStep = 20.0; for (double i = 0; i <= size.width; i += paperStep) canvas.drawLine(Offset(i, 0), Offset(i, size.height), paperGridPaint); for (double i = 0; i <= size.height; i += paperStep) canvas.drawLine(Offset(0, i), Offset(size.width, i), paperGridPaint); }
|
||||
@override bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
class BlitzBackgroundEffect extends StatefulWidget {
|
||||
final int timeLeft;
|
||||
final Color color;
|
||||
const BlitzBackgroundEffect({super.key, required this.timeLeft, required this.color});
|
||||
@override
|
||||
State<BlitzBackgroundEffect> createState() => _BlitzBackgroundEffectState();
|
||||
final int timeLeft; final Color color; final AppThemeType themeType;
|
||||
const BlitzBackgroundEffect({super.key, required this.timeLeft, required this.color, required this.themeType});
|
||||
@override State<BlitzBackgroundEffect> createState() => _BlitzBackgroundEffectState();
|
||||
}
|
||||
|
||||
class _BlitzBackgroundEffectState extends State<BlitzBackgroundEffect> with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
@override
|
||||
void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 400))..repeat(reverse: true); }
|
||||
@override
|
||||
void dispose() { _controller.dispose(); super.dispose(); }
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
return Container(
|
||||
color: widget.color.withOpacity(0.12 * _controller.value),
|
||||
child: Center(
|
||||
child: ImageFiltered(
|
||||
imageFilter: ImageFilter.blur(sigmaX: 2.0, sigmaY: 2.0),
|
||||
child: Text('${widget.timeLeft}', style: TextStyle(fontSize: 300, fontWeight: FontWeight.w900, color: widget.color.withOpacity(0.35 + (0.3 * _controller.value)), height: 1.0)),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@override void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 400))..repeat(reverse: true); }
|
||||
@override void dispose() { _controller.dispose(); super.dispose(); }
|
||||
@override Widget build(BuildContext context) { return AnimatedBuilder(animation: _controller, builder: (context, child) { return Container(color: widget.color.withOpacity(0.12 * _controller.value), child: Center(child: ImageFiltered(imageFilter: ImageFilter.blur(sigmaX: 2.0, sigmaY: 2.0), child: Text('${widget.timeLeft}', style: _getTextStyle(widget.themeType, TextStyle(fontSize: 300, fontWeight: FontWeight.w900, color: widget.color.withOpacity(0.35 + (0.3 * _controller.value)), height: 1.0)))))); }); }
|
||||
}
|
||||
|
||||
class SpecialEventBackgroundEffect extends StatefulWidget {
|
||||
final String text;
|
||||
final Color color;
|
||||
const SpecialEventBackgroundEffect({super.key, required this.text, required this.color});
|
||||
@override
|
||||
State<SpecialEventBackgroundEffect> createState() => _SpecialEventBackgroundEffectState();
|
||||
final String text; final Color color; final AppThemeType themeType;
|
||||
const SpecialEventBackgroundEffect({super.key, required this.text, required this.color, required this.themeType});
|
||||
@override State<SpecialEventBackgroundEffect> createState() => _SpecialEventBackgroundEffectState();
|
||||
}
|
||||
|
||||
class _SpecialEventBackgroundEffectState extends State<SpecialEventBackgroundEffect> with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _scaleAnimation;
|
||||
late Animation<double> _opacityAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 1000))..forward();
|
||||
_scaleAnimation = Tween<double>(begin: 0.5, end: 1.5).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic));
|
||||
_opacityAnimation = Tween<double>(begin: 0.9, end: 0.0).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn));
|
||||
}
|
||||
@override
|
||||
void didUpdateWidget(covariant SpecialEventBackgroundEffect oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.text != widget.text) { _controller.reset(); _controller.forward(); }
|
||||
}
|
||||
@override
|
||||
void dispose() { _controller.dispose(); super.dispose(); }
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
return Center(
|
||||
child: Transform.scale(
|
||||
scale: _scaleAnimation.value,
|
||||
child: Opacity(
|
||||
opacity: _opacityAnimation.value,
|
||||
child: ImageFiltered(
|
||||
imageFilter: ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0),
|
||||
child: Text(widget.text, style: TextStyle(fontSize: 250, fontWeight: FontWeight.w900, color: widget.color, height: 1.0)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
late AnimationController _controller; late Animation<double> _scaleAnimation; late Animation<double> _opacityAnimation;
|
||||
@override void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 1000))..forward(); _scaleAnimation = Tween<double>(begin: 0.5, end: 1.5).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic)); _opacityAnimation = Tween<double>(begin: 0.9, end: 0.0).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn)); }
|
||||
@override void didUpdateWidget(covariant SpecialEventBackgroundEffect oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.text != widget.text) { _controller.reset(); _controller.forward(); } }
|
||||
@override void dispose() { _controller.dispose(); super.dispose(); }
|
||||
@override Widget build(BuildContext context) { return AnimatedBuilder(animation: _controller, builder: (context, child) { return Center(child: Transform.scale(scale: _scaleAnimation.value, child: Opacity(opacity: _opacityAnimation.value, child: ImageFiltered(imageFilter: ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0), child: Text(widget.text, textAlign: TextAlign.center, style: _getTextStyle(widget.themeType, TextStyle(fontSize: 150, fontWeight: FontWeight.w900, color: widget.color, height: 1.0))))))); }); }
|
||||
}
|
||||
|
|
@ -1,5 +1,10 @@
|
|||
// ===========================================================================
|
||||
// FILE: lib/ui/game/score_board.dart
|
||||
// ===========================================================================
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
// Import separati e puliti
|
||||
import '../../logic/game_controller.dart';
|
||||
import '../../models/game_board.dart';
|
||||
import '../../core/theme_manager.dart';
|
||||
|
|
@ -27,7 +32,6 @@ class _ScoreBoardState extends State<ScoreBoard> {
|
|||
bool isRedTurn = controller.board.currentPlayer == Player.red;
|
||||
bool isMuted = AudioService.instance.isMuted;
|
||||
|
||||
// --- LOGICA PER I NOMI ---
|
||||
String nameRed = "ROSSO";
|
||||
String nameBlue = themeType == AppThemeType.cyberpunk ? "VERDE" : "BLU";
|
||||
|
||||
|
|
|
|||
|
|
@ -6,30 +6,35 @@ import 'dart:ui';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'dart:math' as math;
|
||||
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/app_colors.dart';
|
||||
import '../../models/game_board.dart';
|
||||
import '../game/game_screen.dart';
|
||||
import '../settings/settings_screen.dart';
|
||||
import '../../logic/game_controller.dart';
|
||||
import '../../services/storage_service.dart';
|
||||
import '../multiplayer/lobby_screen.dart';
|
||||
import 'history_screen.dart';
|
||||
|
||||
// --- HELPER PER IL FONT ---
|
||||
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;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// IL NOSTRO "PITTORE" DI SCARABOCCHI
|
||||
// Genera bordi irregolari, tratti doppi a penna e riempimenti sbavati.
|
||||
// ===========================================================================
|
||||
class _DoodleBackgroundPainter extends CustomPainter {
|
||||
final Color fillColor;
|
||||
final Color strokeColor;
|
||||
|
|
@ -101,8 +106,6 @@ class _DoodleBackgroundPainter extends CustomPainter {
|
|||
}
|
||||
}
|
||||
|
||||
// --- WIDGET PERSONALIZZATI ---
|
||||
|
||||
class _NeonShapeButton extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
|
|
@ -163,7 +166,6 @@ class _NeonShapeButton extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
// --- STILE STANDARD ---
|
||||
Color mainColor = isSpecial && !isLocked ? Colors.purpleAccent : theme.playerBlue;
|
||||
return GestureDetector(
|
||||
onTap: isLocked ? null : onTap,
|
||||
|
|
@ -375,8 +377,6 @@ class _NeonTimeSwitch extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
|
|
@ -386,6 +386,8 @@ class HomeScreen extends StatefulWidget {
|
|||
|
||||
class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
||||
|
||||
int _debugTapCount = 0;
|
||||
|
||||
@override
|
||||
void 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) {
|
||||
int localRadius = 4;
|
||||
ArenaShape localShape = ArenaShape.classic;
|
||||
bool localTimeMode = true;
|
||||
int cpuLevel = StorageService.instance.cpuLevel;
|
||||
bool isChaosUnlocked = cpuLevel >= 10;
|
||||
bool isChaosUnlocked = StorageService.instance.playerLevel >= 10;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
|
|
@ -681,8 +681,8 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
|||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [theme.background.withOpacity(0.95), theme.background.withOpacity(0.8)]),
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
border: themeType == AppThemeType.cyberpunk ? 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))],
|
||||
border: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? null : Border.all(color: Colors.white.withOpacity(0.15), width: 1.5),
|
||||
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(),
|
||||
|
|
@ -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() {
|
||||
showDialog(
|
||||
context: context,
|
||||
|
|
@ -773,34 +969,57 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
|||
final themeType = themeManager.currentThemeType;
|
||||
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
|
||||
? Transform.rotate(
|
||||
angle: -0.01,
|
||||
child: CustomPaint(
|
||||
painter: _DoodleBackgroundPainter(fillColor: Colors.yellow.shade100, strokeColor: inkColor, seed: 400),
|
||||
painter: _DoodleBackgroundPainter(fillColor: Colors.yellow.shade50, strokeColor: inkColor, seed: 400),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(25.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
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),
|
||||
// TESTO MODIFICATO PER EVIDENZIARE IL POSIZIONAMENTO LIBERO
|
||||
_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),
|
||||
_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),
|
||||
const SizedBox(height: 15),
|
||||
// TESTO MODIFICATO PER L'EFFETTO TOROIDALE
|
||||
_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),
|
||||
_TutorialStep(icon: Icons.lens_blur, text: "Ma presta attenzione! ogni quadrato nasconde un insidia o un regalo!", themeType: themeType, inkColor: inkColor, theme: theme),
|
||||
const SizedBox(height: 15),
|
||||
// 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(
|
||||
onTap: () => Navigator.pop(ctx),
|
||||
child: CustomPaint(
|
||||
painter: _DoodleBackgroundPainter(fillColor: Colors.red.shade200, strokeColor: inkColor, seed: 401),
|
||||
child: Container(
|
||||
height: 50, width: 150,
|
||||
alignment: Alignment.center,
|
||||
child: Text("HO CAPITO!", style: _getTextStyle(themeType, TextStyle(fontSize: 18, fontWeight: FontWeight.w900, color: inkColor))),
|
||||
_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),
|
||||
child: CustomPaint(
|
||||
painter: _DoodleBackgroundPainter(fillColor: Colors.red.shade200, strokeColor: inkColor, seed: 401),
|
||||
child: Container(
|
||||
height: 50, width: 150,
|
||||
alignment: Alignment.center,
|
||||
child: Text("HO CAPITO!", style: _getTextStyle(themeType, TextStyle(fontSize: 18, fontWeight: FontWeight.w900, color: inkColor))),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -814,29 +1033,51 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
|||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [theme.background.withOpacity(0.95), theme.background.withOpacity(0.8)]),
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
border: themeType == AppThemeType.cyberpunk ? 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))],
|
||||
border: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? null : Border.all(color: Colors.white.withOpacity(0.15), width: 1.5),
|
||||
boxShadow: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? [] : [BoxShadow(color: Colors.black.withOpacity(0.5), blurRadius: 20, offset: const Offset(4, 10))],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text("COME GIOCARE", style: _getTextStyle(themeType, TextStyle(fontSize: 24, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 2))),
|
||||
const SizedBox(height: 20),
|
||||
// TESTO MODIFICATO
|
||||
_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),
|
||||
// TESTO MODIFICATO PER L'EFFETTO TOROIDALE
|
||||
_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),
|
||||
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)),
|
||||
),
|
||||
)
|
||||
],
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Center(child: Text("COME GIOCARE", style: _getTextStyle(themeType, TextStyle(fontSize: 24, fontWeight: FontWeight.w900, color: theme.text, letterSpacing: 2)))),
|
||||
const SizedBox(height: 20),
|
||||
_TutorialStep(icon: Icons.grid_4x4, text: "Chiudi i 4 lati di un quadrato e conquisti 1 punto e avere una mossa extra!", themeType: themeType, inkColor: inkColor, theme: theme),
|
||||
const SizedBox(height: 15),
|
||||
_TutorialStep(icon: Icons.lens_blur, text: "Ma presta attenzione! ogni quadrato nasconde un insidia o un regalo!", themeType: themeType, inkColor: inkColor, theme: theme),
|
||||
const SizedBox(height: 15),
|
||||
const Divider(color: Colors.white24, thickness: 1.5),
|
||||
const SizedBox(height: 10),
|
||||
Center(child: Text("GLOSSARIO ARENA", style: _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(
|
||||
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)),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -873,148 +1114,192 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
|||
String playerName = StorageService.instance.playerName;
|
||||
if (playerName.isEmpty) playerName = "GUEST";
|
||||
|
||||
int level = StorageService.instance.playerLevel;
|
||||
int currentXP = StorageService.instance.totalXP;
|
||||
double xpProgress = (currentXP % 100) / 100.0;
|
||||
|
||||
Widget uiContent = SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// --- HEADER ---
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: _showNameDialog,
|
||||
child: Row(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: constraints.maxHeight,
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
themeType == AppThemeType.doodle
|
||||
? CustomPaint(
|
||||
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)),
|
||||
)
|
||||
: Container(
|
||||
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)),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: _showNameDialog,
|
||||
child: Row(
|
||||
children: [
|
||||
themeType == AppThemeType.doodle
|
||||
? CustomPaint(
|
||||
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)),
|
||||
)
|
||||
: 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))]),
|
||||
child: CircleAvatar(backgroundColor: theme.playerBlue.withOpacity(0.2), child: Icon(Icons.person, color: theme.playerBlue, size: 26)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
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))),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
GestureDetector(
|
||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const HistoryScreen())),
|
||||
child: themeType == AppThemeType.doodle
|
||||
? Transform.rotate(
|
||||
angle: 0.04,
|
||||
child: CustomPaint(
|
||||
painter: _DoodleBackgroundPainter(fillColor: Colors.yellow.shade100, strokeColor: inkColor, seed: 2),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.emoji_events, color: inkColor, size: 20), const SizedBox(width: 6),
|
||||
Text("$wins", style: _getTextStyle(themeType, TextStyle(color: inkColor, fontWeight: FontWeight.w900))), const SizedBox(width: 12),
|
||||
Icon(Icons.sentiment_very_dissatisfied, color: inkColor, size: 20), const SizedBox(width: 6),
|
||||
Text("$losses", style: _getTextStyle(themeType, TextStyle(color: inkColor, fontWeight: FontWeight.w900))),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [theme.text.withOpacity(0.15), theme.text.withOpacity(0.02)]),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.1), width: 1.5),
|
||||
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.3), offset: const Offset(2, 4), blurRadius: 8), BoxShadow(color: Colors.white.withOpacity(0.05), offset: const Offset(-1, -1), blurRadius: 2)],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.emoji_events, color: Colors.amber.shade600, size: 20), const SizedBox(width: 6),
|
||||
Text("$wins", style: _getTextStyle(themeType, const TextStyle(color: Colors.white, fontWeight: FontWeight.w900))), const SizedBox(width: 12),
|
||||
Icon(Icons.sentiment_very_dissatisfied, color: theme.playerRed.withOpacity(0.8), size: 20), const SizedBox(width: 6),
|
||||
Text("$losses", style: _getTextStyle(themeType, const TextStyle(color: Colors.white, fontWeight: FontWeight.w900))),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
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)]))),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
Center(
|
||||
child: Transform.rotate(
|
||||
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(
|
||||
"TETRAQ",
|
||||
style: _getTextStyle(themeType, TextStyle(
|
||||
fontSize: 65,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: themeType == AppThemeType.doodle ? inkColor : theme.text,
|
||||
letterSpacing: 10,
|
||||
shadows: themeType == AppThemeType.doodle || themeType == AppThemeType.arcade ? [] : [
|
||||
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),
|
||||
]
|
||||
))
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
// --- NUOVA LISTA BOTTONI CON CLASSIFICHE E SFIDE ---
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_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),
|
||||
const SizedBox(height: 12),
|
||||
_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),
|
||||
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)),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
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)),
|
||||
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)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
GestureDetector(
|
||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const HistoryScreen())),
|
||||
child: themeType == AppThemeType.doodle
|
||||
? Transform.rotate(
|
||||
angle: 0.04,
|
||||
child: CustomPaint(
|
||||
painter: _DoodleBackgroundPainter(fillColor: Colors.yellow.shade100, strokeColor: inkColor, seed: 2),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.emoji_events, color: inkColor, size: 20), const SizedBox(width: 6),
|
||||
Text("$wins", style: _getTextStyle(themeType, TextStyle(color: inkColor, fontWeight: FontWeight.w900))), const SizedBox(width: 12),
|
||||
Icon(Icons.sentiment_very_dissatisfied, color: inkColor, size: 20), const SizedBox(width: 6),
|
||||
Text("$losses", style: _getTextStyle(themeType, TextStyle(color: inkColor, fontWeight: FontWeight.w900))),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [theme.text.withOpacity(0.15), theme.text.withOpacity(0.02)]),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.1), width: 1.5),
|
||||
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.3), offset: const Offset(2, 4), blurRadius: 8), BoxShadow(color: Colors.white.withOpacity(0.05), offset: const Offset(-1, -1), blurRadius: 2)],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.emoji_events, color: Colors.amber.shade600, size: 20), const SizedBox(width: 6),
|
||||
Text("$wins", style: _getTextStyle(themeType, const TextStyle(color: Colors.white, fontWeight: FontWeight.w900))), const SizedBox(width: 12),
|
||||
Icon(Icons.sentiment_very_dissatisfied, color: theme.playerRed.withOpacity(0.8), size: 20), const SizedBox(width: 6),
|
||||
Text("$losses", style: _getTextStyle(themeType, const TextStyle(color: Colors.white, fontWeight: FontWeight.w900))),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
Center(
|
||||
child: Transform.rotate(
|
||||
angle: themeType == AppThemeType.doodle ? -0.04 : 0,
|
||||
child: Text(
|
||||
"TETRAQ",
|
||||
style: _getTextStyle(themeType, TextStyle(
|
||||
fontSize: 65,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: themeType == AppThemeType.doodle ? inkColor : theme.text,
|
||||
letterSpacing: 10,
|
||||
shadows: themeType == AppThemeType.doodle ? [] : [
|
||||
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),
|
||||
]
|
||||
))
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
// --- MENU PRINCIPALE CON COLORI PASTELLO ---
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_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
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
_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
|
||||
),
|
||||
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
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -1047,32 +1332,31 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
|||
}
|
||||
}
|
||||
|
||||
// --- HELPER PER IL TESTO DEL TUTORIAL ---
|
||||
class _TutorialStep extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final Color? iconColor;
|
||||
final String text;
|
||||
final AppThemeType themeType;
|
||||
final Color inkColor;
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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),
|
||||
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 {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
|
|
@ -1082,8 +1366,9 @@ class _FeatureCard extends StatelessWidget {
|
|||
final AppThemeType themeType;
|
||||
final VoidCallback onTap;
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -1102,24 +1387,26 @@ class _FeatureCard extends StatelessWidget {
|
|||
seed: title.length * 5,
|
||||
),
|
||||
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(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, color: inkColor, size: 32),
|
||||
const SizedBox(width: 20),
|
||||
Icon(icon, color: inkColor, size: compact ? 24 : 32),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(title, style: _getTextStyle(themeType, TextStyle(color: inkColor, fontSize: 24, fontWeight: FontWeight.w900))),
|
||||
const SizedBox(height: 2),
|
||||
Text(subtitle, style: _getTextStyle(themeType, TextStyle(color: inkColor.withOpacity(0.8), fontSize: 14, fontWeight: FontWeight.bold))),
|
||||
Text(title, style: _getTextStyle(themeType, TextStyle(color: inkColor, fontSize: compact ? 16 : 24, fontWeight: FontWeight.w900))),
|
||||
if (!compact) ...[
|
||||
const SizedBox(height: 2),
|
||||
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(
|
||||
onTap: onTap,
|
||||
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(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: isFeatured
|
||||
? [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),
|
||||
border: Border.all(color: Colors.white.withOpacity(isFeatured ? 0.3 : 0.1), width: 1.5),
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
border: Border.all(color: color.withOpacity(isFeatured ? 0.5 : 0.2), width: 1.5),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.black.withOpacity(0.6), offset: const Offset(0, 8), blurRadius: 15),
|
||||
BoxShadow(color: 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(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
padding: EdgeInsets.all(compact ? 6 : 10),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
|
|
@ -1163,23 +1449,25 @@ class _FeatureCard extends StatelessWidget {
|
|||
border: Border.all(color: Colors.white.withOpacity(0.2)),
|
||||
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.2), blurRadius: 5, offset: const Offset(2, 4))]
|
||||
),
|
||||
child: Icon(icon, color: isFeatured ? Colors.white : color, size: 26),
|
||||
child: Icon(icon, color: isFeatured ? Colors.white : color, size: compact ? 20 : 26),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
SizedBox(width: compact ? 10 : 20),
|
||||
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
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)])),
|
||||
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(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),
|
||||
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(
|
||||
painter: _CyberBorderPainter(animationValue: _controller.value, color1: theme.playerBlue, color2: theme.playerRed),
|
||||
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),
|
||||
child: widget.child,
|
||||
),
|
||||
|
|
@ -1230,7 +1518,7 @@ class _CyberBorderPainter extends CustomPainter {
|
|||
@override
|
||||
void paint(Canvas canvas, Size 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()
|
||||
..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
|
||||
|
|
|
|||
|
|
@ -1,31 +1,31 @@
|
|||
// ===========================================================================
|
||||
// FILE: lib/ui/multiplayer/lobby_screen.dart
|
||||
// ===========================================================================
|
||||
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'dart:math' as math;
|
||||
import '../../logic/game_controller.dart';
|
||||
import '../../models/game_board.dart';
|
||||
import '../../core/theme_manager.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
import '../../models/game_board.dart';
|
||||
import '../../services/multiplayer_service.dart';
|
||||
import '../../services/storage_service.dart';
|
||||
import '../game/game_screen.dart';
|
||||
import '../../logic/game_controller.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
// --- HELPER PER IL FONT ---
|
||||
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;
|
||||
}
|
||||
|
||||
// --- WIDGET 3D/NEON RIUTILIZZABILI COMPATTATI ---
|
||||
|
||||
class _NeonShapeButton extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
|
|
@ -414,8 +414,6 @@ class _CyberBorderPainter extends CustomPainter {
|
|||
bool shouldRepaint(covariant _CyberBorderPainter oldDelegate) => oldDelegate.animationValue != animationValue;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class LobbyScreen extends StatefulWidget {
|
||||
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.cyberpunk) bgImage = 'assets/images/cyber_bg.jpg';
|
||||
|
||||
int cpuLevel = StorageService.instance.cpuLevel;
|
||||
bool isChaosUnlocked = cpuLevel >= 10;
|
||||
bool isChaosUnlocked = true;
|
||||
|
||||
Color doodlePenColor = const Color(0xFF00008B);
|
||||
|
||||
// --- PANNELLO HOST ---
|
||||
Widget hostPanel = Transform.rotate(
|
||||
angle: themeType == AppThemeType.doodle ? 0.01 : 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 15),
|
||||
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),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(themeType == AppThemeType.doodle ? 5 : 20),
|
||||
|
|
@ -630,7 +625,6 @@ class _LobbyScreenState extends State<LobbyScreen> {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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)))),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
|
|
@ -686,7 +680,6 @@ class _LobbyScreenState extends State<LobbyScreen> {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// --- INTESTAZIONE COMPATTATA ---
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
|
|
@ -712,8 +705,6 @@ class _LobbyScreenState extends State<LobbyScreen> {
|
|||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// --- SEZIONE HOST ---
|
||||
hostPanel,
|
||||
const SizedBox(height: 15),
|
||||
_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),
|
||||
|
||||
// --- SEZIONE JOIN ---
|
||||
Transform.rotate(
|
||||
angle: themeType == AppThemeType.doodle ? 0.02 : 0,
|
||||
child: Container(
|
||||
|
|
@ -747,7 +737,6 @@ class _LobbyScreenState extends State<LobbyScreen> {
|
|||
contentPadding: const EdgeInsets.symmetric(vertical: 12),
|
||||
hintText: "CODICE", hintStyle: _getTextStyle(themeType, TextStyle(color: theme.text.withOpacity(0.3), letterSpacing: 10, fontSize: 20)), counterText: "",
|
||||
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),
|
||||
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)),
|
||||
|
|
@ -770,7 +759,6 @@ class _LobbyScreenState extends State<LobbyScreen> {
|
|||
appBar: AppBar(backgroundColor: Colors.transparent, elevation: 0, iconTheme: IconThemeData(color: theme.text)),
|
||||
body: Stack(
|
||||
children: [
|
||||
// 1. Sfondo
|
||||
Container(
|
||||
decoration: bgImage != null ? BoxDecoration(image: DecorationImage(image: AssetImage(bgImage), fit: BoxFit.cover)) : null,
|
||||
child: bgImage != null && themeType == AppThemeType.cyberpunk
|
||||
|
|
@ -780,7 +768,6 @@ class _LobbyScreenState extends State<LobbyScreen> {
|
|||
: null,
|
||||
),
|
||||
|
||||
// 2. Wi-Fi
|
||||
if (themeType == AppThemeType.doodle)
|
||||
Positioned(
|
||||
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,
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -6,15 +6,23 @@ import 'package:flutter/material.dart';
|
|||
import 'package:provider/provider.dart';
|
||||
import '../../core/theme_manager.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
import '../../services/storage_service.dart';
|
||||
|
||||
class SettingsScreen extends StatelessWidget {
|
||||
class SettingsScreen extends StatefulWidget {
|
||||
const SettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SettingsScreen> createState() => _SettingsScreenState();
|
||||
}
|
||||
|
||||
class _SettingsScreenState extends State<SettingsScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final themeManager = context.watch<ThemeManager>();
|
||||
final theme = themeManager.currentColors;
|
||||
|
||||
int playerLevel = StorageService.instance.playerLevel;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: theme.background,
|
||||
appBar: AppBar(
|
||||
|
|
@ -31,20 +39,8 @@ class SettingsScreen extends StatelessWidget {
|
|||
subtitle: "Linee pulite, sfondo chiaro",
|
||||
type: AppThemeType.minimal,
|
||||
previewColors: AppColors.minimal,
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
_ThemeCard(
|
||||
title: "Quaderno (Doodle)",
|
||||
subtitle: "Sfondo a quadretti, tratto a penna",
|
||||
type: AppThemeType.doodle,
|
||||
previewColors: AppColors.doodle,
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
_ThemeCard(
|
||||
title: "Cyberpunk",
|
||||
subtitle: "Nero profondo, luci al neon",
|
||||
type: AppThemeType.cyberpunk,
|
||||
previewColors: AppColors.cyberpunk,
|
||||
requiredLevel: 1,
|
||||
currentLevel: playerLevel,
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
_ThemeCard(
|
||||
|
|
@ -52,6 +48,44 @@ class SettingsScreen extends StatelessWidget {
|
|||
subtitle: "Tavolo di legno, linee come fiammiferi",
|
||||
type: AppThemeType.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,47 +98,99 @@ class _ThemeCard extends StatelessWidget {
|
|||
final String subtitle;
|
||||
final AppThemeType type;
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final themeManager = context.watch<ThemeManager>();
|
||||
bool isSelected = themeManager.currentThemeType == type;
|
||||
bool isLocked = currentLevel < requiredLevel;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (isLocked) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text("Gioca per raggiungere il Liv. $requiredLevel e sbloccare questo tema!", style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.white)),
|
||||
backgroundColor: Colors.redAccent,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
duration: const Duration(seconds: 2),
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
themeManager.setTheme(type);
|
||||
// --- LA MODIFICA È QUI ---
|
||||
// Chiude la schermata e torna automaticamente alla Home!
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: previewColors.background,
|
||||
color: isLocked ? previewColors.background.withOpacity(0.4) : previewColors.background,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
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,
|
||||
),
|
||||
boxShadow: isSelected ? [BoxShadow(color: previewColors.playerBlue.withOpacity(0.4), blurRadius: 10, spreadRadius: 2)] : [],
|
||||
),
|
||||
child: Row(
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
Opacity(
|
||||
opacity: isLocked ? 0.25 : 1.0,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(title, style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: previewColors.text)),
|
||||
Text(subtitle, style: TextStyle(fontSize: 14, color: previewColors.text.withOpacity(0.7))),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: previewColors.text)),
|
||||
Text(subtitle, style: TextStyle(fontSize: 14, color: previewColors.text.withOpacity(0.7))),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(width: 20, height: 20, decoration: BoxDecoration(color: previewColors.playerRed, shape: BoxShape.circle)),
|
||||
const SizedBox(width: 10),
|
||||
Container(width: 20, height: 20, decoration: BoxDecoration(color: previewColors.playerBlue, shape: BoxShape.circle)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(width: 20, height: 20, decoration: BoxDecoration(color: previewColors.playerRed, shape: BoxShape.circle)),
|
||||
const SizedBox(width: 10),
|
||||
Container(width: 20, height: 20, decoration: BoxDecoration(color: previewColors.playerBlue, shape: BoxShape.circle)),
|
||||
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
BIN
macos/.DS_Store
vendored
Binary file not shown.
|
|
@ -598,7 +598,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
CURRENT_PROJECT_VERSION = 5;
|
||||
DEVELOPMENT_TEAM = 2BX6QRR7GG;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
|
||||
|
|
@ -618,7 +618,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
MARKETING_VERSION = 1.0.2;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
|
|
@ -748,7 +748,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
CURRENT_PROJECT_VERSION = 5;
|
||||
DEVELOPMENT_TEAM = 2BX6QRR7GG;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
|
||||
|
|
@ -768,7 +768,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
MARKETING_VERSION = 1.0.2;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
|
@ -786,7 +786,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
CURRENT_PROJECT_VERSION = 5;
|
||||
DEVELOPMENT_TEAM = 2BX6QRR7GG;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
|
||||
|
|
@ -806,7 +806,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
MARKETING_VERSION = 1.0.2;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -317,6 +317,14 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
name: tetraq
|
||||
description: A new Flutter project.
|
||||
publish_to: 'none'
|
||||
version: 1.0.0+4
|
||||
version: 1.0.2+4
|
||||
environment:
|
||||
sdk: ^3.10.7
|
||||
|
||||
|
|
@ -22,6 +22,7 @@ dependencies:
|
|||
share_plus: ^12.0.1
|
||||
app_links: ^7.0.0
|
||||
google_fonts: ^8.0.2
|
||||
font_awesome_flutter: ^10.12.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
|||
Loading…
Reference in a new issue