| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588 |
- // board.dart
- import 'dart:ui' as ui;
- import 'package:flutter/material.dart';
- import 'package:logging/logging.dart';
- import 'package:puzzleweave/config/device.dart';
- import 'package:puzzleweave/play/piece.dart';
- import 'package:puzzleweave/skin/skin.dart';
- import 'package:puzzleweave/utils/utils.dart';
- import 'package:vector_math/vector_math.dart' as vmath;
- final Logger _log = Logger('board.dart');
- // 增加一个preparing 和 shuffle洗牌的状态
- // preparing: Opacity透明度动画展示核心绘制区
- // shuffle: 发牌和翻牌动画构成
- enum BoardStatus { loading, preparing, shuffle, playing, success }
- // 发牌阶段,dealing:发牌中(发牌动画), fliping:翻转中(翻转动画)
- enum ShuffleStep { dealing, flipping }
- class Board {
- // 原图
- final ui.Image image;
- // 纸牌背面图,用于
- final ui.Image cardImage;
- // 所有拼图碎片
- final List<Piece> pieces = [];
- /// 拼图行数(3/4/5,对应9/16/25宫格)
- final int rows;
- /// 拼图列数(3/4/5)
- final int cols;
- /// 困难模式
- final bool hard;
- /// 整个拼图在屏幕上的目标区域(最终完整显示的位置和大小)
- final Rect targetRect;
- // 初始等于targetRect, 关卡成功后, 核心绘制区上移, 这个rect用于做动画控制,success之后使用这个
- Rect finalRect;
- // 碎片的逻辑宽高
- double get pieceLogicalWidth => targetRect.width / cols;
- double get pieceLogicalHeight => targetRect.height / rows;
- // 碎片的圆角半径,宫格越多圆角半径越小
- double get cornerRadius => 10.0 - rows.toDouble();
- // 设备信息
- final Device device;
- // 静态背景绘图 Picture (只绘制一次)
- ui.Picture? backgroundPicture;
- // 卡片绘图
- ui.Picture? cardPicture;
- ValueNotifier boardNotifier = ValueNotifier(1);
- BoardStatus _status = BoardStatus.loading;
- BoardStatus get status => _status;
- // 备份的 groups (PieceGroup 对象的列表)
- List<PieceGroup> backupGroups = [];
- ShuffleStep _shuffleStep = ShuffleStep.dealing;
- ShuffleStep get shuffleStep => _shuffleStep;
- void shuffle(ShuffleStep step) {
- _status = BoardStatus.shuffle;
- _shuffleStep = step;
- invalidate();
- }
- void prepare() {
- _status = BoardStatus.preparing;
- invalidate();
- }
- void start() {
- _status = BoardStatus.playing;
- invalidate();
- }
- void success() {
- _status = BoardStatus.success;
- invalidate();
- }
- void invalidate() {
- boardNotifier.value++;
- }
- // 是否全部完成
- bool get isAllDone => pieces.every((p) => p.isOK);
- final TickerProviderStateMixin ticker;
- static Future<Board> create(
- TickerProviderStateMixin ticker,
- ui.Image image,
- ui.Image cardImage,
- int rows,
- int cols,
- bool hard,
- Rect targetRect,
- Device device,
- ) async {
- _log.info('Image.size=${image.width}x${image.height}');
- final board = Board(ticker, image, cardImage, rows, cols, hard, targetRect, device);
- return board;
- }
- static Future<Board> restore(
- TickerProviderStateMixin ticker,
- ui.Image image,
- ui.Image cardImage,
- int rows,
- int cols,
- bool hard,
- Rect targetRect,
- Device device,
- String jsonPath,
- ) async {
- _log.info('Image.size=${image.width}x${image.height}');
- try {
- Map<String, dynamic> json = await loadJson(jsonPath) as Map<String, dynamic>;
- if (json['pieces'] == null || json['pieces'][0] == null) {
- throw Exception('invalid json: $json');
- }
- rows = json['pieces'][0]['rows'];
- cols = json['pieces'][0]['cols'];
- final board = Board(ticker, image, cardImage, rows, cols, hard, targetRect, device, json: json);
- return board;
- } catch (e) {
- _log.warning("board restore failed: $e");
- _log.info("游戏恢复失败,转为重新创建");
- final board = Board(ticker, image, cardImage, rows, cols, hard, targetRect, device);
- return board;
- }
- }
- Board(this.ticker, this.image, this.cardImage, this.rows, this.cols, this.hard, this.targetRect, this.device, {Map<String, dynamic>? json})
- : finalRect = targetRect {
- _recordBackground(); // 录制静态背景,提升性能
- _recordCard(); // 新增:录制卡片 Picture
- if (json != null) {
- _restorePieces(json);
- } else {
- _initPieces();
- }
- rebuildAllGroups();
- }
- /// 初始化碎片
- void _initPieces() {
- pieces.clear();
- final imageWidth = image.width.toDouble();
- final imageHeight = image.height.toDouble();
- final pieceWidth = imageWidth / cols;
- final pieceHeight = imageHeight / rows;
- for (int i = 0; i < rows; i++) {
- for (int j = 0; j < cols; j++) {
- final index = i * cols + j;
- final sourceRect = Rect.fromLTWH(j * pieceWidth, i * pieceHeight, pieceWidth, pieceHeight);
- final transform = getTransformByCoordinate(i, j);
- pieces.add(
- Piece(board: this, index: index, row: i, col: j, rows: rows, cols: cols, sourceRect: sourceRect, curRow: i, curCol: j, transform: transform),
- );
- }
- }
- _shufflePieces();
- _sortPieces();
- _log.info('_initPieces');
- }
- // restore 碎片
- void _restorePieces(Map<String, dynamic> json) {
- pieces.clear();
- final imageWidth = image.width.toDouble();
- final imageHeight = image.height.toDouble();
- final pieceWidth = imageWidth / cols;
- final pieceHeight = imageHeight / rows;
- for (var i = 0; i < (json['pieces'] as List).length; i++) {
- var jsonPiece = json['pieces'][i];
- final int index = jsonPiece['index'];
- final int row = jsonPiece['row'];
- final int col = jsonPiece['col'];
- final int curRow = jsonPiece['curRow'];
- final int curCol = jsonPiece['curCol'];
- final sourceRect = Rect.fromLTWH(col * pieceWidth, row * pieceHeight, pieceWidth, pieceHeight);
- final transform = getTransformByCoordinate(curRow, curCol);
- pieces.add(
- Piece(
- board: this,
- index: index,
- row: row,
- col: col,
- rows: rows,
- cols: cols,
- sourceRect: sourceRect,
- curRow: curRow,
- curCol: curCol,
- transform: transform,
- ),
- );
- }
- _sortPieces();
- _log.info('_restorePieces');
- }
- // 洗牌(随机打乱碎片位置)
- void _shufflePieces() {
- final shuffledPositions = List.generate(pieces.length, (i) => i)..shuffle();
- for (int i = 0; i < pieces.length; i++) {
- final targetIndex = shuffledPositions[i];
- pieces[i].curRow = targetIndex ~/ cols;
- pieces[i].curCol = targetIndex % cols;
- pieces[i].transform = getTransformByCoordinate(pieces[i].curRow, pieces[i].curCol);
- }
- }
- void resetAllPieces() {
- for (var p in pieces) {
- p.transform = getTransformByCoordinate(p.curRow, p.curCol);
- }
- }
- void setAllPieceToBottomRight() {
- for (var p in pieces) {
- p.transform = getBottomRightTransform();
- }
- }
- // 是否应该根据当前的curRow和curCol对pieces做一次排序,以方便dealing动画实现依次从左上到右下的发牌效果
- void _sortPieces() {
- pieces.sort((Piece a, Piece b) {
- int ret = a.curRow - b.curRow;
- if (ret == 0) {
- ret = a.curCol - b.curCol;
- }
- return ret;
- });
- }
- // 根据坐标查找指定位置的碎片
- Piece? findPieceAt(Offset localPos) {
- for (var piece in pieces.reversed) {
- // 从上层开始找
- // 计算碎片在画布上的绝对位置board
- final transform = piece.transform;
- final posX = transform.storage[12];
- final posY = transform.storage[13];
- final pieceRect = Rect.fromLTWH(posX, posY, pieceLogicalWidth, pieceLogicalHeight);
- if (pieceRect.contains(localPos)) {
- return piece;
- }
- }
- return null;
- }
- // 查找某个坐标上的碎片,排除某个piece
- Piece? findPieceAtExclude(Offset localPos, Piece excludePiece) {
- for (var piece in pieces.reversed) {
- // 从上层开始找
- if (piece == excludePiece) continue;
- // 计算碎片在画布上的绝对位置
- final transform = piece.transform;
- final posX = transform.storage[12];
- final posY = transform.storage[13];
- final pieceRect = Rect.fromLTWH(posX, posY, pieceLogicalWidth, pieceLogicalHeight);
- if (pieceRect.contains(localPos)) {
- return piece;
- }
- }
- return null;
- }
- // 获取右下角变换矩阵
- vmath.Matrix4 getBottomRightTransform() {
- return getTransformByCoordinate(rows - 1, cols - 1);
- }
- // 根据格子位置(row, col)获取变换矩阵
- vmath.Matrix4 getTransformByCoordinate(int row, int col) {
- final x = targetRect.left + col * pieceLogicalWidth;
- final y = targetRect.top + row * pieceLogicalHeight;
- final transform = vmath.Matrix4.translationValues(x, y, 0.0);
- return transform;
- }
- // 根据坐标获取该位置上的piece
- Piece? getPieceByCoordinate(int row, int col) {
- return pieces.firstWhereOrNull((p) => p.curRow == row && p.curCol == col);
- }
- // 根据坐标获取该位置上的piece
- Piece? getPieceByIndex(int index) {
- return pieces.firstWhereOrNull((p) => p.index == index);
- }
- // 根据当前坐标查找邻居碎片
- /// 获取碎片在其当前网格位置上四个方向的邻居碎片。
- List<Piece> getCurNeighbors(Piece piece) {
- final List<Piece> neighbors = [];
- final int r = piece.curRow;
- final int c = piece.curCol;
- // 上邻居 (curRow - 1, curCol)
- Piece? top = getPieceByCoordinate(r - 1, c);
- if (top != null) {
- neighbors.add(top);
- }
- // 下邻居 (curRow + 1, curCol)
- Piece? bottom = getPieceByCoordinate(r + 1, c);
- if (bottom != null) {
- neighbors.add(bottom);
- }
- // 左邻居 (curRow, curCol - 1)
- Piece? left = getPieceByCoordinate(r, c - 1);
- if (left != null) {
- neighbors.add(left);
- }
- // 右邻居 (curRow, curCol + 1)
- Piece? right = getPieceByCoordinate(r, c + 1);
- if (right != null) {
- neighbors.add(right);
- }
- return neighbors;
- }
- // 初始化检查合并分组 (改进版:固定点迭代合并)
- void rebuildAllGroups() {
- _log.info('rebuildAllGroups');
- // 1. 清除所有旧组 和 原path
- for (var p in pieces) {
- p.group = null;
- p.path = null;
- p.outLinePath = null;
- p.innerLinePath = null;
- }
- bool mergedInPass;
- // 2. 迭代合并,直到一轮循环中没有发生任何合并
- do {
- mergedInPass = false;
- // 3. 遍历所有碎片对 (i, j)
- for (int i = 0; i < pieces.length; i++) {
- for (int j = i + 1; j < pieces.length; j++) {
- final piece = pieces[i];
- final otherPiece = pieces[j];
- // 4. 检查是否可以合并,并且它们不属于同一个组
- if (piece.canMerge(otherPiece)) {
- if (!piece.isSameGroup(otherPiece)) {
- // 5. 合并组
- // piece.groupWith(otherPiece) 会创建一个新的 PieceGroup,
- // 并将 piece 和 otherPiece (以及它们可能已有的组员) 的 group 引用全部指向新组。
- piece.groupWith(otherPiece);
- mergedInPass = true;
- }
- }
- }
- }
- // 如果 mergedInPass 为 true,说明本轮循环发生了合并,需要重新开始下一轮遍历
- // 因为新的合并可能促使其他碎片或碎片组也得以连接
- } while (mergedInPass);
- }
- // 备份所有group, 方便进行比较, 发现新合成的group,以便呈现动画特效
- void backupAllGroups() {
- backupGroups.clear();
- for (var p in pieces) {
- if (p.group != null && !backupGroups.contains(p.group)) {
- backupGroups.add(p.group!);
- }
- }
- _log.info('backupAllGroups: ${backupGroups.length}');
- }
- // 当前group与之前备份的group进行比较,返回新合并的group列表,这些group需要展示动画特效
- List<PieceGroup> compareAllGroups() {
- _log.info('compareAllGroups');
- List<PieceGroup> newGroups = [];
- List<PieceGroup> currentGroups = [];
- for (var p in pieces) {
- if (p.group != null && !currentGroups.contains(p.group)) {
- currentGroups.add(p.group!);
- }
- }
- if (backupGroups.isEmpty) {
- // 之前都不存在群组,那么当前新合成的所有group都是新group
- newGroups.addAll(currentGroups);
- } else {
- for (var g in currentGroups) {
- bool alreadyExists = false;
- for (var bakgroup in backupGroups) {
- if (bakgroup.containsGroup(g)) {
- alreadyExists = true;
- break;
- }
- }
- if (!alreadyExists) {
- newGroups.add(g);
- }
- }
- }
- if (newGroups.isNotEmpty) {
- for (var g in newGroups) {
- _log.info('发现新群组:');
- g.print();
- }
- }
- return newGroups;
- }
- // 检查游戏是否全部完成
- bool checkWinCondition() {
- return pieces.every((p) => p.isOK);
- }
- /// 退出释放资源
- dispose() {
- backgroundPicture?.dispose();
- backgroundPicture = null;
- cardPicture?.dispose();
- cardPicture = null;
- image.dispose();
- boardNotifier.dispose();
- }
- // 录制背景,避免每次都重复绘制,提升性能
- void _recordBackground() {
- // 确保在录制前释放旧的 Picture 资源
- backgroundPicture?.dispose();
- backgroundPicture = null;
- final recorder = ui.PictureRecorder();
- // 录制器的边界设置为整个屏幕尺寸,因为我们在这里绘制的是 full screen background。
- final recordBounds = Rect.fromLTWH(0, 0, device.screenSize.width, device.screenSize.height);
- final canvas = Canvas(recorder, recordBounds);
- // --- 静态绘制配置 ---
- const double strokeWidth = 1.0; // 拼图槽位的线宽
- final double halfStroke = strokeWidth / 2.0;
- // 1. 绘制整个屏幕背景
- canvas.drawRect(
- recordBounds,
- Paint()
- ..color = SkinHelper.wholeBgColor
- ..style = PaintingStyle.fill,
- );
- // 2. 绘制每个拼图槽位(圆角矩形)作为背景的一部分 (静态内容)
- final slotFillPaint = Paint()
- ..color = SkinHelper.slotBgColor
- // .shade100 // 槽位填充色
- ..style = PaintingStyle.fill;
- final slotStrokePaint = Paint()
- ..color = SkinHelper.slotBorderColor
- ..style = PaintingStyle.stroke
- ..strokeWidth = strokeWidth;
- for (int r = 0; r < rows; r++) {
- for (int c = 0; c < cols; c++) {
- // 计算当前槽位的边界 (Canvas坐标系)
- final left = targetRect.left + c * pieceLogicalWidth;
- final top = targetRect.top + r * pieceLogicalHeight;
- final right = left + pieceLogicalWidth;
- final bottom = top + pieceLogicalHeight;
- // 为了避免描边溢出到相邻槽位,将矩形向内收缩 halfStroke
- final slotRect = Rect.fromLTRB(left + halfStroke, top + halfStroke, right - halfStroke, bottom - halfStroke);
- final slotRRect = RRect.fromRectAndRadius(slotRect, Radius.circular(cornerRadius));
- // 绘制填充
- canvas.drawRRect(slotRRect, slotFillPaint);
- // 绘制描边
- canvas.drawRRect(slotRRect, slotStrokePaint);
- }
- }
- // --- 结束录制并存储 ---
- backgroundPicture = recorder.endRecording();
- _log.info('Static background picture recorded. Size: ${recordBounds.size}');
- }
- // 提前录制卡牌(包含边框),避免每次都重复绘制,提升效率
- void _recordCard() {
- // 确保在录制前释放旧的 Picture 资源
- cardPicture?.dispose();
- cardPicture = null;
- final recorder = ui.PictureRecorder();
- // 录制器的边界设置为单个碎片区域
- final recordBounds = Rect.fromLTWH(0, 0, pieceLogicalWidth, pieceLogicalHeight);
- final canvas = Canvas(recorder, recordBounds);
- final double w = pieceLogicalWidth;
- final double h = pieceLogicalHeight;
- final double cornerRadius = this.cornerRadius;
- // 1. 裁剪区域
- final rrect = RRect.fromRectAndRadius(Rect.fromLTWH(0, 0, w, h), Radius.circular(cornerRadius));
- // 绘制图片的 Paint
- final imagePaint = Paint()
- ..isAntiAlias
- ..filterQuality = FilterQuality.none; // 禁用过滤(发牌动画中肉眼无差异) = true;
- // 边框 Paint
- const double pieceStrokeWidth = 1.0;
- final Paint outerBorderPaint = Paint()
- ..color = SkinHelper.outLineBorderColor
- ..style = PaintingStyle.stroke
- ..strokeWidth = pieceStrokeWidth
- ..isAntiAlias = true;
- final Paint innerBorderPaint = Paint()
- ..color = SkinHelper.innerLineBorderColor
- ..style = PaintingStyle.stroke
- ..strokeWidth = pieceStrokeWidth
- ..isAntiAlias = true;
- // 边框 RRects
- final outerRRect = RRect.fromRectAndRadius(Rect.fromLTWH(0.5, 0.5, w - 1.0, h - 1.0), Radius.circular(cornerRadius));
- final innerRRect = RRect.fromRectAndRadius(Rect.fromLTWH(2.5, 2.5, w - 5.0, h - 5.0), Radius.circular(cornerRadius));
- // --- 录制流程 ---
- // 1. 绘制图片内容 (需要裁剪)
- canvas.save();
- canvas.clipRRect(rrect); // 应用裁剪
- final Rect sourceRect = Rect.fromLTWH(0, 0, cardImage.width.toDouble(), cardImage.height.toDouble());
- final Rect dstRect = Rect.fromLTWH(0, 0, w, h);
- canvas.drawImageRect(cardImage, sourceRect, dstRect, imagePaint);
- canvas.restore(); // 移除裁剪
- // 2. 绘制边框 (不需要裁剪,因为边框是为了在图片外部描绘边界)
- canvas.drawRRect(outerRRect, outerBorderPaint);
- canvas.drawRRect(innerRRect, innerBorderPaint);
- // --- 结束录制并存储 ---
- cardPicture = recorder.endRecording();
- _log.info('Card picture recorded (with border). Size: ${recordBounds.size}');
- }
- Map<String, dynamic> toJson() => {'pieces': pieces.map((e) => e.toJson()).toList()};
- }
|