import 'dart:async'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../models/houses_session_model.dart'; import '../models/word_model.dart'; import '../services/houses_service.dart'; import '../services/words_repository.dart'; import '../widgets/gradient_star_badge.dart'; import 'houses_result_screen.dart'; import 'symbol_detail_screen.dart'; class HousesFieldScreen extends StatefulWidget { final String? initialQuestion; final WordItem? initialQuerent; const HousesFieldScreen({super.key, this.initialQuestion, this.initialQuerent}); @override State createState() => _HousesFieldScreenState(); } class _HousesFieldScreenState extends State { final _service = HousesService(); final _repo = WordsRepository(); final TransformationController _transformController = TransformationController(); late HousesSession _session; int _targetHousesCount = 3; // Default 3 houses, can be set 1..9 late final TextEditingController _countController; static const double kCellSize = 64.0; static const double kCellMargin = 3.0; @override void initState() { super.initState(); _countController = TextEditingController(text: '$_targetHousesCount'); // Determine initial querent if explicitly passed final WordItem? qWord = widget.initialQuerent ?? _repo.getWordByName('мужчина'); _session = _service.createNewSession( question: widget.initialQuestion, querentWord: qWord, ); _transformController.value = Matrix4.diagonal3Values(0.55, 0.55, 1.0); WidgetsBinding.instance.addPostFrameCallback((_) { if (_session.querentHouseIndex != null) { _centerOnHouse(_session.querentHouseIndex!); } else { // No querent set — prompt user to choose one _showQuerentSetupModal(isInitial: true); } }); } @override void dispose() { _countController.dispose(); _transformController.dispose(); super.dispose(); } void _centerOnHouse(int houseIndex) { if (!mounted) return; final row = houseIndex ~/ 30; final col = houseIndex % 30; final x = col * (kCellSize + kCellMargin * 2) + 20.0 + kCellSize / 2; final y = row * (kCellSize + kCellMargin * 2) + 70.0 + 20.0 + kCellSize / 2; final size = MediaQuery.of(context).size; const scale = 0.85; final targetX = -(x * scale - size.width / 2); final targetY = -(y * scale - size.height / 2); setState(() { _transformController.value = Matrix4.identity() ..translate(targetX, targetY) ..scale(scale); }); } void _resetSession() { HapticFeedback.mediumImpact(); final newSession = _service.createNewSession( question: _session.question, querentWord: _session.querentWord, ); setState(() { _session = newSession; }); if (newSession.querentHouseIndex != null) { Future.delayed(const Duration(milliseconds: 250), () { if (mounted) _centerOnHouse(newSession.querentHouseIndex!); }); } final qInfo = newSession.querentWord != null && newSession.querentHouseIndex != null ? ' • Кверент «${newSession.querentWord!.word.toUpperCase()}» в Доме #${(newSession.querentHouseIndex! + 1).toString().padLeft(3, "0")}' : ''; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Матрица 33×30 перешафлена: Архонты и Дома сокрыты заново$qInfo'), backgroundColor: const Color(0xFF2A2D4A), behavior: SnackBarBehavior.floating, ), ); } void _showQuerentSetupModal({bool isInitial = false}) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (ctx) => _QuerentSetupSheet( currentQuerent: _session.querentWord, currentCount: _targetHousesCount, currentQuestion: _session.question, isInitial: isInitial, onApplied: (WordItem querent, int count, String? question) { Navigator.pop(ctx); final newSession = _service.createNewSession( question: question, querentWord: querent, ); setState(() { _targetHousesCount = count; _countController.text = '$count'; _session = newSession; }); if (newSession.querentHouseIndex != null) { Future.delayed(const Duration(milliseconds: 300), () { if (mounted) _centerOnHouse(newSession.querentHouseIndex!); }); } final houseNum = newSession.querentHouseIndex != null ? (newSession.querentHouseIndex! + 1).toString().padLeft(3, "0") : '???'; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Кверент установлен: «${querent.word.toUpperCase()}» (Дом #$houseNum)'), backgroundColor: const Color(0xFF381E52), behavior: SnackBarBehavior.floating, ), ); }, ), ); } void _setTargetCount(int count) { if (_session.selectedHouses.isNotEmpty) { showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: const Color(0xFF1E2238), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), title: const Text('Сбросить открытые дома?', style: TextStyle(color: Color(0xFFFFF9C4))), content: const Text( 'Вы уже открыли дома в текущем раскладе. Изменение количества домов начнет новый сеанс.', style: TextStyle(color: Color(0xFFE0E0E0)), ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('Отмена', style: TextStyle(color: Color(0xFF9FA8DA))), ), FilledButton( style: FilledButton.styleFrom(backgroundColor: const Color(0xFFFFD54F)), onPressed: () { Navigator.pop(ctx); setState(() { _targetHousesCount = count.clamp(1, 9); _countController.text = '$_targetHousesCount'; final newSession = _service.createNewSession( question: _session.question, querentWord: _session.querentWord, ); _session = newSession; if (newSession.querentHouseIndex != null) { Future.delayed(const Duration(milliseconds: 250), () { if (mounted) _centerOnHouse(newSession.querentHouseIndex!); }); } }); }, child: const Text('Начать заново', style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold)), ), ], ), ); return; } setState(() { _targetHousesCount = count.clamp(1, 9); _countController.text = '$_targetHousesCount'; }); } /// Triggered when user taps a cell on the field void _onCellTapped(HouseCell cell) { HapticFeedback.selectionClick(); // Check if user tapped the Querent cell if (_session.querentHouseIndex != null && cell.houseIndex == _session.querentHouseIndex) { _showQuerentDetailSheet(); return; } if (cell.isSelected) { // Cell is already revealed, show its details _showRevealedCellDetails(cell); return; } if (_session.selectedHouses.length >= _targetHousesCount) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Все намеченные дома ($_targetHousesCount из $_targetHousesCount) уже открыты!'), behavior: SnackBarBehavior.floating, action: SnackBarAction( label: 'К результатам', textColor: const Color(0xFFFFD54F), onPressed: _openResultScreen, ), ), ); return; } _showCastGuestSheet(cell); } void _showQuerentDetailSheet() { final qWord = _session.querentWord; final qIdx = _session.querentHouseIndex ?? 0; final qSector = _session.sectors[qIdx ~/ 99]; showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: const Color(0xFF141226), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), builder: (ctx) => Padding( padding: const EdgeInsets.fromLTRB(20, 16, 20, 28), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( gradient: const LinearGradient(colors: [Color(0xFFFFD54F), Color(0xFFFFA000)]), borderRadius: BorderRadius.circular(8), ), child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.stars_rounded, color: Colors.black, size: 14), SizedBox(width: 4), Text( 'СИГНИФИКАТОР / КВЕРЕНТ', style: TextStyle(color: Colors.black, fontWeight: FontWeight.w900, fontSize: 11), ), ], ), ), const Spacer(), IconButton( icon: const Icon(Icons.close_rounded, color: Color(0xFF9FA8DA)), onPressed: () => Navigator.pop(ctx), ), ], ), const SizedBox(height: 12), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xFF2C1645), Color(0xFF19142E)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFFFFD54F), width: 1.4), boxShadow: const [ BoxShadow(color: Color(0x66FFD54F), blurRadius: 16, spreadRadius: 1), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( (qWord?.word ?? 'Кверент').toUpperCase(), style: const TextStyle( color: Color(0xFFFFF9C4), fontWeight: FontWeight.w900, fontSize: 24, letterSpacing: 1.1, ), ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: const Color(0xFFFFD54F).withValues(alpha: 0.2), borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFFFFD54F), width: 0.8), ), child: Text( 'Дом #${(qIdx + 1).toString().padLeft(3, '0')}', style: const TextStyle(color: Color(0xFFFFD54F), fontWeight: FontWeight.bold, fontSize: 12), ), ), ], ), const SizedBox(height: 6), Text( 'Триада ${qSector.index + 1}: Архонт ${qSector.archonWord.word.toUpperCase()}', style: TextStyle(color: qSector.sectorColor, fontSize: 12, fontWeight: FontWeight.w600), ), if (qWord?.core.isNotEmpty ?? false) ...[ const SizedBox(height: 10), Text( '✨ Ядро знака: ${qWord!.core}', style: const TextStyle(color: Color(0xFFE8EAF6), fontSize: 13, fontWeight: FontWeight.w500), ), ], if (qWord?.oracle.isNotEmpty ?? false) ...[ const SizedBox(height: 8), Text( qWord!.oracle, maxLines: 3, overflow: TextOverflow.ellipsis, style: const TextStyle(color: Color(0xFFCFD8DC), fontSize: 12, height: 1.35), ), ], if (_session.question != null && _session.question!.isNotEmpty) ...[ const SizedBox(height: 12), Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: const Color(0xFF141226), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFF381E52)), ), child: Row( children: [ const Icon(Icons.help_outline_rounded, color: Color(0xFFFFD54F), size: 16), const SizedBox(width: 8), Expanded( child: Text( 'Вопрос: «${_session.question}»', style: const TextStyle(color: Color(0xFFFFF9C4), fontSize: 12, fontStyle: FontStyle.italic), ), ), ], ), ), ], ], ), ), const SizedBox(height: 16), Row( children: [ Expanded( child: OutlinedButton.icon( style: OutlinedButton.styleFrom( foregroundColor: const Color(0xFFFFD54F), side: const BorderSide(color: Color(0xFFFFD54F)), padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), onPressed: () { Navigator.pop(ctx); _showQuerentSetupModal(isInitial: false); }, icon: const Icon(Icons.swap_horiz_rounded, size: 18), label: const Text('Сменить Кверента'), ), ), const SizedBox(width: 12), if (qWord != null) Expanded( child: FilledButton.icon( style: FilledButton.styleFrom( backgroundColor: const Color(0xFF2A2D4A), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), onPressed: () { Navigator.pop(ctx); Navigator.push( context, MaterialPageRoute(builder: (context) => SymbolDetailScreen(word: qWord)), ); }, icon: const Icon(Icons.menu_book_rounded, size: 18), label: const Text('Полное досье'), ), ), ], ), ], ), ), ); } /// Opens the modal to cast or enter a guest into a house void _showCastGuestSheet(HouseCell cell) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: const Color(0xFF131627), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), builder: (ctx) => _CastGuestSheet( cell: cell, onGuestDetermined: (WordItem guest) { Navigator.pop(ctx); _applyRevealedHouse(cell, guest); }, ), ); } /// Triggers roll for BOTH House and Guest via dice void _startRollHouseAndGuest() { if (_session.selectedHouses.length >= _targetHousesCount) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Все дома ($_targetHousesCount из $_targetHousesCount) уже открыты!'), behavior: SnackBarBehavior.floating, ), ); return; } showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: const Color(0xFF131627), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), builder: (ctx) => _RollHouseAndGuestSheet( session: _session, onDetermined: (HouseCell cell, WordItem guest) { Navigator.pop(ctx); _applyRevealedHouse(cell, guest); }, ), ); } void _applyRevealedHouse(HouseCell cell, WordItem guest) { HapticFeedback.heavyImpact(); setState(() { _session = _service.revealHouse(_session, cell: cell, guestWord: guest); }); _showRevealDialog(_session.selectedHouses.last); } void _showRevealDialog(HouseCell revealedCell) { final sector = _session.sectors[revealedCell.sectorIndex]; final host = revealedCell.houseWord; final guest = revealedCell.guestWord!; final isLast = _session.selectedHouses.length >= _targetHousesCount; showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: const Color(0xFF16192E), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(22)), title: Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: const Color(0xFFFFD54F), borderRadius: BorderRadius.circular(8), ), child: Text( '#${revealedCell.selectionOrder}', style: const TextStyle(color: Colors.black, fontWeight: FontWeight.bold, fontSize: 13), ), ), const SizedBox(width: 10), const Text( 'Откровение Ячейки', style: TextStyle(color: Color(0xFFFFF9C4), fontWeight: FontWeight.bold, fontSize: 17), ), ], ), content: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ // Host Card Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: const Color(0xFF1F233D), borderRadius: BorderRadius.circular(14), border: Border.all(color: sector.sectorColor.withValues(alpha: 0.6)), ), child: Row( children: [ const Icon(Icons.home_work_rounded, color: Color(0xFF90A4AE), size: 20), const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'ДОМ #${(revealedCell.houseIndex + 1).toString().padLeft(3, '0')} (Триада ${revealedCell.sectorIndex + 1})', style: TextStyle(color: sector.sectorColor, fontSize: 11, fontWeight: FontWeight.bold), ), Text( host.word.toUpperCase(), style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w900, fontSize: 18), ), ], ), ), ], ), ), const SizedBox(height: 10), // Guest Card Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: const Color(0xFF252A4A), borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFFFFD54F)), ), child: Row( children: [ const Icon(Icons.auto_awesome_rounded, color: Color(0xFFFFD54F), size: 22), const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'ПРИШЕДШИЙ ГОСТЬ:', style: TextStyle(color: Color(0xFFFFD54F), fontSize: 11, fontWeight: FontWeight.bold), ), Text( guest.word.toUpperCase(), style: const TextStyle(color: Color(0xFFFFF9C4), fontWeight: FontWeight.w900, fontSize: 20), ), ], ), ), ], ), ), const SizedBox(height: 14), if (guest.core.isNotEmpty) ...[ Text( '✨ Закон знака: ${guest.core}', style: const TextStyle(color: Color(0xFFE8EAF6), fontSize: 13, height: 1.4), ), const SizedBox(height: 8), ], Text( guest.oracle.isNotEmpty ? guest.oracle : guest.description, maxLines: 4, overflow: TextOverflow.ellipsis, style: const TextStyle(color: Color(0xFFB0BEC5), fontSize: 12, height: 1.35), ), ], ), ), actions: [ TextButton( onPressed: () { Navigator.pop(ctx); Navigator.push( context, MaterialPageRoute(builder: (context) => SymbolDetailScreen(word: guest)), ); }, child: const Text('Полное досье', style: TextStyle(color: Color(0xFF9FA8DA))), ), FilledButton( style: FilledButton.styleFrom(backgroundColor: const Color(0xFFFFD54F)), onPressed: () { Navigator.pop(ctx); if (isLast) { _openResultScreen(); } }, child: Text( isLast ? 'К результатам' : 'Далее (${_session.selectedHouses.length} из $_targetHousesCount)', style: const TextStyle(color: Colors.black, fontWeight: FontWeight.bold), ), ), ], ), ); } void _showRevealedCellDetails(HouseCell cell) { _showRevealDialog(cell); } void _openResultScreen() { HapticFeedback.heavyImpact(); Navigator.push( context, MaterialPageRoute( builder: (context) => HousesResultScreen(session: _session), ), ); } @override Widget build(BuildContext context) { const totalWidth = 30 * (kCellSize + kCellMargin * 2); const totalHeight = 33 * (kCellSize + kCellMargin * 2) + 120.0; final openedCount = _session.selectedHouses.length; final activeSectorIndices = _session.selectedHouses.map((h) => h.sectorIndex).toSet(); return Scaffold( backgroundColor: const Color(0xFF0A0C16), appBar: AppBar( title: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Полотно Домов 33×30', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 17)), Text( 'Открыто: $openedCount из $_targetHousesCount домов${_session.querentWord != null ? " • Кверент: «${_session.querentWord!.word}»" : ""}', style: const TextStyle(fontSize: 11, color: Color(0xFFFFD54F)), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), actions: [ IconButton( tooltip: 'Настройки кверента и расклада', icon: const Icon(Icons.tune_rounded, color: Color(0xFFFFD54F)), onPressed: () => _showQuerentSetupModal(isInitial: false), ), IconButton( tooltip: 'Перешафлить матрицу', icon: const Icon(Icons.refresh_rounded, color: Color(0xFF90A4AE)), onPressed: _resetSession, ), if (openedCount > 0) IconButton( tooltip: 'К результатам расклада', icon: const Icon(Icons.done_all_rounded, color: Color(0xFF81C784)), onPressed: _openResultScreen, ), ], ), body: Stack( children: [ // 2D Interactive Canvas InteractiveViewer( transformationController: _transformController, boundaryMargin: const EdgeInsets.all(220), minScale: 0.15, maxScale: 2.8, constrained: false, child: SizedBox( width: totalWidth + 40, height: totalHeight + 40, child: Padding( padding: const EdgeInsets.only(left: 20, right: 20, bottom: 20, top: 70), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Archon Headers Row (10 triads) _buildArchonHeadersRow(kCellSize, kCellMargin, activeSectorIndices), const SizedBox(height: 10), // Grid 33 rows x 30 columns _buildGrid(kCellSize, kCellMargin), ], ), ), ), ), // Top Target & Querent Selector Bar Positioned( top: 10, left: 14, right: 14, child: _buildTopCommandBar(), ), // Bottom Control & Action Bar Positioned( left: 16, right: 16, bottom: 20, child: _buildBottomPanel(openedCount), ), ], ), ); } Widget _buildTopCommandBar() { final qWord = _session.querentWord; return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: const Color(0xFF141729).withValues(alpha: 0.95), borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.5), width: 1.2), boxShadow: const [ BoxShadow(color: Color(0x77000000), blurRadius: 12, offset: Offset(0, 4)), ], ), child: Row( children: [ // Querent Pill InkWell( onTap: () { if (_session.querentHouseIndex != null) { _centerOnHouse(_session.querentHouseIndex!); } else { _showQuerentSetupModal(isInitial: false); } }, borderRadius: BorderRadius.circular(10), child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5), decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xFF381E52), Color(0xFF221136)], ), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFFFD54F), width: 1.2), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.stars_rounded, color: Color(0xFFFFD54F), size: 16), const SizedBox(width: 5), Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ const Text( 'КВЕРЕНТ', style: TextStyle(color: Color(0xFFFFD54F), fontSize: 9, fontWeight: FontWeight.bold), ), Text( qWord != null ? qWord.word.toUpperCase() : 'Выбрать 👑', style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.w900), ), ], ), ], ), ), ), if (_session.querentHouseIndex != null) ...[ const SizedBox(width: 4), IconButton( tooltip: 'Центрировать на Кверенте', icon: const Icon(Icons.my_location_rounded, size: 18, color: Color(0xFFFFD54F)), onPressed: () => _centerOnHouse(_session.querentHouseIndex!), padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 28, minHeight: 28), ), ], const Spacer(), // Target houses stepper Row( mainAxisSize: MainAxisSize.min, children: [ const Text( 'Дома:', style: TextStyle(color: Color(0xFFB0BEC5), fontSize: 12, fontWeight: FontWeight.w600), ), const SizedBox(width: 4), IconButton( icon: const Icon(Icons.remove_circle_outline_rounded, size: 20, color: Color(0xFFFFD54F)), onPressed: _targetHousesCount > 1 ? () => _setTargetCount(_targetHousesCount - 1) : null, padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 28, minHeight: 28), ), Container( width: 34, height: 30, decoration: BoxDecoration( color: const Color(0xFF1F233D), borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.6)), ), child: Center( child: Text( '$_targetHousesCount', style: const TextStyle(color: Color(0xFFFFD54F), fontSize: 15, fontWeight: FontWeight.w900), ), ), ), IconButton( icon: const Icon(Icons.add_circle_outline_rounded, size: 20, color: Color(0xFFFFD54F)), onPressed: _targetHousesCount < 9 ? () => _setTargetCount(_targetHousesCount + 1) : null, padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 28, minHeight: 28), ), ], ), ], ), ); } Widget _buildArchonHeadersRow(double cellSize, double cellMargin, Set activeSectors) { final triadWidth = 3 * (cellSize + cellMargin * 2); return Row( children: List.generate(10, (sectorIdx) { final sector = _session.sectors[sectorIdx]; final isRevealed = activeSectors.contains(sectorIdx); return SizedBox( width: triadWidth, height: 58, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 2), child: Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4), decoration: BoxDecoration( color: isRevealed ? sector.sectorColor.withValues(alpha: 0.28) : const Color(0xFF141729), borderRadius: BorderRadius.circular(12), border: Border.all( color: isRevealed ? sector.sectorColor : sector.sectorColor.withValues(alpha: 0.35), width: isRevealed ? 2.0 : 1.0, ), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( isRevealed ? Icons.shield_rounded : Icons.shield_moon_rounded, size: 14, color: sector.sectorColor, ), const SizedBox(width: 4), Text( 'ТРИАДА ${sectorIdx + 1}', style: TextStyle( color: sector.sectorColor, fontWeight: FontWeight.w900, fontSize: 10, letterSpacing: 0.5, ), ), ], ), const SizedBox(height: 2), Text( isRevealed ? sector.archonWord.word.toUpperCase() : 'Архонт сокрыт 👁️', style: TextStyle( color: isRevealed ? const Color(0xFFFFF9C4) : const Color(0xFFB0BEC5), fontSize: isRevealed ? 11 : 9, fontWeight: isRevealed ? FontWeight.w900 : FontWeight.normal, fontStyle: isRevealed ? FontStyle.normal : FontStyle.italic, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), ), ); }), ); } Widget _buildGrid(double cellSize, double cellMargin) { final querentIdx = _session.querentHouseIndex; return Column( children: List.generate(33, (row) { return Row( children: List.generate(30, (col) { final index = row * 30 + col; final cell = _session.grid[index]; final sector = _session.sectors[cell.sectorIndex]; final isRevealed = cell.isSelected && cell.guestWord != null; final isQuerent = querentIdx != null && cell.houseIndex == querentIdx; return GestureDetector( onTap: () => _onCellTapped(cell), child: Container( width: cellSize, height: cellSize, margin: EdgeInsets.all(cellMargin), decoration: BoxDecoration( gradient: isQuerent ? const LinearGradient( colors: [Color(0xFF381E52), Color(0xFF1D1030)], begin: Alignment.topLeft, end: Alignment.bottomRight, ) : null, color: isQuerent ? null : (isRevealed ? const Color(0xFF1F2445) : (col % 3 == 0 ? sector.sectorColor.withValues(alpha: 0.12) : const Color(0xFF131627))), borderRadius: BorderRadius.circular(10), border: Border.all( color: isQuerent ? const Color(0xFFFFD54F) : (isRevealed ? const Color(0xFFFFD54F) : sector.sectorColor.withValues(alpha: 0.35)), width: isQuerent ? 2.6 : (isRevealed ? 2.2 : 0.8), ), boxShadow: isQuerent ? [ BoxShadow( color: const Color(0xFFFFD54F).withValues(alpha: 0.7), blurRadius: 14, spreadRadius: 2, ), BoxShadow( color: const Color(0xFFB388FF).withValues(alpha: 0.45), blurRadius: 20, spreadRadius: 3, ), ] : (isRevealed ? [ BoxShadow( color: const Color(0xFFFFD54F).withValues(alpha: 0.5), blurRadius: 10, spreadRadius: 1, ) ] : null), ), child: isQuerent ? _buildQuerentCellContent(cell, sector) : (isRevealed ? _buildRevealedCellContent(cell, sector) : _buildHiddenCellContent(cell, sector)), ), ); }), ); }), ); } /// Querent cell is always revealed and crowned Widget _buildQuerentCellContent(HouseCell cell, ArchonSector sector) { final qWord = _session.querentWord ?? cell.houseWord; return Stack( children: [ Positioned( top: 2, left: 2, right: 2, child: Container( padding: const EdgeInsets.symmetric(vertical: 1), decoration: BoxDecoration( color: const Color(0xFFFFD54F), borderRadius: BorderRadius.circular(4), ), child: const Center( child: Text( '👑 КВЕРЕНТ', style: TextStyle(color: Colors.black, fontWeight: FontWeight.w900, fontSize: 7.5), ), ), ), ), Center( child: Padding( padding: const EdgeInsets.only(top: 14, left: 2, right: 2), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( qWord.word.toUpperCase(), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: Color(0xFFFFF9C4), fontSize: 9.5, fontWeight: FontWeight.w900, letterSpacing: 0.4, ), ), Text( '#${(cell.houseIndex + 1).toString().padLeft(3, '0')}', style: const TextStyle(color: Color(0xFFFFD54F), fontSize: 8.5, fontWeight: FontWeight.bold), ), ], ), ), ), ], ); } /// Hidden cell shows ONLY its number (e.g. #049) and NO word Widget _buildHiddenCellContent(HouseCell cell, ArchonSector sector) { return Stack( children: [ Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.home_rounded, size: 14, color: sector.sectorColor.withValues(alpha: 0.4)), const SizedBox(height: 2), Text( '#${(cell.houseIndex + 1).toString().padLeft(3, '0')}', style: const TextStyle( color: Color(0xFFB0BEC5), fontSize: 11, fontWeight: FontWeight.bold, letterSpacing: 0.5, ), ), ], ), ), ], ); } /// Revealed cell shows its selection order, Host name, and Guest name Widget _buildRevealedCellContent(HouseCell cell, ArchonSector sector) { return Stack( children: [ Positioned( top: 3, left: 4, child: Container( width: 16, height: 16, decoration: const BoxDecoration( color: Color(0xFFFFD54F), shape: BoxShape.circle, ), child: Center( child: Text( '${cell.selectionOrder}', style: const TextStyle(color: Colors.black, fontWeight: FontWeight.w900, fontSize: 10), ), ), ), ), Positioned( top: 3, right: 4, child: Text( '#${(cell.houseIndex + 1).toString().padLeft(3, '0')}', style: const TextStyle(color: Color(0xFFFFD54F), fontSize: 9, fontWeight: FontWeight.bold), ), ), Center( child: Padding( padding: const EdgeInsets.only(top: 14, left: 2, right: 2), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( cell.houseWord.word.toUpperCase(), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(color: Colors.white, fontSize: 8.5, fontWeight: FontWeight.w800), ), const Icon(Icons.arrow_downward_rounded, size: 10, color: Color(0xFFFFD54F)), Text( cell.guestWord!.word.toUpperCase(), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(color: Color(0xFFFFD54F), fontSize: 8.5, fontWeight: FontWeight.w900), ), ], ), ), ), ], ); } Widget _buildBottomPanel(int openedCount) { final isDone = openedCount >= _targetHousesCount; return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: const Color(0xFF151829).withValues(alpha: 0.95), borderRadius: BorderRadius.circular(22), border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.5), width: 1.2), boxShadow: const [ BoxShadow( color: Color(0x99000000), blurRadius: 18, offset: Offset(0, 6), ), ], ), child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( mainAxisSize: MainAxisSize.min, children: [ // Counter badge Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: const Color(0xFF2A2D4A), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.4)), ), child: Text( '$openedCount / $_targetHousesCount домов', style: const TextStyle(color: Color(0xFFFFD54F), fontWeight: FontWeight.w900, fontSize: 13), ), ), const SizedBox(width: 10), // Roll Dice Button if (!isDone) OutlinedButton.icon( style: OutlinedButton.styleFrom( foregroundColor: const Color(0xFFFFD54F), side: const BorderSide(color: Color(0xFFFFD54F)), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), onPressed: _startRollHouseAndGuest, icon: const Icon(Icons.casino_rounded, size: 18), label: const Text('Бросить кости', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12)), ), const SizedBox(width: 10), // Proceed button ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: isDone ? const Color(0xFF2E7D32) : const Color(0xFF2A2D4A), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), onPressed: openedCount > 0 ? _openResultScreen : null, icon: Icon(isDone ? Icons.auto_awesome : Icons.menu_book_rounded, size: 16), label: Text( isDone ? 'Итог расклада' : 'Просмотр ($openedCount)', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13), ), ), ], ), ), ); } } /// Querent selection mode: Man, Woman, or search any symbol enum QuerentMode { man, woman, search } /// Modal Sheet: Select Querent (Man / Woman / Search Symbol), House Target Count (1..9), and Optional Question class _QuerentSetupSheet extends StatefulWidget { final WordItem? currentQuerent; final int currentCount; final String? currentQuestion; final bool isInitial; final void Function(WordItem querent, int count, String? question) onApplied; const _QuerentSetupSheet({ required this.currentQuerent, required this.currentCount, required this.currentQuestion, required this.isInitial, required this.onApplied, }); @override State<_QuerentSetupSheet> createState() => _QuerentSetupSheetState(); } class _QuerentSetupSheetState extends State<_QuerentSetupSheet> { final _repo = WordsRepository(); final _searchController = TextEditingController(); final _questionController = TextEditingController(); QuerentMode _mode = QuerentMode.man; WordItem? _selectedWord; int _selectedCount = 3; List _searchResults = []; @override void initState() { super.initState(); _selectedCount = widget.currentCount.clamp(1, 9); _questionController.text = widget.currentQuestion ?? ''; // Determine initial mode from current querent if (widget.currentQuerent != null) { final w = widget.currentQuerent!.word.toLowerCase(); if (w == 'мужчина') { _mode = QuerentMode.man; } else if (w == 'женщина') { _mode = QuerentMode.woman; } else { _mode = QuerentMode.search; } _selectedWord = widget.currentQuerent; } else { _mode = QuerentMode.man; _selectedWord = _findWordByName('мужчина'); } } @override void dispose() { _searchController.dispose(); _questionController.dispose(); super.dispose(); } WordItem? _findWordByName(String name) { try { return _repo.allWords.firstWhere((w) => w.word.toLowerCase() == name.toLowerCase()); } catch (_) { return null; } } void _onModeChanged(QuerentMode mode) { HapticFeedback.selectionClick(); setState(() { _mode = mode; switch (mode) { case QuerentMode.man: _selectedWord = _findWordByName('мужчина'); break; case QuerentMode.woman: _selectedWord = _findWordByName('женщина'); break; case QuerentMode.search: // Keep current or clear if (_selectedWord?.word.toLowerCase() == 'мужчина' || _selectedWord?.word.toLowerCase() == 'женщина') { _selectedWord = null; } _searchResults = _repo.allWords.take(20).toList(); break; } }); } void _onSearchChanged(String query) { if (query.trim().isEmpty) { setState(() => _searchResults = _repo.allWords.take(20).toList()); return; } final q = query.trim().toLowerCase(); final results = _repo.allWords.where((w) { return w.word.toLowerCase().contains(q) || w.category.toLowerCase().contains(q) || w.description.toLowerCase().contains(q); }).take(30).toList(); setState(() => _searchResults = results); } @override Widget build(BuildContext context) { final bottomInset = MediaQuery.of(context).viewInsets.bottom; return Container( constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.85, ), decoration: const BoxDecoration( color: Color(0xFF141226), borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Drag handle Container( margin: const EdgeInsets.only(top: 10, bottom: 6), width: 40, height: 4, decoration: BoxDecoration( color: Colors.white24, borderRadius: BorderRadius.circular(2), ), ), // Title Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), child: Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( gradient: const LinearGradient(colors: [Color(0xFFFFD54F), Color(0xFFFFA000)]), borderRadius: BorderRadius.circular(8), ), child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.person_search_rounded, color: Colors.black, size: 14), SizedBox(width: 4), Text( 'ВЫБОР КВЕРЕНТА', style: TextStyle(color: Colors.black, fontWeight: FontWeight.w900, fontSize: 11), ), ], ), ), const Spacer(), IconButton( icon: const Icon(Icons.close_rounded, color: Color(0xFF9FA8DA)), onPressed: () => Navigator.pop(context), ), ], ), ), Expanded( child: SingleChildScrollView( padding: EdgeInsets.fromLTRB(20, 0, 20, bottomInset + 10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // === Step 1: Querent Type Selection === const Text( 'Кто или что является объектом расклада?', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14), ), const SizedBox(height: 4), const Text( 'Кверент — субъект или объект, на который производится гадание', style: TextStyle(color: Color(0xFF90A4AE), fontSize: 11), ), const SizedBox(height: 12), // Three option cards Row( children: [ _buildModeCard( mode: QuerentMode.man, icon: Icons.man_rounded, label: 'Мужчина', color: const Color(0xFF42A5F5), ), const SizedBox(width: 8), _buildModeCard( mode: QuerentMode.woman, icon: Icons.woman_rounded, label: 'Женщина', color: const Color(0xFFEC407A), ), const SizedBox(width: 8), _buildModeCard( mode: QuerentMode.search, icon: Icons.search_rounded, label: 'Символ', color: const Color(0xFFFFD54F), ), ], ), const SizedBox(height: 16), // === Selected Querent Display === if (_selectedWord != null) ...[ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xFF2C1645), Color(0xFF19142E)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFFFFD54F), width: 1.2), boxShadow: const [ BoxShadow(color: Color(0x44FFD54F), blurRadius: 12, spreadRadius: 1), ], ), child: Row( children: [ Container( width: 44, height: 44, decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xFFFFD54F), Color(0xFFFFA000)], ), borderRadius: BorderRadius.circular(10), ), child: Center( child: Text( '#${_selectedWord!.id.toString().padLeft(3, '0')}', style: const TextStyle( color: Colors.black, fontWeight: FontWeight.w900, fontSize: 12, ), ), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( _selectedWord!.word.toUpperCase(), style: const TextStyle( color: Color(0xFFFFF9C4), fontWeight: FontWeight.w900, fontSize: 17, ), ), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: const Color(0xFFFFD54F), borderRadius: BorderRadius.circular(4), ), child: const Text( 'Символ Кверента', style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold, fontSize: 10), ), ), ], ), const SizedBox(height: 3), Text( _selectedWord!.category, style: const TextStyle(color: Color(0xFFE0E0E0), fontSize: 11), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), Icon( _mode == QuerentMode.man ? Icons.man_rounded : _mode == QuerentMode.woman ? Icons.woman_rounded : Icons.stars_rounded, color: const Color(0xFFFFD54F), size: 28, ), ], ), ), const SizedBox(height: 14), ], // === Search field (only in search mode) === if (_mode == QuerentMode.search) ...[ TextField( controller: _searchController, onChanged: _onSearchChanged, style: const TextStyle(color: Colors.white, fontSize: 14), decoration: InputDecoration( hintText: 'Поиск символа (слово, категория)...', hintStyle: const TextStyle(color: Color(0xFF78909C), fontSize: 13), prefixIcon: const Icon(Icons.search_rounded, color: Color(0xFFFFD54F), size: 20), suffixIcon: _searchController.text.isNotEmpty ? IconButton( icon: const Icon(Icons.clear_rounded, size: 18, color: Color(0xFF90A4AE)), onPressed: () { _searchController.clear(); _onSearchChanged(''); }, ) : null, filled: true, fillColor: const Color(0xFF1E223D), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Color(0xFF2C3258)), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Color(0xFF2C3258)), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Color(0xFFFFD54F)), ), contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), ), ), const SizedBox(height: 10), SizedBox( height: 40, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: _searchResults.length, separatorBuilder: (ctx, i) => const SizedBox(width: 6), itemBuilder: (ctx, i) { final w = _searchResults[i]; final isSelected = _selectedWord?.word == w.word; return ChoiceChip( label: Text(w.word.toUpperCase()), selected: isSelected, selectedColor: const Color(0xFFFFD54F), backgroundColor: const Color(0xFF1E223D), labelStyle: TextStyle( color: isSelected ? Colors.black : Colors.white, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, fontSize: 11, ), onSelected: (selected) { if (selected) { setState(() => _selectedWord = w); } }, ); }, ), ), const SizedBox(height: 14), ], // === Step 2: Target Houses Count (1..9) === Row( children: [ const Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Количество открываемых домов:', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13), ), Text( 'Формирует фигуру расклада (1–9 точек)', style: TextStyle(color: Color(0xFF90A4AE), fontSize: 11), ), ], ), ), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: const Color(0xFFFFD54F), borderRadius: BorderRadius.circular(8), ), child: Text( '$_selectedCount ${_selectedCount == 1 ? "дом" : (_selectedCount < 5 ? "дома" : "домов")}', style: const TextStyle(color: Colors.black, fontWeight: FontWeight.w900, fontSize: 12), ), ), ], ), const SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: List.generate(9, (idx) { final n = idx + 1; final isSel = _selectedCount == n; return InkWell( onTap: () { HapticFeedback.selectionClick(); setState(() => _selectedCount = n); }, borderRadius: BorderRadius.circular(8), child: AnimatedContainer( duration: const Duration(milliseconds: 150), width: 32, height: 36, decoration: BoxDecoration( color: isSel ? const Color(0xFFFFD54F) : const Color(0xFF1E223D), borderRadius: BorderRadius.circular(8), border: Border.all( color: isSel ? const Color(0xFFFFD54F) : const Color(0xFF2C3258), ), ), child: Center( child: Text( '$n', style: TextStyle( color: isSel ? Colors.black : Colors.white, fontWeight: FontWeight.bold, fontSize: 14, ), ), ), ), ); }), ), const SizedBox(height: 20), // === Step 3: Question TextField === const Row( children: [ Icon(Icons.help_outline_rounded, color: Color(0xFFFFD54F), size: 16), SizedBox(width: 6), Text( 'Вопрос или тема расклада (по желанию):', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13), ), ], ), const SizedBox(height: 8), TextField( controller: _questionController, maxLines: 2, style: const TextStyle(color: Colors.white, fontSize: 13), decoration: InputDecoration( hintText: 'Например: «Каков скрытый вектор развития нового проекта?»', hintStyle: const TextStyle(color: Color(0xFF78909C), fontSize: 12), filled: true, fillColor: const Color(0xFF1E223D), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Color(0xFF2C3258)), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Color(0xFF2C3258)), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Color(0xFFFFD54F)), ), contentPadding: const EdgeInsets.all(12), ), ), const SizedBox(height: 24), ], ), ), ), // Action Button Padding( padding: const EdgeInsets.fromLTRB(20, 10, 20, 20), child: SizedBox( width: double.infinity, height: 50, child: FilledButton.icon( style: FilledButton.styleFrom( backgroundColor: const Color(0xFFFFD54F), foregroundColor: Colors.black, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), ), onPressed: _selectedWord == null ? null : () { HapticFeedback.heavyImpact(); final q = _questionController.text.trim(); widget.onApplied(_selectedWord!, _selectedCount, q.isNotEmpty ? q : null); }, icon: const Icon(Icons.auto_awesome, color: Colors.black), label: Text( widget.isInitial ? 'Войти в Матрицу Домов' : 'Применить настройки', style: const TextStyle(fontWeight: FontWeight.w900, fontSize: 15), ), ), ), ), ], ), ); } Widget _buildModeCard({ required QuerentMode mode, required IconData icon, required String label, required Color color, }) { final isSelected = _mode == mode; return Expanded( child: InkWell( onTap: () => _onModeChanged(mode), borderRadius: BorderRadius.circular(14), child: AnimatedContainer( duration: const Duration(milliseconds: 200), padding: const EdgeInsets.symmetric(vertical: 14), decoration: BoxDecoration( color: isSelected ? color.withValues(alpha: 0.2) : const Color(0xFF1A1D35), borderRadius: BorderRadius.circular(14), border: Border.all( color: isSelected ? color : const Color(0xFF2C3258), width: isSelected ? 2.0 : 1.0, ), boxShadow: isSelected ? [BoxShadow(color: color.withValues(alpha: 0.3), blurRadius: 12, spreadRadius: 1)] : null, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, color: isSelected ? color : const Color(0xFF78909C), size: 28), const SizedBox(height: 6), Text( label, style: TextStyle( color: isSelected ? color : const Color(0xFF90A4AE), fontWeight: isSelected ? FontWeight.w900 : FontWeight.w500, fontSize: 12, ), ), ], ), ), ), ); } } /// Modal Bottom Sheet: Cast Guest for an already chosen House class _CastGuestSheet extends StatefulWidget { final HouseCell cell; final ValueChanged onGuestDetermined; const _CastGuestSheet({required this.cell, required this.onGuestDetermined}); @override State<_CastGuestSheet> createState() => _CastGuestSheetState(); } class _CastGuestSheetState extends State<_CastGuestSheet> { final _repo = WordsRepository(); String _digits = ''; bool _isVirtualRolling = false; void _onDigit(String d) { HapticFeedback.selectionClick(); if (_digits.length < 3) { setState(() => _digits += d); if (_digits.length == 3) { _finishManualInput(); } } } void _backspace() { if (_digits.isNotEmpty) { setState(() => _digits = _digits.substring(0, _digits.length - 1)); } } void _finishManualInput() { final num = int.tryParse(_digits); if (num != null && num >= 0 && num < 1000) { final guest = _repo.getWordBySessionNumber(num) ?? _repo.allWords[num % _repo.allWords.length]; widget.onGuestDetermined(guest); } } Future _rollVirtualDice() async { setState(() => _isVirtualRolling = true); HapticFeedback.mediumImpact(); final rnd = Random.secure(); int flickerCount = 0; Timer.periodic(const Duration(milliseconds: 60), (timer) { flickerCount++; if (mounted) { setState(() { _digits = rnd.nextInt(1000).toString().padLeft(3, '0'); }); } if (flickerCount > 10) { timer.cancel(); final finalNum = Random.secure().nextInt(1000); if (mounted) { setState(() { _digits = finalNum.toString().padLeft(3, '0'); _isVirtualRolling = false; }); final guest = _repo.getWordBySessionNumber(finalNum) ?? _repo.allWords[finalNum % _repo.allWords.length]; Future.delayed(const Duration(milliseconds: 250), () { if (mounted) { widget.onGuestDetermined(guest); } }); } } }); } @override Widget build(BuildContext context) { return Padding( padding: EdgeInsets.fromLTRB(20, 16, 20, MediaQuery.of(context).viewInsets.bottom + 24), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ const GradientStar(size: 20), const SizedBox(width: 8), Text( 'ДОМ #${(widget.cell.houseIndex + 1).toString().padLeft(3, '0')} ВЫБРАН', style: const TextStyle(color: Color(0xFFFFF9C4), fontWeight: FontWeight.bold, fontSize: 16), ), const Spacer(), IconButton( icon: const Icon(Icons.close_rounded, color: Color(0xFF9FA8DA)), onPressed: () => Navigator.pop(context), ), ], ), const SizedBox(height: 6), const Text( 'Какой гость пришел в этот дом? Бросьте кубики или введите число 000..999 с реальных костей:', style: TextStyle(color: Color(0xFFB0BEC5), fontSize: 13), ), const SizedBox(height: 16), // Digits display & virtual roll button Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 120, height: 48, decoration: BoxDecoration( color: const Color(0xFF1E2238), borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFFFFD54F)), ), child: Center( child: Text( _digits.isEmpty ? '— — —' : _digits.padRight(3, '—'), style: const TextStyle( color: Color(0xFFFFD54F), fontSize: 22, fontWeight: FontWeight.w900, letterSpacing: 4, ), ), ), ), const SizedBox(width: 14), ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF2A2D4A), foregroundColor: const Color(0xFFFFD54F), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), side: const BorderSide(color: Color(0xFFFFD54F)), ), ), onPressed: _isVirtualRolling ? null : _rollVirtualDice, icon: const Icon(Icons.casino_rounded), label: const Text('Бросить 3d10'), ), ], ), const SizedBox(height: 16), // Keypad (0..9) _buildKeypad(), ], ), ); } Widget _buildKeypad() { return Column( children: [ for (var row in [ ['1', '2', '3'], ['4', '5', '6'], ['7', '8', '9'], ['C', '0', '⌫'], ]) Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: row.map((key) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 6), child: SizedBox( width: 76, height: 44, child: OutlinedButton( style: OutlinedButton.styleFrom( backgroundColor: const Color(0xFF191C30), foregroundColor: const Color(0xFFECEFF1), side: const BorderSide(color: Color(0xFF2A2D4A)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), onPressed: () { if (key == 'C') { setState(() => _digits = ''); } else if (key == '⌫') { _backspace(); } else { _onDigit(key); } }, child: Text( key, style: TextStyle( fontSize: (key == 'C' || key == '⌫') ? 16 : 18, fontWeight: FontWeight.bold, ), ), ), ), ); }).toList(), ), ), ], ); } } /// Modal Bottom Sheet: Roll BOTH House and Guest sequentially class _RollHouseAndGuestSheet extends StatefulWidget { final HousesSession session; final void Function(HouseCell cell, WordItem guest) onDetermined; const _RollHouseAndGuestSheet({required this.session, required this.onDetermined}); @override State<_RollHouseAndGuestSheet> createState() => _RollHouseAndGuestSheetState(); } class _RollHouseAndGuestSheetState extends State<_RollHouseAndGuestSheet> { final _repo = WordsRepository(); bool _isRollingHouse = true; // true = Step 1 (House), false = Step 2 (Guest) String _digits = ''; HouseCell? _chosenCell; void _onDigit(String d) { HapticFeedback.selectionClick(); if (_digits.length < 3) { setState(() => _digits += d); if (_digits.length == 3) { _finishStep(); } } } void _finishStep() { final num = int.tryParse(_digits); if (num == null) return; if (_isRollingHouse) { // Step 1: House index (0..989) final houseIdx = num % 990; final cell = widget.session.grid[houseIdx]; setState(() { _chosenCell = cell; _isRollingHouse = false; _digits = ''; }); } else { // Step 2: Guest index (0..999) final guest = _repo.getWordBySessionNumber(num % 1000) ?? _repo.allWords[num % _repo.allWords.length]; widget.onDetermined(_chosenCell!, guest); } } Future _virtualRoll() async { HapticFeedback.mediumImpact(); final rnd = Random.secure(); int flickers = 0; Timer.periodic(const Duration(milliseconds: 60), (timer) { flickers++; if (mounted) { setState(() => _digits = rnd.nextInt(1000).toString().padLeft(3, '0')); } if (flickers > 10) { timer.cancel(); final finalNum = Random.secure().nextInt(1000); if (mounted) { setState(() => _digits = finalNum.toString().padLeft(3, '0')); Future.delayed(const Duration(milliseconds: 250), () { if (mounted) { _finishStep(); } }); } } }); } @override Widget build(BuildContext context) { return Padding( padding: EdgeInsets.fromLTRB(20, 16, 20, MediaQuery.of(context).viewInsets.bottom + 24), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ const GradientStar(size: 20), const SizedBox(width: 8), Text( _isRollingHouse ? 'ШАГ 1: ВЫБОР ДОМА' : 'ШАГ 2: ВЫБОР ГОСТЯ', style: const TextStyle(color: Color(0xFFFFF9C4), fontWeight: FontWeight.bold, fontSize: 16), ), const Spacer(), IconButton( icon: const Icon(Icons.close_rounded, color: Color(0xFF9FA8DA)), onPressed: () => Navigator.pop(context), ), ], ), const SizedBox(height: 6), Text( _isRollingHouse ? 'Бросьте кубики на номер Дома (000..989):' : 'Дом #${(_chosenCell!.houseIndex + 1).toString().padLeft(3, '0')} определен! Бросьте кубики на Гостя (000..999):', style: const TextStyle(color: Color(0xFFB0BEC5), fontSize: 13), ), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 120, height: 48, decoration: BoxDecoration( color: const Color(0xFF1E2238), borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFFFFD54F)), ), child: Center( child: Text( _digits.isEmpty ? '— — —' : _digits.padRight(3, '—'), style: const TextStyle( color: Color(0xFFFFD54F), fontSize: 22, fontWeight: FontWeight.w900, letterSpacing: 4, ), ), ), ), const SizedBox(width: 14), ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF2A2D4A), foregroundColor: const Color(0xFFFFD54F), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), side: const BorderSide(color: Color(0xFFFFD54F)), ), ), onPressed: _virtualRoll, icon: const Icon(Icons.casino_rounded), label: const Text('Бросить 3d10'), ), ], ), const SizedBox(height: 16), // Keypad Column( children: [ for (var row in [ ['1', '2', '3'], ['4', '5', '6'], ['7', '8', '9'], ['C', '0', '⌫'], ]) Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: row.map((key) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 6), child: SizedBox( width: 76, height: 44, child: OutlinedButton( style: OutlinedButton.styleFrom( backgroundColor: const Color(0xFF191C30), foregroundColor: const Color(0xFFECEFF1), side: const BorderSide(color: Color(0xFF2A2D4A)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), onPressed: () { if (key == 'C') { setState(() => _digits = ''); } else if (key == '⌫') { if (_digits.isNotEmpty) { setState(() => _digits = _digits.substring(0, _digits.length - 1)); } } else { _onDigit(key); } }, child: Text( key, style: TextStyle( fontSize: (key == 'C' || key == '⌫') ? 16 : 18, fontWeight: FontWeight.bold, ), ), ), ), ); }).toList(), ), ), ], ), ], ), ); } }