import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../models/divination_layout.dart'; import '../models/syncretic_session_model.dart'; import '../models/tradition_model.dart'; import '../models/word_model.dart'; import '../services/traditions_repository.dart'; import '../services/words_repository.dart'; import '../widgets/divination_instruction_modal.dart'; class RunesDivinationScreen extends StatefulWidget { final String? initialQuestion; const RunesDivinationScreen({super.key, this.initialQuestion}); @override State createState() => _RunesDivinationScreenState(); } class _RunesDivinationScreenState extends State with SingleTickerProviderStateMixin { late TabController _tabController; final _questionController = TextEditingController(); final _repo = WordsRepository(); // 24 runes from canon List _runes = []; // Layout selection DivinationLayout _selectedLayout = DivinationLayouts.runesLayouts[0]; // 1, 3, or 5 int _currentStepIndex = 0; // for multi-step layouts // Dice state (for current step) int? _d12First; int? _d12Second; bool _isRollingDice = false; // Manual input state bool _isManualReversed = false; // Multi-item results List<({TraditionStation rune, bool isReversed, String positionTitle})> _drawnResults = []; bool _savedToSession = false; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); if (widget.initialQuestion != null) { _questionController.text = widget.initialQuestion!; } _loadRunes(); } void _loadRunes() { final canon = TraditionsRepository().byId('runes'); if (canon != null) { _runes = canon.stations.take(24).toList(); } } @override void dispose() { _tabController.dispose(); _questionController.dispose(); super.dispose(); } int _calculateRuneIndexFromDice(int d1, int d2) { final isOdd = (d1 % 2) != 0; final runeNumber = isOdd ? d2 : (d2 + 12); return (runeNumber - 1).clamp(0, 23); } void _onLayoutChanged(DivinationLayout layout) { setState(() { _selectedLayout = layout; _currentStepIndex = 0; _drawnResults.clear(); _d12First = null; _d12Second = null; _savedToSession = false; }); } Future _rollDiceForCurrentStep() async { if (_runes.isEmpty || _currentStepIndex >= _selectedLayout.count) return; setState(() => _isRollingDice = true); HapticFeedback.mediumImpact(); final rnd = Random(); for (int i = 0; i < 8; i++) { await Future.delayed(const Duration(milliseconds: 50)); if (mounted) { setState(() { _d12First = rnd.nextInt(12) + 1; _d12Second = rnd.nextInt(12) + 1; }); } } final finalD1 = rnd.nextInt(12) + 1; final finalD2 = rnd.nextInt(12) + 1; final isRev = rnd.nextBool() && rnd.nextBool(); // 25% final idx = _calculateRuneIndexFromDice(finalD1, finalD2); final rune = _runes[idx]; final posTitle = _selectedLayout.positionTitles[_currentStepIndex]; if (mounted) { setState(() { _d12First = finalD1; _d12Second = finalD2; _isRollingDice = false; _drawnResults.add((rune: rune, isReversed: isRev, positionTitle: posTitle)); _currentStepIndex++; if (_currentStepIndex >= _selectedLayout.count) { _saveToSession('dice'); } }); HapticFeedback.heavyImpact(); } } void _applyManualChoice(int runeIndex) { if (runeIndex < 0 || runeIndex >= _runes.length || _currentStepIndex >= _selectedLayout.count) return; final rune = _runes[runeIndex]; final posTitle = _selectedLayout.positionTitles[_currentStepIndex]; setState(() { _drawnResults.add((rune: rune, isReversed: _isManualReversed, positionTitle: posTitle)); _isManualReversed = false; _currentStepIndex++; if (_currentStepIndex >= _selectedLayout.count) { _saveToSession('manual'); } }); HapticFeedback.selectionClick(); } void _resetSpread() { setState(() { _currentStepIndex = 0; _drawnResults.clear(); _d12First = null; _d12Second = null; _savedToSession = false; }); } Future _saveToSession(String inputMethod) async { if (_drawnResults.isEmpty) return; final items = _drawnResults.map((r) { WordItem? linkedWord; if (r.rune.primary != null) { linkedWord = _repo.getWordById(r.rune.primary!.id) ?? _repo.getWordByName(r.rune.primary!.word); } return ReadingSignItem( title: '${r.positionTitle}: ${r.rune.name}', subtitle: r.rune.group, symbol: r.rune.symbol, lore: r.rune.lore, isReversed: r.isReversed, linkedWord: linkedWord, ); }).toList(); final reading = SyncreticReading( id: 'reading_${DateTime.now().millisecondsSinceEpoch}', systemId: 'runes', systemName: 'Руны Старшего Футарка (24)', systemIcon: 'ᚱ', timestamp: DateTime.now(), layoutTitle: '${_selectedLayout.title} (${inputMethod == "dice" ? "2×d12" : "со стола"})', inputMethod: inputMethod, items: items, question: _questionController.text.trim(), ); await _repo.addReadingToSyncreticSession(reading, question: _questionController.text.trim()); if (mounted) { setState(() => _savedToSession = true); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Расклад «${_selectedLayout.title}» сохранён в сессию дня!'), backgroundColor: const Color(0xFF2E7D32), behavior: SnackBarBehavior.floating, duration: const Duration(seconds: 2), ), ); } } @override Widget build(BuildContext context) { final isSpreadComplete = _currentStepIndex >= _selectedLayout.count; return Scaffold( backgroundColor: const Color(0xFF0C0E1A), appBar: AppBar( actions: [ IconButton( tooltip: 'Инструкция по рунам', icon: const Icon(Icons.help_outline_rounded, color: Color(0xFFFFD54F)), onPressed: () => DivinationInstructionModal.show(context, 'runes'), ), if (_drawnResults.isNotEmpty) IconButton( tooltip: 'Сбросить расклад', icon: const Icon(Icons.refresh_rounded), onPressed: _resetSpread, ), ], title: const Row( children: [ Text('ᚱ', style: TextStyle(fontSize: 22, color: Color(0xFFFFD54F))), SizedBox(width: 8), Text('Руны Старшего Футарка', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 17)), ], ), bottom: TabBar( controller: _tabController, indicatorColor: const Color(0xFFFFD54F), tabs: const [ Tab(icon: Icon(Icons.casino_rounded, size: 18), text: 'Жребий 2×d12'), Tab(icon: Icon(Icons.touch_app_rounded, size: 18), text: 'Ввод со стола'), ], ), ), body: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Question Input TextField( controller: _questionController, style: const TextStyle(color: Colors.white, fontSize: 14), decoration: InputDecoration( hintText: 'Вопрос к рунам (необязательно)...', hintStyle: const TextStyle(color: Color(0xFF8C93B8), fontSize: 13), prefixIcon: const Icon(Icons.help_outline_rounded, color: Color(0xFFFFD54F), size: 18), filled: true, fillColor: const Color(0xFF151829), border: OutlineInputBorder(borderRadius: BorderRadius.circular(14)), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(14), borderSide: const BorderSide(color: Color(0xFF2A2D4A)), ), ), ), const SizedBox(height: 14), // Layout Picker Row Row( children: [ const Icon(Icons.dashboard_customize_rounded, size: 16, color: Color(0xFFFFD54F)), const SizedBox(width: 6), const Text('РАСКЛАД:', style: TextStyle(color: Color(0xFFFFD54F), fontSize: 11, fontWeight: FontWeight.bold)), const SizedBox(width: 8), Expanded( child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: DivinationLayouts.runesLayouts.map((l) { final isSel = _selectedLayout.id == l.id; return Padding( padding: const EdgeInsets.only(right: 6), child: ChoiceChip( label: Text(l.title, style: TextStyle(fontSize: 11.5, fontWeight: isSel ? FontWeight.bold : FontWeight.normal)), selected: isSel, selectedColor: const Color(0xFF4A3E1E), side: BorderSide(color: isSel ? const Color(0xFFFFD54F) : const Color(0xFF2A2D4A)), onSelected: (_) => _onLayoutChanged(l), ), ); }).toList(), ), ), ), ], ), const SizedBox(height: 14), // Progress Banner Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), decoration: BoxDecoration( color: const Color(0xFF181C30), borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFF2A2D4A)), ), child: Row( children: [ Text( 'Позиция: ${_currentStepIndex + 1} из ${_selectedLayout.count}', style: const TextStyle(color: Color(0xFF80DEEA), fontSize: 12, fontWeight: FontWeight.bold), ), const SizedBox(width: 8), if (!isSpreadComplete) Expanded( child: Text( '«${_selectedLayout.positionTitles[_currentStepIndex]}»', style: const TextStyle(color: Color(0xFFFFF9C4), fontSize: 12, fontWeight: FontWeight.bold), overflow: TextOverflow.ellipsis, ), ) else const Text('✓ Расклад собран', style: TextStyle(color: Color(0xFF81C784), fontSize: 12, fontWeight: FontWeight.bold)), ], ), ), const SizedBox(height: 14), // Controls Tabs if (!isSpreadComplete) SizedBox( height: 310, child: TabBarView( controller: _tabController, children: [ _buildDiceTab(), _buildManualTabThreeAetts(), ], ), ), if (isSpreadComplete) ...[ Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: const Color(0xFF1E2238), borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFF81C784)), ), child: Row( children: [ const Icon(Icons.check_circle_rounded, color: Color(0xFF81C784), size: 20), const SizedBox(width: 10), const Expanded( child: Text( 'Все руны расклада извлечены и записаны в сессию дня!', style: TextStyle(color: Color(0xFFECEFF1), fontSize: 12.5, fontWeight: FontWeight.bold), ), ), TextButton( onPressed: _resetSpread, child: const Text('Заново', style: TextStyle(color: Color(0xFFFFD54F), fontWeight: FontWeight.bold)), ), ], ), ), ], const SizedBox(height: 16), // Drawn Runes Cards List if (_drawnResults.isNotEmpty) ...[ const Text( 'ВЫПАВШИЕ РУНЫ В РАСКЛАДЕ:', style: TextStyle(color: Color(0xFFFFD54F), fontSize: 11.5, fontWeight: FontWeight.w900, letterSpacing: 0.8), ), const SizedBox(height: 10), for (int i = 0; i < _drawnResults.length; i++) ...[ _buildResultRuneCard(_drawnResults[i], i + 1), const SizedBox(height: 10), ], ], ], ), ), ); } Widget _buildDiceTab() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFF151829), borderRadius: BorderRadius.circular(20), border: Border.all(color: const Color(0xFF2A2D4A)), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text( 'БРОСОК 2×d12 (1..24 РАВНОМЕРНО)', style: TextStyle(color: Color(0xFFFFD54F), fontSize: 11, fontWeight: FontWeight.w900, letterSpacing: 0.8), ), const SizedBox(height: 6), Text( 'Извлечение позиции: «' + _selectedLayout.positionTitles[_currentStepIndex] + '»', textAlign: TextAlign.center, style: const TextStyle(color: Color(0xFF8C93B8), fontSize: 11.5), ), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildDieBox('d12 #1 (чет/нечет)', _d12First, isOddEven: true), const SizedBox(width: 20), _buildDieBox('d12 #2 (1..12)', _d12Second), ], ), const SizedBox(height: 18), ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFFFD54F), foregroundColor: Colors.black, padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), ), onPressed: _isRollingDice ? null : _rollDiceForCurrentStep, icon: const Icon(Icons.casino_rounded, size: 20), label: Text( _isRollingDice ? 'Кости звенят...' : 'Бросить для шага ${_currentStepIndex + 1}', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13.5), ), ), ], ), ); } Widget _buildDieBox(String label, int? val, {bool isOddEven = false}) { String sub = ''; if (val != null && isOddEven) { sub = (val % 2 != 0) ? 'Нечет (1..12)' : 'Чет (13..24)'; } return Column( children: [ Container( width: 68, height: 68, alignment: Alignment.center, decoration: BoxDecoration( color: const Color(0xFF232742), borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.6), width: 1.5), ), child: Text( val != null ? '$val' : '?', style: const TextStyle(color: Color(0xFFFFF9C4), fontSize: 26, fontWeight: FontWeight.w900), ), ), const SizedBox(height: 6), Text(label, style: const TextStyle(color: Color(0xFF90A4AE), fontSize: 10, fontWeight: FontWeight.bold)), if (sub.isNotEmpty) Text(sub, style: const TextStyle(color: Color(0xFFFFD54F), fontSize: 9, fontWeight: FontWeight.bold)), ], ); } /// Exact 3 Rows of 8 Runes: 1st Aett (1..8), 2nd Aett (9..16), 3rd Aett (17..24) Widget _buildManualTabThreeAetts() { const aettNames = [ 'I ЭТТ ФРЕЙИ (ᚠ..ᚹ)', 'II ЭТТ ХЕЙМДАЛЛЯ (ᚺ..ᛏ)', 'III ЭТТ ТЮРА (ᛒ..ᛟ)', ]; return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: const Color(0xFF151829), borderRadius: BorderRadius.circular(20), border: Border.all(color: const Color(0xFF2A2D4A)), ), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'ВЫБЕРИТЕ РУНУ ДЛЯ ШАГА ${_currentStepIndex + 1}:', style: const TextStyle(color: Color(0xFFFFD54F), fontSize: 10.5, fontWeight: FontWeight.bold), ), FilterChip( label: const Text('Перевёрнутая ᛉ/ᛦ', style: TextStyle(fontSize: 10)), selected: _isManualReversed, selectedColor: const Color(0xFFEF5350).withValues(alpha: 0.3), side: BorderSide(color: _isManualReversed ? const Color(0xFFEF5350) : const Color(0xFF2A2D4A)), onSelected: (v) => setState(() => _isManualReversed = v), ), ], ), const SizedBox(height: 6), // 3 Rows of 8 runes for (int aett = 0; aett < 3; aett++) ...[ Padding( padding: const EdgeInsets.only(top: 4, bottom: 2), child: Align( alignment: Alignment.centerLeft, child: Text( aettNames[aett], style: const TextStyle(color: Color(0xFF80DEEA), fontSize: 9.5, fontWeight: FontWeight.bold), ), ), ), Row( children: [ for (int col = 0; col < 8; col++) ...[ Builder(builder: (ctx) { final idx = aett * 8 + col; if (idx >= _runes.length) return const SizedBox.shrink(); final r = _runes[idx]; return Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 2), child: InkWell( onTap: () => _applyManualChoice(idx), borderRadius: BorderRadius.circular(6), child: Container( height: 38, alignment: Alignment.center, decoration: BoxDecoration( color: const Color(0xFF1E2238), borderRadius: BorderRadius.circular(6), border: Border.all(color: const Color(0xFF2A2D4A)), ), child: Text( r.symbol, style: const TextStyle(color: Color(0xFFFFF9C4), fontSize: 18, fontWeight: FontWeight.bold), ), ), ), ), ); }), ], ], ), ], ], ), ); } Widget _buildResultRuneCard(({TraditionStation rune, bool isReversed, String positionTitle}) res, int index) { final r = res.rune; return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: const Color(0xFF181C30), borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.4)), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 44, height: 44, alignment: Alignment.center, decoration: BoxDecoration( color: const Color(0xFF232742), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFFFD54F)), ), child: Text( r.symbol, style: const TextStyle(fontSize: 26, color: Color(0xFFFFD54F)), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( '#$index. ${res.positionTitle}: ${r.name}', style: const TextStyle(color: Color(0xFFFFF9C4), fontSize: 14, fontWeight: FontWeight.bold), ), ), if (res.isReversed) Container( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2), decoration: BoxDecoration( color: const Color(0xFF5C1D1D), borderRadius: BorderRadius.circular(6), border: Border.all(color: const Color(0xFFEF5350)), ), child: const Text('Перевёрнутая', style: TextStyle(color: Color(0xFFFF8A80), fontSize: 9.5, fontWeight: FontWeight.bold)), ), ], ), const SizedBox(height: 4), Text(r.group, style: const TextStyle(color: Color(0xFF80DEEA), fontSize: 11)), const SizedBox(height: 6), Text( r.lore, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(color: Color(0xFFD1D5DB), fontSize: 12, height: 1.4), ), ], ), ), ], ), ); } }