first features implemented

This commit is contained in:
Ben Cook
2018-12-19 16:56:04 -05:00
parent db76a1924a
commit 0ed7262fc8
11 changed files with 317 additions and 16 deletions
+4
View File
@@ -3,6 +3,10 @@ Add multiclass proficiencies to classes
Add Warlock Incantations Add Warlock Incantations
Add Warlock multiclass spell slots Add Warlock multiclass spell slots
Add disadvantage on STEALTH with armor
Add race / class AC bonuses
Add subclasses Add subclasses
Add features Add features
Auto-add features to PDF Auto-add features to PDF
+11 -1
View File
@@ -154,10 +154,20 @@ class HeavySplintArmor(Armor):
class HeavyPlateArmor(Armor): class HeavyPlateArmor(Armor):
name = "Heavy splint armor" name = "Heavy plate armor"
cost = "1,500 gp" cost = "1,500 gp"
base_armor_class = 18 base_armor_class = 18
dexterity_mod_max = 0 dexterity_mod_max = 0
strength_required = 15 strength_required = 15
stealth_disadvantage = True stealth_disadvantage = True
weight = 65 weight = 65
# Custom Armor
class ElvenChain(Armor):
name = 'Elven Chain'
cost = '5,000 gp'
base_armor_class = 14
dexerity_mod_max = 2
weight = 20
+15
View File
@@ -1,8 +1,12 @@
from . import features as feats
class Background(): class Background():
name = "Generic background" name = "Generic background"
skill_proficiencies = () skill_proficiencies = ()
weapon_proficiencies = () weapon_proficiencies = ()
proficiencies_text = () proficiencies_text = ()
features = ()
languages = () languages = ()
def __str__(self): def __str__(self):
@@ -98,3 +102,14 @@ class Soldier(Background):
class Urchin(Background): class Urchin(Background):
name = "Urchin" name = "Urchin"
skill_proficiencies = ('sleight of hand', 'stealth') skill_proficiencies = ('sleight of hand', 'stealth')
class UrbanBountyHunter(Background):
name = 'Urban Bounty Hunter'
skill_proficiencies = ('[choose one]', '[choose one]')
class FarTraveler(Background):
name = 'Far Traveler'
skill_proficiencies = ('insight', 'perception')
languages = ('[choose one]',)
+57 -7
View File
@@ -9,7 +9,7 @@ import importlib.util
from .stats import Ability, Skill, findattr from .stats import Ability, Skill, findattr
from .dice import read_dice_str from .dice import read_dice_str
from . import (weapons, race, background, spells, armor, monsters, from . import (weapons, race, background, spells, armor, monsters,
exceptions, classes) exceptions, classes, features)
from .weapons import Weapon from .weapons import Weapon
from .armor import Armor, NoArmor, Shield, NoShield from .armor import Armor, NoArmor, Shield, NoShield
@@ -62,7 +62,9 @@ class Character():
intelligence = Ability() intelligence = Ability()
wisdom = Ability() wisdom = Ability()
charisma = Ability() charisma = Ability()
other_weapon_proficiencies = tuple()
skill_proficiencies = tuple() skill_proficiencies = tuple()
skill_expertise = tuple()
class_skill_choices = tuple() class_skill_choices = tuple()
num_skill_choices = 2 num_skill_choices = 2
proficiencies_extra = tuple() proficiencies_extra = tuple()
@@ -108,7 +110,8 @@ class Character():
spellcasting_ability = None spellcasting_ability = None
spells = tuple() spells = tuple()
spells_prepared = tuple() spells_prepared = tuple()
# MISC # Features IN MAJOR DEVELOPMENT
other_features = ()
def __init__(self, **attrs): def __init__(self, **attrs):
"""Takes a bunch of attrs and passes them to ``set_attrs``""" """Takes a bunch of attrs and passes them to ``set_attrs``"""
@@ -158,9 +161,10 @@ class Character():
@property @property
def weapon_proficiencies(self): def weapon_proficiencies(self):
wp = set(self.other_weapon_proficiencies)
if not self.class_initialized: if not self.class_initialized:
return () return wp
wp = set(self.primary_class.weapon_proficiencies) wp |= set(self.primary_class.weapon_proficiencies)
if self.num_classes > 1: if self.num_classes > 1:
for c in self.class_list[1:]: for c in self.class_list[1:]:
wp |= set(c.multiclass_weapon_proficiencies) wp |= set(c.multiclass_weapon_proficiencies)
@@ -168,7 +172,20 @@ class Character():
wp |= set(getattr(self.race, 'weapon_proficiencies', ())) wp |= set(getattr(self.race, 'weapon_proficiencies', ()))
if self.background is not None: if self.background is not None:
wp |= set(getattr(self.background, 'weapon_proficiencies', ())) wp |= set(getattr(self.background, 'weapon_proficiencies', ()))
return wp return tuple(wp)
@property
def features(self):
fts = set(self.other_features)
if not self.class_initialized:
return fts
for c in self.class_list:
fts |= set(c.features)
if self.race is not None:
fts |= set(getattr(self.race, 'features', ()))
if self.background is not None:
fts |= set(getattr(self.background, 'features', ()))
return tuple(fts)
@property @property
def saving_throw_proficiencies(self): def saving_throw_proficiencies(self):
@@ -216,6 +233,9 @@ class Character():
# Treat weapons specially # Treat weapons specially
for weap in val: for weap in val:
self.wield_weapon(weap) self.wield_weapon(weap)
elif attr == 'weapon_proficiencies':
self.other_weapon_proficiencies = tuple([findattr(weapons, w)
for w in val])
elif attr == 'race': elif attr == 'race':
MyRace = findattr(race, val) MyRace = findattr(race, val)
self.race = MyRace() self.race = MyRace()
@@ -240,6 +260,18 @@ class Character():
c.circle = val c.circle = val
self.circle = val self.circle = val
break break
elif attr == 'features':
if isinstance(val, str):
val = [val]
_features = []
for f in val:
try:
_features.append(findattr(features, f)())
except AttributeError:
msg = (f'Feature "{f}" not defined. '
f'Please add it to ``features.py``')
warnings.warn(msg)
self.other_features = tuple(_features)
elif (attr == 'spells') or (attr == 'spells_prepared'): elif (attr == 'spells') or (attr == 'spells_prepared'):
# Create a list of actual spell objects # Create a list of actual spell objects
_spells = [] _spells = []
@@ -287,6 +319,10 @@ class Character():
weapon weapon
The weapon to be tested for proficiency. The weapon to be tested for proficiency.
Returns
-------
Boolean: is this character proficient with this weapon?
""" """
all_proficiencies = self.weapon_proficiencies all_proficiencies = self.weapon_proficiencies
is_proficient = any((isinstance(weapon, W) for W in all_proficiencies)) is_proficient = any((isinstance(weapon, W) for W in all_proficiencies))
@@ -320,6 +356,14 @@ class Character():
final_text += '.' final_text += '.'
return final_text return final_text
@property
def features_text(self):
s = '\n\n*'.join([f.name for f in self.features])
if s != '':
s = '(See Features Details Page)\n\n*' + s
s += '\n\n=================\n\n'
return s
def wear_armor(self, new_armor): def wear_armor(self, new_armor):
"""Accepts a string or Armor class and replaces the current armor. """Accepts a string or Armor class and replaces the current armor.
@@ -381,6 +425,11 @@ class Character():
# Check for prifiency # Check for prifiency
if self.is_proficient(weapon_): if self.is_proficient(weapon_):
weapon_.attack_bonus += self.proficiency_bonus weapon_.attack_bonus += self.proficiency_bonus
# check if features add any bonuses
for f in self.features:
a_bonus, d_bonus = f.weapon_func(weapon_)
weapon_.attack_bonus += a_bonus
weapon_.bonus_damage += d_bonus
# Save it to the array # Save it to the array
self.weapons.append(weapon_) self.weapons.append(weapon_)
@@ -422,8 +471,9 @@ class Character():
else: else:
modifier = min(self.dexterity.modifier, armor.dexterity_mod_max) modifier = min(self.dexterity.modifier, armor.dexterity_mod_max)
# Calculate final armor class # Calculate final armor class
ac = armor.base_armor_class + shield.base_armor_class + modifier ac = [armor.base_armor_class + shield.base_armor_class + modifier]
return ac ac += [f.AC_func(self) for f in self.features]
return max(ac)
@classmethod @classmethod
def load(cls, character_file): def load(cls, character_file):
+33 -1
View File
@@ -1,8 +1,10 @@
__all__ = ('Barbarian', 'Bard', 'Cleric', 'Druid', 'Fighter', 'Monk', __all__ = ('Barbarian', 'Bard', 'Cleric', 'Druid', 'Fighter', 'Monk',
'Paladin', 'Ranger', 'Rogue', 'Sorceror', 'Warlock', 'Wizard', ) 'Paladin', 'Ranger', 'Rogue', 'Sorceror', 'Warlock', 'Wizard',
'Revisedranger')
from .stats import findattr from .stats import findattr
from . import (weapons, monsters, exceptions) from . import (weapons, monsters, exceptions)
from . import features as feats
import math import math
import warnings import warnings
@@ -18,6 +20,7 @@ class CharClass():
_proficiencies_text = () _proficiencies_text = ()
multiclass_weapon_proficiencies = () multiclass_weapon_proficiencies = ()
_multiclass_proficiencies_text = () _multiclass_proficiencies_text = ()
features = ()
languages = () languages = ()
class_skill_choices = () class_skill_choices = ()
num_skill_choices = 2 num_skill_choices = 2
@@ -418,6 +421,30 @@ class Sorceror(CharClass):
weapons.LightCrossbow) weapons.LightCrossbow)
class_skill_choices = ('Arcana', 'Deception', 'Insight', class_skill_choices = ('Arcana', 'Deception', 'Insight',
'Intimidation', 'Persuasion', 'Religion') 'Intimidation', 'Persuasion', 'Religion')
spellcasting_ability = 'charisma'
spell_slots_by_level = {
# char_lvl: (cantrips, 1st, 2nd, 3rd, ...)
1: (4, 2, 0, 0, 0, 0, 0, 0, 0, 0),
2: (4, 3, 0, 0, 0, 0, 0, 0, 0, 0),
3: (4, 4, 2, 0, 0, 0, 0, 0, 0, 0),
4: (5, 4, 3, 0, 0, 0, 0, 0, 0, 0),
5: (5, 4, 3, 2, 0, 0, 0, 0, 0, 0),
6: (5, 4, 3, 3, 0, 0, 0, 0, 0, 0),
7: (5, 4, 3, 3, 1, 0, 0, 0, 0, 0),
8: (5, 4, 3, 3, 2, 0, 0, 0, 0, 0),
9: (5, 4, 3, 3, 3, 1, 0, 0, 0, 0),
10: (6, 4, 3, 3, 3, 2, 0, 0, 0, 0),
11: (6, 4, 3, 3, 3, 2, 1, 0, 0, 0),
12: (6, 4, 3, 3, 3, 2, 1, 0, 0, 0),
13: (6, 4, 3, 3, 3, 2, 1, 1, 0, 0),
14: (6, 4, 3, 3, 3, 2, 1, 1, 0, 0),
15: (6, 4, 3, 3, 3, 2, 1, 1, 1, 0),
16: (6, 4, 3, 3, 3, 2, 1, 1, 1, 0),
17: (6, 4, 3, 3, 3, 2, 1, 1, 1, 1),
18: (6, 4, 3, 3, 3, 3, 1, 1, 1, 1),
19: (6, 4, 3, 3, 3, 3, 2, 1, 1, 1),
20: (6, 4, 3, 3, 3, 3, 2, 2, 1, 1),
}
class Warlock(CharClass): class Warlock(CharClass):
@@ -489,3 +516,8 @@ class Wizard(CharClass):
19: (5, 4, 3, 3, 3, 3, 2, 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), 20: (5, 4, 3, 3, 3, 3, 2, 2, 1, 1),
} }
# Custom Classes
class Revisedranger(Ranger):
class_name = 'Revisedranger'
+82
View File
@@ -0,0 +1,82 @@
from . import weapons, armor
class Feature():
"""
Provide full text of rules in documentation
"""
name = "Generic Feature"
source = '' # race, class, background, etc.
def weapon_func(self, weapon: weapons.Weapon, **kwargs):
"""
Return the attack/damage bonus from having this feature
Parameters
----------
weapon
The weapon to be tested for special bonuses
kwargs
Any other key-word arguments the function may require
Returns
-------
attack bonus : integer attack bonus
damage bonus : integer attack bonus
"""
return (0, 0)
def AC_func(self, char, **kwargs):
"""
Return the alternative AC from having this feat
The character will take max AC from all available feats / standard AC,
so the default is to output very low AC
Parameters
----------
char
Character object, to check for necessary abilities, etc.
kwargs
Any other key-word arguments the function may require
Returns
-------
AC : integer armor class from this feature
"""
return -100
class Archery(Feature):
"""
You gain a +2 bonus to attack rolls you make
with ranged weapons.
"""
name = "Archery"
source = 'Revised Ranger'
def weapon_func(self, weapon: weapons.Weapon):
"""
+2 attack roll bonus if weapon is ranged
"""
return (2, 0) if weapon.is_ranged else (0, 0)
class UnarmoredDefense(Feature):
"""Beginning at 1st level, while you are wearing no armor and not wearing a
shield, your AC equals 10 + your Dexterity modifier + your Wisdom modifier.
"""
name = "Unarmored Defense"
source = "Monk"
def AC_func(self, char):
no_armor = ((char.armor is None)
or (isinstance(char.armor, armor.NoAmor)))
no_shield = ((char.shield is None)
or (isinstance(char.shield, armor.NoShield)))
if no_armor and no_shield:
return 10 + char.dexterity.modifier + char.wisdom.modifier
else:
return -100
+27
View File
@@ -0,0 +1,27 @@
\documentclass[twocolumn,lettersize]{article}
%% \usepackage{fullpage}
\usepackage[margin=1.5cm]{geometry}
\usepackage[dvipsnames]{color}
\definecolor{mygrey}{gray}{0.7}
\title{Features and Traits}
\author{[[ character.name ]]}
\date{}
\begin{document}
\maketitle
[% for feat in character.features %]
\section*{[[ feat.name ]]}
\noindent
\textbf{Source:} [[ feat.source ]] \\
[[ feat.__doc__|rst_to_latex ]]
[% endfor %]
\end{document}
+18 -2
View File
@@ -75,6 +75,11 @@ def create_spellbook_pdf(char, basename):
return create_latex_pdf(char, basename, template) return create_latex_pdf(char, basename, template)
def create_features_pdf(char, basename):
template = jinja_env.get_template('features_template.tex')
return create_latex_pdf(char, basename, template)
def create_latex_pdf(char, basename, template): def create_latex_pdf(char, basename, template):
tex = template.render(character=char) tex = template.render(character=char)
# Create tex document # Create tex document
@@ -95,7 +100,7 @@ def create_latex_pdf(char, basename, template):
try: try:
result = subprocess.run(['pdflatex', '--output-directory', result = subprocess.run(['pdflatex', '--output-directory',
output_dir, tex_file, '-halt-on-error'], output_dir, tex_file, '-halt-on-error'],
stdout=subprocess.DEVNULL, timeout=10) stdout=subprocess.DEVNULL, timeout=30)
except FileNotFoundError: except FileNotFoundError:
# Remove temporary files # Remove temporary files
remove_temp_files(basename) remove_temp_files(basename)
@@ -244,7 +249,7 @@ def create_character_pdf(char, basename, flatten=False):
'Ideals': text_box(char.ideals), 'Ideals': text_box(char.ideals),
'Bonds': text_box(char.bonds), 'Bonds': text_box(char.bonds),
'Flaws': text_box(char.flaws), 'Flaws': text_box(char.flaws),
'Features and Traits': text_box(char.features_and_traits), 'Features and Traits': text_box(char.features_text + char.features_and_traits),
# Inventory # Inventory
'CP': char.cp, 'CP': char.cp,
'SP': char.sp, 'SP': char.sp,
@@ -441,6 +446,17 @@ def make_sheet(character_file, char=None, flatten=False):
os.path.splitext(character_file)[0]) os.path.splitext(character_file)[0])
create_spells_pdf(char=char, basename=spell_base, flatten=flatten) create_spells_pdf(char=char, basename=spell_base, flatten=flatten)
sheets.append(spell_base + '.pdf') sheets.append(spell_base + '.pdf')
if len(char.features) > 0:
feat_base = '{:s}_feats'.format(
os.path.splitext(character_file)[0])
try:
create_features_pdf(char=char, basename=feat_base)
except exceptions.LatexNotFoundError as e:
log.warning('``pdflatex`` not available. Skipping features book '
f'for {char.name}')
else:
sheets.append(feat_base + '.pdf')
if char.is_spellcaster:
# Create spell book # Create spell book
spellbook_base = os.path.splitext(character_file)[0] + '_spellbook' spellbook_base = os.path.splitext(character_file)[0] + '_spellbook'
try: try:
+15 -1
View File
@@ -1,11 +1,12 @@
from . import weapons from . import weapons
from . import features as feats
__all__ = ('Dwarf', 'HillDwarf', 'MountainDwarf', 'Elf', 'HighElf', __all__ = ('Dwarf', 'HillDwarf', 'MountainDwarf', 'Elf', 'HighElf',
'WoodElf', 'DarkElf', 'Halfling', 'LightfootHalfling', 'WoodElf', 'DarkElf', 'Halfling', 'LightfootHalfling',
'StoutHalfling', 'Human', 'Dragonborn', 'Gnome', 'ForestGnome', 'StoutHalfling', 'Human', 'Dragonborn', 'Gnome', 'ForestGnome',
'RockGnome', 'HalfElf', 'HalfOrc', 'Tiefling', 'Aasimar', 'RockGnome', 'HalfElf', 'HalfOrc', 'Tiefling', 'Aasimar',
'FallenAasimar', 'Lizardfolk', 'Kenku') 'FallenAasimar', 'Lizardfolk', 'Kenku', 'Aarakocra')
class Race(): class Race():
@@ -16,6 +17,7 @@ class Race():
proficiencies_text = tuple() proficiencies_text = tuple()
weapon_proficiences = tuple() weapon_proficiences = tuple()
skill_proficiencies = () skill_proficiencies = ()
features = tuple()
strength_bonus = 0 strength_bonus = 0
dexterity_bonus = 0 dexterity_bonus = 0
constitution_bonus = 0 constitution_bonus = 0
@@ -214,3 +216,15 @@ class Kenku(Race):
dexterity_bonus = 2 dexterity_bonus = 2
wisdom_bonus = 1 wisdom_bonus = 1
languages = ('Common', 'Auran') languages = ('Common', 'Auran')
# Aarakocra
class Aarakocra(Race):
name = 'Aarakocra'
size = 'medium'
speed = "25 (50 fly)"
dexterity_bonus = 2
wisdom_bonus = 1
languages = ('Common', 'Aarakocra', 'Auran')
weapon_proficiencies = (weapons.Talons,)
proficiences_text = ('talons',)
+4
View File
@@ -88,4 +88,8 @@ class Skill():
is_proficient = self.skill_name in character.skill_proficiencies is_proficient = self.skill_name in character.skill_proficiencies
if is_proficient: if is_proficient:
modifier += character.proficiency_bonus modifier += character.proficiency_bonus
# Check for expertise
is_expert = self.skill_name in character.skill_expertise
if is_expert:
modifier += character.proficiency_bonus
return modifier return modifier
+50 -3
View File
@@ -1,5 +1,6 @@
from .stats import mod_str from .stats import mod_str
class Weapon(): class Weapon():
name = "" name = ""
cost = "0 gp" cost = "0 gp"
@@ -19,6 +20,10 @@ class Weapon():
dam_str += '' + mod_str(self.bonus_damage) dam_str += '' + mod_str(self.bonus_damage)
return dam_str return dam_str
@property
def is_ranged(self):
return ('range' in self.properties.lower()) and ('thrown' not in self.properties.lower())
class Club(Weapon): class Club(Weapon):
name = "Club" name = "Club"
@@ -127,7 +132,7 @@ class LightCrossbow(Weapon):
base_damage = "1d8" base_damage = "1d8"
damage_type = "p" damage_type = "p"
weight = 5 weight = 5
properties = "Ammunition (range 80/320, loading, two-handed" properties = "Ammunition (range 80/320), loading, two-handed"
ability = 'dexterity' ability = 'dexterity'
@@ -383,7 +388,7 @@ class HeavyCrossbow(Weapon):
damage_type = "p" damage_type = "p"
weight = 18 weight = 18
properties = "Ammunition (range 100/400), heaving, loading, two-handed" properties = "Ammunition (range 100/400), heaving, loading, two-handed"
ability = 'strength' ability = 'dexterity'
class Longbow(Weapon): class Longbow(Weapon):
@@ -393,7 +398,7 @@ class Longbow(Weapon):
damage_type = "p" damage_type = "p"
weight = 2 weight = 2
properties = "Ammunition (range 150/600), heavy, two-handed" properties = "Ammunition (range 150/600), heavy, two-handed"
ability = 'strength' ability = 'dexterity'
class Net(Weapon): class Net(Weapon):
@@ -445,6 +450,46 @@ class Bite(Weapon):
ability = "strength" ability = "strength"
class Talons(Weapon):
name = 'Talons'
base_damage = '1d4'
damage_type = 's'
cost = '0 gp'
weight = 0
properties = ''
ability = 'strength'
class Firearm(Weapon):
name = 'Firearm'
ability = 'dexterity'
damage_type = 'p'
class Blunderbuss(Firearm):
name = 'Blunderbuss'
base_damage = '2d8'
cost = '300 gp'
weight = 10
properties = "Ammunition (range 15/60), Reload 1, Misfire 2"
class Pistol(Firearm):
name = 'Pistol'
base_damage = '1d10'
cost = '150 gp'
weight = 3
properties = "Ammunition (range 60/240), Reload 4, Misfire 1"
class Musket(Firearm):
name = 'Musket'
base_damage = '1d12'
cost = '300'
weight = 10
properties = "Ammunition (range 120/480), Two-Handed, Reload 1, Misfire 2"
# Some lists of weapons for easy proficiency resolution # Some lists of weapons for easy proficiency resolution
simple_melee_weapons = (Club, Dagger, Greatclub, Handaxe, Javelin, simple_melee_weapons = (Club, Dagger, Greatclub, Handaxe, Javelin,
LightHammer, Mace, Quarterstaff, Sickle, Spear) LightHammer, Mace, Quarterstaff, Sickle, Spear)
@@ -459,3 +504,5 @@ martial_melee_weapons = (Battleaxe, Flail, Glaive, Greataxe,
martial_ranged_weapons = (Blowgun, HandCrossbow, HeavyCrossbow, martial_ranged_weapons = (Blowgun, HandCrossbow, HeavyCrossbow,
Longbow, Net) Longbow, Net)
martial_weapons = martial_melee_weapons + martial_ranged_weapons martial_weapons = martial_melee_weapons + martial_ranged_weapons
firearms = (Firearm)