Compare commits

...

2 commits

Author SHA1 Message Date
io
42b8180f5e Sync da MacBook: 20260314_235447 2026-03-14 23:54:48 +01:00
dfe392ea88 Auto-sync: 20260304_142715 2026-03-04 14:27:15 +01:00
8 changed files with 394 additions and 705 deletions

BIN
.DS_Store vendored

Binary file not shown.

View file

@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited. # This file should be version controlled and should not be manually edited.
version: version:
revision: "3b62efc2a3da49882f43c372e0bc53daef7295a6" revision: "ff37bef603469fb030f2b72995ab929ccfc227f0"
channel: "stable" channel: "stable"
project_type: app project_type: app
@ -13,17 +13,11 @@ project_type: app
migration: migration:
platforms: platforms:
- platform: root - platform: root
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
- platform: android
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
- platform: ios
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
- platform: macos - platform: macos
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
# User provided section # User provided section

View file

@ -114,11 +114,12 @@ class GameController extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void placeJoker(int bx, int by) { // --- AGGIUNTA LA COORDINATA Z PER IL 3D ---
void placeJoker(int bx, int by, {int bz = 0}) {
if (!isSetupPhase) return; if (!isSetupPhase) return;
Box? target; Box? target;
try { target = board.boxes.firstWhere((b) => 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; if (target == null || target.type == BoxType.invisible || target.hiddenJokerOwner != null) return;
@ -131,7 +132,7 @@ class GameController extends ChangeNotifier {
String prefix = isHost ? 'p1' : 'p2'; String prefix = isHost ? 'p1' : 'p2';
FirebaseFirestore.instance.collection('games').doc(roomCode).update({ FirebaseFirestore.instance.collection('games').doc(roomCode).update({
'${prefix}_joker': {'x': bx, 'y': by} '${prefix}_joker': {'x': bx, 'y': by, 'z': bz}
}); });
} else { } else {
target.hiddenJokerOwner = jokerTurn; target.hiddenJokerOwner = jokerTurn;
@ -360,14 +361,15 @@ class GameController extends ChangeNotifier {
} }
if (isSetupPhase) { if (isSetupPhase) {
// --- SINCRONIZZAZIONE JOLLY IN 3D ---
if (!isHost && data['p1_joker'] != null && !oppJokerPlaced) { if (!isHost && data['p1_joker'] != null && !oppJokerPlaced) {
int jx = data['p1_joker']['x']; int jy = data['p1_joker']['y']; 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).hiddenJokerOwner = Player.red; board.boxes.firstWhere((b) => b.x == jx && b.y == jy && b.z == jz).hiddenJokerOwner = Player.red;
oppJokerPlaced = true; _checkSetupComplete(); oppJokerPlaced = true; _checkSetupComplete();
} }
if (isHost && data['p2_joker'] != null && !oppJokerPlaced) { if (isHost && data['p2_joker'] != null && !oppJokerPlaced) {
int jx = data['p2_joker']['x']; int jy = data['p2_joker']['y']; 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).hiddenJokerOwner = Player.blue; board.boxes.firstWhere((b) => b.x == jx && b.y == jy && b.z == jz).hiddenJokerOwner = Player.blue;
oppJokerPlaced = true; _checkSetupComplete(); oppJokerPlaced = true; _checkSetupComplete();
} }
} }
@ -519,9 +521,7 @@ class GameController extends ChangeNotifier {
int myScore = isHost ? board.scoreRed : board.scoreBlue; int myScore = isHost ? board.scoreRed : board.scoreBlue;
int oppScore = isHost ? board.scoreBlue : board.scoreRed; int oppScore = isHost ? board.scoreBlue : board.scoreRed;
StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: oppName, myScore: myScore, oppScore: oppScore, isOnline: true); StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: oppName, myScore: myScore, oppScore: oppScore, isOnline: true);
if (isWin) StorageService.instance.updateQuestProgress(0, 1);
if (isWin) StorageService.instance.updateQuestProgress(0, 1); // Missione: Vinci Online
} else if (isVsCPU) { } else if (isVsCPU) {
int myScore = board.scoreRed; int cpuScore = board.scoreBlue; int myScore = board.scoreRed; int cpuScore = board.scoreBlue;
bool isWin = myScore > cpuScore; bool isWin = myScore > cpuScore;
@ -529,7 +529,7 @@ class GameController extends ChangeNotifier {
if (isWin) { if (isWin) {
StorageService.instance.addWin(); StorageService.instance.addWin();
StorageService.instance.updateQuestProgress(1, 1); // Missione: Vinci vs CPU StorageService.instance.updateQuestProgress(1, 1);
} else if (cpuScore > myScore) { } else if (cpuScore > myScore) {
StorageService.instance.addLoss(); 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); 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) { 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(); lastMatchXP = calculatedXP; StorageService.instance.addXP(calculatedXP); notifyListeners();

View file

@ -5,18 +5,20 @@
import 'dart:math'; import 'dart:math';
enum Player { red, blue, none } enum Player { red, blue, none }
enum BoxType { normal, gold, bomb, invisible, swap, ice, multiplier } // Aggiunti ice e multiplier enum BoxType { normal, gold, bomb, invisible, swap, ice, multiplier }
enum ArenaShape { classic, cross, donut, hourglass, chaos } // --- AGGIUNTA LA FORMA 3D ---
enum ArenaShape { classic, cross, donut, hourglass, chaos, test, pyramid3D }
class Dot { class Dot {
final int x; final int x;
final int y; final int y;
Dot(this.x, this.y); final int z; // --- NUOVO: ALTEZZA 3D ---
Dot(this.x, this.y, {this.z = 0});
@override @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 @override
int get hashCode => x.hashCode ^ y.hashCode; int get hashCode => x.hashCode ^ y.hashCode ^ z.hashCode;
} }
class Line { class Line {
@ -24,7 +26,7 @@ class Line {
final Dot p2; final Dot p2;
Player owner = Player.none; Player owner = Player.none;
bool isPlayable = false; bool isPlayable = false;
bool isIceCracked = false; // NUOVO: Stato per il blocco di ghiaccio bool isIceCracked = false;
Line(this.p1, this.p2); Line(this.p1, this.p2);
@ -34,6 +36,7 @@ class Line {
class Box { class Box {
final int x; final int x;
final int y; final int y;
final int z; // --- NUOVO: PIANO DELLA SCATOLA ---
Player owner = Player.none; Player owner = Player.none;
late Line top, bottom, left, right; late Line top, bottom, left, right;
BoxType type = BoxType.normal; BoxType type = BoxType.normal;
@ -42,7 +45,7 @@ class Box {
Player? hiddenJokerOwner; Player? hiddenJokerOwner;
bool isJokerRevealed = false; bool isJokerRevealed = false;
Box(this.x, this.y); Box(this.x, this.y, {this.z = 0});
bool isClosed() { bool isClosed() {
if (type == BoxType.invisible) return false; if (type == BoxType.invisible) return false;
@ -50,13 +53,13 @@ class Box {
} }
int getCalculatedValue(Player closer) { int getCalculatedValue(Player closer) {
if (hiddenJokerOwner != null) { if (hiddenJokerOwner != null) return (closer == hiddenJokerOwner) ? 2 : -1;
return (closer == hiddenJokerOwner) ? 2 : -1;
}
if (type == BoxType.gold) return 2; if (type == BoxType.gold) return 2;
if (type == BoxType.bomb) return -1; if (type == BoxType.bomb) return -1;
if (type == BoxType.swap || type == BoxType.ice || type == BoxType.multiplier) return 0; // Il moltiplicatore e il ghiaccio non danno punti base if (type == BoxType.swap || type == BoxType.ice || type == BoxType.multiplier) return 0;
return 1;
// --- NUOVO: NELLA PIRAMIDE I PIANI ALTI VALGONO DI PIÙ! ---
return 1 + z;
} }
} }
@ -77,10 +80,7 @@ class GameBoard {
int scoreRed = 0; int scoreRed = 0;
int scoreBlue = 0; int scoreBlue = 0;
bool isGameOver = false; bool isGameOver = false;
Line? lastMove; Line? lastMove;
// Variabili per il Moltiplicatore
bool redHasMultiplier = false; bool redHasMultiplier = false;
bool blueHasMultiplier = false; bool blueHasMultiplier = false;
@ -89,205 +89,111 @@ class GameBoard {
} }
void _generateBoard() { void _generateBoard() {
final random = seed != null ? Random(seed) : Random(); dots.clear(); lines.clear(); boxes.clear(); lastMove = null;
int chaosAlgorithm = random.nextInt(5);
if (shape == ArenaShape.chaos) { if (shape == ArenaShape.pyramid3D) {
columns = radius * 2 + 1; // --- LOGICA GENERAZIONE PIRAMIDE 3D ---
rows = (radius * 3) + 2; columns = 4; rows = 4; // Base 4x4
} else { int maxZ = 4; // 4 Piani (0, 1, 2, 3)
columns = radius * 2 + 1;
rows = radius * 2 + 1; 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);
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);
box.top = _getOrAddLine(tl, tr); box.bottom = _getOrAddLine(bl, br);
box.left = _getOrAddLine(tl, bl); box.right = _getOrAddLine(tr, br);
} }
}
dots.clear(); }
lines.clear(); _updatePyramidPlayability(); // Blocchiamo i piani superiori!
boxes.clear(); } else {
lastMove = null; // (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 (int y = 0; y < rows; y++) { for (int y = 0; y < rows; y++) {
for (int x = 0; x < columns; x++) { for (int x = 0; x < columns; x++) {
var box = Box(x, y); var box = Box(x, y);
bool isVisible = true; boxes.add(box);
Dot tl = _getOrAddDot(x, y, 0); Dot tr = _getOrAddDot(x + 1, y, 0);
if (shape != ArenaShape.chaos) { Dot bl = _getOrAddDot(x, y + 1, 0); Dot br = _getOrAddDot(x + 1, y + 1, 0);
int dx = (x - radius).abs(); box.top = _getOrAddLine(tl, tr); box.bottom = _getOrAddLine(bl, br);
int dy = (y - radius).abs(); box.left = _getOrAddLine(tl, bl); box.right = _getOrAddLine(tr, br);
isVisible = (dx + dy) <= radius; box.top.isPlayable = true; box.bottom.isPlayable = true; box.left.isPlayable = true; box.right.isPlayable = true;
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;
} }
} }
}
}
// 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 { } else {
double percentY = y / rows; // Cerca i 4 quadrati del piano di sotto che lo sorreggono
if (chaosAlgorithm == 0) { bool isSupported = true;
isVisible = (x % 2 == 0) && (random.nextDouble() > 0.15); for (int dy = 0; dy <= 1; dy++) {
} else if (chaosAlgorithm == 1) { for (int dx = 0; dx <= 1; dx++) {
double chance = 0.2 + (percentY * 0.7); var supportBox = boxes.where((b) => b.z == box.z - 1 && b.x == box.x + dx && b.y == box.y + dy).firstOrNull;
isVisible = random.nextDouble() < chance; if (supportBox == null || !supportBox.isClosed()) isSupported = false;
} 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);
} }
} }
if (box.owner == Player.none) {
for (var box in boxes) { box.top.isPlayable = isSupported; box.bottom.isPlayable = isSupported;
Dot tl = _getOrAddDot(box.x, box.y); box.left.isPlayable = isSupported; box.right.isPlayable = isSupported;
Dot tr = _getOrAddDot(box.x + 1, box.y); }
Dot bl = _getOrAddDot(box.x, box.y + 1);
Dot br = _getOrAddDot(box.x + 1, box.y + 1);
box.top = _getOrAddLine(tl, tr);
box.bottom = _getOrAddLine(bl, br);
box.left = _getOrAddLine(tl, bl);
box.right = _getOrAddLine(tr, br);
if (box.type != BoxType.invisible) {
box.top.isPlayable = true; box.bottom.isPlayable = true;
box.left.isPlayable = true; box.right.isPlayable = true;
} }
} }
} }
Dot _getOrAddDot(int x, int y) { Dot _getOrAddDot(int x, int y, int z) {
for (var dot in dots) { if (dot.x == x && dot.y == y) return dot; } for (var dot in dots) { if (dot.x == x && dot.y == y && dot.z == z) return dot; }
var newDot = Dot(x, y); var newDot = Dot(x, y, z: z); dots.add(newDot); return newDot;
dots.add(newDot); return newDot;
} }
Line _getOrAddLine(Dot a, Dot b) { Line _getOrAddLine(Dot a, Dot b) {
for (var line in lines) { if (line.connects(a, b)) return line; } for (var line in lines) { if (line.connects(a, b)) return line; }
var newLine = Line(a, b); var newLine = Line(a, b); lines.add(newLine); return newLine;
lines.add(newLine); return newLine;
} }
bool playMove(Line lineToPlay, {Player? forcedPlayer}) { bool playMove(Line lineToPlay, {Player? forcedPlayer}) {
if (isGameOver) return false; if (isGameOver) return false;
Player playerMakingMove = forcedPlayer ?? currentPlayer; Player playerMakingMove = forcedPlayer ?? currentPlayer;
Line? actualLine; Line? actualLine;
for (var l in lines) { for (var l in lines) { if (l.connects(lineToPlay.p1, lineToPlay.p2)) { actualLine = l; break; } }
if (l.connects(lineToPlay.p1, lineToPlay.p2)) { actualLine = l; break; }
}
if (actualLine == null || actualLine.owner != Player.none || !actualLine.isPlayable) return false; if (actualLine == null || actualLine.owner != Player.none || !actualLine.isPlayable) return false;
// --- LOGICA BLOCCO DI GHIACCIO ---
bool closesIce = false;
for (var box in boxes) {
if (box.type == BoxType.ice && box.owner == Player.none) {
int linesCount = 0;
if (box.top.owner != Player.none || box.top == actualLine) linesCount++;
if (box.bottom.owner != Player.none || box.bottom == actualLine) linesCount++;
if (box.left.owner != Player.none || box.left == actualLine) linesCount++;
if (box.right.owner != Player.none || box.right == actualLine) linesCount++;
if (linesCount == 4) closesIce = true;
}
}
if (closesIce && !actualLine.isIceCracked) {
actualLine.isIceCracked = true; // Si incrina ma non si chiude!
lastMove = actualLine;
if (forcedPlayer == null) currentPlayer = (currentPlayer == Player.red) ? Player.blue : Player.red;
else currentPlayer = (forcedPlayer == Player.red) ? Player.blue : Player.red;
return true; // Mossa valida, ma turno finito.
}
// Mossa normale o secondo colpo al ghiaccio
actualLine.isIceCracked = false;
actualLine.owner = playerMakingMove; actualLine.owner = playerMakingMove;
lastMove = actualLine; lastMove = actualLine;
bool scoredPoint = false; bool scoredPoint = false;
bool triggeredSwap = false;
for (var box in boxes) { for (var box in boxes) {
if (box.owner == Player.none && box.isClosed()) { if (box.owner == Player.none && box.isClosed()) {
box.owner = playerMakingMove; box.owner = playerMakingMove; scoredPoint = true;
scoredPoint = true;
if (box.hiddenJokerOwner != null) box.isJokerRevealed = true;
int points = box.getCalculatedValue(playerMakingMove); int points = box.getCalculatedValue(playerMakingMove);
if (playerMakingMove == Player.red) scoreRed += points; else scoreBlue += points;
// --- LOGICA MOLTIPLICATORE x2 ---
if (box.type == BoxType.multiplier) {
if (playerMakingMove == Player.red) redHasMultiplier = true;
else blueHasMultiplier = true;
} else if (points != 0) {
// Se la scatola chiusa punti e il giocatore ha un x2 attivo...
if (playerMakingMove == Player.red && redHasMultiplier) {
points *= 2;
redHasMultiplier = false; // Si consuma
} else if (playerMakingMove == Player.blue && blueHasMultiplier) {
points *= 2;
blueHasMultiplier = false; // Si consuma
} }
} }
if (playerMakingMove == Player.red) { scoreRed += points; } if (shape == ArenaShape.pyramid3D) _updatePyramidPlayability(); // Ricalcola chi può giocare sopra!
else { scoreBlue += points; }
if (box.type == BoxType.swap && box.hiddenJokerOwner == null) { if (lines.where((l) => l.isPlayable).every((l) => l.owner != Player.none)) isGameOver = true;
triggeredSwap = true; if (!scoredPoint && !isGameOver) currentPlayer = (playerMakingMove == Player.red) ? Player.blue : Player.red;
}
}
if (box.type == BoxType.invisible && !box.isRevealed) {
if (box.top.owner != Player.none && box.bottom.owner != Player.none &&
box.left.owner != Player.none && box.right.owner != Player.none) {
box.isRevealed = true;
}
}
}
if (triggeredSwap) {
int temp = scoreRed; scoreRed = scoreBlue; scoreBlue = temp;
}
if (lines.where((l) => l.isPlayable).every((l) => l.owner != Player.none)) { isGameOver = true; }
if (forcedPlayer == null) {
if (!scoredPoint && !isGameOver) { currentPlayer = (currentPlayer == Player.red) ? Player.blue : Player.red; }
else if (scoredPoint && !isGameOver) { currentPlayer = playerMakingMove; }
} else {
if (!scoredPoint && !isGameOver) { currentPlayer = (forcedPlayer == Player.red) ? Player.blue : Player.red; }
else { currentPlayer = forcedPlayer; }
}
return true; return true;
} }

