This commit is contained in:
tim_jackins
2021-06-09 01:10:02 -04:00
8 changed files with 215 additions and 140 deletions
+3
View File
@@ -105,6 +105,9 @@ ENV/
# Rope project settings
.ropeproject
# PyCharm project settings
.idea
# mkdocs documentation
/site
+13 -54
View File
@@ -20,16 +20,10 @@ from dungeonsheets import (
spells,
weapons,
)
from dungeonsheets.stats import Ability, ArmorClass, Initiative, Skill, Speed, findattr
from dungeonsheets.stats import findattr
from dungeonsheets.weapons import Weapon
from dungeonsheets.readers import read_character_file
def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read()
__version__ = read("../VERSION").strip()
from dungeonsheets.entity import Entity
dice_re = re.compile(r"(\d+)d(\d+)")
@@ -145,17 +139,11 @@ def _resolve_mechanic(mechanic, module, SuperClass, warning_message=None):
return Mechanic
class Character:
class Character(Entity):
"""A generic player character."""
# General attirubtes
name = ""
# Character-specific
player_name = ""
alignment = "Neutral"
dungeonsheets_version = __version__
class_list = list()
_race = None
_background = None
xp = 0
# Hit points
hp_max = None
@@ -172,32 +160,11 @@ class Character:
initiative = Initiative()
speed = Speed()
inspiration = False
_saving_throw_proficiencies = tuple() # use to overwrite class proficiencies
other_weapon_proficiencies = tuple() # add to class/race proficiencies
skill_proficiencies = list()
skill_expertise = list()
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")
persuasion = 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 = ""
class_list = list()
_background = None
# Characteristics
personality_traits = (
"TODO: Describe how your character behaves, interacts with others"
)
@@ -205,17 +172,7 @@ class Character:
bonds = "TODO: Describe your character's commitments or ongoing quests."
flaws = "TODO: Describe your character's interesting flaws."
features_and_traits = "Describe any other features and abilities."
# Inventory
cp = 0
sp = 0
ep = 0
gp = 0
pp = 0
equipment = ""
weapons = list()
magic_items = list()
armor = None
shield = None
_proficiencies_text = list()
# Magic
spellcasting_ability = None
@@ -225,6 +182,7 @@ class Character:
# Features IN MAJOR DEVELOPMENT
custom_features = list()
feature_choices = list()
# Appearance
# portrait = placeholder not sure how to implement
age = 0
@@ -268,6 +226,7 @@ class Character:
character.
"""
super(Character, self).__init__()
self.clear()
# make sure class, race, background are set first
my_classes = classes
@@ -298,7 +257,7 @@ class Character:
self.__set_max_hp(attrs.get("hp_max", None))
def clear(self):
# reset class-definied items
# reset class-defined items
self.class_list = list()
self.weapons = list()
self.magic_items = list()
@@ -1001,7 +960,7 @@ class Character:
make_sheet(filename, character=self, flatten=kwargs.get("flatten", True))
# Add backwards compatability for tests
# Add backwards compatibility for tests
class Artificer(Character):
def __init__(self, level=1, **attrs):
attrs["classes"] = ["Artificer"]
+9
View File
@@ -1,3 +1,4 @@
import random
import re
from collections import namedtuple
@@ -22,3 +23,11 @@ def read_dice_str(dice_str):
raise DiceError(f"Cannot interpret dice string {dice_str}")
dice = Dice(num=int(match.group(1)), faces=int(match.group(2)))
return dice
def roll(a, b=None):
"""roll(20) means roll 1d20, roll(2, 6) means roll 2d6"""
if b is None:
return random.randint(1, a)
else:
return sum([random.randint(1, b) for _ in range(a)])
+89
View File
@@ -0,0 +1,89 @@
from dungeonsheets.stats import Ability, ArmorClass, Initiative, Speed, Skill
from abc import ABC
import os
def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read()
__version__ = read("../VERSION").strip()
class Entity(ABC):
"""A thing with stats. Use Monster or Character, not this class directly!"""
# General attributes
dungeonsheets_version = __version__
name = ""
alignment = "Neutral"
_race = None
# Hit points
hp_max = None
# Base stats (ability scores)
strength = Ability()
dexterity = Ability()
constitution = Ability()
intelligence = Ability()
wisdom = Ability()
charisma = Ability()
# Numerical things
armor_class = ArmorClass()
initiative = Initiative()
speed = Speed()
# Proficiencies and Languages
_saving_throw_proficiencies = tuple() # use to overwrite class proficiencies
other_weapon_proficiencies = tuple() # add to class/race proficiencies
skill_proficiencies = list()
skill_expertise = list()
languages = ""
senses = ""
# 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")
persuasion = Skill(ability="charisma")
religion = Skill(ability="intelligence")
sleight_of_hand = Skill(ability="dexterity")
stealth = Skill(ability="dexterity")
survival = Skill(ability="wisdom")
# Inventory
cp = 0
sp = 0
ep = 0
gp = 0
pp = 0
equipment = ""
weapons = list()
magic_items = list()
armor = None
shield = None
# Magic
spellcasting_ability = None
_spells = list()
_spells_prepared = list()
infusions = list()
# Features IN MAJOR DEVELOPMENT
custom_features = list()
feature_choices = list()
def __init__(self):
pass
+15 -21
View File
@@ -3,30 +3,24 @@ shape forms."""
from dungeonsheets.stats import Ability
from dungeonsheets.entity import Entity
class Monster:
class Monster(Entity):
"""A monster that may be encountered when adventuring."""
name = "Generic Monster"
description = ""
challenge_rating = 0
armor_class = 0
skills = "Perception +3, Stealth +4"
senses = ""
languages = ""
strength = Ability()
dexterity = Ability()
constitution = Ability()
intelligence = Ability()
wisdom = Ability()
charisma = Ability()
speed = 30
swim_speed = 0
swim_speed = 0 # TODO: Consider refactoring stats.Speed to consider all of these just like we do stats.Ability
fly_speed = 0
hp_max = 10
hit_dice = "1d6"
def __init__(self):
super(Monster, self).__init__()
@property
def is_beast(self):
is_beast = "beast" in self.description.lower()
@@ -50,7 +44,7 @@ class Ankylosaurus(Monster):
challenge_rating = 3
armor_class = 15
skills = ""
senses = "Passive perception 11"
senses = "passive Perception 11"
strength = Ability(19)
dexterity = Ability(11)
constitution = Ability(15)
@@ -80,7 +74,7 @@ class Ape(Monster):
challenge_rating = 1 / 2
armor_class = 12
skills = "Athletics +5, Perception +3"
senses = "Passive perception 13"
senses = "passive Perception 13"
strength = Ability(16)
dexterity = Ability(14)
constitution = Ability(14)
@@ -115,7 +109,7 @@ class BlackBear(Monster):
challenge_rating = 1 / 2
armor_class = 11
skills = "Perception +3"
senses = "Passive perception 13"
senses = "passive Perception 13"
strength = Ability(15)
dexterity = Ability(10)
constitution = Ability(14)
@@ -183,7 +177,7 @@ class GiantEagle(Monster):
challenge_rating = 1
armor_class = 13
skills = "Perception +4"
senses = "Passive perception 14"
senses = "passive Perception 14"
languages = "understands common and Auran but can't speak."
strength = Ability(16)
dexterity = Ability(17)
@@ -209,8 +203,8 @@ class GiantFrog(Monster):
description = "Medium beast, unaligned"
challenge_rating = 1 / 4
armor_class = 11
skills = "Pe rce ption +2, Stealth +3"
senses = "darkvi sion 30ft., passive Perception 12"
skills = "Perception +2, Stealth +3"
senses = "darkvision 30ft., passive Perception 12"
languages = ""
strength = Ability(12)
dexterity = Ability(13)
@@ -242,7 +236,7 @@ class GiantRat(Monster):
challenge_rating = 1 / 8
armor_class = 12
skills = ""
senses = "Darkvision 60 ft., Passive perception 10"
senses = "Darkvision 60 ft., passive Perception 10"
languages = ""
strength = Ability(7)
dexterity = Ability(15)
@@ -287,9 +281,9 @@ class GiantPoisonousSnake(Monster):
class PoisonousSnake(Monster):
"""**Bite:** Melee Weapon Attack: +5 to hit, reach 5 ft., one target.
Hit: 1 piercing damage, and the target must ma ke a DC 10
Hit: 1 piercing damage, and the target must make a DC 10
Constitution saving throw, taking 5 (2d4) poison dam age on a
failed save, or ha lf as much damage on a successful one.
failed save, or half as much damage on a successful one.
"""
name = "Poisonous snake"
+2 -2
View File
@@ -256,7 +256,7 @@ class HalfOrc(Race):
features = (feats.Darkvision, feats.RelentlessEndurance, feats.SavageAttacks)
# Tielflings
# Tieflings
class Tiefling(Race):
name = "Tiefling"
size = "medium"
@@ -267,7 +267,7 @@ class Tiefling(Race):
features = (feats.Darkvision, feats.HellishResistance, feats.InfernalLegacy)
# Aassimar
# Aasimar
class _Aasimar(Race):
name = "Aasimar"
size = "medium"
+68 -63
View File
@@ -64,10 +64,6 @@ def findattr(obj, name):
def mod_str(modifier):
"""Converts a modifier to a string, eg 2 -> '+2'."""
return "{:+d}".format(modifier)
if modifier == 0:
return str(modifier)
else:
return "{:+}".format(modifier)
AbilityScore = namedtuple("AbilityScore", ("value", "modifier", "saving_throw"))
@@ -90,25 +86,25 @@ class Ability:
# 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._check_dict(character)
score = character._ability_scores[self.ability_name]
def __get__(self, entity, Entity):
self._check_dict(entity)
score = entity._ability_scores[self.ability_name]
modifier = math.floor((score - 10) / 2)
# Check for proficiency
saving_throw = modifier
if self.ability_name is not None and hasattr(
character, "saving_throw_proficiencies"
entity, "saving_throw_proficiencies"
):
is_proficient = self.ability_name in character.saving_throw_proficiencies
is_proficient = self.ability_name in entity.saving_throw_proficiencies
if is_proficient:
saving_throw += character.proficiency_bonus
saving_throw += entity.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
def __set__(self, entity, val):
self._check_dict(entity)
entity._ability_scores[self.ability_name] = val
self.value = val
@@ -118,27 +114,27 @@ class Skill:
def __init__(self, ability):
self.ability_name = ability
def __set_name__(self, character, name):
def __set_name__(self, entity, name):
self.skill_name = name.lower().replace("_", " ")
self.character = character
self.character = entity
def __get__(self, character, owner):
ability = getattr(character, self.ability_name)
def __get__(self, entity, owner):
ability = getattr(entity, self.ability_name)
modifier = ability.modifier
# Check for proficiency
is_proficient = self.skill_name in character.skill_proficiencies
is_proficient = self.skill_name in entity.skill_proficiencies
if is_proficient:
modifier += character.proficiency_bonus
elif character.has_feature(JackOfAllTrades):
modifier += character.proficiency_bonus // 2
elif character.has_feature(RemarkableAthelete):
modifier += entity.proficiency_bonus
elif entity.has_feature(JackOfAllTrades):
modifier += entity.proficiency_bonus // 2
elif entity.has_feature(RemarkableAthelete):
if self.ability_name.lower() in ("strength", "dexterity", "constitution"):
modifier += ceil(character.proficienc_bonus / 2.0)
modifier += ceil(entity.proficiency_bonus / 2.0)
# Check for expertise
is_expert = self.skill_name in character.skill_expertise
is_expert = self.skill_name in entity.skill_expertise
if is_expert:
modifier += character.proficiency_bonus
modifier += entity.proficiency_bonus
return modifier
@@ -147,36 +143,36 @@ class ArmorClass:
The Armor Class of a character
"""
def __get__(self, char, Character):
armor = char.armor or NoArmor()
def __get__(self, entity, Entity):
armor = entity.armor or NoArmor()
ac = armor.base_armor_class
# calculate and apply modifiers
if armor.dexterity_mod_max is None:
ac += char.dexterity.modifier
ac += entity.dexterity.modifier
else:
ac += min(char.dexterity.modifier, armor.dexterity_mod_max)
if char.has_feature(NaturalArmor):
ac = max(ac, 13 + char.dexterity.modifier)
shield = char.shield or NoShield()
ac += min(entity.dexterity.modifier, armor.dexterity_mod_max)
if entity.has_feature(NaturalArmor):
ac = max(ac, 13 + entity.dexterity.modifier)
shield = entity.shield or NoShield()
ac += shield.base_armor_class
# Compute feature-specific additions
if char.has_feature(UnarmoredDefenseMonk):
if entity.has_feature(UnarmoredDefenseMonk):
if isinstance(armor, NoArmor) and isinstance(shield, NoShield):
ac += char.wisdom.modifier
if char.has_feature(UnarmoredDefenseBarbarian):
ac += entity.wisdom.modifier
if entity.has_feature(UnarmoredDefenseBarbarian):
if isinstance(armor, NoArmor):
ac += char.constitution.modifier
if char.has_feature(DraconicResilience):
ac += entity.constitution.modifier
if entity.has_feature(DraconicResilience):
if isinstance(armor, NoArmor):
ac += 3
if char.has_feature(Defense):
if entity.has_feature(Defense):
if not isinstance(armor, NoArmor):
ac += 1
if char.has_feature(SoulOfTheForge):
if entity.has_feature(SoulOfTheForge):
if isinstance(armor, HeavyArmor):
ac += 1
# Check if any magic items add to AC
for mitem in char.magic_items:
for mitem in entity.magic_items:
if hasattr(mitem, "ac_bonus"):
ac += mitem.ac_bonus
return ac
@@ -187,47 +183,56 @@ class Speed:
The speed of a character
"""
def __get__(self, char, Character):
speed = char.race.speed
def __get__(self, entity, Entity):
speed = entity.race.speed
other_speed = ""
if isinstance(speed, str):
other_speed = speed[2:]
speed = int(speed[:2]) # ignore other speeds, like fly
if char.has_feature(FastMovement):
if not isinstance(char.armor, HeavyArmor):
if entity.has_feature(FastMovement):
if not isinstance(entity.armor, HeavyArmor):
speed += 10
if char.has_feature(SuperiorMobility):
if entity.has_feature(SuperiorMobility):
speed += 10
if isinstance(char.armor, NoArmor) or (char.armor is None):
for f in char.features:
if isinstance(entity.armor, NoArmor) or (entity.armor is None):
for f in entity.features:
if isinstance(f, UnarmoredMovement):
speed += f.speed_bonus
if char.has_feature(GiftOfTheDepths):
if entity.has_feature(GiftOfTheDepths):
if "swim" not in other_speed:
other_speed += " ({:d} swim)".format(speed)
if char.has_feature(SeaSoul):
if entity.has_feature(SeaSoul):
if "swim" not in other_speed:
other_speed += " (30 swim)"
return "{:d}{:s}".format(speed, other_speed)
class Initiative:
class NumericalInitiative:
"""A numerical representation of initiative"""
def __get__(self, entity, Entity):
ini = entity.dexterity.modifier
if entity.has_feature(QuickDraw):
ini += entity.proficiency_bonus
if entity.has_feature(DreadAmbusher):
ini += entity.wisdom.modifier
if entity.has_feature(RakishAudacity):
ini += entity.charisma.modifier
has_advantage = (
entity.has_feature(NaturalExplorerRevised)
or entity.has_feature(FeralInstinct)
or entity.has_feature(AmbushMaster)
)
return ini, has_advantage
class Initiative(NumericalInitiative):
"""A character's initiative"""
def __get__(self, char, Character):
ini = char.dexterity.modifier
if char.has_feature(QuickDraw):
ini += char.proficiency_bonus
if char.has_feature(DreadAmbusher):
ini += char.wisdom.modifier
if char.has_feature(RakishAudacity):
ini += char.charisma.modifier
def __get__(self, entity, Entity):
ini, has_advantage = super(Initiative, self).__get__(entity, Entity)
ini = "{:+d}".format(ini)
has_advantage = (
char.has_feature(NaturalExplorerRevised)
or char.has_feature(FeralInstinct)
or char.has_feature(AmbushMaster)
)
if has_advantage:
ini += "(A)"
return ini
+16
View File
@@ -1,10 +1,12 @@
from unittest import TestCase
from dungeonsheets.dice import roll
from dungeonsheets.exceptions import DiceError
from dungeonsheets import dice
class TestDice(TestCase):
def test_read_dice_str(self):
out = dice.read_dice_str("1d6")
self.assertEqual(out.faces, 6)
@@ -16,3 +18,17 @@ class TestDice(TestCase):
# Check a bad value
with self.assertRaises(DiceError):
dice.read_dice_str("Ed15")
def test_simple_rolling(self):
num_tests = 100
for _ in range(num_tests):
result = roll(6)
self.assertGreaterEqual(result, 1)
self.assertLessEqual(result, 6)
def test_multi_rolling(self):
num_tests = 100
for _ in range(num_tests):
result = roll(2, 4) # Roll 2d4
self.assertGreaterEqual(result, 2)
self.assertLessEqual(result, 8)