feat(ui): floating pulse hint for booster targeting
Replaces the plain bottom SnackBar with a BoosterHint pill that floats in the empty space above the board: season-accent coloured, a breathing glow that pulses only while armed (idle otherwise — no wasted ticker), slides/ fades in, shows the booster icon + prompt, and cancels on tap. 3 widget tests; full suite 234 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ import '../widgets/board_geometry.dart';
|
||||
import '../widgets/board_painter.dart';
|
||||
import '../widgets/board_widget.dart';
|
||||
import '../widgets/booster_bar.dart';
|
||||
import '../widgets/booster_hint.dart';
|
||||
import '../widgets/effects_overlay.dart';
|
||||
import '../widgets/hud_widget.dart';
|
||||
import '../widgets/piece_painter.dart';
|
||||
@@ -143,18 +144,9 @@ class _GameScreenState extends ConsumerState<GameScreen>
|
||||
await ref.read(gameSessionProvider.notifier).useShuffle();
|
||||
return;
|
||||
}
|
||||
// hammer / lineBomb need a board target.
|
||||
// hammer / lineBomb need a board target; the floating BoosterHint above
|
||||
// the board shows the prompt (and lets the player cancel) while armed.
|
||||
setState(() => _arming = type);
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final hint =
|
||||
type == BoosterType.hammer ? l10n.boosterTapTarget : l10n.boosterTapLine;
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(SnackBar(
|
||||
content: Text(hint),
|
||||
duration: const Duration(seconds: 3),
|
||||
));
|
||||
}
|
||||
|
||||
/// Converts a board tap into a cell, then applies the armed booster.
|
||||
@@ -410,32 +402,54 @@ class _GameScreenState extends ConsumerState<GameScreen>
|
||||
),
|
||||
HudWidget(view: view),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: AnimatedBuilder(
|
||||
animation: _shake,
|
||||
builder: (context, child) {
|
||||
final t = _shake.value;
|
||||
final dx =
|
||||
math.sin(t * math.pi * 10) * 6 * (1 - t);
|
||||
return Transform.translate(
|
||||
offset: Offset(dx, 0), child: child);
|
||||
},
|
||||
// While a targeted booster is armed, taps on the
|
||||
// board pick a cell. When not arming, onTapUp
|
||||
// returns immediately so it never steals the
|
||||
// tray-drag placement gestures.
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.deferToChild,
|
||||
onTapUp: _arming == null
|
||||
? null
|
||||
: (details) => _onBoardTapUp(details),
|
||||
child: BoardWidget(
|
||||
key: _boardKey,
|
||||
view: view,
|
||||
ghost: ghost,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Center(
|
||||
child: AnimatedBuilder(
|
||||
animation: _shake,
|
||||
builder: (context, child) {
|
||||
final t = _shake.value;
|
||||
final dx = math.sin(t * math.pi * 10) *
|
||||
6 *
|
||||
(1 - t);
|
||||
return Transform.translate(
|
||||
offset: Offset(dx, 0), child: child);
|
||||
},
|
||||
// While a targeted booster is armed, taps on
|
||||
// the board pick a cell. When not arming,
|
||||
// onTapUp returns immediately so it never
|
||||
// steals the tray-drag placement gestures.
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.deferToChild,
|
||||
onTapUp: _arming == null
|
||||
? null
|
||||
: (details) => _onBoardTapUp(details),
|
||||
child: BoardWidget(
|
||||
key: _boardKey,
|
||||
view: view,
|
||||
ghost: ghost,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Floating targeting prompt, in the empty space above
|
||||
// the centered board so it never covers cells.
|
||||
Positioned(
|
||||
top: 4,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: BoosterHint(
|
||||
arming: _arming,
|
||||
accent: ThemeColors(theme).accent,
|
||||
onCancel: () =>
|
||||
setState(() => _arming = null),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
TrayWidget(
|
||||
|
||||
Reference in New Issue
Block a user