View file

@ -4,6 +4,7 @@
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import '../../models/game_board.dart'; import '../../models/game_board.dart';
import '../../core/app_colors.dart'; import '../../core/app_colors.dart';
@ -20,6 +21,8 @@ class BoardPainter extends CustomPainter {
final Player myPlayer; final Player myPlayer;
final Player jokerTurn; final Player jokerTurn;
final double cameraAngle; // Angolazione della telecamera a 360 gradi!
BoardPainter({ BoardPainter({
required this.board, required this.board,
required this.theme, required this.theme,
@ -29,318 +32,231 @@ class BoardPainter extends CustomPainter {
required this.isSetupPhase, required this.isSetupPhase,
required this.myPlayer, required this.myPlayer,
required this.jokerTurn, 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 @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
if (themeType == AppThemeType.doodle) { if (board.shape == ArenaShape.pyramid3D) _paint3D(canvas, size);
final Paint paperGridPaint = Paint() else _paint2D(canvas, size);
..color = Colors.grey.withOpacity(0.3) }
..strokeWidth = 1.0
..style = PaintingStyle.stroke; void _paint3D(Canvas canvas, Size size) {
double visualZHeight = (size.width / 3.8) * 0.45;
double paperStep = 20.0; int maxZ = 4;
for (double i = 0; i <= size.width; i += paperStep) { Set<Line> drawnLines = {};
canvas.drawLine(Offset(i, 0), Offset(i, size.height), paperGridPaint);
// 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();
// Ordiniamo dal fondo allo schermo
currentLevelBoxes.sort((a, b) => getDepth(a).compareTo(getDepth(b)));
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);
}
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);
// 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);
// --- IL CUBO SOLIDO ---
if (isOwned) {
// Calcoliamo i 4 vertici del TETTO (Z + 1)
List<Offset> 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),
];
// 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];
Color baseColor = box.owner == Player.red ? theme.playerRed : theme.playerBlue;
// 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);
// 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);
// 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);
_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);
}
} }
for (double i = 0; i <= size.height; i += paperStep) {
canvas.drawLine(Offset(0, i), Offset(size.width, i), paperGridPaint);
} }
} }
int gridPoints = board.columns + 1; void _paint2D(Canvas canvas, Size size) {
double spacing = size.width / gridPoints;
double offset = spacing / 2;
Offset getScreenPos(int x, int y) => Offset(x * spacing + offset, y * spacing + offset);
for (var box in board.boxes) { for (var box in board.boxes) {
Offset p1 = getScreenPos(box.x, box.y); if (box.type == BoxType.invisible) continue;
Offset p2 = getScreenPos(box.x + 1, box.y + 1); Offset p1 = projectLogical(box.top.p1.x.toDouble(), box.top.p1.y.toDouble(), 0, size);
Rect rect = Rect.fromPoints(p1, p2); 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);
if (box.type == BoxType.invisible) { Offset p4 = projectLogical(box.bottom.p1.x.toDouble(), box.bottom.p1.y.toDouble(), 0, size);
if (box.isRevealed) { Path poly = Path()..moveTo(p1.dx, p1.dy)..lineTo(p2.dx, p2.dy)..lineTo(p3.dx, p3.dy)..lineTo(p4.dx, p4.dy)..close();
_drawIconInBox(canvas, rect, ThemeIcons.block(themeType), Colors.grey.shade500);
}
continue;
}
// Sfondo azzurrino se è di ghiaccio (anche prima di chiuderla)
if (box.type == BoxType.ice && box.owner == Player.none) {
canvas.drawRect(rect.deflate(2.0), Paint()..color = Colors.cyanAccent.withOpacity(0.05)..style=PaintingStyle.fill);
}
if (box.owner != Player.none) { if (box.owner != Player.none) {
final boxPaint = Paint() Color c = box.owner == Player.red ? theme.playerRed : theme.playerBlue;
..style = PaintingStyle.fill canvas.drawPath(poly, Paint()..color = c.withOpacity(0.85)..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));
}
}
}
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);
} }
_drawBoxIcon(canvas, poly, box);
} }
for (var line in board.lines) { for (var line in board.lines) {
if (!line.isPlayable) continue; 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);
Offset p1 = getScreenPos(line.p1.x, line.p1.y); if (line.isIceCracked) { _drawCrackedIceLine(canvas, p1, p2, blinkValue); continue; }
Offset p2 = getScreenPos(line.p2.x, line.p2.y); 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));
// --- DISEGNO DELLA LINEA "INCRINATA" DAL GHIACCIO --- canvas.drawLine(p1, p2, Paint()..color = lineColor..strokeWidth = (line.owner == Player.none ? 3.0 : 6.0)..strokeCap = StrokeCap.round);
if (line.isIceCracked) {
_drawCrackedIceLine(canvas, p1, p2, blinkValue);
continue; // Non ha ancora un proprietario, passiamo alla prossima!
} }
bool isLastMove = (line == board.lastMove); for (var dot in board.dots) {
Color lineColor = line.owner == Player.none bool isVisible = board.lines.any((l) => (l.p1 == dot || l.p2 == dot) && l.isPlayable);
? theme.gridLine.withOpacity(0.4) if (isVisible) canvas.drawCircle(projectLogical(dot.x.toDouble(), dot.y.toDouble(), 0, size), 4.0, Paint()..color = theme.text.withOpacity(0.8));
: (line.owner == Player.red ? theme.playerRed : theme.playerBlue);
if (isLastMove && line.owner != Player.none && themeType != AppThemeType.wood && themeType != AppThemeType.cyberpunk && themeType != AppThemeType.arcade && themeType != AppThemeType.grimorio) {
canvas.drawLine(p1, p2, Paint()..color = Colors.white.withOpacity(blinkValue * 0.5)..strokeWidth = 16.0..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6.0));
}
if (themeType == AppThemeType.wood) {
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);
}
} 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; void _drawBoxIcon(Canvas canvas, Path face, Box box) {
Set<Dot> activeDots = {}; if (box.type == BoxType.gold) _drawIconOnPath(canvas, face, FontAwesomeIcons.crown, Colors.amber);
for (var line in board.lines) { else if (box.type == BoxType.bomb) _drawIconOnPath(canvas, face, FontAwesomeIcons.skull, Colors.redAccent);
if (line.isPlayable) { else if (box.type == BoxType.swap) _drawIconOnPath(canvas, face, FontAwesomeIcons.arrowsRotate, Colors.purpleAccent);
activeDots.add(line.p1); activeDots.add(line.p2); 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);
} }
} }
for (var dot in activeDots) { void _drawIconOnPath(Canvas canvas, Path path, IconData icon, Color color) {
Offset pos = getScreenPos(dot.x, dot.y); Rect bounds = path.getBounds();
if (themeType == AppThemeType.wood) { 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();
canvas.drawCircle(pos, 3.5, dotPaint..color = const Color(0xFF3E2723).withOpacity(0.2)); tp.paint(canvas, Offset(bounds.center.dx - tp.width / 2, bounds.center.dy - tp.height / 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 _drawCrackedIceLine(Canvas canvas, Offset p1, Offset p2, double blink) { void _drawCrackedIceLine(Canvas canvas, Offset p1, Offset p2, double blink) {
Paint crackPaint = Paint() 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);
..color = Colors.cyanAccent.withOpacity(0.6 + (0.4 * blink))
..strokeWidth = 3.0
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round
..maskFilter = const MaskFilter.blur(BlurStyle.solid, 2.0);
// Effetto linea frammentata
canvas.drawLine(p1, p2, Paint()..color = Colors.cyan.withOpacity(0.2)..strokeWidth=6.0); canvas.drawLine(p1, p2, Paint()..color = Colors.cyan.withOpacity(0.2)..strokeWidth=6.0);
double dx = p2.dx - p1.dx; double dy = p2.dy - p1.dy;
Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy); double len = sqrt(dx * dx + dy * dy);
double len = dir.length; Vector2 ndir = dir.normalized(); Vector2 perp = Vector2(-ndir.y, ndir.x); if (len == 0) return;
double ndx = dx / len; double ndy = dy / len;
Path crack = Path()..moveTo(p1.dx, p1.dy); Path crack = Path()..moveTo(p1.dx, p1.dy);
int zigzags = 6; for (int i = 1; i < 6; i++) {
for (int i=1; i<zigzags; i++) {
double d = len * (i / zigzags);
Offset basePt = Offset(p1.dx + ndir.x * d, p1.dy + ndir.y * d);
double offset = (i % 2 == 0 ? 3.0 : -3.0); double offset = (i % 2 == 0 ? 3.0 : -3.0);
crack.lineTo(basePt.dx + perp.x * offset, basePt.dy + perp.y * offset); crack.lineTo(p1.dx + ndx * (len * (i / 6)) + (-ndy) * offset, p1.dy + ndy * (len * (i / 6)) + ndx * offset);
} }
crack.lineTo(p2.dx, p2.dy); crack.lineTo(p2.dx, p2.dy);
canvas.drawPath(crack, crackPaint); canvas.drawPath(crack, crackPaint);
} }
void _drawArcadeBox(Canvas canvas, Rect rect, Color color) {
double pixelSize = 4.0; Paint paint = Paint()..color = color.withOpacity(0.9)..style = PaintingStyle.fill;
for (double y = rect.top; y < rect.bottom; y += pixelSize) {
for (double x = rect.left; x < rect.right; x += pixelSize) {
int xi = ((x - rect.left) / pixelSize).floor(); int yi = ((y - rect.top) / pixelSize).floor();
if ((xi + yi) % 2 == 0) canvas.drawRect(Rect.fromLTWH(x, y, pixelSize, pixelSize), paint);
}
}
canvas.drawRect(rect.deflate(2.0), Paint()..color = Colors.white.withOpacity(0.4)..style = PaintingStyle.stroke..strokeWidth = 2.0);
}
void _drawGrimorioBox(Canvas canvas, Rect rect, Color color) {
canvas.drawRect(rect, Paint()..color = color.withOpacity(0.15)..style=PaintingStyle.fill);
Offset c = rect.center; double r = rect.width * 0.35;
Paint linePaint = Paint()..color = color.withOpacity(0.8)..style = PaintingStyle.stroke..strokeWidth = 1.5..maskFilter = const MaskFilter.blur(BlurStyle.solid, 1.0);
canvas.drawCircle(c, r, linePaint); canvas.drawCircle(c, r * 0.8, linePaint..strokeWidth = 0.5);
Path p = Path();
for(int i=0; i<3; i++) {
double a = -pi/2 + i * 2*pi/3; Offset pt = Offset(c.dx + r*cos(a), c.dy + r*sin(a));
if(i==0) p.moveTo(pt.dx, pt.dy); else p.lineTo(pt.dx, pt.dy);
}
p.close(); canvas.drawPath(p, linePaint..strokeWidth = 1.0);
}
void _drawArcadeLine(Canvas canvas, Offset p1, Offset p2, Color color, bool isConquered, {bool isLastMove = false, double blinkValue = 0.0}) {
double pixelSize = 6.0; Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy); double len = dir.length; Vector2 ndir = dir.normalized();
Paint paint = Paint()..color = isConquered ? color : color.withOpacity(0.15)..style = PaintingStyle.fill;
Paint highlight = Paint()..color = Colors.white.withOpacity(0.6)..style = PaintingStyle.fill;
for(double d = 0; d <= len; d += pixelSize + 1.0) {
Offset pt = Offset(p1.dx + ndir.x * d, p1.dy + ndir.y * d);
canvas.drawRect(Rect.fromCenter(center: pt, width: pixelSize, height: pixelSize), paint);
if (isConquered && (d / (pixelSize+1.0)).floor() % 3 == 0) canvas.drawRect(Rect.fromCenter(center: pt - const Offset(1,1), width: pixelSize*0.4, height: pixelSize*0.4), highlight);
}
if (isLastMove && isConquered) canvas.drawRect(Rect.fromPoints(p1, p2).inflate(4.0), Paint()..color = Colors.white.withOpacity(blinkValue*0.4)..style=PaintingStyle.stroke..strokeWidth=2.0);
}
void _drawGrimorioLine(Canvas canvas, Offset p1, Offset p2, Color color, bool isConquered, {bool isLastMove = false, double blinkValue = 0.0}) {
if (!isConquered) { canvas.drawLine(p1, p2, Paint()..color = color.withOpacity(0.15)..strokeWidth = 2.0..strokeCap = StrokeCap.round); return; }
canvas.drawLine(p1, p2, Paint()..color = color.withOpacity(0.6)..strokeWidth = 5.0..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4.0));
canvas.drawLine(p1, p2, Paint()..color = Colors.white.withOpacity(0.7)..strokeWidth = 1.5..strokeCap = StrokeCap.round);
int seed = (p1.dx * 1000 + p1.dy).toInt(); Random rand = Random(seed);
Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy); double len = dir.length; Vector2 ndir = dir.normalized(); Vector2 perp = Vector2(-ndir.y, ndir.x);
Path thread1 = Path(); Path thread2 = Path(); int segments = 15; double step = len / segments;
double phaseOffset = (isLastMove ? blinkValue * pi * 4 : 0) + rand.nextDouble()*pi;
for(int i = 0; i <= segments; i++) {
double d = i * step; Offset basePt = Offset(p1.dx + ndir.x * d, p1.dy + ndir.y * d);
double amplitude = 3.5; double wave1 = sin(d * 0.15 + phaseOffset) * amplitude; double wave2 = cos(d * 0.15 + phaseOffset) * amplitude;
Offset pt1 = basePt + Offset(perp.x * wave1, perp.y * wave1); Offset pt2 = basePt + Offset(perp.x * wave2, perp.y * wave2);
if (i == 0) { thread1.moveTo(pt1.dx, pt1.dy); thread2.moveTo(pt2.dx, pt2.dy); } else { thread1.lineTo(pt1.dx, pt1.dy); thread2.lineTo(pt2.dx, pt2.dy); }
}
Paint threadPaint = Paint()..color = color.withOpacity(0.9)..style = PaintingStyle.stroke..strokeWidth = 1.5..maskFilter = const MaskFilter.blur(BlurStyle.solid, 1.0);
canvas.drawPath(thread1, threadPaint); canvas.drawPath(thread2, threadPaint..color = Colors.white.withOpacity(0.5));
}
void _drawFlameBox(Canvas canvas, Rect baseRect, bool isRed) {
final rand = Random((baseRect.left + baseRect.top).toInt());
Offset center = baseRect.center; double w = baseRect.width * 0.35; double h = baseRect.height * 0.55; Offset bottomCenter = Offset(center.dx, center.dy + h * 0.5);
Color outerColor = isRed ? Colors.red.shade600.withOpacity(0.85) : Colors.blue.shade700.withOpacity(0.85); Color midColor = isRed ? Colors.orangeAccent : Colors.lightBlueAccent; Color coreColor = isRed ? Colors.yellowAccent : Colors.white;
canvas.drawOval(Rect.fromCenter(center: bottomCenter, width: w * 1.5, height: w * 0.5), Paint()..color = Colors.black.withOpacity(0.4)..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4.0));
void drawFlameLayer(double scale, Color color, double tipOffsetX) {
Path path = Path(); double fw = w * scale; double fh = h * scale;
path.moveTo(bottomCenter.dx, bottomCenter.dy); path.cubicTo(bottomCenter.dx + fw, bottomCenter.dy, bottomCenter.dx + fw * 0.8, bottomCenter.dy - fh * 0.6, bottomCenter.dx + tipOffsetX, bottomCenter.dy - fh); path.cubicTo(bottomCenter.dx - fw * 0.8, bottomCenter.dy - fh * 0.6, bottomCenter.dx - fw, bottomCenter.dy, bottomCenter.dx, bottomCenter.dy);
canvas.drawPath(path, Paint()..color = color..style = PaintingStyle.fill..maskFilter = const MaskFilter.blur(BlurStyle.normal, 1.5));
}
double randomTipX = (rand.nextDouble() - 0.5) * w * 0.8; drawFlameLayer(1.0, outerColor, randomTipX); drawFlameLayer(0.65, midColor.withOpacity(0.9), randomTipX * 0.6); drawFlameLayer(0.35, coreColor.withOpacity(0.9), randomTipX * 0.2);
}
void _drawScribbleBox(Canvas canvas, Rect baseRect, Color color) {
final rand = Random((baseRect.left + baseRect.top).toInt());
final paint = Paint()..color = color.withOpacity(0.85)..style = PaintingStyle.stroke..strokeWidth = 3.5..strokeCap = StrokeCap.round..strokeJoin = StrokeJoin.round;
final path = Path(); Rect rect = baseRect.deflate(4.0); int numZigs = 15 + rand.nextInt(6); double stepY = rect.height / numZigs;
path.moveTo(rect.left + rand.nextDouble() * 5, rect.top + rand.nextDouble() * 5);
for (int i = 1; i <= numZigs; i++) { double targetX = (i % 2 != 0) ? rect.right + (rand.nextDouble() * 4 - 2) : rect.left + (rand.nextDouble() * 4 - 2); double targetY = rect.top + stepY * i + (rand.nextDouble() - 0.5) * 3; double ctrlX = rect.center.dx + (rand.nextDouble() - 0.5) * 20; double ctrlY = targetY - stepY / 2; path.quadraticBezierTo(ctrlX, ctrlY, targetX, targetY); }
canvas.drawPath(path, paint);
}
void _drawRealisticMatch(Canvas canvas, Offset p1, Offset p2, Color headColor, {bool isLastMove = false, double blinkValue = 0.0}) {
int seed = (p1.dx * 1000 + p1.dy).toInt(); Random rand = Random(seed); Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy).normalized(); double shrink = 8.0; Offset start = Offset(p1.dx + dir.x * shrink, p1.dy + dir.y * shrink); Offset end = Offset(p2.dx - dir.x * shrink, p2.dy - dir.y * shrink); start += Offset(rand.nextDouble() * 4 - 2, rand.nextDouble() * 4 - 2); end += Offset(rand.nextDouble() * 4 - 2, rand.nextDouble() * 4 - 2); bool headAtEnd = rand.nextBool(); Offset headPos = headAtEnd ? end : start; Offset tailPos = headAtEnd ? start : end; Vector2 matchDir = Vector2(headPos.dx - tailPos.dx, headPos.dy - tailPos.dy).normalized();
canvas.drawLine(tailPos + const Offset(4, 4), headPos + const Offset(4, 4), Paint()..color = Colors.black.withOpacity(0.6)..strokeWidth = 7.0..strokeCap = StrokeCap.round);
if (isLastMove) { canvas.drawCircle(headPos, 8.0 + (blinkValue * 6.0), Paint()..color = Colors.orangeAccent.withOpacity(0.6 * blinkValue)..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6.0)); }
canvas.drawLine(tailPos, headPos, Paint()..color = const Color(0xFF6D4C41)..strokeWidth = 7.0..strokeCap = StrokeCap.round); canvas.drawLine(tailPos, headPos, Paint()..color = const Color(0xFFEDC498)..strokeWidth = 4.0..strokeCap = StrokeCap.round); Offset burnPos = Offset(headPos.dx - matchDir.x * 8, headPos.dy - matchDir.y * 8); canvas.drawLine(burnPos, headPos, Paint()..color = const Color(0xFF2E1A14)..strokeWidth = 6.0..strokeCap = StrokeCap.round);
canvas.save(); canvas.translate(headPos.dx, headPos.dy); double angle = atan2(matchDir.y, matchDir.x); canvas.rotate(angle); Rect headOval = Rect.fromCenter(center: Offset.zero, width: 18.0, height: 13.0); canvas.drawOval(headOval.shift(const Offset(1, 2)), Paint()..color = Colors.black.withOpacity(0.6)); canvas.drawOval(headOval, Paint()..color = headColor); canvas.restore();
}
void _drawNeonLine(Canvas canvas, Offset p1, Offset p2, Color color, bool isConquered, {bool isLastMove = false, double blinkValue = 0.0}) {
double mainWidth = isConquered ? (isLastMove ? 6.0 + (blinkValue * 3.0) : 6.0) : 3.0; Color coreColor = isConquered ? (isLastMove ? Color.lerp(Colors.white, color, 1.0 - blinkValue)! : Colors.white.withOpacity(0.9)) : color.withOpacity(0.6);
canvas.drawLine(p1, p2, Paint()..color = color.withOpacity(isConquered ? (isLastMove ? 0.4 + (0.4 * blinkValue) : 0.4) : 0.2)..strokeWidth = mainWidth * 4..strokeCap = StrokeCap.round..maskFilter = MaskFilter.blur(BlurStyle.normal, isConquered ? 12.0 : 6.0));
if (isConquered) { canvas.drawLine(p1, p2, Paint()..color = color.withOpacity(isLastMove ? 0.7 + (0.3 * blinkValue) : 0.7)..strokeWidth = mainWidth * 2..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6.0)); }
canvas.drawLine(p1, p2, Paint()..color = coreColor..strokeWidth = mainWidth..strokeCap = StrokeCap.round);
}
void _drawWobblyLine(Canvas canvas, Offset p1, Offset p2, Color color, bool isConquered, {bool isLastMove = false, double blinkValue = 0.0}) {
final random = Random((p1.dx + p1.dy + p2.dx + p2.dy).toInt()); final dx = p2.dx - p1.dx; final dy = p2.dy - p1.dy;
double strokeW = isConquered ? (isLastMove ? 4.5 + (2.0 * blinkValue) : 4.5) : 2.0;
final basePaint = Paint()..color = color..strokeWidth = strokeW..style = PaintingStyle.stroke..strokeCap = StrokeCap.round;
final mid1 = Offset(p1.dx + dx / 2 + (random.nextDouble() - 0.5) * 8, p1.dy + dy / 2 + (random.nextDouble() - 0.5) * 8); canvas.drawPath(Path()..moveTo(p1.dx, p1.dy)..quadraticBezierTo(mid1.dx, mid1.dy, p2.dx, p2.dy), basePaint);
final mid2 = Offset(p1.dx + dx / 2 + (random.nextDouble() - 0.5) * 6, p1.dy + dy / 2 + (random.nextDouble() - 0.5) * 6); canvas.drawPath(Path()..moveTo(p1.dx, p1.dy)..quadraticBezierTo(mid2.dx, mid2.dy, p2.dx, p2.dy), basePaint..strokeWidth = strokeW * 0.5..color = color.withOpacity(0.8));
}
@override bool shouldRepaint(covariant BoardPainter oldDelegate) => true; @override bool shouldRepaint(covariant BoardPainter oldDelegate) => true;
} }
class Vector2 {
final double x, y; Vector2(this.x, this.y); double get length => sqrt(x * x + y * y);
Vector2 normalized() { double l = length; return l == 0 ? Vector2(0, 0) : Vector2(x / l, y / l); }
}

