Files
dungeon-sheets/dungeonsheets/character.py
T
2018-04-14 01:28:46 -05:00

460 lines
16 KiB
Python

"""Tools for describing a player character."""
import re
import warnings
from .stats import Ability, Skill, findattr
from .dice import read_dice_str
from . import weapons, race, spells, armor
from .weapons import Weapon
from .armor import Armor, NoArmor, Shield, NoShield
dice_re = re.compile('(\d+)d(\d+)')
class Character():
"""A generic player character. Intended to be subclasses by the
various classes.
"""
# General attirubtes
name = ""
class_name = ""
player_name = ""
background = ""
level = 1
alignment = "Neutral"
race = None
xp = 0
# Hit points
hp_max = 10
hit_dice_faces = 2
# Base stats (ability scores)
strength = Ability()
dexterity = Ability()
constitution = Ability()
intelligence = Ability()
wisdom = Ability()
charisma = Ability()
saving_throw_proficiencies = []
skill_proficiencies = tuple()
weapon_proficiencies = tuple()
proficiencies_extra = tuple()
languages = ""
# Skills
acrobatics = Skill(ability='dexterity')
animal_handling = Skill(ability='wisdom')
arcana = Skill(ability='intelligence')
athletics = Skill(ability='strength')
deception = Skill(ability='charisma')
history = Skill(ability='intelligence')
insight = Skill(ability='wisdom')
intimidation = Skill(ability='charisma')
investigation = Skill(ability='intelligence')
medicine = Skill(ability='wisdom')
nature = Skill(ability='intelligence')
perception = Skill(ability='wisdom')
performance = Skill(ability='charisma')
persuasian = Skill(ability='charisma')
religion = Skill(ability='intelligence')
sleight_of_hand = Skill(ability='dexterity')
stealth = Skill(ability='dexterity')
survival = Skill(ability='wisdom')
# Characteristics
attacks_and_spellcasting = ""
personality_traits = ""
ideals = ""
bonds = ""
flaws = ""
features_and_traits = ""
# Inventory
cp = 0
sp = 0
ep = 0
gp = 0
pp = 0
equipment = ""
weapons = [] # Replaced in __init__ constructor
armor = None
shield = None
_proficiencies_text = tuple()
# Magic
spellcasting_ability = None
spells = tuple()
spells_prepared = tuple()
def __init__(self, **attrs):
"""Takes a bunch of attrs and passes them to ``set_attrs``"""
self.weapons = []
self.set_attrs(**attrs)
def __str__(self):
return self.name
def __repr__(self):
return f"<{self.class_name}: {self.name}>"
@property
def speed(self):
return self.race.speed
def set_attrs(self, **attrs):
"""Bulk setting of attributes. Useful for loading a character from a
dictionary."""
for attr, val in attrs.items():
if attr == 'weapons':
# Treat weapons specially
for weap in val:
self.wield_weapon(weap)
elif attr == 'race':
MyRace = findattr(race, val)
self.race = MyRace()
elif attr == 'armor':
self.wear_armor(val)
elif attr == 'shield':
self.wield_shield(val)
elif (attr == 'spells') or (attr == 'spells_prepared'):
# Create a list of actual spell objects
_spells = []
for spell_name in val:
try:
_spells.append(findattr(spells, spell_name))
except AttributeError:
msg = f'Spell "{spell_name}" not defined. Please add it to ``spells.py``'
warnings.warn(msg)
# Create temporary spell
_spells.append(spells.create_spell(name=spell_name, level=9))
# raise AttributeError(msg)
if attr == 'spells':
# Instantiate them all for the spells list
self.spells = tuple(S() for S in _spells)
else:
self.spells_prepared = tuple(_spells)
else:
if not hasattr(self, attr):
warnings.warn(f"Setting unknown character attribute {attr}",
RuntimeWarning)
# Lookup general attributes
setattr(self, attr, val)
@property
def is_spellcaster(self):
result = (self.spellcasting_ability is not None)
return result
@property
def spell_save_dc(self):
ability_mod = getattr(self, self.spellcasting_ability).modifier
return (8 + self.proficiency_bonus + ability_mod)
@property
def spell_attack_bonus(self):
ability_mod = getattr(self, self.spellcasting_ability).modifier
return (self.proficiency_bonus + ability_mod)
def spell_slots(self, spell_level):
"""How many spells slots are available for this spell level."""
return self.spell_slots_by_level[self.level][spell_level]
def is_proficient(self, weapon: Weapon):
"""Is the character proficient with this item?
Considers class proficiencies and race proficiencies.
Parameters
----------
weapon
The weapon to be tested for proficiency.
"""
all_proficiencies = tuple(self.weapon_proficiencies)
all_proficiencies += tuple(getattr(self.race, 'weapon_proficiencies', tuple()))
is_proficient = any((isinstance(weapon, W) for W in all_proficiencies))
return is_proficient
@property
def proficiencies_text(self):
final_text = ""
all_proficiencies = self._proficiencies_text
if self.race is not None:
all_proficiencies += self.race.proficiencies_text
all_proficiencies += self.proficiencies_extra
# Create a single string out of all the proficiencies
for txt in all_proficiencies:
if not final_text:
# Capitalize the first entry
txt = txt.capitalize()
else:
# Put a comma first
txt = ", " + txt
# Add this item to the list text
final_text += txt
# Add a period at the end
final_text += '.'
return final_text
def wear_armor(self, new_armor):
"""Accepts a string or Armor class and replaces the current armor.
If a string is given, then a subclass of
:py:class:`~dungeonsheets.armor.Armor` is retrived from the
``armor.py`` file. Otherwise, an subclass of
:py:class:`~dungeonsheets.armor.Armor` can be provided
directly.
"""
try:
NewArmor = findattr(armor, new_armor)
except AttributeError:
# Not a string, so just treat it as Armor
NewArmor = new_armor
self.armor = NewArmor()
def wield_shield(self, shield):
"""Accepts a string or Shield class and replaces the current armor.
If a string is given, then a subclass of
:py:class:`~dungeonsheets.armor.Shield` is retrived from the
``armor.py`` file. Otherwise, an subclass of
:py:class:`~dungeonsheets.armor.Shield` can be provided
directly.
"""
try:
NewShield = findattr(armor, shield)
except AttributeError:
# Not a string, so just treat it as Armor
NewShield = shield
self.shield = NewShield()
def wield_weapon(self, weapon):
"""Accepts a string and adds it to the list of wielded weapons.
Parameters
----------
weapon : str
Case-insensitive string with a name of the weapon.
"""
# Retrieve the weapon class from the weapons module
try:
NewWeapon = findattr(weapons, weapon)
except AttributeError:
raise AttributeError(f'Weapon {weapon} is not defined')
weapon_ = NewWeapon()
# Set weapon attributes based on character
if weapon_.is_finesse:
ability_mod = max(self.strength.modifier, self.dexterity.modifier)
else:
ability_mod = getattr(self, weapon_.ability).modifier
weapon_.attack_bonus += ability_mod
weapon_.bonus_damage += ability_mod
# Check for prifiency
if self.is_proficient(weapon_):
weapon_.attack_bonus += self.proficiency_bonus
# Save it to the array
self.weapons.append(weapon_)
@property
def hit_dice(self):
"""What type and how many dice to use for re-gaining hit points.
To change, set hit_dice_num and hit_dice_faces."""
return f"{self.level}d{self.hit_dice_faces}"
@property
def proficiency_bonus(self):
if self.level < 5:
prof = 2
elif 5 <= self.level < 9:
prof = 3
elif 9 <= self.level < 13:
prof = 4
elif 13 <= self.level < 17:
prof = 5
elif 17 <= self.level:
prof = 6
return prof
@property
def armor_class(self):
"""Armor class, including contributions from worn armor and shield."""
# Retrieve current armor (or a generic armor substitute)
armor = self.armor if self.armor is not None else NoArmor()
shield = self.shield if self.shield is not None else NoShield()
# Calculate and apply modifiers
if armor.dexterity_mod_max is None:
modifier = self.dexterity.modifier
else:
modifier = min(self.dexterity.modifier, armor.dexterity_mod_max)
# Calculate final armor class
ac = armor.base_armor_class + shield.base_armor_class + modifier
return ac
class Barbarian(Character):
class_name = 'Barbarian'
hit_dice_faces = 12
saving_throw_proficiencies = ('strength', 'constitution')
_proficiencies_text = ('light armor', 'medium armor', 'shields',
'simple weapons', 'martial weapons')
weapon_proficiencies = (weapons.simple_weapons + weapons.martial_weapons)
class Bard(Character):
class_name = 'Bard'
hit_dice_faces = 8
saving_throw_proficiencies = ('dexterity', 'charisma')
_proficiencies_text = (
'Light armor', 'simple weapons', 'hand crossbows', 'longswords',
'rapiers', 'shortswords', 'three musical instruments of your choice')
weapon_proficiencies = ((weapons.HandCrossbow, weapons.Longsword,
weapons.Rapier, weapons.Shortsword) +
weapons.simple_weapons)
class Cleric(Character):
class_name = 'Cleric'
hit_dice_faces = 8
saving_throw_proficiencies = ('wisdom', 'charisma')
_proficiencies_text = ('light armor', 'medium armor', 'shields',
'all simple weapons')
weapon_proficiencies = weapons.simple_weapons
class Druid(Character):
class_name = 'Druid'
hit_dice_faces = 8
saving_throw_proficiencies = ('intelligence', 'wisdom')
_proficiencies_text = (
'Light armor', 'medium armor',
'shields (druids will not wear armor or use shields made of metal)',
'clubs', 'daggers', 'darts', 'javelins', 'maces', 'quarterstaffs',
'scimitars', 'sickles', 'slings', 'spears')
weapon_proficiencies = (weapons.Club, weapons.Dagger, weapons.Dart,
weapons.Javelin, weapons.Mace, weapons.Quarterstaff,
weapons.Scimitar, weapons.Sickle, weapons.Sling, weapons.Spear)
class Fighter(Character):
class_name = 'Fighter'
hit_dice_faces = 10
saving_throw_proficiencies = ('strength', 'constitution')
_proficiencies_text = ('All armar', 'shields', 'simple weapons', 'martial weapons')
weapon_proficiencies = weapons.simple_weapons + weapons.martial_weapons
class Monk(Character):
class_name = 'Monk'
hit_dice_faces = 8
saving_throw_proficiencies = ('strength', 'dexterity')
_proficiencies_text = (
'simple weapons', 'shortswords',
"one type of artisan's tools or one musical instrument")
weapon_proficiencies = (weapons.Shortsword,) + weapons.simple_weapons
class Paladin(Character):
class_name = 'Paladin'
hit_dice_faces = 10
saving_throw_proficiencies = ('wisdom', 'charisma')
_proficiencies_text = ('All armor', 'shields', 'simple weapons',
'martial weapons')
weapon_proficiencies = weapons.simple_weapons + weapons.martial_weapons
class Ranger(Character):
class_name = 'Ranger'
hit_dice_faces = 10
saving_throw_proficiencies = ('strength', 'dexterity')
_proficiencies_text = ("light armor", "medium armor", "shields",
"simple weapons", "martial weapons")
weapon_proficiencies = weapons.simple_weapons + weapons.martial_weapons
class Rogue(Character):
class_name = 'Rogue'
hit_dice_faces = 8
saving_throw_proficiencies = ('dexterity', 'intelligence')
_proficiencies_text = (
'light armor', 'simple weapons', 'hand crossbows', 'longswords',
'rapiers', 'shortswords', "thieves' tools")
weapon_proficiencies = (weapons.HandCrossbow, weapons.Longsword,
weapons.Rapier, weapons.Shortsword) + weapons.simple_weapons
class Sorceror(Character):
class_name = 'Sorceror'
hit_dice_faces = 6
saving_throw_proficiencies = ('constitution', 'charisma')
_proficiencies_text = ('daggers', 'darts', 'slings',
'quarterstaffs', 'light crossbows')
weapon_proficiencies = (weapons.Dagger, weapons.Dart,
weapons.Sling, weapons.Quarterstaff,
weapons.LightCrossbow)
class Warlock(Character):
class_name = 'Warlock'
hit_dice_faces = 8
saving_throw_proficiencies = ('wisdom', 'charisma')
_proficiencies_text = ("light Armor", "simple weapons")
weapon_proficiencies = weapons.simple_weapons
spellcasting_ability = 'charisma'
spell_slots_by_level = {
1: (2, 1, 0, 0, 0, 0, 0, 0, 0, 0),
2: (2, 2, 0, 0, 0, 0, 0, 0, 0, 0),
3: (2, 0, 2, 0, 0, 0, 0, 0, 0, 0),
4: (3, 0, 2, 0, 0, 0, 0, 0, 0, 0),
5: (3, 0, 0, 3, 0, 0, 0, 0, 0, 0),
6: (3, 0, 0, 3, 0, 0, 0, 0, 0, 0),
7: (3, 0, 0, 0, 2, 0, 0, 0, 0, 0),
8: (3, 0, 0, 0, 2, 0, 0, 0, 0, 0),
9: (3, 0, 0, 0, 0, 2, 0, 0, 0, 0),
10: (4, 0, 0, 0, 0, 2, 0, 0, 0, 0),
11: (4, 0, 0, 0, 0, 3, 0, 0, 0, 0),
12: (4, 0, 0, 0, 0, 3, 0, 0, 0, 0),
13: (4, 0, 0, 0, 0, 3, 0, 0, 0, 0),
14: (4, 0, 0, 0, 0, 3, 0, 0, 0, 0),
15: (4, 0, 0, 0, 0, 3, 0, 0, 0, 0),
16: (4, 0, 0, 0, 0, 3, 0, 0, 0, 0),
17: (4, 0, 0, 0, 0, 4, 0, 0, 0, 0),
18: (4, 0, 0, 0, 0, 4, 0, 0, 0, 0),
19: (4, 0, 0, 0, 0, 4, 0, 0, 0, 0),
20: (4, 0, 0, 0, 0, 4, 0, 0, 0, 0),
}
class Wizard(Character):
class_name = 'Wizard'
hit_dice_faces = 6
saving_throw_proficiencies = ('intelligence', 'wisdom')
_proficiencies_text = ('daggers', 'darts', 'slings',
'quarterstaffs', 'light crossbows')
weapon_proficiencies = (weapons.Dagger, weapons.Dart,
weapons.Sling, weapons.Quarterstaff,
weapons.LightCrossbow)
spellcasting_ability = 'intelligence'
spell_slots_by_level = {
# char_lvl: (cantrips, 1st, 2nd, 3rd, ...)
1: (3, 2, 0, 0, 0, 0, 0, 0, 0, 0),
2: (3, 3, 0, 0, 0, 0, 0, 0, 0, 0),
3: (3, 4, 2, 0, 0, 0, 0, 0, 0, 0),
4: (4, 4, 3, 0, 0, 0, 0, 0, 0, 0),
5: (4, 4, 3, 2, 0, 0, 0, 0, 0, 0),
6: (4, 4, 3, 3, 0, 0, 0, 0, 0, 0),
7: (4, 4, 3, 3, 1, 0, 0, 0, 0, 0),
8: (4, 4, 3, 3, 2, 0, 0, 0, 0, 0),
9: (4, 4, 3, 3, 3, 1, 0, 0, 0, 0),
10: (5, 4, 3, 3, 3, 2, 0, 0, 0, 0),
11: (5, 4, 3, 3, 3, 2, 1, 0, 0, 0),
12: (5, 4, 3, 3, 3, 2, 1, 0, 0, 0),
13: (5, 4, 3, 3, 3, 2, 1, 1, 0, 0),
14: (5, 4, 3, 3, 3, 2, 1, 1, 0, 0),
15: (5, 4, 3, 3, 3, 2, 1, 1, 1, 0),
16: (5, 4, 3, 3, 3, 2, 1, 1, 1, 0),
17: (5, 4, 3, 3, 3, 2, 1, 1, 1, 1),
18: (5, 4, 3, 3, 3, 3, 1, 1, 1, 1),
19: (5, 4, 3, 3, 3, 3, 2, 1, 1, 1),
20: (5, 4, 3, 3, 3, 3, 2, 2, 1, 1),
}