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

623 lines
32 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.

import random
from typing import Dict, List, Optional
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: Серьёзность угрозы.
Допустимы: easy (лёгкая), 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.
trigger_range_ft: Расстояние, на котором триггер срабатывает.
0 = касание, положительное число = футы до цели.
По умолчанию: 0.
effect_type: Основной эффект ловушки.
Допустимы: damage, condition, restrain, imprison, teleport,
summon, alarm, delay, environment, curse, flooding, illusion.
По умолчанию: damage (случайно из damage/condition/restrain).
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, net.
По умолчанию: single_target (случайно cone/cube).
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.
reset: Перезарядка ловушки.
Допустимы: once (одноразовая), manual (ручная), automatic (автоматическая).
По умолчанию: once.
avoidable_by: Особые условия избегания.
Допустимы: password, specific_item, specific_creature_type.
По умолчанию: [].
trap_race: Раса/культура создателей ловушки (влияет на визуальные/звуковые подсказки).
Допустимы: dwarven, elven, humanoid, goblinoid, giant, draconic,
elemental, undead, fey, lizardfolk, drow, aberration, thief_guild.
Если не указана, выбирается случайно на основе environment.
summon_type: Тип существа, которое призывается (если effect_type=summon).
Допустимы: aberration, giant, humanoid, dragon, beast, fiend,
construct, monstrosity, celestial, undead, plant, ooze, fey, elemental.
Если не указан, выбирается случайно на основе environment.
disarm_fail_threshold: При попытке обезвредить, если результат проверки меньше Сл на это значение или более,
ловушка срабатывает. По умолчанию 0. Для deadly рекомендуется 5.
flooding_medium: Тип жидкости при затоплении (если effect_type=flooding).
Допустимы: water, acid, lava. По умолчанию: water.
illusion_type: Тип иллюзии (если effect_type=illusion).
Допустимы: visual, auditory, tactile. По умолчанию: visual.
illusion_save: Спасбросок против иллюзии.
Допустимы: wisdom, intelligence. По умолчанию: wisdom.
"""
environment: str
level: int
trap_type: str
complexity: str
severity: str
trigger_type: str
trigger_location: str
trigger_range_ft: int # 0 = касание, иначе расстояние в футах
effect_type: str
damage_type: str
area_shape: str
duration: str
detection_skill: str
disarm_skill: str
reset: str
avoidable_by: List[str]
trap_race: str
summon_type: str
disarm_fail_threshold: int
flooding_medium: str
illusion_type: str
illusion_save: str
DAMAGE_SAVE_MAP = {
"poison": "Телосложение",
"fire": "Ловкость",
"cold": "Телосложение",
"acid": "Ловкость",
"lightning": "Ловкость",
"bludgeoning": "Ловкость",
"piercing": "Ловкость",
"slashing": "Ловкость",
"psychic": "Интеллект",
"necrotic": "Мудрость",
"radiant": "Мудрость",
"thunder": "Телосложение"
}
def _get_save_ability(damage_type: str) -> str:
return DAMAGE_SAVE_MAP.get(damage_type, "Ловкость")
AREA_SIZES = {
"cone": 15, # 15-футовый конус
"cube": 10, # 10-футовый куб
"line": 30, # 30-футовая линия
"sphere": 20, # 20-футовый радиус
"room": 30, # 30x30 комната (или всё помещение)
"pit": 10, # 10 футов глубины
"net": 10, # 10x10 футов сеть
"single_target": 0 # нет размера
}
# ----------------------------------------------------------------------
# Словари соответствий (environment -> возможные расы ловушек)
# ----------------------------------------------------------------------
TRAP_RACES_BY_ENVIRONMENT = {
"arctic": ["dwarven", "giant", "draconic"],
"coastal": ["elven", "dwarven", "humanoid"],
"desert": ["dwarven", "draconic", "elemental"],
"forest": ["elven", "goblinoid", "fey"],
"grassland": ["humanoid", "goblinoid"],
"hill": ["dwarven", "giant"],
"mountain": ["dwarven", "draconic", "giant"],
"swamp": ["undead", "lizardfolk", "goblinoid"],
"underdark": ["drow", "dwarven", "aberration"],
"underwater": ["elven", "dwarven", "draconic"],
"urban": ["humanoid", "dwarven", "thief_guild"],
"plane_of_air": ["elemental", "draconic", "humanoid"],
"plane_of_earth": ["dwarven", "elemental", "giant"],
"plane_of_fire": ["draconic", "elemental", "humanoid"],
"plane_of_water": ["elven", "elemental", "lizardfolk"],
}
# Визуальные и звуковые подсказки для каждой расы
TRAP_RACE_CLUES = {
"dwarven": {
"visual": ["gears", "rune_carvings", "stone_brick_pattern", "soot", "oil_stains"],
"audible": ["click", "clank", "grinding_stone"]
},
"elven": {
"visual": ["delicate_lines", "leaves", "moon_motif", "faint_glow"],
"audible": ["soft_chime", "whisper"]
},
"humanoid": {
"visual": ["scorch_marks", "loose_flagstone", "rope_fibers"],
"audible": ["squeak", "creak"]
},
"goblinoid": {
"visual": ["wooden_spikes", "bones", "crude_writing"],
"audible": ["jingle_bell", "twang"]
},
"giant": {
"visual": ["massive_dents", "broken_columns", "scratch_marks_high"],
"audible": ["heavy_thud", "groaning_metal"]
},
"draconic": {
"visual": ["scaly_pattern", "claw_marks", "bas-relief_dragon"],
"audible": ["hiss", "roar_echo"]
},
"elemental": {
"visual": ["glowing_veins", "crackling_energy", "charred_floor"],
"audible": ["hum", "crackle", "whoosh"]
},
"undead": {
"visual": ["cobwebs", "dust", "hidden_bones", "decayed_fabric"],
"audible": ["moan", "skittering"]
},
"fey": {
"visual": ["glowing_mushrooms", "twisted_vines", "sparkling_dust"],
"audible": ["giggle", "chime"]
},
"lizardfolk": {
"visual": ["scales", "eggshells", "claw_scratches"],
"audible": ["hiss", "scraping"]
},
"drow": {
"visual": ["spider_motif", "web_strands", "poison_droplets"],
"audible": ["skittering", "faint_scream"]
},
"aberration": {
"visual": ["organic_growth", "wrong_angles", "eyeball_patterns"],
"audible": ["whisper_gibberish", "squelch"]
},
"thief_guild": {
"visual": ["tool_marks", "hidden_catch", "false_bottom"],
"audible": ["spring_coil", "tiny_click"]
}
}
# environment -> возможные типы призываемых существ
SUMMON_TYPE_BY_ENVIRONMENT = {
"arctic": ["giant", "beast", "elemental"],
"coastal": ["beast", "elemental", "dragon"],
"desert": ["elemental", "undead", "beast"],
"forest": ["beast", "fey", "plant"],
"grassland": ["beast", "humanoid", "giant"],
"hill": ["giant", "beast", "humanoid"],
"mountain": ["giant", "dragon", "elemental"],
"swamp": ["undead", "plant", "beast"],
"underdark": ["aberration", "humanoid", "monstrosity"],
"underwater": ["beast", "elemental", "dragon"],
"urban": ["humanoid", "construct", "undead"],
"plane_of_air": ["elemental", "dragon", "beast", "giant"],
"plane_of_earth": ["elemental", "humanoid", "monstrosity", "construct"],
"plane_of_fire": ["elemental", "fiend", "dragon"],
"plane_of_water": ["elemental", "dragon", "beast", "humanoid"],
}
# ----------------------------------------------------------------------
# Таблицы CR → характеристики (из таблицы монстров)
# ----------------------------------------------------------------------
MONSTER_STATS = {
0.125: {"pb": 2, "dc": 13, "attack": 3, "hp": 21, "dmg_low": 2, "dmg_high": 3},
0.25: {"pb": 2, "dc": 13, "attack": 3, "hp": 43, "dmg_low": 4, "dmg_high": 5},
0.5: {"pb": 2, "dc": 13, "attack": 3, "hp": 60, "dmg_low": 6, "dmg_high": 8},
1: {"pb": 2, "dc": 13, "attack": 3, "hp": 78, "dmg_low": 9, "dmg_high": 14},
2: {"pb": 2, "dc": 13, "attack": 3, "hp": 93, "dmg_low": 15, "dmg_high": 20},
3: {"pb": 2, "dc": 13, "attack": 4, "hp": 108, "dmg_low": 21, "dmg_high": 26},
4: {"pb": 2, "dc": 14, "attack": 5, "hp": 123, "dmg_low": 27, "dmg_high": 32},
5: {"pb": 3, "dc": 15, "attack": 6, "hp": 138, "dmg_low": 33, "dmg_high": 38},
6: {"pb": 3, "dc": 15, "attack": 6, "hp": 153, "dmg_low": 39, "dmg_high": 44},
7: {"pb": 3, "dc": 15, "attack": 6, "hp": 168, "dmg_low": 45, "dmg_high": 50},
8: {"pb": 3, "dc": 16, "attack": 7, "hp": 183, "dmg_low": 51, "dmg_high": 56},
9: {"pb": 4, "dc": 16, "attack": 7, "hp": 198, "dmg_low": 57, "dmg_high": 62},
10: {"pb": 4, "dc": 16, "attack": 7, "hp": 213, "dmg_low": 63, "dmg_high": 68},
11: {"pb": 4, "dc": 17, "attack": 8, "hp": 228, "dmg_low": 69, "dmg_high": 74},
12: {"pb": 4, "dc": 17, "attack": 8, "hp": 243, "dmg_low": 75, "dmg_high": 80},
13: {"pb": 5, "dc": 18, "attack": 8, "hp": 258, "dmg_low": 81, "dmg_high": 86},
14: {"pb": 5, "dc": 18, "attack": 8, "hp": 273, "dmg_low": 87, "dmg_high": 92},
15: {"pb": 5, "dc": 18, "attack": 8, "hp": 288, "dmg_low": 93, "dmg_high": 98},
16: {"pb": 5, "dc": 18, "attack": 9, "hp": 303, "dmg_low": 99, "dmg_high": 104},
17: {"pb": 6, "dc": 19, "attack": 10, "hp": 318, "dmg_low": 105, "dmg_high": 110},
18: {"pb": 6, "dc": 19, "attack": 10, "hp": 333, "dmg_low": 111, "dmg_high": 116},
19: {"pb": 6, "dc": 19, "attack": 10, "hp": 348, "dmg_low": 117, "dmg_high": 122},
20: {"pb": 6, "dc": 19, "attack": 10, "hp": 363, "dmg_low": 123, "dmg_high": 140},
}
VALID_CRS = sorted(MONSTER_STATS.keys())
# ----------------------------------------------------------------------
# Вспомогательные функции
# ----------------------------------------------------------------------
def _get_cr(level: int, severity: str) -> float:
mult = {"easy": 0.25, "moderate": 0.5, "dangerous": 0.75, "deadly": 1.0}[severity]
cr_raw = level * mult
if cr_raw < 0.125:
return 0.125
if cr_raw < 1.0:
if cr_raw < 0.1875:
return 0.125
if cr_raw < 0.375:
return 0.25
else:
return 0.5
cr_int = int(cr_raw)
if cr_int not in MONSTER_STATS:
available = [c for c in VALID_CRS if c <= cr_int]
return max(available) if available else 1
return cr_int
def _get_monster_stats(cr: float) -> Dict:
return MONSTER_STATS.get(cr, MONSTER_STATS[20])
def _get_damage(level: int, severity: str, area: bool = False) -> str:
cr = _get_cr(level, severity)
stats = _get_monster_stats(cr)
avg_damage = (stats["dmg_low"] + stats["dmg_high"]) // 2
if avg_damage < 1:
avg_damage = 1
num_dice = max(1, round(avg_damage / 5.5))
if area:
return f"{num_dice}d6"
else:
return f"{num_dice}d10"
def _derive_escape_requirements(area_shape: str, effect_type: str) -> List[str]:
if area_shape == "pit":
return ["climb_speed", "climbing_gear", "spider_climb"]
if effect_type == "restrain" and area_shape == "net":
return ["strength_check", "slashing_damage"]
if effect_type == "imprison":
return ["strength_check", "lockpick", "magic"]
return []
def _get_trap_race(environment: str, user_trap_race: Optional[str]) -> str:
if user_trap_race and user_trap_race in TRAP_RACE_CLUES:
return user_trap_race
possible = TRAP_RACES_BY_ENVIRONMENT.get(environment, ["humanoid"])
return random.choice(possible)
def _get_visual_audible_clues(trap_race: str):
clues = TRAP_RACE_CLUES.get(trap_race, TRAP_RACE_CLUES["humanoid"])
return clues["visual"], clues["audible"]
def _get_summon_type(environment: str, user_summon_type: Optional[str]) -> str:
if user_summon_type:
return user_summon_type
possible = SUMMON_TYPE_BY_ENVIRONMENT.get(environment, ["humanoid"])
return random.choice(possible)
# ----------------------------------------------------------------------
# Основная функция генерации
# ----------------------------------------------------------------------
def generate_trap(debug_description: str, requests: List[TrapRequest]) -> str:
if not requests:
requests = [{}]
results = []
for idx, 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")
if trap_type not in ["mechanical", "magical", "haunted", "living"]:
trap_type = random.choice(
["mechanical", "magical", "haunted", "living"])
complexity = req.get("complexity", "simple")
if complexity not in ["simple", "complex"]:
complexity = "simple"
severity = req.get("severity", "moderate")
if severity not in ["easy", "moderate", "dangerous", "deadly"]:
severity = "moderate"
trigger_type = req.get("trigger_type")
valid_triggers = [
"pressure_plate", "trip_wire", "door_open", "lock_pick", "touch",
"time", "magical_ward", "manual", "force", "enter_area",
"looked_at"
]
if not trigger_type or trigger_type not in valid_triggers:
trigger_type = random.choice(valid_triggers)
trigger_location = req.get("trigger_location")
valid_locs = [
"floor", "wall", "ceiling", "door", "chest", "object", "corridor"
]
if not trigger_location or trigger_location not in valid_locs:
trigger_location = random.choice(valid_locs)
effect_type = req.get("effect_type")
valid_effects = [
"damage", "condition", "restrain", "imprison", "teleport",
"summon", "alarm", "delay", "environment", "curse", "flooding",
"illusion"
]
if not effect_type or effect_type not in valid_effects:
effect_type = random.choice(["damage", "condition", "restrain"])
# TODO: Можно поддержать ещё secondary effect для complex ловушек, с шансом, например, 10 или 20%, в виде:
# secondary_effect_type (те же значения, что effect_type: damage, condition, restrain, imprison, teleport, summon, alarm, delay, environment, curse, flooding, illusion)
# secondary_damage_type (если secondary_effect_type = damage)
# secondary_severity_factor (float, по умолчанию 0.5 — множитель к урону/Сл)
damage_type = req.get("damage_type")
valid_damage = [
"poison", "fire", "cold", "acid", "lightning", "bludgeoning",
"piercing", "slashing", "psychic", "necrotic", "radiant", "thunder"
]
if effect_type == "damage" and (not damage_type
or damage_type not in valid_damage):
damage_type = random.choice(valid_damage)
area_shape = req.get("area_shape")
valid_shapes = [
"single_target", "cone", "cube", "line", "sphere", "room", "pit",
"net"
]
if not area_shape or area_shape not in valid_shapes:
area_shape = random.choice(["single_target", "cone", "cube"])
duration = req.get("duration")
valid_durations = [
"instant", "persistent", "repeating", "until_escape"
]
if not duration or duration not in valid_durations:
duration = "instant"
detection_skill = req.get("detection_skill")
valid_detection = ["perception", "investigation", "arcana", "none"]
if not detection_skill or detection_skill not in valid_detection:
detection_skill = "perception"
disarm_skill = req.get("disarm_skill")
valid_disarm = [
"thieves_tools", "athletics", "arcana", "religion",
"simple_action", "none"
]
if not disarm_skill or disarm_skill not in valid_disarm:
disarm_skill = "thieves_tools"
reset = req.get("reset")
valid_reset = ["once", "manual", "automatic"]
if not reset or reset not in valid_reset:
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]
# Новые поля
trap_race = _get_trap_race(env, req.get("trap_race"))
summon_type = _get_summon_type(env, req.get("summon_type"))
disarm_fail_threshold = req.get("disarm_fail_threshold",
5 if severity == "deadly" else 0)
flooding_medium = req.get(
"flooding_medium", "water") if effect_type == "flooding" else None
illusion_type = req.get(
"illusion_type", "visual") if effect_type == "illusion" else None
illusion_save = req.get(
"illusion_save", "wisdom") if effect_type == "illusion" else None
# Рассчёт характеристик
cr = _get_cr(level, severity)
stats = _get_monster_stats(cr)
save_dc = stats["dc"]
attack_bonus = stats["attack"]
hp = stats["hp"]
damage_single = _get_damage(level, severity, area=False)
damage_area = _get_damage(
level, severity,
area=True) if area_shape != "single_target" else damage_single
init_bonus = stats["pb"] if complexity == "complex" else None
escape_req = _derive_escape_requirements(area_shape, effect_type)
visual_clues, audible_clues = _get_visual_audible_clues(trap_race)
# --- Генерация Markdown ---
lines = []
lines.append(f"### 🪤 Ловушка №{idx}")
lines.append(f"- **Локация:** {env.capitalize()}")
lines.append(
f"- **Уровень группы:** {level} | **Угроза:** {severity.capitalize()} | **Сложность:** {complexity.capitalize()}"
)
lines.append(
f"- **Тип ловушки:** {trap_type.capitalize()} | **Раса создателей:** {trap_race.capitalize()}"
)
# Триггер с указанием дальности
trigger_range = req.get("trigger_range_ft", 0)
if trigger_range > 0:
trigger_text = f"{trigger_type.replace('_', ' ').capitalize()} на расстоянии {trigger_range} футов ({trigger_location})"
else:
trigger_text = f"{trigger_type.replace('_', ' ').capitalize()} ({trigger_location})"
lines.append(f"- **Триггер:** {trigger_text}")
lines.append(
f"- **Эффект:** {effect_type.capitalize()} | **Область:** {area_shape.replace('_', ' ').capitalize()}"
)
# Размер области
area_size = AREA_SIZES.get(area_shape, 0)
if area_size > 0 and area_shape != "single_target":
lines.append(f"- **Размер области:** {area_size} футов")
lines.append(f"- **Длительность:** {duration.capitalize()}")
lines.append("")
lines.append("**Описание:**")
lines.append(
f"*{visual_clues[0].replace('_', ' ').capitalize()} и {audible_clues[0]} выдают скрытую угрозу.*"
)
lines.append("")
lines.append(
f"🔍 **Обнаружение:** Успешная проверка Мудрости ({detection_skill.capitalize()}) Сл {save_dc} позволяет заметить признаки ловушки."
)
disarm_text = ""
if disarm_skill == "thieves_tools":
disarm_text = f"Успешная проверка Ловкости (Воровские инструменты) Сл {save_dc} обезвреживает ловушку. При провале на {disarm_fail_threshold} или более ловушка срабатывает."
elif disarm_skill == "athletics":
disarm_text = f"Успешная проверка Силы (Атлетика) Сл {save_dc} позволяет сломать или заблокировать механизм."
elif disarm_skill == "arcana":
disarm_text = f"Успешная проверка Интеллекта (Магия) Сл {save_dc} или заклинание `dispel magic` (Сл {save_dc}) отключает магическую ловушку."
elif disarm_skill == "religion":
disarm_text = f"Успешная проверка Интеллекта (Религия) Сл {save_dc} позволяет стереть священную руну."
elif disarm_skill == "simple_action":
disarm_text = "Достаточно простого действия: перешагнуть, забить клин или перерезать верёвку."
else:
disarm_text = "Ловушку невозможно обезвредить; её можно только избежать."
lines.append(f"🛠️ **Обезвреживание:** {disarm_text}")
reset_text = {
"once": "Ловушка одноразовая.",
"manual": "Требуется ручная перезарядка (10 минут).",
"automatic": "Перезаряжается автоматически через 1d10 минут."
}.get(reset, "")
lines.append(f"⏳ **Перезарядка:** {reset_text}")
if avoidable_by:
lines.append(
f"✨ **Избегание:** Можно полностью избежать, если {', '.join(avoidable_by)}."
)
if escape_req:
lines.append(f"🏃 **Побег:** Требуется: {', '.join(escape_req)}.")
if complexity == "complex":
lines.append(
f"⚙️ **Сложная ловушка:** Действует на инициативе (бонус {init_bonus:+d}). В свой ход повторяет эффект. Требуется 3 успешные проверки для отключения."
)
if effect_type == "damage":
save_ability = _get_save_ability(damage_type)
if area_shape == "single_target":
lines.append(
f"💥 **Урон:** {damage_single} ({damage_type.capitalize()}) + спасбросок {save_ability} Сл {save_dc} для половины."
)
else:
lines.append(
f"💥 **Урон по области:** {damage_area} ({damage_type.capitalize()}) + спасбросок {save_ability} Сл {save_dc} для половины."
)
if damage_type in ["fire", "acid"]:
lines.append("🔥 Легковоспламеняющиеся предметы загораются.")
elif effect_type == "condition":
cond = random.choice([
"отравлен", "ослеплён", "оглушён", "ошеломлён", "напуган",
"парализован"
])
lines.append(
f"😵 **Состояние:** {cond} на 1 минуту (спасбросок Мудрости Сл {save_dc})."
)
elif effect_type == "restrain":
lines.append(
f"⛓️ **Опутывание:** Цель должна преуспеть в спасброске Ловкости Сл {save_dc}, иначе становится опутанной. Для освобождения требуется проверка Силы (Сл 15)."
)
elif effect_type == "imprison":
lines.append(
f"🔒 **Заточение:** Цель должна преуспеть в спасброске Ловкости Сл {save_dc}, иначе оказывается запертой. Для освобождения требуется проверка Силы (Сл 15) или магия."
)
elif effect_type == "flooding":
lines.append(
f"🌊 **Затопление:** Каждый раунд уровень {flooding_medium} поднимается на 1 фут. Существа в зоне должны делать спасброски Силы Сл {save_dc} или быть сметёнными."
)
elif effect_type == "illusion":
lines.append(
f"👁️ **Иллюзия ({illusion_type}):** Спасбросок {illusion_save.upper()} Сл {save_dc} иначе ложное восприятие на 1 минуту."
)
elif effect_type == "summon":
lines.append(
f"📯 **Призыв:** Появляется существо типа {summon_type} (CR {cr})."
)
elif effect_type == "teleport":
lines.append(
f"🌀 **Телепортация:** Цель переносится в другое место (спасбросок Мудрости Сл {save_dc} избегает)."
)
elif effect_type == "alarm":
lines.append(f"🔔 **Тревога:** Громкий звук оповещает обитателей.")
elif effect_type == "delay":
lines.append(
f"🚧 **Задержка:** Проход блокируется (требуется проверка Силы Сл {save_dc} чтобы пробиться)."
)
elif effect_type == "environment":
lines.append(
f"🌪️ **Изменение среды:** Пол становится скользким, появляется труднопроходимая местность и т.п."
)
elif effect_type == "curse":
lines.append(
f"☠️ **Проклятие:** Цель должна преуспеть в спасброске Мудрости Сл {save_dc} иначе получает проклятие (снимается только `remove curse`)."
)
lines.append("")
lines.append(
f"*💬 Реплика Мастера: Эта ловушка {'смертельно опасна' if severity=='deadly' else 'серьёзна' if severity=='dangerous' else 'доставит неприятности' if severity=='moderate' else 'скорее досадная помеха'} для группы {level} уровня.*"
)
results.append("\n".join(lines))
return "\n\n---\n\n".join(results)
# ----------------------------------------------------------------------
# Пример использования
# ----------------------------------------------------------------------
if __name__ == "__main__":
test_req = [
{"environment": "dungeon", "level": 5, "severity": "dangerous", "effect_type": "damage", "area_shape": "cone"},
{"environment": "swamp", "level": 12, "severity": "deadly", "complexity": "complex", "trap_type": "haunted", "effect_type": "illusion"}
]
print(generate_trap("Тест", test_req))