696 lines
30 KiB
Dart
696 lines
30 KiB
Dart
|
|
// ===========================================================================
|
||
|
|
// FILE: lib/ui/game/game_screen.dart
|
||
|
|
// ===========================================================================
|
||
|
|
|
||
|
|
import 'dart:ui';
|
||
|
|
import 'dart:math' as math;
|
||
|
|
import 'package:flutter/material.dart';
|
||
|
|
import 'package:provider/provider.dart';
|
||
|
|
import '../../logic/game_controller.dart';
|
||
|
|
import '../../core/theme_manager.dart';
|
||
|
|
import '../../core/app_colors.dart';
|
||
|
|
import 'board_painter.dart';
|
||
|
|
import 'score_board.dart';
|
||
|
|
import '../../models/game_board.dart';
|
||
|
|
|
||
|
|
class GameScreen extends StatefulWidget {
|
||
|
|
const GameScreen({super.key});
|
||
|
|
|
||
|
|
@override
|
||
|
|
State<GameScreen> createState() => _GameScreenState();
|
||
|
|
}
|
||
|
|
|
||
|
|
class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
|
||
|
|
late AnimationController _blinkController;
|
||
|
|
bool _gameOverDialogShown = false;
|
||
|
|
bool _opponentLeftDialogShown = false;
|
||
|
|
|
||
|
|
@override
|
||
|
|
void initState() {
|
||
|
|
super.initState();
|
||
|
|
_blinkController = AnimationController(
|
||
|
|
vsync: this,
|
||
|
|
duration: const Duration(milliseconds: 600),
|
||
|
|
)..repeat(reverse: true);
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
void dispose() {
|
||
|
|
_blinkController.dispose();
|
||
|
|
super.dispose();
|
||
|
|
}
|
||
|
|
|
||
|
|
void _showGameOverDialog(BuildContext context, GameController game, ThemeColors theme, AppThemeType themeType) {
|
||
|
|
_gameOverDialogShown = true;
|
||
|
|
|
||
|
|
showDialog(
|
||
|
|
barrierDismissible: false,
|
||
|
|
context: context,
|
||
|
|
builder: (dialogContext) => Consumer<GameController>(
|
||
|
|
builder: (context, controller, child) {
|
||
|
|
if (!controller.isGameOver) {
|
||
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
|
|
if (Navigator.canPop(dialogContext)) Navigator.pop(dialogContext);
|
||
|
|
_gameOverDialogShown = false;
|
||
|
|
});
|
||
|
|
return const SizedBox();
|
||
|
|
}
|
||
|
|
|
||
|
|
int red = controller.board.scoreRed;
|
||
|
|
int blue = controller.board.scoreBlue;
|
||
|
|
bool playerBeatCPU = controller.isVsCPU && red > blue;
|
||
|
|
|
||
|
|
String nameRed = controller.isOnline ? controller.onlineHostName.toUpperCase() : "TU";
|
||
|
|
String nameBlue = controller.isOnline ? controller.onlineGuestName.toUpperCase() : (themeType == AppThemeType.cyberpunk ? "VERDE" : "BLU");
|
||
|
|
if (controller.isVsCPU) nameBlue = "CPU";
|
||
|
|
|
||
|
|
String winnerText = "";
|
||
|
|
Color winnerColor = theme.text;
|
||
|
|
if (red > blue) { winnerText = "VINCE $nameRed!"; winnerColor = theme.playerRed; }
|
||
|
|
else if (blue > red) { winnerText = "VINCE $nameBlue!"; winnerColor = theme.playerBlue; }
|
||
|
|
else { winnerText = "PAREGGIO!"; winnerColor = theme.text; }
|
||
|
|
|
||
|
|
return AlertDialog(
|
||
|
|
backgroundColor: theme.background,
|
||
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: BorderSide(color: winnerColor.withOpacity(0.5), width: 2)),
|
||
|
|
title: Text("FINE PARTITA", textAlign: TextAlign.center, style: TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 22)),
|
||
|
|
content: Column(
|
||
|
|
mainAxisSize: MainAxisSize.min,
|
||
|
|
children: [
|
||
|
|
Text(winnerText, textAlign: TextAlign.center, style: TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: winnerColor)),
|
||
|
|
const SizedBox(height: 20),
|
||
|
|
Container(
|
||
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||
|
|
decoration: BoxDecoration(color: theme.text.withOpacity(0.05), borderRadius: BorderRadius.circular(15)),
|
||
|
|
child: Row(
|
||
|
|
mainAxisSize: MainAxisSize.min,
|
||
|
|
children: [
|
||
|
|
Text("$nameRed: $red", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerRed)),
|
||
|
|
Text(" - ", style: TextStyle(fontSize: 18, color: theme.text)),
|
||
|
|
Text("$nameBlue: $blue", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerBlue)),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
if (controller.isVsCPU) ...[
|
||
|
|
const SizedBox(height: 15),
|
||
|
|
Text("Difficoltà CPU: Livello ${controller.cpuLevel}", style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: theme.text.withOpacity(0.7))),
|
||
|
|
],
|
||
|
|
if (controller.isOnline) ...[
|
||
|
|
const SizedBox(height: 20),
|
||
|
|
if (controller.rematchRequested && !controller.opponentWantsRematch)
|
||
|
|
Text("In attesa di $nameBlue...", style: TextStyle(color: Colors.amber, fontWeight: FontWeight.bold, fontStyle: FontStyle.italic)),
|
||
|
|
if (controller.opponentWantsRematch && !controller.rematchRequested)
|
||
|
|
Text("$nameBlue vuole la rivincita!", style: TextStyle(color: Colors.greenAccent, fontWeight: FontWeight.bold)),
|
||
|
|
if (controller.rematchRequested && controller.opponentWantsRematch)
|
||
|
|
Text("Avvio nuova partita...", style: TextStyle(color: Colors.green, fontWeight: FontWeight.bold)),
|
||
|
|
]
|
||
|
|
],
|
||
|
|
),
|
||
|
|
actionsPadding: const EdgeInsets.only(left: 20, right: 20, bottom: 20, top: 10),
|
||
|
|
actionsAlignment: MainAxisAlignment.center,
|
||
|
|
actions: [
|
||
|
|
Column(
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
|
|
children: [
|
||
|
|
if (playerBeatCPU)
|
||
|
|
ElevatedButton(
|
||
|
|
style: ElevatedButton.styleFrom(backgroundColor: winnerColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5),
|
||
|
|
onPressed: () { Navigator.pop(dialogContext); _gameOverDialogShown = false; controller.increaseLevelAndRestart(); },
|
||
|
|
child: const Text("PROSSIMO LIVELLO ➔", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||
|
|
)
|
||
|
|
else if (controller.isOnline)
|
||
|
|
ElevatedButton(
|
||
|
|
style: ElevatedButton.styleFrom(backgroundColor: controller.rematchRequested ? Colors.grey : (winnerColor == theme.text ? theme.playerBlue : winnerColor), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5),
|
||
|
|
onPressed: controller.rematchRequested ? null : () {
|
||
|
|
controller.requestRematch();
|
||
|
|
},
|
||
|
|
child: Text(controller.opponentWantsRematch ? "ACCETTA RIVINCITA" : "CHIEDI RIVINCITA", style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 1.0)),
|
||
|
|
)
|
||
|
|
else
|
||
|
|
ElevatedButton(
|
||
|
|
style: ElevatedButton.styleFrom(backgroundColor: winnerColor == theme.text ? theme.playerBlue : winnerColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5),
|
||
|
|
onPressed: () { Navigator.pop(dialogContext); _gameOverDialogShown = false; controller.startNewGame(controller.board.radius, vsCPU: controller.isVsCPU); },
|
||
|
|
child: const Text("RIGIOCA", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 2)),
|
||
|
|
),
|
||
|
|
const SizedBox(height: 12),
|
||
|
|
OutlinedButton(
|
||
|
|
style: OutlinedButton.styleFrom(foregroundColor: theme.text, side: BorderSide(color: theme.text.withOpacity(0.3), width: 2), padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))),
|
||
|
|
onPressed: () {
|
||
|
|
if (controller.isOnline) controller.disconnectOnlineGame();
|
||
|
|
Navigator.pop(dialogContext);
|
||
|
|
Navigator.pop(context);
|
||
|
|
},
|
||
|
|
child: Text("TORNA AL MENU", style: TextStyle(fontWeight: FontWeight.bold, color: theme.text, fontSize: 14, letterSpacing: 1.5)),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
],
|
||
|
|
);
|
||
|
|
}
|
||
|
|
)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
final themeManager = context.watch<ThemeManager>();
|
||
|
|
final themeType = themeManager.currentThemeType;
|
||
|
|
final theme = themeManager.currentColors;
|
||
|
|
final gameController = context.watch<GameController>();
|
||
|
|
|
||
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
|
|
if (gameController.opponentLeft && !_opponentLeftDialogShown) {
|
||
|
|
_opponentLeftDialogShown = true;
|
||
|
|
showDialog(
|
||
|
|
barrierDismissible: false,
|
||
|
|
context: context,
|
||
|
|
builder: (dialogContext) => AlertDialog(
|
||
|
|
backgroundColor: theme.background,
|
||
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||
|
|
title: Text("VITTORIA A TAVOLINO!", textAlign: TextAlign.center, style: TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold)),
|
||
|
|
content: Text("L'avversario ha abbandonato la stanza.\nSei il vincitore incontestato!", textAlign: TextAlign.center, style: TextStyle(color: theme.text, fontSize: 16)),
|
||
|
|
actionsAlignment: MainAxisAlignment.center,
|
||
|
|
actions: [
|
||
|
|
ElevatedButton(
|
||
|
|
style: ElevatedButton.styleFrom(backgroundColor: theme.playerBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))),
|
||
|
|
onPressed: () { gameController.disconnectOnlineGame(); Navigator.pop(dialogContext); Navigator.pop(context); },
|
||
|
|
child: const Text("MENU PRINCIPALE", style: TextStyle(fontWeight: FontWeight.bold)),
|
||
|
|
)
|
||
|
|
],
|
||
|
|
)
|
||
|
|
);
|
||
|
|
} else if (gameController.board.isGameOver && !_gameOverDialogShown) {
|
||
|
|
_showGameOverDialog(context, gameController, theme, themeType);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
String? bgImage;
|
||
|
|
if (themeType == AppThemeType.wood) bgImage = 'assets/images/wood_bg.jpg';
|
||
|
|
if (themeType == AppThemeType.doodle) bgImage = 'assets/images/doodle_bg.jpg';
|
||
|
|
|
||
|
|
Color indicatorColor = themeType == AppThemeType.cyberpunk ? Colors.white : Colors.black;
|
||
|
|
|
||
|
|
Widget emojiBar = const SizedBox();
|
||
|
|
if (gameController.isOnline && !gameController.isGameOver) {
|
||
|
|
final List<String> emojis = ['😂', '😡', '😱', '🥳', '👀'];
|
||
|
|
emojiBar = Container(
|
||
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: themeType == AppThemeType.cyberpunk ? Colors.black.withOpacity(0.6) : Colors.white.withOpacity(0.8),
|
||
|
|
borderRadius: BorderRadius.circular(30),
|
||
|
|
border: Border.all(color: themeType == AppThemeType.cyberpunk ? theme.playerBlue.withOpacity(0.3) : Colors.black12, width: 2),
|
||
|
|
),
|
||
|
|
child: Row(
|
||
|
|
mainAxisSize: MainAxisSize.min,
|
||
|
|
children: emojis.map((e) => GestureDetector(
|
||
|
|
onTap: () => gameController.sendReaction(e),
|
||
|
|
child: Padding(padding: const EdgeInsets.symmetric(horizontal: 6), child: Text(e, style: const TextStyle(fontSize: 22))),
|
||
|
|
)).toList(),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget gameContent = SafeArea(
|
||
|
|
child: Stack(
|
||
|
|
children: [
|
||
|
|
Column(
|
||
|
|
children: [
|
||
|
|
const ScoreBoard(),
|
||
|
|
Expanded(
|
||
|
|
child: Center(
|
||
|
|
child: Padding(
|
||
|
|
padding: const EdgeInsets.all(10.0),
|
||
|
|
child: AspectRatio(
|
||
|
|
aspectRatio: 1,
|
||
|
|
child: LayoutBuilder(
|
||
|
|
builder: (context, constraints) {
|
||
|
|
return GestureDetector(
|
||
|
|
behavior: HitTestBehavior.opaque,
|
||
|
|
onTapDown: (details) => _handleTap(details.localPosition, constraints.maxWidth, gameController, themeType),
|
||
|
|
child: AnimatedBuilder(
|
||
|
|
animation: _blinkController,
|
||
|
|
builder: (context, child) {
|
||
|
|
return CustomPaint(
|
||
|
|
size: Size(constraints.maxWidth, constraints.maxHeight),
|
||
|
|
painter: BoardPainter(board: gameController.board, theme: theme, themeType: themeType, blinkValue: _blinkController.value),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
|
||
|
|
Padding(
|
||
|
|
padding: const EdgeInsets.only(bottom: 20.0, left: 20.0, right: 20.0),
|
||
|
|
child: Row(
|
||
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
|
|
children: [
|
||
|
|
if (gameController.isVsCPU)
|
||
|
|
Container(
|
||
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||
|
|
decoration: BoxDecoration(color: indicatorColor.withOpacity(0.1), borderRadius: BorderRadius.circular(20), border: Border.all(color: indicatorColor.withOpacity(0.3))),
|
||
|
|
child: Row(
|
||
|
|
mainAxisSize: MainAxisSize.min,
|
||
|
|
children: [
|
||
|
|
Icon(Icons.smart_toy_rounded, size: 16, color: indicatorColor), const SizedBox(width: 8),
|
||
|
|
Text("LIVELLO CPU: ${gameController.cpuLevel}", style: TextStyle(color: indicatorColor, fontWeight: FontWeight.bold, fontSize: 13, letterSpacing: 1.0)),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
)
|
||
|
|
else
|
||
|
|
emojiBar,
|
||
|
|
|
||
|
|
Container(
|
||
|
|
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.4), offset: const Offset(0, 4), blurRadius: 5)]),
|
||
|
|
child: TextButton.icon(
|
||
|
|
style: TextButton.styleFrom(backgroundColor: bgImage != null ? Colors.black87 : theme.background, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: BorderSide(color: Colors.white.withOpacity(0.1), width: 1))),
|
||
|
|
icon: Icon(Icons.exit_to_app, color: bgImage != null ? Colors.white : theme.text, size: 20),
|
||
|
|
onPressed: () { gameController.disconnectOnlineGame(); Navigator.pop(context); },
|
||
|
|
label: Text("ESCI", style: TextStyle(color: bgImage != null ? Colors.white : theme.text, fontWeight: FontWeight.bold, fontSize: 14)),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
)
|
||
|
|
],
|
||
|
|
),
|
||
|
|
|
||
|
|
if (gameController.myReaction != null)
|
||
|
|
Positioned(
|
||
|
|
top: 80,
|
||
|
|
left: gameController.isHost ? 30 : null,
|
||
|
|
right: gameController.isHost ? null : 30,
|
||
|
|
child: _BouncingEmoji(emoji: gameController.myReaction!),
|
||
|
|
),
|
||
|
|
if (gameController.opponentReaction != null)
|
||
|
|
Positioned(
|
||
|
|
top: 80,
|
||
|
|
left: !gameController.isHost ? 30 : null,
|
||
|
|
right: !gameController.isHost ? null : 30,
|
||
|
|
child: _BouncingEmoji(emoji: gameController.opponentReaction!),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
);
|
||
|
|
|
||
|
|
return PopScope(
|
||
|
|
canPop: true,
|
||
|
|
onPopInvoked: (didPop) { gameController.disconnectOnlineGame(); },
|
||
|
|
child: Scaffold(
|
||
|
|
backgroundColor: bgImage != null ? Colors.transparent : theme.background,
|
||
|
|
body: CustomPaint(
|
||
|
|
painter: themeType == AppThemeType.minimal ? FullScreenGridPainter(Colors.black.withOpacity(0.06)) : null,
|
||
|
|
child: Container(
|
||
|
|
decoration: bgImage != null ? BoxDecoration(image: DecorationImage(image: AssetImage(bgImage), fit: BoxFit.cover, colorFilter: themeType == AppThemeType.doodle ? ColorFilter.mode(Colors.white.withOpacity(0.7), BlendMode.lighten) : null)) : null,
|
||
|
|
child: Stack(
|
||
|
|
children: [
|
||
|
|
if (gameController.isTimeMode && !gameController.isCPUThinking && !gameController.isGameOver && gameController.timeLeft > 0 && gameController.timeLeft <= 5)
|
||
|
|
Positioned.fill(child: BlitzBackgroundEffect(timeLeft: gameController.timeLeft, color: theme.playerRed)),
|
||
|
|
|
||
|
|
if (gameController.effectText.isNotEmpty)
|
||
|
|
Positioned.fill(child: SpecialEventBackgroundEffect(text: gameController.effectText, color: gameController.effectColor)),
|
||
|
|
|
||
|
|
Positioned.fill(child: gameContent),
|
||
|
|
|
||
|
|
// ==========================================
|
||
|
|
// EFFETTI VISIVI (VFX) DI FINE PARTITA
|
||
|
|
// ==========================================
|
||
|
|
if (gameController.isGameOver && gameController.board.scoreRed != gameController.board.scoreBlue)
|
||
|
|
Positioned.fill(
|
||
|
|
child: IgnorePointer(
|
||
|
|
child: WinnerVFXOverlay(
|
||
|
|
winnerColor: gameController.board.scoreRed > gameController.board.scoreBlue ? theme.playerRed : theme.playerBlue,
|
||
|
|
themeType: themeType,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
void _handleTap(Offset tapPos, double size, GameController controller, AppThemeType themeType) {
|
||
|
|
final board = controller.board;
|
||
|
|
if (board.isGameOver) return;
|
||
|
|
|
||
|
|
int gridPoints = board.radius * 2 + 2;
|
||
|
|
double spacing = size / gridPoints;
|
||
|
|
double offset = spacing / 2;
|
||
|
|
|
||
|
|
Line? closestLine;
|
||
|
|
double minDistance = double.infinity;
|
||
|
|
double maxTouchDistance = spacing * 0.4;
|
||
|
|
|
||
|
|
for (var line in board.lines) {
|
||
|
|
if (line.owner != Player.none || !line.isPlayable) continue;
|
||
|
|
|
||
|
|
Offset screenP1 = Offset(line.p1.x * spacing + offset, line.p1.y * spacing + offset);
|
||
|
|
Offset screenP2 = Offset(line.p2.x * spacing + offset, line.p2.y * spacing + offset);
|
||
|
|
|
||
|
|
double dist = _distanceToSegment(tapPos, screenP1, screenP2);
|
||
|
|
|
||
|
|
if (dist < minDistance && dist < maxTouchDistance) { minDistance = dist; closestLine = line; }
|
||
|
|
}
|
||
|
|
|
||
|
|
if (closestLine != null) { controller.handleLineTap(closestLine, themeType); }
|
||
|
|
}
|
||
|
|
|
||
|
|
double _distanceToSegment(Offset p, Offset a, Offset b) {
|
||
|
|
double l2 = (a.dx - b.dx) * (a.dx - b.dx) + (a.dy - b.dy) * (a.dy - b.dy);
|
||
|
|
if (l2 == 0) return (p - a).distance;
|
||
|
|
double t = (((p.dx - a.dx) * (b.dx - a.dx) + (p.dy - a.dy) * (b.dy - a.dy)) / l2).clamp(0.0, 1.0);
|
||
|
|
Offset projection = Offset(a.dx + t * (b.dx - a.dx), a.dy + t * (b.dy - a.dy));
|
||
|
|
return (p - projection).distance;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===========================================================================
|
||
|
|
// CLASSI PER IL MOTORE PARTICELLARE (VFX) DI FINE PARTITA
|
||
|
|
// ===========================================================================
|
||
|
|
|
||
|
|
class _Particle {
|
||
|
|
double x, y;
|
||
|
|
double vx, vy;
|
||
|
|
Color color;
|
||
|
|
double size;
|
||
|
|
double angle;
|
||
|
|
double spin;
|
||
|
|
int type; // 0=cerchio, 1=quadrato, 2=triangolo
|
||
|
|
|
||
|
|
_Particle({required this.x, required this.y, required this.vx, required this.vy, required this.color, required this.size, required this.angle, required this.spin, required this.type});
|
||
|
|
}
|
||
|
|
|
||
|
|
class WinnerVFXOverlay extends StatefulWidget {
|
||
|
|
final Color winnerColor;
|
||
|
|
final AppThemeType themeType;
|
||
|
|
|
||
|
|
const WinnerVFXOverlay({super.key, required this.winnerColor, required this.themeType});
|
||
|
|
|
||
|
|
@override
|
||
|
|
State<WinnerVFXOverlay> createState() => _WinnerVFXOverlayState();
|
||
|
|
}
|
||
|
|
|
||
|
|
class _WinnerVFXOverlayState extends State<WinnerVFXOverlay> with SingleTickerProviderStateMixin {
|
||
|
|
late AnimationController _vfxController;
|
||
|
|
final List<_Particle> _particles = [];
|
||
|
|
final math.Random _rand = math.Random();
|
||
|
|
bool _initialized = false;
|
||
|
|
|
||
|
|
@override
|
||
|
|
void initState() {
|
||
|
|
super.initState();
|
||
|
|
// L'animazione gira a 60fps per 4 secondi e poi si ferma
|
||
|
|
_vfxController = AnimationController(vsync: this, duration: const Duration(seconds: 4))
|
||
|
|
..addListener(() {
|
||
|
|
_updateParticles();
|
||
|
|
})
|
||
|
|
..forward();
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
void didChangeDependencies() {
|
||
|
|
super.didChangeDependencies();
|
||
|
|
if (!_initialized) {
|
||
|
|
_initParticles(MediaQuery.of(context).size);
|
||
|
|
_initialized = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void _initParticles(Size screenSize) {
|
||
|
|
int particleCount = widget.themeType == AppThemeType.cyberpunk ? 150 : 100;
|
||
|
|
|
||
|
|
// Lista di colori da mixare (colore vincitore + bianco + colori a tema)
|
||
|
|
List<Color> palette = [widget.winnerColor, widget.winnerColor.withOpacity(0.7), Colors.white];
|
||
|
|
if (widget.themeType == AppThemeType.cyberpunk) {
|
||
|
|
palette.add(Colors.cyanAccent);
|
||
|
|
palette.add(Colors.yellowAccent);
|
||
|
|
} else if (widget.themeType == AppThemeType.doodle) {
|
||
|
|
palette.add(const Color(0xFF00008B)); // Inchiostro biro
|
||
|
|
palette.add(Colors.redAccent);
|
||
|
|
} else if (widget.themeType == AppThemeType.wood) {
|
||
|
|
palette = [Colors.orangeAccent, Colors.yellow, Colors.red, Colors.white];
|
||
|
|
}
|
||
|
|
|
||
|
|
for (int i = 0; i < particleCount; i++) {
|
||
|
|
// Esplosione dal centro verso l'esterno
|
||
|
|
double speed = _rand.nextDouble() * 20 + 5;
|
||
|
|
double theta = _rand.nextDouble() * 2 * math.pi;
|
||
|
|
|
||
|
|
_particles.add(_Particle(
|
||
|
|
x: screenSize.width / 2,
|
||
|
|
y: screenSize.height / 2,
|
||
|
|
vx: speed * math.cos(theta),
|
||
|
|
vy: speed * math.sin(theta) - 5, // Leggera spinta verso l'alto
|
||
|
|
color: palette[_rand.nextInt(palette.length)],
|
||
|
|
size: _rand.nextDouble() * 10 + 6,
|
||
|
|
angle: _rand.nextDouble() * math.pi,
|
||
|
|
spin: (_rand.nextDouble() - 0.5) * 0.5,
|
||
|
|
type: _rand.nextInt(3),
|
||
|
|
));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void _updateParticles() {
|
||
|
|
setState(() {
|
||
|
|
for (var p in _particles) {
|
||
|
|
p.x += p.vx;
|
||
|
|
p.y += p.vy;
|
||
|
|
|
||
|
|
// Gravità e attrito
|
||
|
|
if (widget.themeType == AppThemeType.cyberpunk) {
|
||
|
|
p.vy += 0.1; // Gravità bassa (fluttuano di più)
|
||
|
|
p.vx *= 0.98; // Attrito
|
||
|
|
p.vy *= 0.98;
|
||
|
|
} else if (widget.themeType == AppThemeType.wood) {
|
||
|
|
p.vy -= 0.2; // Vanno verso l'alto come fumo/scintille!
|
||
|
|
p.x += math.sin(p.y * 0.05) * 2; // Tremolio
|
||
|
|
} else {
|
||
|
|
p.vy += 0.5; // Gravità standard (coriandoli cadono)
|
||
|
|
}
|
||
|
|
|
||
|
|
p.angle += p.spin;
|
||
|
|
p.size *= 0.99; // Si rimpiccioliscono nel tempo
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
void dispose() {
|
||
|
|
_vfxController.dispose();
|
||
|
|
super.dispose();
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
return CustomPaint(
|
||
|
|
painter: _VFXPainter(particles: _particles, themeType: widget.themeType),
|
||
|
|
child: Container(),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class _VFXPainter extends CustomPainter {
|
||
|
|
final List<_Particle> particles;
|
||
|
|
final AppThemeType themeType;
|
||
|
|
|
||
|
|
_VFXPainter({required this.particles, required this.themeType});
|
||
|
|
|
||
|
|
@override
|
||
|
|
void paint(Canvas canvas, Size size) {
|
||
|
|
for (var p in particles) {
|
||
|
|
if (p.size < 0.5) continue;
|
||
|
|
|
||
|
|
final paint = Paint()
|
||
|
|
..color = p.color
|
||
|
|
..style = PaintingStyle.fill;
|
||
|
|
|
||
|
|
// Glow per il Cyberpunk
|
||
|
|
if (themeType == AppThemeType.cyberpunk) {
|
||
|
|
paint.maskFilter = const MaskFilter.blur(BlurStyle.solid, 4.0);
|
||
|
|
}
|
||
|
|
|
||
|
|
canvas.save();
|
||
|
|
canvas.translate(p.x, p.y);
|
||
|
|
canvas.rotate(p.angle);
|
||
|
|
|
||
|
|
if (themeType == AppThemeType.doodle) {
|
||
|
|
// Stile schizzato
|
||
|
|
paint.style = PaintingStyle.stroke;
|
||
|
|
paint.strokeWidth = 2.0;
|
||
|
|
if (p.type == 0) {
|
||
|
|
canvas.drawCircle(Offset.zero, p.size, paint);
|
||
|
|
} else {
|
||
|
|
canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: p.size*2, height: p.size*2), paint);
|
||
|
|
}
|
||
|
|
} else if (themeType == AppThemeType.wood) {
|
||
|
|
// Scintille rotonde e sfuocate
|
||
|
|
paint.maskFilter = const MaskFilter.blur(BlurStyle.normal, 3.0);
|
||
|
|
canvas.drawCircle(Offset.zero, p.size, paint);
|
||
|
|
} else {
|
||
|
|
// Forme standard per Minimal e Cyberpunk
|
||
|
|
if (p.type == 0) {
|
||
|
|
canvas.drawCircle(Offset.zero, p.size, paint);
|
||
|
|
} else if (p.type == 1) {
|
||
|
|
canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: p.size * 2, height: p.size * 2), paint);
|
||
|
|
} else {
|
||
|
|
var path = Path()
|
||
|
|
..moveTo(0, -p.size)
|
||
|
|
..lineTo(p.size, p.size)
|
||
|
|
..lineTo(-p.size, p.size)
|
||
|
|
..close();
|
||
|
|
canvas.drawPath(path, paint);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
canvas.restore();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
bool shouldRepaint(covariant _VFXPainter oldDelegate) => true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===========================================================================
|
||
|
|
// WIDGET INTERNI ESISTENTENTI
|
||
|
|
// ===========================================================================
|
||
|
|
|
||
|
|
class _BouncingEmoji extends StatefulWidget {
|
||
|
|
final String emoji;
|
||
|
|
const _BouncingEmoji({required this.emoji});
|
||
|
|
|
||
|
|
@override
|
||
|
|
State<_BouncingEmoji> createState() => _BouncingEmojiState();
|
||
|
|
}
|
||
|
|
|
||
|
|
class _BouncingEmojiState extends State<_BouncingEmoji> with SingleTickerProviderStateMixin {
|
||
|
|
late AnimationController _ctrl;
|
||
|
|
late Animation<double> _anim;
|
||
|
|
|
||
|
|
@override
|
||
|
|
void initState() {
|
||
|
|
super.initState();
|
||
|
|
_ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 500))..repeat(reverse: true);
|
||
|
|
_anim = Tween<double>(begin: -10, end: 10).animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeInOut));
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
void dispose() { _ctrl.dispose(); super.dispose(); }
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
return AnimatedBuilder(
|
||
|
|
animation: _anim,
|
||
|
|
builder: (ctx, child) => Transform.translate(
|
||
|
|
offset: Offset(0, _anim.value),
|
||
|
|
child: Container(
|
||
|
|
padding: const EdgeInsets.all(8),
|
||
|
|
decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle, boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 5)]),
|
||
|
|
child: Text(widget.emoji, style: const TextStyle(fontSize: 32)),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class FullScreenGridPainter extends CustomPainter {
|
||
|
|
final Color gridColor;
|
||
|
|
FullScreenGridPainter(this.gridColor);
|
||
|
|
|
||
|
|
@override
|
||
|
|
void paint(Canvas canvas, Size size) {
|
||
|
|
final Paint paperGridPaint = Paint()..color = gridColor..strokeWidth = 1.0..style = PaintingStyle.stroke;
|
||
|
|
double paperStep = 20.0;
|
||
|
|
for (double i = 0; i <= size.width; i += paperStep) canvas.drawLine(Offset(i, 0), Offset(i, size.height), paperGridPaint);
|
||
|
|
for (double i = 0; i <= size.height; i += paperStep) canvas.drawLine(Offset(0, i), Offset(size.width, i), paperGridPaint);
|
||
|
|
}
|
||
|
|
@override
|
||
|
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||
|
|
}
|
||
|
|
|
||
|
|
class BlitzBackgroundEffect extends StatefulWidget {
|
||
|
|
final int timeLeft;
|
||
|
|
final Color color;
|
||
|
|
const BlitzBackgroundEffect({super.key, required this.timeLeft, required this.color});
|
||
|
|
@override
|
||
|
|
State<BlitzBackgroundEffect> createState() => _BlitzBackgroundEffectState();
|
||
|
|
}
|
||
|
|
|
||
|
|
class _BlitzBackgroundEffectState extends State<BlitzBackgroundEffect> with SingleTickerProviderStateMixin {
|
||
|
|
late AnimationController _controller;
|
||
|
|
@override
|
||
|
|
void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 400))..repeat(reverse: true); }
|
||
|
|
@override
|
||
|
|
void dispose() { _controller.dispose(); super.dispose(); }
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
return AnimatedBuilder(
|
||
|
|
animation: _controller,
|
||
|
|
builder: (context, child) {
|
||
|
|
return Container(
|
||
|
|
color: widget.color.withOpacity(0.12 * _controller.value),
|
||
|
|
child: Center(
|
||
|
|
child: ImageFiltered(
|
||
|
|
imageFilter: ImageFilter.blur(sigmaX: 2.0, sigmaY: 2.0),
|
||
|
|
child: Text('${widget.timeLeft}', style: TextStyle(fontSize: 300, fontWeight: FontWeight.w900, color: widget.color.withOpacity(0.35 + (0.3 * _controller.value)), height: 1.0)),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class SpecialEventBackgroundEffect extends StatefulWidget {
|
||
|
|
final String text;
|
||
|
|
final Color color;
|
||
|
|
const SpecialEventBackgroundEffect({super.key, required this.text, required this.color});
|
||
|
|
@override
|
||
|
|
State<SpecialEventBackgroundEffect> createState() => _SpecialEventBackgroundEffectState();
|
||
|
|
}
|
||
|
|
|
||
|
|
class _SpecialEventBackgroundEffectState extends State<SpecialEventBackgroundEffect> with SingleTickerProviderStateMixin {
|
||
|
|
late AnimationController _controller;
|
||
|
|
late Animation<double> _scaleAnimation;
|
||
|
|
late Animation<double> _opacityAnimation;
|
||
|
|
|
||
|
|
@override
|
||
|
|
void initState() {
|
||
|
|
super.initState();
|
||
|
|
_controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 1000))..forward();
|
||
|
|
_scaleAnimation = Tween<double>(begin: 0.5, end: 1.5).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic));
|
||
|
|
_opacityAnimation = Tween<double>(begin: 0.9, end: 0.0).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn));
|
||
|
|
}
|
||
|
|
@override
|
||
|
|
void didUpdateWidget(covariant SpecialEventBackgroundEffect oldWidget) {
|
||
|
|
super.didUpdateWidget(oldWidget);
|
||
|
|
if (oldWidget.text != widget.text) { _controller.reset(); _controller.forward(); }
|
||
|
|
}
|
||
|
|
@override
|
||
|
|
void dispose() { _controller.dispose(); super.dispose(); }
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
return AnimatedBuilder(
|
||
|
|
animation: _controller,
|
||
|
|
builder: (context, child) {
|
||
|
|
return Center(
|
||
|
|
child: Transform.scale(
|
||
|
|
scale: _scaleAnimation.value,
|
||
|
|
child: Opacity(
|
||
|
|
opacity: _opacityAnimation.value,
|
||
|
|
child: ImageFiltered(
|
||
|
|
imageFilter: ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0),
|
||
|
|
child: Text(widget.text, style: TextStyle(fontSize: 250, fontWeight: FontWeight.w900, color: widget.color, height: 1.0)),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|