import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:jeweled/cubit/game_cubit.dart'; import 'package:jeweled/models/game.dart'; import 'package:jeweled/models/cell_location.dart'; import 'package:jeweled/ui/painters/game_board_painter.dart'; class GameBoard extends StatefulWidget { const GameBoard({super.key}); @override State<GameBoard> createState() => _GameBoard(); } class _GameBoard extends State<GameBoard> with TickerProviderStateMixin { final int animationDuration = 500; List<List<Animation<double>?>> animations = []; void resetAnimations(int boardSize) { animations = List.generate( boardSize, (i) => List.generate( boardSize, (i) => null, ), ); } void animateCells(List<CellLocation> cellsToRemove) { final GameCubit gameCubit = BlocProvider.of<GameCubit>(context); final Game currentGame = gameCubit.state.currentGame; // "move down" cells final controller = AnimationController( vsync: this, duration: Duration(milliseconds: animationDuration), )..addListener(() { if (mounted) { setState(() {}); } }); if (mounted) { setState(() {}); } Animation<double> animation(int count) => Tween( begin: 0.0, end: count.toDouble(), ).animate(CurvedAnimation( curve: Curves.bounceOut, parent: controller, )) ..addStatusListener((status) { if (status == AnimationStatus.completed) { gameCubit.postAnimate(); resetAnimations(currentGame.gameSettings.boardSize); setState(() {}); controller.dispose(); } }); // Count translation length for each cell to move final List<List<int>> stepsDownCounts = List.generate( currentGame.gameSettings.boardSize, (i) => List.generate( currentGame.gameSettings.boardSize, (i) => 0, ), ); for (CellLocation cellToRemove in cellsToRemove) { for (int i = 0; i < cellToRemove.row; i++) { stepsDownCounts[(cellToRemove.row - i) - 1][cellToRemove.col] += 1; } } // Build animation for each cell to move bool needAnimation = false; for (CellLocation cellToRemove in cellsToRemove) { for (int i = 0; i < cellToRemove.row; i++) { final int stepsCount = stepsDownCounts[(cellToRemove.row - i) - 1][cellToRemove.col]; animations[(cellToRemove.row - i) - 1][cellToRemove.col] = animation(stepsCount); needAnimation = true; } } controller.forward().orCancel; if (!needAnimation) { gameCubit.postAnimate(); } } @override Widget build(BuildContext context) { final double displayWidth = MediaQuery.of(context).size.width; return Center( child: BlocBuilder<GameCubit, GameState>( builder: (BuildContext context, GameState gameState) { final Game currentGame = gameState.currentGame; if (animations.isEmpty) { resetAnimations(currentGame.gameSettings.boardSize); } return GestureDetector( onTapUp: (details) { bool animationInProgress = false; for (List<Animation<double>?> row in animations) { for (Animation<double>? cell in row) { if (cell != null) { animationInProgress = true; } } } if (!animationInProgress) { final double xTap = details.localPosition.dx; final double yTap = details.localPosition.dy; final int col = xTap ~/ (displayWidth / currentGame.gameSettings.boardSize); final int row = yTap ~/ (displayWidth / currentGame.gameSettings.boardSize); final GameCubit gameCubit = BlocProvider.of<GameCubit>(context); animateCells(gameCubit.tapOnCell(CellLocation.go(row, col))); } }, child: CustomPaint( size: Size(displayWidth, displayWidth), willChange: false, painter: GameBoardPainter( game: currentGame, animations: animations, ), isComplex: true, ), ); }, ), ); } }