# This is a combination of 2 commits.
# This is the 1st commit message: Rewrite! This is a full rewrite of the npc generator code to use the newly-refactored dnd-name-generator code # This is the commit message #2: bugfixen
This commit is contained in:
parent
2e5b5556a3
commit
1208616ab3
|
@ -0,0 +1,26 @@
|
||||||
|
import importlib
|
||||||
|
import os
|
||||||
|
import pkgutil
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from types import ModuleType
|
||||||
|
|
||||||
|
ancestry_pack = None
|
||||||
|
supported_ancestries = None
|
||||||
|
|
||||||
|
|
||||||
|
def _import_submodules(module):
|
||||||
|
pkgs = pkgutil.iter_modules(module.__path__)
|
||||||
|
for loader, module_name, is_pkg in pkgs:
|
||||||
|
yield importlib.import_module(f"{module.__name__}.{module_name}")
|
||||||
|
|
||||||
|
|
||||||
|
def load_ancestry_pack(module_name: str = "") -> ModuleType:
|
||||||
|
if not module_name:
|
||||||
|
module_name = os.getenv("NPC_ANCESTRY_PACK", "npc.ancestries")
|
||||||
|
ancestry_pack = importlib.import_module(module_name)
|
||||||
|
_import_submodules(ancestry_pack)
|
||||||
|
supported_ancestries = dict(
|
||||||
|
(module.__name__.split(".")[-1], module) for module in list(_import_submodules(sys.modules[module_name]))
|
||||||
|
)
|
||||||
|
return ancestry_pack, supported_ancestries
|
|
@ -1,45 +1,29 @@
|
||||||
from npc.languages import draconic
|
from functools import cached_property
|
||||||
from npc.generator.base import BaseNPC, a_or_an
|
from npc import types
|
||||||
from npc.generator import traits
|
|
||||||
import textwrap
|
import textwrap
|
||||||
import random
|
import random
|
||||||
|
|
||||||
|
from language.languages import draconic
|
||||||
|
|
||||||
class NPC(BaseNPC):
|
|
||||||
|
|
||||||
ancestry = 'Dragon'
|
class Dragon(types.NPC):
|
||||||
language = draconic.Dragon()
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
language = draconic
|
||||||
super().__init__(*args, **kwargs)
|
is_noble = True
|
||||||
|
|
||||||
self._tail = None
|
has_tail = True
|
||||||
self._horns = None
|
has_horns = True
|
||||||
self._fangs = None
|
has_fangs = True
|
||||||
self._wings = None
|
has_wings = True
|
||||||
|
|
||||||
@property
|
|
||||||
def nickname(self):
|
|
||||||
if not self._nickname:
|
|
||||||
self._nickname = "the " + random.choice(traits.personality)
|
|
||||||
return self._nickname
|
|
||||||
|
|
||||||
@property
|
@cached_property
|
||||||
def age(self):
|
def age(self) -> str:
|
||||||
if not self._age:
|
return random.choice(['wyrmling', 'young', 'adult', 'ancient'])
|
||||||
self._age = random.choice([
|
|
||||||
'wyrmling',
|
|
||||||
'young',
|
|
||||||
'adult',
|
|
||||||
'ancient',
|
|
||||||
])
|
|
||||||
return self._age
|
|
||||||
|
|
||||||
@property
|
@cached_property
|
||||||
def pronouns(self):
|
def pronouns(self) -> str:
|
||||||
if not self._pronouns:
|
return 'they/they'
|
||||||
self._pronouns = 'they/they'
|
|
||||||
return self._pronouns
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def skin_color(self):
|
def skin_color(self):
|
||||||
|
@ -68,8 +52,8 @@ class NPC(BaseNPC):
|
||||||
self.facial_structure,
|
self.facial_structure,
|
||||||
])
|
])
|
||||||
return (
|
return (
|
||||||
f"{self.full_name} ({self.pronouns}) is {a_or_an(self.age)} {self.age} {self.skin_color} "
|
f"{self.name} ({self.pronouns}) is {types.a_or_an(self.age)} {self.age} {self.skin_color} "
|
||||||
f"{self.ancestry.lower()} with {a_or_an(self.nose)} {self.nose} snout, {self.body} body and {trait}."
|
f"{self.ancestry.lower()} with {types.a_or_an(self.nose)} {self.nose} snout, {self.body} body and {trait}."
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -99,3 +83,6 @@ Goal: {self.goal}
|
||||||
Whereabouts: {self.whereabouts}
|
Whereabouts: {self.whereabouts}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
NPC = Dragon
|
9
npc/ancestries/drow.py
Normal file
9
npc/ancestries/drow.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from language.languages import undercommon
|
||||||
|
from npc import types
|
||||||
|
|
||||||
|
|
||||||
|
class Drow(types.NPC):
|
||||||
|
language = undercommon
|
||||||
|
|
||||||
|
|
||||||
|
NPC = Drow
|
9
npc/ancestries/dwarf.py
Normal file
9
npc/ancestries/dwarf.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from language.languages import dwarvish
|
||||||
|
from npc import types
|
||||||
|
|
||||||
|
|
||||||
|
class Dwarf(types.NPC):
|
||||||
|
language = dwarvish
|
||||||
|
|
||||||
|
|
||||||
|
NPC = Dwarf
|
9
npc/ancestries/elf.py
Normal file
9
npc/ancestries/elf.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from language.languages import elvish
|
||||||
|
from npc import types
|
||||||
|
|
||||||
|
|
||||||
|
class Elf(types.NPC):
|
||||||
|
language = elvish
|
||||||
|
|
||||||
|
|
||||||
|
NPC = Elf
|
9
npc/ancestries/halfling.py
Normal file
9
npc/ancestries/halfling.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from language.languages import halfling
|
||||||
|
from npc import types
|
||||||
|
|
||||||
|
|
||||||
|
class Halfling(types.NPC):
|
||||||
|
language = halfling
|
||||||
|
|
||||||
|
|
||||||
|
NPC = Halfling
|
9
npc/ancestries/halforc.py
Normal file
9
npc/ancestries/halforc.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from language.languages import orcish
|
||||||
|
from npc import types
|
||||||
|
|
||||||
|
|
||||||
|
class HalfOrc(types.NPC):
|
||||||
|
language = orcish
|
||||||
|
|
||||||
|
|
||||||
|
NPC = HalfOrc
|
9
npc/ancestries/human.py
Normal file
9
npc/ancestries/human.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from language.languages import common
|
||||||
|
from npc import types
|
||||||
|
|
||||||
|
|
||||||
|
class Human(types.NPC):
|
||||||
|
language = common
|
||||||
|
|
||||||
|
|
||||||
|
NPC = Human
|
|
@ -1,22 +1,17 @@
|
||||||
from npc.languages import lizardfolk
|
from functools import cached_property
|
||||||
from npc.generator.base import BaseNPC, a_or_an
|
|
||||||
|
|
||||||
import textwrap
|
import textwrap
|
||||||
import random
|
import random
|
||||||
|
|
||||||
|
from language.languages import lizardfolk
|
||||||
|
from npc import types
|
||||||
|
|
||||||
class NPC(BaseNPC):
|
|
||||||
|
|
||||||
ancestry = 'Lizardfolk'
|
class Lizardfolk(types.NPC):
|
||||||
language = lizardfolk.Lizardfolk()
|
language = lizardfolk
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
has_tail = True
|
||||||
super().__init__(*args, **kwargs)
|
has_horns = True
|
||||||
|
has_fangs = True
|
||||||
self._tail = None
|
|
||||||
self._horns = None
|
|
||||||
self._fangs = None
|
|
||||||
self._frills = None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def age(self):
|
def age(self):
|
||||||
|
@ -38,19 +33,17 @@ class NPC(BaseNPC):
|
||||||
self._tail = 'no'
|
self._tail = 'no'
|
||||||
return self._tail
|
return self._tail
|
||||||
|
|
||||||
@property
|
@cached_property
|
||||||
def frills(self):
|
def frills(self):
|
||||||
if not self._frills:
|
if self.age in ('adult', 'ancient'):
|
||||||
if self.age in ('adult', 'ancient'):
|
return random.choice([
|
||||||
self._frills = random.choice([
|
'orange',
|
||||||
'orange',
|
'red',
|
||||||
'red',
|
'yellow',
|
||||||
'yellow',
|
'green',
|
||||||
'green',
|
'blue',
|
||||||
'blue',
|
'silvery',
|
||||||
'silvery',
|
])
|
||||||
])
|
|
||||||
return self._frills
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def skin_color(self):
|
def skin_color(self):
|
||||||
|
@ -76,8 +69,8 @@ class NPC(BaseNPC):
|
||||||
self.facial_structure,
|
self.facial_structure,
|
||||||
])
|
])
|
||||||
return (
|
return (
|
||||||
f"{self.full_name} ({self.pronouns}) is {a_or_an(self.age)} {self.age}, {self.skin_color}-scaled "
|
f"{self.fullname} ({self.pronouns}) is {types.a_or_an(self.age)} {self.age}, {self.skin_color}-scaled "
|
||||||
f"{self.ancestry.lower()} with {a_or_an(self.nose)} {self.nose} snout, {self.body} body and {trait}."
|
f"{self.ancestry.lower()} with {types.a_or_an(self.nose)} {self.nose} snout, {self.body} body and {trait}."
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -106,3 +99,6 @@ Goal: {self.goal}
|
||||||
Whereabouts: {self.whereabouts}
|
Whereabouts: {self.whereabouts}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
NPC = Lizardfolk
|
32
npc/ancestries/tiefling.py
Normal file
32
npc/ancestries/tiefling.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import random
|
||||||
|
|
||||||
|
from language.languages import infernal
|
||||||
|
from npc import types
|
||||||
|
|
||||||
|
|
||||||
|
class Tiefling(types.NPC):
|
||||||
|
language = infernal
|
||||||
|
|
||||||
|
has_tail = True
|
||||||
|
has_horns = True
|
||||||
|
has_fangs = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def skin_color(self):
|
||||||
|
if not self._skin_color:
|
||||||
|
self._skin_color = random.choice([
|
||||||
|
'reddish',
|
||||||
|
'white',
|
||||||
|
'green',
|
||||||
|
'black',
|
||||||
|
'blue',
|
||||||
|
'brassy',
|
||||||
|
'bronze',
|
||||||
|
'coppery',
|
||||||
|
'silvery',
|
||||||
|
'gold',
|
||||||
|
])
|
||||||
|
return self._skin_color
|
||||||
|
|
||||||
|
|
||||||
|
NPC = Tiefling
|
230
npc/cli.py
230
npc/cli.py
|
@ -1,172 +1,104 @@
|
||||||
from npc.generator.base import generate_npc, npc_type
|
from npc import load_ancestry_pack
|
||||||
from npc import languages
|
|
||||||
|
|
||||||
import random
|
|
||||||
import typer
|
import logging
|
||||||
|
import os
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
import typer
|
||||||
from rich import print
|
from rich import print
|
||||||
|
from rich.logging import RichHandler
|
||||||
|
|
||||||
|
|
||||||
class Ancestry(str, Enum):
|
from language import load_language_pack
|
||||||
dragon = 'dragon'
|
|
||||||
drow = 'drow'
|
|
||||||
dwarf = 'dwarf'
|
|
||||||
elf = 'elf'
|
|
||||||
halfling = 'halfling'
|
|
||||||
halforc = 'halforc'
|
|
||||||
highelf = 'highelf'
|
|
||||||
highttiefling = 'hightiefling'
|
|
||||||
human = 'human'
|
|
||||||
tiefling = 'tiefling'
|
|
||||||
lizardfolk = 'lizardfolk'
|
|
||||||
|
|
||||||
|
|
||||||
class Language(str, Enum):
|
|
||||||
abyssal = 'abyssal'
|
|
||||||
celestial = 'celestial'
|
|
||||||
common = 'commmon'
|
|
||||||
draconic = 'draconic'
|
|
||||||
dwarvish = 'dwarvish'
|
|
||||||
elven = 'elven'
|
|
||||||
gnomish = 'gnomish'
|
|
||||||
halfling = 'halfing'
|
|
||||||
infernal = 'infernal'
|
|
||||||
orcish = 'orcish'
|
|
||||||
undercommon = 'undercommon'
|
|
||||||
lizardfolk = 'lizardfolk'
|
|
||||||
|
|
||||||
|
|
||||||
app = typer.Typer()
|
app = typer.Typer()
|
||||||
|
|
||||||
|
app_state = {}
|
||||||
|
|
||||||
|
language_pack, supported_languages = load_language_pack()
|
||||||
|
SupportedLanguages = Enum("SupportedLanguages", ((k, k) for k in supported_languages.keys()))
|
||||||
|
|
||||||
|
ancestry_pack, supported_ancestries = load_ancestry_pack()
|
||||||
|
SupportedAncestries = Enum("SupportedAncestries", ((k, k) for k in supported_ancestries.keys()))
|
||||||
|
|
||||||
|
|
||||||
|
def get_npc(**kwargs):
|
||||||
|
return app_state['ancestry'].NPC(language=app_state['language'], **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@app.callback(invoke_without_command=True)
|
||||||
|
def main(
|
||||||
|
ctx: typer.Context,
|
||||||
|
ancestry: SupportedAncestries = typer.Option(
|
||||||
|
default="human",
|
||||||
|
help="The ancestry to use."
|
||||||
|
),
|
||||||
|
language: Union[SupportedLanguages, None] = typer.Option(
|
||||||
|
default=None,
|
||||||
|
help="The language to use. Will be derived from ancestry if not specified."
|
||||||
|
),
|
||||||
|
verbose: bool = typer.Option(
|
||||||
|
default=False,
|
||||||
|
help="If True, print verbose character descriptions."
|
||||||
|
)
|
||||||
|
):
|
||||||
|
app_state["ancestry"] = supported_ancestries[ancestry.name]
|
||||||
|
if language:
|
||||||
|
app_state["language"] = supported_languages[language.name]
|
||||||
|
else:
|
||||||
|
app_state["language"] = None
|
||||||
|
|
||||||
|
debug = os.getenv("NPC_DEBUG", None)
|
||||||
|
logging.basicConfig(
|
||||||
|
format="%(name)s %(message)s",
|
||||||
|
level=logging.DEBUG if debug else logging.INFO,
|
||||||
|
handlers=[RichHandler(rich_tracebacks=True, tracebacks_suppress=[typer])],
|
||||||
|
)
|
||||||
|
logging.debug(f"Loaded ancestry pack {ancestry_pack}.")
|
||||||
|
logging.debug(f"Loaded language pack {language_pack}.")
|
||||||
|
|
||||||
|
app_state['verbose'] = verbose
|
||||||
|
|
||||||
|
if ctx.invoked_subcommand is None:
|
||||||
|
return commoner()
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def npc(
|
def commoner() -> None:
|
||||||
ancestry: Ancestry = typer.Option(
|
|
||||||
None,
|
|
||||||
help='Derive NPC characteristics from a specific ancestry. Randomized if not specified.',
|
|
||||||
),
|
|
||||||
name: str = typer.Option(
|
|
||||||
None,
|
|
||||||
help='Specify the NPC name. Randomized names are derived from ancestry',
|
|
||||||
),
|
|
||||||
pronouns: str = typer.Option(
|
|
||||||
None,
|
|
||||||
help='Specify the NPC pronouns.',
|
|
||||||
),
|
|
||||||
title: str = typer.Option(
|
|
||||||
None,
|
|
||||||
help='Specify the NPC title.',
|
|
||||||
),
|
|
||||||
nickname: str = typer.Option(
|
|
||||||
None,
|
|
||||||
help='Specify the NPC nickname.',
|
|
||||||
),
|
|
||||||
whereabouts: str = typer.Option(
|
|
||||||
None,
|
|
||||||
help='Specify the NPC whereabouts.',
|
|
||||||
),
|
|
||||||
STR: str = typer.Option(
|
|
||||||
None,
|
|
||||||
help='Specify the NPC strength score.',
|
|
||||||
),
|
|
||||||
DEX: str = typer.Option(
|
|
||||||
None,
|
|
||||||
help='Specify the NPC dexterity score.',
|
|
||||||
),
|
|
||||||
CON: str = typer.Option(
|
|
||||||
None,
|
|
||||||
help='Specify the NPC constitution score.',
|
|
||||||
),
|
|
||||||
INT: str = typer.Option(
|
|
||||||
None,
|
|
||||||
help='Specify the NPC intelligence score.',
|
|
||||||
),
|
|
||||||
WIS: str = typer.Option(
|
|
||||||
None,
|
|
||||||
help='Specify the NPC wisdom score.',
|
|
||||||
),
|
|
||||||
CHA: str = typer.Option(
|
|
||||||
None,
|
|
||||||
help='Specify the NPC charisma score.',
|
|
||||||
),
|
|
||||||
randomize: bool = typer.Option(
|
|
||||||
False,
|
|
||||||
help='If True, randomize default stat scores. If False, all stats are 10.'
|
|
||||||
),
|
|
||||||
) -> None:
|
|
||||||
"""
|
"""
|
||||||
Generate a basic NPC.
|
Generate a basic NPC.
|
||||||
"""
|
"""
|
||||||
print(generate_npc(
|
char = get_npc()
|
||||||
ancestry=ancestry,
|
if app_state['verbose']:
|
||||||
names=name.split() if name else [],
|
print(char.character_sheet)
|
||||||
pronouns=pronouns,
|
else:
|
||||||
title=title,
|
print(char)
|
||||||
nickname=nickname,
|
|
||||||
whereabouts=whereabouts,
|
|
||||||
STR=STR,
|
|
||||||
DEX=DEX,
|
|
||||||
CON=CON,
|
|
||||||
INT=INT,
|
|
||||||
WIS=WIS,
|
|
||||||
CHA=CHA,
|
|
||||||
randomize=randomize
|
|
||||||
).character_sheet)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def names(ancestry: Ancestry = typer.Option(
|
def adventurer() -> None:
|
||||||
None,
|
"""
|
||||||
help='Derive NPC characteristics from a specific ancestry. Randomized if not specified.',
|
Generate a basic NPC.
|
||||||
),
|
"""
|
||||||
count: int = typer.Option(
|
char = get_npc(randomize=True)
|
||||||
1,
|
if app_state['verbose']:
|
||||||
help='How many names to generate.'
|
print(char.character_sheet)
|
||||||
),
|
else:
|
||||||
) -> None:
|
print(char)
|
||||||
for _ in range(int(count)):
|
|
||||||
print(npc_type(ancestry)().full_name)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def text(
|
def noble() -> None:
|
||||||
language: Language = typer.Option(
|
"""
|
||||||
'common',
|
Generate a basic NPC.
|
||||||
help='The language for which to generate text.',
|
"""
|
||||||
),
|
char = get_npc(randomize=True, noble=True)
|
||||||
count: int = typer.Argument(
|
if app_state['verbose']:
|
||||||
50,
|
print(char.character_sheet)
|
||||||
help='How many words to generate.'
|
else:
|
||||||
),
|
print(char)
|
||||||
) -> None:
|
|
||||||
mod = getattr(languages, language, None)
|
|
||||||
if not mod:
|
|
||||||
print(f'Unsupported Language: {language}.')
|
|
||||||
return
|
|
||||||
lang_class = getattr(mod, language.capitalize(), None)
|
|
||||||
if not lang_class:
|
|
||||||
print(f'Unsupported Language: {language} in {mod}.')
|
|
||||||
return
|
|
||||||
lang = lang_class()
|
|
||||||
|
|
||||||
phrases = []
|
|
||||||
phrase = []
|
|
||||||
for word in [lang.word() for _ in range(int(count))]:
|
|
||||||
phrase.append(str(word))
|
|
||||||
if len(phrase) >= random.randint(1, 12):
|
|
||||||
phrases.append(' '.join(phrase))
|
|
||||||
phrase = []
|
|
||||||
if phrase:
|
|
||||||
phrases.append(' '.join(phrase))
|
|
||||||
|
|
||||||
paragraph = phrases[0].capitalize()
|
|
||||||
for phrase in phrases[1:]:
|
|
||||||
if random.choice([0, 0, 1]):
|
|
||||||
paragraph = paragraph + random.choice('?!.') + ' ' + phrase.capitalize()
|
|
||||||
else:
|
|
||||||
paragraph = paragraph + ', ' + phrase
|
|
||||||
print(f"{paragraph}.")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
from npc.languages import undercommon
|
|
||||||
from npc.generator.base import BaseNPC
|
|
||||||
|
|
||||||
|
|
||||||
class NPC(BaseNPC):
|
|
||||||
|
|
||||||
ancestry = 'Drow'
|
|
||||||
language = undercommon.DrowPerson()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def full_name(self):
|
|
||||||
return ' '.join([
|
|
||||||
str(self.names[0]).capitalize(),
|
|
||||||
str(self.names[1]).capitalize()
|
|
||||||
])
|
|
|
@ -1,12 +0,0 @@
|
||||||
from npc.languages import dwarvish
|
|
||||||
from npc.generator.base import BaseNPC
|
|
||||||
|
|
||||||
|
|
||||||
class NPC(BaseNPC):
|
|
||||||
|
|
||||||
ancestry = 'Dwarf'
|
|
||||||
language = dwarvish.Dwarvish()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def full_name(self):
|
|
||||||
return ' '.join([str(x).capitalize() for x in self.language.person()])
|
|
|
@ -1,16 +0,0 @@
|
||||||
from npc.languages import elven
|
|
||||||
from npc.generator.base import BaseNPC
|
|
||||||
|
|
||||||
|
|
||||||
class NPC(BaseNPC):
|
|
||||||
|
|
||||||
ancestry = 'Elf'
|
|
||||||
language = elven.ElvenPerson()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def full_name(self):
|
|
||||||
return ' '.join([
|
|
||||||
str(self.names[0]).capitalize(),
|
|
||||||
str(self.names[1]).lower(),
|
|
||||||
str(self.names[2]).capitalize()
|
|
||||||
])
|
|
|
@ -1,10 +0,0 @@
|
||||||
import random
|
|
||||||
|
|
||||||
from npc.languages import halfling
|
|
||||||
from npc.generator.base import BaseNPC
|
|
||||||
|
|
||||||
|
|
||||||
class NPC(BaseNPC):
|
|
||||||
|
|
||||||
ancestry = 'Halfling'
|
|
||||||
language = halfling.Halfling()
|
|
|
@ -1,12 +0,0 @@
|
||||||
from npc.languages import orcish
|
|
||||||
from npc.generator.base import BaseNPC
|
|
||||||
|
|
||||||
|
|
||||||
class NPC(BaseNPC):
|
|
||||||
|
|
||||||
ancestry = 'Half-Orc'
|
|
||||||
language = orcish.HalfOrcPerson()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def full_name(self):
|
|
||||||
return ' '.join([str(x).capitalize() for x in self.language.person()])
|
|
|
@ -1,16 +0,0 @@
|
||||||
from npc.languages import elven
|
|
||||||
from npc.generator.base import BaseNPC
|
|
||||||
|
|
||||||
|
|
||||||
class NPC(BaseNPC):
|
|
||||||
|
|
||||||
ancestry = 'Elf'
|
|
||||||
language = elven.HighElvenPerson()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def full_name(self):
|
|
||||||
return ' '.join([
|
|
||||||
str(self.names[0]).capitalize(),
|
|
||||||
str(self.names[1]).lower(),
|
|
||||||
str(self.names[2]).capitalize()
|
|
||||||
])
|
|
|
@ -1,6 +0,0 @@
|
||||||
from npc.languages import infernal
|
|
||||||
from npc.generator import tiefling
|
|
||||||
|
|
||||||
|
|
||||||
class NPC(tiefling.NPC):
|
|
||||||
language = infernal.HighTiefling()
|
|
|
@ -1,8 +0,0 @@
|
||||||
from language.languages.common import common_name
|
|
||||||
from npc.generator.base import BaseNPC
|
|
||||||
|
|
||||||
|
|
||||||
class NPC(BaseNPC):
|
|
||||||
|
|
||||||
ancestry = 'Human'
|
|
||||||
name_generator = common_name
|
|
|
@ -1,39 +0,0 @@
|
||||||
from npc.languages import infernal
|
|
||||||
from npc.generator.base import BaseNPC
|
|
||||||
import random
|
|
||||||
|
|
||||||
|
|
||||||
class NPC(BaseNPC):
|
|
||||||
ancestry = 'Tiefling'
|
|
||||||
language = infernal.Tiefling()
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self._tail = None
|
|
||||||
self._horns = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def skin_color(self):
|
|
||||||
if not self._skin_color:
|
|
||||||
self._skin_color = random.choice([
|
|
||||||
'reddish',
|
|
||||||
'white',
|
|
||||||
'green',
|
|
||||||
'black',
|
|
||||||
'blue',
|
|
||||||
'brassy',
|
|
||||||
'bronze',
|
|
||||||
'coppery',
|
|
||||||
'silvery',
|
|
||||||
'gold',
|
|
||||||
])
|
|
||||||
return self._skin_color
|
|
||||||
|
|
||||||
@property
|
|
||||||
def full_name(self):
|
|
||||||
name = ' '.join([n.capitalize() for n in self.names])
|
|
||||||
if self.title:
|
|
||||||
name = self.title.capitalize() + ' ' + name
|
|
||||||
if self.nickname:
|
|
||||||
name = name + ' ' + self.nickname.capitalize()
|
|
||||||
return name
|
|
|
@ -1,27 +0,0 @@
|
||||||
from npc.languages import abyssal
|
|
||||||
from npc.languages import celestial
|
|
||||||
from npc.languages import common
|
|
||||||
from npc.languages import draconic
|
|
||||||
from npc.languages import dwarvish
|
|
||||||
from npc.languages import elven
|
|
||||||
from npc.languages import gnomish
|
|
||||||
from npc.languages import halfling
|
|
||||||
from npc.languages import infernal
|
|
||||||
from npc.languages import orcish
|
|
||||||
from npc.languages import undercommon
|
|
||||||
|
|
||||||
|
|
||||||
__ALL__ = [
|
|
||||||
'abyssal',
|
|
||||||
'base',
|
|
||||||
'celestial',
|
|
||||||
'common',
|
|
||||||
'draconic',
|
|
||||||
'dwarvish',
|
|
||||||
'elven',
|
|
||||||
'gnomish',
|
|
||||||
'halfling',
|
|
||||||
'infernal',
|
|
||||||
'orcish',
|
|
||||||
'undercommon',
|
|
||||||
]
|
|
|
@ -1,32 +0,0 @@
|
||||||
from npc.languages.base import BaseLanguage
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
class Abyssal(BaseLanguage):
|
|
||||||
|
|
||||||
vowels = ['a', 'e', 'i', 'o', 'u', 'î', 'ê', 'â', 'û', 'ô', 'ä', 'ö', 'ü', 'äu', 'ȧ', 'ė', 'ị', 'ȯ', 'u̇']
|
|
||||||
|
|
||||||
consonants = ['c', 'g', 'j', 'k', 'p', 'ss', 't']
|
|
||||||
|
|
||||||
syllable_template = ('V', 'v', 'c', 'v')
|
|
||||||
|
|
||||||
_invalid_sequences = re.compile(
|
|
||||||
r'[' + ''.join(vowels) + ']{5}|' +
|
|
||||||
r'[' + ''.join(consonants) + ']{3}'
|
|
||||||
)
|
|
||||||
|
|
||||||
syllable_weights = [3, 2]
|
|
||||||
|
|
||||||
minimum_length = 2
|
|
||||||
|
|
||||||
def validate_sequence(self, sequence, total_syllables):
|
|
||||||
too_short = len(''.join(sequence)) < self.minimum_length
|
|
||||||
if too_short:
|
|
||||||
return False
|
|
||||||
|
|
||||||
t = ''.join(sequence)
|
|
||||||
|
|
||||||
if self._invalid_sequences.match(t):
|
|
||||||
self._logger.debug(f"Invalid sequence: {t}")
|
|
||||||
return False
|
|
||||||
return True
|
|
|
@ -1,194 +0,0 @@
|
||||||
import random
|
|
||||||
import logging
|
|
||||||
from collections import namedtuple
|
|
||||||
|
|
||||||
grapheme = namedtuple('Grapheme', ['char', 'weight'])
|
|
||||||
|
|
||||||
|
|
||||||
class LanguageException(Exception):
|
|
||||||
"""
|
|
||||||
Thrown when language validators fail.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class SyllableFactory:
|
|
||||||
|
|
||||||
def __init__(self, template, weights, prefixes, vowels, consonants, suffixes, affixes):
|
|
||||||
self.template = template
|
|
||||||
self.weights = weights
|
|
||||||
self.grapheme = {
|
|
||||||
'chars': {
|
|
||||||
'p': [x.char for x in prefixes],
|
|
||||||
'c': [x.char for x in consonants],
|
|
||||||
'v': [x.char for x in vowels],
|
|
||||||
's': [x.char for x in suffixes],
|
|
||||||
'a': [x.char for x in affixes]
|
|
||||||
},
|
|
||||||
'weights': {
|
|
||||||
'p': [x.weight for x in prefixes],
|
|
||||||
'c': [x.weight for x in consonants],
|
|
||||||
'v': [x.weight for x in vowels],
|
|
||||||
's': [x.weight for x in suffixes],
|
|
||||||
'a': [x.weight for x in affixes]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def _filtered_graphemes(self, key):
|
|
||||||
return [(k, v) for (k, v) in self.grapheme['chars'].items() if k in key]
|
|
||||||
|
|
||||||
def graphemes(self, key='apcvs'):
|
|
||||||
for _, chars in self._filtered_graphemes(key):
|
|
||||||
for char in chars:
|
|
||||||
yield char
|
|
||||||
|
|
||||||
def is_valid(self, chars, key='apcvs'):
|
|
||||||
for grapheme_type, _ in self._filtered_graphemes(key):
|
|
||||||
if chars in self.grapheme['chars'][grapheme_type]:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
"""
|
|
||||||
Generate a single syllable
|
|
||||||
"""
|
|
||||||
syllable = ''
|
|
||||||
for t in self.template:
|
|
||||||
if t.islower() and random.random() < 0.5:
|
|
||||||
continue
|
|
||||||
if '|' in t:
|
|
||||||
t = random.choice(t.split('|'))
|
|
||||||
t = t.lower()
|
|
||||||
syllable = syllable + random.choices(self.grapheme['chars'][t], self.grapheme['weights'][t])[0]
|
|
||||||
return syllable
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.get()
|
|
||||||
|
|
||||||
|
|
||||||
class WordFactory:
|
|
||||||
|
|
||||||
def __init__(self, language):
|
|
||||||
self.language = language
|
|
||||||
|
|
||||||
def random_syllable_count(self):
|
|
||||||
return 1 + random.choices(range(len(self.language.syllable.weights)), self.language.syllable.weights)[0]
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
|
|
||||||
total_syllables = self.random_syllable_count()
|
|
||||||
seq = []
|
|
||||||
while not self.language.validate_sequence(seq, total_syllables):
|
|
||||||
seq = [self.language.syllable.get()]
|
|
||||||
while len(seq) < total_syllables - 2:
|
|
||||||
seq.append(self.language.syllable.get())
|
|
||||||
if len(seq) < total_syllables:
|
|
||||||
seq.append(self.language.syllable.get())
|
|
||||||
return ''.join(seq)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.get()
|
|
||||||
|
|
||||||
|
|
||||||
class BaseLanguage:
|
|
||||||
"""
|
|
||||||
Words are created by combining syllables selected from random phonemes according to templates, each containing one
|
|
||||||
or more of the following grapheme indicators:
|
|
||||||
|
|
||||||
c - an optional consonant
|
|
||||||
C - a required consonant
|
|
||||||
v - an optional vowel
|
|
||||||
V - a required consonant
|
|
||||||
|
|
||||||
The simplest possible syllable consists of a single grapheme, and the simplest possible word a single syllable.
|
|
||||||
|
|
||||||
Words can also be generated from affixes; these are specified by the special template specifiers 'a'/'A'.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
('c', 'V') - a syllable consisting of exactly one vowel, possibly preceeded by a single consonant
|
|
||||||
('C', 'c', 'V', 'v') - a syllable consisting of one or two consonants followed by one or two vowels
|
|
||||||
('a', 'C', 'V') - a syllable consisting of an optional affix, a consonant and a vowel.
|
|
||||||
|
|
||||||
Word length is determined by the number of syllables, which is chosen at random using relative weights:
|
|
||||||
|
|
||||||
[2, 2, 1] - Names may contain one, two or three syllables, but are half as likely to contain three.
|
|
||||||
[0, 1] - Names must have exactly two syllables
|
|
||||||
"""
|
|
||||||
|
|
||||||
affixes = []
|
|
||||||
vowels = []
|
|
||||||
consonants = []
|
|
||||||
|
|
||||||
prefixes = vowels + consonants
|
|
||||||
suffixes = vowels + consonants
|
|
||||||
|
|
||||||
syllable_template = ('C', 'V')
|
|
||||||
syllable_weights = [1, 1]
|
|
||||||
|
|
||||||
minimum_length = 3
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._logger = logging.getLogger()
|
|
||||||
|
|
||||||
self.syllable = SyllableFactory(
|
|
||||||
template=self.syllable_template,
|
|
||||||
weights=self.syllable_weights,
|
|
||||||
prefixes=[grapheme(char=c, weight=1) for c in self.__class__.prefixes],
|
|
||||||
suffixes=[grapheme(char=c, weight=1) for c in self.__class__.suffixes],
|
|
||||||
vowels=[grapheme(char=c, weight=1) for c in self.__class__.vowels],
|
|
||||||
consonants=[grapheme(char=c, weight=1) for c in self.__class__.consonants],
|
|
||||||
affixes=[grapheme(char=c, weight=1) for c in self.__class__.affixes]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _valid_syllable(self, syllable, text, key='apcvs', reverse=False):
|
|
||||||
length = 0
|
|
||||||
for seq in reverse(sorted(syllable.graphemes(key=key), key=len)):
|
|
||||||
length = len(seq)
|
|
||||||
substr = text[-1 * length:] if reverse else text[0:length]
|
|
||||||
if substr == seq:
|
|
||||||
return length
|
|
||||||
return False
|
|
||||||
|
|
||||||
def is_valid(self, text):
|
|
||||||
|
|
||||||
for part in text.lower().split(' '):
|
|
||||||
|
|
||||||
if part in self.affixes:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if len(part) < self.minimum_length:
|
|
||||||
self._logger.debug(f"'{part}' too short; must be {self.minimum_length} characters.")
|
|
||||||
return False
|
|
||||||
|
|
||||||
first_offset = self._valid_syllable(self.syllable, text=part, key='p')
|
|
||||||
if first_offset is False:
|
|
||||||
self._logger.debug(f"'{part}' is not a valid syllable.")
|
|
||||||
return False
|
|
||||||
|
|
||||||
last_offset = self._valid_syllable(self.last_syllable, text=part, key='s', reverse=True)
|
|
||||||
if last_offset is False:
|
|
||||||
self._logger.debug(f"'{part}' is not a valid syllable.")
|
|
||||||
return False
|
|
||||||
last_offset = len(part) - last_offset
|
|
||||||
|
|
||||||
while first_offset < last_offset:
|
|
||||||
middle = part[first_offset:last_offset]
|
|
||||||
new_offset = self._valid_syllable(self.syllable, text=middle, key='cv')
|
|
||||||
if new_offset is False:
|
|
||||||
self._logger.debug(f"'{middle}' is not a valid middle sequence.")
|
|
||||||
return False
|
|
||||||
first_offset = first_offset + new_offset
|
|
||||||
return True
|
|
||||||
|
|
||||||
def validate_sequence(self, sequence, total_syllables):
|
|
||||||
return len(''.join(sequence)) > self.minimum_length
|
|
||||||
|
|
||||||
def word(self):
|
|
||||||
return WordFactory(language=self)
|
|
||||||
|
|
||||||
def place(self):
|
|
||||||
return self.word()
|
|
||||||
|
|
||||||
def person(self):
|
|
||||||
return (self.word(), self.word())
|
|
|
@ -1,32 +0,0 @@
|
||||||
from npc.languages.base import BaseLanguage
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
class Celestial(BaseLanguage):
|
|
||||||
|
|
||||||
vowels = ['a', 'e', 'i', 'o', 'u', 'î', 'ê', 'â', 'û', 'ô', 'ä', 'ö', 'ü', 'äu', 'ȧ', 'ė', 'ị', 'ȯ', 'u̇']
|
|
||||||
|
|
||||||
consonants = ['b', 'sc', 'f', 'h', 'l', 'm', 'n', 'r', 's', 'v']
|
|
||||||
|
|
||||||
syllable_template = ('V', 'v', 'c', 'c', 'v', 'v')
|
|
||||||
|
|
||||||
_invalid_sequences = re.compile(
|
|
||||||
r'[' + ''.join(vowels) + ']{5}|' +
|
|
||||||
r'[' + ''.join(consonants) + ']{3}'
|
|
||||||
)
|
|
||||||
|
|
||||||
syllable_weights = [3, 2]
|
|
||||||
|
|
||||||
minimum_length = 5
|
|
||||||
|
|
||||||
def validate_sequence(self, sequence, total_syllables):
|
|
||||||
too_short = len(''.join(sequence)) < self.minimum_length
|
|
||||||
if too_short:
|
|
||||||
return False
|
|
||||||
|
|
||||||
t = ''.join(sequence)
|
|
||||||
|
|
||||||
if self._invalid_sequences.match(t):
|
|
||||||
self._logger.debug(f"Invalid sequence: {t}")
|
|
||||||
return False
|
|
||||||
return True
|
|
|
@ -1,94 +0,0 @@
|
||||||
import re
|
|
||||||
import random
|
|
||||||
from npc.languages.base import BaseLanguage, WordFactory
|
|
||||||
|
|
||||||
|
|
||||||
class Common(BaseLanguage):
|
|
||||||
vowels = ['a', 'e', 'i', 'o', 'u']
|
|
||||||
|
|
||||||
consonants = [
|
|
||||||
'b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'p', 'q', 'r', 's', 't',
|
|
||||||
'v', 'w', 'x', 'y', 'z'
|
|
||||||
]
|
|
||||||
|
|
||||||
_middle_clusters = re.compile(
|
|
||||||
r'bs|ct|ch|ck|dd|ff|gh|gs|ms|ns|ps|qu|rb|rd|rf|rk|rl|rm|rn|rp|rs|rt|ry' +
|
|
||||||
r'|sh|sk|ss|st|sy|th|tk|ts|tt|ty|ws|yd|yk|yl|ym|yn|yp|yr|ys|yt|yz|mcd' +
|
|
||||||
r'|[' + ''.join(vowels) + '][' + ''.join(consonants) + ']' +
|
|
||||||
r'|[' + ''.join(consonants) + '][' + ''.join(vowels) + ']' +
|
|
||||||
r'|[' + ''.join(vowels) + ']{1,2}'
|
|
||||||
)
|
|
||||||
|
|
||||||
_invalid_sequences = re.compile(
|
|
||||||
r'[' + ''.join(vowels) + ']{3}|' +
|
|
||||||
r'[' + ''.join(consonants) + ']{4}'
|
|
||||||
)
|
|
||||||
|
|
||||||
suffixes = [
|
|
||||||
'ad', 'ed', 'id', 'od', 'ud',
|
|
||||||
'af', 'ef', 'if', 'of', 'uf',
|
|
||||||
'ah', 'eh', 'ih', 'oh', 'uh',
|
|
||||||
'al', 'el', 'il', 'ol', 'ul',
|
|
||||||
'am', 'em', 'im', 'om', 'um',
|
|
||||||
'an', 'en', 'in', 'on', 'un',
|
|
||||||
'ar', 'er', 'ir', 'or', 'ur',
|
|
||||||
'as', 'es', 'is', 'os', 'us',
|
|
||||||
'at', 'et', 'it', 'ot', 'ut',
|
|
||||||
'ax', 'ex', 'ix', 'ox', 'ux',
|
|
||||||
'ay', 'ey', 'iy', 'oy', 'uy',
|
|
||||||
'az', 'ez', 'iz', 'oz', 'uz',
|
|
||||||
]
|
|
||||||
|
|
||||||
prefixes = [s[::-1] for s in suffixes]
|
|
||||||
|
|
||||||
affixes = []
|
|
||||||
|
|
||||||
syllable_template = ('p', 'c|v', 's')
|
|
||||||
|
|
||||||
minimum_length = 1
|
|
||||||
|
|
||||||
def validate_sequence(self, sequence, total_syllables):
|
|
||||||
too_short = len(''.join(sequence)) < self.minimum_length
|
|
||||||
if too_short:
|
|
||||||
return False
|
|
||||||
|
|
||||||
t = ''.join(sequence)
|
|
||||||
|
|
||||||
if self._invalid_sequences.match(t):
|
|
||||||
self._logger.debug(f"Invalid sequence: {t}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
for pos in range(len(t)):
|
|
||||||
seq = t[pos:pos+2]
|
|
||||||
if len(seq) != 2:
|
|
||||||
return True
|
|
||||||
if not self._middle_clusters.match(seq):
|
|
||||||
self._logger.debug(f"Invalid sequence: {seq}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class CommonSurname(Common):
|
|
||||||
|
|
||||||
syllable_template = ('P|C', 'v', 'S')
|
|
||||||
syllable_weights = [1]
|
|
||||||
|
|
||||||
word_suffixes = [
|
|
||||||
'berg', 'borg', 'borough', 'bury', 'berry', 'by', 'ford', 'gard', 'grave', 'grove', 'gren', 'hardt', 'hart',
|
|
||||||
'heim', 'holm', 'land', 'leigh', 'ley', 'ly', 'lof', 'love', 'lund', 'man', 'mark', 'ness', 'olf', 'olph',
|
|
||||||
'quist', 'rop', 'rup', 'stad', 'stead', 'stein', 'strom', 'thal', 'thorpe', 'ton', 'vall', 'wich', 'win',
|
|
||||||
'some', 'smith', 'bridge', 'cope', 'town', 'er', 'don', 'den', 'dell', 'son',
|
|
||||||
]
|
|
||||||
|
|
||||||
def word(self):
|
|
||||||
return str(WordFactory(self)) + random.choice(self.word_suffixes)
|
|
||||||
|
|
||||||
|
|
||||||
class CommonPerson(Common):
|
|
||||||
|
|
||||||
syllable_template = ('p', 'C', 'V', 's')
|
|
||||||
syllable_weights = [3, 1]
|
|
||||||
|
|
||||||
minimum_length = 2
|
|
||||||
|
|
||||||
def person(self):
|
|
||||||
return (WordFactory(language=self), CommonSurname().word())
|
|
|
@ -1,67 +0,0 @@
|
||||||
from npc.languages.base import BaseLanguage, WordFactory
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
class Draconic(BaseLanguage):
|
|
||||||
|
|
||||||
vowels = ["a'", "aa", "ah", "e'", "ee", "ei", "ey", "i'", "ii", "ir", "o'", "u'", "uu"]
|
|
||||||
|
|
||||||
consonants = [
|
|
||||||
'd', 'f', 'g', 'h', 'j', 'k', 'l',
|
|
||||||
'n', 'r', 's', 't', 'v', 'x', 'y', 'z',
|
|
||||||
]
|
|
||||||
|
|
||||||
syllable_template = ('C', 'V')
|
|
||||||
|
|
||||||
_invalid_sequences = re.compile(
|
|
||||||
r'[' + ''.join(vowels) + ']{3}|' +
|
|
||||||
r'[' + ''.join(consonants) + ']{4}'
|
|
||||||
)
|
|
||||||
|
|
||||||
syllable_weights = [0, 0, 1, 2, 2, 1]
|
|
||||||
|
|
||||||
minimum_length = 3
|
|
||||||
|
|
||||||
def validate_sequence(self, sequence, total_syllables):
|
|
||||||
too_short = len(''.join(sequence)) < self.minimum_length
|
|
||||||
if too_short:
|
|
||||||
return False
|
|
||||||
|
|
||||||
t = ''.join(sequence)
|
|
||||||
|
|
||||||
if self._invalid_sequences.match(t):
|
|
||||||
self._logger.debug(f"Invalid sequence: {t}")
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class Dragon(Draconic):
|
|
||||||
|
|
||||||
syllable_template = ('v', 'C', 'V')
|
|
||||||
syllable_weights = [0, 1, 2]
|
|
||||||
|
|
||||||
vowels = ['a', 'e', 'i', 'o', 'u']
|
|
||||||
last_vowels = vowels
|
|
||||||
last_consonants = ['th', 'x', 'ss', 'z']
|
|
||||||
|
|
||||||
minimum_length = 2
|
|
||||||
|
|
||||||
_invalid_sequences = re.compile(
|
|
||||||
r'[' + ''.join(last_vowels) + ']{2}|' +
|
|
||||||
r'[' + ''.join(Draconic.consonants) + ']{2}'
|
|
||||||
)
|
|
||||||
|
|
||||||
def names(self):
|
|
||||||
prefix = str(WordFactory(self))
|
|
||||||
suffix = ''
|
|
||||||
while not self.validate_sequence(suffix, 1):
|
|
||||||
suffix = ''.join([
|
|
||||||
random.choice(self.last_vowels),
|
|
||||||
random.choice(self.last_consonants),
|
|
||||||
random.choice(['us', 'ux', 'as', 'ax', 'is', 'ix', 'es', 'ex'])
|
|
||||||
|
|
||||||
])
|
|
||||||
return [prefix + suffix]
|
|
||||||
|
|
||||||
person = names
|
|
|
@ -1,42 +0,0 @@
|
||||||
import random
|
|
||||||
|
|
||||||
from npc.languages.base import BaseLanguage
|
|
||||||
|
|
||||||
|
|
||||||
class Dwarvish(BaseLanguage):
|
|
||||||
|
|
||||||
consonants = [
|
|
||||||
'b', 'p', 'ph', 'd', 't', 'th', 'j', 'c', 'ch', 'g', 'k', 'kh', 'v', 'f', 'z', 's', 'zh', 'sh', 'hy', 'h', 'r',
|
|
||||||
'l', 'y', 'w', 'm', 'n'
|
|
||||||
]
|
|
||||||
|
|
||||||
vowels = [
|
|
||||||
'a', 'e', 'i', 'o', 'u', 'î', 'ê', 'â', 'û', 'ô'
|
|
||||||
]
|
|
||||||
|
|
||||||
affixes = []
|
|
||||||
|
|
||||||
first_consonants = consonants
|
|
||||||
first_vowels = vowels
|
|
||||||
first_affixes = affixes
|
|
||||||
|
|
||||||
last_vowels = vowels
|
|
||||||
last_consonants = consonants
|
|
||||||
last_affixes = affixes
|
|
||||||
|
|
||||||
syllable_template = ('C', 'V', 'c')
|
|
||||||
syllable_weights = [4, 1]
|
|
||||||
|
|
||||||
name_suffixes = ['son', 'sson', 'zhon', 'dottir', 'dothir', 'dottyr']
|
|
||||||
|
|
||||||
def person(self):
|
|
||||||
words = super().person()
|
|
||||||
suffix = random.choice(Dwarvish.name_suffixes)
|
|
||||||
return (str(words[0]), f"{words[1]}{suffix}")
|
|
||||||
|
|
||||||
def is_valid(self, text):
|
|
||||||
for suffix in self.name_suffixes:
|
|
||||||
if text.endswith(suffix):
|
|
||||||
text = text[0:len(suffix)]
|
|
||||||
break
|
|
||||||
return super().is_valid(text)
|
|
|
@ -1,166 +0,0 @@
|
||||||
import random
|
|
||||||
import re
|
|
||||||
|
|
||||||
from npc.languages.base import BaseLanguage, WordFactory
|
|
||||||
|
|
||||||
|
|
||||||
class Elven(BaseLanguage):
|
|
||||||
"""
|
|
||||||
Phonetics for the Elven language in Telisar. Inspired by Tolkein's Quenya language, but with naming conventions
|
|
||||||
following Twirrim's conventions in-game.
|
|
||||||
"""
|
|
||||||
|
|
||||||
vowels = ['a', 'e', 'i', 'o', 'u']
|
|
||||||
consonants = ['b', 'c', 'd', 'f', 'g', 'h', 'k', 'l', 'm', 'n', 'p', 'r', 's', 't', 'v', 'w', 'y', 'z']
|
|
||||||
affixes = []
|
|
||||||
|
|
||||||
first_vowels = ['a', 'e', 'i', 'o', 'u', 'y']
|
|
||||||
first_consonants = ['c', 'g', 'l', 'm', 'n', 'r', 's', 't', 'v', 'z']
|
|
||||||
first_affixes = []
|
|
||||||
|
|
||||||
last_vowels = ['a', 'i', 'e']
|
|
||||||
last_consonants = ['t', 's', 'm', 'n', 'l', 'r', 'd', 'a', 'th']
|
|
||||||
last_affixes = []
|
|
||||||
|
|
||||||
syllable_template = ('c', 'v', 'c', 'V', 'C', 'v')
|
|
||||||
minimum_length = 4
|
|
||||||
|
|
||||||
_valid_consonant_sequences = [
|
|
||||||
'cc', 'ht', 'kd', 'kl', 'km', 'kp', 'kt', 'kv', 'kw', 'ky', 'lc', 'ld',
|
|
||||||
'lf', 'll', 'lm', 'lp', 'lt', 'lv', 'lw', 'ly', 'mb', 'mm', 'mp', 'my',
|
|
||||||
'nc', 'nd', 'ng', 'nn', 'nt', 'nw', 'ny', 'ps', 'pt', 'rc', 'rd', 'rm',
|
|
||||||
'rn', 'rp', 'rr', 'rs', 'rt', 'rw', 'ry', 'sc', 'ss', 'ts', 'tt', 'th',
|
|
||||||
'tw', 'ty'
|
|
||||||
]
|
|
||||||
|
|
||||||
_invalid_sequences = re.compile(
|
|
||||||
r'[' + ''.join(vowels) + ']{3}|' +
|
|
||||||
r'[' + ''.join(consonants) + ']{4}'
|
|
||||||
)
|
|
||||||
|
|
||||||
def validate_sequence(self, sequence, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Ensure the specified sequence of syllables results in valid letter combinations.
|
|
||||||
"""
|
|
||||||
too_short = len(''.join(sequence)) < self.minimum_length
|
|
||||||
if too_short:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# the whole string must be checked against the invalid sequences pattern
|
|
||||||
chars = ''.join(sequence)
|
|
||||||
if self._invalid_sequences.match(chars):
|
|
||||||
self._logger.debug(f"Invalid sequence: {chars}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Now step through the sequence, two letters at a time, and verify that
|
|
||||||
# all pairs of consonants are valid.
|
|
||||||
for offset in range(0, len(chars), 2):
|
|
||||||
seq = chars[offset:2]
|
|
||||||
if not seq:
|
|
||||||
break
|
|
||||||
if seq[0] in self.consonants and seq[1] in self.consonants:
|
|
||||||
if seq not in self._valid_consonant_sequences:
|
|
||||||
self._logger.debug(f"Invalid sequence: {seq}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class ElvenPlaceName(Elven):
|
|
||||||
"""
|
|
||||||
Place names are a restricted subset of Elven; the initial syllables are constructed as normal, but place names
|
|
||||||
end in a sequence consisting of exactly one vowel and one consonant.
|
|
||||||
"""
|
|
||||||
syllable_template = ('v', 'C', 'v')
|
|
||||||
syllable_weights = [2, 1]
|
|
||||||
first_consonants = Elven.first_consonants + ['q']
|
|
||||||
|
|
||||||
minimum_length = 2
|
|
||||||
|
|
||||||
affixes = ['el']
|
|
||||||
|
|
||||||
def word(self):
|
|
||||||
prefix = str(WordFactory(self))
|
|
||||||
suffix = []
|
|
||||||
while not self.validate_sequence(suffix):
|
|
||||||
suffix = [
|
|
||||||
random.choice(self.last_vowels),
|
|
||||||
random.choice(self.last_consonants + ['ss']),
|
|
||||||
]
|
|
||||||
return prefix + ''.join(suffix)
|
|
||||||
|
|
||||||
def full_name(self):
|
|
||||||
return 'el '.join(self.names)
|
|
||||||
|
|
||||||
|
|
||||||
class HighElvenSurname(Elven):
|
|
||||||
"""
|
|
||||||
High Elven names follow the same naming conventions as more modern names, but ancient place names were longer, and
|
|
||||||
suffixes always followed a pattern of vowel, consonant, two vowels, and a final consonant, but the rules for
|
|
||||||
each are much more restrictive. In practice just a few suffixes are permitted: ieth, ies, ier, ien, iath, ias, iar,
|
|
||||||
ian, ioth, ios, ior, and ion.
|
|
||||||
"""
|
|
||||||
|
|
||||||
syllable_template = ('v', 'C', 'v')
|
|
||||||
syllable_weights = [1, 2, 2]
|
|
||||||
minimum_length = 2
|
|
||||||
|
|
||||||
def word(self):
|
|
||||||
prefix = str(WordFactory(self))
|
|
||||||
suffix = ''
|
|
||||||
while not self.validate_sequence(suffix):
|
|
||||||
suffix = ''.join([
|
|
||||||
random.choice(self.last_vowels),
|
|
||||||
random.choice(self.last_consonants + ['ss']),
|
|
||||||
random.choice([
|
|
||||||
'ie',
|
|
||||||
'ia',
|
|
||||||
'io',
|
|
||||||
]),
|
|
||||||
random.choice(['th', 's', 'r', 'n'])
|
|
||||||
])
|
|
||||||
return prefix + suffix
|
|
||||||
|
|
||||||
|
|
||||||
class ElvenPerson(Elven):
|
|
||||||
"""
|
|
||||||
A modern Elven name. Surnames follow the same convention as High Elven in including place names, though over time
|
|
||||||
the social function of denoting where renown was earned has been lost. An elf who names himself "am Uman", for
|
|
||||||
example, would be seen as either foolish or obnoxious, or both. Like "Johnny New York."
|
|
||||||
"""
|
|
||||||
|
|
||||||
syllable_template = ('c', 'V', 'C', 'v')
|
|
||||||
syllable_weights = [1, 2]
|
|
||||||
|
|
||||||
last_affixes = ['am', 'an', 'al', 'um']
|
|
||||||
|
|
||||||
def place(self):
|
|
||||||
return ElvenPlaceName().word()
|
|
||||||
|
|
||||||
def word(self):
|
|
||||||
return (
|
|
||||||
super().word(),
|
|
||||||
random.choice(self.last_affixes),
|
|
||||||
self.place()
|
|
||||||
)
|
|
||||||
|
|
||||||
person = word
|
|
||||||
|
|
||||||
|
|
||||||
class HighElvenPerson(ElvenPerson):
|
|
||||||
"""
|
|
||||||
Given names in High Elven and modern Elven follow the same conventions, but a High Elven surname is generally
|
|
||||||
chosen by the individual, to indicate "the place where renown is earned." So the High-Elven Elstuviar am
|
|
||||||
Vakaralithien implies a place or organization named Vakarlithien where the elf Elstuviar was first recognized by
|
|
||||||
their peers for worthy accompliments.
|
|
||||||
"""
|
|
||||||
syllable_weights = [2, 2, 2]
|
|
||||||
|
|
||||||
def word(self):
|
|
||||||
return (
|
|
||||||
super(Elven, self).word(),
|
|
||||||
random.choice(self.last_affixes),
|
|
||||||
HighElvenSurname().word()
|
|
||||||
)
|
|
||||||
|
|
||||||
person = word
|
|
|
@ -1,24 +0,0 @@
|
||||||
from npc.languages.base import BaseLanguage
|
|
||||||
|
|
||||||
|
|
||||||
class Gnomish(BaseLanguage):
|
|
||||||
|
|
||||||
vowels = ['a', 'e', 'i', 'o', 'u', 'y']
|
|
||||||
consonants = ['b', 'd', 'f', 'g', 'h', 'j', 'l', 'm', 'n', 'p', 'r', 's', 't', 'v', 'w', 'z']
|
|
||||||
affixes = []
|
|
||||||
|
|
||||||
first_vowels = vowels
|
|
||||||
first_consonants = consonants
|
|
||||||
first_affixes = affixes
|
|
||||||
|
|
||||||
last_vowels = ['a', 'e', 'i', 'o', 'y']
|
|
||||||
last_consonants = consonants
|
|
||||||
last_affixes = affixes
|
|
||||||
|
|
||||||
syllable_template = ('C', 'V', 'v')
|
|
||||||
syllable_weights = [3, 1]
|
|
||||||
|
|
||||||
minimum_length = 1
|
|
||||||
|
|
||||||
def person(self):
|
|
||||||
return (self.word(), self.word())
|
|
|
@ -1,53 +0,0 @@
|
||||||
from npc.languages.base import BaseLanguage
|
|
||||||
|
|
||||||
|
|
||||||
class Halfling(BaseLanguage):
|
|
||||||
|
|
||||||
vowels = ["a'", "e'", "i'" "o'", 'a', 'e', 'i', 'o', 'y']
|
|
||||||
consonants = ['b', 'd', 'f', 'g', 'h', 'j', 'l', 'm', 'n', 'p', 'r', 's', 't', 'v', 'w', 'z']
|
|
||||||
affixes = []
|
|
||||||
|
|
||||||
first_vowels = vowels
|
|
||||||
first_consonants = consonants
|
|
||||||
first_affixes = affixes
|
|
||||||
|
|
||||||
last_vowels = ['a', 'e', 'i', 'o', 'y']
|
|
||||||
last_consonants = consonants
|
|
||||||
last_affixes = affixes
|
|
||||||
|
|
||||||
syllable_template = ('c', 'V')
|
|
||||||
syllable_weights = [0, 1, 2, 3, 2, 1]
|
|
||||||
|
|
||||||
nicknames = [
|
|
||||||
'able', 'clean', 'enthusiastic', 'heartening', 'meek', 'reasonable', 'talented',
|
|
||||||
'accommodating', 'clever', 'ethical', 'helpful', 'meritorious', 'refined', 'temperate',
|
|
||||||
'accomplished', 'commendable', 'excellent', 'moral', 'reliable', 'terrific',
|
|
||||||
'adept', 'compassionate', 'exceptional', 'honest', 'neat', 'remarkable', 'tidy',
|
|
||||||
'admirable', 'composed', 'exemplary', 'honorable', 'noble', 'resilient', 'quality',
|
|
||||||
'agreeable', 'considerate', 'exquisite', 'hopeful', 'obliging', 'respectable', 'tremendous',
|
|
||||||
'amazing', 'consummate', 'extraordinary', 'humble', 'observant', 'respectful', 'trustworthy',
|
|
||||||
'appealing', 'cooperative', 'fabulous', 'important', 'optimistic', 'resplendent', 'trusty',
|
|
||||||
'astute', 'correct', 'faithful', 'impressive', 'organized', 'responsible', 'truthful',
|
|
||||||
'attractive', 'courageous', 'fantastic', 'incisive', 'outstanding', 'robust', 'unbeatable',
|
|
||||||
'awesome', 'courteous', 'fascinating', 'incredible', 'peaceful', 'selfless', 'understanding',
|
|
||||||
'beautiful', 'dazzling', 'fine', 'innocent', 'perceptive', 'sensational', 'unequaled',
|
|
||||||
'benevolent', 'decent', 'classy', 'insightful', 'perfect', 'sensible', 'unparalleled',
|
|
||||||
'brave', 'delightful', 'fortitudinous', 'inspiring', 'pleasant', 'serene', 'upbeat',
|
|
||||||
'breathtaking', 'dependable', 'gallant', 'intelligent', 'pleasing', 'sharp', 'valiant',
|
|
||||||
'bright', 'devoted', 'generous', 'joyful', 'polite', 'shining', 'valuable',
|
|
||||||
'brilliant', 'diplomatic', 'gentle', 'judicious', 'positive', 'shrewd', 'vigilant',
|
|
||||||
'bubbly', 'discerning', 'gifted', 'just', 'praiseworthy', 'smart', 'vigorous',
|
|
||||||
'buoyant', 'disciplined', 'giving', 'kindly', 'precious', 'sparkling', 'virtuous',
|
|
||||||
'calm', 'elegant', 'gleaming', 'laudable', 'priceless', 'spectacular', 'well mannered',
|
|
||||||
'capable', 'elevating', 'glowing', 'likable', 'principled', 'splendid', 'wholesome',
|
|
||||||
'charitable', 'enchanting', 'good', 'lovable', 'prompt', 'steadfast', 'wise',
|
|
||||||
'charming', 'encouraging', 'gorgeous', 'lovely', 'prudent', 'stunning', 'witty',
|
|
||||||
'chaste', 'endearing', 'graceful', 'loyal', 'punctual', 'super', 'wonderful',
|
|
||||||
'cheerful', 'energetic', 'gracious', 'luminous', 'pure', 'superb', 'worthy',
|
|
||||||
'chivalrous', 'engaging', 'great', 'magnanimous', 'quick', 'superior', 'zesty',
|
|
||||||
'gallant', 'enhanced', 'happy', 'magnificent', 'radiant', 'supportive',
|
|
||||||
'civil', 'enjoyable', 'hardy', 'marvelous', 'rational', 'supreme'
|
|
||||||
]
|
|
||||||
|
|
||||||
def person(self):
|
|
||||||
return (self.word(), self.word(), self.word())
|
|
|
@ -1,108 +0,0 @@
|
||||||
from npc.languages.base import BaseLanguage
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
class Infernal(BaseLanguage):
|
|
||||||
|
|
||||||
vowels = ['a', 'e', 'i', 'o', 'u']
|
|
||||||
|
|
||||||
consonants = [
|
|
||||||
'b', 'c', 'd', 'f', 'g', 'j', 'k', 'l', 'm',
|
|
||||||
'n', 'p', 'r', 's', 't', 'v', 'x', 'y', 'z',
|
|
||||||
"t'h", "t'j", "t'z", "x't", "x'z", "x'j"
|
|
||||||
]
|
|
||||||
|
|
||||||
syllable_template = ('C', 'V')
|
|
||||||
|
|
||||||
_invalid_sequences = re.compile(
|
|
||||||
r'[' + ''.join(vowels) + ']{3}|' +
|
|
||||||
r'[' + ''.join(consonants) + ']{4}'
|
|
||||||
)
|
|
||||||
|
|
||||||
syllable_weights = [3, 2]
|
|
||||||
|
|
||||||
minimum_length = 1
|
|
||||||
|
|
||||||
def validate_sequence(self, sequence, total_syllables):
|
|
||||||
too_short = len(''.join(sequence)) < self.minimum_length
|
|
||||||
if too_short:
|
|
||||||
return False
|
|
||||||
|
|
||||||
t = ''.join(sequence)
|
|
||||||
|
|
||||||
if self._invalid_sequences.match(t):
|
|
||||||
self._logger.debug(f"Invalid sequence: {t}")
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class Tiefling(Infernal):
|
|
||||||
"""
|
|
||||||
Tiefling names are formed using an infernal root and a few common suffixes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
nicknames = [
|
|
||||||
'eternal',
|
|
||||||
'wondrous',
|
|
||||||
'luminous',
|
|
||||||
'perfect',
|
|
||||||
'essential',
|
|
||||||
'golden',
|
|
||||||
'unfailing',
|
|
||||||
'perpetual',
|
|
||||||
'infinite',
|
|
||||||
'exquisite',
|
|
||||||
'sinless',
|
|
||||||
'ultimate',
|
|
||||||
'flawless',
|
|
||||||
'timeless',
|
|
||||||
'glorious',
|
|
||||||
'absolute',
|
|
||||||
'boundless',
|
|
||||||
'true',
|
|
||||||
'incredible',
|
|
||||||
'virtuous',
|
|
||||||
'supreme',
|
|
||||||
'enchanted',
|
|
||||||
'magnificent',
|
|
||||||
'superior',
|
|
||||||
'spectacular',
|
|
||||||
'divine',
|
|
||||||
] + ['' for _ in range(50)]
|
|
||||||
|
|
||||||
def person(self):
|
|
||||||
suffix = random.choice([
|
|
||||||
'us',
|
|
||||||
'ius'
|
|
||||||
'to',
|
|
||||||
'tro'
|
|
||||||
'eus',
|
|
||||||
'a',
|
|
||||||
'an',
|
|
||||||
'is',
|
|
||||||
])
|
|
||||||
return [str(self.word()) + suffix]
|
|
||||||
|
|
||||||
|
|
||||||
class HighTiefling(Tiefling):
|
|
||||||
"""
|
|
||||||
"High" Tieflings revere their bloodlines and take their lineage as part of their name.
|
|
||||||
"""
|
|
||||||
|
|
||||||
nicknames = []
|
|
||||||
|
|
||||||
def person(self):
|
|
||||||
bloodline = random.choice([
|
|
||||||
'Asmodeus',
|
|
||||||
'Baalzebul',
|
|
||||||
'Rimmon',
|
|
||||||
'Dispater',
|
|
||||||
'Fierna',
|
|
||||||
'Glasya',
|
|
||||||
'Levistus',
|
|
||||||
'Mammon',
|
|
||||||
'Mephistopheles',
|
|
||||||
'Zariel',
|
|
||||||
])
|
|
||||||
return [bloodline] + super().person()
|
|
|
@ -1,41 +0,0 @@
|
||||||
from npc.languages.base import BaseLanguage
|
|
||||||
import random
|
|
||||||
|
|
||||||
|
|
||||||
class Lizardfolk(BaseLanguage):
|
|
||||||
|
|
||||||
vowels = []
|
|
||||||
consonants = []
|
|
||||||
affixes = []
|
|
||||||
|
|
||||||
syllable_template = ()
|
|
||||||
syllable_weights = []
|
|
||||||
|
|
||||||
family = [
|
|
||||||
'sweet', 'floral', 'fruity', 'sour', 'fermented', 'green', 'vegetal', 'old',
|
|
||||||
'roasted', 'spiced', 'nutty', 'cocoa', 'pepper', 'pungent', 'burnt', 'carmelized',
|
|
||||||
'raw', 'rotting', 'dead', 'young',
|
|
||||||
]
|
|
||||||
|
|
||||||
scents = [
|
|
||||||
'honey', 'caramel', 'maple syrup', 'molasses', 'dark chocolate', 'chocolate', 'almond',
|
|
||||||
'hazelnut', 'peanut', 'clove', 'cinnamon', 'nutmeg', 'anise', 'malt', 'grain', 'roast',
|
|
||||||
'smoke', 'ash', 'acrid', 'rubber', 'skunk', 'petroleum', 'medicine', 'salt', 'bitter',
|
|
||||||
'phrenolic', 'meat', 'broth', 'animal', 'musty', 'earth', 'mould', 'damp', 'wood', 'paper',
|
|
||||||
'cardboard', 'stale', 'herb', 'hay', 'grass', 'peapod', 'whisky', 'wine', 'malic',
|
|
||||||
'citric', 'isovaleric', 'butyric', 'acetic', 'lime', 'lemon',
|
|
||||||
'orange', 'grapefruit', 'pear', 'peach', 'apple', 'grape', 'pineapple', 'pomegranate',
|
|
||||||
'cherry', 'coconut', 'prune', 'raisin', 'strawberry', 'blueberry', 'raspberry',
|
|
||||||
'blackberry', 'jasmine', 'rose', 'camomile', 'tobacco',
|
|
||||||
]
|
|
||||||
|
|
||||||
nicknames = []
|
|
||||||
|
|
||||||
def person(self):
|
|
||||||
return(
|
|
||||||
random.choice(self.family),
|
|
||||||
'-'.join([
|
|
||||||
random.choice(self.scents),
|
|
||||||
random.choice(self.scents),
|
|
||||||
]),
|
|
||||||
)
|
|
|
@ -1,57 +0,0 @@
|
||||||
import re
|
|
||||||
|
|
||||||
from npc.languages.base import BaseLanguage
|
|
||||||
|
|
||||||
|
|
||||||
class Orcish(BaseLanguage):
|
|
||||||
|
|
||||||
vowels = ['a', 'e', 'i', 'o', 'u']
|
|
||||||
consonants = ['b', 'c', 'ch', 'd', 'f', 'h', 'k', 'm', 'n', 'p', 'r', 's', 'sh', 't', 'z']
|
|
||||||
affixes = []
|
|
||||||
|
|
||||||
first_vowels = vowels
|
|
||||||
first_consonants = consonants
|
|
||||||
first_affixes = affixes
|
|
||||||
|
|
||||||
last_vowels = vowels
|
|
||||||
last_consonants = consonants
|
|
||||||
last_affixes = affixes
|
|
||||||
|
|
||||||
syllable_template = ('C', 'c', 'V')
|
|
||||||
syllable_weights = [2, 4, 0.5]
|
|
||||||
|
|
||||||
_middle_clusters = re.compile(
|
|
||||||
r'\S?[' +
|
|
||||||
r'bd|bk|br|bs|' +
|
|
||||||
r'ch|ck|cp|cr|cs|ct|' +
|
|
||||||
r'db|dk|ds|' +
|
|
||||||
r'fr|ft|' +
|
|
||||||
r'kr|ks|kz|' +
|
|
||||||
r'ms|' +
|
|
||||||
r'ns|nt|nz|' +
|
|
||||||
r'ps|pt|' +
|
|
||||||
r'rk|rt|rz|' +
|
|
||||||
r'sc|sh|sk|sr|st|' +
|
|
||||||
r'tc|th|tr|ts|tz' +
|
|
||||||
r']\S?'
|
|
||||||
)
|
|
||||||
|
|
||||||
def validate_sequence(self, sequence, total_syllables):
|
|
||||||
too_short = len(''.join(sequence)) < self.minimum_length
|
|
||||||
if too_short:
|
|
||||||
return False
|
|
||||||
seq = ''.join(sequence[-2:])
|
|
||||||
if not self._middle_clusters.match(seq):
|
|
||||||
self._logger.debug(f"Invalid sequence: {sequence[-2:]}")
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class OrcishPerson(Orcish):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class HalfOrcPerson(Orcish):
|
|
||||||
syllable_template = ('C', 'V', 'c')
|
|
||||||
first_consonants = ['b', 'c', 'd', 'k', 'p', 't', 'z']
|
|
||||||
last_consonants = Orcish.consonants + ['sht', 'cht', 'zt', 'zch']
|
|
|
@ -1,122 +0,0 @@
|
||||||
import random
|
|
||||||
import re
|
|
||||||
|
|
||||||
from npc.languages.base import BaseLanguage, WordFactory
|
|
||||||
|
|
||||||
|
|
||||||
class Undercommon(BaseLanguage):
|
|
||||||
vowels = ['a', 'e', 'i', 'o', 'u', 'a', 'e', 'i', 'o', 'u', 'ä', 'ö', 'ü', 'äu']
|
|
||||||
consonants = ['b', 'c', 'd', 'f', 'g', 'h', 'k', 'l', 'm', 'n', 'p', 'r', 's', 't', 'v', 'w', 'y', 'z']
|
|
||||||
affixes = []
|
|
||||||
|
|
||||||
first_vowels = ['a', 'e', 'i', 'o', 'u', 'y']
|
|
||||||
first_consonants = ['c', 'g', 'l', 'm', 'n', 'r', 's', 't', 'v', 'z']
|
|
||||||
first_affixes = []
|
|
||||||
|
|
||||||
last_vowels = ['a', 'i', 'e']
|
|
||||||
last_consonants = ['t', 's', 'm', 'n', 'l', 'r', 'd', 'a', 'th']
|
|
||||||
last_affixes = []
|
|
||||||
|
|
||||||
syllable_template = ('c', 'v', 'c', 'V', 'C', 'v')
|
|
||||||
minimum_length = 4
|
|
||||||
|
|
||||||
_valid_consonant_sequences = [
|
|
||||||
'cc', 'ht', 'kd', 'kl', 'km', 'kp', 'kt', 'kv', 'kw', 'ky', 'lc', 'ld',
|
|
||||||
'lf', 'll', 'lm', 'lp', 'lt', 'lv', 'lw', 'ly', 'mb', 'mm', 'mp', 'my',
|
|
||||||
'nc', 'nd', 'ng', 'nn', 'nt', 'nw', 'ny', 'ps', 'pt', 'rc', 'rd', 'rm',
|
|
||||||
'rn', 'rp', 'rr', 'rs', 'rt', 'rw', 'ry', 'sc', 'ss', 'ts', 'tt', 'th',
|
|
||||||
'tw', 'ty'
|
|
||||||
]
|
|
||||||
|
|
||||||
_invalid_sequences = re.compile(
|
|
||||||
r'[' + ''.join(vowels) + ']{3}|' +
|
|
||||||
r'[' + ''.join(consonants) + ']{4}'
|
|
||||||
)
|
|
||||||
|
|
||||||
def validate_sequence(self, sequence, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Ensure the specified sequence of syllables results in valid letter combinations.
|
|
||||||
"""
|
|
||||||
too_short = len(''.join(sequence)) < self.minimum_length
|
|
||||||
if too_short:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# the whole string must be checked against the invalid sequences pattern
|
|
||||||
chars = ''.join(sequence)
|
|
||||||
if self._invalid_sequences.match(chars):
|
|
||||||
self._logger.debug(f"Invalid sequence: {chars}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Now step through the sequence, two letters at a time, and verify that
|
|
||||||
# all pairs of consonants are valid.
|
|
||||||
for offset in range(0, len(chars), 2):
|
|
||||||
seq = chars[offset:2]
|
|
||||||
if not seq:
|
|
||||||
break
|
|
||||||
if seq[0] in self.consonants and seq[1] in self.consonants:
|
|
||||||
if seq not in self._valid_consonant_sequences:
|
|
||||||
self._logger.debug(f"Invalid sequence: {seq}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class DrowPlaceName(Undercommon):
|
|
||||||
syllable_template = ('v', 'C', 'v')
|
|
||||||
syllable_weights = [2, 1]
|
|
||||||
first_consonants = Undercommon.first_consonants + ['q']
|
|
||||||
|
|
||||||
minimum_length = 2
|
|
||||||
|
|
||||||
affixes = ['el']
|
|
||||||
|
|
||||||
def word(self):
|
|
||||||
prefix = str(WordFactory(self))
|
|
||||||
suffix = []
|
|
||||||
while not self.validate_sequence(suffix):
|
|
||||||
suffix = [
|
|
||||||
random.choice(self.last_vowels),
|
|
||||||
random.choice(self.last_consonants + ['ss']),
|
|
||||||
]
|
|
||||||
return prefix + ''.join(suffix)
|
|
||||||
|
|
||||||
def full_name(self):
|
|
||||||
return 'el '.join(self.names)
|
|
||||||
|
|
||||||
|
|
||||||
class DrowSurname(Undercommon):
|
|
||||||
syllable_template = ('v', 'C', 'v')
|
|
||||||
syllable_weights = [1, 2, 2]
|
|
||||||
minimum_length = 2
|
|
||||||
|
|
||||||
def word(self):
|
|
||||||
prefix = str(WordFactory(self))
|
|
||||||
suffix = ''
|
|
||||||
while not self.validate_sequence(suffix):
|
|
||||||
suffix = ''.join([
|
|
||||||
random.choice(self.last_vowels),
|
|
||||||
random.choice(self.last_consonants + ['ss']),
|
|
||||||
random.choice([
|
|
||||||
'ie',
|
|
||||||
'ia',
|
|
||||||
'io',
|
|
||||||
]),
|
|
||||||
random.choice(['th', 's', 'r', 'n'])
|
|
||||||
])
|
|
||||||
return prefix + suffix
|
|
||||||
|
|
||||||
|
|
||||||
class DrowPerson(Undercommon):
|
|
||||||
syllable_template = ('c', 'V', 'C', 'v')
|
|
||||||
syllable_weights = [1, 2]
|
|
||||||
|
|
||||||
def place(self):
|
|
||||||
return DrowPlaceName().word()
|
|
||||||
|
|
||||||
def word(self):
|
|
||||||
return (
|
|
||||||
super().word(),
|
|
||||||
DrowSurname().word(),
|
|
||||||
)
|
|
||||||
|
|
||||||
person = word
|
|
|
@ -1,39 +1,125 @@
|
||||||
from importlib import import_module
|
from npc import traits
|
||||||
from npc.generator import traits
|
|
||||||
import os
|
|
||||||
import glob
|
|
||||||
import random
|
import random
|
||||||
import dice
|
import dice
|
||||||
import textwrap
|
import textwrap
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
AVAILABLE_NPC_TYPES = {}
|
from language.types import Name, Language
|
||||||
|
|
||||||
|
|
||||||
def a_or_an(s):
|
def a_or_an(s):
|
||||||
return 'an' if s[0] in 'aeiouh' else 'a'
|
return 'an' if s[0] in 'aeiouh' else 'a'
|
||||||
|
|
||||||
|
|
||||||
class BaseNPC:
|
class StatBlock:
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
STR: int = 10,
|
||||||
|
DEX: int = 10,
|
||||||
|
CON: int = 10,
|
||||||
|
INT: int = 10,
|
||||||
|
WIS: int = 10,
|
||||||
|
CHA: int = 10,
|
||||||
|
HP: int = 10,
|
||||||
|
AC: int = 10,
|
||||||
|
speed: int = 30,
|
||||||
|
passive_perception: int = 10,
|
||||||
|
passive_investigation: int = 10,
|
||||||
|
):
|
||||||
|
self.STR = STR
|
||||||
|
self.DEX = DEX
|
||||||
|
self.CON = CON
|
||||||
|
self.INT = INT
|
||||||
|
self.WIS = WIS
|
||||||
|
self.CHA = CHA
|
||||||
|
self.HP = HP
|
||||||
|
self.AC = AC
|
||||||
|
self.speed = speed
|
||||||
|
self.passive_perception = passive_perception
|
||||||
|
self.passive_investigation = passive_investigation
|
||||||
|
|
||||||
|
def randomize(self):
|
||||||
|
stats = [15, 14, 13, 12, 10, 8]
|
||||||
|
random.shuffle(stats)
|
||||||
|
if random.random() < 0.3:
|
||||||
|
i = random.choice(range(len(stats)))
|
||||||
|
stats[i] += (random.choice([-1, 1]) * random.randint(1, 3))
|
||||||
|
(self.STR, self.DEX, self.CON, self.INT, self.WIS, self.CHA) = stats
|
||||||
|
self.HP = str(sum(dice.roll('2d8')) + 2) + ' (2d8+2)'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return textwrap.dedent(f"""
|
||||||
|
AC {self.AC}
|
||||||
|
HP {self.HP}
|
||||||
|
STR {self.STR}
|
||||||
|
DEX {self.DEX}
|
||||||
|
CON {self.CON}
|
||||||
|
INT {self.INT}
|
||||||
|
WIS {self.WIS}
|
||||||
|
CHA {self.CHA}
|
||||||
|
|
||||||
|
Speed: {self.speed}
|
||||||
|
|
||||||
|
Passive Perception: {self.passive_perception}
|
||||||
|
Passive Investigation: {self.passive_perception}
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
class NPC:
|
||||||
"""
|
"""
|
||||||
The base class for NPCs.
|
Return a randomized NPC. Any supplied keyword parameters will override
|
||||||
|
generated, randomized values.
|
||||||
|
|
||||||
|
By default, NPC stats are all 10 (+0). If randomize is True, the NPC will
|
||||||
|
be given random stats from the standard distribution, but overrides will
|
||||||
|
still take precedence.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# define this on your subclass
|
# Define this as a language module from language.supported_languages.values()
|
||||||
language = None
|
language = None
|
||||||
|
|
||||||
_names = []
|
# appearance
|
||||||
|
has_eyes = True
|
||||||
|
has_hair = True
|
||||||
|
has_face = True
|
||||||
|
has_body = True
|
||||||
|
has_nose = True
|
||||||
|
has_lips = True
|
||||||
|
has_teeth = True
|
||||||
|
has_skin_tone = True
|
||||||
|
has_skin_color = True
|
||||||
|
has_facial_hair = True
|
||||||
|
has_facial_structure = True
|
||||||
|
has_eyebrows = True
|
||||||
|
|
||||||
def __init__(self, names=[], title=None, pronouns=None, nickname=None, whereabouts='Unknown', randomize=False,
|
has_age = True
|
||||||
STR=None, DEX=None, CON=None, INT=None, WIS=None, CHA=None):
|
has_voice = True
|
||||||
|
|
||||||
|
has_tail = False
|
||||||
|
has_horns = False
|
||||||
|
has_fangs = False
|
||||||
|
has_wings = False
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: Union[Name, None] = None,
|
||||||
|
pronouns: Union[str, None] = None,
|
||||||
|
whereabouts: str = "Unknown",
|
||||||
|
stats: StatBlock = StatBlock(),
|
||||||
|
noble: bool = False,
|
||||||
|
randomize: bool = False,
|
||||||
|
language: Union[Language, None] = None,
|
||||||
|
):
|
||||||
|
|
||||||
# identity
|
# identity
|
||||||
self._names = []
|
self._name = name
|
||||||
self._pronouns = pronouns
|
self._is_noble = noble
|
||||||
self._nickname = nickname
|
|
||||||
self._title = title
|
|
||||||
|
|
||||||
self._whereabouts = whereabouts
|
self._whereabouts = whereabouts
|
||||||
|
self._stats = stats
|
||||||
|
self._pronouns = pronouns
|
||||||
|
|
||||||
# appearance
|
# appearance
|
||||||
self._eyes = None
|
self._eyes = None
|
||||||
|
@ -50,53 +136,40 @@ class BaseNPC:
|
||||||
self._eyebrows = None
|
self._eyebrows = None
|
||||||
self._age = None
|
self._age = None
|
||||||
self._voice = None
|
self._voice = None
|
||||||
|
self._tail = None
|
||||||
self._tail = False
|
self._horns = None
|
||||||
self._horns = False
|
self._fangs = None
|
||||||
self._fangs = False
|
self._wings = None
|
||||||
self._wings = False
|
|
||||||
|
|
||||||
# character
|
# character
|
||||||
self._flaw = None
|
self._flaw = None
|
||||||
self._goal = None
|
self._goal = None
|
||||||
self._personality = None
|
self._personality = None
|
||||||
|
|
||||||
stats = (10, 10, 10, 10, 10, 10)
|
if language:
|
||||||
|
self.language = language
|
||||||
|
|
||||||
if randomize:
|
if randomize:
|
||||||
stats = self._roll_stats()
|
self.stats.randomize()
|
||||||
self.STR = STR if STR else stats[0]
|
|
||||||
self.DEX = DEX if DEX else stats[1]
|
|
||||||
self.CON = CON if DEX else stats[2]
|
|
||||||
self.INT = INT if DEX else stats[3]
|
|
||||||
self.WIS = WIS if DEX else stats[4]
|
|
||||||
self.CHA = CHA if DEX else stats[5]
|
|
||||||
|
|
||||||
self._HP = None
|
|
||||||
|
|
||||||
def _roll_stats(self):
|
|
||||||
stats = [15, 14, 13, 12, 10, 8]
|
|
||||||
random.shuffle(stats)
|
|
||||||
r = random.random()
|
|
||||||
if r < 0.3:
|
|
||||||
i = random.choice(range(len(stats)))
|
|
||||||
stats[i] += (random.choice([-1, 1]) * random.randint(1, 3))
|
|
||||||
return stats
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def HP(self):
|
def ancestry(self) -> str:
|
||||||
if not self._HP:
|
return self.__class__.__name__
|
||||||
self._HP = str(sum(dice.roll('2d8')) + 2) + ' (2d8+2)'
|
|
||||||
return self._HP
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def names(self):
|
def is_noble(self) -> bool:
|
||||||
if not self._names:
|
return self._is_noble
|
||||||
self._names = next(self.name_generator.name(1))
|
|
||||||
return self._names
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def full_name(self):
|
def name(self):
|
||||||
return self.names.fullname
|
if not self._name:
|
||||||
|
generator = getattr(
|
||||||
|
self.language,
|
||||||
|
'NobleName' if self.is_noble else 'Name'
|
||||||
|
)
|
||||||
|
self._name = generator.name()[0]
|
||||||
|
logging.debug(self._name)
|
||||||
|
return self._name['fullname']
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pronouns(self):
|
def pronouns(self):
|
||||||
|
@ -250,10 +323,14 @@ class BaseNPC:
|
||||||
self._voice = random.choice(traits.voice)
|
self._voice = random.choice(traits.voice)
|
||||||
return self._voice
|
return self._voice
|
||||||
|
|
||||||
|
@property
|
||||||
|
def stats(self):
|
||||||
|
return self._stats
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def description(self):
|
def description(self):
|
||||||
desc = (
|
desc = (
|
||||||
f"{self.full_name} ({self.pronouns}) is {a_or_an(self.age)} {self.age}, {self.body} "
|
f"{self.name} ({self.pronouns}) is {a_or_an(self.age)} {self.age}, {self.body} "
|
||||||
f"{self.ancestry.lower()} with {self.hair} hair, {self.eyes} eyes and {self.skin_color} skin."
|
f"{self.ancestry.lower()} with {self.hair} hair, {self.eyes} eyes and {self.skin_color} skin."
|
||||||
)
|
)
|
||||||
trait = None
|
trait = None
|
||||||
|
@ -267,9 +344,9 @@ class BaseNPC:
|
||||||
self.facial_structure if self.facial_structure else None,
|
self.facial_structure if self.facial_structure else None,
|
||||||
])
|
])
|
||||||
desc = desc + ' ' + f"Their face is {self.face}, with {trait}."
|
desc = desc + ' ' + f"Their face is {self.face}, with {trait}."
|
||||||
if self.tail:
|
if self.has_tail:
|
||||||
desc = desc + f" Their tail is {self.tail}."
|
desc = desc + f" Their tail is {self.tail}."
|
||||||
if self.horns:
|
if self.has_horns:
|
||||||
desc = desc + f" Their horns are {self.horns}."
|
desc = desc + f" Their horns are {self.horns}."
|
||||||
return desc
|
return desc
|
||||||
|
|
||||||
|
@ -295,14 +372,7 @@ Wings: {self.wings}
|
||||||
Voice: {self.voice}
|
Voice: {self.voice}
|
||||||
|
|
||||||
Stats:
|
Stats:
|
||||||
AC 10
|
{textwrap.indent(str(self.stats), prefix=' ')}
|
||||||
HP {self.HP}
|
|
||||||
STR {self.STR}
|
|
||||||
DEX {self.DEX}
|
|
||||||
CON {self.CON}
|
|
||||||
INT {self.INT}
|
|
||||||
WIS {self.WIS}
|
|
||||||
CHA {self.CHA}
|
|
||||||
|
|
||||||
Details:
|
Details:
|
||||||
|
|
||||||
|
@ -315,52 +385,4 @@ Whereabouts: {self.whereabouts}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"{self.full_name}"
|
return self.description
|
||||||
|
|
||||||
|
|
||||||
def available_npc_types():
|
|
||||||
"""
|
|
||||||
Load all available NPC submodules and return a dictionary keyed by module name.
|
|
||||||
"""
|
|
||||||
if not AVAILABLE_NPC_TYPES:
|
|
||||||
for filename in glob.glob(os.path.join(os.path.dirname(os.path.abspath(__file__)), '*.py')):
|
|
||||||
module_name = os.path.basename(filename)[:-3]
|
|
||||||
if module_name not in ['base', '__init__', 'traits']:
|
|
||||||
AVAILABLE_NPC_TYPES[module_name] = import_module(f'npc.generator.{module_name}').NPC
|
|
||||||
return AVAILABLE_NPC_TYPES
|
|
||||||
|
|
||||||
|
|
||||||
def npc_type(ancestry=None):
|
|
||||||
"""
|
|
||||||
Return the NPC class for the specified ancestry, or a random one.
|
|
||||||
"""
|
|
||||||
if not ancestry:
|
|
||||||
non_humans = [x for x in available_npc_types() if x != 'human']
|
|
||||||
if random.random() <= 0.7:
|
|
||||||
ancestry = 'human'
|
|
||||||
else:
|
|
||||||
ancestry = random.choice(non_humans)
|
|
||||||
return available_npc_types()[ancestry]
|
|
||||||
|
|
||||||
|
|
||||||
def generate_npc(ancestry=None, names=[], pronouns=None, title=None, nickname=None, whereabouts="Unknown",
|
|
||||||
STR=0, DEX=0, CON=0, INT=0, WIS=0, CHA=0, randomize=False):
|
|
||||||
"""
|
|
||||||
Return a randomized NPC. Any supplied keyword parameters will override the generated values.
|
|
||||||
|
|
||||||
By default, NPC stats are all 10 (+0). If randomize is True, the NPC will be given random stats from the standard distribution, but overrides will still take precedence.
|
|
||||||
"""
|
|
||||||
return npc_type(ancestry)(
|
|
||||||
names=names,
|
|
||||||
pronouns=pronouns,
|
|
||||||
title=title,
|
|
||||||
nickname=nickname,
|
|
||||||
whereabouts=whereabouts,
|
|
||||||
STR=STR,
|
|
||||||
DEX=DEX,
|
|
||||||
CON=CON,
|
|
||||||
INT=INT,
|
|
||||||
WIS=WIS,
|
|
||||||
CHA=CHA,
|
|
||||||
randomize=randomize
|
|
||||||
)
|
|
|
@ -11,14 +11,15 @@ packages = [
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.10"
|
python = "^3.10"
|
||||||
typer = "latest"
|
rich = "^13.7.0"
|
||||||
rich = "latest"
|
typer = "^0.9.0"
|
||||||
dice = "latest"
|
dice = "^4.0.0"
|
||||||
|
|
||||||
dnd-languages = { git = "https://github.com/evilchili/dnd-languages", branch = 'mainline' }
|
#dnd-name-generator = { git = "https://github.com/evilchili/dnd-name-generator", branch='main' }
|
||||||
|
dnd-name-generator = {path = "../dnd-name-generator/dist/dnd_name_generator-1.0-py3-none-any.whl"}
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
pytest = "latest"
|
pytest = "^7.4.3"
|
||||||
black = "^23.3.0"
|
black = "^23.3.0"
|
||||||
isort = "^5.12.0"
|
isort = "^5.12.0"
|
||||||
pyproject-autoflake = "^1.0.2"
|
pyproject-autoflake = "^1.0.2"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user