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

492 lines
28 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.

# races.py
import random
from languages import (
Common, Celestial, Elvish, Dwarvish, Draconic, GiantLanguage,
GnomishLanguage, GoblinLanguage, HalflingLanguage, OrcLanguage,
Infernal, Primordial, Sylvan, Abyssal, GithLanguage, LeoninLanguage,
Undercommon, Alzhedo, Chondathan, Damaran, Illuskan, KoryoLanguage,
Mulani, Rashemi, Shou,
living_languages_except_common,
)
# ─── Расы ────────────────────────────────────────────────────────────────────
RACES: dict = {
"Aasimar": {
"chance": 1.00,
"subraces": {
"Protector": {"chance": 50.00},
"Scourge": {"chance": 30.00},
"Fallen": {"chance": 20.00},
},
"names": "Human",
"languages": [Common, Celestial],
},
"Bugbear": {
"chance": 0.50,
"names": "Goblin",
"languages": [Common, GoblinLanguage],
},
"Dragonborn": {
"chance": 1.00,
"subraces": {color: {"chance": 10.00} for color in
["Black","Blue","Brass","Bronze","Copper",
"Gold","Green","Red","Silver","White"]},
"names": {
"M": ["Арджхан","Баласар","Бхараш","Гхеш","Донаар","Крив","Медраш","Мехен",
"Надарр","Панджед","Патрин","Рхогар","Тархун","Торинн","Хескан","Шамаш","Шединн"],
"F": ["Акра","Бири","Даар","Джхери","Кава","Коринн","Мисханн","Нала",
"Перра","Райанн","Сора","Сурина","Тхава","Уаджит","Фаридэ","Хавилар","Харанн"],
},
"languages": [Common, Draconic],
},
"Dwarf": {
"chance": 10.00,
"subraces": {
"Gold/Hill": {"chance": 50.00, "languages": [Common, Dwarvish]},
"Shield/Mountain":{"chance": 40.00, "languages": [Common, Dwarvish]},
"Duergar": {"chance": 10.00, "languages": [Common, Dwarvish, Undercommon]},
},
"names": {
"M": ["Адрик","Альберих","Баренд","Барундар","Баэрн","Броттор","Бруенор","Вондал",
"Вулгар","Вэйт","Гардаин","Даин","Даррак","Делг","Джойин","Дорн","Килдрак",
"Моргран","Орсик","Оскар","Рангрим","Рорин","Рюрик","Сторн","Таклинн",
"Торадин","Тордек","Торин","Торик","Травок","Траубон","Ульфгар","Фаргрим",
"Флинт","Харбек","Хондар","Эберк","Эйнкиль"],
"F": ["Артин","Бардрин","Белмара","Вистра","Гуннлода","Гурдис","Дагнал","Джойлин",
"Диеза","Дорна","Илде","Катра","Киира","Кристид","Лифтраса","Мардред",
"Одхильд","Рисвин","Самбрил","Саннл","Тейс","Торбера","Торгга","Умиль",
"Фалкрунн","Финеллен","Хельджа","Хлин","Эльдет","Эмбер"],
"S": ["Балдерк","Блейтбайт","Боевой Молот","Гордривер","Горунн","Данкил",
"Железный Кулак","Каменный Щит","Крауншилд","Крепкая Наковальня",
"Ледяная Борода","Лодерр","Лютгер","Огненная Кузня","Рамнахейм",
"Рог","Скуллдарк","Стракелн","Торунн","Унгарт","Холдерхек"],
},
"languages": [Common, Dwarvish],
},
"Elf": {
"chance": 10.00,
"subraces": {
"Avariel": {"chance": 0.50, "names": "Moon", "languages": [Common, Elvish]},
"Drow": {
"chance": 10.00,
"names": {
"M": ["Алак","Дриззт","Ильмрин","Меринид","Фарон","Риззон","Тебрин","Закнафейн"],
"F": ["Акордия","Чалитра","Эклавдра","Джаэлрина","Недилен","Квилуэ","СиНафай","Влондрил"],
"S": ["Абафейр","Колоара","Гланнат","Иллистин","Фарн","Сиреар","Вринн","Ксилтин"],
},
"languages": [Common, Elvish, Undercommon],
},
"Eladrin": {"chance": 2.50, "names": "Moon", "languages": [Common, Elvish]},
"Moon": {
"chance": 34.50,
"names": {
"M": ["Адран","Аравилар","Арамиль","Араннис","Ауст","Аэлар","Бейро","Берриан",
"Варис","Галинндан","Ивеллиос","Иммераль","Каррик","Куарион","Лаусиан",
"Миндартис","Морн","Ним","Орлпар","Паэлиас","Перен","Респен","Риардон",
"Ристель","Ролен","Саэвел","Совелисс","Тамиорн","Таривол","Терен",
"Тэген","Фейлар","Хадарай","Химо","Хэйян","Эниалис","Эрдан","Эреван"],
"F": ["Адрие","Альтеа","Амра","Анастрианна","Андрасте","Антинуа","Бетринна",
"Бирель","Вадания","Валанте","Вестеле","Джаумрит","Джеленетт","Друсилиа",
"Имизаэль","Йелениа","Каэлинн","Квамара","Квеленна","Квиласи","Кейлет",
"Ксанафия","Лешанна","Лиа","Миали","Мэриэль","Найвара","Сариэль","Силакви",
"Талиндра","Теирастра","Тиа","Фелосиаль","Хакатра","Шава","Шанайра","Энна","Ястра"],
"S": ["Амакиир (Сверкающий Цветок)","Амалит","Амастасия (Звёздный Цветок)",
"Брэген","Галанодель (Лунный Шёпот)","Ивнингфолл","Ильфелкиир (Сверкающий Бутон)",
"Калаудра","Ксилосент (Золотой Лепесток)","Лаэлитар","Лиадон (Серебряный Лист)",
"Мундаун","Найло (Ночной Бриз)","Сианодель (Лунный Ручей)","Тарнрут",
"Холимион (Алмазная Роса)"],
},
"languages": [Common, Elvish],
},
"Shadar-kai":{"chance": 2.50, "names": "Moon", "languages": [Common, Elvish]},
"Sun": {"chance": 25.00, "names": "Moon", "languages": [Common, Elvish]},
"Wood": {"chance": 25.00, "names": "Moon", "languages": [Common, Elvish]},
},
"languages": [Common, Elvish],
},
"Firbolg": {
"chance": 0.20,
"names": "Elf",
"languages": [Common, Elvish, GiantLanguage],
},
"Genasi": {
"chance": 1.00,
"subraces": {
"Air": {"chance": 25.00},
"Earth": {"chance": 25.00},
"Fire": {"chance": 25.00},
"Water": {"chance": 25.00},
},
"names": "Human",
"languages": [Common, Primordial],
},
"Gith": {
"chance": 0.20,
"names": {
"M": ["Al'chaia","Dak'kon","Lae'zel","Vhostym","Yrlakka"],
"F": ["Al'chaia","Dak'kon","Lae'zel","Vhostym","Yrlakka"],
},
"languages": [Common, GithLanguage],
},
"Gnome": {
"chance": 2.00,
"subraces": {
"Forest": {"chance": 50.00},
"Rock": {"chance": 50.00},
},
"names": {
"M": ["Алвин","Алстон","Боддинок","Брок","Бургелл","Бюргел","Варрин","Вренн","Гербо",
"Гимбл","Глим","Джебеддо","Димбл","Дорган","Зук","Келлен","Колмарр","Намфудл",
"Орламм","Оррин","Ронделл","Рундар","Сибо","Синдри","Столиг","Фалринн","Фонкин",
"Фрюг","Халбринн","Элдон","Эрки"],
"F": ["Бимпноттин","Брина","Вейуокет","Джари","Донелла","Дувамил","Занна","Зелазадда",
"Ийисс","Каландра","Карамип","Карлин","Лилли","Лисса","Лорилла","Лупмоттин",
"Маднаб","Мери","Нати","Никс","Нисса","Ода","Орла","Ройвин","Тана","Шамил",
"Эливик","Элиджобелл","Элла","Эрисс"],
"S": ["Берен","Блэкрок","Блимс","Гаррик","Грейторм","Даергел","Мёрниг","Накл",
"Нингел","Раулнор","Ривенстоун","Тавартарр","Тимберс","Турен","Уайтхорн",
"Уварикк","Фолькор","Шеппен"],
},
"languages": [Common, GnomishLanguage],
},
"Goblin": {
"chance": 1.00,
"names": {
"M": ["Аругет","Вании","Ванон","Вуударадж","Гудруун","Даавн","Дабрак","Дагии",
"Джазаал","Древдуул","Дуулан","Каллаад","Кракуул","Кроотад","Мазаан","Мунта",
"Насаар","Ракари","Рексиит","Тарик","Тарууж","Туун","Феник","Четин","Халуун",
"Харуук","Аззинакс","Баболакс","Бликсаникс","Вазозав","Вексин","Даззаз",
"Зиззикс","Калузакс","Криксизикс","Лизакса","Миззикс","Мизнар","Никсиспикс",
"Паксизаз","Равиксиз","Стиксил","Сунникс","Тозинокс","Уксивози","Эстрикс",
"Финизикс","Юзба"],
"F": ["Ааспар","Агуус","Белалуур","Вали","Вуун","Грааль","Гадуул","Денаал","Джелуум",
"Драраар","Дууша","Келаал","Мулаан","Насри","Разу","Ралин","Рексин","Сенен",
"Таджин","Тюнер","Хашак","Шедруор","Эхаас","Элуун"],
},
"languages": [Common, GoblinLanguage],
},
"Goliath": {
"chance": 0.50,
"names": {
"M": ["Аукан","Ваунеа","Вимак","Гае-Ал","Иглат","Иликан","Кеотхи","Куори",
"Ло-Каг","Мавейт","Маннео","Налла","Орило","Пааву","Петани","Талай","Тхотам","Утхал"],
"F": ["Аукан","Ваунеа","Вимак","Гае-Ал","Иглат","Иликан","Кеотхи","Куори",
"Ло-Каг","Мавейт","Маннео","Налла","Орило","Пааву","Петани","Талай","Тхотам","Утхал"],
},
"languages": [Common, GiantLanguage],
},
"Half-Elf": {
"chance": 6.00,
"subraces": {
"Drow": {"chance": 3.50},
"Eladrin": {"chance": 2.50},
"Moon": {"chance": 56.50},
"Shadar-kai":{"chance": 2.50},
"Sun": {"chance": 10.00},
"Wood": {"chance": 25.00},
},
"names": ["Human", "Elf"],
"languages": [Common, Elvish, living_languages_except_common],
},
"Half-Orc": {
"chance": 3.00,
"names": {
"M": ["Гел","Денч","Имш","Кет","Краск","Муррен","Ронт","Токк","Фенг","Хенк","Холг","Шамп"],
"F": ["Багги","Вола","Волен","Евельда","Кансиф","Мев","Нига","Овак","Оунка","Сута","Шаута","Эмен","Энгонг"],
},
"languages": [Common, OrcLanguage],
},
"Halfling": {
"chance": 3.00,
"subraces": {
"Ghostwise": {"chance": 10.00},
"Lightfoot": {"chance": 60.00},
"Stout": {"chance": 30.00},
},
"names": {
"M": ["Альтон","Андер","Блазанар","Вилимак","Гаррет","Далабрак","Кейд","Коррин",
"Коркаури","Лайл","Линдал","Майло","Меррик","Омберт","Осборн","Перрин","Рид",
"Роберк","Роско","Тирраури","Уэллби","Финнан","Халандар","Элдон","Эррих"],
"F": ["Алонирра","Бри","Вани","Венинда","Верна","Делдира","Джиллиан","Калатра",
"Китри","Кора","Кэлли","Лавиния","Лидда","Мелинден","Мерла","Недда","Олпара",
"Паэла","Портия","Росинден","Серафина","Тара","Трим","Шаэна","Эндри","Юфемия"],
"S": ["Аумбль","Брамблфут","Вверхтормашкин","Высокохолм","Галькоброс","Дардаргон",
"Добробочка","Зеленофляг","Кустосбор","Лугодуг","Мерримар","Подветкин",
"Репейник","Старнхап","Хардингдейл","Чайнолист"],
},
"languages": [Common, HalflingLanguage],
},
"Hobgoblin": {
"chance": 0.50,
"names": "Goblin",
"languages": [Common, GoblinLanguage],
},
"Human": {
"chance": 54.70,
"subraces": {
"Calishite": {
"chance": 20.00,
"names": {"M":["Асеир","Бардеид","Зашеир","Кхемед","Мехмен","Судейман","Хасеид"],
"F":["Атала","Джасмаль","Зашеида","Мейлиль","Сейдиль","Сейпора","Хама","Яшеира"],
"S":["Баша","Джассан","Думеин","Кхалид","Мостана","Пашар","Рейн"]},
"languages": [Common, Alzhedo],
},
"Chondathan": {
"chance": 20.00,
"names": {"M":["Горстаг","Грим","Дарвин","Дорн","Маларк","Морн","Рэндал","Стедд","Хельм","Эвендур"],
"F":["Арвин","Джессаиль","Керри","Лурин","Мири","Рован","Тесселе","Шандри","Эсвель"],
"S":["Бакмэн","Грэйкасл","Дандрэгон","Толстаг","Эвенвуд","Эмблкроун"]},
"languages": [Common, Chondathan],
},
"Damaran": {
"chance": 15.00,
"names": {"M":["Айвор","Бор","Глэр","Григор","Иган","Козеф","Миваль","Орел","Павел","Сергор","Фодель"],
"F":["Алетра","Зора","Кара","Катернин","Мара","Натали","Ольма","Тана"],
"S":["Берск","Дотск","Куленов","Марск","Немецк","Стараг","Чернин","Шемов"]},
"languages": [Common, Damaran],
},
"Illuskan": {
"chance": 15.00,
"names": {"M":["Блас","Бран","Гет","Ландер","Лют","Малсер","Стор","Таман","Урт","Фрат","Эндер"],
"F":["Амафрея","Бетха","Вестра","Кетра","Мара","Ольга","Силифрей","Цефрея"],
"S":["Брайтвуд","Виндривер","Лакмэн","Хелдер","Хорнрейвен","Штормвинд"]},
"languages": [Common, Illuskan],
},
"Koryoan": {
"chance": 2.50,
"names": "Shou",
"languages": [Common, KoryoLanguage],
},
"Mulan": {
"chance": 10.00,
"names": {"M":["Аот","Барерис","Кетот","Мумед","Рамас","Со-Кехур","Тхазар-Де","Урхур","ЭхпутКи"],
"F":["Аризима","Золис","Мурити","Нефис","Нулара","Сефрис","Тола","Умара","Чатхи"],
"S":["Анкхалаб","Анскульд","Натандем","Серпет","Уутракт","Фезим","Хахпет"]},
"languages": [Common, Mulani],
},
"Rashemi": {
"chance": 7.50,
"names": {"M":["Боривик","Владислак","Джандар","Канитар","Мадислак","Ральмевик","Фаургар","Шаумар"],
"F":["Имзель","Иммит","Наварра","Таммит","Файварра","Хулмарра","Шеварра","Юльдра"],
"S":["Дайрнина","Илтазяра","Мурнитара","Стаянога","Улмокина","Чергоба"]},
"languages": [Common, Rashemi],
},
"Shou": {
"chance": 2.50,
"names": {"M":["Ан","Вэнь","Лонг","Лянь","Менг","Он","Фай","Цзюн","Цзянь","Чен","Чи","Шань","Шуй"],
"F":["Бай","Ксяо","Лей","Мей","Тай","Цзя","Чао","Шуй"],
"S":["Вань","Као","Кунг","Лао","Линг","Мей","Пинь","Сум","Тань","Хуан","Чиень","Шин"]},
"languages": [Common, Shou],
},
"Tethyrian": {
"chance": 7.50,
"names": "Chondathan",
"languages": [Common, Chondathan],
},
},
},
"Kenku": {
"chance": 0.50,
"names": {
"M": ["Разбиватель","Лязгун","Разрезатель","Грохотун","Крысиный Скрежет","Свистун","Мышатник","Рычун"],
"F": ["Свистун","Мышатник","Рычун"],
},
"languages": [Common, Primordial],
},
"Kobold": {"chance": 0.50, "names": [], "languages": [Common, Draconic]},
"Leonin": {
"chance": 0.10,
"names": {
"M": ["Апто","Ат","Барагон","Брыг","Горий","Грекс","Земн","Зиор","Орий","Пиксатор","Тей","Эрем"],
"F": ["Алета","Атагона","Грагонда","Демна","Доксия","Зиора","Иадма","Койла","Орамна","Сеза","Эката","Эриз"],
},
"languages": [Common, LeoninLanguage],
},
"Lizardfolk": {"chance": 0.50, "names": [], "languages": [Common, Draconic]},
"Orc": {
"chance": 2.00,
"names": {
"M": ["Абзуг","Бажок","Беск","Готог","Груток","Джахрук","Дурт","Кеск","Лортар","Мобад",
"Мугруб","Орруск","Рук","Рхог","Тараг","Тог","Угурт","Урзул","Фанг","Харл","Шамок","Шугог"],
"F": ["Багги","Бетарра","Варра","Вола","Волен","Дуваега","Имен","Йескарра","Кансиф","Креске",
"Муай","Неске","Ниига","Овак","Орваега","Оунка","Сута","Шаута","Эдарреске","Энгонг"],
"S": ["Думник","Хортор","Ламмар","Сормужик","Турнчерпал","Улькруннар","Зоргар"],
},
"languages": [Common, OrcLanguage],
},
"Satyr": {"chance": 0.10, "names": [], "languages": [Common, Sylvan]},
"Tabaxi": {"chance": 0.50, "names": [], "languages": [Common, living_languages_except_common]},
"Tiefling": {
"chance": 1.00,
"names": {
"M": ["Акменос","Амнон","Баракас","Дамакос","Йадос","Кайрон","Люцис","Мелех","Мордай",
"Мортос","Пелайос","Скамос","Терай","Экемон"],
"F": ["Акта","Анакис","Брисеис","Дамая","Каллиста","Криелла","Лерисса","Макария",
"Немея","Орианна","Риета","Фелая","Эа"],
"S": ["Амезис","Барбатос","Дракос","Инфернус","Мефисто","Фирнас","Хекатон","Харгрейв","Эбони"],
},
"languages": [Common, Infernal],
},
"Triton": {
"chance": 0.10,
"names": {
"M": ["Водос","Делнис","Джимас","Зунис","Керос","Корус","Молос","Налос"],
"F": ["Белтин","Вуолин","Влэрин","Дьютин","Отанин","Филорин","Шелрин","Эрин"],
},
"languages": [Common, Primordial],
},
"Yuan-ti Pureblood": {
"chance": 0.10,
"names": [],
"languages": [Common, Abyssal, Draconic],
},
}
# ─── Вспомогательные функции ─────────────────────────────────────────────────
def _weighted_choice(items: list[tuple[str, float]]) -> str:
"""items = [(name, chance), ...]. Возвращает выбранное name."""
total = sum(c for _, c in items)
r = random.random() * total
cumulative = 0.0
for name, chance in items:
cumulative += chance
if r <= cumulative:
return name
return items[-1][0]
def select_random_race_and_subrace() -> dict:
"""Возвращает {"race": str, "subrace": str | None}."""
race_items = [(r, d["chance"]) for r, d in RACES.items()]
race_name = _weighted_choice(race_items)
race_data = RACES[race_name]
subrace_name = None
if "subraces" in race_data and race_data["subraces"]:
sub_items = [(s, d["chance"]) for s, d in race_data["subraces"].items()]
subrace_name = _weighted_choice(sub_items)
return {"race": race_name, "subrace": subrace_name}
def get_name(race_obj: dict, gender: str) -> dict:
"""Возвращает {"name": str, "race": str, "subrace": str | None}."""
gender_key = gender.upper()
race_key = race_obj["race"]
subrace_key = race_obj.get("subrace")
def resolve_names(r_key: str, s_key: str | None):
"""Рекурсивно разрешает ссылки на имена, возвращает (nameData, source_race, source_subrace)."""
race_data = RACES.get(r_key)
if not race_data:
return None, None, None
# Определяем имена
names = None
is_race_level_ref = False
if s_key and "subraces" in race_data and s_key in race_data["subraces"]:
sub_data = race_data["subraces"][s_key]
if "names" in sub_data:
names = sub_data["names"]
else:
names = race_data.get("names")
is_race_level_ref = True
else:
if "subraces" in race_data and race_data["subraces"]:
# Выбираем случайную подрасу с именами
for candidate in random.sample(list(race_data["subraces"].keys()),
len(race_data["subraces"])):
if "names" in race_data["subraces"][candidate]:
names = race_data["subraces"][candidate]["names"]
s_key = candidate
break
if names is None:
names = race_data.get("names")
is_race_level_ref = True
else:
names = race_data.get("names")
is_race_level_ref = True
if names is None:
return None, None, None
# Строка-ссылка
if isinstance(names, str):
if is_race_level_ref:
return resolve_names(names, None)
else:
return resolve_names(r_key, names)
# Список ссылок (Half-Elf: ["Human", "Elf"])
if isinstance(names, list) and names and isinstance(names[0], str):
ref = random.choice(names)
return resolve_names(ref, None)
# Прямой словарь {"M":[], "F":[], "S":[]}
if isinstance(names, dict):
name_data: dict[str, list] = {"M": [], "F": [], "S": []}
for g in ("M", "F", "S"):
if g in names:
name_data[g] = list(names[g])
return name_data, r_key, s_key
return None, None, None
name_data, src_race, src_subrace = resolve_names(race_key, subrace_key)
if not name_data:
return {"name": "", "race": race_key, "subrace": subrace_key}
first_names = name_data.get(gender_key, [])
if not first_names:
first_names = name_data.get("F" if gender_key == "M" else "M", [])
if not first_names:
return {"name": "", "race": src_race, "subrace": src_subrace}
surname_list = name_data.get("S", [])
first_name = random.choice(first_names)
surname = random.choice(surname_list) if surname_list else ""
return {
"name": f"{first_name} {surname}".strip(),
"race": src_race,
"subrace": src_subrace,
}
def get_languages(race_obj: dict, full_name_obj: dict) -> list[dict]:
"""Возвращает список языков для расы/подрасы персонажа."""
race_key = race_obj["race"]
subrace_key = race_obj.get("subrace")
def get_lang_list(r_key: str, s_key: str | None) -> list:
r_data = RACES.get(r_key)
if not r_data:
return []
if s_key and "subraces" in r_data and s_key in r_data["subraces"]:
sub = r_data["subraces"][s_key]
if "languages" in sub:
return sub["languages"]
return r_data.get("languages", [])
lang_list = get_lang_list(race_key, subrace_key)
languages: list[dict] = []
for item in lang_list:
if isinstance(item, list):
# Массив языков на выбор — выбираем один случайный, не совпадающий с уже добавленными
available = [l for l in item if l not in languages]
if available:
languages.append(random.choice(available))
else:
languages.append(item)
# Дедупликация с сохранением порядка
seen = []
for lang in languages:
if lang not in seen:
seen.append(lang)
return seen