// =========================================================================== // 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 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); } }