From e4bd7475cf527ff715c13e4e3f00417062c239aa Mon Sep 17 00:00:00 2001 From: Alma Alexyz Date: Mon, 31 Aug 2026 14:23:08 +0500 Subject: [PATCH] feat: 3xd10 cleromancy input flow, animated dice roll sequence, and cleromancy guide dialog --- lib/screens/home_search_screen.dart | 166 ++++++++----- lib/widgets/cleromancy_dialog.dart | 193 +++++++++++++++ lib/widgets/d10_dice_roller_widget.dart | 309 ++++++++++++++++++++++++ 3 files changed, 601 insertions(+), 67 deletions(-) create mode 100644 lib/widgets/cleromancy_dialog.dart create mode 100644 lib/widgets/d10_dice_roller_widget.dart diff --git a/lib/screens/home_search_screen.dart b/lib/screens/home_search_screen.dart index 19035b6..48d4e71 100644 --- a/lib/screens/home_search_screen.dart +++ b/lib/screens/home_search_screen.dart @@ -3,6 +3,8 @@ import 'package:flutter/services.dart'; import '../models/history_entry_model.dart'; import '../models/word_model.dart'; import '../services/words_repository.dart'; +import '../widgets/cleromancy_dialog.dart'; +import '../widgets/d10_dice_roller_widget.dart'; import '../widgets/gradient_star_badge.dart'; import '../widgets/word_card_widget.dart'; @@ -18,16 +20,17 @@ class _HomeSearchScreenState extends State { final _searchController = TextEditingController(); final _questionController = TextEditingController(); final _focusNode = FocusNode(); + final _diceRollerKey = GlobalKey(); WordItem? _currentWord; String? _activeHistoryId; List _searchResults = []; bool _showKeypad = true; + String _currentDigits = ''; // Tracks 0..3 digits input for d10 dice cleromancy @override void initState() { super.initState(); - // Do NOT show word by default until user interacts or starts a session _searchController.addListener(_onSearchChanged); } @@ -45,17 +48,38 @@ class _HomeSearchScreenState extends State { if (query.isEmpty) { setState(() { _searchResults = []; + _currentDigits = ''; }); return; } - final results = _repo.search(query); - setState(() { - _searchResults = results; - if (results.isNotEmpty) { - _setWord(results.first, recordHistory: true); + final isNumeric = int.tryParse(query) != null; + + if (isNumeric) { + setState(() { + _currentDigits = query; + }); + // In Cleromancy, exactly 3 digits (d10x3) are required to resolve a number 000..999 + if (query.length >= 3) { + final num = int.tryParse(query); + if (num != null && num <= 999) { + final word = _repo.getWordBySessionNumber(num); + if (word != null) { + _setWord(word, recordHistory: true); + } + } } - }); + } else { + // Text search by word + final results = _repo.search(query); + setState(() { + _searchResults = results; + _currentDigits = ''; + if (results.isNotEmpty) { + _setWord(results.first, recordHistory: true); + } + }); + } } Future _setWord(WordItem word, {bool updateText = false, bool recordHistory = true}) async { @@ -71,7 +95,7 @@ class _HomeSearchScreenState extends State { sessionNumber: word.sessionNumber, partOfSpeech: word.partOfSpeech, symbolicMeaning: word.description, - readingType: 'Жребий номера', + readingType: 'Клеромантия (3× d10)', question: question.isNotEmpty ? question : null, ); await _repo.addHistoryEntry(entry); @@ -80,8 +104,9 @@ class _HomeSearchScreenState extends State { setState(() { _currentWord = word; _activeHistoryId = newHistId ?? _activeHistoryId; + _currentDigits = word.formattedSessionNumber; if (updateText) { - _searchController.text = word.sessionNumber.toString(); + _searchController.text = word.formattedSessionNumber; } }); HapticFeedback.selectionClick(); @@ -93,6 +118,7 @@ class _HomeSearchScreenState extends State { if (val == 'C') { _searchController.clear(); + setState(() => _currentDigits = ''); return; } @@ -106,17 +132,11 @@ class _HomeSearchScreenState extends State { return; } - // Append digit + // In Cleromancy, append digit up to 3 digits if (currentText.length >= 3) { _searchController.text = val; } else { - final newText = currentText + val; - final num = int.tryParse(newText); - if (num != null && num <= 999) { - _searchController.text = newText; - } else { - _searchController.text = val; - } + _searchController.text = currentText + val; } _searchController.selection = TextSelection.fromPosition( @@ -124,9 +144,11 @@ class _HomeSearchScreenState extends State { ); } - void _startNewSessionDraw() { - final rnd = _repo.getRandomWord(); - _setWord(rnd, updateText: true, recordHistory: true); + void _onVirtualRollCompleted(int finalNumber) { + final word = _repo.getWordBySessionNumber(finalNumber); + if (word != null) { + _setWord(word, updateText: true, recordHistory: true); + } } void _resetToWelcome() { @@ -134,6 +156,7 @@ class _HomeSearchScreenState extends State { _currentWord = null; _searchController.clear(); _searchResults = []; + _currentDigits = ''; }); } @@ -151,11 +174,7 @@ class _HomeSearchScreenState extends State { void _reshuffleSession() { _repo.reshuffleSessionNumbers(); - setState(() { - _currentWord = null; - _searchController.clear(); - _searchResults = []; - }); + _resetToWelcome(); HapticFeedback.heavyImpact(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -200,6 +219,11 @@ class _HomeSearchScreenState extends State { ], ), actions: [ + IconButton( + tooltip: 'Инструкция: Клеромантия и кубики d10', + icon: const Icon(Icons.help_outline_rounded, color: Color(0xFFFFD54F)), + onPressed: () => CleromancyGuideDialog.show(context), + ), if (_currentWord != null) IconButton( tooltip: 'Новый вопрос / Сброс', @@ -207,7 +231,7 @@ class _HomeSearchScreenState extends State { onPressed: _resetToWelcome, ), IconButton( - tooltip: 'Перетасовать числа (#000..#999)', + tooltip: 'Перетасовать матрицу (#000..#999)', icon: const Icon(Icons.shuffle_rounded), onPressed: _reshuffleSession, ), @@ -231,7 +255,7 @@ class _HomeSearchScreenState extends State { controller: _questionController, decoration: InputDecoration( labelText: 'Ваш вопрос или намерение (необязательно)', - hintText: 'Например: В чем скрытый потенциал ситуации?', + hintText: 'Например: В чем корень текущей ситуации?', prefixIcon: const Icon(Icons.help_outline_rounded, color: Color(0xFFFFD54F), size: 20), suffixIcon: _questionController.text.isNotEmpty ? IconButton( @@ -263,9 +287,20 @@ class _HomeSearchScreenState extends State { ), ), + const SizedBox(height: 12), + + // D10 Dice Roller Slots Widget + D10DiceRollerWidget( + key: _diceRollerKey, + digits: _currentDigits, + isRolling: false, + onRollPressed: () {}, + onRollCompleted: _onVirtualRollCompleted, + ), + const SizedBox(height: 10), - // Search & Number Input Bar + // Search & Direct Number Input Bar TextField( controller: _searchController, focusNode: _focusNode, @@ -283,6 +318,7 @@ class _HomeSearchScreenState extends State { onPressed: () { _searchController.clear(); _focusNode.unfocus(); + setState(() => _currentDigits = ''); }, ) : null, @@ -312,8 +348,8 @@ class _HomeSearchScreenState extends State { ), ), - // Search matches dropdown list - if (_searchResults.length > 1 && _searchController.text.isNotEmpty) ...[ + // Search matches dropdown list (for text search) + if (_searchResults.length > 1 && _searchController.text.isNotEmpty && int.tryParse(_searchController.text) == null) ...[ const SizedBox(height: 8), Container( constraints: const BoxConstraints(maxHeight: 140), @@ -357,7 +393,7 @@ class _HomeSearchScreenState extends State { ), ), onTap: () { - _setWord(item, updateText: false, recordHistory: true); + _setWord(item, updateText: true, recordHistory: true); }, ); }, @@ -371,7 +407,7 @@ class _HomeSearchScreenState extends State { if (_currentWord == null) ...[ // Welcome Initiation Card Container( - padding: const EdgeInsets.all(22), + padding: const EdgeInsets.all(20), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, @@ -386,58 +422,47 @@ class _HomeSearchScreenState extends State { color: const Color(0xFFFFD54F).withValues(alpha: 0.35), width: 1.2, ), - boxShadow: [ - BoxShadow( - color: const Color(0xFFFFD54F).withValues(alpha: 0.1), - blurRadius: 16, - ), - ], ), child: Column( children: [ - const GradientStar(size: 44), - const SizedBox(height: 14), + const GradientStar(size: 38), + const SizedBox(height: 12), const Text( - 'СВЯЩЕННЫЙ ЖРЕБИЙ ОРАКУЛА', + 'СВЯЩЕННЫЙ ЖРЕБИЙ КЛЕРОМАНТИИ', textAlign: TextAlign.center, style: TextStyle( - fontSize: 16, + fontSize: 15, fontWeight: FontWeight.w900, - letterSpacing: 1.0, + letterSpacing: 0.8, color: Color(0xFFFFF9C4), ), ), - const SizedBox(height: 10), + const SizedBox(height: 8), Text( - 'Сформулируйте в мыслях или запишите сверху ваш вопрос, затем нажмите кнопку или наберите номер от 000 до 999 для раскрытия символического ключа.', + 'Сформулируйте вопрос, затем совершите 3 броска кубика d10 кнопкой выше или введите 3 выпавших числа (Сотни, Десятки, Единицы).', textAlign: TextAlign.center, style: TextStyle( fontSize: 13, - height: 1.45, + height: 1.4, color: theme.colorScheme.onSurface.withValues(alpha: 0.85), ), ), - const SizedBox(height: 18), - FilledButton.icon( - onPressed: _startNewSessionDraw, - icon: const GradientStar(size: 20), + const SizedBox(height: 14), + OutlinedButton.icon( + onPressed: () => CleromancyGuideDialog.show(context), + icon: const Icon(Icons.menu_book_rounded, size: 16, color: Color(0xFFFFD54F)), label: const Text( - 'НАЧАТЬ СЕССИЮ / БРОСИТЬ ЖРЕБИЙ', + 'Инструкция по броскам d10', style: TextStyle( - fontWeight: FontWeight.w900, - letterSpacing: 0.5, + color: Color(0xFFFFD54F), + fontWeight: FontWeight.bold, + fontSize: 12.5, ), ), - style: FilledButton.styleFrom( - backgroundColor: const Color(0xFF2A2D4A), - foregroundColor: const Color(0xFFFFF9C4), - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + style: OutlinedButton.styleFrom( + side: const BorderSide(color: Color(0xFFFFD54F), width: 1), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(18), - side: const BorderSide( - color: Color(0xFFFFD54F), - width: 1.2, - ), + borderRadius: BorderRadius.circular(14), ), ), ), @@ -467,7 +492,9 @@ class _HomeSearchScreenState extends State { historyEntryId: _activeHistoryId, onNext: _goToNext, onPrevious: _goToPrevious, - onRandom: _startNewSessionDraw, + onRandom: () { + _diceRollerKey.currentState?.triggerRollAnimation(); + }, ), ), ], @@ -494,10 +521,10 @@ class _HomeSearchScreenState extends State { children: [ const Row( children: [ - GradientStar(size: 14), + Icon(Icons.casino_outlined, size: 15, color: Color(0xFFFFD54F)), SizedBox(width: 6), Text( - 'Случайный жребий числа:', + 'Ввод граней d10 (3 цифры):', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w700, @@ -507,10 +534,15 @@ class _HomeSearchScreenState extends State { ], ), Text( - '000 — 999', + _currentDigits.length == 3 + ? 'Код: #$_currentDigits' + : 'Введено: ${_currentDigits.length} из 3', style: TextStyle( fontSize: 11, - color: theme.colorScheme.outline, + fontWeight: FontWeight.bold, + color: _currentDigits.length == 3 + ? const Color(0xFFFFD54F) + : theme.colorScheme.outline, ), ), ], diff --git a/lib/widgets/cleromancy_dialog.dart b/lib/widgets/cleromancy_dialog.dart new file mode 100644 index 0000000..4b29ded --- /dev/null +++ b/lib/widgets/cleromancy_dialog.dart @@ -0,0 +1,193 @@ +import 'package:flutter/material.dart'; +import 'gradient_star_badge.dart'; + +class CleromancyGuideDialog extends StatelessWidget { + const CleromancyGuideDialog({super.key}); + + static void show(BuildContext context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => const CleromancyGuideDialog(), + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + + return Container( + padding: const EdgeInsets.only(top: 12), + decoration: BoxDecoration( + color: isDark ? const Color(0xFF151728) : theme.colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(28)), + border: Border.all( + color: const Color(0xFFFFD54F).withValues(alpha: 0.35), + width: 1.2, + ), + ), + child: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Top drag indicator + Center( + child: Container( + width: 44, + height: 4, + decoration: BoxDecoration( + color: const Color(0xFFFFD54F).withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(2), + ), + ), + ), + + const SizedBox(height: 16), + + // Title Header + Row( + children: [ + const GradientStar(size: 26), + const SizedBox(width: 10), + Expanded( + child: Text( + 'Клеромантия и Жребий 999', + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w900, + letterSpacing: 0.5, + color: isDark ? const Color(0xFFFFF9C4) : const Color(0xFF1E2138), + ), + ), + ), + ], + ), + + const SizedBox(height: 14), + + // Introduction + _buildSection( + icon: Icons.auto_awesome, + title: 'Что такое Клеромантия?', + content: + 'Клеромантия (от греч. *klēros* — жребий, *manteia* — гадание) — древнейшая священная практика вопрошания Неведомого через случайное выпадение знаков. ' + 'В системе «Оракул 999» случайность не случайна: она служит мостом между вашим глубинным подсознанием и полем синхронистичности.', + isDark: isDark, + ), + + const SizedBox(height: 12), + + // D10 Dice Rule + _buildSection( + icon: Icons.casino_rounded, + title: 'Правило трёх бросков кубика d10 (0..9)', + content: + 'В основе оракула заложена 10-ричная матрица из 1000 ячеек (от #000 до #999). ' + 'Каждое число формируется строго из **3 последовательных бросков десятигранного кубика (d10)** со значениями граней от 0 до 9:\n\n' + '🎲 **1-й бросок (Сотни)**: первая цифра числа (0..9);\n' + '🎲 **2-й бросок (Десятки)**: вторая цифра числа (0..9);\n' + '🎲 **3-й бросок (Единицы)**: третья цифра числа (0..9).\n\n' + '👉 *Пример:* броски 7, затем 4, затем 2 дают сакральный номер **#742**.', + isDark: isDark, + ), + + const SizedBox(height: 12), + + // How to use physically and digitally + _buildSection( + icon: Icons.touch_app_rounded, + title: 'Как совершать гадание?', + content: + '1. **С физическими кубиками:**\n' + 'Возьмите кубик d10 (или три кубика разных цветов), бросьте их и наберите выпавшую комбинацию из 3 цифр на экранной панели.\n\n' + '2. **С виртуальным жребием в приложении:**\n' + 'Нажмите кнопку «Бросить жребий d10». Встроенный генератор вероятностей последовательно совершит 3 броска, показав каждую выпавшую грань.\n\n' + '3. **Формулировка вопроса:**\n' + 'Перед броском зафиксируйте вопрос в поле ввода — это свяжет ваш смысловой контекст со знаком при AI-трактовке.', + isDark: isDark, + ), + + const SizedBox(height: 18), + + // Close Button + SizedBox( + width: double.infinity, + child: FilledButton.icon( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.check_rounded), + label: const Text( + 'ПОНЯТНО, ВЕРНУТЬСЯ К ОРАКУЛУ', + style: TextStyle(fontWeight: FontWeight.bold), + ), + style: FilledButton.styleFrom( + backgroundColor: const Color(0xFF2A2D4A), + foregroundColor: const Color(0xFFFFF9C4), + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: const BorderSide( + color: Color(0xFFFFD54F), + width: 1.2, + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildSection({ + required IconData icon, + required String title, + required String content, + required bool isDark, + }) { + return Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: isDark ? const Color(0xFF1B1E32) : const Color(0xFFF4F5FB), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: const Color(0xFFFFD54F).withValues(alpha: 0.2), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(icon, size: 18, color: const Color(0xFFFFD54F)), + const SizedBox(width: 8), + Expanded( + child: Text( + title, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Color(0xFFFFD54F), + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + content, + style: TextStyle( + fontSize: 13, + height: 1.45, + color: isDark ? const Color(0xFFE2E4F0) : const Color(0xFF2C3042), + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/d10_dice_roller_widget.dart b/lib/widgets/d10_dice_roller_widget.dart new file mode 100644 index 0000000..4d8c35b --- /dev/null +++ b/lib/widgets/d10_dice_roller_widget.dart @@ -0,0 +1,309 @@ +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 onRollCompleted; + + const D10DiceRollerWidget({ + super.key, + required this.digits, + required this.isRolling, + required this.onRollPressed, + required this.onRollCompleted, + }); + + @override + State createState() => D10DiceRollerWidgetState(); +} + +class D10DiceRollerWidgetState extends State { + 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( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Row( + children: [ + GradientStar(size: 16), + SizedBox(width: 8), + Text( + 'КЛЕРОМАНТИЯ • 3× d10 (0..9)', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w900, + letterSpacing: 0.8, + 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(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), + ), + ), + ], + ), + ); + } +}