dnd5-scripts/generate_trap/generate_trap (old).py
2026-06-17 22:32:48 +03:00

584 lines
34 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.

"""
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: Средний уровень группы (120).
По умолчанию: 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))