Files
999/lib/widgets/d10_dice_roller_widget.dart
T

307 lines
9.4 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'gradient_star_badge.dart';
class D10DiceRollerWidget extends StatefulWidget {
final String digits; // Current 0..3 digits string, e.g. "7", "74", "742" or ""
final bool isRolling;
final VoidCallback onRollPressed;
final ValueChanged<int> onRollCompleted;
const D10DiceRollerWidget({
super.key,
required this.digits,
required this.isRolling,
required this.onRollPressed,
required this.onRollCompleted,
});
@override
State<D10DiceRollerWidget> createState() => D10DiceRollerWidgetState();
}
class D10DiceRollerWidgetState extends State<D10DiceRollerWidget> {
final _random = Random();
Timer? _rollTimer;
int? _animatedDigit0;
int? _animatedDigit1;
int? _animatedDigit2;
bool _internalRolling = false;
void triggerRollAnimation() {
if (_internalRolling) return;
setState(() {
_internalRolling = true;
_animatedDigit0 = _random.nextInt(10);
_animatedDigit1 = _random.nextInt(10);
_animatedDigit2 = _random.nextInt(10);
});
final target0 = _random.nextInt(10);
final target1 = _random.nextInt(10);
final target2 = _random.nextInt(10);
int ticks = 0;
const interval = Duration(milliseconds: 60);
_rollTimer?.cancel();
_rollTimer = Timer.periodic(interval, (timer) {
ticks++;
// Slot 0 stops after 10 ticks (~600ms)
if (ticks < 10) {
setState(() => _animatedDigit0 = _random.nextInt(10));
} else if (ticks == 10) {
setState(() => _animatedDigit0 = target0);
HapticFeedback.mediumImpact();
}
// Slot 1 stops after 18 ticks (~1080ms)
if (ticks < 18) {
setState(() => _animatedDigit1 = _random.nextInt(10));
} else if (ticks == 18) {
setState(() => _animatedDigit1 = target1);
HapticFeedback.mediumImpact();
}
// Slot 2 stops after 26 ticks (~1560ms)
if (ticks < 26) {
setState(() => _animatedDigit2 = _random.nextInt(10));
} else if (ticks == 26) {
setState(() => _animatedDigit2 = target2);
HapticFeedback.heavyImpact();
}
if (ticks >= 28) {
timer.cancel();
setState(() => _internalRolling = false);
final finalNumber = target0 * 100 + target1 * 10 + target2;
widget.onRollCompleted(finalNumber);
}
});
}
@override
void dispose() {
_rollTimer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
// Determine current display values for the 3 slots
String d0 = '•';
String d1 = '•';
String d2 = '•';
if (_internalRolling) {
d0 = _animatedDigit0 != null ? '$_animatedDigit0' : '•';
d1 = _animatedDigit1 != null ? '$_animatedDigit1' : '•';
d2 = _animatedDigit2 != null ? '$_animatedDigit2' : '•';
} else {
final text = widget.digits;
if (text.isNotEmpty) d0 = text[0];
if (text.length > 1) d1 = text[1];
if (text.length > 2) d2 = text[2];
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF181B2E) : theme.colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(22),
border: Border.all(
color: const Color(0xFFFFD54F).withValues(alpha: 0.3),
width: 1.2,
),
),
child: Column(
children: [
Row(
children: [
const GradientStar(size: 15),
const SizedBox(width: 6),
const Expanded(
child: Text(
'КЛЕРОМАНТИЯ • 3× d10',
style: TextStyle(
fontSize: 11.5,
fontWeight: FontWeight.w900,
letterSpacing: 0.5,
color: Color(0xFFFFD54F),
),
),
),
Text(
_internalRolling
? 'Бросок...'
: (widget.digits.length == 3 ? 'Зафиксировано' : '3 цифры'),
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: _internalRolling
? const Color(0xFFBA68C8)
: theme.colorScheme.outline,
),
),
],
),
const SizedBox(height: 12),
// 3 Dice Slots Display
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildDieSlot(
title: '1-й d10 (Сотни)',
value: d0,
isActive: widget.digits.isNotEmpty || _internalRolling,
isDark: isDark,
),
const SizedBox(width: 12),
_buildDieSlot(
title: '2-й d10 (Десятки)',
value: d1,
isActive: widget.digits.length >= 2 || _internalRolling,
isDark: isDark,
),
const SizedBox(width: 12),
_buildDieSlot(
title: '3-й d10 (Единицы)',
value: d2,
isActive: widget.digits.length >= 3 || _internalRolling,
isDark: isDark,
),
],
),
const SizedBox(height: 12),
// Action Button: Roll 3xd10
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: _internalRolling
? null
: () {
triggerRollAnimation();
widget.onRollPressed();
},
icon: _internalRolling
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFFFD54F)),
),
)
: const Icon(Icons.casino_rounded, size: 20, color: Color(0xFFFFD54F)),
label: Text(
_internalRolling ? 'ВРАЩЕНИЕ ЖРЕБИЯ...' : 'СОВЕРШИТЬ 3 БРОСКА d10',
style: const TextStyle(
fontWeight: FontWeight.w900,
letterSpacing: 0.6,
color: Color(0xFFFFF9C4),
),
),
style: FilledButton.styleFrom(
backgroundColor: const Color(0xFF2A2D4A),
padding: const EdgeInsets.symmetric(vertical: 13),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(
color: Color(0xFFFFD54F),
width: 1.2,
),
),
),
),
),
],
),
);
}
Widget _buildDieSlot({
required String title,
required String value,
required bool isActive,
required bool isDark,
}) {
final isFilled = value != '•';
return Expanded(
child: Column(
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 200),
height: 64,
alignment: Alignment.center,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isFilled
? [
const Color(0xFF2E2452),
const Color(0xFF1E1B38),
]
: [
isDark ? const Color(0xFF131525) : const Color(0xFFEAEBF5),
isDark ? const Color(0xFF0F111E) : const Color(0xFFDEDFEF),
],
),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isFilled
? const Color(0xFFFFD54F)
: const Color(0xFFFFD54F).withValues(alpha: 0.25),
width: isFilled ? 1.6 : 1.0,
),
boxShadow: isFilled
? [
BoxShadow(
color: const Color(0xFFFFD54F).withValues(alpha: 0.2),
blurRadius: 10,
spreadRadius: 1,
),
]
: null,
),
child: Text(
value,
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.w900,
color: isFilled
? const Color(0xFFFFF9C4)
: isDark
? Colors.white24
: Colors.black26,
),
),
),
const SizedBox(height: 6),
Text(
title,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 10.5,
fontWeight: FontWeight.w600,
color: Color(0xFFFFD54F),
),
),
],
),
);
}
}