623 lines
32 KiB
Python
623 lines
32 KiB
Python
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: Средний уровень группы (1–20).
|
||
По умолчанию: 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))
|