Update generate_encounter tool: now it gets also world and region parameters

This commit is contained in:
dimitrievgs 2026-07-12 14:53:11 +03:00
parent 8d8eff6863
commit 65cbed3b88

View File

@ -59,6 +59,17 @@ from get_npc_races import get_npc_races
from generate_npc import generate_npc as get_npc
# Множество планов существования (не прайм-миры).
# Если world попадает сюда — монстры берутся из planar_monsters_data
# (через ключ world), а биом environment игнорируется.
PLANAR_WORLDS: set[str] = {
"astral_sea", "plane_of_air", "plane_of_earth", "plane_of_fire", "plane_of_water",
"feywild", "shadowfell",
"lower_planes_le", "lower_planes_ne", "lower_planes_ce",
"upper_planes_lg", "upper_planes_ng", "upper_planes_cg",
"planes_of_chaos", "plane_of_law",
}
# ──────────────────────────────────────────────────────────────────────────────
# TypedDict
# ──────────────────────────────────────────────────────────────────────────────
@ -72,12 +83,34 @@ class EncounterRequest(TypedDict, total=False):
environment: Биом.
Допустимы: arctic, coastal, desert, forest,
grassland, hill, mountain, swamp, underdark,
underwater, urban,
astral_sea, plane_of_air, plane_of_earth, plane_of_fire, plane_of_water,
feywild, shadowfell, lower_planes_le, lower_planes_ne, lower_planes_ce,
upper_planes_lg, upper_planes_ng, upper_planes_cg, planes_of_chaos, plane_of_law.
underwater, urban.
По умолчанию: forest.
world: Космологический уровень сцены (англ.). Прайм-мир
("Toril", "Krynn", "Oerth") ЛИБО план существования ("feywild",
"shadowfell", "lower_planes_le/ne/ce",
"upper_planes_lg/ng/cg", "planes_of_chaos",
"plane_of_law", "astral_sea", "plane_of_air",
"plane_of_earth", "plane_of_fire", "plane_of_water").
Пусто прайм-мир, монстры по биому environment.
region: Регион ВНУТРИ прайм-мира (обычное англ. название,
код нормализует через symspellpy).
Определение СТРОГО ПО ПРИОРИТЕТУ:
0. Никаких артиклей нигде применять нельзя, пример, nelanther_isles, а не the_nelanther_isles
1. Если world план, region опускается.
2. Подземелье/пещера/канализация "Underdark".
3. Крупный город название города
(Waterdeep, Baldur's Gate, Neverwinter,
Suzail, Athkatla, Calimport, Menzoberranzan).
4. Дикая местность историческая область по
ближайшему городу/хребту/лесу
(Western Heartlands, Silver Marches, Cormyr,
Amn, Tethyr, Thay, Chult и т.д.).
5. Море/побережье ближайшая страна или
уникальная акватория (Sea of Fallen Stars).
6. Не определить пусто (глобальные шансы).
level: Средний уровень группы (120).
По умолчанию: 1.
@ -119,6 +152,8 @@ class EncounterRequest(TypedDict, total=False):
По умолчанию: True.
"""
environment: str
world: str
region: str
level: int
encounter_type: Optional[str]
party_surprise_mods: Dict[str, int]
@ -219,23 +254,28 @@ def _roll_count(notation: str) -> int:
# Вспомогательные функции — сборка субъектов
# ──────────────────────────────────────────────────────────────────────────────
def _build_subjects(hook_name: str, enc_type: str, env: str, level: int) -> list[dict]:
def _build_subjects(
hook_name: str,
enc_type: str,
env: str,
level: int,
world: Optional[str] = None,
region: Optional[str] = None,
) -> list[dict]:
"""
По схеме хука вызывает get_creature() или get_npc()
и возвращает список субъектов с ролями.
Каждый элемент результата:
{
"source": "creatures" | "npc",
"role": "primary" | "aggressor" | "defender" | "victim" | "bystander",
"count": int, # сколько существ/NPC
"result": str, # текст из таблицы или описание NPC
}
"""
hook_data = HOOKS.get(enc_type, {}).get(hook_name, {})
slots = hook_data.get("subject_sources", [])
subjects = []
# Определяем ключ для таблиц монстров:
# если world — план, монстры берутся из planar-таблиц по ключу world,
# иначе (прайм-мир) — по биому environment.
is_planar = bool(world) and world.lower().strip() in PLANAR_WORLDS
creature_env = world.lower().strip() if is_planar else env
for slot in slots:
# Опциональный слот — 50% шанс появления
if slot.get("optional") and random.random() < 0.5:
@ -246,17 +286,27 @@ def _build_subjects(hook_name: str, enc_type: str, env: str, level: int) -> list
role = slot.get("role", "primary")
if source == "creatures":
result_text = get_creature(env, level)
result_text = get_creature(creature_env, level)
elif source == "npc":
allowed_races = get_npc_races(env)
# Приоритет — географический контекст (world/region).
# Если он задан, get_npc сам подберёт расы через REGIONAL_OVERWRITES.
# Иначе — fallback на список рас по биому.
npc_constraint = {
"count": count,
"level": level,
"commoner_chance": 0.85,
}
if world:
npc_constraint["world"] = world
if region:
npc_constraint["region"] = region
else:
# TODO: не очевидноЮ тут бы подумал, но, кажется, тут есть смысл пробрасывать только если у этого тула есть такой же параметр, чтобы можно было создать столкновения только с человеческими бандитами
npc_constraint["allowed_races"] = get_npc_races(env)
result_text = get_npc(
debug_description="encounter npc",
constraints=[{
"count": count,
"level": level,
"commoner_chance": 0.85,
"allowed_races": allowed_races,
}],
constraints=[npc_constraint],
)
else:
result_text = f"[Неизвестный источник: {source}]"
@ -390,18 +440,25 @@ def generate_encounter(
env = req.get("environment", "forest").lower().strip()
level = int(req.get("level", 1))
# Географический / планарный контекст
world = req.get("world", None)
region = req.get("region", None)
# Считываем явно заданный тип или None
req_type = req.get("encounter_type")
# ── Тип и хук ────────────────────────────────────────────────────────
enc_type = _pick_encounter_type(env, req_type)
# Для выбора весов типа встречи: если world — план, используем его,
# иначе — биом прайм-мира.
type_key = world.lower().strip() if (world and world.lower().strip() in PLANAR_WORLDS) else env
enc_type = _pick_encounter_type(type_key, req_type)
hook_name = _pick_hook(enc_type)
hook_meta = HOOKS.get(enc_type, {}).get(hook_name, {})
hook_desc = hook_meta.get("description", "")
latent = hook_meta.get("latent_threat", False)
# ── Субъекты ──────────────────────────────────────────────────────────
subjects = _build_subjects(hook_name, enc_type, env, level)
subjects = _build_subjects(hook_name, enc_type, env, level, world, region)
subjects_text = _format_subjects(subjects, latent)
# ── Surprise ──────────────────────────────────────────────────────────
@ -420,9 +477,16 @@ def generate_encounter(
reaction_text = _determine_reaction(req.get("reaction_mods", {}))
# ── Сборка секции ─────────────────────────────────────────────────────
loc_line = ""
if world:
loc_line += f"- **Мир/План:** {world}\n"
if region:
loc_line += f"- **Регион:** {region}\n"
section = (
f"### Встреча №{i}\n"
f"- **Биом:** {env.capitalize()}\n"
f"{loc_line}"
f"- **Уровень группы:** {level}\n"
f"- **Тип встречи:** {enc_type}\n"
f"- **Хук:** {hook_name} — *{hook_desc}*\n\n"