Files
999/lib/screens/runes_divination_screen.dart
T

609 lines
22 KiB
Dart
Raw Blame History

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