Files
BlockSeasons/lib/ui/widgets/board_widget.dart
T
airkjw 3138fc4b08 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>
2026-06-11 13:19:34 +09:00

96 lines
2.7 KiB
Dart

import 'package:flutter/material.dart';
import '../../state/game_session_notifier.dart';
import 'board_painter.dart';
/// The board with clear-flash and combo-text effects. Pure display: drag
/// orchestration lives in the game screen.
class BoardWidget extends StatefulWidget {
const BoardWidget({super.key, required this.view, required this.ghost});
final GameViewState view;
final GhostSpec? ghost;
@override
State<BoardWidget> createState() => _BoardWidgetState();
}
class _BoardWidgetState extends State<BoardWidget>
with SingleTickerProviderStateMixin {
late final AnimationController _flash = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 350),
);
List<int> _flashRows = const [];
List<int> _flashCols = const [];
int _comboStreak = 0;
@override
void didUpdateWidget(BoardWidget old) {
super.didUpdateWidget(old);
final placement = widget.view.lastPlacement;
if (widget.view.fxTick != old.view.fxTick &&
placement != null &&
placement.linesCleared > 0) {
_flashRows = placement.clearedRows;
_flashCols = placement.clearedCols;
_comboStreak = placement.comboStreak;
_flash.forward(from: 0);
}
}
@override
void dispose() {
_flash.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 1,
child: AnimatedBuilder(
animation: _flash,
builder: (context, _) {
final fading = _flash.isAnimating ? 1 - _flash.value : 0.0;
return Stack(
fit: StackFit.expand,
children: [
CustomPaint(
painter: BoardPainter(
grid: widget.view.grid,
ghost: widget.ghost,
flashProgress: fading,
flashRows: _flashRows,
flashCols: _flashCols,
),
),
if (_flash.isAnimating && _comboStreak >= 2)
Center(
child: Transform.scale(
scale: 0.8 + 0.6 * _flash.value,
child: Opacity(
opacity: 1 - _flash.value,
child: Text(
'COMBO x$_comboStreak',
style: TextStyle(
fontSize: 36,
fontWeight: FontWeight.w900,
color: Colors.amber.shade300,
shadows: const [
Shadow(blurRadius: 12, color: Colors.black54),
],
),
),
),
),
),
],
);
},
),
);
}
}