""" time_utils.py — утилиты для работы с игровым временем в Obsidian vault. Хранит время в файле Campaign_Time.md в корне хранилища, в frontmatter. Поля: year, month, day, hour. Месяц = 30 дней, год = 360 дней (упрощение для Forgotten Realms). Экспортирует: get_campaign_time() -> dict set_campaign_time(y,m,d,h) -> bool shift_campaign_time(...) -> dict get_week_holidays() -> list[tuple[ int, int, str, str ]] MONTH_NAMES_RU -> dict[int, str] MONTH_NAME_TO_NUM -> dict[str, int] # только русские названия HOLIDAYS -> dict[tuple[int, int], str] """ import sys from pathlib import Path from typing import Optional, List, Tuple # --- КОРРЕКЦИЯ ПУТЕЙ --- scripts_path = Path(__file__).resolve().parent.parent if str(scripts_path) not in sys.path: sys.path.insert(0, str(scripts_path)) from vault_utils import vault_query, write_file, create_file # ------------------------------------------------------------------- # НАЗВАНИЯ МЕСЯЦЕВ (только русские) # ------------------------------------------------------------------- MONTH_NAMES_RU = { 1: "Хаммер", 2: "Алтуриак", 3: "Чес", 4: "Тарсак", 5: "Миртул", 6: "Киторн", 7: "Флеймрул", 8: "Элейнт", 9: "Элесиас", 10: "Марпенот", 11: "Уктар", 12: "Найтол", } # Обратный словарь: русское название → номер # Добавляем варианты: "Укта" (без "р") и "Элизис" (синоним Элейнта) MONTH_NAME_TO_NUM = {} for num, name in MONTH_NAMES_RU.items(): MONTH_NAME_TO_NUM[name.lower()] = num # Дополнительные варианты, принятые в сообществе MONTH_NAME_TO_NUM["укта"] = 11 # вариант без "р" MONTH_NAME_TO_NUM["элизис"] = 8 # альтернативное написание для Элейнта # ------------------------------------------------------------------- # ПРАЗДНИКИ И ОСОБЫЕ ДНИ # ------------------------------------------------------------------- HOLIDAYS = { # Межкалендарные дни (31-й день месяца) (1, 31): "🌨️ Середина зимы (Midwinter) — праздник середины зимы", (4, 31): "🌿 Зеленотравье (Greengrass) — праздник прихода весны", (7, 31): "☀️ Середина лета (Midsummer) — летнее солнцестояние, праздник огней", (8, 31): "🌾 Высокая жатва (Highharvesttide) — праздник урожая", (11, 31): "🌙 Праздник луны (Feast of the Moon) — день поминовения предков", # Астрономические даты (3, 19): "🌸 Весеннее равноденствие", (6, 20): "☀️ Летнее солнцестояние", (9, 21): "🍂 Осеннее равноденствие", (12, 20): "❄️ Зимнее солнцестояние", } SHIELDMET_DESC = "🛡️ Щитовой сход (Shieldmeet) — високосный день, праздник обновления клятв" def is_leap_year(year: int) -> bool: return year % 4 == 0 def get_holiday(month: int, day: int, year: int) -> Optional[str]: key = (month, day) if key in HOLIDAYS: return HOLIDAYS[key] if month == 6 and day == 31 and is_leap_year(year): return SHIELDMET_DESC return None def get_week_holidays(year: int, month: int, day: int) -> List[Tuple[int, int, str, str]]: result = [] for offset in range(7): y, m, d = year, month, day + offset while d > 30: d -= 30 m += 1 if m > 12: m = 1 y += 1 holiday_desc = get_holiday(m, d, y) if holiday_desc: if offset == 0: day_str = "сегодня" elif offset == 1: day_str = "завтра" else: day_str = f"через {offset} дней" result.append((m, d, holiday_desc, day_str)) return result # ------------------------------------------------------------------- # ОСНОВНЫЕ ФУНКЦИИ РАБОТЫ С ВРЕМЕНЕМ # ------------------------------------------------------------------- CAMPAIGN_TIME_PATH = "Campaign_Time.md" DEFAULT_TIME = (1492, 1, 1, 0) def _normalize_time(year: int, month: int, day: int, hour: int) -> tuple[int, int, int, int]: extra_days, hour = divmod(hour, 24) day += extra_days extra_months, day = divmod(day - 1, 30) day += 1 month += extra_months extra_years, month = divmod(month - 1, 12) month += 1 year += extra_years if year < 1: year = 1 return year, month, day, hour def get_campaign_time() -> dict: try: data = vault_query("get_frontmatter", {"path": CAMPAIGN_TIME_PATH}) except RuntimeError: data = None if data and "frontmatter" in data: fm = data["frontmatter"] year = fm.get("year") month = fm.get("month") day = fm.get("day") hour = fm.get("hour") if all(v is not None for v in (year, month, day, hour)): return {"year": year, "month": month, "day": day, "hour": hour} y, m, d, h = DEFAULT_TIME set_campaign_time(y, m, d, h) return {"year": y, "month": m, "day": d, "hour": h} def set_campaign_time(year: int, month: int, day: int, hour: int) -> bool: if not (1 <= month <= 12) or not (1 <= day <= 30) or not (0 <= hour <= 23): return False frontmatter = {"year": year, "month": month, "day": day, "hour": hour} return write_file(CAMPAIGN_TIME_PATH, frontmatter, "") def shift_campaign_time( delta_years: int = 0, delta_months: int = 0, delta_days: int = 0, delta_hours: int = 0 ) -> dict: current = get_campaign_time() y = current["year"] + delta_years m = current["month"] + delta_months d = current["day"] + delta_days h = current["hour"] + delta_hours y, m, d, h = _normalize_time(y, m, d, h) set_campaign_time(y, m, d, h) return {"year": y, "month": m, "day": d, "hour": h} def parse_month(month_input) -> int: """ Преобразует месяц из числа или строки в номер (1-12). Поддерживает: - целые числа 1-12 - строки-числа "1" … "12" - русские названия (регистр не важен): 1: Хаммер 2: Алтуриак 3: Чес 4: Тарсак 5: Миртул 6: Киторн 7: Флеймрул 8: Элейнт 9: Элесиас 10: Марпенот 11: Уктар (допускается "Укта") 12: Найтол """ if isinstance(month_input, int): if 1 <= month_input <= 12: return month_input raise ValueError(f"Номер месяца должен быть от 1 до 12, получено {month_input}") if isinstance(month_input, str): try: num = int(month_input) if 1 <= num <= 12: return num raise ValueError(f"Номер месяца должен быть от 1 до 12, получено {num}") except ValueError: key = month_input.lower().strip() if key in MONTH_NAME_TO_NUM: return MONTH_NAME_TO_NUM[key] raise ValueError(f"Неизвестное название месяца: '{month_input}'") raise TypeError(f"month должен быть int или str, получено {type(month_input)}")