import 'culture_branch.dart'; class SymbolLayer { final String system; final String title; final String law; const SymbolLayer({ required this.system, required this.title, required this.law, }); factory SymbolLayer.fromMap(Map map) { return SymbolLayer( system: map['system'] as String? ?? '', title: map['title'] as String? ?? '', law: map['law'] as String? ?? '', ); } Map toMap() => { 'system': system, 'title': title, 'law': law, }; } class SymbolSource { final String system; final String ref; final double weight; const SymbolSource({ required this.system, required this.ref, required this.weight, }); factory SymbolSource.fromMap(Map map) { return SymbolSource( system: map['system'] as String? ?? '', ref: map['ref'] as String? ?? '', weight: (map['weight'] as num?)?.toDouble() ?? 1.0, ); } Map toMap() => { 'system': system, 'ref': ref, 'weight': weight, }; } class SymbolGrammar { final String operator; final String valence; final String modifies; const SymbolGrammar({ this.operator = 'substance', this.valence = 'liminal', this.modifies = '', }); factory SymbolGrammar.fromMap(Map map) { return SymbolGrammar( operator: map['operator'] as String? ?? 'substance', valence: map['valence'] as String? ?? 'liminal', modifies: map['modifies'] as String? ?? '', ); } Map toMap() => { 'operator': operator, 'valence': valence, 'modifies': modifies, }; } class WordItem { final int id; // Base index 0..999 final int sessionNumber; // Dynamic randomized session number 0..999 final String word; final String partOfSpeech; final double frequency; final String category; final String description; final String emoji; // Enriched Oracle Knowledge Base fields final String family; final String operator; final String valence; final List jurisdictions; final String lookup; // 'A', 'B', 'C' final bool hasCanon; final String core; final String oracle; final List layers; final String shadow; final String asks; final String step; final List binds; final List sources; final SymbolGrammar? grammar; final bool isUserEdited; final Map cultures; final Map languages; const WordItem({ required this.id, this.sessionNumber = 0, required this.word, required this.partOfSpeech, required this.frequency, this.category = '', this.description = '', this.emoji = '', this.family = '', this.operator = 'substance', this.valence = 'liminal', this.jurisdictions = const [], this.lookup = 'C', this.hasCanon = false, this.core = '', this.oracle = '', this.layers = const [], this.shadow = '', this.asks = '', this.step = '', this.binds = const [], this.sources = const [], this.grammar, this.isUserEdited = false, this.cultures = const {}, this.languages = const {}, }); WordItem copyWith({ int? id, int? sessionNumber, String? word, String? partOfSpeech, double? frequency, String? category, String? description, String? emoji, String? family, String? operator, String? valence, List? jurisdictions, String? lookup, bool? hasCanon, String? core, String? oracle, List? layers, String? shadow, String? asks, String? step, List? binds, List? sources, SymbolGrammar? grammar, bool? isUserEdited, Map? cultures, Map? languages, }) { return WordItem( id: id ?? this.id, sessionNumber: sessionNumber ?? this.sessionNumber, word: word ?? this.word, partOfSpeech: partOfSpeech ?? this.partOfSpeech, frequency: frequency ?? this.frequency, category: category ?? this.category, description: description ?? this.description, emoji: emoji ?? this.emoji, family: family ?? this.family, operator: operator ?? this.operator, valence: valence ?? this.valence, jurisdictions: jurisdictions ?? this.jurisdictions, lookup: lookup ?? this.lookup, hasCanon: hasCanon ?? this.hasCanon, core: core ?? this.core, oracle: oracle ?? this.oracle, layers: layers ?? this.layers, shadow: shadow ?? this.shadow, asks: asks ?? this.asks, step: step ?? this.step, binds: binds ?? this.binds, sources: sources ?? this.sources, grammar: grammar ?? this.grammar, isUserEdited: isUserEdited ?? this.isUserEdited, cultures: cultures ?? this.cultures, languages: languages ?? this.languages, ); } /// Enrich existing WordItem with data from oracle_kb.json entry WordItem enrichWithKb(Map kb) { final rawLayers = kb['layers'] as List? ?? []; final parsedLayers = rawLayers .whereType>() .map((m) => SymbolLayer.fromMap(m)) .toList(); final rawSourcesList = kb['sources'] is List ? (kb['sources'] as List) : []; final parsedSources = rawSourcesList .whereType>() .map((m) => SymbolSource.fromMap(m)) .toList(); final rawJurisdictions = kb['jurisdictions'] is List ? (kb['jurisdictions'] as List).map((e) => e.toString()).toList() : []; final rawBinds = kb['binds'] is List ? (kb['binds'] as List).map((e) => e.toString()).toList() : (kb['binds'] is String ? [kb['binds'].toString()] : []); final rawGrammar = kb['grammar'] as Map?; final rawKbCultures = kb['cultures'] as Map?; final parsedKbCultures = rawKbCultures != null ? rawKbCultures.map((k, v) => MapEntry(k.toString(), v.toString())) : {}; final rawKbLanguages = kb['languages'] as Map?; final parsedKbLanguages = rawKbLanguages != null ? rawKbLanguages.map((k, v) => MapEntry(k.toString(), v.toString())) : {}; final kbCore = (kb['core'] as String? ?? '').trim(); final kbOracle = (kb['oracle'] as String? ?? '').trim(); final kbHasCanon = (kb['has_canon'] as bool? ?? false) || (kbCore.isNotEmpty && kbOracle.isNotEmpty); return copyWith( partOfSpeech: partOfSpeech.isNotEmpty ? partOfSpeech : ((kb['pos'] as String?) ?? partOfSpeech), category: (kb['category'] as String?)?.isNotEmpty == true ? kb['category'] as String : category, family: kb['family'] as String? ?? family, operator: kb['operator'] as String? ?? operator, valence: kb['valence'] as String? ?? valence, jurisdictions: rawJurisdictions.isNotEmpty ? rawJurisdictions : jurisdictions, lookup: kb['lookup'] as String? ?? lookup, hasCanon: kbHasCanon, core: kbCore, oracle: kbOracle, layers: parsedLayers, shadow: kb['shadow'] as String? ?? shadow, asks: kb['asks'] as String? ?? asks, step: kb['step'] as String? ?? step, binds: rawBinds, sources: parsedSources, emoji: (kb['emoji'] as String?)?.isNotEmpty == true ? kb['emoji'] as String : emoji, grammar: rawGrammar != null ? SymbolGrammar.fromMap(rawGrammar) : grammar, cultures: parsedKbCultures.isNotEmpty ? parsedKbCultures : cultures, languages: parsedKbLanguages.isNotEmpty ? parsedKbLanguages : languages, ); } /// Returns description stripped of raw metadata bracket tags like [Таро: ...] String get cleanDescription { final clean = description.replaceAll(RegExp(r'\[[^\]]+\]\s*'), '').trim(); return clean.isNotEmpty ? clean : description; } /// Культурные ветви знака: явная редакция (cultures) приоритетна, /// иначе — автоматическое разложение толкования по традициям. Map get resolvedCultures { if (cultures.isNotEmpty) return cultures; return extractCulturalBranches('$oracle\n\n$shadow\n\n$cleanDescription'); } /// Primary poetic/canonical essence: core if available, otherwise clean description String get displayMeaning { if (core.isNotEmpty) return core; if (cleanDescription.isNotEmpty) return cleanDescription; return description; } String get formattedSessionNumber => sessionNumber.toString().padLeft(3, '0'); String get formattedId => formattedSessionNumber; Map toMap() { return { 'id': id, 'sessionNumber': sessionNumber, 'word': word, 'partOfSpeech': partOfSpeech, 'frequency': frequency, 'category': category, 'description': description, 'emoji': emoji, 'family': family, 'operator': operator, 'valence': valence, 'jurisdictions': jurisdictions, 'lookup': lookup, 'hasCanon': hasCanon, 'core': core, 'oracle': oracle, 'layers': layers.map((l) => l.toMap()).toList(), 'shadow': shadow, 'asks': asks, 'step': step, 'binds': binds, 'sources': sources.map((s) => s.toMap()).toList(), 'grammar': grammar?.toMap(), 'cultures': cultures, 'languages': languages, }; } factory WordItem.fromMap(Map map) { final rawLayers = map['layers'] as List? ?? []; final parsedLayers = rawLayers .whereType>() .map((m) => SymbolLayer.fromMap(m)) .toList(); final rawSources = map['sources'] as List? ?? []; final parsedSources = rawSources .whereType>() .map((m) => SymbolSource.fromMap(m)) .toList(); final rawJurisdictions = (map['jurisdictions'] as List? ?? []) .map((e) => e.toString()) .toList(); final rawBinds = (map['binds'] as List? ?? []) .map((e) => e.toString()) .toList(); final rawGrammar = map['grammar'] as Map?; final rawCultures = map['cultures'] as Map?; final parsedCultures = rawCultures != null ? rawCultures.map((k, v) => MapEntry(k.toString(), v.toString())) : {}; final rawLanguages = map['languages'] as Map?; final parsedLanguages = rawLanguages != null ? rawLanguages.map((k, v) => MapEntry(k.toString(), v.toString())) : {}; return WordItem( id: map['id'] as int, sessionNumber: map['sessionNumber'] as int? ?? (map['id'] as int), word: map['word'] as String, partOfSpeech: map['partOfSpeech'] as String? ?? '', frequency: (map['frequency'] as num?)?.toDouble() ?? 50.0, category: map['category'] as String? ?? '', description: map['description'] as String? ?? '', emoji: map['emoji'] as String? ?? '', family: map['family'] as String? ?? '', operator: map['operator'] as String? ?? 'substance', valence: map['valence'] as String? ?? 'liminal', jurisdictions: rawJurisdictions, lookup: map['lookup'] as String? ?? 'C', hasCanon: map['hasCanon'] as bool? ?? false, core: map['core'] as String? ?? '', oracle: map['oracle'] as String? ?? '', layers: parsedLayers, shadow: map['shadow'] as String? ?? '', asks: map['asks'] as String? ?? '', step: map['step'] as String? ?? '', binds: rawBinds, sources: parsedSources, grammar: rawGrammar != null ? SymbolGrammar.fromMap(rawGrammar) : null, isUserEdited: map['isUserEdited'] as bool? ?? false, cultures: parsedCultures, languages: parsedLanguages, ); } @override String toString() => '#: ()'; } extension WordItemLocalization on WordItem { String get localizedValence => localizeValence(valence); String get localizedOperator => localizeOperator(operator); String get localizedFamily => localizeFamily(family); static String localizeValence(String v) { switch (v.trim().toLowerCase()) { case 'active': return 'Активная'; case 'receptive': return 'Восприимчивая'; case 'liminal': return 'Лиминальная'; case 'stable': return 'Устойчивая'; case 'cutting': return 'Отсекающая'; case 'binding': return 'Связующая'; case 'solar': return 'Солярная'; case 'lunar': return 'Лунарная'; case 'light': return 'Светлая'; case 'dark': return 'Теневая'; case 'neutral': return 'Нейтральная'; case 'chthonic': return 'Хтоническая'; default: final s = v.trim(); if (s.isEmpty) return ''; return s[0].toUpperCase() + s.substring(1); } } static String localizeOperator(String op) { switch (op.trim().toLowerCase()) { case 'substance': return 'Субстанция'; case 'act': return 'Действие'; case 'relation': return 'Связь'; case 'shifter': return 'Сдвиг'; case 'qualifier': return 'Качество'; case 'filter': return 'Фильтр'; case 'pronoun': return 'Указатель'; case 'number': return 'Число'; case 'nexus': return 'Узел'; case 'vector': return 'Вектор'; case 'catalyst': return 'Катализатор'; default: final s = op.trim(); if (s.isEmpty) return ''; return s[0].toUpperCase() + s.substring(1); } } static String localizeFamily(String f) { switch (f.trim().toLowerCase()) { case 'totem': return 'Тотем'; case 'sacred_symbol': return 'Сакральный символ'; case 'sacred_place': return 'Сакральное место'; case 'element': return 'Стихия'; case 'cosmos': return 'Космос'; case 'body': return 'Тело'; case 'moral': return 'Этика'; case 'quality': return 'Качество'; case 'time_calendar': return 'Календарь'; case 'time_cycle': return 'Цикл времени'; case 'cult_object': return 'Культ'; case 'person_archetype': return 'Архетип'; case 'social': return 'Социум'; case 'speech': return 'Речь'; case 'circumstance': return 'Обстоятельство'; case 'conjunction': return 'Связка'; case 'preposition': return 'Отношение'; case 'polarity': return 'Полярность'; case 'verb_transform': return 'Трансформация'; case 'particle': return 'Частица'; case 'determiner': return 'Определитель'; case 'adverb_index': return 'Индекс'; case 'misc': return 'Разное'; default: final s = f.trim(); if (s.isEmpty) return ''; return s[0].toUpperCase() + s.substring(1); } } static String localizeSystem(String sys) { switch (sys.trim().toLowerCase()) { case 'tarot': return 'Таро'; case 'runes': return 'Руны'; case 'lenormand': return 'Ленорман'; case 'iching': case 'i ching': return 'И Цзин'; case 'astrology': return 'Астрология'; case 'alchemy': return 'Алхимия'; case 'kabbalah': return 'Каббала'; case 'ogham': return 'Огам'; case 'cosmology': return 'Космология'; case 'slavic': return 'Славянский канон'; case 'bestiary': return 'Бестиарий'; case 'dream': return 'Сновидения'; case 'geomancy': return 'Геомантия'; default: final s = sys.trim(); if (s.isEmpty) return ''; return s[0].toUpperCase() + s.substring(1); } } static String cleanTagDetail(String det) { var s = det.trim(); // Remove Latin transliterations like (Cancer / Вода / Луна) -> (Вода · Луна) s = s.replaceAllMapped(RegExp(r'\([A-Za-z]+\s*[/·]\s*([^)]+)\)'), (m) => '(${m.group(1)})'); s = s.replaceAll(' / ', ' · '); // Geomancy 12 Puer (Юноша) -> 12 Юноша s = s.replaceAllMapped(RegExp(r'(\d+\s+)[A-Za-z\s_]+\(([А-Яа-яЁё\s·-]+)\)'), (m) => '${m.group(1)}${m.group(2)}'); // Alchemy Огонь (Ignis) -> Огонь if (!RegExp(r'[ᚠ-ᛪ]').hasMatch(s)) { s = s.replaceAllMapped(RegExp(r'([А-Яа-яЁё\s]+)\s*\([A-Za-z\s_]+\)'), (m) => m.group(1)!); } // Runes: (Kenaz / ᚲ) -> (ᚲ) s = s.replaceAllMapped(RegExp(r'\([A-Za-z]+\s*[/·]\s*([ᚠ-ᛪ])\)'), (m) => '(${m.group(1)})'); // Clean leftover English in parens s = s.replaceAll(RegExp(r'\([A-Za-z\s_]+\)'), ''); return s.trim(); } static String localizeSourceRef(String system, String ref) { final r = ref.trim().toLowerCase(); const map = { // Runes 'fehu': 'Феху (ᚠ)', 'uruz': 'Уруз (ᚢ)', 'thurisaz': 'Турисаз (ᚦ)', 'ansuz': 'Ансуз (ᚨ)', 'raidho': 'Райдо (ᚱ)', 'kenaz': 'Кеназ (ᚲ)', 'gebo': 'Гебо (ᚷ)', 'wunjo': 'Вуньо (ᚹ)', 'hagalaz': 'Хагалаз (ᚺ)', 'nauthiz': 'Наутиз (ᚾ)', 'isa': 'Иса (ᛁ)', 'jera': 'Йера (ᛃ)', 'eihwaz': 'Эйваз (ᛇ)', 'perthro': 'Перт (ᛈ)', 'algiz': 'Альгиз (ᛉ)', 'sowilo': 'Совило (ᛊ)', 'tiwaz': 'Тейваз (ᛏ)', 'berkano': 'Беркана (ᛒ)', 'ehwaz': 'Эваз (ᛖ)', 'mannaz': 'Манназ (ᛗ)', 'laguz': 'Лагуз (ᛚ)', 'ingwaz': 'Ингваз (ᛜ)', 'dagaz': 'Дагаз (ᛞ)', 'othala': 'Отала (ᛟ)', // Tarot 'wands': 'Жезлы (Огонь)', 'pentacles': 'Пентакли (Земля)', 'cups': 'Кубки (Вода)', 'swords': 'Мечи (Воздух)', 'fool': 'Шут (0)', 'magician': 'Маг (I)', 'priestess': 'Жрица (II)', 'empress': 'Императрица (III)', 'emperor': 'Император (IV)', 'hierophant': 'Иерофант (V)', 'lovers': 'Влюблённые (VI)', 'chariot': 'Колесница (VII)', 'strength': 'Сила (VIII)', 'hermit': 'Отшельник (IX)', 'wheel': 'Колесо Фортуны (X)', 'justice': 'Справедливость (XI)', 'hanged': 'Повешенный (XII)', 'death': 'Смерть (XIII)', 'temperance': 'Умеренность (XIV)', 'devil': 'Дьявол (XV)', 'tower': 'Башня (XVI)', 'star': 'Звезда (XVII)', 'moon': 'Луна', 'sun': 'Солнце', 'judgement': 'Суд (XX)', 'world': 'Мир (XXI)', // I Ching 'li': 'Ли ☲ (Огонь)', 'kan': 'Кань ☵ (Вода)', 'xun': 'Сюнь ☴ (Ветер)', 'kun': 'Кунь ☷ (Земля)', 'qian': 'Цянь ☰ (Небо)', 'zhen': 'Чжэнь ☳ (Гром)', 'gen': 'Гэнь ☶ (Гора)', 'dui': 'Дуй ☱ (Водоём)', 'kan_repeat': 'Кань ䷜ (Повторная опасность)', 'pi': 'Пи ䷋ (Упадок)', 'tai': 'Тай ䷊ (Расцвет)', 'gu': 'Гу ䷑ (Исправление)', 'fu': 'Фу ䷗ (Возврат)', 'meng': 'Мэн ䷃ (Юность)', 'ge': 'Гэ ䷰ (Смена)', 'wei_ji': 'Вэй-цзи ䷿ (Ещё не конец)', // Alchemy 'ignis': 'Огонь', 'aqua': 'Вода', 'aer': 'Воздух', 'terra': 'Земля', 'sulfur': 'Сера (Душа)', 'mercury': 'Ртуть (Дух)', 'sal': 'Соль (Тело)', 'albedo': 'Альбедо (Белизна)', 'nigredo': 'Нигредо (Чернота)', 'rubedo': 'Рубедо (Краснота)', 'ash': 'Пепел', 'luna': 'Луна (Серебро)', 'egg': 'Философское Яйцо', 'vessel': 'Сосуд Гермеса', 'forge': 'Горн', // Astrology 'comet': 'Комета', 'eclipse': 'Затмение', 'dawn': 'Рассвет', 'dusk': 'Закат', 'zenith': 'Зенит', 'nadir': 'Надир', 'constellation': 'Созвездие', 'planet': 'Планета', 'meteor': 'Метеор', 'equinox': 'Равноденствие', 'orbit': 'Орбита', 'libra': 'Весы', 'aries': 'Овен', 'taurus': 'Телец', 'gemini': 'Близнецы', 'cancer': 'Рак', 'leo': 'Лев', 'virgo': 'Дева', 'scorpio': 'Скорпион', 'sagittarius': 'Стрелец', 'capricorn': 'Козерог', 'aquarius': 'Водолей', 'pisces': 'Рыбы', // Bestiary 'dog': 'Собака (Страж)', 'snake': 'Змея (Мудрость)', 'eagle': 'Орёл (Высота)', 'wolf': 'Волк (Чутьё)', 'raven': 'Ворон (Память)', 'lion': 'Лев (Царственность)', 'owl': 'Сова (Ночное зрение)', 'serpent_feather': 'Пернатый Змей', 'dragon': 'Дракон', 'phoenix': 'Феникс', 'bear': 'Медведь (Сила)', 'fox': 'Лиса (Хитрость)', 'stag': 'Олень (Благородство)', 'horse': 'Конь (Порыв)', 'spider': 'Паук (Нить судьбы)', // Virtues & Archetypes 'courage': 'Мужество', 'charity': 'Милосердие', 'karma': 'Карма / Воздаяние', 'guilt': 'Вина', 'shame': 'Стыд', 'pride': 'Гордыня', 'humility': 'Смирение', 'envy': 'Зависть', 'vow': 'Обет', 'greed': 'Алчность', 'gluttony': 'Чревоугодие', 'sloth': 'Уныние', 'wrath': 'Гнев', 'lust': 'Страсть', 'donor': 'Даритель', 'dispatcher': 'Отправитель', 'shadow': 'Тень', 'hero': 'Герой', 'self': 'Самость', 'helper': 'Помощник', 'witch': 'Ведунья', 'princess': 'Царевна', 'exile': 'Изгнанник', 'false_hero': 'Ложный герой', 'judge': 'Судия', 'trickster': 'Трикстер', // Slavic 'mesyats': 'Месяц ясный', 'oak': 'Священный Дуб', 'willow': 'Плакучая Ива', 'birch': 'Берёза', 'ivy_field': 'Чисто поле', 'crossroads': 'Перепутье', 'threshold': 'Порог', 'well': 'Студёный колодец', 'hearth': 'Родной очаг', // Ogham 'dair': 'Дуб (Даир / ᚇ)', 'sail': 'Ива (Салье / ᚄ)', 'beith': 'Берёза (Бэйт / ᚁ)', 'gort': 'Плющ (Горт / ᚌ)', // Symbols & Dreams 'fire': 'Огонь', 'water': 'Вода', 'air': 'Воздух', 'earth': 'Земля', 'key': 'Ключ от врат', 'spark': 'Живая искра', 'path': 'Тропа / Путь', 'book': 'Книга судеб', 'blood': 'Кровь / Род', 'heart': 'Сердце', 'fire_d': 'Сон об огне', 'water_d': 'Сон о глубине', 'death_d': 'Сон о переходе', 'key_d': 'Сон о тайне', 'path_d': 'Сон о странствии', 'child_d': 'Сон о дитяти', 'mirror_d': 'Сон о зеркале', 'wolf_d': 'Сон о звере', 'dream_fire': 'Сон об огне', }; if (map.containsKey(r)) return map[r]!; final s = ref.trim().replaceAll('_', ' '); if (s.isEmpty) return ''; return s[0].toUpperCase() + s.substring(1); } }