Compare commits
3 commits
main
...
v_20260315
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c90f75cf66 | ||
|
|
42b8180f5e | ||
| dfe392ea88 |
9 changed files with 394 additions and 705 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
16
.metadata
16
.metadata
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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 dà punti e il giocatore ha un x2 attivo...
|
|
||||||
if (playerMakingMove == Player.red && redHasMultiplier) {
|
|
||||||
points *= 2;
|
|
||||||
redHasMultiplier = false; // Si consuma
|
|
||||||
} else if (playerMakingMove == Player.blue && blueHasMultiplier) {
|
|
||||||
points *= 2;
|
|
||||||
blueHasMultiplier = false; // Si consuma
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playerMakingMove == Player.red) { scoreRed += points; }
|
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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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); }
|
|
||||||
}
|
|
||||||
|
|
@ -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); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
|
||||||
BIN
macos/.DS_Store
vendored
BIN
macos/.DS_Store
vendored
Binary file not shown.
16
pubspec.lock
16
pubspec.lock
|
|
@ -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:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue