215 lines
7.8 KiB
Python
215 lines
7.8 KiB
Python
"""
|
||
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 get_file, 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 = get_file(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)}") |