dnd5-scripts/time/time_utils.py
2026-07-08 23:37:24 +03:00

215 lines
7.8 KiB
Python
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.

"""
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)}")