Files
999/lib/screens/houses_field_screen.dart

2183 lines
82 KiB
Dart

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<HousesFieldScreen> createState() => _HousesFieldScreenState();
}
class _HousesFieldScreenState extends State<HousesFieldScreen> {
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<int> 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<WordItem> _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<WordItem> 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<void> _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<void> _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(),
),
),
],
),
],
),
);
}
}