feat: juice kit - sparks, score popups, combo banners, confetti, haptics, shake
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../game/engine/game_engine.dart';
|
||||
@@ -10,6 +13,7 @@ import '../theme/palette.dart';
|
||||
import '../widgets/board_geometry.dart';
|
||||
import '../widgets/board_painter.dart';
|
||||
import '../widgets/board_widget.dart';
|
||||
import '../widgets/effects_overlay.dart';
|
||||
import '../widgets/hud_widget.dart';
|
||||
import '../widgets/piece_painter.dart';
|
||||
import '../widgets/season_background.dart';
|
||||
@@ -24,9 +28,15 @@ class GameScreen extends ConsumerStatefulWidget {
|
||||
ConsumerState<GameScreen> createState() => _GameScreenState();
|
||||
}
|
||||
|
||||
class _GameScreenState extends ConsumerState<GameScreen> {
|
||||
class _GameScreenState extends ConsumerState<GameScreen>
|
||||
with TickerProviderStateMixin {
|
||||
final _boardKey = GlobalKey();
|
||||
final _stackKey = GlobalKey();
|
||||
final _effectsKey = GlobalKey<EffectsOverlayState>();
|
||||
late final AnimationController _shake = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 350),
|
||||
);
|
||||
|
||||
int? _dragIndex;
|
||||
Offset? _dragGlobal;
|
||||
@@ -94,6 +104,12 @@ class _GameScreenState extends ConsumerState<GameScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_shake.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onSessionChange(GameViewState? prev, GameViewState? next) {
|
||||
if (next == null) return;
|
||||
final audio = ref.read(audioServiceProvider);
|
||||
@@ -101,8 +117,25 @@ class _GameScreenState extends ConsumerState<GameScreen> {
|
||||
final placement = next.lastPlacement!;
|
||||
if (placement.linesCleared > 0) {
|
||||
audio.play(placement.comboStreak >= 2 ? Sfx.combo : Sfx.clear);
|
||||
HapticFeedback.mediumImpact();
|
||||
if (placement.comboStreak >= 4) {
|
||||
HapticFeedback.heavyImpact();
|
||||
_shake.forward(from: 0);
|
||||
}
|
||||
} else {
|
||||
audio.play(Sfx.place);
|
||||
HapticFeedback.lightImpact();
|
||||
}
|
||||
final boardBox = _boardBox;
|
||||
final stackBox =
|
||||
_stackKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
if (boardBox != null && stackBox != null) {
|
||||
final topLeft =
|
||||
stackBox.globalToLocal(boardBox.localToGlobal(Offset.zero));
|
||||
_effectsKey.currentState?.onPlacement(
|
||||
placement,
|
||||
boardRect: topLeft & boardBox.size,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (prev?.phase != next.phase) {
|
||||
@@ -112,6 +145,11 @@ class _GameScreenState extends ConsumerState<GameScreen> {
|
||||
ref
|
||||
.read(seasonFlowProvider.notifier)
|
||||
.recordWin(stars: next.starsEarned, score: next.score);
|
||||
final stackBox =
|
||||
_stackKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
if (stackBox != null) {
|
||||
_effectsKey.currentState?.onWin(stackBox.size);
|
||||
}
|
||||
}
|
||||
if (next.phase == GamePhase.lost) audio.play(Sfx.lose);
|
||||
if (next.phase == GamePhase.won || next.phase == GamePhase.lost) {
|
||||
@@ -159,10 +197,20 @@ class _GameScreenState extends ConsumerState<GameScreen> {
|
||||
HudWidget(view: view),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: BoardWidget(
|
||||
key: _boardKey,
|
||||
view: view,
|
||||
ghost: ghost,
|
||||
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);
|
||||
},
|
||||
child: BoardWidget(
|
||||
key: _boardKey,
|
||||
view: view,
|
||||
ghost: ghost,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -185,6 +233,7 @@ class _GameScreenState extends ConsumerState<GameScreen> {
|
||||
boardBox != null &&
|
||||
_dragIndex! < view.tray.length)
|
||||
_draggedPieceOverlay(view, draggedTopLeft, boardBox),
|
||||
Positioned.fill(child: EffectsOverlay(key: _effectsKey)),
|
||||
if (view.phase != GamePhase.playing) _resultOverlay(view),
|
||||
if (Navigator.of(context).canPop())
|
||||
Positioned(
|
||||
|
||||
Reference in New Issue
Block a user