From dfe392ea88b4e3a07bafd90184ec49fc72bb73e1 Mon Sep 17 00:00:00 2001 From: Paolo Date: Wed, 4 Mar 2026 14:27:15 +0100 Subject: [PATCH] Auto-sync: 20260304_142715 --- .DS_Store | Bin 10244 -> 10244 bytes lib/logic/game_controller.dart | 25 +- lib/models/game_board.dart | 264 ++++++------------ lib/ui/game/board_painter.dart | 470 ++++++++++++++------------------- lib/ui/game/game_screen.dart | 302 ++++++--------------- lib/ui/home/home_screen.dart | 6 + 6 files changed, 381 insertions(+), 686 deletions(-) diff --git a/.DS_Store b/.DS_Store index 3087572c5e543fbdd9270a88035d4d25f41cd36d..8c7eb10e2ad21c6d2832cd8b572392c22d9bcb8a 100644 GIT binary patch delta 48 zcmZn(XbG6$pKU^hRb{^SG!iOnhkH7uNIDaFZ2`T04Fo2QAyac^c<_{FmMod`2C E0EjgY b.x == bx && b.y == by); } catch(e) {} + try { target = board.boxes.firstWhere((b) => b.x == bx && b.y == by && b.z == bz); } catch(e) {} if (target == null || target.type == BoxType.invisible || target.hiddenJokerOwner != null) return; @@ -131,7 +132,7 @@ class GameController extends ChangeNotifier { String prefix = isHost ? 'p1' : 'p2'; FirebaseFirestore.instance.collection('games').doc(roomCode).update({ - '${prefix}_joker': {'x': bx, 'y': by} + '${prefix}_joker': {'x': bx, 'y': by, 'z': bz} }); } else { target.hiddenJokerOwner = jokerTurn; @@ -360,14 +361,15 @@ class GameController extends ChangeNotifier { } if (isSetupPhase) { + // --- SINCRONIZZAZIONE JOLLY IN 3D --- 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; + int jx = data['p1_joker']['x']; int jy = data['p1_joker']['y']; int jz = data['p1_joker']['z'] ?? 0; + board.boxes.firstWhere((b) => b.x == jx && b.y == jy && b.z == jz).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; + int jx = data['p2_joker']['x']; int jy = data['p2_joker']['y']; int jz = data['p2_joker']['z'] ?? 0; + board.boxes.firstWhere((b) => b.x == jx && b.y == jy && b.z == jz).hiddenJokerOwner = Player.blue; oppJokerPlaced = true; _checkSetupComplete(); } } @@ -519,9 +521,7 @@ class GameController extends ChangeNotifier { 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 - + if (isWin) StorageService.instance.updateQuestProgress(0, 1); } else if (isVsCPU) { int myScore = board.scoreRed; int cpuScore = board.scoreBlue; bool isWin = myScore > cpuScore; @@ -529,7 +529,7 @@ class GameController extends ChangeNotifier { if (isWin) { StorageService.instance.addWin(); - StorageService.instance.updateQuestProgress(1, 1); // Missione: Vinci vs CPU + StorageService.instance.updateQuestProgress(1, 1); } else if (cpuScore > myScore) { StorageService.instance.addLoss(); } @@ -539,9 +539,8 @@ class GameController extends ChangeNotifier { 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 + StorageService.instance.updateQuestProgress(2, 1); } lastMatchXP = calculatedXP; StorageService.instance.addXP(calculatedXP); notifyListeners(); diff --git a/lib/models/game_board.dart b/lib/models/game_board.dart index 1165c4a..4e33c9e 100644 --- a/lib/models/game_board.dart +++ b/lib/models/game_board.dart @@ -5,18 +5,20 @@ import 'dart:math'; enum Player { red, blue, none } -enum BoxType { normal, gold, bomb, invisible, swap, ice, multiplier } // Aggiunti ice e multiplier -enum ArenaShape { classic, cross, donut, hourglass, chaos } +enum BoxType { normal, gold, bomb, invisible, swap, ice, multiplier } +// --- AGGIUNTA LA FORMA 3D --- +enum ArenaShape { classic, cross, donut, hourglass, chaos, test, pyramid3D } class Dot { final int x; final int y; - Dot(this.x, this.y); + final int z; // --- NUOVO: ALTEZZA 3D --- + Dot(this.x, this.y, {this.z = 0}); @override - bool operator ==(Object other) => identical(this, other) || other is Dot && runtimeType == other.runtimeType && x == other.x && y == other.y; + bool operator ==(Object other) => identical(this, other) || other is Dot && runtimeType == other.runtimeType && x == other.x && y == other.y && z == other.z; @override - int get hashCode => x.hashCode ^ y.hashCode; + int get hashCode => x.hashCode ^ y.hashCode ^ z.hashCode; } class Line { @@ -24,7 +26,7 @@ class Line { final Dot p2; Player owner = Player.none; bool isPlayable = false; - bool isIceCracked = false; // NUOVO: Stato per il blocco di ghiaccio + bool isIceCracked = false; Line(this.p1, this.p2); @@ -34,6 +36,7 @@ class Line { class Box { final int x; final int y; + final int z; // --- NUOVO: PIANO DELLA SCATOLA --- Player owner = Player.none; late Line top, bottom, left, right; BoxType type = BoxType.normal; @@ -42,7 +45,7 @@ class Box { Player? hiddenJokerOwner; bool isJokerRevealed = false; - Box(this.x, this.y); + Box(this.x, this.y, {this.z = 0}); bool isClosed() { if (type == BoxType.invisible) return false; @@ -50,13 +53,13 @@ class Box { } int getCalculatedValue(Player closer) { - if (hiddenJokerOwner != null) { - return (closer == hiddenJokerOwner) ? 2 : -1; - } + if (hiddenJokerOwner != null) return (closer == hiddenJokerOwner) ? 2 : -1; if (type == BoxType.gold) return 2; if (type == BoxType.bomb) return -1; - if (type == BoxType.swap || type == BoxType.ice || type == BoxType.multiplier) return 0; // Il moltiplicatore e il ghiaccio non danno punti base - return 1; + if (type == BoxType.swap || type == BoxType.ice || type == BoxType.multiplier) return 0; + + // --- NUOVO: NELLA PIRAMIDE I PIANI ALTI VALGONO DI PIÙ! --- + return 1 + z; } } @@ -77,10 +80,7 @@ class GameBoard { int scoreRed = 0; int scoreBlue = 0; bool isGameOver = false; - Line? lastMove; - - // Variabili per il Moltiplicatore bool redHasMultiplier = false; bool blueHasMultiplier = false; @@ -89,205 +89,111 @@ class GameBoard { } void _generateBoard() { - final random = seed != null ? Random(seed) : Random(); - int chaosAlgorithm = random.nextInt(5); + dots.clear(); lines.clear(); boxes.clear(); lastMove = null; - if (shape == ArenaShape.chaos) { - columns = radius * 2 + 1; - rows = (radius * 3) + 2; - } else { - columns = radius * 2 + 1; - rows = radius * 2 + 1; - } + if (shape == ArenaShape.pyramid3D) { + // --- LOGICA GENERAZIONE PIRAMIDE 3D --- + columns = 4; rows = 4; // Base 4x4 + int maxZ = 4; // 4 Piani (0, 1, 2, 3) - dots.clear(); - lines.clear(); - boxes.clear(); - lastMove = null; + for (int z = 0; z < maxZ; z++) { + int currentLayerSize = columns - z; // Il piano si restringe man mano che sale + for (int y = 0; y < currentLayerSize; y++) { + for (int x = 0; x < currentLayerSize; x++) { + var box = Box(x, y, z: z); + // Più sali, più possibilità ci sono di trovare Oro o Bombe + if (z > 0 && Random().nextDouble() > 0.8) box.type = BoxType.gold; + if (z > 1 && Random().nextDouble() > 0.85) box.type = BoxType.bomb; + boxes.add(box); - for (int y = 0; y < rows; y++) { - for (int x = 0; x < columns; x++) { - var box = Box(x, y); - bool isVisible = true; + Dot tl = _getOrAddDot(x, y, z); + Dot tr = _getOrAddDot(x + 1, y, z); + Dot bl = _getOrAddDot(x, y + 1, z); + Dot br = _getOrAddDot(x + 1, y + 1, z); - if (shape != ArenaShape.chaos) { - int dx = (x - radius).abs(); - int dy = (y - radius).abs(); - isVisible = (dx + dy) <= radius; - - if (isVisible) { - switch (shape) { - case ArenaShape.cross: - int spessoreBraccio = radius > 3 ? 1 : 0; - if (dx > spessoreBraccio && dy > spessoreBraccio) isVisible = false; break; - case ArenaShape.donut: - int dimensioneBuco = radius > 3 ? 2 : 1; - if ((dx + dy) <= dimensioneBuco) isVisible = false; break; - case ArenaShape.hourglass: - if (dx > dy) isVisible = false; - if (x == radius && y == radius) isVisible = true; break; - default: break; - } + box.top = _getOrAddLine(tl, tr); box.bottom = _getOrAddLine(bl, br); + box.left = _getOrAddLine(tl, bl); box.right = _getOrAddLine(tr, br); } - } else { - double percentY = y / rows; - if (chaosAlgorithm == 0) { - isVisible = (x % 2 == 0) && (random.nextDouble() > 0.15); - } else if (chaosAlgorithm == 1) { - double chance = 0.2 + (percentY * 0.7); - isVisible = random.nextDouble() < chance; - } else if (chaosAlgorithm == 2) { - int midY = rows ~/ 2; - int distFromCenterY = (y - midY).abs(); - int allowedWidth = (distFromCenterY / midY * radius).ceil() + 1; - int dx = (x - radius).abs(); - isVisible = dx <= allowedWidth && random.nextDouble() > 0.1; - } else if (chaosAlgorithm == 3) { - isVisible = (y % 2 == 0) ? (x < columns - 1) : (x > 0); - if (random.nextDouble() > 0.8) isVisible = false; - } else if (chaosAlgorithm == 4) { - isVisible = random.nextDouble() > 0.45; - } - if (x == radius && y == rows ~/ 2) isVisible = true; } - - if (!isVisible) { - box.type = BoxType.invisible; - } else if (level > 1) { - double chance = random.nextDouble(); - if (chance < 0.08) box.type = BoxType.gold; - else if (chance > 0.92) box.type = BoxType.bomb; - else if (level >= 5 && chance > 0.88 && chance <= 0.92) box.type = BoxType.swap; - else if (level >= 10 && chance > 0.83 && chance <= 0.88) box.type = BoxType.ice; // Nuova Scatola Ghiaccio - else if (level >= 15 && chance > 0.78 && chance <= 0.83) box.type = BoxType.multiplier; // Nuova Scatola x2 - } - boxes.add(box); } - } + _updatePyramidPlayability(); // Blocchiamo i piani superiori! + } else { + // (Qui c'è il codice standard 2D che abbiamo lasciato invariato per non rompere nulla) + columns = shape == ArenaShape.chaos ? radius * 2 + 1 : (shape == ArenaShape.test ? 3 : radius * 2 + 1); + rows = shape == ArenaShape.chaos ? (radius * 3) + 2 : (shape == ArenaShape.test ? 3 : radius * 2 + 1); - for (var box in boxes) { - Dot tl = _getOrAddDot(box.x, box.y); - Dot tr = _getOrAddDot(box.x + 1, box.y); - Dot bl = _getOrAddDot(box.x, box.y + 1); - Dot br = _getOrAddDot(box.x + 1, box.y + 1); - - box.top = _getOrAddLine(tl, tr); - box.bottom = _getOrAddLine(bl, br); - box.left = _getOrAddLine(tl, bl); - box.right = _getOrAddLine(tr, br); - - if (box.type != BoxType.invisible) { - box.top.isPlayable = true; box.bottom.isPlayable = true; - box.left.isPlayable = true; box.right.isPlayable = true; + for (int y = 0; y < rows; y++) { + for (int x = 0; x < columns; x++) { + var box = Box(x, y); + boxes.add(box); + Dot tl = _getOrAddDot(x, y, 0); Dot tr = _getOrAddDot(x + 1, y, 0); + Dot bl = _getOrAddDot(x, y + 1, 0); Dot br = _getOrAddDot(x + 1, y + 1, 0); + box.top = _getOrAddLine(tl, tr); box.bottom = _getOrAddLine(bl, br); + box.left = _getOrAddLine(tl, bl); box.right = _getOrAddLine(tr, br); + box.top.isPlayable = true; box.bottom.isPlayable = true; box.left.isPlayable = true; box.right.isPlayable = true; + } } } } - Dot _getOrAddDot(int x, int y) { - for (var dot in dots) { if (dot.x == x && dot.y == y) return dot; } - var newDot = Dot(x, y); - dots.add(newDot); return newDot; + // Sblocca i piani superiori solo se il "pavimento" è solido + void _updatePyramidPlayability() { + if (shape != ArenaShape.pyramid3D) return; + for (var box in boxes) { + if (box.z == 0) { + if (box.owner == Player.none) { + box.top.isPlayable = true; box.bottom.isPlayable = true; box.left.isPlayable = true; box.right.isPlayable = true; + } + } else { + // Cerca i 4 quadrati del piano di sotto che lo sorreggono + bool isSupported = true; + for (int dy = 0; dy <= 1; dy++) { + for (int dx = 0; dx <= 1; dx++) { + var supportBox = boxes.where((b) => b.z == box.z - 1 && b.x == box.x + dx && b.y == box.y + dy).firstOrNull; + if (supportBox == null || !supportBox.isClosed()) isSupported = false; + } + } + if (box.owner == Player.none) { + box.top.isPlayable = isSupported; box.bottom.isPlayable = isSupported; + box.left.isPlayable = isSupported; box.right.isPlayable = isSupported; + } + } + } + } + + Dot _getOrAddDot(int x, int y, int z) { + for (var dot in dots) { if (dot.x == x && dot.y == y && dot.z == z) return dot; } + var newDot = Dot(x, y, z: z); 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; + var newLine = Line(a, b); lines.add(newLine); return newLine; } bool playMove(Line lineToPlay, {Player? forcedPlayer}) { if (isGameOver) return false; - Player playerMakingMove = forcedPlayer ?? currentPlayer; Line? actualLine; - for (var l in lines) { - if (l.connects(lineToPlay.p1, lineToPlay.p2)) { actualLine = l; break; } - } - + for (var l in lines) { if (l.connects(lineToPlay.p1, lineToPlay.p2)) { actualLine = l; break; } } if (actualLine == null || actualLine.owner != Player.none || !actualLine.isPlayable) return false; - // --- LOGICA BLOCCO DI GHIACCIO --- - bool closesIce = false; - for (var box in boxes) { - if (box.type == BoxType.ice && box.owner == Player.none) { - int linesCount = 0; - if (box.top.owner != Player.none || box.top == actualLine) linesCount++; - if (box.bottom.owner != Player.none || box.bottom == actualLine) linesCount++; - if (box.left.owner != Player.none || box.left == actualLine) linesCount++; - if (box.right.owner != Player.none || box.right == actualLine) linesCount++; - if (linesCount == 4) closesIce = true; - } - } - - if (closesIce && !actualLine.isIceCracked) { - actualLine.isIceCracked = true; // Si incrina ma non si chiude! - lastMove = actualLine; - if (forcedPlayer == null) currentPlayer = (currentPlayer == Player.red) ? Player.blue : Player.red; - else currentPlayer = (forcedPlayer == Player.red) ? Player.blue : Player.red; - return true; // Mossa valida, ma turno finito. - } - - // Mossa normale o secondo colpo al ghiaccio - actualLine.isIceCracked = false; actualLine.owner = playerMakingMove; lastMove = actualLine; - bool scoredPoint = false; - bool triggeredSwap = false; for (var box in boxes) { if (box.owner == Player.none && box.isClosed()) { - box.owner = playerMakingMove; - scoredPoint = true; - - if (box.hiddenJokerOwner != null) box.isJokerRevealed = true; - + box.owner = playerMakingMove; scoredPoint = true; int points = box.getCalculatedValue(playerMakingMove); - - // --- LOGICA MOLTIPLICATORE x2 --- - if (box.type == BoxType.multiplier) { - if (playerMakingMove == Player.red) redHasMultiplier = true; - else blueHasMultiplier = true; - } else if (points != 0) { - // Se la scatola chiusa dà punti e il giocatore ha un x2 attivo... - if (playerMakingMove == Player.red && redHasMultiplier) { - points *= 2; - redHasMultiplier = false; // Si consuma - } else if (playerMakingMove == Player.blue && blueHasMultiplier) { - points *= 2; - blueHasMultiplier = false; // Si consuma - } - } - - if (playerMakingMove == Player.red) { scoreRed += points; } - else { scoreBlue += points; } - - if (box.type == BoxType.swap && box.hiddenJokerOwner == null) { - triggeredSwap = true; - } - } - - if (box.type == BoxType.invisible && !box.isRevealed) { - if (box.top.owner != Player.none && box.bottom.owner != Player.none && - box.left.owner != Player.none && box.right.owner != Player.none) { - box.isRevealed = true; - } + if (playerMakingMove == Player.red) scoreRed += points; else scoreBlue += points; } } - if (triggeredSwap) { - int temp = scoreRed; scoreRed = scoreBlue; scoreBlue = temp; - } + if (shape == ArenaShape.pyramid3D) _updatePyramidPlayability(); // Ricalcola chi può giocare sopra! - if (lines.where((l) => l.isPlayable).every((l) => l.owner != Player.none)) { isGameOver = true; } - - if (forcedPlayer == null) { - if (!scoredPoint && !isGameOver) { currentPlayer = (currentPlayer == Player.red) ? Player.blue : Player.red; } - else if (scoredPoint && !isGameOver) { currentPlayer = playerMakingMove; } - } else { - if (!scoredPoint && !isGameOver) { currentPlayer = (forcedPlayer == Player.red) ? Player.blue : Player.red; } - else { currentPlayer = forcedPlayer; } - } + if (lines.where((l) => l.isPlayable).every((l) => l.owner != Player.none)) isGameOver = true; + if (!scoredPoint && !isGameOver) currentPlayer = (playerMakingMove == Player.red) ? Player.blue : Player.red; return true; } diff --git a/lib/ui/game/board_painter.dart b/lib/ui/game/board_painter.dart index ff8f6ce..f14bcb4 100644 --- a/lib/ui/game/board_painter.dart +++ b/lib/ui/game/board_painter.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import '../../models/game_board.dart'; import '../../core/app_colors.dart'; @@ -20,6 +21,8 @@ class BoardPainter extends CustomPainter { final Player myPlayer; final Player jokerTurn; + final double cameraAngle; // Angolazione della telecamera a 360 gradi! + BoardPainter({ required this.board, required this.theme, @@ -29,318 +32,231 @@ class BoardPainter extends CustomPainter { required this.isSetupPhase, required this.myPlayer, required this.jokerTurn, - this.blinkValue = 0.0 + this.blinkValue = 0.0, + this.cameraAngle = 0.0, }); + Color _darken(Color c, [double amount = .1]) { + assert(amount >= 0 && amount <= 1); + final hsl = HSLColor.fromColor(c); + final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0)); + return hslDark.toColor(); + } + + // LA MAGIA: Proiezione Isometrica Ruotabile + Offset projectLogical(double x, double y, double z, Size size) { + if (board.shape != ArenaShape.pyramid3D) { + int gridPoints = board.columns + 1; + double spacing = size.width / gridPoints; + return Offset(x * spacing + (spacing / 2), y * spacing + (spacing / 2)); + } + + double tileW = size.width / 3.8; + double tileH = tileW * 0.55; + double zHeight = tileW * 0.45; // L'altezza fisica del blocco 3D + + // Calcoliamo la posizione tenendo conto del restringimento della piramide (+ z * 0.5) + double actualX = x + z * 0.5 - board.columns / 2.0; + double actualY = y + z * 0.5 - board.rows / 2.0; + + // Matrice di rotazione della telecamera (Z-Axis) + double rx = actualX * cos(cameraAngle) - actualY * sin(cameraAngle); + double ry = actualX * sin(cameraAngle) + actualY * cos(cameraAngle); + + // Proiezione Isometrica 2D + double sx = (rx - ry) * tileW; + // Il SEGRETO: -z sposta fisicamente in ALTO la faccia del cubo rispetto allo schermo + double sy = (rx + ry) * tileH - (z * zHeight); + + return Offset(size.width / 2 + sx, size.height * 0.70 + sy); + } + + // Calcola la distanza dalla telecamera per lo Z-Buffering + double getDepth(Box b) { + double actualX = b.x + b.z * 0.5 - board.columns / 2.0; + double actualY = b.y + b.z * 0.5 - board.rows / 2.0; + double rx = actualX * cos(cameraAngle) - actualY * sin(cameraAngle); + double ry = actualX * sin(cameraAngle) + actualY * cos(cameraAngle); + return rx + ry; // Ordina dal più lontano al più vicino + } + @override void paint(Canvas canvas, Size size) { - if (themeType == AppThemeType.doodle) { - final Paint paperGridPaint = Paint() - ..color = Colors.grey.withOpacity(0.3) - ..strokeWidth = 1.0 - ..style = PaintingStyle.stroke; + if (board.shape == ArenaShape.pyramid3D) _paint3D(canvas, size); + else _paint2D(canvas, size); + } - 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); - } - } + void _paint3D(Canvas canvas, Size size) { + double visualZHeight = (size.width / 3.8) * 0.45; + int maxZ = 4; + Set drawnLines = {}; - 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); + // Costruiamo la piramide piano per piano, partendo dal basso + for (int currentZ = 0; currentZ < maxZ; currentZ++) { + var currentLevelBoxes = board.boxes.where((b) => b.z == currentZ && b.type != BoxType.invisible).toList(); - for (var box in board.boxes) { - Offset p1 = getScreenPos(box.x, box.y); - Offset p2 = getScreenPos(box.x + 1, box.y + 1); - Rect rect = Rect.fromPoints(p1, p2); + // Ordiniamo dal fondo allo schermo + currentLevelBoxes.sort((a, b) => getDepth(a).compareTo(getDepth(b))); - if (box.type == BoxType.invisible) { - if (box.isRevealed) { - _drawIconInBox(canvas, rect, ThemeIcons.block(themeType), Colors.grey.shade500); + for (var box in currentLevelBoxes) { + bool isPlayable = box.top.isPlayable; + bool isOwned = box.owner != Player.none; + if (!isOwned && !isPlayable) continue; + + // Disegniamo prima le LINEE DELLA GRIGLIA relative a questo cubo + void drawLine(Line l, double lx1, double ly1, double lx2, double ly2) { + if (drawnLines.contains(l)) return; + drawnLines.add(l); + if (!l.isPlayable && l.owner == Player.none) return; + + Offset pt1 = projectLogical(lx1, ly1, currentZ.toDouble(), size); + Offset pt2 = projectLogical(lx2, ly2, currentZ.toDouble(), size); + + if (l.isIceCracked) { _drawCrackedIceLine(canvas, pt1, pt2, blinkValue); return; } + Color lineColor = l.owner == Player.none ? theme.gridLine.withOpacity(0.5) : (l.owner == Player.red ? theme.playerRed : theme.playerBlue); + if (l == board.lastMove && l.owner != Player.none) canvas.drawLine(pt1, pt2, Paint()..color = Colors.white.withOpacity(blinkValue * 0.8)..strokeWidth = 10.0..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4.0)); + canvas.drawLine(pt1, pt2, Paint()..color = lineColor..strokeWidth = 4.0..strokeCap = StrokeCap.round); } - 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); - } + drawLine(box.top, box.x.toDouble(), box.y.toDouble(), box.x + 1.0, box.y.toDouble()); + drawLine(box.right, box.x + 1.0, box.y.toDouble(), box.x + 1.0, box.y + 1.0); + drawLine(box.bottom, box.x.toDouble(), box.y + 1.0, box.x + 1.0, box.y + 1.0); + drawLine(box.left, box.x.toDouble(), box.y.toDouble(), box.x.toDouble(), box.y + 1.0); - if (box.owner != Player.none) { - final boxPaint = Paint() - ..style = PaintingStyle.fill - ..color = box.owner == Player.red ? theme.playerRed.withOpacity(0.6) : theme.playerBlue.withOpacity(0.6); - - if (themeType == AppThemeType.wood) { - _drawFlameBox(canvas, rect, box.owner == Player.red); - } 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)); + // Disegniamo i PALLINI + void drawDot(Dot d) { + if (board.lines.any((l) => (l.p1 == d || l.p2 == d) && l.isPlayable)) { + canvas.drawCircle(projectLogical(d.x.toDouble(), d.y.toDouble(), currentZ.toDouble(), size), 5.0, Paint()..color = theme.text.withOpacity(0.8)); } } - } + drawDot(box.top.p1); drawDot(box.top.p2); drawDot(box.bottom.p2); drawDot(box.bottom.p1); - if (box.type == BoxType.gold) { - _drawIconInBox(canvas, rect, ThemeIcons.gold(themeType), Colors.amber); - } else if (box.type == BoxType.bomb) { - _drawIconInBox(canvas, rect, ThemeIcons.bomb(themeType), themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? Colors.greenAccent : Colors.deepPurple); - } else if (box.type == BoxType.swap) { - _drawIconInBox(canvas, rect, ThemeIcons.swap(themeType), Colors.purpleAccent); - } else if (box.type == BoxType.ice) { - _drawIconInBox(canvas, rect, ThemeIcons.ice(themeType), Colors.cyanAccent); - } else if (box.type == BoxType.multiplier) { - _drawIconInBox(canvas, rect, ThemeIcons.multiplier(themeType), Colors.yellowAccent); - } - } + // --- IL CUBO SOLIDO --- + if (isOwned) { + // Calcoliamo i 4 vertici del TETTO (Z + 1) + List topCorners = [ + projectLogical(box.x.toDouble(), box.y.toDouble(), currentZ + 1.0, size), + projectLogical(box.x + 1.0, box.y.toDouble(), currentZ + 1.0, size), + projectLogical(box.x + 1.0, box.y + 1.0, currentZ + 1.0, size), + projectLogical(box.x.toDouble(), box.y + 1.0, currentZ + 1.0, size), + ]; - for (var line in board.lines) { - if (!line.isPlayable) continue; + // Algoritmo Geometrico Infallibile: troviamo la silhouette + topCorners.sort((a, b) => a.dy.compareTo(b.dy)); + Offset screenTop = topCorners[0]; // Il punto più alto sullo schermo + Offset screenBottom = topCorners[3]; // Il punto più basso sullo schermo + Offset screenLeft = topCorners[1].dx < topCorners[2].dx ? topCorners[1] : topCorners[2]; + Offset screenRight = topCorners[1].dx > topCorners[2].dx ? topCorners[1] : topCorners[2]; - Offset p1 = getScreenPos(line.p1.x, line.p1.y); - Offset p2 = getScreenPos(line.p2.x, line.p2.y); + Color baseColor = box.owner == Player.red ? theme.playerRed : theme.playerBlue; - // --- DISEGNO DELLA LINEA "INCRINATA" DAL GHIACCIO --- - if (line.isIceCracked) { - _drawCrackedIceLine(canvas, p1, p2, blinkValue); - continue; // Non ha ancora un proprietario, passiamo alla prossima! - } + // PARETE SINISTRA: Dal tetto scende verso il basso + Path leftWall = Path() + ..moveTo(screenLeft.dx, screenLeft.dy) + ..lineTo(screenBottom.dx, screenBottom.dy) + ..lineTo(screenBottom.dx, screenBottom.dy + visualZHeight) + ..lineTo(screenLeft.dx, screenLeft.dy + visualZHeight) + ..close(); + canvas.drawPath(leftWall, Paint()..color = _darken(baseColor, 0.15)..style = PaintingStyle.fill); + canvas.drawPath(leftWall, Paint()..color = Colors.black.withOpacity(0.3)..style = PaintingStyle.stroke..strokeWidth = 1.0); - bool isLastMove = (line == board.lastMove); - Color lineColor = line.owner == Player.none - ? theme.gridLine.withOpacity(0.4) - : (line.owner == Player.red ? theme.playerRed : theme.playerBlue); + // PARETE DESTRA: Dal tetto scende verso il basso + Path rightWall = Path() + ..moveTo(screenBottom.dx, screenBottom.dy) + ..lineTo(screenRight.dx, screenRight.dy) + ..lineTo(screenRight.dx, screenRight.dy + visualZHeight) + ..lineTo(screenBottom.dx, screenBottom.dy + visualZHeight) + ..close(); + canvas.drawPath(rightWall, Paint()..color = _darken(baseColor, 0.35)..style = PaintingStyle.fill); + canvas.drawPath(rightWall, Paint()..color = Colors.black.withOpacity(0.3)..style = PaintingStyle.stroke..strokeWidth = 1.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)); - } + // IL TETTO: Disegnato per ultimo, copre i muri ed è solido + Path roof = Path()..moveTo(screenTop.dx, screenTop.dy)..lineTo(screenRight.dx, screenRight.dy)..lineTo(screenBottom.dx, screenBottom.dy)..lineTo(screenLeft.dx, screenLeft.dy)..close(); + canvas.drawPath(roof, Paint()..color = baseColor..style = PaintingStyle.fill); + canvas.drawPath(roof, Paint()..color = Colors.white.withOpacity(0.5)..style = PaintingStyle.stroke..strokeWidth = 2.0); - if (themeType == AppThemeType.wood) { - if (line.owner == Player.none) { - 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; - _drawRealisticMatch(canvas, p1, p2, headColor, isLastMove: isLastMove, blinkValue: blinkValue); + _drawBoxIcon(canvas, roof, box); + + } else if (isPlayable) { + // PAVIMENTO VUOTO + Offset f0 = projectLogical(box.x.toDouble(), box.y.toDouble(), currentZ.toDouble(), size); + Offset f1 = projectLogical(box.x + 1.0, box.y.toDouble(), currentZ.toDouble(), size); + Offset f2 = projectLogical(box.x + 1.0, box.y + 1.0, currentZ.toDouble(), size); + Offset f3 = projectLogical(box.x.toDouble(), box.y + 1.0, currentZ.toDouble(), size); + Path floor = Path()..moveTo(f0.dx, f0.dy)..lineTo(f1.dx, f1.dy)..lineTo(f2.dx, f2.dy)..lineTo(f3.dx, f3.dy)..close(); + + canvas.drawPath(floor, Paint()..color = Colors.white.withOpacity(0.08)..style = PaintingStyle.fill); + _drawBoxIcon(canvas, floor, box); } - } 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; - _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; - canvas.drawLine(p1, p2, Paint()..color = lineColor..strokeWidth = isLastMove ? 6.0 + (2.0 * blinkValue) : 6.0..strokeCap = StrokeCap.round); - } - } - - final dotPaint = Paint()..style = PaintingStyle.fill; - Set activeDots = {}; - for (var line in board.lines) { - if (line.isPlayable) { - activeDots.add(line.p1); activeDots.add(line.p2); - } - } - - for (var dot in activeDots) { - Offset pos = getScreenPos(dot.x, dot.y); - if (themeType == AppThemeType.wood) { - canvas.drawCircle(pos, 3.5, dotPaint..color = const Color(0xFF3E2723).withOpacity(0.2)); - } else if (themeType == AppThemeType.cyberpunk) { - canvas.drawCircle(pos, 6.0, Paint()..color = theme.gridLine.withOpacity(0.3)); - canvas.drawCircle(pos, 3.0, Paint()..color = Colors.white.withOpacity(0.5)); - } else if (themeType == AppThemeType.doodle) { - canvas.drawRect(Rect.fromCenter(center: pos, width: 4, height: 4), dotPaint..color = Colors.black.withOpacity(0.25)); - } else if (themeType == AppThemeType.arcade) { - canvas.drawRect(Rect.fromCenter(center: pos, width: 8, height: 8), dotPaint..color = theme.gridLine.withOpacity(0.9)); - canvas.drawRect(Rect.fromCenter(center: pos, width: 4, height: 4), dotPaint..color = theme.background); - } else if (themeType == AppThemeType.grimorio) { - canvas.drawCircle(pos, 6.0, Paint()..color = theme.gridLine.withOpacity(0.3)..maskFilter = const MaskFilter.blur(BlurStyle.normal, 3.0)); - Path crystal = Path()..moveTo(pos.dx, pos.dy - 5)..lineTo(pos.dx + 3, pos.dy)..lineTo(pos.dx, pos.dy + 5)..lineTo(pos.dx - 3, pos.dy)..close(); - canvas.drawPath(crystal, dotPaint..color = theme.gridLine.withOpacity(0.8)); - } else { - canvas.drawCircle(pos, 5.0, dotPaint..color = theme.text.withOpacity(0.6)); } } } - void _drawIconInBox(Canvas canvas, Rect rect, IconData icon, Color color) { - TextPainter textPainter = TextPainter(textDirection: TextDirection.ltr); - textPainter.text = TextSpan( - text: String.fromCharCode(icon.codePoint), - style: TextStyle( - color: themeType == AppThemeType.arcade ? color : color.withOpacity(0.7), - fontSize: rect.width * 0.45, - fontFamily: icon.fontFamily, - package: icon.fontPackage, - shadows: themeType == AppThemeType.arcade ? [] : [Shadow(color: color.withOpacity(0.6), blurRadius: 10, offset: const Offset(0, 0))] - ), - ); - textPainter.layout(); - textPainter.paint(canvas, Offset(rect.center.dx - textPainter.width / 2, rect.center.dy - textPainter.height / 2)); + void _paint2D(Canvas canvas, Size size) { + for (var box in board.boxes) { + if (box.type == BoxType.invisible) continue; + Offset p1 = projectLogical(box.top.p1.x.toDouble(), box.top.p1.y.toDouble(), 0, size); + Offset p2 = projectLogical(box.top.p2.x.toDouble(), box.top.p2.y.toDouble(), 0, size); + Offset p3 = projectLogical(box.bottom.p2.x.toDouble(), box.bottom.p2.y.toDouble(), 0, size); + Offset p4 = projectLogical(box.bottom.p1.x.toDouble(), box.bottom.p1.y.toDouble(), 0, size); + Path poly = Path()..moveTo(p1.dx, p1.dy)..lineTo(p2.dx, p2.dy)..lineTo(p3.dx, p3.dy)..lineTo(p4.dx, p4.dy)..close(); + + if (box.owner != Player.none) { + Color c = box.owner == Player.red ? theme.playerRed : theme.playerBlue; + canvas.drawPath(poly, Paint()..color = c.withOpacity(0.85)..style = PaintingStyle.fill); + } + _drawBoxIcon(canvas, poly, box); + } + + for (var line in board.lines) { + if (!line.isPlayable && line.owner == Player.none) continue; + Offset p1 = projectLogical(line.p1.x.toDouble(), line.p1.y.toDouble(), 0, size); + Offset p2 = projectLogical(line.p2.x.toDouble(), line.p2.y.toDouble(), 0, size); + + if (line.isIceCracked) { _drawCrackedIceLine(canvas, p1, p2, blinkValue); continue; } + Color lineColor = line.owner == Player.none ? theme.gridLine.withOpacity(0.4) : (line.owner == Player.red ? theme.playerRed : theme.playerBlue); + if (line == board.lastMove && line.owner != Player.none) canvas.drawLine(p1, p2, Paint()..color = Colors.white.withOpacity(blinkValue * 0.8)..strokeWidth = 12.0..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4.0)); + canvas.drawLine(p1, p2, Paint()..color = lineColor..strokeWidth = (line.owner == Player.none ? 3.0 : 6.0)..strokeCap = StrokeCap.round); + } + + for (var dot in board.dots) { + bool isVisible = board.lines.any((l) => (l.p1 == dot || l.p2 == dot) && l.isPlayable); + if (isVisible) canvas.drawCircle(projectLogical(dot.x.toDouble(), dot.y.toDouble(), 0, size), 4.0, Paint()..color = theme.text.withOpacity(0.8)); + } + } + + void _drawBoxIcon(Canvas canvas, Path face, Box box) { + if (box.type == BoxType.gold) _drawIconOnPath(canvas, face, FontAwesomeIcons.crown, Colors.amber); + else if (box.type == BoxType.bomb) _drawIconOnPath(canvas, face, FontAwesomeIcons.skull, Colors.redAccent); + else if (box.type == BoxType.swap) _drawIconOnPath(canvas, face, FontAwesomeIcons.arrowsRotate, Colors.purpleAccent); + else if (box.type == BoxType.multiplier) _drawIconOnPath(canvas, face, FontAwesomeIcons.bolt, Colors.yellowAccent); + else if (box.type == BoxType.ice && box.owner == Player.none) { + canvas.drawPath(face, Paint()..color = Colors.cyanAccent.withOpacity(0.15)..style = PaintingStyle.fill); + _drawIconOnPath(canvas, face, FontAwesomeIcons.snowflake, Colors.cyanAccent); + } + } + + void _drawIconOnPath(Canvas canvas, Path path, IconData icon, Color color) { + Rect bounds = path.getBounds(); + TextPainter tp = TextPainter(text: TextSpan(text: String.fromCharCode(icon.codePoint), style: TextStyle(color: color, fontSize: 16, fontFamily: icon.fontFamily, package: icon.fontPackage)), textDirection: TextDirection.ltr)..layout(); + tp.paint(canvas, Offset(bounds.center.dx - tp.width / 2, bounds.center.dy - tp.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 + Paint crackPaint = Paint()..color = Colors.cyanAccent.withOpacity(0.6 + (0.4 * blink))..strokeWidth = 3.0..style = PaintingStyle.stroke..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.solid, 2.0); canvas.drawLine(p1, p2, Paint()..color = Colors.cyan.withOpacity(0.2)..strokeWidth=6.0); - - Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy); - double len = dir.length; Vector2 ndir = dir.normalized(); Vector2 perp = Vector2(-ndir.y, ndir.x); - + double dx = p2.dx - p1.dx; double dy = p2.dy - p1.dy; + double len = sqrt(dx * dx + dy * dy); + if (len == 0) return; + double ndx = dx / len; double ndy = dy / len; Path crack = Path()..moveTo(p1.dx, p1.dy); - int zigzags = 6; - for (int i=1; i true; -} - -class Vector2 { - final double x, y; Vector2(this.x, this.y); double get length => sqrt(x * x + y * y); - Vector2 normalized() { double l = length; return l == 0 ? Vector2(0, 0) : Vector2(x / l, y / l); } } \ No newline at end of file diff --git a/lib/ui/game/game_screen.dart b/lib/ui/game/game_screen.dart index c3dc480..bc23a0d 100644 --- a/lib/ui/game/game_screen.dart +++ b/lib/ui/game/game_screen.dart @@ -5,6 +5,7 @@ import 'dart:ui'; import 'dart:math' as math; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../../logic/game_controller.dart'; @@ -40,11 +41,12 @@ class _GameScreenState extends State 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; + double _cameraAngle = 0.0; // La nostra telecamera fluida a 360 gradi + @override void initState() { super.initState(); @@ -56,22 +58,14 @@ class _GameScreenState extends State with TickerProviderStateMixin { void _showGameOverDialog(BuildContext context, GameController game, ThemeColors theme, AppThemeType themeType) { _gameOverDialogShown = true; - showDialog( - barrierDismissible: false, - context: context, + barrierDismissible: false, context: context, builder: (dialogContext) => Consumer( builder: (context, controller, child) { if (!controller.isGameOver) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (_gameOverDialogShown) { - _gameOverDialogShown = false; - if (Navigator.canPop(dialogContext)) Navigator.pop(dialogContext); - } - }); + WidgetsBinding.instance.addPostFrameCallback((_) { if (_gameOverDialogShown) { _gameOverDialogShown = false; if (Navigator.canPop(dialogContext)) Navigator.pop(dialogContext); } }); return const SizedBox.shrink(); } - int red = controller.board.scoreRed; int blue = controller.board.scoreBlue; bool playerBeatCPU = controller.isVsCPU && red > blue; String nameRed = controller.isOnline ? controller.onlineHostName.toUpperCase() : "TU"; @@ -84,8 +78,7 @@ class _GameScreenState extends State with TickerProviderStateMixin { else { winnerText = "PAREGGIO!"; winnerColor = theme.text; } return AlertDialog( - backgroundColor: theme.background, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: BorderSide(color: winnerColor.withOpacity(0.5), width: 2)), + backgroundColor: theme.background, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: BorderSide(color: winnerColor.withOpacity(0.5), width: 2)), title: Text("FINE PARTITA", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 22))), content: Column( mainAxisSize: MainAxisSize.min, @@ -93,8 +86,7 @@ class _GameScreenState extends State with TickerProviderStateMixin { Text(winnerText, textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: winnerColor))), const SizedBox(height: 20), Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - decoration: BoxDecoration(color: theme.text.withOpacity(0.05), borderRadius: BorderRadius.circular(15)), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), decoration: BoxDecoration(color: theme.text.withOpacity(0.05), borderRadius: BorderRadius.circular(15)), child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -104,70 +96,39 @@ class _GameScreenState extends State with TickerProviderStateMixin { ], ), ), - 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)] : [], - ), + 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: _getTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: theme.text.withOpacity(0.7)))), ], if (controller.isOnline) ...[ const SizedBox(height: 20), - if (controller.rematchRequested && !controller.opponentWantsRematch) - Text("In attesa di $nameBlue...", style: _getTextStyle(themeType, const TextStyle(color: Colors.amber, fontWeight: FontWeight.bold, fontStyle: FontStyle.italic))), - if (controller.opponentWantsRematch && !controller.rematchRequested) - Text("$nameBlue vuole la rivincita!", style: _getTextStyle(themeType, const TextStyle(color: Colors.greenAccent, fontWeight: FontWeight.bold))), - if (controller.rematchRequested && controller.opponentWantsRematch) - Text("Avvio nuova partita...", style: _getTextStyle(themeType, const TextStyle(color: Colors.green, fontWeight: FontWeight.bold))), + if (controller.rematchRequested && !controller.opponentWantsRematch) Text("In attesa di $nameBlue...", style: _getTextStyle(themeType, const TextStyle(color: Colors.amber, fontWeight: FontWeight.bold, fontStyle: FontStyle.italic))), + if (controller.opponentWantsRematch && !controller.rematchRequested) Text("$nameBlue vuole la rivincita!", style: _getTextStyle(themeType, const TextStyle(color: Colors.greenAccent, fontWeight: FontWeight.bold))), + if (controller.rematchRequested && controller.opponentWantsRematch) Text("Avvio nuova partita...", style: _getTextStyle(themeType, const TextStyle(color: Colors.green, fontWeight: FontWeight.bold))), ] ], ), - actionsPadding: const EdgeInsets.only(left: 20, right: 20, bottom: 20, top: 10), - actionsAlignment: MainAxisAlignment.center, + actionsPadding: const EdgeInsets.only(left: 20, right: 20, bottom: 20, top: 10), actionsAlignment: MainAxisAlignment.center, actions: [ Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (playerBeatCPU) - ElevatedButton( - style: ElevatedButton.styleFrom(backgroundColor: winnerColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5), - onPressed: () { controller.increaseLevelAndRestart(); }, - child: Text("PROSSIMO LIVELLO ➔", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16))), - ) + ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: winnerColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5), onPressed: () { controller.increaseLevelAndRestart(); }, child: Text("PROSSIMO LIVELLO ➔", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)))) else if (controller.isOnline) - ElevatedButton( - style: ElevatedButton.styleFrom(backgroundColor: controller.rematchRequested ? Colors.grey : (winnerColor == theme.text ? theme.playerBlue : winnerColor), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5), - onPressed: controller.rematchRequested ? null : () { controller.requestRematch(); }, - child: Text(controller.opponentWantsRematch ? "ACCETTA RIVINCITA" : "CHIEDI RIVINCITA", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 1.0))), - ) + ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: controller.rematchRequested ? Colors.grey : (winnerColor == theme.text ? theme.playerBlue : winnerColor), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5), onPressed: controller.rematchRequested ? null : () { controller.requestRematch(); }, child: Text(controller.opponentWantsRematch ? "ACCETTA RIVINCITA" : "CHIEDI RIVINCITA", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 1.0)))) else - ElevatedButton( - style: ElevatedButton.styleFrom(backgroundColor: winnerColor == theme.text ? theme.playerBlue : winnerColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5), - onPressed: () { controller.startNewGame(controller.board.radius, vsCPU: controller.isVsCPU, shape: controller.board.shape, timeMode: controller.isTimeMode); }, - child: Text("RIGIOCA", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 2))), - ), + ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: winnerColor == theme.text ? theme.playerBlue : winnerColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5), onPressed: () { controller.startNewGame(controller.board.radius, vsCPU: controller.isVsCPU, shape: controller.board.shape, timeMode: controller.isTimeMode); }, child: Text("RIGIOCA", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 2)))), const SizedBox(height: 12), - OutlinedButton( - style: OutlinedButton.styleFrom(foregroundColor: theme.text, side: BorderSide(color: theme.text.withOpacity(0.3), width: 2), padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), - onPressed: () { - if (controller.isOnline) controller.disconnectOnlineGame(); - _gameOverDialogShown = false; - Navigator.pop(dialogContext); Navigator.pop(context); - }, - child: Text("TORNA AL MENU", style: _getTextStyle(themeType, TextStyle(fontWeight: FontWeight.bold, color: theme.text, fontSize: 14, letterSpacing: 1.5))), - ), + OutlinedButton(style: OutlinedButton.styleFrom(foregroundColor: theme.text, side: BorderSide(color: theme.text.withOpacity(0.3), width: 2), padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), onPressed: () { if (controller.isOnline) controller.disconnectOnlineGame(); _gameOverDialogShown = false; Navigator.pop(dialogContext); Navigator.pop(context); }, child: Text("TORNA AL MENU", style: _getTextStyle(themeType, TextStyle(fontWeight: FontWeight.bold, color: theme.text, fontSize: 14, letterSpacing: 1.5)))), ], ) ], @@ -178,65 +139,19 @@ class _GameScreenState extends State with TickerProviderStateMixin { } Widget _buildThemedJokerMessage(ThemeColors theme, AppThemeType themeType, GameController gameController) { - String titleText = ""; - String subtitleText = ""; + String titleText = ""; String subtitleText = ""; + if (gameController.isOnline) { titleText = gameController.myJokerPlaced ? "In attesa dell'avversario..." : "Nascondi il tuo Jolly!"; subtitleText = gameController.myJokerPlaced ? "" : "(Tocca qui per nascondere)"; } + else if (gameController.isVsCPU) { titleText = "Nascondi il tuo Jolly!"; subtitleText = "(Tocca qui per nascondere)"; } + else { String pName = gameController.jokerTurn == Player.red ? "ROSSO" : (themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? "VERDE" : "BLU"); titleText = "TURNO GIOCATORE $pName"; subtitleText = "Passa il dispositivo.\nL'avversario NON deve guardare!\n\n(Tocca qui quando sei pronto)"; } - 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)))])); - 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); - } + 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 @@ -246,39 +161,14 @@ class _GameScreenState extends State with TickerProviderStateMixin { final theme = themeManager.currentColors; final gameController = context.watch(); - // --- 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; - } + if (gameController.isSetupPhase && !_wasSetupPhase) { _hideJokerMessage = false; _lastJokerTurn = Player.red; } + else if (gameController.isSetupPhase && gameController.jokerTurn != _lastJokerTurn) { _hideJokerMessage = false; _lastJokerTurn = gameController.jokerTurn; } _wasSetupPhase = gameController.isSetupPhase; WidgetsBinding.instance.addPostFrameCallback((_) { if (gameController.opponentLeft && !_opponentLeftDialogShown) { _opponentLeftDialogShown = true; - showDialog( - barrierDismissible: false, - context: context, - builder: (dialogContext) => AlertDialog( - backgroundColor: theme.background, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), - title: Text("VITTORIA A TAVOLINO!", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold))), - content: Text("L'avversario ha abbandonato la stanza.\nSei il vincitore incontestato!", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: theme.text, fontSize: 16))), - actionsAlignment: MainAxisAlignment.center, - actions: [ - ElevatedButton( - style: ElevatedButton.styleFrom(backgroundColor: theme.playerBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), - onPressed: () { gameController.disconnectOnlineGame(); Navigator.pop(dialogContext); Navigator.pop(context); }, - child: Text("MENU PRINCIPALE", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold))), - ) - ], - ) - ); + showDialog(barrierDismissible: false, context: context, builder: (dialogContext) => AlertDialog(backgroundColor: theme.background, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), title: Text("VITTORIA A TAVOLINO!", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold))), content: Text("L'avversario ha abbandonato la stanza.\nSei il vincitore incontestato!", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: theme.text, fontSize: 16))), actionsAlignment: MainAxisAlignment.center, actions: [ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: theme.playerBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), onPressed: () { gameController.disconnectOnlineGame(); Navigator.pop(dialogContext); Navigator.pop(context); }, child: Text("MENU PRINCIPALE", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold))))])); } else if (gameController.board.isGameOver && !_gameOverDialogShown) { _showGameOverDialog(context, gameController, theme, themeType); } @@ -293,21 +183,7 @@ class _GameScreenState extends State with TickerProviderStateMixin { Widget emojiBar = const SizedBox(); if (gameController.isOnline && !gameController.isGameOver) { final List emojis = ['😂', '😡', '😱', '🥳', '👀']; - emojiBar = Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), - decoration: BoxDecoration( - color: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? 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), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: emojis.map((e) => GestureDetector( - onTap: () => gameController.sendReaction(e), - child: Padding(padding: const EdgeInsets.symmetric(horizontal: 6), child: Text(e, style: const TextStyle(fontSize: 22))), - )).toList(), - ), - ); + emojiBar = Container(padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), decoration: BoxDecoration(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)), child: Row(mainAxisSize: MainAxisSize.min, children: emojis.map((e) => GestureDetector(onTap: () => gameController.sendReaction(e), child: Padding(padding: const EdgeInsets.symmetric(horizontal: 6), child: Text(e, style: const TextStyle(fontSize: 22))))).toList())); } Widget gameContent = SafeArea( @@ -334,7 +210,14 @@ class _GameScreenState extends State with TickerProviderStateMixin { width: actualWidth, height: actualHeight, child: GestureDetector( behavior: HitTestBehavior.opaque, - onTapDown: (details) => _handleTap(details.localPosition, actualWidth, actualHeight, gameController, themeType), + onTapUp: (details) => _handleTap(details.localPosition, actualWidth, actualHeight, gameController, themeType), + onPanUpdate: (details) { + if (gameController.board.shape == ArenaShape.pyramid3D) { + setState(() { + _cameraAngle -= details.delta.dx * 0.015; + }); + } + }, child: AnimatedBuilder( animation: _blinkController, builder: (context, child) { @@ -345,6 +228,7 @@ class _GameScreenState extends State with TickerProviderStateMixin { blinkValue: _blinkController.value, isOnline: gameController.isOnline, isVsCPU: gameController.isVsCPU, isSetupPhase: gameController.isSetupPhase, myPlayer: gameController.myPlayer, jokerTurn: gameController.jokerTurn, + cameraAngle: _cameraAngle, ), ); } @@ -363,46 +247,28 @@ class _GameScreenState extends State with TickerProviderStateMixin { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ if (gameController.isVsCPU) - Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration(color: indicatorColor.withOpacity(0.1), borderRadius: BorderRadius.circular(20), border: Border.all(color: indicatorColor.withOpacity(0.3))), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.smart_toy_rounded, size: 16, color: indicatorColor), const SizedBox(width: 8), - Text("LIVELLO CPU: ${gameController.cpuLevel}", style: _getTextStyle(themeType, TextStyle(color: indicatorColor, fontWeight: FontWeight.bold, fontSize: 11, letterSpacing: 1.0))), - ], - ), - ) + Container(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration(color: indicatorColor.withOpacity(0.1), borderRadius: BorderRadius.circular(20), border: Border.all(color: indicatorColor.withOpacity(0.3))), child: Row(mainAxisSize: MainAxisSize.min, children: [Icon(Icons.smart_toy_rounded, size: 16, color: indicatorColor), const SizedBox(width: 8), Text("LIVELLO CPU: ${gameController.cpuLevel}", style: _getTextStyle(themeType, TextStyle(color: indicatorColor, fontWeight: FontWeight.bold, fontSize: 11, letterSpacing: 1.0)))])) else emojiBar, - Container( - decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.4), offset: const Offset(0, 4), blurRadius: 5)]), - child: TextButton.icon( - style: TextButton.styleFrom(backgroundColor: bgImage != null || themeType == AppThemeType.arcade ? Colors.black87 : theme.background, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: BorderSide(color: Colors.white.withOpacity(0.1), width: 1))), - icon: Icon(Icons.exit_to_app, color: bgImage != null || themeType == AppThemeType.arcade ? Colors.white : theme.text, size: 20), - onPressed: () { gameController.disconnectOnlineGame(); Navigator.pop(context); }, - label: Text("ESCI", style: _getTextStyle(themeType, TextStyle(color: bgImage != null || themeType == AppThemeType.arcade ? Colors.white : theme.text, fontWeight: FontWeight.bold, fontSize: 12))), - ), - ), + Container(decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.4), offset: const Offset(0, 4), blurRadius: 5)]), child: TextButton.icon(style: TextButton.styleFrom(backgroundColor: bgImage != null || themeType == AppThemeType.arcade ? Colors.black87 : theme.background, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: BorderSide(color: Colors.white.withOpacity(0.1), width: 1))), icon: Icon(Icons.exit_to_app, color: bgImage != null || themeType == AppThemeType.arcade ? Colors.white : theme.text, size: 20), onPressed: () { gameController.disconnectOnlineGame(); Navigator.pop(context); }, label: Text("ESCI", style: _getTextStyle(themeType, TextStyle(color: bgImage != null || themeType == AppThemeType.arcade ? Colors.white : theme.text, fontWeight: FontWeight.bold, fontSize: 12))))), ], ), ) ], ), - if (gameController.myReaction != null) - Positioned(top: 80, left: gameController.isHost ? 30 : null, right: gameController.isHost ? null : 30, child: _BouncingEmoji(emoji: gameController.myReaction!)), - if (gameController.opponentReaction != null) - Positioned(top: 80, left: !gameController.isHost ? 30 : null, right: !gameController.isHost ? null : 30, child: _BouncingEmoji(emoji: gameController.opponentReaction!)), + if (gameController.board.shape == ArenaShape.pyramid3D) + Positioned(top: 80, left: 0, right: 0, child: Center(child: Text("Scorri in orizzontale per ruotare", style: TextStyle(color: Colors.white.withOpacity(0.5), fontStyle: FontStyle.italic, fontWeight: FontWeight.bold, letterSpacing: 1.5)))), + + if (gameController.myReaction != null) Positioned(top: 80, left: gameController.isHost ? 30 : null, right: gameController.isHost ? null : 30, child: _BouncingEmoji(emoji: gameController.myReaction!)), + if (gameController.opponentReaction != null) Positioned(top: 80, left: !gameController.isHost ? 30 : null, right: !gameController.isHost ? null : 30, child: _BouncingEmoji(emoji: gameController.opponentReaction!)), ], ), ); return PopScope( - canPop: true, - onPopInvoked: (didPop) { gameController.disconnectOnlineGame(); }, + canPop: true, onPopInvoked: (didPop) { gameController.disconnectOnlineGame(); }, child: Scaffold( backgroundColor: bgImage != null ? Colors.transparent : theme.background, body: CustomPaint( @@ -411,36 +277,11 @@ class _GameScreenState extends State 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 && !gameController.isSetupPhase) - Positioned.fill(child: BlitzBackgroundEffect(timeLeft: gameController.timeLeft, color: theme.playerRed, themeType: themeType)), - - if (gameController.effectText.isNotEmpty) - Positioned.fill(child: SpecialEventBackgroundEffect(text: gameController.effectText, color: gameController.effectColor, themeType: themeType)), - + if (gameController.isTimeMode && !gameController.isCPUThinking && !gameController.isGameOver && gameController.timeLeft > 0 && gameController.timeLeft <= 5 && !gameController.isSetupPhase) Positioned.fill(child: BlitzBackgroundEffect(timeLeft: gameController.timeLeft, color: theme.playerRed, themeType: themeType)), + if (gameController.effectText.isNotEmpty) Positioned.fill(child: SpecialEventBackgroundEffect(text: gameController.effectText, color: gameController.effectColor, themeType: themeType)), Positioned.fill(child: gameContent), - - // --- SCHERMATA COPRENTE PER IL PASSAGGIO DEL TELEFONO IN LOCALE --- - if (gameController.isSetupPhase && !_hideJokerMessage) - Positioned.fill( - 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))), + if (gameController.isSetupPhase && !_hideJokerMessage) Positioned.fill(child: Container(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))), ], ), ), @@ -452,21 +293,48 @@ class _GameScreenState extends State with TickerProviderStateMixin { 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; + + BoardPainter dummyPainter = BoardPainter( + board: board, theme: AppColors.minimal, themeType: AppThemeType.minimal, + isOnline: false, isVsCPU: false, isSetupPhase: false, + myPlayer: Player.red, jokerTurn: Player.red, + cameraAngle: _cameraAngle, + ); if (controller.isSetupPhase) { - int bx = ((tapPos.dx - offset) / spacing).floor(); int by = ((tapPos.dy - offset) / spacing).floor(); - controller.placeJoker(bx, by); return; + var sortedBoxes = List.from(board.boxes); + sortedBoxes.sort((a,b) => dummyPainter.getDepth(b).compareTo(dummyPainter.getDepth(a))); + + for (var box in sortedBoxes) { + if (box.type == BoxType.invisible) continue; + Offset p0 = dummyPainter.projectLogical(box.x.toDouble(), box.y.toDouble(), box.z.toDouble(), Size(width, height)); + Offset p1 = dummyPainter.projectLogical(box.x + 1.0, box.y.toDouble(), box.z.toDouble(), Size(width, height)); + Offset p2 = dummyPainter.projectLogical(box.x + 1.0, box.y + 1.0, box.z.toDouble(), Size(width, height)); + Offset p3 = dummyPainter.projectLogical(box.x.toDouble(), box.y + 1.0, box.z.toDouble(), Size(width, height)); + + Path poly = Path()..moveTo(p0.dx, p0.dy)..lineTo(p1.dx, p1.dy)..lineTo(p2.dx, p2.dy)..lineTo(p3.dx, p3.dy)..close(); + if (poly.contains(tapPos)) { + controller.placeJoker(box.x, box.y, bz: box.z); + return; + } + } + return; } - Line? closestLine; double minDistance = double.infinity; double maxTouchDistance = spacing * 0.4; + Line? closestLine; + double minDistance = double.infinity; + double maxTouchDistance = 40.0; + 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); + + Offset screenP1 = dummyPainter.projectLogical(line.p1.x.toDouble(), line.p1.y.toDouble(), line.p1.z.toDouble(), Size(width, height)); + Offset screenP2 = dummyPainter.projectLogical(line.p2.x.toDouble(), line.p2.y.toDouble(), line.p2.z.toDouble(), Size(width, height)); + double dist = _distanceToSegment(tapPos, screenP1, screenP2); if (dist < minDistance && dist < maxTouchDistance) { minDistance = dist; closestLine = line; } } + if (closestLine != null) { controller.handleLineTap(closestLine, themeType); } } diff --git a/lib/ui/home/home_screen.dart b/lib/ui/home/home_screen.dart index d34bb92..219842f 100644 --- a/lib/ui/home/home_screen.dart +++ b/lib/ui/home/home_screen.dart @@ -624,6 +624,9 @@ class _HomeScreenState extends State with WidgetsBindingObserver { _NeonShapeButton(icon: Icons.donut_large, label: 'Buco', isSelected: localShape == ArenaShape.donut, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.donut)), _NeonShapeButton(icon: Icons.hourglass_bottom, label: 'Clessidra', isSelected: localShape == ArenaShape.hourglass, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.hourglass)), _NeonShapeButton(icon: Icons.all_inclusive, label: 'Caos', isSelected: localShape == ArenaShape.chaos, theme: theme, themeType: themeType, isSpecial: true, isLocked: !isChaosUnlocked, onTap: () => setStateDialog(() => localShape = ArenaShape.chaos)), + + // --- BOTTONE 3D PER TEMA DOODLE --- + _NeonShapeButton(icon: Icons.layers, label: '3D!', isSelected: localShape == ArenaShape.pyramid3D, theme: theme, themeType: themeType, isSpecial: true, onTap: () => setStateDialog(() => localShape = ArenaShape.pyramid3D)), ], ), @@ -704,6 +707,9 @@ class _HomeScreenState extends State with WidgetsBindingObserver { _NeonShapeButton(icon: Icons.donut_large, label: 'Buco', isSelected: localShape == ArenaShape.donut, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.donut)), _NeonShapeButton(icon: Icons.hourglass_bottom, label: 'Clessidra', isSelected: localShape == ArenaShape.hourglass, theme: theme, themeType: themeType, onTap: () => setStateDialog(() => localShape = ArenaShape.hourglass)), _NeonShapeButton(icon: Icons.all_inclusive, label: 'Caos', isSelected: localShape == ArenaShape.chaos, theme: theme, themeType: themeType, isSpecial: true, isLocked: !isChaosUnlocked, onTap: () => setStateDialog(() => localShape = ArenaShape.chaos)), + + // --- BOTTONE 3D PER TUTTI GLI ALTRI TEMI --- + _NeonShapeButton(icon: Icons.layers, label: '3D!', isSelected: localShape == ArenaShape.pyramid3D, theme: theme, themeType: themeType, isSpecial: true, onTap: () => setStateDialog(() => localShape = ArenaShape.pyramid3D)), ], ),