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 .stats import Ability, Skill, findattr
from .dice import read_dice_str from .dice import read_dice_str
from . import weapons, race, spells from . import weapons, race, spells, armor
from .weapons import Weapon from .weapons import Weapon
from .armor import Armor, NoArmor, Shield, NoShield
dice_re = re.compile('(\d+)d(\d+)') dice_re = re.compile('(\d+)d(\d+)')
@@ -73,6 +74,8 @@ class Character():
pp = 0 pp = 0
equipment = "" equipment = ""
weapons = [] # Replaced in __init__ constructor weapons = [] # Replaced in __init__ constructor
armor = None
shield = None
_proficiencies_text = tuple() _proficiencies_text = tuple()
# Magic # Magic
spellcasting_ability = None spellcasting_ability = None
@@ -105,6 +108,10 @@ class Character():
elif attr == 'race': elif attr == 'race':
MyRace = findattr(race, val) MyRace = findattr(race, val)
self.race = MyRace() self.race = MyRace()
elif attr == 'armor':
self.wear_armor(val)
elif attr == 'shield':
self.wield_shield(val)
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 = []
@@ -185,6 +192,40 @@ class Character():
final_text += '.' final_text += '.'
return 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): def wield_weapon(self, weapon):
"""Accepts a string and adds it to the list of wielded weapons. """Accepts a string and adds it to the list of wielded weapons.
@@ -236,8 +277,18 @@ class Character():
@property @property
def armor_class(self): def armor_class(self):
"""Armor class, without items.""" """Armor class, including contributions from worn armor and shield."""
return 10 + self.dexterity.modifier # 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 Barbarian(Character):
+5 -1
View File
@@ -181,7 +181,6 @@ def create_character_pdf(character, basename, flatten=False):
('Ideals', text_box(character.ideals)), ('Ideals', text_box(character.ideals)),
('Bonds', text_box(character.bonds)), ('Bonds', text_box(character.bonds)),
('Flaws', text_box(character.flaws)), ('Flaws', text_box(character.flaws)),
('AttacksSpellcasting', text_box(character.attacks_and_spellcasting)),
('Features and Traits', text_box(character.features_and_traits)), ('Features and Traits', text_box(character.features_and_traits)),
# Inventory # Inventory
('CP', character.cp), ('CP', character.cp),
@@ -234,6 +233,11 @@ def create_character_pdf(character, basename, flatten=False):
fields.append((name_field, weapon.name)) fields.append((name_field, weapon.name))
fields.append((atk_field, mod_str(weapon.attack_bonus))) fields.append((atk_field, mod_str(weapon.attack_bonus)))
fields.append((dmg_field, f'{weapon.damage} {weapon.damage_type}')) 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 # Other proficiencies and languages
prof_text = "Proficiencies:\n" + text_box(character.proficiencies_text) prof_text = "Proficiencies:\n" + text_box(character.proficiencies_text)
prof_text += "\n\nLanguages:\n" + text_box(character.languages) prof_text += "\n\nLanguages:\n" + text_box(character.languages)
+29 -21
View File
@@ -1,4 +1,5 @@
import math import math
from collections import namedtuple
def findattr(obj, name): def findattr(obj, name):
@@ -28,39 +29,46 @@ def mod_str(modifier):
return mod_str return mod_str
AbilityScore = namedtuple('AbilityScore', ('value', 'modifier', 'saving_throw'))
class Ability(): class Ability():
value = 10
ability_name = None ability_name = None
character = None
def __init__(self, value=10): def __init__(self, default_value=10):
self.value = value self.default_value = default_value
def __set_name__(self, character, name): def __set_name__(self, character, name):
self.ability_name = 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): def __get__(self, character, Character):
self.character = character self._check_dict(character)
return self score = character._ability_scores[self.ability_name]
modifier = math.floor((score - 10) / 2)
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
# Check for proficiency # Check for proficiency
saving_throw = modifier
if self.ability_name is not None: 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: if is_proficient:
modifier += self.character.proficiency_bonus saving_throw += character.proficiency_bonus
# Return the value # Create the named tuple
return modifier 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(): class Skill():
Binary file not shown.
+2
View File
@@ -33,6 +33,8 @@ ep = 50
gp = 120 gp = 120
pp = 0 pp = 0
weapons = ('shortsword', 'shortbow') weapons = ('shortsword', 'shortbow')
armor = 'light leather armor'
shield = 'shield'
equipment = ( equipment = (
"""Shortsword, shortbow, 20 arrows, leather armor, thieves tools, """Shortsword, shortbow, 20 arrows, leather armor, thieves tools,
backpack, bell, 5 candles, crowbar, hammer, 10 pitons, 50 feet of backpack, bell, 5 candles, crowbar, hammer, 10 pitons, 50 feet of
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -1,14 +1,14 @@
#!/usr/bin/env python #!/usr/bin/env python
import os import os
# from distutils.core import setup
from setuptools import setup from setuptools import setup
def read(fname): def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read() return open(os.path.join(os.path.dirname(__file__), fname)).read()
setup(name='dungeonsheets', setup(name='dungeonsheets',
version='0.1.0', version='0.2.1',
description='Dungeons and Dragons 5e Character Tools', description='Dungeons and Dragons 5e Character Tools',
long_description=read('README.rst'), long_description=read('README.rst'),
long_description_content_type='text/x-rst', long_description_content_type='text/x-rst',
+28
View File
@@ -5,6 +5,7 @@ from unittest import TestCase
from dungeonsheets import race from dungeonsheets import race
from dungeonsheets.character import Character, Wizard from dungeonsheets.character import Character, Wizard
from dungeonsheets.weapons import Weapon, Shortsword from dungeonsheets.weapons import Weapon, Shortsword
from dungeonsheets.armor import Armor, LightLeatherArmor, Shield
class TestCharacter(TestCase): class TestCharacter(TestCase):
@@ -28,6 +29,10 @@ class TestCharacter(TestCase):
char.set_attrs(weapons=['shortsword']) char.set_attrs(weapons=['shortsword'])
self.assertEqual(len(char.weapons), 1) self.assertEqual(len(char.weapons), 1)
self.assertTrue(isinstance(char.weapons[0], Shortsword)) self.assertTrue(isinstance(char.weapons[0], Shortsword))
# Check that armor and shield gets set_attrs
char.set_attrs(armor='light leather armor', shield='shield')
self.assertFalse(isinstance(char.armor, str))
self.assertFalse(isinstance(char.shield, str))
# Check that race gets set to an object # Check that race gets set to an object
char.set_attrs(race='high elf') char.set_attrs(race='high elf')
self.assertIsInstance(char.race, race.HighElf) self.assertIsInstance(char.race, race.HighElf)
@@ -125,3 +130,26 @@ class TestCharacter(TestCase):
self.assertEqual(char.spell_slots(spell_level=0), 3) self.assertEqual(char.spell_slots(spell_level=0), 3)
self.assertEqual(char.spell_slots(spell_level=1), 3) self.assertEqual(char.spell_slots(spell_level=1), 3)
self.assertEqual(char.spell_slots(spell_level=2), 0) self.assertEqual(char.spell_slots(spell_level=2), 0)
def test_equip_armor(self):
char = Character(dexterity=16)
char.wear_armor('light leather armor')
self.assertTrue(isinstance(char.armor, Armor))
# Now make sure the armor class is correct
self.assertEqual(char.armor_class, 14)
# Try passing an Armor object directly
char.wear_armor(LightLeatherArmor)
self.assertEqual(char.armor_class, 14)
# Test equipped armor with max dexterity mod_str
char.armor.dexterity_mod_max = 1
self.assertEqual(char.armor_class, 12)
def test_wield_shield(self):
char = Character(dexterity=16)
char.wield_shield('shield')
self.assertTrue(isinstance(char.shield, Shield), msg=char.shield)
# Now make sure the armor class is correct
self.assertEqual(char.armor_class, 15)
# Try passing an Armor object directly
char.wield_shield(Shield)
self.assertEqual(char.armor_class, 15)
+11 -8
View File
@@ -10,10 +10,8 @@ class TestStats(TestCase):
self.assertEqual(stats.mod_str(2), '+2') self.assertEqual(stats.mod_str(2), '+2')
def test_saving_throw(self): def test_saving_throw(self):
stat = stats.Ability(14) # Try it with an ST proficiency
self.assertEqual(stat.saving_throw, 2) class MyClass(character.Character):
# Now try it with an ST proficiency
class MyClass():
saving_throw_proficiencies = ['strength'] saving_throw_proficiencies = ['strength']
proficiency_bonus = 2 proficiency_bonus = 2
strength = stats.Ability(14) strength = stats.Ability(14)
@@ -21,6 +19,11 @@ class TestStats(TestCase):
self.assertEqual(my_class.strength.saving_throw, 4) self.assertEqual(my_class.strength.saving_throw, 4)
def test_modifier(self): def test_modifier(self):
class MyCharacter(character.Character):
saving_throw_proficiencies = ['strength']
proficiency_bonus = 2
strength = stats.Ability(14)
my_char = MyCharacter()
ranges = [ ranges = [
((1,), -5), ((1,), -5),
((2, 3), -4), ((2, 3), -4),
@@ -40,17 +43,17 @@ class TestStats(TestCase):
((30,), 10), ((30,), 10),
] ]
# Test the values for each modifier range # Test the values for each modifier range
stat = stats.Ability()
for range_, target in ranges: for range_, target in ranges:
for value in range_: for value in range_:
stat.value = value my_char.strength = value
stat = my_char.strength
msg = f"Stat {value} doesn't produce modifier {target} ({stat.modifier})" msg = f"Stat {value} doesn't produce modifier {target} ({stat.modifier})"
self.assertEqual(stat.modifier, target, msg) self.assertEqual(stat.modifier, target, msg)
def test_setter(self): def test_setter(self):
"""Verify that this class works as a data descriptor.""" """Verify that this class works as a data descriptor."""
# Set up a dummy class # Set up a dummy class
class MyCharacter(): class MyCharacter(character.Character):
stat = stats.Ability() stat = stats.Ability()
char = MyCharacter() char = MyCharacter()
# Check that the stat works as expected once set # Check that the stat works as expected once set
@@ -60,7 +63,7 @@ class TestStats(TestCase):
def test_skill(self): def test_skill(self):
"""Test for a skill, that depends on another ability.""" """Test for a skill, that depends on another ability."""
class MyClass(): class MyClass(character.Character):
dexterity = stats.Ability(14) dexterity = stats.Ability(14)
acrobatics = stats.Skill(ability='dexterity') acrobatics = stats.Skill(ability='dexterity')
skill_proficiencies = [] skill_proficiencies = []