mirror of
https://github.com/Threnklyn/dungeon-sheets.git
synced 2026-06-06 04:38:28 +02:00
Merge branch 'master' of https://github.com/Tim-Jackins/dungeon-sheets
This commit is contained in:
@@ -105,6 +105,9 @@ ENV/
|
|||||||
# Rope project settings
|
# Rope project settings
|
||||||
.ropeproject
|
.ropeproject
|
||||||
|
|
||||||
|
# PyCharm project settings
|
||||||
|
.idea
|
||||||
|
|
||||||
# mkdocs documentation
|
# mkdocs documentation
|
||||||
/site
|
/site
|
||||||
|
|
||||||
|
|||||||
+13
-54
@@ -20,16 +20,10 @@ from dungeonsheets import (
|
|||||||
spells,
|
spells,
|
||||||
weapons,
|
weapons,
|
||||||
)
|
)
|
||||||
from dungeonsheets.stats import Ability, ArmorClass, Initiative, Skill, Speed, findattr
|
from dungeonsheets.stats import findattr
|
||||||
from dungeonsheets.weapons import Weapon
|
from dungeonsheets.weapons import Weapon
|
||||||
from dungeonsheets.readers import read_character_file
|
from dungeonsheets.readers import read_character_file
|
||||||
|
from dungeonsheets.entity import Entity
|
||||||
|
|
||||||
def read(fname):
|
|
||||||
return open(os.path.join(os.path.dirname(__file__), fname)).read()
|
|
||||||
|
|
||||||
|
|
||||||
__version__ = read("../VERSION").strip()
|
|
||||||
|
|
||||||
|
|
||||||
dice_re = re.compile(r"(\d+)d(\d+)")
|
dice_re = re.compile(r"(\d+)d(\d+)")
|
||||||
@@ -145,17 +139,11 @@ def _resolve_mechanic(mechanic, module, SuperClass, warning_message=None):
|
|||||||
return Mechanic
|
return Mechanic
|
||||||
|
|
||||||
|
|
||||||
class Character:
|
class Character(Entity):
|
||||||
"""A generic player character."""
|
"""A generic player character."""
|
||||||
|
|
||||||
# General attirubtes
|
# Character-specific
|
||||||
name = ""
|
|
||||||
player_name = ""
|
player_name = ""
|
||||||
alignment = "Neutral"
|
|
||||||
dungeonsheets_version = __version__
|
|
||||||
class_list = list()
|
|
||||||
_race = None
|
|
||||||
_background = None
|
|
||||||
xp = 0
|
xp = 0
|
||||||
# Hit points
|
# Hit points
|
||||||
hp_max = None
|
hp_max = None
|
||||||
@@ -172,32 +160,11 @@ class Character:
|
|||||||
initiative = Initiative()
|
initiative = Initiative()
|
||||||
speed = Speed()
|
speed = Speed()
|
||||||
inspiration = False
|
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 = ""
|
attacks_and_spellcasting = ""
|
||||||
|
class_list = list()
|
||||||
|
_background = None
|
||||||
|
|
||||||
|
# Characteristics
|
||||||
personality_traits = (
|
personality_traits = (
|
||||||
"TODO: Describe how your character behaves, interacts with others"
|
"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."
|
bonds = "TODO: Describe your character's commitments or ongoing quests."
|
||||||
flaws = "TODO: Describe your character's interesting flaws."
|
flaws = "TODO: Describe your character's interesting flaws."
|
||||||
features_and_traits = "Describe any other features and abilities."
|
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()
|
_proficiencies_text = list()
|
||||||
# Magic
|
# Magic
|
||||||
spellcasting_ability = None
|
spellcasting_ability = None
|
||||||
@@ -225,6 +182,7 @@ class Character:
|
|||||||
# Features IN MAJOR DEVELOPMENT
|
# Features IN MAJOR DEVELOPMENT
|
||||||
custom_features = list()
|
custom_features = list()
|
||||||
feature_choices = list()
|
feature_choices = list()
|
||||||
|
|
||||||
# Appearance
|
# Appearance
|
||||||
# portrait = placeholder not sure how to implement
|
# portrait = placeholder not sure how to implement
|
||||||
age = 0
|
age = 0
|
||||||
@@ -268,6 +226,7 @@ class Character:
|
|||||||
character.
|
character.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
super(Character, self).__init__()
|
||||||
self.clear()
|
self.clear()
|
||||||
# make sure class, race, background are set first
|
# make sure class, race, background are set first
|
||||||
my_classes = classes
|
my_classes = classes
|
||||||
@@ -298,7 +257,7 @@ class Character:
|
|||||||
self.__set_max_hp(attrs.get("hp_max", None))
|
self.__set_max_hp(attrs.get("hp_max", None))
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
# reset class-definied items
|
# reset class-defined items
|
||||||
self.class_list = list()
|
self.class_list = list()
|
||||||
self.weapons = list()
|
self.weapons = list()
|
||||||
self.magic_items = list()
|
self.magic_items = list()
|
||||||
@@ -1001,7 +960,7 @@ class Character:
|
|||||||
make_sheet(filename, character=self, flatten=kwargs.get("flatten", True))
|
make_sheet(filename, character=self, flatten=kwargs.get("flatten", True))
|
||||||
|
|
||||||
|
|
||||||
# Add backwards compatability for tests
|
# Add backwards compatibility for tests
|
||||||
class Artificer(Character):
|
class Artificer(Character):
|
||||||
def __init__(self, level=1, **attrs):
|
def __init__(self, level=1, **attrs):
|
||||||
attrs["classes"] = ["Artificer"]
|
attrs["classes"] = ["Artificer"]
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import random
|
||||||
import re
|
import re
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
@@ -22,3 +23,11 @@ def read_dice_str(dice_str):
|
|||||||
raise DiceError(f"Cannot interpret dice string {dice_str}")
|
raise DiceError(f"Cannot interpret dice string {dice_str}")
|
||||||
dice = Dice(num=int(match.group(1)), faces=int(match.group(2)))
|
dice = Dice(num=int(match.group(1)), faces=int(match.group(2)))
|
||||||
return dice
|
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)])
|
||||||
@@ -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
@@ -3,30 +3,24 @@ shape forms."""
|
|||||||
|
|
||||||
|
|
||||||
from dungeonsheets.stats import Ability
|
from dungeonsheets.stats import Ability
|
||||||
|
from dungeonsheets.entity import Entity
|
||||||
|
|
||||||
|
|
||||||
class Monster:
|
class Monster(Entity):
|
||||||
"""A monster that may be encountered when adventuring."""
|
"""A monster that may be encountered when adventuring."""
|
||||||
|
|
||||||
name = "Generic Monster"
|
name = "Generic Monster"
|
||||||
description = ""
|
description = ""
|
||||||
challenge_rating = 0
|
challenge_rating = 0
|
||||||
armor_class = 0
|
|
||||||
skills = "Perception +3, Stealth +4"
|
skills = "Perception +3, Stealth +4"
|
||||||
senses = ""
|
swim_speed = 0 # TODO: Consider refactoring stats.Speed to consider all of these just like we do stats.Ability
|
||||||
languages = ""
|
|
||||||
strength = Ability()
|
|
||||||
dexterity = Ability()
|
|
||||||
constitution = Ability()
|
|
||||||
intelligence = Ability()
|
|
||||||
wisdom = Ability()
|
|
||||||
charisma = Ability()
|
|
||||||
speed = 30
|
|
||||||
swim_speed = 0
|
|
||||||
fly_speed = 0
|
fly_speed = 0
|
||||||
hp_max = 10
|
hp_max = 10
|
||||||
hit_dice = "1d6"
|
hit_dice = "1d6"
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(Monster, self).__init__()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_beast(self):
|
def is_beast(self):
|
||||||
is_beast = "beast" in self.description.lower()
|
is_beast = "beast" in self.description.lower()
|
||||||
@@ -50,7 +44,7 @@ class Ankylosaurus(Monster):
|
|||||||
challenge_rating = 3
|
challenge_rating = 3
|
||||||
armor_class = 15
|
armor_class = 15
|
||||||
skills = ""
|
skills = ""
|
||||||
senses = "Passive perception 11"
|
senses = "passive Perception 11"
|
||||||
strength = Ability(19)
|
strength = Ability(19)
|
||||||
dexterity = Ability(11)
|
dexterity = Ability(11)
|
||||||
constitution = Ability(15)
|
constitution = Ability(15)
|
||||||
@@ -80,7 +74,7 @@ class Ape(Monster):
|
|||||||
challenge_rating = 1 / 2
|
challenge_rating = 1 / 2
|
||||||
armor_class = 12
|
armor_class = 12
|
||||||
skills = "Athletics +5, Perception +3"
|
skills = "Athletics +5, Perception +3"
|
||||||
senses = "Passive perception 13"
|
senses = "passive Perception 13"
|
||||||
strength = Ability(16)
|
strength = Ability(16)
|
||||||
dexterity = Ability(14)
|
dexterity = Ability(14)
|
||||||
constitution = Ability(14)
|
constitution = Ability(14)
|
||||||
@@ -115,7 +109,7 @@ class BlackBear(Monster):
|
|||||||
challenge_rating = 1 / 2
|
challenge_rating = 1 / 2
|
||||||
armor_class = 11
|
armor_class = 11
|
||||||
skills = "Perception +3"
|
skills = "Perception +3"
|
||||||
senses = "Passive perception 13"
|
senses = "passive Perception 13"
|
||||||
strength = Ability(15)
|
strength = Ability(15)
|
||||||
dexterity = Ability(10)
|
dexterity = Ability(10)
|
||||||
constitution = Ability(14)
|
constitution = Ability(14)
|
||||||
@@ -183,7 +177,7 @@ class GiantEagle(Monster):
|
|||||||
challenge_rating = 1
|
challenge_rating = 1
|
||||||
armor_class = 13
|
armor_class = 13
|
||||||
skills = "Perception +4"
|
skills = "Perception +4"
|
||||||
senses = "Passive perception 14"
|
senses = "passive Perception 14"
|
||||||
languages = "understands common and Auran but can't speak."
|
languages = "understands common and Auran but can't speak."
|
||||||
strength = Ability(16)
|
strength = Ability(16)
|
||||||
dexterity = Ability(17)
|
dexterity = Ability(17)
|
||||||
@@ -209,8 +203,8 @@ class GiantFrog(Monster):
|
|||||||
description = "Medium beast, unaligned"
|
description = "Medium beast, unaligned"
|
||||||
challenge_rating = 1 / 4
|
challenge_rating = 1 / 4
|
||||||
armor_class = 11
|
armor_class = 11
|
||||||
skills = "Pe rce ption +2, Stealth +3"
|
skills = "Perception +2, Stealth +3"
|
||||||
senses = "darkvi sion 30ft., passive Perception 12"
|
senses = "darkvision 30ft., passive Perception 12"
|
||||||
languages = ""
|
languages = ""
|
||||||
strength = Ability(12)
|
strength = Ability(12)
|
||||||
dexterity = Ability(13)
|
dexterity = Ability(13)
|
||||||
@@ -242,7 +236,7 @@ class GiantRat(Monster):
|
|||||||
challenge_rating = 1 / 8
|
challenge_rating = 1 / 8
|
||||||
armor_class = 12
|
armor_class = 12
|
||||||
skills = ""
|
skills = ""
|
||||||
senses = "Darkvision 60 ft., Passive perception 10"
|
senses = "Darkvision 60 ft., passive Perception 10"
|
||||||
languages = ""
|
languages = ""
|
||||||
strength = Ability(7)
|
strength = Ability(7)
|
||||||
dexterity = Ability(15)
|
dexterity = Ability(15)
|
||||||
@@ -287,9 +281,9 @@ class GiantPoisonousSnake(Monster):
|
|||||||
|
|
||||||
class PoisonousSnake(Monster):
|
class PoisonousSnake(Monster):
|
||||||
"""**Bite:** Melee Weapon Attack: +5 to hit, reach 5 ft., one target.
|
"""**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
|
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"
|
name = "Poisonous snake"
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ class HalfOrc(Race):
|
|||||||
features = (feats.Darkvision, feats.RelentlessEndurance, feats.SavageAttacks)
|
features = (feats.Darkvision, feats.RelentlessEndurance, feats.SavageAttacks)
|
||||||
|
|
||||||
|
|
||||||
# Tielflings
|
# Tieflings
|
||||||
class Tiefling(Race):
|
class Tiefling(Race):
|
||||||
name = "Tiefling"
|
name = "Tiefling"
|
||||||
size = "medium"
|
size = "medium"
|
||||||
@@ -267,7 +267,7 @@ class Tiefling(Race):
|
|||||||
features = (feats.Darkvision, feats.HellishResistance, feats.InfernalLegacy)
|
features = (feats.Darkvision, feats.HellishResistance, feats.InfernalLegacy)
|
||||||
|
|
||||||
|
|
||||||
# Aassimar
|
# Aasimar
|
||||||
class _Aasimar(Race):
|
class _Aasimar(Race):
|
||||||
name = "Aasimar"
|
name = "Aasimar"
|
||||||
size = "medium"
|
size = "medium"
|
||||||
|
|||||||
+68
-63
@@ -64,10 +64,6 @@ def findattr(obj, name):
|
|||||||
def mod_str(modifier):
|
def mod_str(modifier):
|
||||||
"""Converts a modifier to a string, eg 2 -> '+2'."""
|
"""Converts a modifier to a string, eg 2 -> '+2'."""
|
||||||
return "{:+d}".format(modifier)
|
return "{:+d}".format(modifier)
|
||||||
if modifier == 0:
|
|
||||||
return str(modifier)
|
|
||||||
else:
|
|
||||||
return "{:+}".format(modifier)
|
|
||||||
|
|
||||||
|
|
||||||
AbilityScore = namedtuple("AbilityScore", ("value", "modifier", "saving_throw"))
|
AbilityScore = namedtuple("AbilityScore", ("value", "modifier", "saving_throw"))
|
||||||
@@ -90,25 +86,25 @@ class Ability:
|
|||||||
# ability score dictionary exists but doesn't have this ability
|
# ability score dictionary exists but doesn't have this ability
|
||||||
obj._ability_scores[self.ability_name] = self.default_value
|
obj._ability_scores[self.ability_name] = self.default_value
|
||||||
|
|
||||||
def __get__(self, character, Character):
|
def __get__(self, entity, Entity):
|
||||||
self._check_dict(character)
|
self._check_dict(entity)
|
||||||
score = character._ability_scores[self.ability_name]
|
score = entity._ability_scores[self.ability_name]
|
||||||
modifier = math.floor((score - 10) / 2)
|
modifier = math.floor((score - 10) / 2)
|
||||||
# Check for proficiency
|
# Check for proficiency
|
||||||
saving_throw = modifier
|
saving_throw = modifier
|
||||||
if self.ability_name is not None and hasattr(
|
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:
|
if is_proficient:
|
||||||
saving_throw += character.proficiency_bonus
|
saving_throw += entity.proficiency_bonus
|
||||||
# Create the named tuple
|
# Create the named tuple
|
||||||
value = AbilityScore(modifier=modifier, value=score, saving_throw=saving_throw)
|
value = AbilityScore(modifier=modifier, value=score, saving_throw=saving_throw)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def __set__(self, character, val):
|
def __set__(self, entity, val):
|
||||||
self._check_dict(character)
|
self._check_dict(entity)
|
||||||
character._ability_scores[self.ability_name] = val
|
entity._ability_scores[self.ability_name] = val
|
||||||
self.value = val
|
self.value = val
|
||||||
|
|
||||||
|
|
||||||
@@ -118,27 +114,27 @@ class Skill:
|
|||||||
def __init__(self, ability):
|
def __init__(self, ability):
|
||||||
self.ability_name = ability
|
self.ability_name = ability
|
||||||
|
|
||||||
def __set_name__(self, character, name):
|
def __set_name__(self, entity, name):
|
||||||
self.skill_name = name.lower().replace("_", " ")
|
self.skill_name = name.lower().replace("_", " ")
|
||||||
self.character = character
|
self.character = entity
|
||||||
|
|
||||||
def __get__(self, character, owner):
|
def __get__(self, entity, owner):
|
||||||
ability = getattr(character, self.ability_name)
|
ability = getattr(entity, self.ability_name)
|
||||||
modifier = ability.modifier
|
modifier = ability.modifier
|
||||||
# Check for proficiency
|
# Check for proficiency
|
||||||
is_proficient = self.skill_name in character.skill_proficiencies
|
is_proficient = self.skill_name in entity.skill_proficiencies
|
||||||
if is_proficient:
|
if is_proficient:
|
||||||
modifier += character.proficiency_bonus
|
modifier += entity.proficiency_bonus
|
||||||
elif character.has_feature(JackOfAllTrades):
|
elif entity.has_feature(JackOfAllTrades):
|
||||||
modifier += character.proficiency_bonus // 2
|
modifier += entity.proficiency_bonus // 2
|
||||||
elif character.has_feature(RemarkableAthelete):
|
elif entity.has_feature(RemarkableAthelete):
|
||||||
if self.ability_name.lower() in ("strength", "dexterity", "constitution"):
|
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
|
# Check for expertise
|
||||||
is_expert = self.skill_name in character.skill_expertise
|
is_expert = self.skill_name in entity.skill_expertise
|
||||||
if is_expert:
|
if is_expert:
|
||||||
modifier += character.proficiency_bonus
|
modifier += entity.proficiency_bonus
|
||||||
return modifier
|
return modifier
|
||||||
|
|
||||||
|
|
||||||
@@ -147,36 +143,36 @@ class ArmorClass:
|
|||||||
The Armor Class of a character
|
The Armor Class of a character
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __get__(self, char, Character):
|
def __get__(self, entity, Entity):
|
||||||
armor = char.armor or NoArmor()
|
armor = entity.armor or NoArmor()
|
||||||
ac = armor.base_armor_class
|
ac = armor.base_armor_class
|
||||||
# calculate and apply modifiers
|
# calculate and apply modifiers
|
||||||
if armor.dexterity_mod_max is None:
|
if armor.dexterity_mod_max is None:
|
||||||
ac += char.dexterity.modifier
|
ac += entity.dexterity.modifier
|
||||||
else:
|
else:
|
||||||
ac += min(char.dexterity.modifier, armor.dexterity_mod_max)
|
ac += min(entity.dexterity.modifier, armor.dexterity_mod_max)
|
||||||
if char.has_feature(NaturalArmor):
|
if entity.has_feature(NaturalArmor):
|
||||||
ac = max(ac, 13 + char.dexterity.modifier)
|
ac = max(ac, 13 + entity.dexterity.modifier)
|
||||||
shield = char.shield or NoShield()
|
shield = entity.shield or NoShield()
|
||||||
ac += shield.base_armor_class
|
ac += shield.base_armor_class
|
||||||
# Compute feature-specific additions
|
# Compute feature-specific additions
|
||||||
if char.has_feature(UnarmoredDefenseMonk):
|
if entity.has_feature(UnarmoredDefenseMonk):
|
||||||
if isinstance(armor, NoArmor) and isinstance(shield, NoShield):
|
if isinstance(armor, NoArmor) and isinstance(shield, NoShield):
|
||||||
ac += char.wisdom.modifier
|
ac += entity.wisdom.modifier
|
||||||
if char.has_feature(UnarmoredDefenseBarbarian):
|
if entity.has_feature(UnarmoredDefenseBarbarian):
|
||||||
if isinstance(armor, NoArmor):
|
if isinstance(armor, NoArmor):
|
||||||
ac += char.constitution.modifier
|
ac += entity.constitution.modifier
|
||||||
if char.has_feature(DraconicResilience):
|
if entity.has_feature(DraconicResilience):
|
||||||
if isinstance(armor, NoArmor):
|
if isinstance(armor, NoArmor):
|
||||||
ac += 3
|
ac += 3
|
||||||
if char.has_feature(Defense):
|
if entity.has_feature(Defense):
|
||||||
if not isinstance(armor, NoArmor):
|
if not isinstance(armor, NoArmor):
|
||||||
ac += 1
|
ac += 1
|
||||||
if char.has_feature(SoulOfTheForge):
|
if entity.has_feature(SoulOfTheForge):
|
||||||
if isinstance(armor, HeavyArmor):
|
if isinstance(armor, HeavyArmor):
|
||||||
ac += 1
|
ac += 1
|
||||||
# Check if any magic items add to AC
|
# 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"):
|
if hasattr(mitem, "ac_bonus"):
|
||||||
ac += mitem.ac_bonus
|
ac += mitem.ac_bonus
|
||||||
return ac
|
return ac
|
||||||
@@ -187,47 +183,56 @@ class Speed:
|
|||||||
The speed of a character
|
The speed of a character
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __get__(self, char, Character):
|
def __get__(self, entity, Entity):
|
||||||
speed = char.race.speed
|
speed = entity.race.speed
|
||||||
other_speed = ""
|
other_speed = ""
|
||||||
if isinstance(speed, str):
|
if isinstance(speed, str):
|
||||||
other_speed = speed[2:]
|
other_speed = speed[2:]
|
||||||
speed = int(speed[:2]) # ignore other speeds, like fly
|
speed = int(speed[:2]) # ignore other speeds, like fly
|
||||||
if char.has_feature(FastMovement):
|
if entity.has_feature(FastMovement):
|
||||||
if not isinstance(char.armor, HeavyArmor):
|
if not isinstance(entity.armor, HeavyArmor):
|
||||||
speed += 10
|
speed += 10
|
||||||
if char.has_feature(SuperiorMobility):
|
if entity.has_feature(SuperiorMobility):
|
||||||
speed += 10
|
speed += 10
|
||||||
if isinstance(char.armor, NoArmor) or (char.armor is None):
|
if isinstance(entity.armor, NoArmor) or (entity.armor is None):
|
||||||
for f in char.features:
|
for f in entity.features:
|
||||||
if isinstance(f, UnarmoredMovement):
|
if isinstance(f, UnarmoredMovement):
|
||||||
speed += f.speed_bonus
|
speed += f.speed_bonus
|
||||||
if char.has_feature(GiftOfTheDepths):
|
if entity.has_feature(GiftOfTheDepths):
|
||||||
if "swim" not in other_speed:
|
if "swim" not in other_speed:
|
||||||
other_speed += " ({:d} swim)".format(speed)
|
other_speed += " ({:d} swim)".format(speed)
|
||||||
if char.has_feature(SeaSoul):
|
if entity.has_feature(SeaSoul):
|
||||||
if "swim" not in other_speed:
|
if "swim" not in other_speed:
|
||||||
other_speed += " (30 swim)"
|
other_speed += " (30 swim)"
|
||||||
return "{:d}{:s}".format(speed, other_speed)
|
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"""
|
"""A character's initiative"""
|
||||||
|
|
||||||
def __get__(self, char, Character):
|
def __get__(self, entity, Entity):
|
||||||
ini = char.dexterity.modifier
|
ini, has_advantage = super(Initiative, self).__get__(entity, Entity)
|
||||||
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
|
|
||||||
ini = "{:+d}".format(ini)
|
ini = "{:+d}".format(ini)
|
||||||
has_advantage = (
|
|
||||||
char.has_feature(NaturalExplorerRevised)
|
|
||||||
or char.has_feature(FeralInstinct)
|
|
||||||
or char.has_feature(AmbushMaster)
|
|
||||||
)
|
|
||||||
if has_advantage:
|
if has_advantage:
|
||||||
ini += "(A)"
|
ini += "(A)"
|
||||||
return ini
|
return ini
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from dungeonsheets.dice import roll
|
||||||
from dungeonsheets.exceptions import DiceError
|
from dungeonsheets.exceptions import DiceError
|
||||||
from dungeonsheets import dice
|
from dungeonsheets import dice
|
||||||
|
|
||||||
|
|
||||||
class TestDice(TestCase):
|
class TestDice(TestCase):
|
||||||
|
|
||||||
def test_read_dice_str(self):
|
def test_read_dice_str(self):
|
||||||
out = dice.read_dice_str("1d6")
|
out = dice.read_dice_str("1d6")
|
||||||
self.assertEqual(out.faces, 6)
|
self.assertEqual(out.faces, 6)
|
||||||
@@ -16,3 +18,17 @@ class TestDice(TestCase):
|
|||||||
# Check a bad value
|
# Check a bad value
|
||||||
with self.assertRaises(DiceError):
|
with self.assertRaises(DiceError):
|
||||||
dice.read_dice_str("Ed15")
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user