2183 lines
82 KiB
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(),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|