Added armor and shields to calculate armor class.

This commit is contained in:
Mark Wolfman
2018-04-14 01:25:56 -05:00
parent 9ff160eb59
commit c9bcaac18d
11 changed files with 290 additions and 35 deletions
+159
View File
@@ -0,0 +1,159 @@
class Shield():
"""A shield that can be worn on one hand."""
name = "Shield"
cost = "10 gp"
base_armor_class = 2
def __str__(self):
return self.name
class NoShield(Shield):
"""If a character is carrying no shield."""
name = "No shield"
cost = "0"
base_armor_class = 0
def __str__(self):
return self.name
class Armor():
"""A piece of armor that can be worn.
Attributes
----------
name : str
Human-readable name for this armor.
cost : str
Cost and currency for this armor.
base_armor_class : int
Armor class granted before modifiers.
dexterity_mod_max : int
How much dexterity can the user contribute. ``0`` for no
dexterity modifier, ``None`` for unlimited dexterity modifier.
strength_required : int
Minimum strength needed to use this armor properly.
stealth_disadvantage : bool
If true, the armor causes disadvantage on stealth rolls.
weight : int
In lbs.
"""
name = "Unknown Armor"
cost = "0 gp"
base_armor_class = 10
dexterity_mod_max = None
strength_required = None
stealth_disadvantage = False
weight = 0 # In lbs
def __str__(self):
return self.name
class NoArmor(Armor):
name = "No armor"
class LightPaddedArmor(Armor):
name = "Light padded armor"
cost = "5 gp"
base_armor_class = 11
weight = 8
stealth_disadvantage = True
class LightLeatherArmor(Armor):
name = "Light leather armor"
cost = "10 gp"
base_armor_class = 11
weight = 10
class LightStuddedArmor(Armor):
name = "Light studded armor"
cost = "45 gp"
base_armor_class = 12
weight = 13
class MediumHideArmor(Armor):
name = "Medium hide armor"
cost = "10 gp"
base_armor_class = 12
dexterity_mod_max = 2
weight = 12
class MediumChainShirtArmor(Armor):
name = "Medium chain shirt armor"
cost = "50 gp"
base_armor_class = 13
dexterity_mod_max = 2
weight = 20
class MediumScaleMailArmor(Armor):
name = "Medium scale mail armor"
cost = "50 gp"
base_armor_class = 14
dexterity_mod_max = 2
stealth_disadvantage = True
weight = 45
class MediumBrassplateArmor(Armor):
name = "Medium brassplate armor"
cost = "400 gp"
base_armor_class = 14
dexterity_mod_max = 2
weight = 20
class MediumHalfPlateArmor(Armor):
name = "Medium half plate armor"
cost = "750 gp"
base_armor_class = 15
dexterity_mod_max = 2
stealth_disadvantage = True
weight = 40
class HeavyRingMailArmor(Armor):
name = "Heavy ring mail armor"
cost = "30 gp"
base_armor_class = 14
dexterity_mod_max = 0
stealth_disadvantage = True
weight = 40
class HeavyChainMailArmor(Armor):
name = "Heavy chain mail armor"
cost = "75 gp"
base_armor_class = 16
dexterity_mod_max = 0
strength_required = 13
stealth_disadvantage = True
weight = 55
class HeavySplintArmor(Armor):
name = "Heavy splint armor"
cost = "200 gp"
base_armor_class = 17
dexterity_mod_max = 0
strength_required = 15
stealth_disadvantage = True
weight = 60
class HeavyPlateArmor(Armor):
name = "Heavy splint armor"
cost = "1,500 gp"
base_armor_class = 18
dexterity_mod_max = 0
strength_required = 15
stealth_disadvantage = True
weight = 65
+54 -3
View File
@@ -5,8 +5,9 @@ import warnings
from .stats import Ability, Skill, findattr
from .dice import read_dice_str
from . import weapons, race, spells
from . import weapons, race, spells, armor
from .weapons import Weapon
from .armor import Armor, NoArmor, Shield, NoShield
dice_re = re.compile('(\d+)d(\d+)')
@@ -73,6 +74,8 @@ class Character():
pp = 0
equipment = ""
weapons = [] # Replaced in __init__ constructor
armor = None
shield = None
_proficiencies_text = tuple()
# Magic
spellcasting_ability = None
@@ -105,6 +108,10 @@ class Character():
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 = []
@@ -185,6 +192,40 @@ class Character():
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.
@@ -236,8 +277,18 @@ class Character():
@property
def armor_class(self):
"""Armor class, without items."""
return 10 + self.dexterity.modifier
"""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):
+5 -1
View File
@@ -181,7 +181,6 @@ def create_character_pdf(character, basename, flatten=False):
('Ideals', text_box(character.ideals)),
('Bonds', text_box(character.bonds)),
('Flaws', text_box(character.flaws)),
('AttacksSpellcasting', text_box(character.attacks_and_spellcasting)),
('Features and Traits', text_box(character.features_and_traits)),
# Inventory
('CP', character.cp),
@@ -234,6 +233,11 @@ def create_character_pdf(character, basename, flatten=False):
fields.append((name_field, weapon.name))
fields.append((atk_field, mod_str(weapon.attack_bonus)))
fields.append((dmg_field, f'{weapon.damage} {weapon.damage_type}'))
# Other attack information
attack_str = f'Armor: {character.armor}'
attack_str += f'Shield: {character.shield}\n\n'
attack_str += character.attacks_and_spellcasting
fields.append(('AttacksSpellcasting', text_box(attack_str)))
# Other proficiencies and languages
prof_text = "Proficiencies:\n" + text_box(character.proficiencies_text)
prof_text += "\n\nLanguages:\n" + text_box(character.languages)
+29 -21
View File
@@ -1,4 +1,5 @@
import math
from collections import namedtuple
def findattr(obj, name):
@@ -28,39 +29,46 @@ def mod_str(modifier):
return mod_str
AbilityScore = namedtuple('AbilityScore', ('value', 'modifier', 'saving_throw'))
class Ability():
value = 10
ability_name = None
character = None
def __init__(self, value=10):
self.value = value
def __init__(self, default_value=10):
self.default_value = default_value
def __set_name__(self, character, name):
self.ability_name = name
def _check_dict(self, obj):
if not hasattr(obj, '_ability_scores'):
# No ability score dictionary exists
obj._ability_scores = {
self.ability_name: self.default_value
}
elif self.ability_name not in obj._ability_scores.keys():
# ability score dictionary exists but doesn't have this ability
obj._ability_scores[self.ability_name] = self.default_value
def __get__(self, character, Character):
self.character = character
return self
def __set__(self, obj, val):
self.value = val
@property
def modifier(self):
return math.floor((self.value - 10) / 2)
@property
def saving_throw(self):
modifier = self.modifier
self._check_dict(character)
score = character._ability_scores[self.ability_name]
modifier = math.floor((score - 10) / 2)
# Check for proficiency
saving_throw = modifier
if self.ability_name is not None:
is_proficient = (self.ability_name in self.character.saving_throw_proficiencies)
is_proficient = (self.ability_name in character.saving_throw_proficiencies)
if is_proficient:
modifier += self.character.proficiency_bonus
# Return the value
return modifier
saving_throw += character.proficiency_bonus
# Create the named tuple
value = AbilityScore(modifier=modifier, value=score, saving_throw=saving_throw)
return value
def __set__(self, character, val):
self._check_dict(character)
character._ability_scores[self.ability_name] = val
self.value = val
class Skill():