// =========================================================================== // 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; final bool isOnline; final bool isVsCPU; final bool isSetupPhase; final Player myPlayer; final Player jokerTurn; 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 }); @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.columns + 1; 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) { Offset p1 = getScreenPos(box.x, box.y); Offset p2 = getScreenPos(box.x + 1, box.y + 1); Rect rect = Rect.fromPoints(p1, p2); if (box.type == BoxType.invisible) { if (box.isRevealed) { _drawIconInBox(canvas, rect, ThemeIcons.block(themeType), Colors.grey.shade500); } continue; } 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) { 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 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); } } 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); if (line.isIceCracked) { _drawCrackedIceLine(canvas, p1, p2, blinkValue); continue; } 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 && 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; 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 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) { 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); 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 crack = Path()..moveTo(p1.dx, p1.dy); int zigzags = 6; for (int i=1; i 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); } }