1732 lines
65 KiB
Dart
1732 lines
65 KiB
Dart
import 'dart:math';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter/services.dart';
|
||
import '../models/divination_layout.dart';
|
||
import '../models/history_entry_model.dart';
|
||
import '../models/syncretic_session_model.dart';
|
||
import '../models/tradition_model.dart';
|
||
import '../models/word_model.dart';
|
||
import '../services/ai_oracle_service.dart';
|
||
import '../services/traditions_repository.dart';
|
||
import '../services/words_repository.dart';
|
||
import '../widgets/divination_instruction_modal.dart';
|
||
import 'symbol_detail_screen.dart';
|
||
import '../services/oracle_master_prompt_generator.dart';
|
||
|
||
class GeomancyFigure {
|
||
final String id;
|
||
final String fullName;
|
||
final String latinName;
|
||
final String arabicName;
|
||
final String russianName;
|
||
final List<int> lines; // 4 rows: 1 or 2 dots (Fire, Air, Water, Earth)
|
||
final String element;
|
||
final String planet;
|
||
final String group;
|
||
final String lore;
|
||
final int? primaryWordId;
|
||
final TraditionStation station;
|
||
|
||
const GeomancyFigure({
|
||
required this.id,
|
||
required this.fullName,
|
||
required this.latinName,
|
||
required this.arabicName,
|
||
required this.russianName,
|
||
required this.lines,
|
||
required this.element,
|
||
required this.planet,
|
||
required this.group,
|
||
required this.lore,
|
||
this.primaryWordId,
|
||
required this.station,
|
||
});
|
||
|
||
String get signature => lines.join(',');
|
||
int get totalDots => lines.reduce((a, b) => a + b);
|
||
bool get isEven => totalDots % 2 == 0;
|
||
|
||
static List<int> combineLines(List<int> a, List<int> b) {
|
||
return List.generate(4, (i) => (a[i] + b[i]) % 2 == 0 ? 2 : 1);
|
||
}
|
||
}
|
||
|
||
class GeomancyDivinationScreen extends StatefulWidget {
|
||
final String? initialQuestion;
|
||
|
||
const GeomancyDivinationScreen({super.key, this.initialQuestion});
|
||
|
||
@override
|
||
State<GeomancyDivinationScreen> createState() => _GeomancyDivinationScreenState();
|
||
}
|
||
|
||
class _GeomancyDivinationScreenState extends State<GeomancyDivinationScreen> with SingleTickerProviderStateMixin {
|
||
late TabController _tabController;
|
||
final _questionController = TextEditingController();
|
||
final _repo = WordsRepository();
|
||
final _aiService = AiOracleService();
|
||
|
||
List<GeomancyFigure> _allFigures = [];
|
||
final Map<String, GeomancyFigure> _figureBySig = {};
|
||
|
||
DivinationLayout _selectedLayout = DivinationLayouts.geomancyLayouts[0];
|
||
|
||
// Sand animation / casting state
|
||
bool _isCasting = false;
|
||
List<int> _currentSandDotCounts = [0, 0, 0, 0];
|
||
|
||
// Drawn results for 1-figure and 3-figure layouts
|
||
final List<({GeomancyFigure figure, String positionTitle})> _drawnResults = [];
|
||
|
||
// Shield state (4 Mothers cast, others derived)
|
||
final List<GeomancyFigure> _shieldMothers = [];
|
||
List<GeomancyFigure> _shieldDaughters = [];
|
||
List<GeomancyFigure> _shieldNephews = [];
|
||
GeomancyFigure? _shieldRightWitness;
|
||
GeomancyFigure? _shieldLeftWitness;
|
||
GeomancyFigure? _shieldJudge;
|
||
GeomancyFigure? _shieldReconciler;
|
||
GeomancyFigure? _inspectedShieldFigure;
|
||
String? _inspectedShieldTitle;
|
||
|
||
bool _savedToSession = false;
|
||
bool _isAiLoading = false;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_tabController = TabController(length: 2, vsync: this);
|
||
if (widget.initialQuestion != null) {
|
||
_questionController.text = widget.initialQuestion!;
|
||
} else {
|
||
final q = _repo.currentSyncreticSession.mainQuestion;
|
||
if (q != null && q.trim().isNotEmpty) {
|
||
_questionController.text = q.trim();
|
||
}
|
||
}
|
||
_loadGeomancyFigures();
|
||
}
|
||
|
||
void _loadGeomancyFigures() {
|
||
final canon = TraditionsRepository().byId('geomancy');
|
||
if (canon == null) return;
|
||
|
||
final parsed = <GeomancyFigure>[];
|
||
for (final st in canon.stations) {
|
||
final rawLines = st.symbol.split(RegExp(r'[\r\n]+'));
|
||
final lines = <int>[];
|
||
for (final l in rawLines) {
|
||
final clean = l.replaceAll(' ', '').trim();
|
||
if (clean.isNotEmpty) {
|
||
lines.add(clean.length);
|
||
}
|
||
}
|
||
|
||
if (lines.length != 4) continue;
|
||
|
||
String latin = st.id;
|
||
String arabic = '';
|
||
String russian = '';
|
||
|
||
final m = RegExp(r'^\d*\s*([A-Za-z\s]+)\s*\(([^/]+)/([^)]+)\)').firstMatch(st.name);
|
||
if (m != null) {
|
||
latin = m.group(1)?.trim() ?? st.id;
|
||
arabic = m.group(2)?.trim() ?? '';
|
||
russian = m.group(3)?.trim() ?? '';
|
||
} else {
|
||
latin = st.name;
|
||
}
|
||
|
||
final parts = st.group.split('·').map((p) => p.trim()).toList();
|
||
final element = parts.length > 1 ? parts[1] : 'Земля';
|
||
final planet = parts.length > 2 ? parts[2] : 'Марс';
|
||
|
||
final fig = GeomancyFigure(
|
||
id: st.id,
|
||
fullName: st.name,
|
||
latinName: latin,
|
||
arabicName: arabic,
|
||
russianName: russian.isNotEmpty ? russian : latin,
|
||
lines: lines,
|
||
element: element,
|
||
planet: planet,
|
||
group: st.group,
|
||
lore: st.lore,
|
||
primaryWordId: st.primary?.id,
|
||
station: st,
|
||
);
|
||
|
||
parsed.add(fig);
|
||
_figureBySig[fig.signature] = fig;
|
||
}
|
||
|
||
setState(() {
|
||
_allFigures = parsed;
|
||
});
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_tabController.dispose();
|
||
_questionController.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
void _onLayoutChanged(DivinationLayout layout) {
|
||
setState(() {
|
||
_selectedLayout = layout;
|
||
_reset();
|
||
});
|
||
HapticFeedback.selectionClick();
|
||
}
|
||
|
||
void _reset() {
|
||
setState(() {
|
||
_drawnResults.clear();
|
||
_shieldMothers.clear();
|
||
_shieldDaughters.clear();
|
||
_shieldNephews.clear();
|
||
_shieldRightWitness = null;
|
||
_shieldLeftWitness = null;
|
||
_shieldJudge = null;
|
||
_shieldReconciler = null;
|
||
_inspectedShieldFigure = null;
|
||
_inspectedShieldTitle = null;
|
||
_savedToSession = false;
|
||
_currentSandDotCounts = [0, 0, 0, 0];
|
||
});
|
||
}
|
||
|
||
void _deriveShield() {
|
||
if (_shieldMothers.length != 4) return;
|
||
|
||
final m1 = _shieldMothers[0];
|
||
final m2 = _shieldMothers[1];
|
||
final m3 = _shieldMothers[2];
|
||
final m4 = _shieldMothers[3];
|
||
|
||
// 4 Daughters:
|
||
final d1Sig = [m1.lines[0], m2.lines[0], m3.lines[0], m4.lines[0]].join(',');
|
||
final d2Sig = [m1.lines[1], m2.lines[1], m3.lines[1], m4.lines[1]].join(',');
|
||
final d3Sig = [m1.lines[2], m2.lines[2], m3.lines[2], m4.lines[2]].join(',');
|
||
final d4Sig = [m1.lines[3], m2.lines[3], m3.lines[3], m4.lines[3]].join(',');
|
||
|
||
final d1 = _figureBySig[d1Sig] ?? m1;
|
||
final d2 = _figureBySig[d2Sig] ?? m2;
|
||
final d3 = _figureBySig[d3Sig] ?? m3;
|
||
final d4 = _figureBySig[d4Sig] ?? m4;
|
||
_shieldDaughters = [d1, d2, d3, d4];
|
||
|
||
// 4 Nephews:
|
||
// N1 = M1 + M2, N2 = M3 + M4, N3 = D1 + D2, N4 = D3 + D4
|
||
final n1Sig = GeomancyFigure.combineLines(m1.lines, m2.lines).join(',');
|
||
final n2Sig = GeomancyFigure.combineLines(m3.lines, m4.lines).join(',');
|
||
final n3Sig = GeomancyFigure.combineLines(d1.lines, d2.lines).join(',');
|
||
final n4Sig = GeomancyFigure.combineLines(d3.lines, d4.lines).join(',');
|
||
|
||
final n1 = _figureBySig[n1Sig] ?? m1;
|
||
final n2 = _figureBySig[n2Sig] ?? m2;
|
||
final n3 = _figureBySig[n3Sig] ?? d1;
|
||
final n4 = _figureBySig[n4Sig] ?? d2;
|
||
_shieldNephews = [n1, n2, n3, n4];
|
||
|
||
// 2 Witnesses:
|
||
// Right Witness (W1) = N1 + N2 (outer situation/querent's question)
|
||
// Left Witness (W2) = N3 + N4 (inner soul/querent themselves)
|
||
final w1Sig = GeomancyFigure.combineLines(n1.lines, n2.lines).join(',');
|
||
final w2Sig = GeomancyFigure.combineLines(n3.lines, n4.lines).join(',');
|
||
_shieldRightWitness = _figureBySig[w1Sig] ?? n1;
|
||
_shieldLeftWitness = _figureBySig[w2Sig] ?? n2;
|
||
|
||
// Judge = W1 + W2
|
||
final jSig = GeomancyFigure.combineLines(_shieldRightWitness!.lines, _shieldLeftWitness!.lines).join(',');
|
||
_shieldJudge = _figureBySig[jSig] ?? _shieldRightWitness!;
|
||
|
||
// Reconciler = Judge + M1
|
||
final rSig = GeomancyFigure.combineLines(_shieldJudge!.lines, m1.lines).join(',');
|
||
_shieldReconciler = _figureBySig[rSig] ?? _shieldJudge!;
|
||
|
||
_inspectedShieldFigure = _shieldJudge;
|
||
_inspectedShieldTitle = 'Судья (Итоговое Разрешение)';
|
||
}
|
||
|
||
void _addFigureToSpread(GeomancyFigure fig) {
|
||
if (_selectedLayout.id == 'geomancy_shield') {
|
||
if (_shieldMothers.length < 4) {
|
||
final completed = (_shieldMothers.length + 1 == 4);
|
||
setState(() {
|
||
_shieldMothers.add(fig);
|
||
if (completed) {
|
||
_deriveShield();
|
||
}
|
||
});
|
||
if (completed) {
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
_saveToSyncreticSession(silent: false);
|
||
});
|
||
}
|
||
}
|
||
} else {
|
||
if (_drawnResults.length < _selectedLayout.count) {
|
||
final completed = (_drawnResults.length + 1 == _selectedLayout.count);
|
||
final posTitle = _selectedLayout.positionTitles[_drawnResults.length];
|
||
setState(() {
|
||
_drawnResults.add((figure: fig, positionTitle: posTitle));
|
||
});
|
||
if (completed) {
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
_saveToSyncreticSession(silent: false);
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Future<void> _performSandRitualCast() async {
|
||
if (_isCasting) return;
|
||
final isShield = _selectedLayout.id == 'geomancy_shield';
|
||
final isComplete = isShield ? _shieldMothers.length >= 4 : _drawnResults.length >= _selectedLayout.count;
|
||
if (isComplete) return;
|
||
|
||
setState(() {
|
||
_isCasting = true;
|
||
_currentSandDotCounts = [0, 0, 0, 0];
|
||
});
|
||
|
||
final rng = Random();
|
||
final counts = <int>[];
|
||
final lines = <int>[];
|
||
|
||
for (int row = 0; row < 4; row++) {
|
||
HapticFeedback.lightImpact();
|
||
final dotCount = 7 + rng.nextInt(15);
|
||
counts.add(dotCount);
|
||
lines.add(dotCount % 2 == 1 ? 1 : 2);
|
||
|
||
setState(() {
|
||
_currentSandDotCounts = List.from(counts)..addAll(List.filled(4 - counts.length, 0));
|
||
});
|
||
await Future.delayed(const Duration(milliseconds: 140));
|
||
}
|
||
|
||
final sig = lines.join(',');
|
||
final fig = _figureBySig[sig] ?? _allFigures[rng.nextInt(_allFigures.length)];
|
||
|
||
HapticFeedback.mediumImpact();
|
||
setState(() {
|
||
_isCasting = false;
|
||
_addFigureToSpread(fig);
|
||
});
|
||
}
|
||
|
||
Future<void> _castAllRemainingSteps() async {
|
||
final isShield = _selectedLayout.id == 'geomancy_shield';
|
||
final target = isShield ? 4 : _selectedLayout.count;
|
||
final current = isShield ? _shieldMothers.length : _drawnResults.length;
|
||
final remaining = target - current;
|
||
|
||
for (int i = 0; i < remaining; i++) {
|
||
await _performSandRitualCast();
|
||
await Future.delayed(const Duration(milliseconds: 120));
|
||
}
|
||
}
|
||
|
||
WordItem? _resolveLinkedWord(int? wordId) {
|
||
if (wordId == null) return null;
|
||
return _repo.allWords.firstWhere(
|
||
(w) => w.sessionNumber == wordId,
|
||
orElse: () => _repo.allWords.firstWhere(
|
||
(w) => w.id == wordId,
|
||
orElse: () => _repo.allWords.first,
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> _saveToSyncreticSession({bool silent = false}) async {
|
||
final isShield = _selectedLayout.id == 'geomancy_shield';
|
||
final items = <ReadingSignItem>[];
|
||
|
||
if (isShield) {
|
||
if (_shieldMothers.length < 4 || _shieldJudge == null) return;
|
||
|
||
for (int i = 0; i < 4; i++) {
|
||
final m = _shieldMothers[i];
|
||
final word = _resolveLinkedWord(m.primaryWordId);
|
||
items.add(ReadingSignItem(
|
||
title: '${i + 1}-я Мать: ${m.fullName}',
|
||
subtitle: '${m.element} • ${m.planet}',
|
||
symbol: m.station.symbol,
|
||
lore: m.lore,
|
||
linkedWord: word,
|
||
));
|
||
}
|
||
for (int i = 0; i < 4; i++) {
|
||
final d = _shieldDaughters[i];
|
||
final word = _resolveLinkedWord(d.primaryWordId);
|
||
items.add(ReadingSignItem(
|
||
title: '${i + 1}-я Дочь: ${d.fullName}',
|
||
subtitle: '${d.element} • ${d.planet}',
|
||
symbol: d.station.symbol,
|
||
lore: d.lore,
|
||
linkedWord: word,
|
||
));
|
||
}
|
||
for (int i = 0; i < 4; i++) {
|
||
final n = _shieldNephews[i];
|
||
final word = _resolveLinkedWord(n.primaryWordId);
|
||
items.add(ReadingSignItem(
|
||
title: '${i + 1}-й Племянник: ${n.fullName}',
|
||
subtitle: '${n.element} • ${n.planet}',
|
||
symbol: n.station.symbol,
|
||
lore: n.lore,
|
||
linkedWord: word,
|
||
));
|
||
}
|
||
if (_shieldRightWitness != null) {
|
||
items.add(ReadingSignItem(
|
||
title: 'Правый Свидетель (Внешнее): ${_shieldRightWitness!.fullName}',
|
||
subtitle: '${_shieldRightWitness!.element} • ${_shieldRightWitness!.planet}',
|
||
symbol: _shieldRightWitness!.station.symbol,
|
||
lore: _shieldRightWitness!.lore,
|
||
linkedWord: _resolveLinkedWord(_shieldRightWitness!.primaryWordId),
|
||
));
|
||
}
|
||
if (_shieldLeftWitness != null) {
|
||
items.add(ReadingSignItem(
|
||
title: 'Левый Свидетель (Внутреннее): ${_shieldLeftWitness!.fullName}',
|
||
subtitle: '${_shieldLeftWitness!.element} • ${_shieldLeftWitness!.planet}',
|
||
symbol: _shieldLeftWitness!.station.symbol,
|
||
lore: _shieldLeftWitness!.lore,
|
||
linkedWord: _resolveLinkedWord(_shieldLeftWitness!.primaryWordId),
|
||
));
|
||
}
|
||
items.add(ReadingSignItem(
|
||
title: 'СУДЬЯ (Итог): ${_shieldJudge!.fullName}',
|
||
subtitle: '${_shieldJudge!.element} • ${_shieldJudge!.planet}',
|
||
symbol: _shieldJudge!.station.symbol,
|
||
lore: _shieldJudge!.lore,
|
||
linkedWord: _resolveLinkedWord(_shieldJudge!.primaryWordId),
|
||
));
|
||
if (_shieldReconciler != null) {
|
||
items.add(ReadingSignItem(
|
||
title: 'Примиритель: ${_shieldReconciler!.fullName}',
|
||
subtitle: '${_shieldReconciler!.element} • ${_shieldReconciler!.planet}',
|
||
symbol: _shieldReconciler!.station.symbol,
|
||
lore: _shieldReconciler!.lore,
|
||
linkedWord: _resolveLinkedWord(_shieldReconciler!.primaryWordId),
|
||
));
|
||
}
|
||
} else {
|
||
for (final r in _drawnResults) {
|
||
final word = _resolveLinkedWord(r.figure.primaryWordId);
|
||
items.add(ReadingSignItem(
|
||
title: '${r.positionTitle}: ${r.figure.fullName}',
|
||
subtitle: '${r.figure.element} • ${r.figure.planet}',
|
||
symbol: r.figure.station.symbol,
|
||
lore: r.figure.lore,
|
||
linkedWord: word,
|
||
));
|
||
}
|
||
}
|
||
|
||
final reading = SyncreticReading(
|
||
id: 'reading_${DateTime.now().millisecondsSinceEpoch}',
|
||
systemId: 'geomancy',
|
||
systemName: "Арабская Геомантия ('Илм ар-рамл)",
|
||
systemIcon: '⌛',
|
||
timestamp: DateTime.now(),
|
||
layoutTitle: _selectedLayout.title,
|
||
inputMethod: _tabController.index == 0 ? 'sand_ritual' : 'manual_selection',
|
||
items: items,
|
||
question: _questionController.text.trim(),
|
||
);
|
||
|
||
await _repo.addReadingToSyncreticSession(reading, question: _questionController.text.trim());
|
||
if (mounted) {
|
||
setState(() => _savedToSession = true);
|
||
if (!silent) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text(isShield
|
||
? 'Великий Щит Геомантии (16 фигур) сохранён в сессию дня!'
|
||
: 'Расклад геомантии (${items.length} фигур) сохранён в сессию дня!'),
|
||
backgroundColor: const Color(0xFF2E7D32),
|
||
behavior: SnackBarBehavior.floating,
|
||
duration: const Duration(seconds: 3),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
void _copySpreadSummary() {
|
||
final buffer = StringBuffer();
|
||
buffer.writeln("=== АРАБСКАЯ ГЕОМАНТИЯ ('ИЛМ АР-РАМЛ) ===");
|
||
if (_questionController.text.trim().isNotEmpty) {
|
||
buffer.writeln('Вопрос: «${_questionController.text.trim()}»');
|
||
}
|
||
buffer.writeln('Расклад: ${_selectedLayout.title}\n');
|
||
|
||
final isShield = _selectedLayout.id == 'geomancy_shield';
|
||
if (isShield) {
|
||
buffer.writeln('СУДЬЯ: ${_shieldJudge?.fullName ?? "—"}');
|
||
buffer.writeln('ПРИМИРИТЕЛЬ: ${_shieldReconciler?.fullName ?? "—"}');
|
||
buffer.writeln('Правый свидетель (Внешнее): ${_shieldRightWitness?.fullName ?? "—"}');
|
||
buffer.writeln('Левый свидетель (Внутреннее): ${_shieldLeftWitness?.fullName ?? "—"}\n');
|
||
buffer.writeln('4 МАТЕРИ:');
|
||
for (int i = 0; i < _shieldMothers.length; i++) {
|
||
buffer.writeln(' М${i + 1}: ${_shieldMothers[i].fullName}');
|
||
}
|
||
buffer.writeln('\n4 ДОЧЕРИ:');
|
||
for (int i = 0; i < _shieldDaughters.length; i++) {
|
||
buffer.writeln(' Д${i + 1}: ${_shieldDaughters[i].fullName}');
|
||
}
|
||
buffer.writeln('\n4 ПЛЕМЯННИКА:');
|
||
for (int i = 0; i < _shieldNephews.length; i++) {
|
||
buffer.writeln(' П${i + 1}: ${_shieldNephews[i].fullName}');
|
||
}
|
||
} else {
|
||
for (final r in _drawnResults) {
|
||
buffer.writeln('${r.positionTitle}: ${r.figure.fullName} (${r.figure.element} • ${r.figure.planet})');
|
||
buffer.writeln(' Смысл: ${r.figure.lore}');
|
||
}
|
||
}
|
||
|
||
Clipboard.setData(ClipboardData(text: buffer.toString()));
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(
|
||
content: Text('Расклад геомантии скопирован в буфер обмена!'),
|
||
behavior: SnackBarBehavior.floating,
|
||
),
|
||
);
|
||
}
|
||
|
||
String _buildMasterPrompt() {
|
||
final isShield = _selectedLayout.id == 'geomancy_shield';
|
||
|
||
if (isShield) {
|
||
GeomancyPromptFigure toPromptFigure(GeomancyFigure f, String pos) {
|
||
return GeomancyPromptFigure(
|
||
position: pos,
|
||
name: f.fullName,
|
||
latinName: f.latinName,
|
||
arabicName: f.arabicName,
|
||
lines: f.lines,
|
||
element: f.element,
|
||
planet: f.planet,
|
||
lore: f.lore,
|
||
linkedWord: _resolveLinkedWord(f.primaryWordId),
|
||
);
|
||
}
|
||
|
||
return OracleMasterPromptGenerator.generateGeomancyMasterPrompt(
|
||
question: _questionController.text.trim(),
|
||
layoutTitle: _selectedLayout.title,
|
||
isShield: true,
|
||
mothers: List.generate(
|
||
_shieldMothers.length,
|
||
(i) => toPromptFigure(_shieldMothers[i], 'Мать #' + (i + 1).toString()),
|
||
),
|
||
daughters: List.generate(
|
||
_shieldDaughters.length,
|
||
(i) => toPromptFigure(_shieldDaughters[i], 'Дочь #' + (i + 1).toString()),
|
||
),
|
||
nephews: List.generate(
|
||
_shieldNephews.length,
|
||
(i) => toPromptFigure(_shieldNephews[i], 'Племянник #' + (i + 1).toString()),
|
||
),
|
||
rightWitness: _shieldRightWitness != null
|
||
? toPromptFigure(_shieldRightWitness!, 'Правый Свидетель')
|
||
: null,
|
||
leftWitness: _shieldLeftWitness != null
|
||
? toPromptFigure(_shieldLeftWitness!, 'Левый Свидетель')
|
||
: null,
|
||
judge: _shieldJudge != null ? toPromptFigure(_shieldJudge!, 'Верховный Судья') : null,
|
||
reconciler: _shieldReconciler != null
|
||
? toPromptFigure(_shieldReconciler!, 'Примиритель')
|
||
: null,
|
||
);
|
||
} else {
|
||
final figures = _drawnResults.map((r) {
|
||
return GeomancyPromptFigure(
|
||
position: r.positionTitle,
|
||
name: r.figure.fullName,
|
||
latinName: r.figure.latinName,
|
||
arabicName: r.figure.arabicName,
|
||
lines: r.figure.lines,
|
||
element: r.figure.element,
|
||
planet: r.figure.planet,
|
||
lore: r.figure.lore,
|
||
linkedWord: _resolveLinkedWord(r.figure.primaryWordId),
|
||
);
|
||
}).toList();
|
||
|
||
return OracleMasterPromptGenerator.generateGeomancyMasterPrompt(
|
||
question: _questionController.text.trim(),
|
||
layoutTitle: _selectedLayout.title,
|
||
isShield: false,
|
||
figures: figures,
|
||
);
|
||
}
|
||
}
|
||
|
||
void _showMasterPromptModal() {
|
||
final prompt = _buildMasterPrompt();
|
||
showModalBottomSheet(
|
||
context: context,
|
||
isScrollControlled: true,
|
||
backgroundColor: const Color(0xFF100E19),
|
||
shape: const RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||
),
|
||
builder: (ctx) => DraggableScrollableSheet(
|
||
initialChildSize: 0.8,
|
||
minChildSize: 0.4,
|
||
maxChildSize: 0.95,
|
||
expand: false,
|
||
builder: (_, scrollController) => Padding(
|
||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 20),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
Center(
|
||
child: Container(
|
||
width: 40,
|
||
height: 4,
|
||
margin: const EdgeInsets.only(bottom: 12),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFFFD54F).withValues(alpha: 0.4),
|
||
borderRadius: BorderRadius.circular(2),
|
||
),
|
||
),
|
||
),
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
const Row(
|
||
children: [
|
||
Icon(Icons.psychology_alt_rounded, color: Color(0xFFFFD54F), size: 24),
|
||
SizedBox(width: 10),
|
||
Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
"Мастер-Промпт Геомантии ('Илм ар-рамл)",
|
||
style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold),
|
||
),
|
||
Text(
|
||
'Для Grok, Claude 3.5 Sonnet, Gemini 1.5 Pro, GPT-4o',
|
||
style: TextStyle(color: Color(0xFF90A4AE), fontSize: 11),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
IconButton(
|
||
icon: const Icon(Icons.close_rounded, color: Color(0xFF90A4AE)),
|
||
onPressed: () => Navigator.pop(ctx),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 12),
|
||
Row(
|
||
children: [
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF2A2338),
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: Text(
|
||
'${prompt.length} символов • ${prompt.split(RegExp(r"\s+")).length} слов',
|
||
style: const TextStyle(color: Color(0xFFFFD54F), fontSize: 11, fontWeight: FontWeight.bold),
|
||
),
|
||
),
|
||
const Spacer(),
|
||
ElevatedButton.icon(
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: const Color(0xFFFFD54F),
|
||
foregroundColor: Colors.black,
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||
),
|
||
onPressed: () {
|
||
Clipboard.setData(ClipboardData(text: prompt));
|
||
HapticFeedback.heavyImpact();
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(
|
||
content: Text('Мастер-промпт геомантии скопирован! Вставьте в Grok или Claude.'),
|
||
backgroundColor: Color(0xFF388E3C),
|
||
behavior: SnackBarBehavior.floating,
|
||
),
|
||
);
|
||
},
|
||
icon: const Icon(Icons.copy_rounded, size: 16),
|
||
label: const Text('Скопировать всё', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12.5)),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 12),
|
||
Expanded(
|
||
child: Container(
|
||
padding: const EdgeInsets.all(14),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF07060B),
|
||
borderRadius: BorderRadius.circular(16),
|
||
border: Border.all(color: const Color(0xFF2A2338), width: 1.0),
|
||
),
|
||
child: SingleChildScrollView(
|
||
controller: scrollController,
|
||
child: SelectableText(
|
||
prompt,
|
||
style: const TextStyle(
|
||
color: Color(0xFFECEFF1),
|
||
fontFamily: 'monospace',
|
||
fontSize: 12.5,
|
||
height: 1.45,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> _requestAiInterpretation() async {
|
||
final isShield = _selectedLayout.id == 'geomancy_shield';
|
||
final words = <SessionWord>[];
|
||
|
||
if (isShield) {
|
||
if (_shieldJudge == null) return;
|
||
final jWord = _resolveLinkedWord(_shieldJudge!.primaryWordId);
|
||
if (jWord != null) {
|
||
words.add(SessionWord.fromWordItem(jWord, positionTitle: 'Судья: ${_shieldJudge!.fullName}'));
|
||
}
|
||
if (_shieldRightWitness != null) {
|
||
final wWord = _resolveLinkedWord(_shieldRightWitness!.primaryWordId);
|
||
if (wWord != null) {
|
||
words.add(SessionWord.fromWordItem(wWord, positionTitle: 'Правый Свидетель: ${_shieldRightWitness!.fullName}'));
|
||
}
|
||
}
|
||
if (_shieldLeftWitness != null) {
|
||
final wWord = _resolveLinkedWord(_shieldLeftWitness!.primaryWordId);
|
||
if (wWord != null) {
|
||
words.add(SessionWord.fromWordItem(wWord, positionTitle: 'Левый Свидетель: ${_shieldLeftWitness!.fullName}'));
|
||
}
|
||
}
|
||
for (int i = 0; i < _shieldMothers.length; i++) {
|
||
final mWord = _resolveLinkedWord(_shieldMothers[i].primaryWordId);
|
||
if (mWord != null) {
|
||
words.add(SessionWord.fromWordItem(mWord, positionTitle: 'Мать #${i + 1}: ${_shieldMothers[i].fullName}'));
|
||
}
|
||
}
|
||
} else {
|
||
for (final r in _drawnResults) {
|
||
final word = _resolveLinkedWord(r.figure.primaryWordId);
|
||
if (word != null) {
|
||
words.add(SessionWord.fromWordItem(word, positionTitle: '${r.positionTitle}: ${r.figure.fullName}'));
|
||
}
|
||
}
|
||
}
|
||
|
||
if (words.isEmpty) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(content: Text('Сначала сформируйте расклад геомантии!')),
|
||
);
|
||
return;
|
||
}
|
||
|
||
setState(() => _isAiLoading = true);
|
||
try {
|
||
final q = _questionController.text.trim().isNotEmpty
|
||
? _questionController.text.trim()
|
||
: 'Срез геомантического расклада: ключевые векторы стихий и вердикт Судьи.';
|
||
|
||
final promptText = StringBuffer();
|
||
promptText.writeln("РАСКЛАД АРАБСКОЙ ГЕОМАНТИИ ('ИЛМ АР-РАМЛ):");
|
||
promptText.writeln('Расклад: ${_selectedLayout.title}');
|
||
if (isShield) {
|
||
promptText.writeln('Вердикт Судьи: ${_shieldJudge?.fullName} (${_shieldJudge?.element}, ${_shieldJudge?.planet})');
|
||
promptText.writeln('Правый свидетель (Внешний мир): ${_shieldRightWitness?.fullName}');
|
||
promptText.writeln('Левый свидетель (Внутренний мир): ${_shieldLeftWitness?.fullName}');
|
||
promptText.writeln('Примиритель: ${_shieldReconciler?.fullName}');
|
||
}
|
||
promptText.writeln('\nДай сакральный синтез песка: как стихии взаимодействуют, где конфликт или поддержка, и каков главный совет геомантии.');
|
||
|
||
final interpretation = await _aiService.interpretSession(
|
||
words: words,
|
||
question: '$q\n\n$promptText',
|
||
);
|
||
|
||
if (mounted) {
|
||
setState(() => _isAiLoading = false);
|
||
_showAiInterpretationModal(interpretation);
|
||
}
|
||
} catch (e) {
|
||
if (mounted) {
|
||
setState(() => _isAiLoading = false);
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(content: Text('Ошибка ИИ-толкования: $e')),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
void _showAiInterpretationModal(String text) {
|
||
showModalBottomSheet(
|
||
context: context,
|
||
isScrollControlled: true,
|
||
backgroundColor: const Color(0xFF121018),
|
||
shape: const RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||
),
|
||
builder: (ctx) => DraggableScrollableSheet(
|
||
initialChildSize: 0.8,
|
||
minChildSize: 0.4,
|
||
maxChildSize: 0.95,
|
||
expand: false,
|
||
builder: (_, scroll) => Container(
|
||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 24),
|
||
child: ListView(
|
||
controller: scroll,
|
||
children: [
|
||
Center(
|
||
child: Container(
|
||
width: 44,
|
||
height: 4,
|
||
decoration: BoxDecoration(
|
||
color: Colors.white24,
|
||
borderRadius: BorderRadius.circular(2),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: 14),
|
||
const Row(
|
||
children: [
|
||
Text('⌛', style: TextStyle(fontSize: 22)),
|
||
SizedBox(width: 8),
|
||
Text(
|
||
'Толкование Песка (ИИ-Оракул)',
|
||
style: TextStyle(
|
||
color: Color(0xFFFFD54F),
|
||
fontSize: 17,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 14),
|
||
SelectableText(
|
||
text,
|
||
style: const TextStyle(color: Color(0xFFECEFF1), fontSize: 14, height: 1.5),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildDotsVisual(List<int> lines, {double dotSize = 9, double spacing = 6, Color? color}) {
|
||
final activeColor = color ?? const Color(0xFFFFD54F);
|
||
return Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: lines.map((count) {
|
||
return Padding(
|
||
padding: EdgeInsets.symmetric(vertical: spacing / 2),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: List.generate(count, (idx) {
|
||
return Container(
|
||
margin: EdgeInsets.symmetric(horizontal: count == 2 ? 5 : 0),
|
||
width: dotSize,
|
||
height: dotSize,
|
||
decoration: BoxDecoration(
|
||
shape: BoxShape.circle,
|
||
color: activeColor,
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: activeColor.withValues(alpha: 0.4),
|
||
blurRadius: 4,
|
||
spreadRadius: 1,
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}),
|
||
),
|
||
);
|
||
}).toList(),
|
||
);
|
||
}
|
||
|
||
Widget _buildLayoutSelector() {
|
||
return Container(
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF14121E),
|
||
borderRadius: BorderRadius.circular(16),
|
||
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.2)),
|
||
),
|
||
padding: const EdgeInsets.all(4),
|
||
child: Row(
|
||
children: DivinationLayouts.geomancyLayouts.map((layout) {
|
||
final isSelected = _selectedLayout.id == layout.id;
|
||
return Expanded(
|
||
child: InkWell(
|
||
onTap: () => _onLayoutChanged(layout),
|
||
borderRadius: BorderRadius.circular(12),
|
||
child: AnimatedContainer(
|
||
duration: const Duration(milliseconds: 200),
|
||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
|
||
decoration: BoxDecoration(
|
||
color: isSelected ? const Color(0xFF332616) : Colors.transparent,
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(
|
||
color: isSelected ? const Color(0xFFFFD54F) : Colors.transparent,
|
||
),
|
||
),
|
||
child: Column(
|
||
children: [
|
||
Text(
|
||
layout.title.split('(').first.trim(),
|
||
textAlign: TextAlign.center,
|
||
style: TextStyle(
|
||
color: isSelected ? const Color(0xFFFFD54F) : const Color(0xFF8C93B8),
|
||
fontSize: 12,
|
||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||
),
|
||
),
|
||
Text(
|
||
layout.id == 'geomancy_1'
|
||
? '1 знак'
|
||
: (layout.id == 'geomancy_3' ? '3 знака' : '16 знаков'),
|
||
style: TextStyle(
|
||
color: isSelected ? const Color(0xFFFFE082) : const Color(0xFF61688B),
|
||
fontSize: 10,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}).toList(),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildSandRitualTab() {
|
||
final isShield = _selectedLayout.id == 'geomancy_shield';
|
||
final currentCount = isShield ? _shieldMothers.length : _drawnResults.length;
|
||
final totalCount = isShield ? 4 : _selectedLayout.count;
|
||
final isComplete = currentCount >= totalCount;
|
||
|
||
String stepLabel;
|
||
if (isShield) {
|
||
stepLabel = isComplete
|
||
? 'Все 4 Матери нанесены! Щит рассчитан.'
|
||
: 'Нанесение ${currentCount + 1}-й Матери (${_selectedLayout.positionTitles[currentCount]})';
|
||
} else {
|
||
stepLabel = isComplete
|
||
? 'Расклад сформирован!'
|
||
: 'Фигура ${currentCount + 1} из $totalCount: ${_selectedLayout.positionTitles[currentCount]}';
|
||
}
|
||
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF1E1A29),
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.3)),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
const Text('⏳', style: TextStyle(fontSize: 16)),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: Text(
|
||
stepLabel,
|
||
style: const TextStyle(
|
||
color: Color(0xFFFFD54F),
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
),
|
||
Text(
|
||
'$currentCount/$totalCount',
|
||
style: const TextStyle(color: Color(0xFF8C93B8), fontSize: 11, fontWeight: FontWeight.bold),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: 14),
|
||
|
||
// Interactive Sand Box
|
||
Container(
|
||
height: 180,
|
||
padding: const EdgeInsets.all(16),
|
||
decoration: BoxDecoration(
|
||
gradient: const LinearGradient(
|
||
colors: [Color(0xFF241C16), Color(0xFF15101A)],
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
),
|
||
borderRadius: BorderRadius.circular(20),
|
||
border: Border.all(
|
||
color: _isCasting ? const Color(0xFFFFD54F) : const Color(0xFF8D6E63).withValues(alpha: 0.5),
|
||
width: _isCasting ? 2 : 1,
|
||
),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: const Color(0xFFFFB74D).withValues(alpha: 0.08),
|
||
blurRadius: 16,
|
||
),
|
||
],
|
||
),
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||
children: List.generate(4, (rowIdx) {
|
||
final rowNames = ['Огонь (Голова)', 'Воздух (Шея)', 'Вода (Тело)', 'Земля (Стопы)'];
|
||
final count = _currentSandDotCounts[rowIdx];
|
||
final parity = count == 0 ? null : (count % 2 == 1 ? 1 : 2);
|
||
|
||
return Row(
|
||
children: [
|
||
SizedBox(
|
||
width: 95,
|
||
child: Text(
|
||
rowNames[rowIdx],
|
||
style: const TextStyle(color: Color(0xFF8C93B8), fontSize: 10, fontWeight: FontWeight.bold),
|
||
),
|
||
),
|
||
Expanded(
|
||
child: Container(
|
||
height: 24,
|
||
alignment: Alignment.center,
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF0F0C14),
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(color: Colors.white10),
|
||
),
|
||
child: count == 0
|
||
? const Text('••• песок •••', style: TextStyle(color: Colors.white24, fontSize: 10))
|
||
: Text(
|
||
'${"· " * min(count, 12)}${count > 12 ? "..." : ""} ($count)',
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: const TextStyle(color: Color(0xFFFFD54F), fontSize: 11, letterSpacing: 2),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 10),
|
||
Container(
|
||
width: 44,
|
||
height: 24,
|
||
alignment: Alignment.center,
|
||
decoration: BoxDecoration(
|
||
color: parity == null
|
||
? Colors.transparent
|
||
: (parity == 1 ? const Color(0xFF3E2723) : const Color(0xFF263238)),
|
||
borderRadius: BorderRadius.circular(6),
|
||
border: Border.all(
|
||
color: parity == null
|
||
? Colors.white12
|
||
: (parity == 1 ? const Color(0xFFFFB74D) : const Color(0xFF80DEEA)),
|
||
),
|
||
),
|
||
child: Text(
|
||
parity == null ? '—' : (parity == 1 ? '· (1)' : '·· (2)'),
|
||
style: TextStyle(
|
||
color: parity == 1 ? const Color(0xFFFFD54F) : const Color(0xFF80DEEA),
|
||
fontSize: 11,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}),
|
||
),
|
||
),
|
||
const SizedBox(height: 14),
|
||
|
||
// Casting Action Buttons
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
flex: 3,
|
||
child: ElevatedButton.icon(
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: const Color(0xFFFFD54F),
|
||
foregroundColor: Colors.black,
|
||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||
),
|
||
onPressed: (isComplete || _isCasting) ? null : _performSandRitualCast,
|
||
icon: _isCasting
|
||
? const SizedBox(
|
||
width: 16,
|
||
height: 16,
|
||
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.black),
|
||
)
|
||
: const Icon(Icons.grain_rounded, size: 20),
|
||
label: Text(
|
||
_isCasting
|
||
? 'Песок осыпается...'
|
||
: (isComplete ? 'Расклад заполнен' : 'Нанести точки на песок'),
|
||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
|
||
),
|
||
),
|
||
),
|
||
if (!isComplete) ...[
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
flex: 2,
|
||
child: OutlinedButton(
|
||
style: OutlinedButton.styleFrom(
|
||
foregroundColor: const Color(0xFFFFD54F),
|
||
side: const BorderSide(color: Color(0xFFFFD54F)),
|
||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||
),
|
||
onPressed: _isCasting ? null : _castAllRemainingSteps,
|
||
child: const Text('Все сразу', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold)),
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildManualCatalogTab() {
|
||
final isShield = _selectedLayout.id == 'geomancy_shield';
|
||
final currentCount = isShield ? _shieldMothers.length : _drawnResults.length;
|
||
final totalCount = isShield ? 4 : _selectedLayout.count;
|
||
final isComplete = currentCount >= totalCount;
|
||
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
Text(
|
||
isComplete
|
||
? 'Все позиции заполнены. Нажмите «Сброс» для нового расклада.'
|
||
: 'Выберите фигуру для: ${isShield ? "Матери #${currentCount + 1}" : _selectedLayout.positionTitles[currentCount]}',
|
||
style: const TextStyle(color: Color(0xFF8C93B8), fontSize: 12),
|
||
),
|
||
const SizedBox(height: 10),
|
||
GridView.builder(
|
||
shrinkWrap: true,
|
||
physics: const NeverScrollableScrollPhysics(),
|
||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||
crossAxisCount: 2,
|
||
childAspectRatio: 1.8,
|
||
crossAxisSpacing: 8,
|
||
mainAxisSpacing: 8,
|
||
),
|
||
itemCount: _allFigures.length,
|
||
itemBuilder: (ctx, idx) {
|
||
final fig = _allFigures[idx];
|
||
return InkWell(
|
||
onTap: isComplete ? null : () => _addFigureToSpread(fig),
|
||
borderRadius: BorderRadius.circular(12),
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF14121E),
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.2)),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
_buildDotsVisual(fig.lines, dotSize: 7, spacing: 4),
|
||
const SizedBox(width: 10),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Text(
|
||
fig.russianName,
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: const TextStyle(
|
||
color: Color(0xFFFFD54F),
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
Text(
|
||
fig.latinName,
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: const TextStyle(color: Color(0xFF8C93B8), fontSize: 10),
|
||
),
|
||
const SizedBox(height: 2),
|
||
Text(
|
||
'${fig.element} • ${fig.planet}',
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: const TextStyle(color: Color(0xFF80DEEA), fontSize: 9.5),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildShieldFigureBadge({
|
||
required GeomancyFigure? figure,
|
||
required String label,
|
||
required String role,
|
||
bool isSupreme = false,
|
||
}) {
|
||
final isSelected = figure != null && _inspectedShieldFigure?.signature == figure.signature;
|
||
return InkWell(
|
||
onTap: figure == null
|
||
? null
|
||
: () {
|
||
setState(() {
|
||
_inspectedShieldFigure = figure;
|
||
_inspectedShieldTitle = '$role: ${figure.russianName}';
|
||
});
|
||
HapticFeedback.selectionClick();
|
||
},
|
||
borderRadius: BorderRadius.circular(12),
|
||
child: AnimatedContainer(
|
||
duration: const Duration(milliseconds: 200),
|
||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
|
||
decoration: BoxDecoration(
|
||
color: isSupreme
|
||
? const Color(0xFF332211)
|
||
: (isSelected ? const Color(0xFF2A2038) : const Color(0xFF14121E)),
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(
|
||
color: isSupreme
|
||
? const Color(0xFFFFD54F)
|
||
: (isSelected
|
||
? const Color(0xFF80DEEA)
|
||
: const Color(0xFFFFD54F).withValues(alpha: 0.25)),
|
||
width: isSupreme ? 1.5 : 1.0,
|
||
),
|
||
boxShadow: isSupreme
|
||
? [
|
||
BoxShadow(
|
||
color: const Color(0xFFFFD54F).withValues(alpha: 0.2),
|
||
blurRadius: 8,
|
||
)
|
||
]
|
||
: null,
|
||
),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Text(
|
||
label,
|
||
style: TextStyle(
|
||
color: isSupreme ? const Color(0xFFFFD54F) : const Color(0xFF8C93B8),
|
||
fontSize: 9,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
const SizedBox(height: 4),
|
||
if (figure != null) ...[
|
||
_buildDotsVisual(figure.lines, dotSize: 5, spacing: 3),
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
figure.russianName,
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: TextStyle(
|
||
color: isSupreme ? const Color(0xFFFFD54F) : Colors.white,
|
||
fontSize: 10,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
] else ...[
|
||
const Text('···', style: TextStyle(color: Colors.white24, fontSize: 10)),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildShieldHierarchyView() {
|
||
return Container(
|
||
padding: const EdgeInsets.all(12),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF0F0D16),
|
||
borderRadius: BorderRadius.circular(18),
|
||
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.3)),
|
||
),
|
||
child: Column(
|
||
children: [
|
||
const Text(
|
||
'ВЕЛИКИЙ ЩИТ ГЕОМАНТИИ',
|
||
style: TextStyle(
|
||
color: Color(0xFFFFD54F),
|
||
fontSize: 11,
|
||
fontWeight: FontWeight.w900,
|
||
letterSpacing: 1.2,
|
||
),
|
||
),
|
||
const SizedBox(height: 10),
|
||
|
||
// Row 1: Judge & Reconciler
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Expanded(
|
||
flex: 4,
|
||
child: _buildShieldFigureBadge(
|
||
figure: _shieldJudge,
|
||
label: 'СУДЬЯ',
|
||
role: 'Судья (Вердикт)',
|
||
isSupreme: true,
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
flex: 3,
|
||
child: _buildShieldFigureBadge(
|
||
figure: _shieldReconciler,
|
||
label: 'ПРИМИРИТЕЛЬ',
|
||
role: 'Примиритель (Судья + М1)',
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
|
||
// Row 2: Witnesses (Right & Left)
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: _buildShieldFigureBadge(
|
||
figure: _shieldLeftWitness,
|
||
label: 'ЛЕВЫЙ СВИДЕТЕЛЬ',
|
||
role: 'Левый Свидетель (Внутреннее)',
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: _buildShieldFigureBadge(
|
||
figure: _shieldRightWitness,
|
||
label: 'ПРАВЫЙ СВИДЕТЕЛЬ',
|
||
role: 'Правый Свидетель (Внешнее)',
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
|
||
// Row 3: 4 Nephews
|
||
Row(
|
||
children: List.generate(4, (idx) {
|
||
final n = _shieldNephews.length > (3 - idx) ? _shieldNephews[3 - idx] : null;
|
||
return Expanded(
|
||
child: Padding(
|
||
padding: EdgeInsets.only(right: idx < 3 ? 4 : 0),
|
||
child: _buildShieldFigureBadge(
|
||
figure: n,
|
||
label: 'П${4 - idx}',
|
||
role: 'Племянник #${4 - idx}',
|
||
),
|
||
),
|
||
);
|
||
}),
|
||
),
|
||
const SizedBox(height: 8),
|
||
|
||
// Row 4: 4 Daughters
|
||
Row(
|
||
children: List.generate(4, (idx) {
|
||
final d = _shieldDaughters.length > (3 - idx) ? _shieldDaughters[3 - idx] : null;
|
||
return Expanded(
|
||
child: Padding(
|
||
padding: EdgeInsets.only(right: idx < 3 ? 4 : 0),
|
||
child: _buildShieldFigureBadge(
|
||
figure: d,
|
||
label: 'Д${4 - idx}',
|
||
role: 'Дочь #${4 - idx}',
|
||
),
|
||
),
|
||
);
|
||
}),
|
||
),
|
||
const SizedBox(height: 8),
|
||
|
||
// Row 5: 4 Mothers
|
||
Row(
|
||
children: List.generate(4, (idx) {
|
||
final m = _shieldMothers.length > (3 - idx) ? _shieldMothers[3 - idx] : null;
|
||
return Expanded(
|
||
child: Padding(
|
||
padding: EdgeInsets.only(right: idx < 3 ? 4 : 0),
|
||
child: _buildShieldFigureBadge(
|
||
figure: m,
|
||
label: 'М${4 - idx}',
|
||
role: 'Мать #${4 - idx}',
|
||
),
|
||
),
|
||
);
|
||
}),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildFigureDetailCard({
|
||
required GeomancyFigure figure,
|
||
required String positionTitle,
|
||
}) {
|
||
final linkedWord = _resolveLinkedWord(figure.primaryWordId);
|
||
|
||
return Container(
|
||
margin: const EdgeInsets.only(bottom: 12),
|
||
padding: const EdgeInsets.all(16),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF161320),
|
||
borderRadius: BorderRadius.circular(18),
|
||
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.3)),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF0F0C14),
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.4)),
|
||
),
|
||
child: _buildDotsVisual(figure.lines, dotSize: 8, spacing: 5),
|
||
),
|
||
const SizedBox(width: 14),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
positionTitle,
|
||
style: const TextStyle(
|
||
color: Color(0xFFFFD54F),
|
||
fontSize: 11,
|
||
fontWeight: FontWeight.bold,
|
||
letterSpacing: 0.8,
|
||
),
|
||
),
|
||
const SizedBox(height: 2),
|
||
Text(
|
||
figure.russianName,
|
||
style: const TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 17,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
Text(
|
||
'${figure.latinName} (${figure.arabicName})',
|
||
style: const TextStyle(color: Color(0xFF8C93B8), fontSize: 11.5),
|
||
),
|
||
const SizedBox(height: 6),
|
||
Row(
|
||
children: [
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF263238),
|
||
borderRadius: BorderRadius.circular(6),
|
||
),
|
||
child: Text(
|
||
figure.element,
|
||
style: const TextStyle(color: Color(0xFF80DEEA), fontSize: 10, fontWeight: FontWeight.bold),
|
||
),
|
||
),
|
||
const SizedBox(width: 6),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF33202C),
|
||
borderRadius: BorderRadius.circular(6),
|
||
),
|
||
child: Text(
|
||
figure.planet,
|
||
style: const TextStyle(color: Color(0xFFFF80AB), fontSize: 10, fontWeight: FontWeight.bold),
|
||
),
|
||
),
|
||
const SizedBox(width: 6),
|
||
Text(
|
||
figure.isEven ? 'Чёт (внутр.)' : 'Нечет (вектор)',
|
||
style: const TextStyle(color: Color(0xFF61688B), fontSize: 10),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 10),
|
||
Text(
|
||
figure.lore,
|
||
style: const TextStyle(color: Color(0xFFCFD8DC), fontSize: 12.5, height: 1.4),
|
||
),
|
||
if (linkedWord != null) ...[
|
||
const SizedBox(height: 12),
|
||
InkWell(
|
||
onTap: () {
|
||
Navigator.push(
|
||
context,
|
||
MaterialPageRoute(builder: (_) => SymbolDetailScreen(word: linkedWord)),
|
||
);
|
||
},
|
||
borderRadius: BorderRadius.circular(12),
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF241C16),
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: const Color(0xFFFFB74D).withValues(alpha: 0.4)),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Text(linkedWord.emoji.isNotEmpty ? linkedWord.emoji : '🔗', style: const TextStyle(fontSize: 16)),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'Связанный знак 999: #${linkedWord.formattedSessionNumber} «${linkedWord.word.toUpperCase()}»',
|
||
style: const TextStyle(
|
||
color: Color(0xFFFFD54F),
|
||
fontSize: 11.5,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
if (linkedWord.core.isNotEmpty)
|
||
Text(
|
||
linkedWord.core,
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: const TextStyle(color: Color(0xFF8C93B8), fontSize: 10),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const Icon(Icons.arrow_forward_ios_rounded, size: 12, color: Color(0xFFFFD54F)),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final isShield = _selectedLayout.id == 'geomancy_shield';
|
||
final hasResults = isShield ? _shieldJudge != null : _drawnResults.isNotEmpty;
|
||
|
||
return Scaffold(
|
||
backgroundColor: const Color(0xFF0C0A12),
|
||
appBar: AppBar(
|
||
title: const Row(
|
||
children: [
|
||
Text('⌛', style: TextStyle(fontSize: 20)),
|
||
SizedBox(width: 8),
|
||
Text(
|
||
"Геомантия ('Илм ар-рамл)",
|
||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||
),
|
||
],
|
||
),
|
||
actions: [
|
||
IconButton(
|
||
tooltip: 'Инструкция по Геомантии',
|
||
icon: const Icon(Icons.help_outline_rounded, color: Color(0xFFFFD54F)),
|
||
onPressed: () => DivinationInstructionModal.show(context, 'geomancy'),
|
||
),
|
||
if (hasResults) ...[
|
||
IconButton(
|
||
tooltip: 'Мастер-Промпт для Нейросетей',
|
||
icon: const Icon(Icons.psychology_alt_rounded, color: Color(0xFFFFD54F)),
|
||
onPressed: _showMasterPromptModal,
|
||
),
|
||
IconButton(
|
||
tooltip: 'Сбросить расклад',
|
||
icon: const Icon(Icons.refresh_rounded),
|
||
onPressed: _reset,
|
||
),
|
||
],
|
||
],
|
||
bottom: TabBar(
|
||
controller: _tabController,
|
||
indicatorColor: const Color(0xFFFFD54F),
|
||
tabs: const [
|
||
Tab(icon: Icon(Icons.grain_rounded, size: 18), text: 'Ритуал Песка'),
|
||
Tab(icon: Icon(Icons.grid_view_rounded, size: 18), text: '16 Фигур'),
|
||
],
|
||
),
|
||
),
|
||
body: SingleChildScrollView(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
// Question Field
|
||
TextField(
|
||
controller: _questionController,
|
||
style: const TextStyle(color: Colors.white, fontSize: 14),
|
||
decoration: InputDecoration(
|
||
hintText: 'Вопрос к песку и стихиям...',
|
||
hintStyle: const TextStyle(color: Color(0xFF8C93B8), fontSize: 13),
|
||
filled: true,
|
||
fillColor: const Color(0xFF14121E),
|
||
border: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(14),
|
||
borderSide: const BorderSide(color: Color(0xFF2A2338)),
|
||
),
|
||
prefixIcon: const Icon(Icons.psychology_alt_rounded, color: Color(0xFFFFD54F), size: 20),
|
||
),
|
||
),
|
||
const SizedBox(height: 12),
|
||
|
||
// Layout Selector
|
||
_buildLayoutSelector(),
|
||
const SizedBox(height: 16),
|
||
|
||
// Tab Content (Ritual or Manual)
|
||
AnimatedBuilder(
|
||
animation: _tabController,
|
||
builder: (ctx, _) {
|
||
return _tabController.index == 0 ? _buildSandRitualTab() : _buildManualCatalogTab();
|
||
},
|
||
),
|
||
const SizedBox(height: 20),
|
||
|
||
// Results Section
|
||
if (hasResults) ...[
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
const Text(
|
||
'РЕЗУЛЬТАТ РАСКЛАДА',
|
||
style: TextStyle(
|
||
color: Color(0xFFFFD54F),
|
||
fontSize: 11.5,
|
||
fontWeight: FontWeight.w900,
|
||
letterSpacing: 1.0,
|
||
),
|
||
),
|
||
Row(
|
||
children: [
|
||
IconButton(
|
||
tooltip: 'Скопировать сводку',
|
||
icon: const Icon(Icons.copy_rounded, size: 18, color: Color(0xFF8C93B8)),
|
||
onPressed: _copySpreadSummary,
|
||
),
|
||
ElevatedButton.icon(
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: _savedToSession ? const Color(0xFF1B5E20) : const Color(0xFF2E7D32),
|
||
foregroundColor: Colors.white,
|
||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||
),
|
||
onPressed: _savedToSession ? null : () => _saveToSyncreticSession(silent: false),
|
||
icon: Icon(_savedToSession ? Icons.check_circle_rounded : Icons.bookmark_add_rounded, size: 16),
|
||
label: Text(_savedToSession ? 'В сессии ✓' : 'В сессию', style: const TextStyle(fontSize: 11)),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 10),
|
||
|
||
// Shield Layout or List of Figures
|
||
if (isShield) ...[
|
||
_buildShieldHierarchyView(),
|
||
const SizedBox(height: 12),
|
||
if (_inspectedShieldFigure != null)
|
||
_buildFigureDetailCard(
|
||
figure: _inspectedShieldFigure!,
|
||
positionTitle: _inspectedShieldTitle ?? 'Инспекция Фигуры',
|
||
),
|
||
] else ...[
|
||
..._drawnResults.map((r) => _buildFigureDetailCard(
|
||
figure: r.figure,
|
||
positionTitle: r.positionTitle,
|
||
)),
|
||
],
|
||
const SizedBox(height: 12),
|
||
|
||
// AI Interpretation Button
|
||
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(14)),
|
||
),
|
||
onPressed: _showMasterPromptModal,
|
||
icon: const Icon(Icons.psychology_alt_rounded, size: 18),
|
||
label: const Text(
|
||
'Мастер-Промпт для Нейросетей (Grok / Claude / GPT)',
|
||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
|
||
),
|
||
),
|
||
const SizedBox(height: 10),
|
||
ElevatedButton.icon(
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: const Color(0xFF33202C),
|
||
foregroundColor: const Color(0xFFFF80AB),
|
||
side: const BorderSide(color: Color(0xFFFF80AB)),
|
||
padding: const EdgeInsets.symmetric(vertical: 13),
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||
),
|
||
onPressed: _isAiLoading ? null : _requestAiInterpretation,
|
||
icon: _isAiLoading
|
||
? const SizedBox(
|
||
width: 16,
|
||
height: 16,
|
||
child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFFFF80AB)),
|
||
)
|
||
: const Icon(Icons.auto_awesome_rounded, size: 18),
|
||
label: Text(
|
||
_isAiLoading ? 'ИИ-Оракул читает песок...' : 'ИИ-Толкование Песка',
|
||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|