tetraq/lib/ui/game/board_painter.dart
2026-02-27 23:35:54 +01:00

337 lines
No EOL
14 KiB
Dart

// ===========================================================================
// FILE: lib/ui/game/board_painter.dart
// ===========================================================================
import 'dart:math';
import 'package:flutter/material.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;
BoardPainter({required this.board, required this.theme, required this.themeType, this.blinkValue = 0.0});
@override
void paint(Canvas canvas, Size size) {
if (themeType == AppThemeType.doodle) {
final Paint paperGridPaint = Paint()
..color = Colors.grey.withOpacity(0.3)
..strokeWidth = 1.0
..style = PaintingStyle.stroke;
double paperStep = 20.0;
for (double i = 0; i <= size.width; i += paperStep) {
canvas.drawLine(Offset(i, 0), Offset(i, size.height), paperGridPaint);
}
for (double i = 0; i <= size.height; i += paperStep) {
canvas.drawLine(Offset(0, i), Offset(size.width, i), paperGridPaint);
}
}
int gridPoints = board.radius * 2 + 2;
double spacing = size.width / gridPoints;
double offset = spacing / 2;
Offset getScreenPos(int x, int y) => Offset(x * spacing + offset, y * spacing + offset);
// --- 1. DISEGNO AREE CONQUISTATE E ICONE ---
for (var box in board.boxes) {
if (box.type == BoxType.invisible) continue;
Offset p1 = getScreenPos(box.x, box.y);
Offset p2 = getScreenPos(box.x + 1, box.y + 1);
Rect rect = Rect.fromPoints(p1, p2);
if (box.owner != Player.none) {
final boxPaint = Paint()
..style = PaintingStyle.fill
..color = box.owner == Player.red ? theme.playerRed.withOpacity(0.6) : theme.playerBlue.withOpacity(0.6);
if (themeType == AppThemeType.wood) {
_drawFlameBox(canvas, rect, box.owner == Player.red);
} else if (themeType == AppThemeType.doodle) {
Color penColor = box.owner == Player.red ? Colors.redAccent.shade700 : Colors.blueAccent.shade700;
_drawScribbleBox(canvas, rect, penColor);
} else {
canvas.drawRect(rect, boxPaint);
}
}
if (box.type == BoxType.gold) {
_drawIconInBox(canvas, rect, Icons.star_rounded, Colors.amber);
} else if (box.type == BoxType.bomb) {
_drawIconInBox(canvas, rect, Icons.mood_bad_rounded, themeType == AppThemeType.cyberpunk ? Colors.greenAccent : Colors.deepPurple);
} else if (box.type == BoxType.swap) {
// NUOVA ICONA SWAP: Frecce circolari viola (o cyan) per indicare l'inversione
_drawIconInBox(canvas, rect, Icons.sync_rounded, Colors.purpleAccent);
}
}
// --- 2. DISEGNO LINEE CON EFFETTO LAMPEGGIAMENTO ---
for (var line in board.lines) {
if (!line.isPlayable) continue;
Offset p1 = getScreenPos(line.p1.x, line.p1.y);
Offset p2 = getScreenPos(line.p2.x, line.p2.y);
bool isLastMove = (line == board.lastMove);
Color lineColor = line.owner == Player.none
? theme.gridLine.withOpacity(0.4)
: (line.owner == Player.red ? theme.playerRed : theme.playerBlue);
if (isLastMove && line.owner != Player.none && themeType != AppThemeType.wood && themeType != AppThemeType.cyberpunk) {
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 (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);
}
}
// --- 3. DISEGNO PUNTINI ---
final dotPaint = Paint()..style = PaintingStyle.fill;
Set<Dot> activeDots = {};
for (var line in board.lines) {
if (line.isPlayable) {
activeDots.add(line.p1);
activeDots.add(line.p2);
}
}
for (var dot in activeDots) {
Offset pos = getScreenPos(dot.x, dot.y);
if (themeType == AppThemeType.wood) {
canvas.drawCircle(pos, 3.5, dotPaint..color = const Color(0xFF3E2723).withOpacity(0.2));
} else if (themeType == AppThemeType.cyberpunk) {
canvas.drawCircle(pos, 6.0, Paint()..color = theme.gridLine.withOpacity(0.3));
canvas.drawCircle(pos, 3.0, Paint()..color = Colors.white.withOpacity(0.5));
} else if (themeType == AppThemeType.doodle) {
canvas.drawRect(Rect.fromCenter(center: pos, width: 4, height: 4), dotPaint..color = Colors.black.withOpacity(0.25));
} else {
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: color.withOpacity(0.7),
fontSize: rect.width * 0.45,
fontFamily: icon.fontFamily,
package: icon.fontPackage,
shadows: [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 _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;
}
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);
}
}