diff --git a/android/gradle.properties b/android/gradle.properties index 4bb5439f682100f8ef4ba80a557fe4f2f0ab14c2..6bf54a6ed821c19f76d860d4a24e7c85d440b575 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,5 +1,5 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true -app.versionName=0.0.9 -app.versionCode=9 +app.versionName=0.0.10 +app.versionCode=10 diff --git a/fastlane/metadata/android/en-US/changelogs/10.txt b/fastlane/metadata/android/en-US/changelogs/10.txt new file mode 100644 index 0000000000000000000000000000000000000000..f4722ac93441fb3adbabd2e02e515699ad03b527 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/10.txt @@ -0,0 +1 @@ +Add animation on delete blocks. diff --git a/fastlane/metadata/android/fr-FR/changelogs/10.txt b/fastlane/metadata/android/fr-FR/changelogs/10.txt new file mode 100644 index 0000000000000000000000000000000000000000..556780846a9dbb43a76fc2cfd2523a4efe3e09a1 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/10.txt @@ -0,0 +1 @@ +Ajout d'animation à la suppression des blocs. diff --git a/lib/cubit/game_cubit.dart b/lib/cubit/game_cubit.dart index 86e9c01600c0e8acadd57612553ab4fd813f1a18..e3803dfe9624f7d0de400281fa0fd5b63afdea4b 100644 --- a/lib/cubit/game_cubit.dart +++ b/lib/cubit/game_cubit.dart @@ -95,37 +95,35 @@ class GameCubit extends HydratedCubit<GameState> { block.forEach((CellLocation blockItemToDelete) { this.updateCellValue(blockItemToDelete, null); }); - // Gravity! - this.moveCellsDown(); } int getScoreFromBlock(int blockSize) { return 3 * (blockSize - 2); } - void tapOnCell(CellLocation tappedCellLocation) { + List<CellLocation> tapOnCell(CellLocation tappedCellLocation) { final Game currentGame = this.state.currentGame; final int? cellValue = currentGame.getCellValue(tappedCellLocation); - print('Tap on cell: col=' + - tappedCellLocation.col.toString() + - ' ; row=' + - tappedCellLocation.row.toString() + - ' ; value=' + - cellValue.toString()); if (cellValue != null) { - List<CellLocation> block = currentGame.getSiblingCells(tappedCellLocation, []); - print('block size: ' + block.length.toString()); - if (block.length >= 3) { - this.deleteBlock(block); + List<CellLocation> blockCells = currentGame.getSiblingCells(tappedCellLocation, []); + if (blockCells.length >= 3) { + this.deleteBlock(blockCells); this.increaseMovesCount(); - this.increaseScore(getScoreFromBlock(block.length)); - this.updateAvailableBlocksCount(); + this.increaseScore(getScoreFromBlock(blockCells.length)); + return blockCells; } } - if (!currentGame.hasAtLeastOneAvailableBlock()) { + return []; + } + + void postAnimate() { + this.moveCellsDown(); + this.updateAvailableBlocksCount(); + + if (!this.state.currentGame.hasAtLeastOneAvailableBlock()) { print('no more block found. finish game.'); this.updateGameIsFinished(true); } diff --git a/lib/models/cell_location.dart b/lib/models/cell_location.dart index b7a70ffd791c385899cb582eabcc664bd163ad74..be4c1063325d10d5bce45deb13ec60a1bedbc224 100644 --- a/lib/models/cell_location.dart +++ b/lib/models/cell_location.dart @@ -10,4 +10,8 @@ class CellLocation { factory CellLocation.go(int row, int col) { return new CellLocation(col: col, row: row); } + + String toString() { + return 'CellLocation(col: ' + col.toString() + ', row: ' + row.toString() + ')'; + } } diff --git a/lib/models/game.dart b/lib/models/game.dart index 90426df03f00d80d8082dcdb98f95c868328dd3d..91f636bdb351588ee52f1e17ef1f2e9134a07af3 100644 --- a/lib/models/game.dart +++ b/lib/models/game.dart @@ -82,8 +82,8 @@ class Game { final int? referenceValue = this.getCellValue(referenceCellLocation); - for (var deltaRow = -1; deltaRow <= 1; deltaRow++) { - for (var deltaCol = -1; deltaCol <= 1; deltaCol++) { + for (int deltaRow = -1; deltaRow <= 1; deltaRow++) { + for (int deltaCol = -1; deltaCol <= 1; deltaCol++) { if (deltaCol == 0 || deltaRow == 0) { final int candidateRow = referenceCellLocation.row + deltaRow; final int candidateCol = referenceCellLocation.col + deltaCol; @@ -94,7 +94,7 @@ class Game { if (this.getCellValue(candidateLocation) == referenceValue) { bool alreadyFound = false; - for (var index = 0; index < siblingCells.length; index++) { + for (int index = 0; index < siblingCells.length; index++) { if ((siblingCells[index].row == candidateRow) && (siblingCells[index].col == candidateCol)) { alreadyFound = true; @@ -119,8 +119,8 @@ class Game { final List<List<CellLocation>> blocks = []; - for (var row = 0; row < boardSizeVertical; row++) { - for (var col = 0; col < boardSizeHorizontal; col++) { + for (int row = 0; row < boardSizeVertical; row++) { + for (int col = 0; col < boardSizeHorizontal; col++) { final CellLocation cellLocation = CellLocation.go(row, col); if (game.getCellValue(cellLocation) != null) { // if current cell not already in a found block @@ -150,8 +150,8 @@ class Game { final int boardSizeHorizontal = this.settings.boardSize; final int boardSizeVertical = this.settings.boardSize; - for (var row = 0; row < boardSizeVertical; row++) { - for (var col = 0; col < boardSizeHorizontal; col++) { + for (int row = 0; row < boardSizeVertical; row++) { + for (int col = 0; col < boardSizeHorizontal; col++) { final CellLocation cellLocation = CellLocation.go(row, col); if (this.getCellValue(cellLocation) != null) { final List<CellLocation> block = this.getSiblingCells(cellLocation, []); diff --git a/lib/models/game_board.dart b/lib/models/game_board.dart index bea21887bc25242d69c813194674ac15d1b8092e..71776f1ae4171dc8033fb51faa6eca65a537651b 100644 --- a/lib/models/game_board.dart +++ b/lib/models/game_board.dart @@ -22,9 +22,9 @@ class GameBoard { final rand = new Random(); List<List<GameCell>> cells = []; - for (var rowIndex = 0; rowIndex < boardSizeVertical; rowIndex++) { + for (int rowIndex = 0; rowIndex < boardSizeVertical; rowIndex++) { List<GameCell> row = []; - for (var colIndex = 0; colIndex < boardSizeHorizontal; colIndex++) { + for (int colIndex = 0; colIndex < boardSizeHorizontal; colIndex++) { int value = 1 + rand.nextInt(maxValue); row.add(GameCell(value)); } @@ -45,9 +45,9 @@ class GameBoard { print('Board:'); print(horizontalRule); - for (var rowIndex = 0; rowIndex < cells.length; rowIndex++) { + for (int rowIndex = 0; rowIndex < cells.length; rowIndex++) { String row = '| '; - for (var colIndex = 0; colIndex < cells[rowIndex].length; colIndex++) { + for (int colIndex = 0; colIndex < cells[rowIndex].length; colIndex++) { row += cells[rowIndex][colIndex].value.toString(); } row += ' |'; diff --git a/lib/ui/painters/game_board_painter.dart b/lib/ui/painters/game_board_painter.dart index 08cad440ef153547368e0ec7ac1a1c5e01513137..5bb11b449d3c403ce63966f8e4b8c1a461b407bf 100644 --- a/lib/ui/painters/game_board_painter.dart +++ b/lib/ui/painters/game_board_painter.dart @@ -8,114 +8,68 @@ import 'package:jeweled/models/cell_location.dart'; import 'package:jeweled/utils/color_theme.dart'; class GameBoardPainter extends CustomPainter { - const GameBoardPainter(this.currentGame); + const GameBoardPainter({required this.game, required this.animations}); - final Game currentGame; - - static const drawTextValue = false; + final Game game; + final List<List<Animation<double>?>> animations; @override void paint(Canvas canvas, Size size) { - final int sizeHorizontal = currentGame.settings.boardSize; - final int sizeVertical = currentGame.settings.boardSize; - - final List cells = currentGame.board.cells; - final double cellSize = size.width / max(sizeHorizontal, sizeVertical); + final double canvasSize = min(size.width, size.height); - // background - for (var row = 0; row < sizeVertical; row++) { - final double y = cellSize * row; - for (var col = 0; col < sizeHorizontal; col++) { - final double x = cellSize * col; + final int cellsCountHorizontal = game.settings.boardSize; + final int cellsCountVertical = game.settings.boardSize; - final GameCell cell = cells[row][col]; - final int colorCode = ColorTheme.getColorCode(cell.value); - - final cellPaintBackground = Paint(); - cellPaintBackground.color = Color(colorCode); - cellPaintBackground.style = PaintingStyle.fill; - - final Rect cellBackground = - Rect.fromPoints(Offset(x - 1, y - 1), Offset(x + cellSize + 2, y + cellSize + 2)); - - canvas.drawRect(cellBackground, cellPaintBackground); - - // draw value on cell - if (drawTextValue) { - final textPainter = TextPainter( - text: TextSpan( - text: cell.value.toString(), - style: const TextStyle( - color: Colors.black, - fontSize: 15, - ), - ), - textDirection: TextDirection.ltr, - )..layout( - minWidth: 0, - maxWidth: size.width, - ); - textPainter.paint(canvas, Offset(x + 4, y + 2)); - } - } - } + final double cellSize = canvasSize / max(cellsCountHorizontal, cellsCountVertical); - // borders + const double overlapping = 1; const double borderSize = 4; - final cellPaintBorder = Paint(); - cellPaintBorder.color = ColorTheme.getBorderColor(); - cellPaintBorder.strokeWidth = borderSize; - cellPaintBorder.strokeCap = StrokeCap.round; - - for (var row = 0; row < sizeVertical; row++) { - final double y = cellSize * row; - for (var col = 0; col < sizeHorizontal; col++) { + + // background + for (int row = 0; row < cellsCountVertical; row++) { + final double yOrigin = cellSize * row; + for (int col = 0; col < cellsCountHorizontal; col++) { final double x = cellSize * col; - final GameCell cell = cells[row][col]; - final int? cellValue = cell.value; + final CellLocation cellLocation = CellLocation.go(row, col); + final GameCell cell = game.getCell(cellLocation); + if (cell.value != null) { + final int colorCode = ColorTheme.getColorCode(cell.value); - // top border - if ((row == 0) || - (row > 1 && - cellValue != currentGame.getCellValue(CellLocation.go(row - 1, col)))) { - final Offset borderStart = Offset(x, y); - final Offset borderStop = Offset(x + cellSize, y); - canvas.drawLine(borderStart, borderStop, cellPaintBorder); - } + final Animation<double>? translation = this.animations[row][col]; + final double y = yOrigin + (translation?.value ?? 0) * cellSize; - // bottom border - if ((row == sizeVertical - 1) || - ((row + 1) < sizeVertical && - cellValue != currentGame.getCellValue(CellLocation.go(row + 1, col)))) { - final Offset borderStart = Offset(x, y + cellSize); - final Offset borderStop = Offset(x + cellSize, y + cellSize); - canvas.drawLine(borderStart, borderStop, cellPaintBorder); - } + final cellPaintBackground = Paint(); + cellPaintBackground.color = Color(colorCode); + cellPaintBackground.style = PaintingStyle.fill; - // left border - if ((col == 0) || - (col > 1 && - cellValue != currentGame.getCellValue(CellLocation.go(row, col - 1)))) { - final Offset borderStart = Offset(x, y); - final Offset borderStop = Offset(x, y + cellSize); - canvas.drawLine(borderStart, borderStop, cellPaintBorder); - } + final Rect cellBackground = Rect.fromPoints(Offset(x - overlapping, y - overlapping), + Offset(x + cellSize + overlapping, y + cellSize + overlapping)); - // right border - if ((col == sizeHorizontal - 1) || - ((col + 1) < sizeHorizontal && - cellValue != currentGame.getCellValue(CellLocation.go(row, col + 1)))) { - final Offset borderStart = Offset(x + cellSize, y); - final Offset borderStop = Offset(x + cellSize, y + cellSize); - canvas.drawLine(borderStart, borderStop, cellPaintBorder); + canvas.drawRect(cellBackground, cellPaintBackground); } } } + + // board borders + final boardPaintBorder = Paint(); + boardPaintBorder.color = ColorTheme.getBorderColor(); + boardPaintBorder.strokeWidth = borderSize; + boardPaintBorder.strokeCap = StrokeCap.round; + + final Offset boardTopLeft = Offset(0, 0); + final Offset boardTopRight = Offset(canvasSize, 0); + final Offset boardBottomLeft = Offset(0, canvasSize); + final Offset boardBottomRight = Offset(canvasSize, canvasSize); + + canvas.drawLine(boardTopLeft, boardTopRight, boardPaintBorder); + canvas.drawLine(boardTopRight, boardBottomRight, boardPaintBorder); + canvas.drawLine(boardBottomRight, boardBottomLeft, boardPaintBorder); + canvas.drawLine(boardBottomLeft, boardTopLeft, boardPaintBorder); } @override bool shouldRepaint(CustomPainter oldDelegate) { - return false; + return true; } } diff --git a/lib/ui/widgets/game.dart b/lib/ui/widgets/game.dart index 3170f63f912fb6b0a8dd03ec3a62dc59fe75a5a9..443c554a062e2b6ccf10e29971d9905e0399689b 100644 --- a/lib/ui/widgets/game.dart +++ b/lib/ui/widgets/game.dart @@ -16,17 +16,22 @@ class GameWidget extends StatelessWidget { builder: (BuildContext context, GameState gameState) { final Game currentGame = gameState.currentGame; - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const SizedBox(height: 8), - const GameTopIndicatorWidget(), - const SizedBox(height: 2), - const GameBoard(), - const SizedBox(height: 2), - currentGame.isFinished ? const GameBottomButtonsWidget() : const SizedBox.shrink(), - ], + return Container( + padding: const EdgeInsets.all(4), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 8), + const GameTopIndicatorWidget(), + const SizedBox(height: 2), + const GameBoard(), + const SizedBox(height: 2), + currentGame.isFinished + ? const GameBottomButtonsWidget() + : const SizedBox.shrink(), + ], + ), ); }, ); diff --git a/lib/ui/widgets/game_board.dart b/lib/ui/widgets/game_board.dart index ab9265d89728819612587a86e890975cbc4996f0..746ebbc572867642cb81689939ef30f5c5f2d601 100644 --- a/lib/ui/widgets/game_board.dart +++ b/lib/ui/widgets/game_board.dart @@ -6,9 +6,89 @@ 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 StatelessWidget { +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) { + this.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.settings.boardSize); + setState(() {}); + + controller.dispose(); + } + }); + + // Count translation length for each cell to move + final List<List<int>> stepsDownCounts = List.generate( + currentGame.settings.boardSize, + (i) => List.generate( + currentGame.settings.boardSize, + (i) => 0, + ), + ); + cellsToRemove.forEach((cellToRemove) { + for (int i = 0; i < cellToRemove.row; i++) { + stepsDownCounts[(cellToRemove.row - i) - 1][cellToRemove.col] += 1; + } + }); + + // Build animation for each cell to move + cellsToRemove.forEach((cellToRemove) { + for (int i = 0; i < cellToRemove.row; i++) { + final int stepsCount = stepsDownCounts[(cellToRemove.row - i) - 1][cellToRemove.col]; + this.animations[(cellToRemove.row - i) - 1][cellToRemove.col] = animation(stepsCount); + } + }); + + controller.forward().orCancel; + } + @override Widget build(BuildContext context) { final double displayWidth = MediaQuery.of(context).size.width; @@ -18,20 +98,27 @@ class GameBoard extends StatelessWidget { builder: (BuildContext context, GameState gameState) { final Game currentGame = gameState.currentGame; + if (this.animations.length == 0) { + resetAnimations(currentGame.settings.boardSize); + } + return GestureDetector( onTapUp: (details) { - double xTap = details.localPosition.dx; - double yTap = details.localPosition.dy; - int col = xTap ~/ (displayWidth / currentGame.settings.boardSize); - int row = yTap ~/ (displayWidth / currentGame.settings.boardSize); + final double xTap = details.localPosition.dx; + final double yTap = details.localPosition.dy; + final int col = xTap ~/ (displayWidth / currentGame.settings.boardSize); + final int row = yTap ~/ (displayWidth / currentGame.settings.boardSize); final GameCubit gameCubit = BlocProvider.of<GameCubit>(context); - gameCubit.tapOnCell(CellLocation.go(row, col)); + animateCells(gameCubit.tapOnCell(CellLocation.go(row, col))); }, child: CustomPaint( size: Size(displayWidth, displayWidth), willChange: false, - painter: GameBoardPainter(currentGame), + painter: GameBoardPainter( + game: currentGame, + animations: this.animations, + ), isComplex: true, ), ); diff --git a/lib/utils/color_theme.dart b/lib/utils/color_theme.dart index 2b10565a36d1f956202b2d30f13fb21bef8bd707..18d2006fc56b8a320b4caa921e2577aaf93216e2 100644 --- a/lib/utils/color_theme.dart +++ b/lib/utils/color_theme.dart @@ -29,6 +29,6 @@ class ColorTheme { } static Color getBorderColor() { - return Colors.black; + return Colors.grey; } } diff --git a/pubspec.yaml b/pubspec.yaml index 7f45afaa50d390a418c62fb833059908491b3c9a..3dd4fa9f2549c9daba7b0e9ee1ffa32ff7a26660 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: Jeweled Game publish_to: 'none' -version: 0.0.9+9 +version: 0.0.10+10 environment: sdk: '^3.0.0'