2023-12-02 18:24:26 -08:00
|
|
|
from npc import traits
|
2022-08-03 00:19:13 -07:00
|
|
|
import random
|
|
|
|
import dice
|
|
|
|
import textwrap
|
2023-12-02 18:24:26 -08:00
|
|
|
import logging
|
2022-08-03 00:19:13 -07:00
|
|
|
|
2023-12-02 18:24:26 -08:00
|
|
|
from typing import Union
|
2022-08-03 00:19:13 -07:00
|
|
|
|
2023-12-02 18:24:26 -08:00
|
|
|
from language.types import Name, Language
|
2022-08-03 00:19:13 -07:00
|
|
|
|
|
|
|
|
|
|
|
def a_or_an(s):
|
|
|
|
return 'an' if s[0] in 'aeiouh' else 'a'
|
|
|
|
|
|
|
|
|
2023-12-02 18:24:26 -08:00
|
|
|
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:
|
2022-08-03 00:19:13 -07:00
|
|
|
"""
|
2023-12-02 18:24:26 -08:00
|
|
|
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.
|
2022-08-03 00:19:13 -07:00
|
|
|
"""
|
|
|
|
|
2023-12-02 18:24:26 -08:00
|
|
|
# Define this as a language module from language.supported_languages.values()
|
2022-08-03 00:19:13 -07:00
|
|
|
language = None
|
|
|
|
|
2023-12-02 18:24:26 -08:00
|
|
|
# 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
|
|
|
|
|
|
|
|
has_age = True
|
|
|
|
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,
|
|
|
|
):
|
2022-08-03 00:19:13 -07:00
|
|
|
|
|
|
|
# identity
|
2023-12-02 18:24:26 -08:00
|
|
|
self._name = name
|
|
|
|
self._is_noble = noble
|
2022-08-03 00:19:13 -07:00
|
|
|
self._whereabouts = whereabouts
|
2023-12-02 18:24:26 -08:00
|
|
|
self._stats = stats
|
|
|
|
self._pronouns = pronouns
|
2022-08-03 00:19:13 -07:00
|
|
|
|
|
|
|
# appearance
|
|
|
|
self._eyes = None
|
|
|
|
self._hair = None
|
|
|
|
self._face = None
|
|
|
|
self._body = None
|
|
|
|
self._nose = None
|
|
|
|
self._lips = None
|
|
|
|
self._teeth = None
|
|
|
|
self._skin_tone = None
|
|
|
|
self._skin_color = None
|
|
|
|
self._facial_hair = None
|
|
|
|
self._facial_structure = None
|
|
|
|
self._eyebrows = None
|
|
|
|
self._age = None
|
|
|
|
self._voice = None
|
2023-12-02 18:24:26 -08:00
|
|
|
self._tail = None
|
|
|
|
self._horns = None
|
|
|
|
self._fangs = None
|
|
|
|
self._wings = None
|
2022-08-03 00:19:13 -07:00
|
|
|
|
|
|
|
# character
|
|
|
|
self._flaw = None
|
|
|
|
self._goal = None
|
|
|
|
self._personality = None
|
|
|
|
|
2023-12-02 18:24:26 -08:00
|
|
|
if language:
|
|
|
|
self.language = language
|
2022-08-03 00:19:13 -07:00
|
|
|
|
2023-12-02 18:24:26 -08:00
|
|
|
if randomize:
|
|
|
|
self.stats.randomize()
|
2022-08-03 00:19:13 -07:00
|
|
|
|
|
|
|
@property
|
2023-12-02 18:24:26 -08:00
|
|
|
def ancestry(self) -> str:
|
|
|
|
return self.__class__.__name__
|
2022-08-03 00:19:13 -07:00
|
|
|
|
2023-12-02 18:25:45 -08:00
|
|
|
@property
|
|
|
|
def names(self):
|
|
|
|
if not self._names:
|
|
|
|
self._names = next(self.name_generator.name(1))
|
|
|
|
return self._names
|
|
|
|
|
|
|
|
@property
|
|
|
|
def full_name(self):
|
2024-01-19 14:41:26 -08:00
|
|
|
"""Legacy interface"""
|
|
|
|
return self.name
|
2023-12-02 18:25:45 -08:00
|
|
|
|
2022-08-03 00:19:13 -07:00
|
|
|
@property
|
2023-12-02 18:24:26 -08:00
|
|
|
def is_noble(self) -> bool:
|
|
|
|
return self._is_noble
|
2022-08-03 00:19:13 -07:00
|
|
|
|
|
|
|
@property
|
2023-12-02 18:24:26 -08:00
|
|
|
def name(self):
|
|
|
|
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']
|
2022-08-03 00:19:13 -07:00
|
|
|
|
|
|
|
@property
|
|
|
|
def pronouns(self):
|
|
|
|
if not self._pronouns:
|
|
|
|
self._pronouns = random.choice([
|
|
|
|
'he/him',
|
|
|
|
'she/her',
|
|
|
|
'they/they',
|
|
|
|
])
|
|
|
|
return self._pronouns
|
|
|
|
|
|
|
|
@property
|
|
|
|
def title(self):
|
2023-11-19 21:19:53 -08:00
|
|
|
return ' '.join(self.names.titles)
|
2022-08-03 00:19:13 -07:00
|
|
|
|
|
|
|
@property
|
|
|
|
def nickname(self):
|
2023-11-19 21:19:53 -08:00
|
|
|
return ', '.join(self.names.nicknames)
|
2022-08-03 00:19:13 -07:00
|
|
|
|
|
|
|
@property
|
|
|
|
def whereabouts(self):
|
|
|
|
return self._whereabouts
|
|
|
|
|
|
|
|
@property
|
|
|
|
def flaw(self):
|
|
|
|
if self._flaw is None:
|
|
|
|
self._flaw = random.choice(traits.flaws)
|
|
|
|
return self._flaw
|
|
|
|
|
|
|
|
@property
|
|
|
|
def goal(self):
|
|
|
|
if self._goal is None:
|
|
|
|
self._goal = random.choice(traits.goals)
|
|
|
|
return self._goal
|
|
|
|
|
|
|
|
@property
|
|
|
|
def personality(self):
|
|
|
|
if self._personality is None:
|
|
|
|
self._personality = ', '.join([
|
|
|
|
random.choice(traits.personality),
|
|
|
|
random.choice(traits.personality),
|
|
|
|
random.choice(traits.personality),
|
|
|
|
])
|
|
|
|
return self._personality
|
|
|
|
|
|
|
|
@property
|
|
|
|
def eyes(self):
|
|
|
|
if self._eyes is None:
|
|
|
|
self._eyes = ', '.join([random.choice(traits.eye_shape), random.choice(traits.eye_color)])
|
|
|
|
return self._eyes
|
|
|
|
|
|
|
|
@property
|
|
|
|
def skin_color(self):
|
|
|
|
if self._skin_color is None:
|
|
|
|
self._skin_color = random.choice(traits.skin_color)
|
|
|
|
return self._skin_color
|
|
|
|
|
|
|
|
@property
|
|
|
|
def skin_tone(self):
|
|
|
|
if self._skin_tone is None:
|
|
|
|
self._skin_tone = random.choice(traits.skin_tone)
|
|
|
|
return self._skin_tone
|
|
|
|
|
|
|
|
@property
|
|
|
|
def hair(self):
|
|
|
|
if self._hair is None:
|
|
|
|
self._hair = ' '.join([random.choice(traits.hair_style), random.choice(traits.hair_color)])
|
|
|
|
return self._hair
|
|
|
|
|
|
|
|
@property
|
|
|
|
def face(self):
|
|
|
|
if not self._face:
|
|
|
|
self._face = random.choice(traits.face)
|
|
|
|
return self._face
|
|
|
|
|
|
|
|
@property
|
|
|
|
def facial_structure(self):
|
|
|
|
if self._facial_structure is None:
|
|
|
|
self._facial_structure = random.choice(traits.facial_structure)
|
|
|
|
return self._facial_structure
|
|
|
|
|
|
|
|
@property
|
|
|
|
def lips(self):
|
|
|
|
if self._lips is None:
|
|
|
|
self._lips = random.choice(traits.lips)
|
|
|
|
return self._lips
|
|
|
|
|
|
|
|
@property
|
|
|
|
def teeth(self):
|
|
|
|
if self._teeth is None:
|
|
|
|
self._teeth = random.choice(traits.teeth)
|
|
|
|
return self._teeth
|
|
|
|
|
|
|
|
@property
|
|
|
|
def nose(self):
|
|
|
|
if self._nose is None:
|
|
|
|
self._nose = random.choice(traits.nose)
|
|
|
|
return self._nose
|
|
|
|
|
|
|
|
@property
|
|
|
|
def eyebrows(self):
|
|
|
|
if self._eyebrows is None:
|
|
|
|
self._eyebrows = random.choice(traits.eyebrows)
|
|
|
|
return self._eyebrows
|
|
|
|
|
|
|
|
@property
|
|
|
|
def facial_hair(self):
|
|
|
|
if self._facial_hair is None:
|
|
|
|
self._facial_hair = random.choice(traits.facial_hair)
|
|
|
|
return self._facial_hair
|
|
|
|
|
|
|
|
@property
|
|
|
|
def body(self):
|
|
|
|
if self._body is None:
|
|
|
|
self._body = random.choice(traits.body)
|
|
|
|
return self._body
|
|
|
|
|
|
|
|
@property
|
|
|
|
def tail(self):
|
|
|
|
if self._tail is None:
|
|
|
|
self._tail = random.choice(traits.tail)
|
|
|
|
return self._tail
|
|
|
|
|
|
|
|
@property
|
|
|
|
def horns(self):
|
|
|
|
if self._horns is None:
|
|
|
|
self._horns = random.choice(traits.horns)
|
|
|
|
return self._horns
|
|
|
|
|
|
|
|
@property
|
|
|
|
def wings(self):
|
|
|
|
if self._wings is None:
|
|
|
|
self._wings = random.choice(traits.wings)
|
|
|
|
return self._wings
|
|
|
|
|
|
|
|
@property
|
|
|
|
def fangs(self):
|
|
|
|
if self._fangs is None:
|
|
|
|
self._fangs = random.choice(traits.fangs)
|
|
|
|
return self._fangs
|
|
|
|
|
|
|
|
@property
|
|
|
|
def age(self):
|
|
|
|
if not self._age:
|
|
|
|
self._age = random.choice(traits.age)
|
|
|
|
return self._age
|
|
|
|
|
|
|
|
@property
|
|
|
|
def voice(self):
|
|
|
|
if not self._voice:
|
|
|
|
self._voice = random.choice(traits.voice)
|
|
|
|
return self._voice
|
|
|
|
|
2023-12-02 18:24:26 -08:00
|
|
|
@property
|
|
|
|
def stats(self):
|
|
|
|
return self._stats
|
|
|
|
|
2022-08-03 00:19:13 -07:00
|
|
|
@property
|
|
|
|
def description(self):
|
|
|
|
desc = (
|
2023-12-02 18:24:26 -08:00
|
|
|
f"{self.name} ({self.pronouns}) is {a_or_an(self.age)} {self.age}, {self.body} "
|
2022-08-03 00:19:13 -07:00
|
|
|
f"{self.ancestry.lower()} with {self.hair} hair, {self.eyes} eyes and {self.skin_color} skin."
|
|
|
|
)
|
|
|
|
trait = None
|
|
|
|
while not trait:
|
|
|
|
trait = random.choice([
|
|
|
|
f'{self.eyebrows} eyebrows' if self.eyebrows else None,
|
|
|
|
self.facial_hair if self.facial_hair else None,
|
|
|
|
f'a {self.nose} nose' if self.nose else None,
|
|
|
|
f'{self.lips} lips' if self.lips else None,
|
|
|
|
f'{self.teeth} teeth' if self.teeth else None,
|
|
|
|
self.facial_structure if self.facial_structure else None,
|
|
|
|
])
|
|
|
|
desc = desc + ' ' + f"Their face is {self.face}, with {trait}."
|
2023-12-02 18:24:26 -08:00
|
|
|
if self.has_tail:
|
2022-08-03 00:19:13 -07:00
|
|
|
desc = desc + f" Their tail is {self.tail}."
|
2023-12-02 18:24:26 -08:00
|
|
|
if self.has_horns:
|
2022-08-03 00:19:13 -07:00
|
|
|
desc = desc + f" Their horns are {self.horns}."
|
|
|
|
return desc
|
|
|
|
|
|
|
|
@property
|
|
|
|
def character_sheet(self):
|
|
|
|
desc = '\n'.join(textwrap.wrap(self.description, width=120))
|
|
|
|
return f"""\
|
|
|
|
|
|
|
|
{desc}
|
|
|
|
|
|
|
|
Physical Traits:
|
|
|
|
|
|
|
|
Face: {self.face}, {self.eyebrows} eyebrows, {self.nose} nose, {self.lips} lips,
|
|
|
|
{self.teeth} teeth, {self.facial_hair}
|
|
|
|
Eyes: {self.eyes}
|
|
|
|
Skin: {self.skin_tone}, {self.skin_color}
|
|
|
|
Hair: {self.hair}
|
|
|
|
Body: {self.body}
|
|
|
|
Tail: {self.tail}
|
|
|
|
Horns: {self.horns}
|
|
|
|
Fangs: {self.fangs}
|
|
|
|
Wings: {self.wings}
|
|
|
|
Voice: {self.voice}
|
|
|
|
|
|
|
|
Stats:
|
2023-12-02 18:24:26 -08:00
|
|
|
{textwrap.indent(str(self.stats), prefix=' ')}
|
2022-08-03 00:19:13 -07:00
|
|
|
|
|
|
|
Details:
|
|
|
|
|
|
|
|
Personality: {self.personality}
|
|
|
|
Flaw: {self.flaw}
|
|
|
|
Goal: {self.goal}
|
|
|
|
|
|
|
|
Whereabouts: {self.whereabouts}
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __repr__(self):
|
2023-12-02 18:24:26 -08:00
|
|
|
return self.description
|