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 multiclass spell slots
Add disadvantage on STEALTH with armor
Add race / class AC bonuses
Add subclasses
Add features
Auto-add features to PDF
+11 -1
View File
@@ -154,10 +154,20 @@ class HeavySplintArmor(Armor):
class HeavyPlateArmor(Armor):
name = "Heavy splint armor"
name = "Heavy plate armor"
cost = "1,500 gp"
base_armor_class = 18
dexterity_mod_max = 0
strength_required = 15
stealth_disadvantage = True
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():
name = "Generic background"
skill_proficiencies = ()
weapon_proficiencies = ()
proficiencies_text = ()
features = ()
languages = ()
def __str__(self):
@@ -98,3 +102,14 @@ class Soldier(Background):
class Urchin(Background):
name = "Urchin"
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 .dice import read_dice_str
from . import (weapons, race, background, spells, armor, monsters,
exceptions, classes)
exceptions, classes, features)
from .weapons import Weapon
from .armor import Armor, NoArmor, Shield, NoShield
@@ -62,7 +62,9 @@ class Character():
intelligence = Ability()
wisdom = Ability()
charisma = Ability()
other_weapon_proficiencies = tuple()
skill_proficiencies = tuple()
skill_expertise = tuple()
class_skill_choices = tuple()
num_skill_choices = 2
proficiencies_extra = tuple()
@@ -108,7 +110,8 @@ class Character():
spellcasting_ability = None
spells = tuple()
spells_prepared = tuple()
# MISC
# Features IN MAJOR DEVELOPMENT
other_features = ()
def __init__(self, **attrs):
"""Takes a bunch of attrs and passes them to ``set_attrs``"""
@@ -158,9 +161,10 @@ class Character():
@property
def weapon_proficiencies(self):
wp = set(self.other_weapon_proficiencies)
if not self.class_initialized:
return ()
wp = set(self.primary_class.weapon_proficiencies)
return wp
wp |= set(self.primary_class.weapon_proficiencies)
if self.num_classes > 1:
for c in self.class_list[1:]:
wp |= set(c.multiclass_weapon_proficiencies)
@@ -168,7 +172,20 @@ class Character():
wp |= set(getattr(self.race, 'weapon_proficiencies', ()))
if self.background is not None:
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
def saving_throw_proficiencies(self):
@@ -216,6 +233,9 @@ class Character():
# Treat weapons specially
for weap in val:
self.wield_weapon(weap)
elif attr == 'weapon_proficiencies':
self.other_weapon_proficiencies = tuple([findattr(weapons, w)
for w in val])
elif attr == 'race':
MyRace = findattr(race, val)
self.race = MyRace()
@@ -240,6 +260,18 @@ class Character():
c.circle = val
self.circle = val
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'):
# Create a list of actual spell objects
_spells = []
@@ -287,6 +319,10 @@ class Character():
weapon
The weapon to be tested for proficiency.
Returns
-------
Boolean: is this character proficient with this weapon?
"""
all_proficiencies = self.weapon_proficiencies
is_proficient = any((isinstance(weapon, W) for W in all_proficiencies))
@@ -320,6 +356,14 @@ class Character():
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):
"""Accepts a string or Armor class and replaces the current armor.
@@ -381,6 +425,11 @@ class Character():
# Check for prifiency
if self.is_proficient(weapon_):
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
self.weapons.append(weapon_)
@@ -422,8 +471,9 @@ class Character():
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
ac = [armor.base_armor_class + shield.base_armor_class + modifier]
ac += [f.AC_func(self) for f in self.features]
return max(ac)
@classmethod
def load(cls, character_file):
+33 -1
View File
@@ -1,8 +1,10 @@
__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 . import (weapons, monsters, exceptions)
from . import features as feats
import math
import warnings
@@ -18,6 +20,7 @@ class CharClass():
_proficiencies_text = ()
multiclass_weapon_proficiencies = ()
_multiclass_proficiencies_text = ()
features = ()
languages = ()
class_skill_choices = ()
num_skill_choices = 2
@@ -418,6 +421,30 @@ class Sorceror(CharClass):
weapons.LightCrossbow)
class_skill_choices = ('Arcana', 'Deception', 'Insight',
'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):
@@ -489,3 +516,8 @@ class Wizard(CharClass):
19: (5, 4, 3, 3, 3, 3, 2, 1, 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)
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):
tex = template.render(character=char)
# Create tex document
@@ -95,7 +100,7 @@ def create_latex_pdf(char, basename, template):
try:
result = subprocess.run(['pdflatex', '--output-directory',
output_dir, tex_file, '-halt-on-error'],
stdout=subprocess.DEVNULL, timeout=10)
stdout=subprocess.DEVNULL, timeout=30)
except FileNotFoundError:
# Remove temporary files
remove_temp_files(basename)
@@ -244,7 +249,7 @@ def create_character_pdf(char, basename, flatten=False):
'Ideals': text_box(char.ideals),
'Bonds': text_box(char.bonds),
'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
'CP': char.cp,
'SP': char.sp,
@@ -441,6 +446,17 @@ def make_sheet(character_file, char=None, flatten=False):
os.path.splitext(character_file)[0])
create_spells_pdf(char=char, basename=spell_base, flatten=flatten)
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
spellbook_base = os.path.splitext(character_file)[0] + '_spellbook'
try:
+15 -1
View File
@@ -1,11 +1,12 @@
from . import weapons
from . import features as feats
__all__ = ('Dwarf', 'HillDwarf', 'MountainDwarf', 'Elf', 'HighElf',
'WoodElf', 'DarkElf', 'Halfling', 'LightfootHalfling',
'StoutHalfling', 'Human', 'Dragonborn', 'Gnome', 'ForestGnome',
'RockGnome', 'HalfElf', 'HalfOrc', 'Tiefling', 'Aasimar',
'FallenAasimar', 'Lizardfolk', 'Kenku')
'FallenAasimar', 'Lizardfolk', 'Kenku', 'Aarakocra')
class Race():
@@ -16,6 +17,7 @@ class Race():
proficiencies_text = tuple()
weapon_proficiences = tuple()
skill_proficiencies = ()
features = tuple()
strength_bonus = 0
dexterity_bonus = 0
constitution_bonus = 0
@@ -214,3 +216,15 @@ class Kenku(Race):
dexterity_bonus = 2
wisdom_bonus = 1
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
if is_proficient:
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
+50 -3
View File
@@ -1,5 +1,6 @@
from .stats import mod_str
class Weapon():
name = ""
cost = "0 gp"
@@ -19,6 +20,10 @@ class Weapon():
dam_str += '' + mod_str(self.bonus_damage)
return dam_str
@property
def is_ranged(self):
return ('range' in self.properties.lower()) and ('thrown' not in self.properties.lower())
class Club(Weapon):
name = "Club"
@@ -127,7 +132,7 @@ class LightCrossbow(Weapon):
base_damage = "1d8"
damage_type = "p"
weight = 5
properties = "Ammunition (range 80/320, loading, two-handed"
properties = "Ammunition (range 80/320), loading, two-handed"
ability = 'dexterity'
@@ -383,7 +388,7 @@ class HeavyCrossbow(Weapon):
damage_type = "p"
weight = 18
properties = "Ammunition (range 100/400), heaving, loading, two-handed"
ability = 'strength'
ability = 'dexterity'
class Longbow(Weapon):
@@ -393,7 +398,7 @@ class Longbow(Weapon):
damage_type = "p"
weight = 2
properties = "Ammunition (range 150/600), heavy, two-handed"
ability = 'strength'
ability = 'dexterity'
class Net(Weapon):
@@ -445,6 +450,46 @@ class Bite(Weapon):
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
simple_melee_weapons = (Club, Dagger, Greatclub, Handaxe, Javelin,
LightHammer, Mace, Quarterstaff, Sickle, Spear)
@@ -459,3 +504,5 @@ martial_melee_weapons = (Battleaxe, Flail, Glaive, Greataxe,
martial_ranged_weapons = (Blowgun, HandCrossbow, HeavyCrossbow,
Longbow, Net)
martial_weapons = martial_melee_weapons + martial_ranged_weapons
firearms = (Firearm)