Skip to content
Snippets Groups Projects
Commit 06ddc977 authored by Benoît Harrault's avatar Benoît Harrault
Browse files

Add board animations

parent 453b6128
Branches
Tags
1 merge request!7Resolve "Improve board animations"
Pipeline #4904 passed
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
Add animation on delete blocks.
Ajout d'animation à la suppression des blocs.
......@@ -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);
}
......
......@@ -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() + ')';
}
}
......@@ -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, []);
......
......@@ -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 += ' |';
......
......@@ -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 double canvasSize = min(size.width, size.height);
final int cellsCountHorizontal = game.settings.boardSize;
final int cellsCountVertical = game.settings.boardSize;
final List cells = currentGame.board.cells;
final double cellSize = size.width / max(sizeHorizontal, sizeVertical);
final double cellSize = canvasSize / max(cellsCountHorizontal, cellsCountVertical);
const double overlapping = 1;
const double borderSize = 4;
// background
for (var row = 0; row < sizeVertical; row++) {
final double y = cellSize * row;
for (var col = 0; col < sizeHorizontal; col++) {
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 CellLocation cellLocation = CellLocation.go(row, col);
final GameCell cell = game.getCell(cellLocation);
if (cell.value != null) {
final int colorCode = ColorTheme.getColorCode(cell.value);
final Animation<double>? translation = this.animations[row][col];
final double y = yOrigin + (translation?.value ?? 0) * cellSize;
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));
final Rect cellBackground = Rect.fromPoints(Offset(x - overlapping, y - overlapping),
Offset(x + cellSize + overlapping, y + cellSize + overlapping));
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));
}
}
}
// borders
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++) {
final double x = cellSize * col;
final GameCell cell = cells[row][col];
final int? cellValue = 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);
}
// 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);
}
// 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);
}
// 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);
}
}
}
// 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;
}
}
......@@ -16,7 +16,9 @@ class GameWidget extends StatelessWidget {
builder: (BuildContext context, GameState gameState) {
final Game currentGame = gameState.currentGame;
return Column(
return Container(
padding: const EdgeInsets.all(4),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
......@@ -25,8 +27,11 @@ class GameWidget extends StatelessWidget {
const SizedBox(height: 2),
const GameBoard(),
const SizedBox(height: 2),
currentGame.isFinished ? const GameBottomButtonsWidget() : const SizedBox.shrink(),
currentGame.isFinished
? const GameBottomButtonsWidget()
: const SizedBox.shrink(),
],
),
);
},
);
......
......@@ -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,
),
);
......
......@@ -29,6 +29,6 @@ class ColorTheme {
}
static Color getBorderColor() {
return Colors.black;
return Colors.grey;
}
}
......@@ -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'
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment