Auto-sync: 20260323_010000
This commit is contained in:
parent
c3390609c3
commit
6372935e1c
8 changed files with 717 additions and 277 deletions
|
|
@ -1,3 +1,3 @@
|
||||||
index.html,1773586765860,5737ce966fa8786becaf7f36a32992cf44102fb3a217c226c30576c993b33e63
|
index.html,1773586765860,5737ce966fa8786becaf7f36a32992cf44102fb3a217c226c30576c993b33e63
|
||||||
404.html,1773344753356,05cbc6f94d7a69ce2e29646eab13be2c884e61ba93e3094df5028866876d18b3
|
404.html,1773344753356,05cbc6f94d7a69ce2e29646eab13be2c884e61ba93e3094df5028866876d18b3
|
||||||
report.html,1773588057140,876c6baaa912c9abfb81ee70e9868d84476b1c204ebca4c99f458f300661a36b
|
report.html,1774223974711,2848745a7b4437e80aabba9bd776c1c7f90b1be21e67ddaf062c22a21ac99554
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,9 @@ class _ClosureResult {
|
||||||
final bool closesSomething;
|
final bool closesSomething;
|
||||||
final int netValue;
|
final int netValue;
|
||||||
final bool causesSwap;
|
final bool causesSwap;
|
||||||
_ClosureResult(this.closesSomething, this.netValue, this.causesSwap);
|
final bool isIceTrap; // NUOVO: Identifica le mosse suicide sul ghiaccio
|
||||||
|
|
||||||
|
_ClosureResult(this.closesSomething, this.netValue, this.causesSwap, this.isIceTrap);
|
||||||
}
|
}
|
||||||
|
|
||||||
class AIEngine {
|
class AIEngine {
|
||||||
|
|
@ -19,6 +21,7 @@ class AIEngine {
|
||||||
|
|
||||||
if (availableLines.isEmpty) return board.lines.first;
|
if (availableLines.isEmpty) return board.lines.first;
|
||||||
|
|
||||||
|
// Più il livello è alto, più l'IA è "intelligente"
|
||||||
double smartChance = 0.50 + ((level - 1) * 0.10);
|
double smartChance = 0.50 + ((level - 1) * 0.10);
|
||||||
if (smartChance > 1.0) smartChance = 1.0;
|
if (smartChance > 1.0) smartChance = 1.0;
|
||||||
|
|
||||||
|
|
@ -29,9 +32,16 @@ class AIEngine {
|
||||||
|
|
||||||
List<Line> goodClosingMoves = [];
|
List<Line> goodClosingMoves = [];
|
||||||
List<Line> badClosingMoves = [];
|
List<Line> badClosingMoves = [];
|
||||||
|
List<Line> iceTraps = []; // Le mosse da evitare assolutamente
|
||||||
|
|
||||||
for (var line in availableLines) {
|
for (var line in availableLines) {
|
||||||
var result = _checkClosure(board, line);
|
var result = _checkClosure(board, line);
|
||||||
|
|
||||||
|
if (result.isIceTrap) {
|
||||||
|
iceTraps.add(line); // Segna la linea come trappola e passa alla prossima
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (result.closesSomething) {
|
if (result.closesSomething) {
|
||||||
if (result.causesSwap) {
|
if (result.causesSwap) {
|
||||||
if (myScore < oppScore) {
|
if (myScore < oppScore) {
|
||||||
|
|
@ -56,10 +66,10 @@ class AIEngine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- REGOLA 2: Mosse Sicure ---
|
// --- REGOLA 2: Mosse Sicure (Ora include le esche del ghiaccio!) ---
|
||||||
List<Line> safeMoves = [];
|
List<Line> safeMoves = [];
|
||||||
for (var line in availableLines) {
|
for (var line in availableLines) {
|
||||||
if (!badClosingMoves.contains(line) && !goodClosingMoves.contains(line) && _isSafeMove(board, line, myScore, oppScore)) {
|
if (!badClosingMoves.contains(line) && !goodClosingMoves.contains(line) && !iceTraps.contains(line) && _isSafeMove(board, line, myScore, oppScore)) {
|
||||||
safeMoves.add(line);
|
safeMoves.add(line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -76,17 +86,19 @@ class AIEngine {
|
||||||
|
|
||||||
// --- REGOLA 3: Scegliere il male minore ---
|
// --- REGOLA 3: Scegliere il male minore ---
|
||||||
if (beSmart) {
|
if (beSmart) {
|
||||||
List<Line> riskyButNotTerrible = availableLines.where((l) => !badClosingMoves.contains(l) && !goodClosingMoves.contains(l)).toList();
|
List<Line> riskyButNotTerrible = availableLines.where((l) => !badClosingMoves.contains(l) && !goodClosingMoves.contains(l) && !iceTraps.contains(l)).toList();
|
||||||
if (riskyButNotTerrible.isNotEmpty) {
|
if (riskyButNotTerrible.isNotEmpty) {
|
||||||
return riskyButNotTerrible[random.nextInt(riskyButNotTerrible.length)];
|
return riskyButNotTerrible[random.nextInt(riskyButNotTerrible.length)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Line> nonTerribleMoves = availableLines.where((l) => !badClosingMoves.contains(l)).toList();
|
// Ultima spiaggia prima del disastro: qualsiasi cosa tranne bombe e trappole ghiacciate
|
||||||
|
List<Line> nonTerribleMoves = availableLines.where((l) => !badClosingMoves.contains(l) && !iceTraps.contains(l)).toList();
|
||||||
if (nonTerribleMoves.isNotEmpty) {
|
if (nonTerribleMoves.isNotEmpty) {
|
||||||
return nonTerribleMoves[random.nextInt(nonTerribleMoves.length)];
|
return nonTerribleMoves[random.nextInt(nonTerribleMoves.length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Se l'IA è messa all'angolo ed è costretta a suicidarsi... pesca a caso
|
||||||
return availableLines[random.nextInt(availableLines.length)];
|
return availableLines[random.nextInt(availableLines.length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,6 +106,7 @@ class AIEngine {
|
||||||
int netValue = 0;
|
int netValue = 0;
|
||||||
bool closesSomething = false;
|
bool closesSomething = false;
|
||||||
bool causesSwap = false;
|
bool causesSwap = false;
|
||||||
|
bool isIceTrap = false;
|
||||||
|
|
||||||
for (var box in board.boxes) {
|
for (var box in board.boxes) {
|
||||||
if (box.type == BoxType.invisible) continue;
|
if (box.type == BoxType.invisible) continue;
|
||||||
|
|
@ -106,17 +119,20 @@ class AIEngine {
|
||||||
if (box.right.owner != Player.none || box.right == line) linesCount++;
|
if (box.right.owner != Player.none || box.right == line) linesCount++;
|
||||||
|
|
||||||
if (linesCount == 4) {
|
if (linesCount == 4) {
|
||||||
|
if (box.type == BoxType.ice && !line.isIceCracked) {
|
||||||
|
// L'IA capisce che questa mossa non chiuderà il box, ma le farà perdere il turno.
|
||||||
|
isIceTrap = true;
|
||||||
|
} else {
|
||||||
closesSomething = true;
|
closesSomething = true;
|
||||||
|
|
||||||
// FIX: Togliamo la "vista a raggi X" all'Intelligenza Artificiale!
|
|
||||||
if (box.hiddenJokerOwner == board.currentPlayer) {
|
if (box.hiddenJokerOwner == board.currentPlayer) {
|
||||||
// L'IA conosce il suo Jolly, sa che vale +2 e cercherà di chiuderlo
|
|
||||||
netValue += 2;
|
netValue += 2;
|
||||||
} else {
|
} else {
|
||||||
// Se c'è il Jolly del giocatore, l'IA NON DEVE SAPERLO e valuta la casella normalmente!
|
|
||||||
if (box.type == BoxType.gold) netValue += 2;
|
if (box.type == BoxType.gold) netValue += 2;
|
||||||
else if (box.type == BoxType.bomb) netValue -= 1;
|
else if (box.type == BoxType.bomb) netValue -= 1;
|
||||||
else if (box.type == BoxType.swap) netValue += 0;
|
else if (box.type == BoxType.swap) netValue += 0;
|
||||||
|
else if (box.type == BoxType.ice) netValue += 0; // Rompere il ghiaccio vale 0 punti, ma fa rigiocare
|
||||||
|
else if (box.type == BoxType.multiplier) netValue += 1; // Leggero boost per dare priorità al x2
|
||||||
else netValue += 1;
|
else netValue += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,7 +140,8 @@ class AIEngine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return _ClosureResult(closesSomething, netValue, causesSwap);
|
}
|
||||||
|
return _ClosureResult(closesSomething, netValue, causesSwap, isIceTrap);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool _isSafeMove(GameBoard board, Line line, int myScore, int oppScore) {
|
static bool _isSafeMove(GameBoard board, Line line, int myScore, int oppScore) {
|
||||||
|
|
@ -139,21 +156,24 @@ class AIEngine {
|
||||||
if (box.right.owner != Player.none) currentLinesCount++;
|
if (box.right.owner != Player.none) currentLinesCount++;
|
||||||
|
|
||||||
if (currentLinesCount == 2) {
|
if (currentLinesCount == 2) {
|
||||||
|
// L'IA valuta cosa succede se lascia questa casella con 3 linee all'avversario
|
||||||
// Nuova logica di sicurezza: cosa succede se l'IA lascia questa scatola all'avversario?
|
|
||||||
int valueForOpponent = 0;
|
int valueForOpponent = 0;
|
||||||
if (box.hiddenJokerOwner == board.currentPlayer) {
|
|
||||||
// Se l'avversario la chiude, becca la trappola dell'IA (-1).
|
if (box.type == BoxType.ice) {
|
||||||
// Quindi PER L'IA È SICURISSIMO LASCIARE QUESTA CASELLA APERTA!
|
// Il ghiaccio è la trappola perfetta. Lasciarlo con 3 linee spingerà l'avversario a incrinarlo e a perdere il turno.
|
||||||
|
// L'IA valuta questa mossa come SICURISSIMA!
|
||||||
|
valueForOpponent = -5;
|
||||||
|
} else if (box.hiddenJokerOwner == board.currentPlayer) {
|
||||||
valueForOpponent = -1;
|
valueForOpponent = -1;
|
||||||
} else {
|
} else {
|
||||||
if (box.type == BoxType.gold) valueForOpponent = 2;
|
if (box.type == BoxType.gold) valueForOpponent = 2;
|
||||||
else if (box.type == BoxType.bomb) valueForOpponent = -1;
|
else if (box.type == BoxType.bomb) valueForOpponent = -1;
|
||||||
else if (box.type == BoxType.swap) valueForOpponent = 0;
|
else if (box.type == BoxType.swap) valueForOpponent = 0;
|
||||||
|
else if (box.type == BoxType.multiplier) valueForOpponent = 1;
|
||||||
else valueForOpponent = 1;
|
else valueForOpponent = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Se per l'avversario vale -1 (bomba normale o trappola dell'IA), lasciamogliela!
|
// Se per l'avversario è una trappola (bomba o ghiaccio), lascia pure la mossa libera
|
||||||
if (valueForOpponent < 0) {
|
if (valueForOpponent < 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -166,7 +186,7 @@ class AIEngine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false; // La mossa regalerebbe punti, quindi NON è sicura
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -553,6 +553,7 @@ class GameController extends ChangeNotifier {
|
||||||
|
|
||||||
if (isOnline && roomCode != null) {
|
if (isOnline && roomCode != null) {
|
||||||
Map<String, dynamic> moveData = {
|
Map<String, dynamic> moveData = {
|
||||||
|
'id': DateTime.now().millisecondsSinceEpoch,
|
||||||
'x1': line.p1.x, 'y1': line.p1.y, 'x2': line.p2.x, 'y2': line.p2.y,
|
'x1': line.p1.x, 'y1': line.p1.y, 'x2': line.p2.x, 'y2': line.p2.y,
|
||||||
'player': myPlayer == Player.red ? 'red' : 'blue'
|
'player': myPlayer == Player.red ? 'red' : 'blue'
|
||||||
};
|
};
|
||||||
|
|
@ -609,7 +610,7 @@ class GameController extends ChangeNotifier {
|
||||||
return unlocks;
|
return unlocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _saveMatchResult() {
|
Future<void> _saveMatchResult() async {
|
||||||
if (_hasSavedResult) return;
|
if (_hasSavedResult) return;
|
||||||
_hasSavedResult = true;
|
_hasSavedResult = true;
|
||||||
|
|
||||||
|
|
@ -626,9 +627,9 @@ class GameController extends ChangeNotifier {
|
||||||
String oppName = isHost ? onlineGuestName : onlineHostName;
|
String oppName = isHost ? onlineGuestName : onlineHostName;
|
||||||
int myScore = isHost ? board.scoreRed : board.scoreBlue;
|
int myScore = isHost ? board.scoreRed : board.scoreBlue;
|
||||||
int oppScore = isHost ? board.scoreBlue : board.scoreRed;
|
int oppScore = isHost ? board.scoreBlue : board.scoreRed;
|
||||||
StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: oppName, myScore: myScore, oppScore: oppScore, isOnline: true);
|
await StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: oppName, myScore: myScore, oppScore: oppScore, isOnline: true);
|
||||||
|
|
||||||
if (isWin) StorageService.instance.updateQuestProgress(0, 1);
|
if (isWin) await StorageService.instance.updateQuestProgress(0, 1);
|
||||||
|
|
||||||
} else if (isVsCPU) {
|
} else if (isVsCPU) {
|
||||||
int myScore = board.scoreRed; int cpuScore = board.scoreBlue;
|
int myScore = board.scoreRed; int cpuScore = board.scoreBlue;
|
||||||
|
|
@ -636,23 +637,23 @@ class GameController extends ChangeNotifier {
|
||||||
calculatedXP = isWin ? (10 + (cpuLevel * 2)) : (isDraw ? 5 : 2);
|
calculatedXP = isWin ? (10 + (cpuLevel * 2)) : (isDraw ? 5 : 2);
|
||||||
|
|
||||||
if (isWin) {
|
if (isWin) {
|
||||||
StorageService.instance.addWin();
|
await StorageService.instance.addWin();
|
||||||
StorageService.instance.updateQuestProgress(1, 1);
|
await StorageService.instance.updateQuestProgress(1, 1);
|
||||||
} else if (cpuScore > myScore) {
|
} else if (cpuScore > myScore) {
|
||||||
StorageService.instance.addLoss();
|
await StorageService.instance.addLoss();
|
||||||
}
|
}
|
||||||
StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "CPU (Liv. $cpuLevel)", myScore: myScore, oppScore: cpuScore, isOnline: false);
|
await StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "CPU (Liv. $cpuLevel)", myScore: myScore, oppScore: cpuScore, isOnline: false);
|
||||||
} else {
|
} else {
|
||||||
calculatedXP = 2;
|
calculatedXP = 2;
|
||||||
StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "Ospite (Locale)", myScore: board.scoreRed, oppScore: board.scoreBlue, isOnline: false);
|
await StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "Ospite (Locale)", myScore: board.scoreRed, oppScore: board.scoreBlue, isOnline: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (board.shape != ArenaShape.classic) {
|
if (board.shape != ArenaShape.classic) {
|
||||||
StorageService.instance.updateQuestProgress(2, 1);
|
await StorageService.instance.updateQuestProgress(2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
lastMatchXP = calculatedXP;
|
lastMatchXP = calculatedXP;
|
||||||
StorageService.instance.addXP(calculatedXP);
|
await StorageService.instance.addXP(calculatedXP);
|
||||||
|
|
||||||
int newLevel = StorageService.instance.playerLevel;
|
int newLevel = StorageService.instance.playerLevel;
|
||||||
if (newLevel > oldLevel) {
|
if (newLevel > oldLevel) {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ class AudioService extends ChangeNotifier {
|
||||||
AudioService._internal();
|
AudioService._internal();
|
||||||
|
|
||||||
bool isMuted = false;
|
bool isMuted = false;
|
||||||
final AudioPlayer _sfxPlayer = AudioPlayer();
|
// Abbiamo rimosso _sfxPlayer perché ora ogni suono crea un player usa e getta
|
||||||
final AudioPlayer _bgmPlayer = AudioPlayer();
|
final AudioPlayer _bgmPlayer = AudioPlayer();
|
||||||
|
|
||||||
AppThemeType _currentTheme = AppThemeType.doodle;
|
AppThemeType _currentTheme = AppThemeType.doodle;
|
||||||
|
|
@ -30,7 +30,6 @@ class AudioService extends ChangeNotifier {
|
||||||
|
|
||||||
if (isMuted) {
|
if (isMuted) {
|
||||||
await _bgmPlayer.pause();
|
await _bgmPlayer.pause();
|
||||||
await _sfxPlayer.stop();
|
|
||||||
} else {
|
} else {
|
||||||
playBgm(_currentTheme);
|
playBgm(_currentTheme);
|
||||||
}
|
}
|
||||||
|
|
@ -92,9 +91,11 @@ class AudioService extends ChangeNotifier {
|
||||||
|
|
||||||
if (file.isNotEmpty) {
|
if (file.isNotEmpty) {
|
||||||
try {
|
try {
|
||||||
await _sfxPlayer.play(AssetSource('audio/sfx/$file'), volume: 1.0);
|
final player = AudioPlayer(); // Player dedicato
|
||||||
|
await player.play(AssetSource('audio/sfx/$file'), volume: 1.0);
|
||||||
|
player.onPlayerComplete.listen((_) => player.dispose());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Errore SFX Linea: $file");
|
debugPrint("Errore SFX Linea: $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -115,9 +116,11 @@ class AudioService extends ChangeNotifier {
|
||||||
|
|
||||||
if (file.isNotEmpty) {
|
if (file.isNotEmpty) {
|
||||||
try {
|
try {
|
||||||
await _sfxPlayer.play(AssetSource('audio/sfx/$file'), volume: 1.0);
|
final player = AudioPlayer(); // Player dedicato
|
||||||
|
await player.play(AssetSource('audio/sfx/$file'), volume: 1.0);
|
||||||
|
player.onPlayerComplete.listen((_) => player.dispose());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Errore SFX Box: $file");
|
debugPrint("Errore SFX Box: $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -125,14 +128,18 @@ class AudioService extends ChangeNotifier {
|
||||||
void playBonusSfx() async {
|
void playBonusSfx() async {
|
||||||
if (isMuted) return;
|
if (isMuted) return;
|
||||||
try {
|
try {
|
||||||
await _sfxPlayer.play(AssetSource('audio/sfx/bonus.wav'), volume: 1.0);
|
final player = AudioPlayer(); // Player dedicato
|
||||||
|
await player.play(AssetSource('audio/sfx/bonus.wav'), volume: 1.0);
|
||||||
|
player.onPlayerComplete.listen((_) => player.dispose());
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
void playBombSfx() async {
|
void playBombSfx() async {
|
||||||
if (isMuted) return;
|
if (isMuted) return;
|
||||||
try {
|
try {
|
||||||
await _sfxPlayer.play(AssetSource('audio/sfx/bomb.wav'), volume: 1.0);
|
final player = AudioPlayer(); // Player dedicato
|
||||||
|
await player.play(AssetSource('audio/sfx/bomb.wav'), volume: 1.0);
|
||||||
|
player.onPlayerComplete.listen((_) => player.dispose());
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -66,10 +66,7 @@ class StorageService {
|
||||||
|
|
||||||
// --- SICUREZZA XP: Inviamo solo INCREMENTI al server ---
|
// --- SICUREZZA XP: Inviamo solo INCREMENTI al server ---
|
||||||
Future<void> addXP(int xp) async {
|
Future<void> addXP(int xp) async {
|
||||||
// Aggiorniamo il locale per la UI
|
|
||||||
await _prefs.setInt('totalXP', totalXP + xp);
|
await _prefs.setInt('totalXP', totalXP + xp);
|
||||||
|
|
||||||
// Aggiorniamo il server in modo sicuro tramite incremento relativo
|
|
||||||
final user = FirebaseAuth.instance.currentUser;
|
final user = FirebaseAuth.instance.currentUser;
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
await FirebaseFirestore.instance.collection('leaderboard').doc(user.uid).set({
|
await FirebaseFirestore.instance.collection('leaderboard').doc(user.uid).set({
|
||||||
|
|
@ -83,7 +80,6 @@ class StorageService {
|
||||||
|
|
||||||
int get wins => _prefs.getInt('wins') ?? 0;
|
int get wins => _prefs.getInt('wins') ?? 0;
|
||||||
|
|
||||||
// --- SICUREZZA WINS: Inviamo solo INCREMENTI al server ---
|
|
||||||
Future<void> addWin() async {
|
Future<void> addWin() async {
|
||||||
await _prefs.setInt('wins', wins + 1);
|
await _prefs.setInt('wins', wins + 1);
|
||||||
final user = FirebaseAuth.instance.currentUser;
|
final user = FirebaseAuth.instance.currentUser;
|
||||||
|
|
@ -96,7 +92,6 @@ class StorageService {
|
||||||
|
|
||||||
int get losses => _prefs.getInt('losses') ?? 0;
|
int get losses => _prefs.getInt('losses') ?? 0;
|
||||||
|
|
||||||
// --- SICUREZZA LOSSES: Inviamo solo INCREMENTI al server ---
|
|
||||||
Future<void> addLoss() async {
|
Future<void> addLoss() async {
|
||||||
await _prefs.setInt('losses', losses + 1);
|
await _prefs.setInt('losses', losses + 1);
|
||||||
final user = FirebaseAuth.instance.currentUser;
|
final user = FirebaseAuth.instance.currentUser;
|
||||||
|
|
@ -116,10 +111,12 @@ class StorageService {
|
||||||
syncLeaderboard();
|
syncLeaderboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ======================================================================
|
||||||
|
// FIX: ORA IL SYNC MANDA I DATI REALI ALLA DASHBOARD ADMIN!
|
||||||
|
// ======================================================================
|
||||||
Future<void> syncLeaderboard() async {
|
Future<void> syncLeaderboard() async {
|
||||||
try {
|
try {
|
||||||
final user = FirebaseAuth.instance.currentUser;
|
final user = FirebaseAuth.instance.currentUser;
|
||||||
|
|
||||||
if (user == null) return;
|
if (user == null) return;
|
||||||
|
|
||||||
String name = playerName;
|
String name = playerName;
|
||||||
|
|
@ -127,12 +124,53 @@ class StorageService {
|
||||||
|
|
||||||
String targetUid = user.uid;
|
String targetUid = user.uid;
|
||||||
|
|
||||||
// --- SICUREZZA: Non inviamo PIÙ i valori assoluti di xp, wins e losses! ---
|
// 1. Recupero Versione App e Modello Dispositivo
|
||||||
// Vengono aggiornati solo dagli incrementi protetti nelle funzioni sopra.
|
String appVer = "N/D";
|
||||||
|
String devModel = "N/D";
|
||||||
|
String osName = kIsWeb ? "Web" : Platform.operatingSystem;
|
||||||
|
|
||||||
|
try {
|
||||||
|
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||||
|
appVer = "${packageInfo.version}+${packageInfo.buildNumber}";
|
||||||
|
|
||||||
|
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||||||
|
if (!kIsWeb) {
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
|
||||||
|
devModel = "${androidInfo.brand} ${androidInfo.model}".toUpperCase();
|
||||||
|
osName = "Android";
|
||||||
|
} else if (Platform.isIOS) {
|
||||||
|
IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
|
||||||
|
devModel = iosInfo.utsname.machine; // Es. "iPhone13,2"
|
||||||
|
osName = "iOS";
|
||||||
|
} else if (Platform.isMacOS) {
|
||||||
|
MacOsDeviceInfo macInfo = await deviceInfo.macOsInfo;
|
||||||
|
devModel = macInfo.model; // Es. "MacBookPro17,1"
|
||||||
|
osName = "macOS";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Errore device info: $e");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Calcolo del Playtime effettivo (aggiornato ad ogni sync)
|
||||||
|
int sessionDurationSec = (DateTime.now().millisecondsSinceEpoch - _sessionStart) ~/ 1000;
|
||||||
|
int savedPlaytime = _prefs.getInt('total_playtime') ?? 0;
|
||||||
|
int totalPlaytime = savedPlaytime + sessionDurationSec;
|
||||||
|
await _prefs.setInt('total_playtime', totalPlaytime);
|
||||||
|
_sessionStart = DateTime.now().millisecondsSinceEpoch; // Resetta il timer di sessione
|
||||||
|
|
||||||
|
// 3. Creazione del payload per Firebase
|
||||||
Map<String, dynamic> dataToSave = {
|
Map<String, dynamic> dataToSave = {
|
||||||
'name': name,
|
'name': name,
|
||||||
'level': playerLevel,
|
'level': playerLevel,
|
||||||
'lastActive': FieldValue.serverTimestamp(),
|
'lastActive': FieldValue.serverTimestamp(),
|
||||||
|
'appVersion': appVer,
|
||||||
|
'deviceModel': devModel,
|
||||||
|
'platform': osName,
|
||||||
|
'ip': lastIp,
|
||||||
|
'city': lastCity,
|
||||||
|
'playtime': totalPlaytime,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (user.metadata.creationTime != null) {
|
if (user.metadata.creationTime != null) {
|
||||||
|
|
@ -184,15 +222,12 @@ class StorageService {
|
||||||
|
|
||||||
if (today != lastDate) {
|
if (today != lastDate) {
|
||||||
_prefs.setString('quest_date', today);
|
_prefs.setString('quest_date', today);
|
||||||
|
|
||||||
_prefs.setInt('q1_type', 0);
|
_prefs.setInt('q1_type', 0);
|
||||||
_prefs.setInt('q1_prog', 0);
|
_prefs.setInt('q1_prog', 0);
|
||||||
_prefs.setInt('q1_target', 3);
|
_prefs.setInt('q1_target', 3);
|
||||||
|
|
||||||
_prefs.setInt('q2_type', 1);
|
_prefs.setInt('q2_type', 1);
|
||||||
_prefs.setInt('q2_prog', 0);
|
_prefs.setInt('q2_prog', 0);
|
||||||
_prefs.setInt('q2_target', 2);
|
_prefs.setInt('q2_target', 2);
|
||||||
|
|
||||||
_prefs.setInt('q3_type', 2);
|
_prefs.setInt('q3_type', 2);
|
||||||
_prefs.setInt('q3_prog', 0);
|
_prefs.setInt('q3_prog', 0);
|
||||||
_prefs.setInt('q3_target', 2);
|
_prefs.setInt('q3_target', 2);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
import 'dart:io' show Platform;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
@ -13,6 +14,9 @@ import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
import 'package:firebase_auth/firebase_auth.dart';
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:app_links/app_links.dart';
|
import 'package:app_links/app_links.dart';
|
||||||
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
import 'package:upgrader/upgrader.dart';
|
||||||
|
import 'package:in_app_update/in_app_update.dart';
|
||||||
|
|
||||||
import '../../logic/game_controller.dart';
|
import '../../logic/game_controller.dart';
|
||||||
import '../../core/theme_manager.dart';
|
import '../../core/theme_manager.dart';
|
||||||
|
|
@ -58,6 +62,9 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
||||||
String? _myRoomCode;
|
String? _myRoomCode;
|
||||||
bool _roomStarted = false;
|
bool _roomStarted = false;
|
||||||
|
|
||||||
|
String _appVersion = '';
|
||||||
|
bool _updateAvailable = false;
|
||||||
|
|
||||||
final MultiplayerService _multiplayerService = MultiplayerService();
|
final MultiplayerService _multiplayerService = MultiplayerService();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -80,6 +87,59 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
||||||
_checkClipboardForInvite();
|
_checkClipboardForInvite();
|
||||||
_initDeepLinks();
|
_initDeepLinks();
|
||||||
_listenToFavoritesOnline();
|
_listenToFavoritesOnline();
|
||||||
|
_loadAppVersion();
|
||||||
|
_checkStoreForUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadAppVersion() async {
|
||||||
|
try {
|
||||||
|
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_appVersion = "v. ${packageInfo.version}";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Errore lettura versione: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _checkStoreForUpdate() async {
|
||||||
|
|
||||||
|
if (kIsWeb) return;
|
||||||
|
try {
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
final info = await InAppUpdate.checkForUpdate();
|
||||||
|
if (info.updateAvailability == UpdateAvailability.updateAvailable) {
|
||||||
|
if (mounted) setState(() => _updateAvailable = true);
|
||||||
|
}
|
||||||
|
} else if (Platform.isIOS || Platform.isMacOS) {
|
||||||
|
final upgrader = Upgrader();
|
||||||
|
await upgrader.initialize();
|
||||||
|
if (upgrader.isUpdateAvailable()) {
|
||||||
|
if (mounted) setState(() => _updateAvailable = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Errore controllo aggiornamenti: $e");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void _triggerUpdate() async {
|
||||||
|
if (kIsWeb) return;
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
try {
|
||||||
|
final info = await InAppUpdate.checkForUpdate();
|
||||||
|
if (info.updateAvailability == UpdateAvailability.updateAvailable) {
|
||||||
|
await InAppUpdate.performImmediateUpdate();
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
Upgrader().sendUserToAppStore();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Upgrader().sendUserToAppStore();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _checkThemeSafety() {
|
void _checkThemeSafety() {
|
||||||
|
|
@ -179,11 +239,13 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
||||||
|
|
||||||
if (diffInSeconds.abs() < 180) {
|
if (diffInSeconds.abs() < 180) {
|
||||||
String name = data['name'] ?? 'Un amico';
|
String name = data['name'] ?? 'Un amico';
|
||||||
|
if (ModalRoute.of(context)?.isCurrent == true) {
|
||||||
_showFavoriteOnlinePopup(name);
|
_showFavoriteOnlinePopup(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -245,10 +307,13 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ModalRoute.of(context)?.isCurrent == true) {
|
||||||
_showInvitePopup(from, code, inviteId);
|
_showInvitePopup(from, code, inviteId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -742,12 +807,120 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
|
||||||
),
|
),
|
||||||
|
|
||||||
Positioned.fill(child: uiContent),
|
Positioned.fill(child: uiContent),
|
||||||
|
|
||||||
|
// --- NUMERO DI VERSIONE APP E BADGE AGGIORNAMENTO ---
|
||||||
|
if (_appVersion.isNotEmpty)
|
||||||
|
Positioned(
|
||||||
|
bottom: MediaQuery.of(context).padding.bottom + 10,
|
||||||
|
left: 20,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Opacity(
|
||||||
|
opacity: 0.6,
|
||||||
|
child: Text(
|
||||||
|
_appVersion,
|
||||||
|
style: getSharedTextStyle(themeType, TextStyle(
|
||||||
|
color: themeType == AppThemeType.doodle ? inkColor : theme.text,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 1.0,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_updateAvailable) ...[
|
||||||
|
const SizedBox(width: 15),
|
||||||
|
_PulsingUpdateBadge(
|
||||||
|
themeType: themeType,
|
||||||
|
theme: theme,
|
||||||
|
onTap: _triggerUpdate,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- NUOVO WIDGET: BADGE AGGIORNAMENTO PULSANTE ---
|
||||||
|
class _PulsingUpdateBadge extends StatefulWidget {
|
||||||
|
final AppThemeType themeType;
|
||||||
|
final ThemeColors theme;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const _PulsingUpdateBadge({
|
||||||
|
required this.themeType,
|
||||||
|
required this.theme,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_PulsingUpdateBadge> createState() => _PulsingUpdateBadgeState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PulsingUpdateBadgeState extends State<_PulsingUpdateBadge> with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _controller;
|
||||||
|
late Animation<double> _scaleAnimation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 800))..repeat(reverse: true);
|
||||||
|
_scaleAnimation = Tween<double>(begin: 0.95, end: 1.05).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Color badgeColor = widget.themeType == AppThemeType.doodle ? Colors.red.shade700 : widget.theme.playerRed;
|
||||||
|
Color textColor = widget.themeType == AppThemeType.doodle ? Colors.white : widget.theme.playerRed;
|
||||||
|
Color bgColor = widget.themeType == AppThemeType.doodle ? badgeColor : badgeColor.withOpacity(0.15);
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: widget.onTap,
|
||||||
|
child: ScaleTransition(
|
||||||
|
scale: _scaleAnimation,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: bgColor,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: badgeColor, width: 1.5),
|
||||||
|
boxShadow: widget.themeType == AppThemeType.doodle
|
||||||
|
? [const BoxShadow(color: Colors.black26, offset: Offset(2, 2))]
|
||||||
|
: [BoxShadow(color: badgeColor.withOpacity(0.4), blurRadius: 8)],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.system_update_alt, color: textColor, size: 14),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
"AGGIORNAMENTO DISPONIBILE",
|
||||||
|
style: getSharedTextStyle(widget.themeType, TextStyle(
|
||||||
|
color: textColor,
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ----------------------------------------------------
|
||||||
|
|
||||||
class FullScreenGridPainter extends CustomPainter {
|
class FullScreenGridPainter extends CustomPainter {
|
||||||
final Color gridColor;
|
final Color gridColor;
|
||||||
FullScreenGridPainter(this.gridColor);
|
FullScreenGridPainter(this.gridColor);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import '../logic/game_controller.dart';
|
||||||
import '../core/theme_manager.dart';
|
import '../core/theme_manager.dart';
|
||||||
import '../core/app_colors.dart';
|
import '../core/app_colors.dart';
|
||||||
import '../services/storage_service.dart';
|
import '../services/storage_service.dart';
|
||||||
|
import 'painters.dart';
|
||||||
|
|
||||||
class GameOverDialog extends StatelessWidget {
|
class GameOverDialog extends StatelessWidget {
|
||||||
const GameOverDialog({super.key});
|
const GameOverDialog({super.key});
|
||||||
|
|
@ -18,6 +19,7 @@ class GameOverDialog extends StatelessWidget {
|
||||||
final themeManager = context.read<ThemeManager>();
|
final themeManager = context.read<ThemeManager>();
|
||||||
final theme = themeManager.currentColors;
|
final theme = themeManager.currentColors;
|
||||||
final themeType = themeManager.currentThemeType;
|
final themeType = themeManager.currentThemeType;
|
||||||
|
Color inkColor = const Color(0xFF111122);
|
||||||
|
|
||||||
int red = game.board.scoreRed;
|
int red = game.board.scoreRed;
|
||||||
int blue = game.board.scoreBlue;
|
int blue = game.board.scoreBlue;
|
||||||
|
|
@ -51,172 +53,269 @@ class GameOverDialog extends StatelessWidget {
|
||||||
winnerColor = theme.playerBlue;
|
winnerColor = theme.playerBlue;
|
||||||
} else {
|
} else {
|
||||||
winnerText = "PAREGGIO!";
|
winnerText = "PAREGGIO!";
|
||||||
winnerColor = theme.text;
|
winnerColor = themeType == AppThemeType.doodle ? inkColor : theme.text;
|
||||||
}
|
}
|
||||||
|
|
||||||
return AlertDialog(
|
Widget dialogContent = Column(
|
||||||
backgroundColor: theme.background,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
side: BorderSide(color: winnerColor.withOpacity(0.5), width: 2),
|
|
||||||
),
|
|
||||||
title: Text("FINE PARTITA", textAlign: TextAlign.center, style: TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 22)),
|
|
||||||
content: SingleChildScrollView(
|
|
||||||
physics: const BouncingScrollPhysics(),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(winnerText, textAlign: TextAlign.center, style: TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: winnerColor)),
|
Text(winnerText, textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: winnerColor))),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.text.withOpacity(0.05),
|
color: themeType == AppThemeType.doodle ? Colors.transparent : theme.text.withOpacity(0.05),
|
||||||
borderRadius: BorderRadius.circular(15),
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
border: themeType == AppThemeType.doodle ? Border.all(color: inkColor.withOpacity(0.3), width: 1.5) : null,
|
||||||
),
|
),
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text("$nameRed: $red", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerRed)),
|
Text("$nameRed: $red", style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerRed))),
|
||||||
Text(" - ", style: TextStyle(fontSize: 18, color: theme.text)),
|
Text(" - ", style: getSharedTextStyle(themeType, TextStyle(fontSize: 18, color: themeType == AppThemeType.doodle ? inkColor : theme.text))),
|
||||||
Text("$nameBlue: $blue", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerBlue)),
|
Text("$nameBlue: $blue", style: getSharedTextStyle(themeType, TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerBlue))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
if (game.lastMatchXP > 0) ...[
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.green.withOpacity(0.15),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(color: themeType == AppThemeType.doodle ? Colors.green.shade700 : Colors.greenAccent, width: 1.5),
|
||||||
|
boxShadow: (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.music) ? [const BoxShadow(color: Colors.greenAccent, blurRadius: 10, spreadRadius: -5)] : [],
|
||||||
|
),
|
||||||
|
child: Text("+ ${game.lastMatchXP} XP", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.green.shade700 : Colors.greenAccent, fontWeight: FontWeight.w900, fontSize: 16, letterSpacing: 1.5))),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
if (game.isVsCPU) ...[
|
if (game.isVsCPU) ...[
|
||||||
const SizedBox(height: 15),
|
const SizedBox(height: 15),
|
||||||
Text("Difficoltà CPU: Livello ${game.cpuLevel}", style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: theme.text.withOpacity(0.7))),
|
Text("Difficoltà CPU: Livello ${game.cpuLevel}", style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.7) : theme.text.withOpacity(0.7)))),
|
||||||
|
],
|
||||||
|
|
||||||
|
if (game.isOnline) ...[
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
if (game.rematchRequested && !game.opponentWantsRematch)
|
||||||
|
Text("In attesa di $nameBlue...", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.orange.shade700 : Colors.amber, fontWeight: FontWeight.bold, fontStyle: FontStyle.italic))),
|
||||||
|
if (game.opponentWantsRematch && !game.rematchRequested)
|
||||||
|
Text("$nameBlue vuole la rivincita!", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.green.shade700 : Colors.greenAccent, fontWeight: FontWeight.bold))),
|
||||||
|
if (game.rematchRequested && game.opponentWantsRematch)
|
||||||
|
Text("Avvio nuova partita...", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.green.shade800 : Colors.green, fontWeight: FontWeight.bold))),
|
||||||
],
|
],
|
||||||
|
|
||||||
// --- SEZIONE LEVEL UP E ROADMAP DINAMICA ---
|
// --- SEZIONE LEVEL UP E ROADMAP DINAMICA ---
|
||||||
if (game.hasLeveledUp && game.unlockedRewards.isNotEmpty) ...[
|
if (game.hasLeveledUp && game.unlockedRewards.isNotEmpty) ...[
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
const Divider(),
|
Divider(color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.3) : theme.text.withOpacity(0.2)),
|
||||||
const SizedBox(height: 15),
|
const SizedBox(height: 15),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 20),
|
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.amber.withOpacity(0.2),
|
color: themeType == AppThemeType.doodle ? Colors.amber.withOpacity(0.1) : Colors.amber.withOpacity(0.2),
|
||||||
borderRadius: BorderRadius.circular(30),
|
borderRadius: BorderRadius.circular(30),
|
||||||
border: Border.all(color: Colors.amber, width: 2)
|
border: Border.all(color: themeType == AppThemeType.doodle ? Colors.amber.shade700 : Colors.amber, width: 2)
|
||||||
),
|
),
|
||||||
child: Text("🎉 LIVELLO ${game.newlyReachedLevel}! 🎉", style: const TextStyle(color: Colors.amber, fontWeight: FontWeight.w900, fontSize: 18)),
|
child: Text("🎉 LIVELLO ${game.newlyReachedLevel}! 🎉", style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? Colors.amber.shade700 : Colors.amber, fontWeight: FontWeight.w900, fontSize: 18))),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 15),
|
const SizedBox(height: 15),
|
||||||
|
|
||||||
...game.unlockedRewards.map((reward) => Container(
|
...game.unlockedRewards.map((reward) {
|
||||||
|
Color rewardColor = themeType == AppThemeType.doodle ? (reward['color'] as Color).withOpacity(0.8) : reward['color'];
|
||||||
|
return Container(
|
||||||
margin: const EdgeInsets.only(bottom: 10),
|
margin: const EdgeInsets.only(bottom: 10),
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: (reward['color'] as Color).withOpacity(0.1),
|
color: rewardColor.withOpacity(0.1),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(color: (reward['color'] as Color).withOpacity(0.5), width: 1.5),
|
border: Border.all(color: rewardColor.withOpacity(0.5), width: 1.5),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: (reward['color'] as Color).withOpacity(0.2),
|
color: rewardColor.withOpacity(0.2),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: Icon(reward['icon'], color: reward['color'], size: 28),
|
child: Icon(reward['icon'], color: rewardColor, size: 28),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 15),
|
const SizedBox(width: 15),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(reward['title'], style: TextStyle(color: reward['color'], fontWeight: FontWeight.w900, fontSize: 16)),
|
Text(reward['title'], style: getSharedTextStyle(themeType, TextStyle(color: rewardColor, fontWeight: FontWeight.w900, fontSize: 16))),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(reward['desc'], style: TextStyle(color: theme.text.withOpacity(0.9), fontSize: 12, height: 1.3)),
|
Text(reward['desc'], style: getSharedTextStyle(themeType, TextStyle(color: themeType == AppThemeType.doodle ? inkColor.withOpacity(0.9) : theme.text.withOpacity(0.9), fontSize: 12, height: 1.3))),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
),
|
||||||
)),
|
);
|
||||||
]
|
}),
|
||||||
// ---------------------------------------------
|
|
||||||
],
|
],
|
||||||
),
|
|
||||||
),
|
const SizedBox(height: 30),
|
||||||
actionsPadding: const EdgeInsets.only(left: 20, right: 20, bottom: 20, top: 10),
|
|
||||||
actionsAlignment: MainAxisAlignment.center,
|
// --- BOTTONI AZIONE ---
|
||||||
actions: [
|
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
if (playerBeatCPU)
|
if (playerBeatCPU)
|
||||||
ElevatedButton(
|
_buildPrimaryButton(
|
||||||
style: ElevatedButton.styleFrom(
|
"PROSSIMO LIVELLO ➔",
|
||||||
backgroundColor: winnerColor,
|
winnerColor,
|
||||||
foregroundColor: Colors.white,
|
themeType,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
inkColor,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
() {
|
||||||
elevation: 5,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
game.increaseLevelAndRestart();
|
game.increaseLevelAndRestart();
|
||||||
},
|
},
|
||||||
child: const Text("PROSSIMO LIVELLO ➔", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
|
||||||
)
|
)
|
||||||
else if (game.isOnline)
|
else if (game.isOnline)
|
||||||
ElevatedButton(
|
_buildPrimaryButton(
|
||||||
style: ElevatedButton.styleFrom(
|
game.opponentWantsRematch ? "ACCETTA RIVINCITA" : "CHIEDI RIVINCITA",
|
||||||
backgroundColor: winnerColor == theme.text ? theme.playerBlue : winnerColor,
|
game.rematchRequested ? Colors.grey : (winnerColor == (themeType == AppThemeType.doodle ? inkColor : theme.text) ? theme.playerBlue : winnerColor),
|
||||||
foregroundColor: Colors.white,
|
themeType,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
inkColor,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
game.rematchRequested ? () {} : () => game.requestRematch(),
|
||||||
elevation: 5,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
if (game.board.isGameOver) {
|
|
||||||
game.requestRematch();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: const Text("RIGIOCA ONLINE", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 1.5)),
|
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
ElevatedButton(
|
_buildPrimaryButton(
|
||||||
style: ElevatedButton.styleFrom(
|
"RIGIOCA",
|
||||||
backgroundColor: winnerColor == theme.text ? theme.playerBlue : winnerColor,
|
winnerColor == (themeType == AppThemeType.doodle ? inkColor : theme.text) ? theme.playerBlue : winnerColor,
|
||||||
foregroundColor: Colors.white,
|
themeType,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
inkColor,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
() {
|
||||||
elevation: 5,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
game.startNewGame(game.board.radius, vsCPU: game.isVsCPU);
|
game.startNewGame(game.board.radius, vsCPU: game.isVsCPU);
|
||||||
},
|
},
|
||||||
child: const Text("RIGIOCA", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 2)),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
OutlinedButton(
|
_buildSecondaryButton(
|
||||||
style: OutlinedButton.styleFrom(
|
"TORNA AL MENU",
|
||||||
foregroundColor: theme.text,
|
themeType,
|
||||||
side: BorderSide(color: theme.text.withOpacity(0.3), width: 2),
|
inkColor,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
theme,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
() {
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
if (game.isOnline) {
|
if (game.isOnline) {
|
||||||
game.disconnectOnlineGame();
|
game.disconnectOnlineGame();
|
||||||
}
|
}
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
child: Text("TORNA AL MENU", style: TextStyle(fontWeight: FontWeight.bold, color: theme.text, fontSize: 14, letterSpacing: 1.5)),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (themeType == AppThemeType.doodle) {
|
||||||
|
dialogContent = Transform.rotate(
|
||||||
|
angle: 0.015,
|
||||||
|
child: CustomPaint(
|
||||||
|
painter: DoodleBackgroundPainter(fillColor: Colors.white.withOpacity(0.95), strokeColor: inkColor, seed: 500),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(25.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text("FINE PARTITA", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 22, fontWeight: FontWeight.w900, color: inkColor, letterSpacing: 2))),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
dialogContent,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
dialogContent = Container(
|
||||||
|
padding: const EdgeInsets.all(25.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.background,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(color: winnerColor.withOpacity(0.5), width: 2),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text("FINE PARTITA", textAlign: TextAlign.center, style: getSharedTextStyle(themeType, TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: theme.text))),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
dialogContent,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Dialog(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
|
||||||
|
child: dialogContent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPrimaryButton(String label, Color color, AppThemeType themeType, Color inkColor, VoidCallback onTap) {
|
||||||
|
if (themeType == AppThemeType.doodle) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: CustomPaint(
|
||||||
|
painter: DoodleBackgroundPainter(fillColor: color, strokeColor: inkColor, seed: label.length * 7),
|
||||||
|
child: Container(
|
||||||
|
height: 55,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(label, style: getSharedTextStyle(themeType, const TextStyle(fontSize: 16, fontWeight: FontWeight.w900, color: Colors.white, letterSpacing: 1.5))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: color,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
||||||
|
elevation: 5,
|
||||||
|
),
|
||||||
|
onPressed: onTap,
|
||||||
|
child: Text(label, style: getSharedTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 1.5))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSecondaryButton(String label, AppThemeType themeType, Color inkColor, ThemeColors theme, VoidCallback onTap) {
|
||||||
|
if (themeType == AppThemeType.doodle) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: CustomPaint(
|
||||||
|
painter: DoodleBackgroundPainter(fillColor: Colors.transparent, strokeColor: inkColor.withOpacity(0.5), seed: label.length * 3),
|
||||||
|
child: Container(
|
||||||
|
height: 55,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(label, style: getSharedTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: inkColor, letterSpacing: 1.5))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return OutlinedButton(
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: theme.text,
|
||||||
|
side: BorderSide(color: theme.text.withOpacity(0.3), width: 2),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
||||||
|
),
|
||||||
|
onPressed: onTap,
|
||||||
|
child: Text(label, style: getSharedTextStyle(themeType, TextStyle(fontWeight: FontWeight.bold, color: theme.text, fontSize: 14, letterSpacing: 1.5))),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3,65 +3,140 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Report Statistiche TetraQ</title>
|
<title>Report Giocatori - TetraQ</title>
|
||||||
|
|
||||||
<script defer src="/__/firebase/10.8.0/firebase-app-compat.js"></script>
|
<script defer src="/__/firebase/10.8.0/firebase-app-compat.js"></script>
|
||||||
<script defer src="/__/firebase/10.8.0/firebase-firestore-compat.js"></script>
|
<script defer src="/__/firebase/10.8.0/firebase-firestore-compat.js"></script>
|
||||||
<script defer src="/__/firebase/init.js"></script>
|
<script defer src="/__/firebase/init.js"></script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f4f7f6; color: #333; padding: 20px; margin: 0; box-sizing: border-box;}
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
color: #2c3e50;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
/* STILI LOGIN */
|
/* --- STILI LOGIN --- */
|
||||||
#login-view { background-color: #34495e; position: fixed; top: 0; left: 0; width: 100%; height: 100vh; display: flex; justify-content: center; align-items: center; z-index: 1000; padding: 20px; box-sizing: border-box; }
|
#login-view {
|
||||||
.login-box { background: white; padding: 40px; border-radius: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.2); text-align: center; width: 100%; max-width: 350px; box-sizing: border-box; }
|
background-color: #34495e;
|
||||||
.login-box h2 { color: #2c3e50; margin-top: 0; }
|
position: fixed;
|
||||||
.login-box input[type="text"], .login-box input[type="password"] { width: 100%; padding: 12px; margin: 10px 0 20px 0; border: 1px solid #bdc3c7; border-radius: 5px; box-sizing: border-box; font-size: 16px; }
|
top: 0; left: 0; width: 100%; height: 100vh;
|
||||||
.login-box button { width: 100%; background-color: #3498db; color: white; border: none; padding: 12px; font-size: 16px; border-radius: 5px; cursor: pointer; font-weight: bold; transition: 0.2s; }
|
display: flex; justify-content: center; align-items: center;
|
||||||
.login-box button:hover { background-color: #2980b9; }
|
z-index: 1000; padding: 20px; box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.login-box {
|
||||||
|
background: white; padding: 40px; border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 25px rgba(0,0,0,0.1); text-align: center;
|
||||||
|
width: 100%; max-width: 350px; box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.login-box h2 { color: #2c3e50; margin-top: 0; font-weight: 800; letter-spacing: 1px;}
|
||||||
|
.login-box input {
|
||||||
|
width: 100%; padding: 12px; margin: 10px 0 20px 0;
|
||||||
|
border: 1px solid #e1e5eb; border-radius: 8px; box-sizing: border-box;
|
||||||
|
font-size: 16px; outline: none; transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
.login-box input:focus { border-color: #3498db; }
|
||||||
|
.login-box button {
|
||||||
|
width: 100%; background-color: #e74c3c; color: white;
|
||||||
|
border: none; padding: 12px; font-size: 16px; border-radius: 8px;
|
||||||
|
cursor: pointer; font-weight: bold; transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
.login-box button:hover { background-color: #c0392b; }
|
||||||
.error { color: #e74c3c; font-weight: bold; font-size: 14px; margin-top: -5px; margin-bottom: 15px; }
|
.error { color: #e74c3c; font-weight: bold; font-size: 14px; margin-top: -5px; margin-bottom: 15px; }
|
||||||
|
|
||||||
/* STILI DASHBOARD */
|
/* --- STILI DASHBOARD --- */
|
||||||
#dashboard-view { display: none; }
|
#dashboard-view { display: none; }
|
||||||
.container { max-width: 1200px; margin: 0 auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); box-sizing: border-box;}
|
.container {
|
||||||
|
max-width: 1200px; margin: 0 auto; background: white;
|
||||||
|
padding: 30px; border-radius: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.05);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
.header-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
.header-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||||
h1 { color: #2c3e50; margin: 0; font-size: 24px;}
|
h1 { color: #2c3e50; margin: 0; font-size: 26px; font-weight: 800;}
|
||||||
.btn-logout { background-color: #e74c3c; color: white; padding: 8px 15px; border-radius: 5px; text-decoration: none; font-weight: bold; font-size: 14px; white-space: nowrap; border: none; cursor: pointer;}
|
.btn-logout {
|
||||||
.btn-logout:hover { background-color: #c0392b; }
|
background-color: transparent; color: #95a5a6; padding: 8px 15px;
|
||||||
|
border-radius: 5px; font-weight: bold; font-size: 14px;
|
||||||
|
border: 1px solid #ecf0f1; cursor: pointer; transition: 0.2s;
|
||||||
|
}
|
||||||
|
.btn-logout:hover { background-color: #f9f9f9; color: #e74c3c; border-color: #e74c3c; }
|
||||||
|
|
||||||
.filter-section { background: #eef2f5; padding: 20px; border-radius: 8px; margin-bottom: 30px; display: flex; flex-wrap: wrap; gap: 15px; align-items: flex-end; }
|
/* --- FILTRI --- */
|
||||||
|
.filter-section {
|
||||||
|
background: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 30px;
|
||||||
|
display: flex; flex-wrap: wrap; gap: 15px; align-items: flex-end;
|
||||||
|
border: 1px solid #e1e5eb;
|
||||||
|
}
|
||||||
.form-group { display: flex; flex-direction: column; flex: 1; min-width: 150px; }
|
.form-group { display: flex; flex-direction: column; flex: 1; min-width: 150px; }
|
||||||
.form-group label { font-size: 13px; font-weight: bold; color: #34495e; margin-bottom: 5px; }
|
.form-group label { font-size: 12px; font-weight: bold; color: #7f8c8d; margin-bottom: 5px; text-transform: uppercase; letter-spacing: 0.5px;}
|
||||||
.form-group input, .form-group select { padding: 10px; border: 1px solid #bdc3c7; border-radius: 5px; font-size: 14px; outline: none; box-sizing: border-box; width: 100%;}
|
.form-group input, .form-group select {
|
||||||
|
padding: 10px; border: 1px solid #bdc3c7; border-radius: 6px;
|
||||||
|
font-size: 14px; outline: none; box-sizing: border-box; width: 100%;
|
||||||
|
}
|
||||||
.form-group input:focus, .form-group select:focus { border-color: #3498db; }
|
.form-group input:focus, .form-group select:focus { border-color: #3498db; }
|
||||||
.btn-filtra { background-color: #3498db; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; font-weight: bold; font-size: 14px; transition: 0.2s; height: 40px; }
|
.btn-filtra {
|
||||||
|
background-color: #3498db; color: white; padding: 10px 20px; border: none;
|
||||||
|
border-radius: 6px; cursor: pointer; font-weight: bold; font-size: 14px;
|
||||||
|
transition: 0.2s; height: 40px;
|
||||||
|
}
|
||||||
.btn-filtra:hover { background-color: #2980b9; }
|
.btn-filtra:hover { background-color: #2980b9; }
|
||||||
.btn-reset { background-color: #95a5a6; color: white; padding: 10px 15px; border: none; border-radius: 5px; cursor: pointer; text-decoration: none; font-size: 14px; height: 40px; line-height: 20px; box-sizing: border-box; display: inline-block; text-align: center;}
|
.btn-reset {
|
||||||
|
background-color: #95a5a6; color: white; padding: 0 15px; border: none;
|
||||||
|
border-radius: 6px; cursor: pointer; font-size: 14px; height: 40px;
|
||||||
|
line-height: 40px; text-align: center;
|
||||||
|
}
|
||||||
|
.btn-reset:hover { background-color: #7f8c8d; }
|
||||||
|
|
||||||
|
/* --- CARDS RIASSUNTIVE --- */
|
||||||
.dashboard { display: flex; justify-content: space-between; margin-bottom: 30px; gap: 20px; text-align: center; flex-wrap: wrap; }
|
.dashboard { display: flex; justify-content: space-between; margin-bottom: 30px; gap: 20px; text-align: center; flex-wrap: wrap; }
|
||||||
.card { flex: 1; min-width: 150px; background: #ecf0f1; padding: 20px; border-radius: 8px; border-left: 5px solid #3498db; box-sizing: border-box;}
|
.card {
|
||||||
|
flex: 1; min-width: 150px; background: white; padding: 20px;
|
||||||
|
border-radius: 8px; border-left: 5px solid #3498db; box-sizing: border-box;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.05); border: 1px solid #e1e5eb; border-left-width: 5px;
|
||||||
|
}
|
||||||
.card.ios { border-left-color: #e74c3c; }
|
.card.ios { border-left-color: #e74c3c; }
|
||||||
.card.android { border-left-color: #2ecc71; }
|
.card.android { border-left-color: #2ecc71; }
|
||||||
.card h3 { margin: 0 0 10px 0; font-size: 14px; color: #7f8c8d; text-transform: uppercase; }
|
.card h3 { margin: 0 0 10px 0; font-size: 12px; color: #7f8c8d; text-transform: uppercase; letter-spacing: 0.5px;}
|
||||||
.card p { margin: 0; font-size: 28px; font-weight: bold; color: #2c3e50; }
|
.card p { margin: 0; font-size: 28px; font-weight: 800; color: #2c3e50; }
|
||||||
|
|
||||||
table { width: 100%; border-collapse: collapse; margin-top: 20px; font-size: 14px; table-layout: fixed; }
|
/* --- TABELLA --- */
|
||||||
th, td { padding: 12px 15px; text-align: left; border-bottom: 1px solid #ddd; word-wrap: break-word;}
|
table { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 14px; table-layout: fixed; }
|
||||||
th { background-color: #34495e; color: white; }
|
th, td { padding: 16px 20px; text-align: left; vertical-align: top;}
|
||||||
tr:hover { background-color: #f1f1f1; }
|
th {
|
||||||
|
background-color: #34495e; color: white; font-weight: 700;
|
||||||
|
font-size: 13px; letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
th:first-child { border-top-left-radius: 6px; border-bottom-left-radius: 6px; }
|
||||||
|
th:last-child { border-top-right-radius: 6px; border-bottom-right-radius: 6px; }
|
||||||
|
td { border-bottom: 1px solid #ecf0f1; color: #2c3e50; }
|
||||||
|
tr:hover td { background-color: #fbfcfc; }
|
||||||
|
|
||||||
/* Larghezze specifiche per evitare colonne deformate */
|
th:nth-child(1) { width: 15%; } /* Accesso */
|
||||||
th:nth-child(1) { width: 15%; }
|
th:nth-child(2) { width: 25%; } /* Giocatore */
|
||||||
th:nth-child(2) { width: 25%; }
|
th:nth-child(3) { width: 15%; } /* Statistiche (Nuova) */
|
||||||
th:nth-child(3) { width: 15%; }
|
th:nth-child(4) { width: 25%; } /* Connessione */
|
||||||
th:nth-child(4) { width: 20%; }
|
th:nth-child(5) { width: 20%; } /* Dispositivo */
|
||||||
th:nth-child(5) { width: 25%; }
|
|
||||||
|
|
||||||
.badge { padding: 4px 8px; border-radius: 4px; color: white; font-weight: bold; font-size: 12px; }
|
/* Tipografia Specifica Tabella */
|
||||||
|
.player-name { font-weight: 800; color: #2c3e50; font-size: 15px; }
|
||||||
|
.player-level { font-weight: normal; color: #e74c3c; } /* Rosso come nell'app */
|
||||||
|
.sub-text { display: block; font-size: 12px; color: #95a5a6; margin-top: 4px; font-weight: 500; }
|
||||||
|
.data-text { font-weight: 500; color: #2c3e50; font-size: 14px; }
|
||||||
|
.stat-value { font-weight: bold; color: #3498db; }
|
||||||
|
|
||||||
|
/* Badge Sistema Operativo */
|
||||||
|
.badge {
|
||||||
|
padding: 4px 8px; border-radius: 4px; color: white;
|
||||||
|
font-weight: 800; font-size: 11px; text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px; display: inline-block; margin-bottom: 5px;
|
||||||
|
}
|
||||||
.badge-ios { background-color: #e74c3c; }
|
.badge-ios { background-color: #e74c3c; }
|
||||||
.badge-android { background-color: #2ecc71; }
|
.badge-android { background-color: #2ecc71; }
|
||||||
.badge-desktop { background-color: #95a5a6; }
|
.badge-desktop { background-color: #95a5a6; }
|
||||||
.empty { text-align: center; padding: 30px; color: #7f8c8d; font-style: italic; background: #f9f9f9; border-radius: 5px;}
|
|
||||||
|
.empty { text-align: center; padding: 40px; color: #95a5a6; font-weight: 500; }
|
||||||
|
|
||||||
/* MOBILE */
|
/* MOBILE */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
|
@ -89,9 +164,9 @@
|
||||||
<h2>Area Riservata</h2>
|
<h2>Area Riservata</h2>
|
||||||
<div id="login-error" class="error"></div>
|
<div id="login-error" class="error"></div>
|
||||||
<form id="login-form">
|
<form id="login-form">
|
||||||
<input type="text" id="username" name="username" placeholder="Nome Utente (io)" autocomplete="username" required>
|
<input type="text" id="username" autocomplete="username" required>
|
||||||
<input type="password" id="password" name="password" placeholder="Inserisci la password" autocomplete="current-password" required>
|
<input type="password" id="password" placeholder="Password" autocomplete="current-password" required>
|
||||||
<button type="submit">Accedi</button>
|
<button type="submit">Accedi al Database</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -99,17 +174,17 @@
|
||||||
<div id="dashboard-view">
|
<div id="dashboard-view">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header-top">
|
<div class="header-top">
|
||||||
<h1>📊 Report Statistiche TetraQ</h1>
|
<h1>Dettaglio Giocatori</h1>
|
||||||
<button class="btn-logout" onclick="logout()">Esci 🚪</button>
|
<button class="btn-logout" onclick="logout()">Disconnetti</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="filter-section" id="filter-form">
|
<form class="filter-section" id="filter-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="data_da">Data Da:</label>
|
<label for="data_da">Ultimo Accesso Da:</label>
|
||||||
<input type="date" id="data_da">
|
<input type="date" id="data_da">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="data_a">Data A:</label>
|
<label for="data_a">Ultimo Accesso A:</label>
|
||||||
<input type="date" id="data_a">
|
<input type="date" id="data_a">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|
@ -122,16 +197,16 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="loc">Località (es. Roma):</label>
|
<label for="loc">Cerca Giocatore/Città:</label>
|
||||||
<input type="text" id="loc" placeholder="Cerca città...">
|
<input type="text" id="loc" placeholder="Digita...">
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn-filtra">🔍 Applica Filtri</button>
|
<button type="submit" class="btn-filtra">🔍 Applica</button>
|
||||||
<button type="button" class="btn-reset" onclick="resetFilters()">✖ Reset</button>
|
<button type="button" class="btn-reset" onclick="resetFilters()">Reset</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="dashboard">
|
<div class="dashboard">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>Giocatori Trovati</h3>
|
<h3>Totale Giocatori</h3>
|
||||||
<p id="totale-text">0</p>
|
<p id="totale-text">0</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card ios">
|
<div class="card ios">
|
||||||
|
|
@ -143,35 +218,54 @@
|
||||||
<p id="android-text">0</p>
|
<p id="android-text">0</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>Desktop / Altro</h3>
|
<h3>Desktop / Mac</h3>
|
||||||
<p id="desktop-text">0</p>
|
<p id="desktop-text">0</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>Dettaglio Giocatori</h2>
|
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Ultimo Accesso</th>
|
<th>Ultimo Accesso</th>
|
||||||
<th>Giocatore (Livello)</th>
|
<th>Giocatore</th>
|
||||||
<th>Sistema</th>
|
<th>Statistiche</th>
|
||||||
<th>Località (IP)</th>
|
<th>Connessione</th>
|
||||||
<th>Dispositivo Hardware</th>
|
<th>Dispositivo</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="table-body">
|
<tbody id="table-body">
|
||||||
<tr><td colspan="5" class="empty">Caricamento dati dal database...</td></tr>
|
<tr><td colspan="5" class="empty">Caricamento dati dal database in corso...</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const PASSWORD_SEGRETA = "!!TetraQ!!"; // La tua password sicura
|
// --- CONFIGURAZIONE SICUREZZA ---
|
||||||
const UTENTE_SEGRETO = "io"; // Il nome utente richiesto
|
const PASSWORD_SEGRETA = "!!TetraQ!!";
|
||||||
|
const UTENTE_SEGRETO = "io";
|
||||||
let allData = [];
|
let allData = [];
|
||||||
|
|
||||||
// GESTIONE LOGIN
|
// --- FUNZIONI DI UTILITÀ ---
|
||||||
|
function formatTime(seconds) {
|
||||||
|
if (!seconds || seconds <= 0) return "00:00";
|
||||||
|
const h = Math.floor(seconds / 3600);
|
||||||
|
const m = Math.floor((seconds % 3600) / 60);
|
||||||
|
return `${h.toString().padStart(2, '0')}h ${m.toString().padStart(2, '0')}m`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(dateObj) {
|
||||||
|
if (!dateObj) return "N/D";
|
||||||
|
const options = { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' };
|
||||||
|
return dateObj.toLocaleDateString('it-IT', options).replace(',', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateShort(dateObj) {
|
||||||
|
if (!dateObj) return "N/D";
|
||||||
|
return dateObj.toLocaleDateString('it-IT', { year: 'numeric', month: '2-digit', day: '2-digit' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- GESTIONE LOGIN ---
|
||||||
document.getElementById('login-form').addEventListener('submit', function(e) {
|
document.getElementById('login-form').addEventListener('submit', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const user = document.getElementById('username').value.trim().toLowerCase();
|
const user = document.getElementById('username').value.trim().toLowerCase();
|
||||||
|
|
@ -195,29 +289,39 @@
|
||||||
allData = [];
|
allData = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// CONNESSIONE A FIREBASE E RECUPERO DATI
|
// --- CONNESSIONE A FIREBASE ---
|
||||||
function loadFirebaseData() {
|
function loadFirebaseData() {
|
||||||
const db = firebase.firestore();
|
const db = firebase.firestore();
|
||||||
db.collection('leaderboard').orderBy('lastActive', 'desc').get().then((snapshot) => {
|
db.collection('leaderboard').orderBy('lastActive', 'desc').onSnapshot((snapshot) => {
|
||||||
allData = [];
|
allData = [];
|
||||||
snapshot.forEach(doc => {
|
snapshot.forEach(doc => {
|
||||||
let data = doc.data();
|
let data = doc.data();
|
||||||
|
|
||||||
|
// Gestione Date Firebase
|
||||||
if (data.lastActive) {
|
if (data.lastActive) {
|
||||||
data.dateObj = data.lastActive.toDate();
|
data.dateObj = data.lastActive.toDate();
|
||||||
data.dateStr = data.dateObj.toISOString().substring(0, 10);
|
data.dateStr = data.dateObj.toISOString().substring(0, 10);
|
||||||
} else {
|
} else {
|
||||||
data.dateStr = "2000-01-01";
|
data.dateStr = "2000-01-01";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.accountCreated) {
|
||||||
|
data.createdObj = data.accountCreated.toDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nascondi account sviluppatore se necessario
|
||||||
|
if ((data.name || '').toUpperCase() !== 'PIPPO') {
|
||||||
allData.push(data);
|
allData.push(data);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
applyFilters();
|
applyFilters();
|
||||||
}).catch(error => {
|
}, error => {
|
||||||
console.error("Errore lettura database:", error);
|
console.error("Errore lettura database:", error);
|
||||||
document.getElementById('table-body').innerHTML = '<tr><td colspan="5" class="empty" style="color:red;">Errore di connessione a Firebase.</td></tr>';
|
document.getElementById('table-body').innerHTML = '<tr><td colspan="5" class="empty" style="color:#e74c3c;">Errore di connessione a Firebase. Riprova.</td></tr>';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// GESTIONE FILTRI
|
// --- GESTIONE FILTRI ---
|
||||||
document.getElementById('filter-form').addEventListener('submit', function(e) {
|
document.getElementById('filter-form').addEventListener('submit', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
applyFilters();
|
applyFilters();
|
||||||
|
|
@ -245,52 +349,53 @@
|
||||||
|
|
||||||
let platform = row.platform || 'Sconosciuta';
|
let platform = row.platform || 'Sconosciuta';
|
||||||
let city = (row.city || '').toLowerCase();
|
let city = (row.city || '').toLowerCase();
|
||||||
let ip = row.ip || 'N/D';
|
let name = (row.name || 'Sconosciuto').toLowerCase();
|
||||||
let name = row.name || 'Sconosciuto';
|
|
||||||
let level = row.level || 1;
|
|
||||||
let device = row.deviceModel || 'N/D';
|
|
||||||
let appVersion = row.appVersion || 'N/D';
|
|
||||||
|
|
||||||
// Formattazione data precisa e sicura
|
// Filtri Logica
|
||||||
let dateDisplay = row.dateObj ? row.dateObj.toLocaleString('it-IT', {
|
|
||||||
year: 'numeric', month: 'short', day: '2-digit',
|
|
||||||
hour: '2-digit', minute: '2-digit', hour12: false
|
|
||||||
}) : 'N/D';
|
|
||||||
|
|
||||||
// Filtro Data Da
|
|
||||||
if (fDa !== '' && row.dateStr < fDa) mostra = false;
|
if (fDa !== '' && row.dateStr < fDa) mostra = false;
|
||||||
// Filtro Data A
|
|
||||||
if (fA !== '' && row.dateStr > fA) mostra = false;
|
if (fA !== '' && row.dateStr > fA) mostra = false;
|
||||||
// Filtro Sistema Operativo
|
|
||||||
if (fOs !== '') {
|
if (fOs !== '') {
|
||||||
if (fOs === 'Desktop' && (platform === 'iOS' || platform === 'Android')) mostra = false;
|
if (fOs === 'Desktop' && (platform === 'iOS' || platform === 'Android')) mostra = false;
|
||||||
if (fOs !== 'Desktop' && platform !== fOs) mostra = false;
|
if (fOs !== 'Desktop' && platform !== fOs) mostra = false;
|
||||||
}
|
}
|
||||||
// Filtro Località
|
|
||||||
if (fLoc !== '' && !city.includes(fLoc)) mostra = false;
|
if (fLoc !== '' && !city.includes(fLoc) && !name.includes(fLoc)) mostra = false;
|
||||||
|
|
||||||
if (mostra) {
|
if (mostra) {
|
||||||
tot++;
|
tot++;
|
||||||
let badgeClass = 'badge-desktop';
|
let badgeClass = 'badge-desktop';
|
||||||
let platformDisplay = platform;
|
let platformDisplay = platform;
|
||||||
|
|
||||||
if (platform === 'iOS') { ios++; badgeClass = 'badge-ios'; }
|
if (platform === 'iOS' || platform === 'macOS') { ios++; badgeClass = 'badge-ios'; }
|
||||||
else if (platform === 'Android') { android++; badgeClass = 'badge-android'; }
|
else if (platform === 'Android') { android++; badgeClass = 'badge-android'; }
|
||||||
else { desktop++; platformDisplay = 'Desktop'; }
|
else { desktop++; platformDisplay = 'Desktop'; }
|
||||||
|
|
||||||
|
// Costruzione Riga Tabella
|
||||||
html += `
|
html += `
|
||||||
<tr>
|
<tr>
|
||||||
<td data-label="Ultimo Accesso">${dateDisplay}</td>
|
<td data-label="Ultimo Accesso">
|
||||||
|
<span class="data-text">${formatDate(row.dateObj)}</span>
|
||||||
|
</td>
|
||||||
<td data-label="Giocatore">
|
<td data-label="Giocatore">
|
||||||
<strong>${name}</strong> (Liv. ${level})<br>
|
<span class="player-name">${(row.name || 'GUEST').toUpperCase()}</span>
|
||||||
<small style="color:#7f8c8d;">App v. ${appVersion}</small>
|
<span class="player-level">(Liv. ${row.level || 1})</span>
|
||||||
|
<span class="sub-text">Iscritto il: ${formatDateShort(row.createdObj)}</span>
|
||||||
</td>
|
</td>
|
||||||
<td data-label="Sistema"><span class="badge ${badgeClass}">${platformDisplay}</span></td>
|
<td data-label="Statistiche">
|
||||||
<td data-label="Località">
|
<span class="sub-text">XP: <span class="stat-value">${row.xp || 0}</span></span>
|
||||||
${row.city || 'N/D'}<br>
|
<span class="sub-text">Vittorie: <span class="stat-value" style="color:#2ecc71;">${row.wins || 0}</span></span>
|
||||||
<small style="color:#7f8c8d;">IP: ${ip}</small>
|
<span class="sub-text">Tempo: <span class="stat-value" style="color:#2c3e50;">${formatTime(row.playtime)}</span></span>
|
||||||
|
</td>
|
||||||
|
<td data-label="Connessione">
|
||||||
|
<span class="badge ${badgeClass}">${platformDisplay}</span>
|
||||||
|
<span class="loc-text" style="display:block; margin-top:2px;">${row.city || 'N/D'}</span>
|
||||||
|
<span class="sub-text">IP: ${row.ip || 'N/D'}</span>
|
||||||
|
</td>
|
||||||
|
<td data-label="Dispositivo" style="font-size: 13px; color: #7f8c8d;">
|
||||||
|
<strong>${row.deviceModel || 'N/D'}</strong>
|
||||||
|
<span class="sub-text">App v. ${row.appVersion || 'N/D'}</span>
|
||||||
</td>
|
</td>
|
||||||
<td data-label="Dispositivo Hardware" style="font-size: 12px; color: #7f8c8d;">${device}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue