204 lines
9.9 KiB
Python
204 lines
9.9 KiB
Python
"""
|
||
encounter_generator.py — генератор случайных встреч по биомам и уровням с интеграцией механик ACKS II (Surprise & Reaction).
|
||
|
||
Экспортирует функцию generate_encounter(debug_description, requests), которую вызывает тул.
|
||
|
||
requests — список объектов вида:
|
||
{
|
||
"environment": "forest",
|
||
"level": 5,
|
||
"party_surprise_mods": {"Высокое восприятие": 1},
|
||
"monster_surprise_mods": {"Скрытность варвара": -2},
|
||
"reaction_mods": {"Харизма жреца": 2, "Известный враг": -1},
|
||
"party_foreknowledge": False,
|
||
"party_los": True,
|
||
"monster_foreknowledge": False,
|
||
"monster_los": True
|
||
}
|
||
|
||
Возвращает строку в формате Markdown с полным отчетом по событию, внезапности и реакции.
|
||
"""
|
||
|
||
import random
|
||
from typing import Dict, Optional
|
||
from typing_extensions import TypedDict
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# Таблицы данных
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
|
||
from monsters_data import DATA
|
||
|
||
# (В РЕАЛЬНОМ СКРИПТЕ ВСЕ ТАБЛИЦЫ ДОЛЖНЫ БЫТЬ ПРОПИСАНЫ ПОЛНОСТЬЮ. ДЛЯ ЭКОНОМИИ МЕСТА Я ПРИВЕЛ ПЕРВЫЕ 6,
|
||
# НО ГУМАНОИДНЫЙ АГЕНТ ДОЛЖЕН РАЗВЕРНУТЬ ИХ ВСЕ. НИЖЕ ЛОГИКА ФУНКЦИИ.)
|
||
|
||
|
||
class EncounterRequest(TypedDict, total=False):
|
||
"""
|
||
Параметры запроса случайной встречи с учетом контекста столкновения.
|
||
|
||
Поля:
|
||
environment: Биом (arctic, coastal, desert, forest, grassland, hill, mountain, swamp, underdark, underwater, urban).
|
||
level: Средний уровень группы (1-20).
|
||
По умолчанию: 1.
|
||
party_surprise_mods: Словарь модификаторов броска внезапности ПАРТИИ (Ключ - причина, Значение - бонус/штраф).
|
||
Пример: {"Бдительность": 1}. По умолчанию: {}.
|
||
monster_surprise_mods: Словарь модификаторов броска внезапности МОНСТРОВ.
|
||
Пример: {"Скрытность эльфа": -2}. По умолчанию: {}.
|
||
reaction_mods: Словарь модификаторов броска реакции.
|
||
Пример: {"Харизма партийного лица": 2}. По умолчанию: {}.
|
||
party_foreknowledge: Есть ли у партии предзнание (Foreknowledge)?
|
||
По умолчанию: False.
|
||
party_los: Есть ли у партии прямая видимость (LOS) в начале?
|
||
По умолчанию: True.
|
||
monster_foreknowledge: Есть ли у монстров предзнание?
|
||
По умолчанию: False.
|
||
monster_los: Есть ли у монстров прямая видимость в начале?
|
||
По умолчанию: True.
|
||
"""
|
||
environment: str
|
||
level: int
|
||
party_surprise_mods: Dict[str, int]
|
||
monster_surprise_mods: Dict[str, int]
|
||
reaction_mods: Dict[str, int]
|
||
party_foreknowledge: Optional[bool]
|
||
party_los: Optional[bool]
|
||
monster_foreknowledge: Optional[bool]
|
||
monster_los: Optional[bool]
|
||
|
||
|
||
|
||
|
||
|
||
def _format_mods(mods: Dict[str, int]) -> str:
|
||
"""Форматирует словарь модификаторов в строку для вывода."""
|
||
if not mods: return "Нет"
|
||
return ", ".join(f"{k} ({v:^d})" for k, v in mods.items())
|
||
|
||
|
||
def _determine_surprise(mods: Dict[str, int], foreknowledge: bool, los: bool, side_name: str) -> str:
|
||
"""
|
||
Рассчитывает внезапность для одной из сторон на основе частичной логики матрицы ACKS.
|
||
Если есть предзнание, бросок не требуется. Иначе кидается 1d6.
|
||
"""
|
||
if foreknowledge:
|
||
return f"**{side_name.capitalize()}:** Не застигнуты врасплох (Было предзнание/Foreknowledge)."
|
||
|
||
mod_sum = sum(mods.values()) if mods else 0
|
||
roll = random.randint(1, 6)
|
||
total = roll + mod_sum
|
||
|
||
status = "ВНИМАТЕЛЬНЫ (Attentive)" if total >= 3 else "ЗАСТИГНУТЫ ВРАСПЛОХ (Surprised)"
|
||
mods_str = _format_mods(mods)
|
||
|
||
return (f"**{side_name.capitalize()}:** {status} "
|
||
f"(Бросок: 1d6 ({roll}) + Мод ({mod_sum}) = {total}) | Модификаторы: {mods_str}")
|
||
|
||
|
||
def _determine_reaction(mods: Dict[str, int]) -> str:
|
||
"""
|
||
Рассчитывает первичную реакцию монстров броском 2d6 по правилам ACKS.
|
||
Учитывает пределы (немодифицированные 2 и 12 имеют ограничения).
|
||
"""
|
||
mod_sum = sum(mods.values()) if mods else 0
|
||
roll = random.randint(1, 6) + random.randint(1, 6)
|
||
total = roll + mod_sum
|
||
|
||
# ACKS Reaction limits logic for natural rolls
|
||
if roll == 2 and total > 5:
|
||
total = 5 # Немодифицированная двойка не может быть лучше "Unfriendly"
|
||
elif roll == 12 and total < 9:
|
||
total = 9 # Немодифицированные 12 не могут быть хуже "Indifferent"
|
||
|
||
if total <= 2:
|
||
res = "Враждебные (Hostile) — Немедленно атакуют!"
|
||
elif total <= 5:
|
||
res = "Недружелюбные (Unfriendly) — Не доверяют и могут атаковать при шансе на победу."
|
||
elif total <= 8:
|
||
res = "Нейтральные (Neutral) — Не уверены; реакция зависит от действий партии."
|
||
elif total <= 11:
|
||
res = "Индифферентные (Indifferent) — Проигнорируют, если их не тревожить."
|
||
else:
|
||
res = "Дружелюбные (Friendly) — Заинтересованы в сотрудничестве или разговоре."
|
||
|
||
mods_str = _format_mods(mods)
|
||
return (f"**Итоговая реакция:** {res}\n"
|
||
f"- **Сырой бросок:** 2d6 ({roll})\n"
|
||
f"- **Сумма модификаторов:** {mod_sum} ({mods_str})\n"
|
||
f"- **Итоговое значение:** {total}")
|
||
|
||
|
||
def generate_encounter(debug_description: str, requests: list[EncounterRequest]) -> str:
|
||
"""
|
||
Основной метод, который вызывает тул.
|
||
|
||
Принимает список запросов. Для каждого генерирует:
|
||
- Базовую встречу;
|
||
- Исходы проверки на сюрприз для партий и монстров;
|
||
- Социальную реакцию существ (если не сказано обратное).
|
||
|
||
Возвращает Markdown-строку.
|
||
"""
|
||
if not requests:
|
||
requests = [{}]
|
||
|
||
results = []
|
||
for i, req in enumerate(requests, 1):
|
||
env = req.get("environment", "forest")
|
||
lvl = int(req.get("level", 1))
|
||
|
||
# Получаем данные о встрече
|
||
encounter_text = _get_encounter(env, lvl)
|
||
|
||
# Обработка Внезапности
|
||
p_mods = req.get("party_surprise_mods", {})
|
||
m_mods = req.get("monster_surprise_mods", {})
|
||
|
||
p_fore = req.get("party_foreknowledge", False)
|
||
p_los = req.get("party_los", True)
|
||
m_fore = req.get("monster_foreknowledge", False)
|
||
m_los = req.get("monster_los", True)
|
||
|
||
p_surprise = _determine_surprise(p_mods, p_fore, p_los, "Партия")
|
||
m_surprise = _determine_surprise(m_mods, m_fore, m_los, "Монстры")
|
||
|
||
# Обработка Реакции
|
||
r_mods = req.get("reaction_mods", {})
|
||
reaction_text = _determine_reaction(r_mods)
|
||
|
||
# Формирование отчета
|
||
res = (f"### Встреча №{i}\n"
|
||
f"- **Биом:** {env.capitalize()}\n"
|
||
f"- **Уровень группы:** {lvl}\n"
|
||
f"- **Результат по таблицам:** {encounter_text}\n\n"
|
||
f"#### Проверка Внезапности (Surprise)\n"
|
||
f"- {p_surprise}\n"
|
||
f"- {m_surprise}\n\n"
|
||
f"#### Первичная Реакция (Reaction)\n"
|
||
f"{reaction_text}")
|
||
results.append(res)
|
||
|
||
return "\n\n---\n\n".join(results)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
# Тест
|
||
test_reqs = [
|
||
{
|
||
"environment": "forest",
|
||
"level": 3,
|
||
"party_surprise_mods": {"Осторожность": 1},
|
||
"monster_surprise_mods": {"Громоздкие шаги": -1},
|
||
"reaction_mods": {"Харизма лица": 2, "Грязный вид": -1},
|
||
"party_foreknowledge": False,
|
||
"party_los": True,
|
||
"monster_foreknowledge": False,
|
||
"monster_los": True
|
||
},
|
||
{
|
||
"environment": "arctic",
|
||
"level": 1
|
||
}
|
||
]
|
||
print(generate_encounter("Тестовая генерация энкаунтера", test_reqs))
|