584 lines
34 KiB
Python
584 lines
34 KiB
Python
"""
|
||
generate_trap.py — генератор случайных ловушек по параметрам окружения, уровня и типа.
|
||
|
||
Интегрирует:
|
||
- Библиотеку ловушек из DMG, XGE, VRGR и пользовательских примеров.
|
||
- Автоматический расчёт урона, Сл спасброска и бонуса атаки по таблицам угроз (moderate/dangerous/deadly).
|
||
- Поддержку простых (simple) и сложных (complex) ловушек.
|
||
- Особые условия избегания (пароль, предмет, тип существа).
|
||
|
||
Экспортирует функцию generate_trap(debug_description, requests).
|
||
|
||
requests — список объектов вида:
|
||
{
|
||
"environment": "dungeon", # локация
|
||
"level": 5, # уровень группы
|
||
"trap_type": "mechanical", # mechanical/magical/haunted/living
|
||
"complexity": "simple", # simple/complex
|
||
"severity": "dangerous", # moderate/dangerous/deadly
|
||
"trigger_type": "pressure_plate",
|
||
"trigger_location": "floor",
|
||
"effect_type": "damage",
|
||
"damage_type": "fire",
|
||
"area_shape": "cone",
|
||
"duration": "instant",
|
||
"detection_skill": "perception",
|
||
"disarm_skill": "thieves_tools",
|
||
"requires_tools": True,
|
||
"reset": "once",
|
||
"avoidable_by": ["password"]
|
||
}
|
||
|
||
Возвращает строку в формате Markdown.
|
||
"""
|
||
|
||
import sys
|
||
import os
|
||
from pathlib import Path
|
||
import random
|
||
from typing import Dict, List, Optional, Any
|
||
from typing_extensions import TypedDict
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# TypedDict для параметров запроса
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
|
||
class TrapRequest(TypedDict, total=False):
|
||
"""
|
||
Параметры одного запроса на генерацию ловушки.
|
||
|
||
Поля:
|
||
environment: Тип локации.
|
||
Допустимы: dungeon, forest, urban, tomb, cave,
|
||
arctic, swamp, ruins, castle, sewer, laboratory.
|
||
По умолчанию: dungeon.
|
||
|
||
level: Средний уровень группы (1–20).
|
||
По умолчанию: 1.
|
||
|
||
trap_type: Тип ловушки по природе.
|
||
Допустимы: mechanical (механическая), magical (магическая),
|
||
haunted (призрачная), living (живая).
|
||
По умолчанию: случайно.
|
||
|
||
complexity: Сложность поведения.
|
||
Допустимы: simple (простая), complex (сложная).
|
||
По умолчанию: simple (с весами).
|
||
|
||
severity: Серьёзность угрозы.
|
||
Допустимы: moderate (средняя), dangerous (опасная), deadly (смертельная).
|
||
По умолчанию: moderate (с весами).
|
||
|
||
trigger_type: Как активируется ловушка.
|
||
Допустимы: pressure_plate, trip_wire, door_open, lock_pick,
|
||
touch, time, magical_ward, manual, force, enter_area.
|
||
По умолчанию: случайно.
|
||
|
||
trigger_location: Где находится триггер.
|
||
Допустимы: floor, wall, ceiling, door, chest, object, corridor.
|
||
По умолчанию: floor.
|
||
|
||
effect_type: Основной эффект ловушки.
|
||
Допустимы: damage, condition, restrain, imprison, teleport,
|
||
summon, alarm, delay, environment, curse.
|
||
По умолчанию: damage (с весами).
|
||
|
||
damage_type: Тип урона (если effect_type=damage).
|
||
Допустимы: poison, fire, cold, acid, lightning,
|
||
bludgeoning, piercing, slashing, psychic, necrotic,
|
||
radiant, thunder.
|
||
По умолчанию: выбирается случайно.
|
||
|
||
area_shape: Форма области поражения.
|
||
Допустимы: single_target, cone, cube, line, sphere, room, pit.
|
||
По умолчанию: single_target.
|
||
|
||
duration: Продолжительность эффекта.
|
||
Допустимы: instant, persistent, repeating, until_escape.
|
||
По умолчанию: instant.
|
||
|
||
detection_skill: Навык для обнаружения ловушки.
|
||
Допустимы: perception, investigation, arcana, none.
|
||
По умолчанию: perception.
|
||
|
||
disarm_skill: Навык/способ обезвреживания.
|
||
Допустимы: thieves_tools, athletics, arcana, religion, simple_action, none.
|
||
По умолчанию: thieves_tools.
|
||
|
||
requires_tools: Требуются ли воровские инструменты для обезвреживания.
|
||
По умолчанию: True.
|
||
|
||
reset: Перезарядка ловушки.
|
||
Допустимы: once (одноразовая), automatic (автоматическая), manual (ручная).
|
||
По умолчанию: once.
|
||
|
||
avoidable_by: Особые условия избегания.
|
||
Допустимы: password, specific_item, specific_creature_type.
|
||
По умолчанию: [].
|
||
"""
|
||
environment: str
|
||
level: int
|
||
trap_type: str
|
||
complexity: str
|
||
severity: str
|
||
trigger_type: str
|
||
trigger_location: str
|
||
effect_type: str
|
||
damage_type: str
|
||
area_shape: str
|
||
duration: str
|
||
detection_skill: str
|
||
disarm_skill: str
|
||
requires_tools: bool
|
||
reset: str
|
||
avoidable_by: List[str]
|
||
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# Библиотека готовых ловушек (упрощённая структура для примера)
|
||
# В реальном проекте можно расширить до полноценной базы.
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
|
||
# Шаблоны описаний и механик для генерации на основе параметров
|
||
TRAP_TEMPLATES = {
|
||
"mechanical": {
|
||
"damage": "Механическая ловушка наносит {damage} урона {damage_type} в области {area_shape}.",
|
||
"condition": "Механическая ловушка накладывает состояние {condition}.",
|
||
"restrain": "Механическая ловушка захватывает жертву, требуя проверки Силы для освобождения.",
|
||
"imprison": "Механическая ловушка запирает жертву в {area_shape}.",
|
||
"teleport": "Механическая ловушка телепортирует жертву в другую зону.",
|
||
"summon": "Механическая ловушка призывает {summon}.",
|
||
"alarm": "Механическая ловушка издаёт громкий звук, привлекая внимание.",
|
||
"delay": "Механическая ловушка создаёт препятствие, блокируя проход.",
|
||
"environment": "Механическая ловушка изменяет физические свойства среды.",
|
||
"curse": "Механическая ловушка накладывает проклятие."
|
||
},
|
||
"magical": {
|
||
"damage": "Магическая ловушка поражает {area_shape} заклинанием, наносящим {damage} урона {damage_type}.",
|
||
"condition": "Магическая ловушка накладывает магическое состояние {condition}.",
|
||
"restrain": "Магическая ловушка удерживает цель магическими цепями.",
|
||
"imprison": "Магическая ловушка заточает жертву в магическую темницу.",
|
||
"teleport": "Магическая ловушка телепортирует жертву в заранее определённое место.",
|
||
"summon": "Магическая ловушка призывает {summon} с помощью заклинания.",
|
||
"alarm": "Магическая ловушка посылает магический сигнал тревоги.",
|
||
"delay": "Магическая ловушка создаёт барьер из чистой магии.",
|
||
"environment": "Магическая ловушка меняет законы физики в области.",
|
||
"curse": "Магическая ловушка накладывает магическое проклятие."
|
||
},
|
||
"haunted": {
|
||
"damage": "Призрачная ловушка наносит {damage} психического или некротического урона.",
|
||
"condition": "Призрачная ловушка вызывает страх, очарование или паралич.",
|
||
"restrain": "Призрачная ловушка парализует жертву от ужаса.",
|
||
"imprison": "Призрачная ловушка запирает душу в предмете.",
|
||
"teleport": "Призрачная ловушка отправляет жертву в иное измерение.",
|
||
"summon": "Призрачная ловушка материализует призрачного музыканта или воина.",
|
||
"alarm": "Призрачная ловушка издаёт жуткие звуки, предупреждающие обитателей.",
|
||
"delay": "Призрачная ловушка заставляет жертву танцевать, теряя время.",
|
||
"environment": "Призрачная ловушка создаёт иллюзию прошлого, изменяя восприятие.",
|
||
"curse": "Призрачная ловушка накладывает проклятие, снимаемое только Remove Curse."
|
||
},
|
||
"living": {
|
||
"damage": "Живая ловушка атакует, нанося {damage} урона {damage_type}.",
|
||
"condition": "Живая ловушка отравляет, парализует или опутывает жертву.",
|
||
"restrain": "Живая ловушка захватывает жертву щупальцами или клешнями.",
|
||
"imprison": "Живая ловушка заглатывает жертву.",
|
||
"teleport": "Живая ловушка переносит жертву в своё логово.",
|
||
"summon": "Живая ловушка призывает других существ.",
|
||
"alarm": "Живая ловушка издаёт крик, зовущий сородичей.",
|
||
"delay": "Живая ловушка опутывает жертву, замедляя её.",
|
||
"environment": "Живая ловушка выделяет слизь, делающую пол скользким.",
|
||
"curse": "Живая ловушка накладывает болезнь или проклятие через укус."
|
||
}
|
||
}
|
||
|
||
# Соответствие типов урона спасброскам
|
||
DAMAGE_SAVE_MAP = {
|
||
"poison": "Телосложение",
|
||
"fire": "Ловкость",
|
||
"cold": "Телосложение",
|
||
"acid": "Ловкость",
|
||
"lightning": "Ловкость",
|
||
"bludgeoning": "Ловкость",
|
||
"piercing": "Ловкость",
|
||
"slashing": "Ловкость",
|
||
"psychic": "Интеллект",
|
||
"necrotic": "Мудрость",
|
||
"radiant": "Мудрость",
|
||
"thunder": "Телосложение"
|
||
}
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# Вспомогательные функции — случайный выбор, веса, расчёт чисел
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
|
||
def _weighted_pick(weights: Dict[str, int]) -> str:
|
||
"""Выбирает ключ из словаря {name: weight} взвешенным случайным образом."""
|
||
total = sum(weights.values())
|
||
r = random.uniform(0, total)
|
||
cumulative = 0
|
||
for name, weight in weights.items():
|
||
cumulative += weight
|
||
if r <= cumulative:
|
||
return name
|
||
return list(weights.keys())[-1]
|
||
|
||
def _random_choice_or_default(value: Optional[str], options: List[str], default: str) -> str:
|
||
"""Если value задано и входит в options, возвращает его. Иначе случайный выбор из options."""
|
||
if value and value in options:
|
||
return value
|
||
return random.choice(options) if options else default
|
||
|
||
def _calculate_damage_and_dc(level: int, severity: str, complexity: str) -> Dict[str, Any]:
|
||
"""Рассчитывает урон, Сл спасброска, бонус атаки по таблицам DMG/XGE."""
|
||
# Таблицы из официальных правил (средняя/опасная/смертельная)
|
||
# Урон в d10 (для одиночной цели). Для площади можно уменьшать количество кубов.
|
||
damage_table = {
|
||
"moderate": {
|
||
1: (1, 10), # 1d10
|
||
5: (2, 10), # 2d10
|
||
11: (4, 10), # 4d10
|
||
17: (10, 10) # 10d10
|
||
},
|
||
"dangerous": {
|
||
1: (2, 10), # 2d10
|
||
5: (4, 10), # 4d10
|
||
11: (10, 10), # 10d10
|
||
17: (18, 10) # 18d10
|
||
},
|
||
"deadly": {
|
||
1: (4, 10), # 4d10
|
||
5: (10, 10), # 10d10
|
||
11: (18, 10), # 18d10
|
||
17: (24, 10) # 24d10
|
||
}
|
||
}
|
||
|
||
# Определяем порог уровня
|
||
if level <= 4:
|
||
tier = 1
|
||
elif level <= 10:
|
||
tier = 5
|
||
elif level <= 16:
|
||
tier = 11
|
||
else:
|
||
tier = 17
|
||
|
||
num_dice, die_size = damage_table[severity][tier]
|
||
# Для сложных ловушек урон может быть немного ниже (повторяемость)
|
||
if complexity == "complex":
|
||
num_dice = max(1, num_dice - 1)
|
||
|
||
damage_dice = f"{num_dice}d{die_size}"
|
||
# Средний урон для текста
|
||
avg_damage = num_dice * (die_size + 1) // 2
|
||
|
||
# Сл спасброска и бонус атаки
|
||
dc_save_table = {
|
||
"moderate": 10,
|
||
"dangerous": 15,
|
||
"deadly": 20
|
||
}
|
||
base_dc = dc_save_table[severity]
|
||
# Для сложных ловушек Сл может быть чуть ниже (чтобы не убить за раз)
|
||
if complexity == "complex":
|
||
base_dc = max(10, base_dc - 2)
|
||
|
||
attack_bonus_table = {
|
||
"moderate": 5,
|
||
"dangerous": 8,
|
||
"deadly": 12
|
||
}
|
||
base_attack = attack_bonus_table[severity]
|
||
if complexity == "complex":
|
||
base_attack = max(3, base_attack - 2)
|
||
|
||
return {
|
||
"damage_dice": damage_dice,
|
||
"avg_damage": avg_damage,
|
||
"save_dc": base_dc,
|
||
"attack_bonus": base_attack
|
||
}
|
||
|
||
def _pick_condition(effect_type: str, damage_type: str = None) -> str:
|
||
"""Выбирает состояние для effect_type='condition' на основе damage_type или случайно."""
|
||
if effect_type != "condition":
|
||
return ""
|
||
condition_map = {
|
||
"poison": "отравлен",
|
||
"fire": "горит",
|
||
"cold": "замедлен",
|
||
"lightning": "оглушён",
|
||
"psychic": "ошеломлён",
|
||
"necrotic": "истощён",
|
||
"radiant": "ослеплён",
|
||
"thunder": "оглушён"
|
||
}
|
||
if damage_type and damage_type in condition_map:
|
||
return condition_map[damage_type]
|
||
# Случайные состояния
|
||
conditions = ["отравлен", "ослеплён", "оглушён", "ошеломлён", "напуган", "парализован"]
|
||
return random.choice(conditions)
|
||
|
||
def _generate_description(req: TrapRequest, calc: Dict[str, Any]) -> str:
|
||
"""Генерирует текст описания ловушки на основе параметров и рассчитанных чисел."""
|
||
trap_type = req.get("trap_type", "mechanical")
|
||
effect_type = req.get("effect_type", "damage")
|
||
damage_type = req.get("damage_type", "bludgeoning")
|
||
area_shape = req.get("area_shape", "single_target")
|
||
trigger_type = req.get("trigger_type", "pressure_plate")
|
||
trigger_location = req.get("trigger_location", "floor")
|
||
detection_skill = req.get("detection_skill", "perception")
|
||
disarm_skill = req.get("disarm_skill", "thieves_tools")
|
||
requires_tools = req.get("requires_tools", True)
|
||
reset = req.get("reset", "once")
|
||
avoidable = req.get("avoidable_by", [])
|
||
complexity = req.get("complexity", "simple")
|
||
duration = req.get("duration", "instant")
|
||
|
||
# Базовый шаблон из библиотеки
|
||
template = TRAP_TEMPLATES.get(trap_type, TRAP_TEMPLATES["mechanical"]).get(effect_type, "Ловушка срабатывает.")
|
||
|
||
# Подставляем значения
|
||
damage_str = f"{calc['damage_dice']} ({calc['avg_damage']})"
|
||
desc = template.format(
|
||
damage=damage_str,
|
||
damage_type=damage_type,
|
||
area_shape=area_shape,
|
||
condition=_pick_condition(effect_type, damage_type),
|
||
summon="монстра" # упрощённо
|
||
)
|
||
|
||
# Добавляем триггер
|
||
trigger_text = {
|
||
"pressure_plate": f"на нажимную плиту на {trigger_location} оказывается вес более 20 фунтов",
|
||
"trip_wire": f"растяжка на {trigger_location} задевается существом",
|
||
"door_open": f"открывается дверь/сундук на {trigger_location}",
|
||
"lock_pick": f"кто-то пытается взломать замок на {trigger_location} неправильным ключом",
|
||
"touch": f"кто-то прикасается к предмету на {trigger_location}",
|
||
"time": f"проходит определённое время",
|
||
"magical_ward": f"магическая стража на {trigger_location} обнаруживает нарушителя",
|
||
"manual": f"кто-то дёргает за рычаг на {trigger_location}",
|
||
"force": f"к {trigger_location} прилагается физическая сила (удар или выбивание)",
|
||
"enter_area": f"существо входит в область на {trigger_location}"
|
||
}.get(trigger_type, f"происходит {trigger_type}")
|
||
|
||
full_desc = f"**Триггер:** Ловушка срабатывает, когда {trigger_text}.\n**Эффект:** {desc}"
|
||
|
||
# Добавляем информацию о спасброске и уроне, если effect_type damage
|
||
if effect_type == "damage":
|
||
save_ability = DAMAGE_SAVE_MAP.get(damage_type, "Ловкость")
|
||
full_desc += f" Каждое существо в области должно преуспеть в спасброске {save_ability} (Сл {calc['save_dc']}), иначе получит {damage_str} урона {damage_type} (или половину при успехе)."
|
||
if damage_type in ["fire", "acid"]:
|
||
full_desc += " Легковоспламеняющиеся предметы загораются."
|
||
elif effect_type == "condition":
|
||
cond = _pick_condition(effect_type, damage_type)
|
||
full_desc += f" Цель должна преуспеть в спасброске Мудрости (Сл {calc['save_dc']}), иначе получит состояние '{cond}' на 1 минуту."
|
||
elif effect_type in ["restrain", "imprison"]:
|
||
full_desc += f" Цель должна преуспеть в спасброске Ловкости (Сл {calc['save_dc']}), иначе становится опутанной/запертой. Для освобождения требуется проверка Силы (Сл 15)."
|
||
|
||
# Обнаружение и обезвреживание
|
||
detect_desc = ""
|
||
if detection_skill == "perception":
|
||
detect_desc = f"Успешная проверка Мудрости (Внимательность) Сл {calc['save_dc']} позволяет заметить признаки ловушки."
|
||
elif detection_skill == "investigation":
|
||
detect_desc = f"Успешная проверка Интеллекта (Расследование) Сл {calc['save_dc']} раскрывает механизм ловушки."
|
||
elif detection_skill == "arcana":
|
||
detect_desc = f"Успешная проверка Интеллекта (Магия) Сл {calc['save_dc']} обнаруживает магическую ауру."
|
||
else:
|
||
detect_desc = "Ловушка почти не оставляет следов; обнаружить её можно только случайно или по подсказкам."
|
||
|
||
disarm_desc = ""
|
||
if disarm_skill == "thieves_tools":
|
||
tool_req = " (требуются воровские инструменты)" if requires_tools else ""
|
||
disarm_desc = f"Успешная проверка Ловкости с использованием воровских инструментов{'' if requires_tools else ' (можно без инструментов со помехой)'} Сл {calc['save_dc']} обезвреживает ловушку."
|
||
elif disarm_skill == "athletics":
|
||
disarm_desc = f"Успешная проверка Силы (Атлетика) Сл {calc['save_dc']} позволяет сломать или заблокировать механизм."
|
||
elif disarm_skill == "arcana":
|
||
disarm_desc = f"Успешная проверка Интеллекта (Магия) Сл {calc['save_dc']} или заклинание `dispel magic` (Сл {calc['save_dc']}) отключает магическую ловушку."
|
||
elif disarm_skill == "religion":
|
||
disarm_desc = f"Успешная проверка Интеллекта (Религия) Сл {calc['save_dc']} позволяет стереть священную руну."
|
||
elif disarm_skill == "simple_action":
|
||
disarm_desc = "Достаточно простого действия: перешагнуть, забить клин или перерезать верёвку."
|
||
else:
|
||
disarm_desc = "Ловушку невозможно обезвредить; её можно только избежать."
|
||
|
||
reset_desc = {
|
||
"once": "Ловушка одноразовая; после срабатывания не представляет угрозы.",
|
||
"automatic": f"Через {random.randint(1, 10)} минут ловушка перезаряжается автоматически.",
|
||
"manual": "Ловушку можно взвести заново, потратив 10 минут и сделав проверку."
|
||
}.get(reset, "")
|
||
|
||
avoid_desc = ""
|
||
if avoidable:
|
||
avoid_list = []
|
||
if "password" in avoidable:
|
||
avoid_list.append("знать пароль")
|
||
if "specific_item" in avoidable:
|
||
avoid_list.append("иметь особый предмет (ключ, амулет, святой символ)")
|
||
if "specific_creature_type" in avoidable:
|
||
avoid_list.append("принадлежать к определённому типу существ (например, нежить)")
|
||
avoid_desc = f"\n**Избегание:** Ловушку можно полностью избежать, если {', '.join(avoid_list)}."
|
||
|
||
complexity_desc = ""
|
||
if complexity == "complex":
|
||
init_bonus = random.randint(5, 12)
|
||
complexity_desc = f"\n**Сложная ловушка:** Действует на инициативе {random.choice([10, 20])} (бонус {init_bonus:+d}). В свой ход повторяет эффект. Требуется 3 успешные проверки для отключения."
|
||
|
||
full_desc += f"\n\n**Обнаружение:** {detect_desc}\n**Обезвреживание:** {disarm_desc}\n**Перезарядка:** {reset_desc}{avoid_desc}{complexity_desc}"
|
||
return full_desc
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# Публичная функция
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
|
||
def generate_trap(debug_description: str, requests: List[TrapRequest]) -> str:
|
||
"""
|
||
Основной метод, который вызывает тул с фронта.
|
||
|
||
Принимает список запросов на генерацию ловушек. Для каждого запроса:
|
||
1. Определяет все параметры (с подстановкой значений по умолчанию).
|
||
2. Рассчитывает урон/Сл/бонус атаки на основе уровня и серьёзности.
|
||
3. Генерирует описание, триггер, эффект, способы обнаружения и обезвреживания.
|
||
4. Формирует Markdown-секцию.
|
||
|
||
Возвращает Markdown-строку со всеми результатами.
|
||
"""
|
||
if not requests:
|
||
requests = [{}]
|
||
|
||
results = []
|
||
for i, req in enumerate(requests, 1):
|
||
# Подстановка значений по умолчанию
|
||
env = req.get("environment", "dungeon").lower()
|
||
level = max(1, min(20, int(req.get("level", 1))))
|
||
trap_type = req.get("trap_type")
|
||
complexity = req.get("complexity", "simple")
|
||
severity = req.get("severity", "moderate")
|
||
|
||
# Случайный выбор, если не указано или значение недопустимо
|
||
valid_trap_types = ["mechanical", "magical", "haunted", "living"]
|
||
trap_type = _random_choice_or_default(trap_type, valid_trap_types, "mechanical")
|
||
|
||
valid_complexities = ["simple", "complex"]
|
||
complexity = _random_choice_or_default(complexity, valid_complexities, "simple")
|
||
|
||
valid_severities = ["moderate", "dangerous", "deadly"]
|
||
severity = _random_choice_or_default(severity, valid_severities, "moderate")
|
||
|
||
valid_trigger_types = ["pressure_plate", "trip_wire", "door_open", "lock_pick", "touch", "time", "magical_ward", "manual", "force", "enter_area"]
|
||
trigger_type = _random_choice_or_default(req.get("trigger_type"), valid_trigger_types, "pressure_plate")
|
||
|
||
valid_trigger_locations = ["floor", "wall", "ceiling", "door", "chest", "object", "corridor"]
|
||
trigger_location = _random_choice_or_default(req.get("trigger_location"), valid_trigger_locations, "floor")
|
||
|
||
valid_effect_types = ["damage", "condition", "restrain", "imprison", "teleport", "summon", "alarm", "delay", "environment", "curse"]
|
||
effect_type = _random_choice_or_default(req.get("effect_type"), valid_effect_types, "damage")
|
||
|
||
valid_damage_types = ["poison", "fire", "cold", "acid", "lightning", "bludgeoning", "piercing", "slashing", "psychic", "necrotic", "radiant", "thunder"]
|
||
damage_type = req.get("damage_type")
|
||
if effect_type == "damage" and (not damage_type or damage_type not in valid_damage_types):
|
||
damage_type = _random_choice_or_default(None, valid_damage_types, "bludgeoning")
|
||
elif effect_type != "damage":
|
||
damage_type = None
|
||
|
||
valid_area_shapes = ["single_target", "cone", "cube", "line", "sphere", "room", "pit"]
|
||
area_shape = _random_choice_or_default(req.get("area_shape"), valid_area_shapes, "single_target")
|
||
|
||
valid_durations = ["instant", "persistent", "repeating", "until_escape"]
|
||
duration = _random_choice_or_default(req.get("duration"), valid_durations, "instant")
|
||
|
||
valid_detection = ["perception", "investigation", "arcana", "none"]
|
||
detection_skill = _random_choice_or_default(req.get("detection_skill"), valid_detection, "perception")
|
||
|
||
valid_disarm = ["thieves_tools", "athletics", "arcana", "religion", "simple_action", "none"]
|
||
disarm_skill = _random_choice_or_default(req.get("disarm_skill"), valid_disarm, "thieves_tools")
|
||
|
||
requires_tools = req.get("requires_tools", True)
|
||
if not isinstance(requires_tools, bool):
|
||
requires_tools = True
|
||
|
||
valid_reset = ["once", "automatic", "manual"]
|
||
reset = _random_choice_or_default(req.get("reset"), valid_reset, "once")
|
||
|
||
avoidable_by = req.get("avoidable_by", [])
|
||
if not isinstance(avoidable_by, list):
|
||
avoidable_by = []
|
||
# фильтруем только допустимые значения
|
||
valid_avoid = ["password", "specific_item", "specific_creature_type"]
|
||
avoidable_by = [a for a in avoidable_by if a in valid_avoid]
|
||
|
||
# Расчёт чисел
|
||
calc = _calculate_damage_and_dc(level, severity, complexity)
|
||
|
||
# Генерация описания
|
||
desc = _generate_description({
|
||
"trap_type": trap_type,
|
||
"effect_type": effect_type,
|
||
"damage_type": damage_type,
|
||
"area_shape": area_shape,
|
||
"trigger_type": trigger_type,
|
||
"trigger_location": trigger_location,
|
||
"detection_skill": detection_skill,
|
||
"disarm_skill": disarm_skill,
|
||
"requires_tools": requires_tools,
|
||
"reset": reset,
|
||
"avoidable_by": avoidable_by,
|
||
"complexity": complexity,
|
||
"duration": duration
|
||
}, calc)
|
||
|
||
# Формирование секции Markdown
|
||
section = f"""### 🪤 Ловушка №{i}
|
||
- **Локация:** {env.capitalize()}
|
||
- **Уровень группы:** {level}
|
||
- **Тип ловушки:** {trap_type.capitalize()} | **Сложность:** {complexity.capitalize()} | **Угроза:** {severity.capitalize()}
|
||
- **Триггер:** {trigger_type.replace('_', ' ').capitalize()} ({trigger_location})
|
||
- **Эффект:** {effect_type.capitalize()} | **Область:** {area_shape.replace('_', ' ').capitalize()}
|
||
- **Длительность:** {duration.capitalize()}
|
||
|
||
**Описание:**
|
||
{desc}
|
||
"""
|
||
results.append(section)
|
||
|
||
return "\n\n---\n\n".join(results)
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# Быстрый тест
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
|
||
if __name__ == "__main__":
|
||
test_requests = [
|
||
{
|
||
"environment": "dungeon",
|
||
"level": 5,
|
||
"trap_type": "mechanical",
|
||
"severity": "dangerous",
|
||
"effect_type": "damage",
|
||
"damage_type": "fire",
|
||
"area_shape": "cone",
|
||
"trigger_type": "pressure_plate",
|
||
"detection_skill": "perception",
|
||
"disarm_skill": "thieves_tools",
|
||
"requires_tools": True,
|
||
"avoidable_by": ["password"]
|
||
},
|
||
{
|
||
"environment": "tomb",
|
||
"level": 12,
|
||
"trap_type": "haunted",
|
||
"complexity": "complex",
|
||
"severity": "deadly",
|
||
"effect_type": "condition",
|
||
"area_shape": "room",
|
||
"trigger_type": "enter_area",
|
||
"avoidable_by": ["specific_creature_type"]
|
||
},
|
||
{
|
||
"environment": "forest",
|
||
"level": 3,
|
||
# минимум полей — всё остальное случайно
|
||
}
|
||
]
|
||
print(generate_trap("Тест генератора ловушек", test_requests))
|