View file

@ -5,6 +5,7 @@
import 'dart:ui'; import 'dart:ui';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../logic/game_controller.dart'; import '../../logic/game_controller.dart';
@ -40,11 +41,12 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
bool _gameOverDialogShown = false; bool _gameOverDialogShown = false;
bool _opponentLeftDialogShown = false; bool _opponentLeftDialogShown = false;
// Variabili per coprire il posizionamento del Jolly in Locale
bool _hideJokerMessage = false; bool _hideJokerMessage = false;
bool _wasSetupPhase = false; bool _wasSetupPhase = false;
Player _lastJokerTurn = Player.red; Player _lastJokerTurn = Player.red;
double _cameraAngle = 0.0; // La nostra telecamera fluida a 360 gradi
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -56,22 +58,14 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
void _showGameOverDialog(BuildContext context, GameController game, ThemeColors theme, AppThemeType themeType) { void _showGameOverDialog(BuildContext context, GameController game, ThemeColors theme, AppThemeType themeType) {
_gameOverDialogShown = true; _gameOverDialogShown = true;
showDialog( showDialog(
barrierDismissible: false, barrierDismissible: false, context: context,
context: context,
builder: (dialogContext) => Consumer<GameController>( builder: (dialogContext) => Consumer<GameController>(
builder: (context, controller, child) { builder: (context, controller, child) {
if (!controller.isGameOver) { if (!controller.isGameOver) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) { if (_gameOverDialogShown) { _gameOverDialogShown = false; if (Navigator.canPop(dialogContext)) Navigator.pop(dialogContext); } });
if (_gameOverDialogShown) {
_gameOverDialogShown = false;
if (Navigator.canPop(dialogContext)) Navigator.pop(dialogContext);
}
});
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
int red = controller.board.scoreRed; int blue = controller.board.scoreBlue; int red = controller.board.scoreRed; int blue = controller.board.scoreBlue;
bool playerBeatCPU = controller.isVsCPU && red > blue; bool playerBeatCPU = controller.isVsCPU && red > blue;
String nameRed = controller.isOnline ? controller.onlineHostName.toUpperCase() : "TU"; String nameRed = controller.isOnline ? controller.onlineHostName.toUpperCase() : "TU";
@ -84,8 +78,7 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
else { winnerText = "PAREGGIO!"; winnerColor = theme.text; } else { winnerText = "PAREGGIO!"; winnerColor = theme.text; }
return AlertDialog( return AlertDialog(
backgroundColor: theme.background, backgroundColor: theme.background, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: BorderSide(color: winnerColor.withOpacity(0.5), width: 2)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: BorderSide(color: winnerColor.withOpacity(0.5), width: 2)),
title: Text("FINE PARTITA", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 22))), title: Text("FINE PARTITA", textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 22))),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -93,8 +86,7 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
Text(winnerText, textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: winnerColor))), Text(winnerText, textAlign: TextAlign.center, style: _getTextStyle(themeType, TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: winnerColor))),
const SizedBox(height: 20), const SizedBox(height: 20),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), decoration: BoxDecoration(color: theme.text.withOpacity(0.05), borderRadius: BorderRadius.circular(15)),
decoration: BoxDecoration(color: theme.text.withOpacity(0.05), borderRadius: BorderRadius.circular(15)),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -104,70 +96,39 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
], ],
), ),
), ),
if (controller.lastMatchXP > 0) ...[ if (controller.lastMatchXP > 0) ...[
const SizedBox(height: 15), const SizedBox(height: 15),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
decoration: BoxDecoration( 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)] : []),
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))), child: Text("+ ${controller.lastMatchXP} XP", style: _getTextStyle(themeType, const TextStyle(color: Colors.greenAccent, fontWeight: FontWeight.w900, fontSize: 16, letterSpacing: 1.5))),
), ),
], ],
if (controller.isVsCPU) ...[ if (controller.isVsCPU) ...[
const SizedBox(height: 15), const SizedBox(height: 15),
Text("Difficoltà CPU: Livello ${controller.cpuLevel}", style: _getTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: theme.text.withOpacity(0.7)))), Text("Difficoltà CPU: Livello ${controller.cpuLevel}", style: _getTextStyle(themeType, TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: theme.text.withOpacity(0.7)))),
], ],
if (controller.isOnline) ...[ if (controller.isOnline) ...[
const SizedBox(height: 20), const SizedBox(height: 20),
if (controller.rematchRequested && !controller.opponentWantsRematch) if (controller.rematchRequested && !controller.opponentWantsRematch) Text("In attesa di $nameBlue...", style: _getTextStyle(themeType, const TextStyle(color: Colors.amber, fontWeight: FontWeight.bold, fontStyle: FontStyle.italic))),
Text("In attesa di $nameBlue...", style: _getTextStyle(themeType, const TextStyle(color: Colors.amber, fontWeight: FontWeight.bold, fontStyle: FontStyle.italic))), if (controller.opponentWantsRematch && !controller.rematchRequested) Text("$nameBlue vuole la rivincita!", style: _getTextStyle(themeType, const TextStyle(color: Colors.greenAccent, fontWeight: FontWeight.bold))),
if (controller.opponentWantsRematch && !controller.rematchRequested) if (controller.rematchRequested && controller.opponentWantsRematch) Text("Avvio nuova partita...", style: _getTextStyle(themeType, const TextStyle(color: Colors.green, fontWeight: FontWeight.bold))),
Text("$nameBlue vuole la rivincita!", style: _getTextStyle(themeType, const TextStyle(color: Colors.greenAccent, fontWeight: FontWeight.bold))),
if (controller.rematchRequested && controller.opponentWantsRematch)
Text("Avvio nuova partita...", style: _getTextStyle(themeType, const TextStyle(color: Colors.green, fontWeight: FontWeight.bold))),
] ]
], ],
), ),
actionsPadding: const EdgeInsets.only(left: 20, right: 20, bottom: 20, top: 10), actionsPadding: const EdgeInsets.only(left: 20, right: 20, bottom: 20, top: 10), actionsAlignment: MainAxisAlignment.center,
actionsAlignment: MainAxisAlignment.center,
actions: [ actions: [
Column( Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
if (playerBeatCPU) if (playerBeatCPU)
ElevatedButton( ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: winnerColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5), onPressed: () { controller.increaseLevelAndRestart(); }, child: Text("PROSSIMO LIVELLO ➔", style: _getTextStyle(themeType, const TextStyle(fontWeight: FontWeight.bold, fontSize: 16))))
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) else if (controller.isOnline)
ElevatedButton( ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: controller.rematchRequested ? Colors.grey : (winnerColor == theme.text ? theme.playerBlue : winnerColor), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5), 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))))
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 else
ElevatedButton( ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: winnerColor == theme.text ? theme.playerBlue : winnerColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5), 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)))),
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), const SizedBox(height: 12),
OutlinedButton( OutlinedButton(style: OutlinedButton.styleFrom(foregroundColor: theme.text, side: BorderSide(color: theme.text.withOpacity(0.3), width: 2), padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), 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)))),
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<GameScreen> with TickerProviderStateMixin {
} }
Widget _buildThemedJokerMessage(ThemeColors theme, AppThemeType themeType, GameController gameController) { Widget _buildThemedJokerMessage(ThemeColors theme, AppThemeType themeType, GameController gameController) {
String titleText = ""; String titleText = ""; String subtitleText = "";
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) { 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)))]));
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( 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);
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 25), 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);
child: Column( 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);
mainAxisSize: MainAxisSize.min, 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);
children: [ 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);
Icon(ThemeIcons.joker(themeType), color: themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? Colors.yellowAccent : theme.playerBlue, size: 50), 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);
const SizedBox(height: 15),
Text(
titleText,
textAlign: TextAlign.center,
style: _getTextStyle(themeType, TextStyle(
color: themeType == AppThemeType.doodle ? Colors.black87 : theme.text,
fontSize: 20,
fontWeight: FontWeight.bold,
)),
),
const SizedBox(height: 25),
Text(
subtitleText,
textAlign: TextAlign.center,
style: _getTextStyle(themeType, TextStyle(
color: themeType == AppThemeType.doodle ? Colors.black54 : theme.text.withOpacity(0.6),
fontSize: 12,
height: 1.5
)),
),
],
),
);
if (themeType == AppThemeType.cyberpunk) {
return Container(decoration: BoxDecoration(color: Colors.black.withOpacity(0.9), borderRadius: BorderRadius.circular(20), border: Border.all(color: Colors.yellowAccent, width: 2), boxShadow: [const BoxShadow(color: Colors.yellowAccent, blurRadius: 15, spreadRadius: 0)]), child: content);
} else if (themeType == AppThemeType.doodle) {
return Container(decoration: BoxDecoration(color: const Color(0xFFF9F9F9), borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.black87, width: 3), boxShadow: const [BoxShadow(color: Colors.black26, offset: Offset(6, 6))]), child: content);
} else if (themeType == AppThemeType.wood) {
return Container(decoration: BoxDecoration(color: const Color(0xFF5D4037), borderRadius: BorderRadius.circular(15), border: Border.all(color: const Color(0xFF3E2723), width: 4), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.6), blurRadius: 15, offset: const Offset(0, 8))]), child: content);
} else if (themeType == AppThemeType.arcade) {
return Container(decoration: BoxDecoration(color: Colors.black, borderRadius: BorderRadius.zero, border: Border.all(color: Colors.greenAccent, width: 4)), child: content);
} else if (themeType == AppThemeType.grimorio) {
return Container(decoration: BoxDecoration(color: const Color(0xFF2C1E3D), borderRadius: BorderRadius.circular(30), border: Border.all(color: const Color(0xFFBCAAA4), width: 3), boxShadow: [BoxShadow(color: Colors.deepPurpleAccent.withOpacity(0.5), blurRadius: 20, spreadRadius: 5)]), child: content);
} else {
return Container(decoration: BoxDecoration(color: theme.background, borderRadius: BorderRadius.circular(20), border: Border.all(color: theme.gridLine, width: 2), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.15), blurRadius: 20, offset: const Offset(0, 10))]), child: content);
}
} }
@override @override
@ -246,39 +161,14 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
final theme = themeManager.currentColors; final theme = themeManager.currentColors;
final gameController = context.watch<GameController>(); final gameController = context.watch<GameController>();
// --- LOGICA CAMBIO TURNO E SCHERMATA JOLLY --- if (gameController.isSetupPhase && !_wasSetupPhase) { _hideJokerMessage = false; _lastJokerTurn = Player.red; }
if (gameController.isSetupPhase && !_wasSetupPhase) { else if (gameController.isSetupPhase && gameController.jokerTurn != _lastJokerTurn) { _hideJokerMessage = false; _lastJokerTurn = gameController.jokerTurn; }
// È appena iniziata una nuova partita
_hideJokerMessage = false;
_lastJokerTurn = Player.red;
} else if (gameController.isSetupPhase && gameController.jokerTurn != _lastJokerTurn) {
// È cambiato il turno durante il setup (in modalità locale), rifacciamo apparire la copertura
_hideJokerMessage = false;
_lastJokerTurn = gameController.jokerTurn;
}
_wasSetupPhase = gameController.isSetupPhase; _wasSetupPhase = gameController.isSetupPhase;
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (gameController.opponentLeft && !_opponentLeftDialogShown) { if (gameController.opponentLeft && !_opponentLeftDialogShown) {
_opponentLeftDialogShown = true; _opponentLeftDialogShown = true;
showDialog( 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))))]));
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) { } else if (gameController.board.isGameOver && !_gameOverDialogShown) {
_showGameOverDialog(context, gameController, theme, themeType); _showGameOverDialog(context, gameController, theme, themeType);
} }
@ -293,21 +183,7 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
Widget emojiBar = const SizedBox(); Widget emojiBar = const SizedBox();
if (gameController.isOnline && !gameController.isGameOver) { if (gameController.isOnline && !gameController.isGameOver) {
final List<String> emojis = ['😂', '😡', '😱', '🥳', '👀']; final List<String> emojis = ['😂', '😡', '😱', '🥳', '👀'];
emojiBar = Container( 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()));
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( Widget gameContent = SafeArea(
@ -334,7 +210,14 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
width: actualWidth, height: actualHeight, width: actualWidth, height: actualHeight,
child: GestureDetector( child: GestureDetector(
behavior: HitTestBehavior.opaque, 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( child: AnimatedBuilder(
animation: _blinkController, animation: _blinkController,
builder: (context, child) { builder: (context, child) {
@ -345,6 +228,7 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
blinkValue: _blinkController.value, isOnline: gameController.isOnline, blinkValue: _blinkController.value, isOnline: gameController.isOnline,
isVsCPU: gameController.isVsCPU, isSetupPhase: gameController.isSetupPhase, isVsCPU: gameController.isVsCPU, isSetupPhase: gameController.isSetupPhase,
myPlayer: gameController.myPlayer, jokerTurn: gameController.jokerTurn, myPlayer: gameController.myPlayer, jokerTurn: gameController.jokerTurn,
cameraAngle: _cameraAngle,
), ),
); );
} }
@ -363,46 +247,28 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
if (gameController.isVsCPU) if (gameController.isVsCPU)
Container( 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)))]))
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 else
emojiBar, emojiBar,
Container( 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))))),
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) if (gameController.board.shape == ArenaShape.pyramid3D)
Positioned(top: 80, left: gameController.isHost ? 30 : null, right: gameController.isHost ? null : 30, child: _BouncingEmoji(emoji: gameController.myReaction!)), 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.opponentReaction != null)
Positioned(top: 80, left: !gameController.isHost ? 30 : null, right: !gameController.isHost ? null : 30, child: _BouncingEmoji(emoji: gameController.opponentReaction!)), 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( return PopScope(
canPop: true, canPop: true, onPopInvoked: (didPop) { gameController.disconnectOnlineGame(); },
onPopInvoked: (didPop) { gameController.disconnectOnlineGame(); },
child: Scaffold( child: Scaffold(
backgroundColor: bgImage != null ? Colors.transparent : theme.background, backgroundColor: bgImage != null ? Colors.transparent : theme.background,
body: CustomPaint( body: CustomPaint(
@ -411,36 +277,11 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
decoration: bgImage != null ? BoxDecoration(image: DecorationImage(image: AssetImage(bgImage), fit: BoxFit.cover, colorFilter: themeType == AppThemeType.doodle ? ColorFilter.mode(Colors.white.withOpacity(0.7), BlendMode.lighten) : null)) : null, decoration: bgImage != null ? BoxDecoration(image: DecorationImage(image: AssetImage(bgImage), fit: BoxFit.cover, colorFilter: themeType == AppThemeType.doodle ? ColorFilter.mode(Colors.white.withOpacity(0.7), BlendMode.lighten) : null)) : null,
child: Stack( child: Stack(
children: [ children: [
if (gameController.isTimeMode && !gameController.isCPUThinking && !gameController.isGameOver && gameController.timeLeft > 0 && gameController.timeLeft <= 5 && !gameController.isSetupPhase) 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)),
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.effectText.isNotEmpty)
Positioned.fill(child: SpecialEventBackgroundEffect(text: gameController.effectText, color: gameController.effectColor, themeType: themeType)),
Positioned.fill(child: gameContent), Positioned.fill(child: gameContent),
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))))))),
// --- SCHERMATA COPRENTE PER IL PASSAGGIO DEL TELEFONO IN LOCALE --- 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(
// 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))),
], ],
), ),
), ),
@ -452,21 +293,48 @@ class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
void _handleTap(Offset tapPos, double width, double height, GameController controller, AppThemeType themeType) { void _handleTap(Offset tapPos, double width, double height, GameController controller, AppThemeType themeType) {
final board = controller.board; final board = controller.board;
if (board.isGameOver) return; if (board.isGameOver) return;
int cols = board.columns + 1; double spacing = width / cols; double offset = spacing / 2;
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) { if (controller.isSetupPhase) {
int bx = ((tapPos.dx - offset) / spacing).floor(); int by = ((tapPos.dy - offset) / spacing).floor(); var sortedBoxes = List<Box>.from(board.boxes);
controller.placeJoker(bx, by); return; 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) { for (var line in board.lines) {
if (line.owner != Player.none || !line.isPlayable) continue; 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); double dist = _distanceToSegment(tapPos, screenP1, screenP2);
if (dist < minDistance && dist < maxTouchDistance) { minDistance = dist; closestLine = line; } if (dist < minDistance && dist < maxTouchDistance) { minDistance = dist; closestLine = line; }
} }
if (closestLine != null) { controller.handleLineTap(closestLine, themeType); } if (closestLine != null) { controller.handleLineTap(closestLine, themeType); }
} }

View file

@ -624,6 +624,9 @@ class _HomeScreenState extends State<HomeScreen> 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.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.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)), _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<HomeScreen> 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.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.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)), _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)),
], ],
), ),

View file

@ -133,10 +133,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: characters name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.1"
checked_yaml: checked_yaml:
dependency: transitive dependency: transitive
description: description:
@ -449,18 +449,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.17" version: "0.12.19"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.11.1" version: "0.13.0"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@ -734,10 +734,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.7" version: "0.7.10"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description: