Add playable core UI: board painter, drag-and-drop, HUD, result overlay
CustomPainter board with gems/ghost/clear-flash, finger-lifted drag with snap preview, combo text effect, HUD chips, phase overlays with rescue stubs, demo stage. E2E widget test drives a real drag gesture. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../game/models/cell.dart';
|
||||
import '../../game/models/grid.dart';
|
||||
import '../../game/models/piece.dart';
|
||||
import '../theme/palette.dart';
|
||||
import 'board_geometry.dart';
|
||||
import 'piece_painter.dart';
|
||||
|
||||
/// Drag ghost preview: a piece hovering at a snapped anchor.
|
||||
class GhostSpec {
|
||||
const GhostSpec({
|
||||
required this.piece,
|
||||
required this.anchorX,
|
||||
required this.anchorY,
|
||||
required this.legal,
|
||||
});
|
||||
|
||||
final Piece piece;
|
||||
final int anchorX;
|
||||
final int anchorY;
|
||||
final bool legal;
|
||||
}
|
||||
|
||||
class BoardPainter extends CustomPainter {
|
||||
const BoardPainter({
|
||||
required this.grid,
|
||||
required this.ghost,
|
||||
required this.flashProgress,
|
||||
required this.flashRows,
|
||||
required this.flashCols,
|
||||
});
|
||||
|
||||
final GridState grid;
|
||||
final GhostSpec? ghost;
|
||||
|
||||
/// 1.0 -> 0.0 while the clear flash fades.
|
||||
final double flashProgress;
|
||||
final List<int> flashRows;
|
||||
final List<int> flashCols;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final geo = BoardGeometry(boardSize: size.width);
|
||||
final radius = Radius.circular(geo.cellSize * 0.18);
|
||||
final inset = geo.cellSize * 0.05;
|
||||
|
||||
final bg = Paint()..color = GamePalette.boardBackground;
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(Offset.zero & size, const Radius.circular(12)),
|
||||
bg,
|
||||
);
|
||||
|
||||
for (var y = 0; y < GridState.size; y++) {
|
||||
for (var x = 0; x < GridState.size; x++) {
|
||||
final rect = geo.cellRect(x, y).deflate(inset);
|
||||
final cell = grid.cellAt(x, y);
|
||||
final paint = Paint()
|
||||
..color = switch (cell.type) {
|
||||
CellType.empty => GamePalette.emptyCell,
|
||||
CellType.filled => GamePalette.tile(cell.colorId),
|
||||
CellType.gem => GamePalette.emptyCell,
|
||||
};
|
||||
canvas.drawRRect(RRect.fromRectAndRadius(rect, radius), paint);
|
||||
|
||||
if (cell.type == CellType.gem) {
|
||||
_paintGem(canvas, rect);
|
||||
} else if (cell.type == CellType.filled) {
|
||||
final highlight = Paint()
|
||||
..color = Colors.white.withValues(alpha: 0.15);
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
Rect.fromLTWH(
|
||||
rect.left, rect.top, rect.width, rect.height * 0.32),
|
||||
radius,
|
||||
),
|
||||
highlight,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final g = ghost;
|
||||
if (g != null) {
|
||||
paintPiece(
|
||||
canvas,
|
||||
g.piece,
|
||||
cellSize: geo.cellSize,
|
||||
origin: Offset(g.anchorX * geo.cellSize, g.anchorY * geo.cellSize),
|
||||
overrideColor:
|
||||
g.legal ? GamePalette.ghostLegal : GamePalette.ghostIllegal,
|
||||
);
|
||||
}
|
||||
|
||||
if (flashProgress > 0) {
|
||||
final flash = Paint()
|
||||
..color = Colors.white.withValues(alpha: 0.75 * flashProgress);
|
||||
for (final y in flashRows) {
|
||||
canvas.drawRect(
|
||||
Rect.fromLTWH(0, y * geo.cellSize, size.width, geo.cellSize),
|
||||
flash,
|
||||
);
|
||||
}
|
||||
for (final x in flashCols) {
|
||||
canvas.drawRect(
|
||||
Rect.fromLTWH(x * geo.cellSize, 0, geo.cellSize, size.height),
|
||||
flash,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _paintGem(Canvas canvas, Rect rect) {
|
||||
final center = rect.center;
|
||||
final r = rect.width * 0.32;
|
||||
final path = Path()
|
||||
..moveTo(center.dx, center.dy - r)
|
||||
..lineTo(center.dx + r, center.dy)
|
||||
..lineTo(center.dx, center.dy + r)
|
||||
..lineTo(center.dx - r, center.dy)
|
||||
..close();
|
||||
canvas.drawPath(path, Paint()..color = GamePalette.gem);
|
||||
canvas.drawPath(
|
||||
path,
|
||||
Paint()
|
||||
..color = Colors.white.withValues(alpha: 0.6)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = rect.width * 0.05,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(BoardPainter old) =>
|
||||
old.grid != grid ||
|
||||
old.ghost != ghost ||
|
||||
old.flashProgress != flashProgress;
|
||||
}
|
||||
Reference in New Issue
Block a user