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