262 lines
No EOL
13 KiB
Dart
262 lines
No EOL
13 KiB
Dart
// ===========================================================================
|
|
// FILE: lib/ui/game/board_painter.dart
|
|
// ===========================================================================
|
|
|
|
import 'dart:math';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
|
|
import '../../models/game_board.dart';
|
|
import '../../core/app_colors.dart';
|
|
|
|
class BoardPainter extends CustomPainter {
|
|
final GameBoard board;
|
|
final ThemeColors theme;
|
|
final AppThemeType themeType;
|
|
final double blinkValue;
|
|
|
|
final bool isOnline;
|
|
final bool isVsCPU;
|
|
final bool isSetupPhase;
|
|
final Player myPlayer;
|
|
final Player jokerTurn;
|
|
|
|
final double cameraAngle; // Angolazione della telecamera a 360 gradi!
|
|
|
|
BoardPainter({
|
|
required this.board,
|
|
required this.theme,
|
|
required this.themeType,
|
|
required this.isOnline,
|
|
required this.isVsCPU,
|
|
required this.isSetupPhase,
|
|
required this.myPlayer,
|
|
required this.jokerTurn,
|
|
this.blinkValue = 0.0,
|
|
this.cameraAngle = 0.0,
|
|
});
|
|
|
|
Color _darken(Color c, [double amount = .1]) {
|
|
assert(amount >= 0 && amount <= 1);
|
|
final hsl = HSLColor.fromColor(c);
|
|
final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
|
|
return hslDark.toColor();
|
|
}
|
|
|
|
// LA MAGIA: Proiezione Isometrica Ruotabile
|
|
Offset projectLogical(double x, double y, double z, Size size) {
|
|
if (board.shape != ArenaShape.pyramid3D) {
|
|
int gridPoints = board.columns + 1;
|
|
double spacing = size.width / gridPoints;
|
|
return Offset(x * spacing + (spacing / 2), y * spacing + (spacing / 2));
|
|
}
|
|
|
|
double tileW = size.width / 3.8;
|
|
double tileH = tileW * 0.55;
|
|
double zHeight = tileW * 0.45; // L'altezza fisica del blocco 3D
|
|
|
|
// Calcoliamo la posizione tenendo conto del restringimento della piramide (+ z * 0.5)
|
|
double actualX = x + z * 0.5 - board.columns / 2.0;
|
|
double actualY = y + z * 0.5 - board.rows / 2.0;
|
|
|
|
// Matrice di rotazione della telecamera (Z-Axis)
|
|
double rx = actualX * cos(cameraAngle) - actualY * sin(cameraAngle);
|
|
double ry = actualX * sin(cameraAngle) + actualY * cos(cameraAngle);
|
|
|
|
// Proiezione Isometrica 2D
|
|
double sx = (rx - ry) * tileW;
|
|
// Il SEGRETO: -z sposta fisicamente in ALTO la faccia del cubo rispetto allo schermo
|
|
double sy = (rx + ry) * tileH - (z * zHeight);
|
|
|
|
return Offset(size.width / 2 + sx, size.height * 0.70 + sy);
|
|
}
|
|
|
|
// Calcola la distanza dalla telecamera per lo Z-Buffering
|
|
double getDepth(Box b) {
|
|
double actualX = b.x + b.z * 0.5 - board.columns / 2.0;
|
|
double actualY = b.y + b.z * 0.5 - board.rows / 2.0;
|
|
double rx = actualX * cos(cameraAngle) - actualY * sin(cameraAngle);
|
|
double ry = actualX * sin(cameraAngle) + actualY * cos(cameraAngle);
|
|
return rx + ry; // Ordina dal più lontano al più vicino
|
|
}
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
if (board.shape == ArenaShape.pyramid3D) _paint3D(canvas, size);
|
|
else _paint2D(canvas, size);
|
|
}
|
|
|
|
void _paint3D(Canvas canvas, Size size) {
|
|
double visualZHeight = (size.width / 3.8) * 0.45;
|
|
int maxZ = 4;
|
|
Set<Line> drawnLines = {};
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void _paint2D(Canvas canvas, Size size) {
|
|
for (var box in board.boxes) {
|
|
if (box.type == BoxType.invisible) continue;
|
|
Offset p1 = projectLogical(box.top.p1.x.toDouble(), box.top.p1.y.toDouble(), 0, size);
|
|
Offset p2 = projectLogical(box.top.p2.x.toDouble(), box.top.p2.y.toDouble(), 0, size);
|
|
Offset p3 = projectLogical(box.bottom.p2.x.toDouble(), box.bottom.p2.y.toDouble(), 0, size);
|
|
Offset p4 = projectLogical(box.bottom.p1.x.toDouble(), box.bottom.p1.y.toDouble(), 0, size);
|
|
Path poly = Path()..moveTo(p1.dx, p1.dy)..lineTo(p2.dx, p2.dy)..lineTo(p3.dx, p3.dy)..lineTo(p4.dx, p4.dy)..close();
|
|
|
|
if (box.owner != Player.none) {
|
|
Color c = box.owner == Player.red ? theme.playerRed : theme.playerBlue;
|
|
canvas.drawPath(poly, Paint()..color = c.withOpacity(0.85)..style = PaintingStyle.fill);
|
|
}
|
|
_drawBoxIcon(canvas, poly, box);
|
|
}
|
|
|
|
for (var line in board.lines) {
|
|
if (!line.isPlayable && line.owner == Player.none) continue;
|
|
Offset p1 = projectLogical(line.p1.x.toDouble(), line.p1.y.toDouble(), 0, size);
|
|
Offset p2 = projectLogical(line.p2.x.toDouble(), line.p2.y.toDouble(), 0, size);
|
|
|
|
if (line.isIceCracked) { _drawCrackedIceLine(canvas, p1, p2, blinkValue); continue; }
|
|
Color lineColor = line.owner == Player.none ? theme.gridLine.withOpacity(0.4) : (line.owner == Player.red ? theme.playerRed : theme.playerBlue);
|
|
if (line == board.lastMove && line.owner != Player.none) canvas.drawLine(p1, p2, Paint()..color = Colors.white.withOpacity(blinkValue * 0.8)..strokeWidth = 12.0..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4.0));
|
|
canvas.drawLine(p1, p2, Paint()..color = lineColor..strokeWidth = (line.owner == Player.none ? 3.0 : 6.0)..strokeCap = StrokeCap.round);
|
|
}
|
|
|
|
for (var dot in board.dots) {
|
|
bool isVisible = board.lines.any((l) => (l.p1 == dot || l.p2 == dot) && l.isPlayable);
|
|
if (isVisible) canvas.drawCircle(projectLogical(dot.x.toDouble(), dot.y.toDouble(), 0, size), 4.0, Paint()..color = theme.text.withOpacity(0.8));
|
|
}
|
|
}
|
|
|
|
void _drawBoxIcon(Canvas canvas, Path face, Box box) {
|
|
if (box.type == BoxType.gold) _drawIconOnPath(canvas, face, FontAwesomeIcons.crown, Colors.amber);
|
|
else if (box.type == BoxType.bomb) _drawIconOnPath(canvas, face, FontAwesomeIcons.skull, Colors.redAccent);
|
|
else if (box.type == BoxType.swap) _drawIconOnPath(canvas, face, FontAwesomeIcons.arrowsRotate, Colors.purpleAccent);
|
|
else if (box.type == BoxType.multiplier) _drawIconOnPath(canvas, face, FontAwesomeIcons.bolt, Colors.yellowAccent);
|
|
else if (box.type == BoxType.ice && box.owner == Player.none) {
|
|
canvas.drawPath(face, Paint()..color = Colors.cyanAccent.withOpacity(0.15)..style = PaintingStyle.fill);
|
|
_drawIconOnPath(canvas, face, FontAwesomeIcons.snowflake, Colors.cyanAccent);
|
|
}
|
|
}
|
|
|
|
void _drawIconOnPath(Canvas canvas, Path path, IconData icon, Color color) {
|
|
Rect bounds = path.getBounds();
|
|
TextPainter tp = TextPainter(text: TextSpan(text: String.fromCharCode(icon.codePoint), style: TextStyle(color: color, fontSize: 16, fontFamily: icon.fontFamily, package: icon.fontPackage)), textDirection: TextDirection.ltr)..layout();
|
|
tp.paint(canvas, Offset(bounds.center.dx - tp.width / 2, bounds.center.dy - tp.height / 2));
|
|
}
|
|
|
|
void _drawCrackedIceLine(Canvas canvas, Offset p1, Offset p2, double blink) {
|
|
Paint crackPaint = Paint()..color = Colors.cyanAccent.withOpacity(0.6 + (0.4 * blink))..strokeWidth = 3.0..style = PaintingStyle.stroke..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.solid, 2.0);
|
|
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;
|
|
double len = sqrt(dx * dx + dy * dy);
|
|
if (len == 0) return;
|
|
double ndx = dx / len; double ndy = dy / len;
|
|
Path crack = Path()..moveTo(p1.dx, p1.dy);
|
|
for (int i = 1; i < 6; i++) {
|
|
double offset = (i % 2 == 0 ? 3.0 : -3.0);
|
|
crack.lineTo(p1.dx + ndx * (len * (i / 6)) + (-ndy) * offset, p1.dy + ndy * (len * (i / 6)) + ndx * offset);
|
|
}
|
|
crack.lineTo(p2.dx, p2.dy);
|
|
canvas.drawPath(crack, crackPaint);
|
|
}
|
|
|
|
@override bool shouldRepaint(covariant BoardPainter oldDelegate) => true;
|
|
} |