Merge pull request #129 from bw-mutley/Companions-and-Weight

Companions and weight
This commit is contained in:
Mark Wolfman
2022-07-08 19:36:38 -05:00
committed by GitHub
19 changed files with 1231 additions and 29 deletions
+65 -2
View File
@@ -25,6 +25,7 @@ from dungeonsheets.content_registry import find_content
from dungeonsheets.weapons import Weapon
from dungeonsheets.content import Creature
from dungeonsheets.dice import combine_dice
from dungeonsheets.equipment_reader import equipment_weight_parser
dice_re = re.compile(r"(\d+)d(\d+)")
@@ -86,6 +87,10 @@ class Character(Creature):
attacks_and_spellcasting = ""
class_list = list()
_background = None
_companions = []
_carrying_capacity = 0
_carrying_weight = 0
equipment_weight_dict = {}
# Characteristics
personality_traits = (
@@ -748,16 +753,38 @@ class Character(Creature):
featS += info_list
return "\n\n".join(featS)
@property
def carrying_capacity(self):
_ccModD = {"tiny":0.5, "small":1, "medium":1,
"large":2, "huge":4, "gargantum":8}
cc_mod = _ccModD[self.race.size.lower()]
return 15*self.strength.value*cc_mod
@property
def carrying_weight(self):
weight = equipment_weight_parser(self.equipment,
self.equipment_weight_dict)
weight += sum([w.weight for w in self.weapons])
if self.armor:
weight += self.armor.weight
if self.shield:
weight += 6
weight += sum([self.cp, self.sp, self.ep, self.gp, self.pp])/50
return round(weight, 2)
@property
def equipment_text(self):
eq_list = []
if hasattr(self, "magic_items"):
if hasattr(self, "magic_items") and len(self.magic_items) > 0:
eq_list += ["**Magic Items**"]
eq_list += [item.name for item in self.magic_items]
if hasattr(self, "equipment"):
if hasattr(self, "equipment") and len(self.equipment.strip()) > 0:
eq_list += ["**Other Equipment**"]
eq_list += [text.strip() for text in self.equipment.split("\n")
if not(text.isspace())]
cw, cc = self.carrying_weight, self.carrying_capacity
eq_list += [f"**Weight:** {cw} lb\n\n**Capacity:** {cc} lb"]
return "\n\n".join(eq_list)
@property
@@ -961,6 +988,42 @@ class Character(Creature):
if hasattr(self, "Druid"):
self.Druid.wild_shapes = new_shapes
@property
def ranger_beast(self):
if hasattr(self, "Ranger"):
return self.Ranger.ranger_beast
else:
return None
@ranger_beast.setter
def ranger_beast(self, beast):
msg = (
f"Companion '{beast}' not found. Please add it to"
" ``monsters.py``" )
beast = self._resolve_mechanic(beast, monsters.Monster, msg)
self.Ranger.ranger_beast = (beast(), self.proficiency_bonus)
@property
def companions(self):
"""Return the list of companions and summonables"""
companions = [compa for compa in self._companions]
if self.ranger_beast:
companions.append(self.ranger_beast)
return companions
@companions.setter
def companions(self, compas):
companions_list = []
# Retrieve the actual monster classes if possible
for compa in compas:
msg = (
f"Companion '{compa}' not found. Please add it to"
" ``monsters.py``" )
new_compa = self._resolve_mechanic(compa, monsters.Monster, msg)
companions_list.append(new_compa())
# Save the updated list for later
self._companions = companions_list
@property
def infusions_text(self):
if hasattr(self, "Artificer"):
+46 -1
View File
@@ -1,10 +1,16 @@
__all__ = ("Ranger", "RevisedRanger")
import re
import warnings
from collections import defaultdict
from dungeonsheets import features, spells, weapons
from dungeonsheets.classes.classes import CharClass, SubClass
from dungeonsheets.stats import (
attack_text_locator,
att_dmg_modifier,
skill_modifier,
)
# PHB
class Hunter(SubClass):
@@ -173,6 +179,7 @@ class MonsterSlayer(SubClass):
class Ranger(CharClass):
name = "Ranger"
hit_dice_faces = 10
_beast = None
saving_throw_proficiencies = ("strength", "dexterity")
primary_abilities = ("dexterity", "wisdom")
_proficiencies_text = (
@@ -245,6 +252,44 @@ class Ranger(CharClass):
20: (0, 4, 3, 3, 3, 2, 0, 0, 0, 0),
}
@property
def ranger_beast(self):
return self._beast
@ranger_beast.setter
def ranger_beast(self, beast_tuple):
"""Takes a tuple (monster, proficiency) to setup a
companion with adjusted stats."""
beast, prof_bonus = beast_tuple
desc_split = beast.description.split()
size = desc_split[0]
size_condition = size.lower() in ['tiny', 'small', 'medium']
cr_condition = beast.challenge_rating <= .25
if beast.is_beast and size_condition and cr_condition:
companion = beast
description, actions = beast.__doc__.split("# Actions")
if actions:
attacks_list = re.findall(attack_text_locator, actions)
start = 0
new_actions = ""
for att_text in attacks_list:
new_att_text = att_dmg_modifier(att_text, prof_bonus)
position = actions.find(att_text, start)
action_change = actions[start:position] + new_att_text
start = position + len(att_text)
new_actions = new_actions + action_change
companion.__doc__ = description + "# Actions" + new_actions
companion.armor_class = beast.armor_class + prof_bonus
companion.skills = skill_modifier(beast.skills, prof_bonus)
companion.saving_throws = skill_modifier(beast.saving_throws,
prof_bonus)
companion.hp_max = max(beast.hp_max, 4*self.level)
self._beast = companion
else:
msg = (
f"{beast.name} does not satisfy criteria to be Ranger's Companion."
)
warnings.warn(msg)
# Revised Ranger
class BeastConclave(SubClass):
+24 -8
View File
@@ -5,27 +5,38 @@ from itertools import groupby
from dungeonsheets.exceptions import DiceError
dice_re = re.compile(r"(\d+)d(\d+)", flags=re.I)
dice_re = re.compile(r"(\d+)d(\d+)([+-]\d+)*", flags=re.I)
dice_part_re = re.compile(r"[0-9d]+", flags=re.I)
Dice = namedtuple("Dice", ("num", "faces"))
Dice = namedtuple("Dice", ("num", "faces", "modifier"))
def read_dice_str(dice_str):
"""Interpret a D&D dice string, eg. 3d10.
"""Interpret a D&D dice string, eg. 3d10+2.
Returns
-------
dice : tuple
A named tuple with the scheme (num, faces), so '3d10' return
(num=3, faces=10)
A named tuple with the scheme (num, faces), so '3d10-2' return
(num=3, faces=10, modifier=-2)
"""
match = dice_re.match(dice_str)
dice_str = dice_str.replace(" ", "").replace("\n", "")
match = dice_re.search(dice_str)
if match is None:
raise DiceError(f"Cannot interpret dice string {dice_str}")
dice = Dice(num=int(match.group(1)), faces=int(match.group(2)))
num, faces = int(match.group(1)), int(match.group(2))
if match.group(3) is None:
modifier = 0
else:
modifier = int(match.group(3))
dice = Dice(num, faces, modifier)
return dice
def _dice_mean(dice, force_min=True):
"""Support function for calculating dice string mean."""
dmg_min = dice.num + dice.modifier
dmg_max = dice.num*dice.faces + dice.modifier
return (dmg_max - dmg_min)/2.0 + dmg_min
def combine_dice(dice_str):
"""Condense a dice string into its simplest representation.
@@ -56,10 +67,15 @@ def combine_dice(dice_str):
new_dice_str = " + ".join(new_parts)
return new_dice_str
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)])
def dice_roll_mean(dice_text):
"""Takes a dice string like '3d6 +3' and returns its average roll."""
dice = read_dice_str(dice_text)
return round(_dice_mean(dice))
+231
View File
@@ -0,0 +1,231 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Tue Feb 15 22:41:46 2022
@author: mauricio
"""
import warnings
import re
from dungeonsheets.weapons import simple_weapons, martial_weapons, firearms
from dungeonsheets.armor import all_armors
all_weapons = simple_weapons + martial_weapons + firearms
item_reader = re.compile(r"(\d*)(\s*)(.+)")
gear_weight = {"abacus":2,
"alms box":3,
"vial of acid":1,
"acid vials":1,
"flask of alchemist's fire":1,
"flasks of alchemist's fire":1,
"flask of oil":1,
"flasks of oil":1,
"arrows":1/20,
"arrow":1/20,
"bowgun needles": 1/50,
"crosbow bolts":1.5/20,
"sling bullets":1.5/20,
"antitoxin":0,
"crystal":1,
"orb":3,
"rod":2,
"staff":4,
"wand":1,
"backpack":5,
"ball bearings":2/1000,
"barrel":70,
"basked":2,
"bedroll":7,
"bell":0,
"blanket":3,
"block and tackle":5,
"block of incense":1/20,
"blocks of incense":1/20,
"censer":1/25,
"book":5,
"book of lore":5,
"glass bottle":2,
"bucket":2,
"caltrops":2/20,
"candle":0,
"candles":0,
"crosbow bolt case":1,
"scroll case":1,
"map case":1,
"cases for maps and scrolls":1,
"feet of chain":1,
"feet chain":1,
"chalk":0,
"chest":25,
"climber's kit":12,
"common clothes":3,
"costume":4,
"costumes":4,
"fine clothes":6,
"traveler's clothes":4,
"component pouch":2,
"crowbar": 5,
"sprig of mistletoe":0,
"totem":0,
"wooden staff":4,
"yew wand":1,
"fishing tackle":4,
"flask":1,
"tankard":1,
"grappling hook":4,
"hammer":3,
"knife":1,
"small knife":1,
"sacrificial knife":1,
"sledge hammer":10,
"healer's kit":3,
"amulet":1,
"emblem":0,
"reliquary":2,
"flask of holy water":1,
"hourglass":1,
"hunting trap":25,
"bottle of ink":0,
"ink pen":0,
"jug":4,
"pitcher":4,
"ladder":25,
"lamp":1,
"bullseye lantern":2,
"hooded lantern":2,
"lock":1,
"magnifying glass":0,
"manacles":6,
"mess kit":1,
"steel mirror":0.5,
"flask of oil":1,
"paper sheet":0,
"parchment":0,
"sheets of parchment":0,
"vial of perfume":0,
"miner's pick":10,
"piton":0.25,
"pitons":0.25,
"vial of poison":0,
"feet pole":7,
"iron pot":10,
"potion of healing":0.5,
"pouch":1,
"little bag of sand":1,
"quiver":1,
"portable ram":35,
"days of rations":2,
"day of ration":2,
"robes":4,
"feet of hempen rope":10/50,
"feet hempen rope":10/50,
"feet of silk rope":5/50,
"feet of string":1/50,
"feet silk rope":5/50,
"sack":0.5,
"merchant's scale":3,
"sealing wax":0,
"shovel":5,
"signal whistle":0,
"signet ring":0,
"soap":0,
"spell book":3,
"iron spikes":5/10,
"spyglass":1,
"two-person tent":20,
"tinderbox":1,
"torch":1,
"torches":1,
"vial":0,
"vestments":3,
"waterskin":5,
"wheatstone":1,
"moonstone":1/20,
"quartz":1/20,
"gemstone":1/20}
tools_weight = {"alchemist's supplies":8,
"brewer's supplies":9,
"calligrapher's supplies":5,
"capenter's tools":6,
"cartographer's tools":6,
"cobbler's tools":5,
"cook's utensils":8,
"glassblower's tools":5,
"jeweler's tools":2,
"leatherworker's tools":5,
"mason's tools":8,
"painter's supplies":5,
"potter's tools":3,
"smith's tools":8,
"tinker's tools":10,
"weaver's tools":5,
"woodcarver's tools":5,
"disguise kit":3,
"forgery kit":5,
"dice set":0,
"set of bone dice":0,
"dragonchess set":0.5,
"playing card set":0,
"three-dragon ante set":0,
"herbalism kit":3,
"bagpipes":6,
"drum":3,
"dulcimer":10,
"flute":1,
"lute":2,
"lyre":2,
"horn":2,
"pan flute":2,
"shawm":1,
"viol":1,
"navigator's tools":2,
"poisoner's kit":2,
"thieves' tools":1}
gear_weight.update(tools_weight)
gear_weight.update({armor.name.lower():armor.weight for armor in all_armors})
gear_weight.update({w.name.lower():w.weight for w in all_weapons})
burglars_pack = """backpack, {ball_bearings} ball bearings,
{string} feet of string, bell, {candles} candles, crowbar, hammer,
{pitons} pitons, hooded lantern,
{oil} flasks of oil, {rations} days of rations, tinderbox, waterskin,
{rope} feet of hempen rope"""
diplomats_pack = """chest, {cases} cases for maps and scrolls,
fine clothes, bottle of ink, ink pen, lamp, {oil} flasks of oil,
{paper} paper sheet, vial of perfume, sealing wax, soap"""
dungeoneers_pack = """backpack, crowbar, hammer, {pitons} pitons,
{torches} torches, tinderbox, {rations} days of rations, waterskin,
{rope} feet of hempen rope"""
entertainers_pack = """backpack, bedroll, {costumes} costumes,
{candles} candles, {rations} days of rations, waterskin, disguise kit"""
explorers_pack = """backpack, bedroll, mess kit, tinderbox, {torches} torches,
{rations} days of rations, waterskin, {rope} feet of hempen rope"""
priests_pack = """backpack, blanket, {candles} candles, tinderbox, alms box,
{incense} blocks of incense, censer, vestments, {rations} days of rations,
waterskin"""
scholars_pack = """backpack, book of lore, bottle of ink, ink pen,
{parchment} sheets of parchment, little bag of sand, small knife"""
def equipment_weight_parser(equipment, gear_dict={}):
if not equipment.strip():
return 0
gear_w = gear_weight.copy()
gear_w.update(gear_dict)
weight = 0
for gear in equipment.split(','):
gear = gear.lower().strip().strip(".")
q, _, item = item_reader.match(gear).groups()
if q:
q = int(q)
else:
q = 1
if not(item in gear_w.keys()):
msg = f'{item} not found in gear_weight dictionary, please add.'
warnings.warn(msg)
continue
weight = weight + q*gear_w[item]
return weight
+9 -9
View File
@@ -75,7 +75,7 @@
\CharismaProficiency{[[ "charisma" in char.saving_throw_proficiencies ]]}
\AcrobaticsProficiency{[[ "acrobatics" in char.skill_proficiencies ]]}
\AnimalHandlingProficiency{[[ "animal_handling" in char.skill_proficiencies ]]}
\AnimalHandlingProficiency{[[ "animal handling" in char.skill_proficiencies ]]}
\ArcanaProficiency{[[ "arcana" in char.skill_proficiencies ]]}
\AthleticsProficiency{[[ "athletics" in char.skill_proficiencies ]]}
\DeceptionProficiency{[[ "deception" in char.skill_proficiencies ]]}
@@ -89,7 +89,7 @@
\PerformanceProficiency{[[ "performance" in char.skill_proficiencies ]]}
\PersuasionProficiency{[[ "persuasion" in char.skill_proficiencies ]]}
\ReligionProficiency{[[ "religion" in char.skill_proficiencies ]]}
\SleightOfHandProficiency{[[ "sleight_of_hand" in char.skill_proficiencies ]]}
\SleightOfHandProficiency{[[ "sleight of hand" in char.skill_proficiencies ]]}
\StealthProficiency{[[ "stealth" in char.skill_proficiencies ]]}
\SurvivalProficiency{[[ "survival" in char.skill_proficiencies ]]}
@@ -101,16 +101,16 @@
\Initiative{[[ char.initiative ]]}
\Speed{[[ char.speed ]]}
\MaxHitPoints{[[ char.hp_max ]]}
\CurrentHitPoints{[[ char.hp_current ]]}
\TemporaryHitPoints{[[ char.hp_temp ]]}
[% if char.hp_current %]\CurrentHitPoints{[[ char.hp_current ]]}[% endif %]
[% if char.hp_temp %]\TemporaryHitPoints{[[ char.hp_temp ]]}[% endif %]
\MaxHitDice{[[ char.hit_dice.replace(" ","") ]]}
\CurrentHitDice{[[ char.hit_dice_current.replace(" ", "") ]]}
\CP{[[ char.cp ]]}
\SP{[[ char.sp ]]}
\GP{[[ char.gp ]]}
\EP{[[ char.ep ]]}
\PP{[[ char.pp ]]}
\CP{[% if char.cp > 0 %][[ char.cp ]][% endif %]}
\SP{[% if char.sp > 0 %][[ char.sp ]][% endif %]}
\GP{[% if char.gp > 0 %][[ char.gp ]][% endif %]}
\EP{[% if char.ep > 0 %][[ char.ep ]][% endif %]}
\PP{[% if char.pp > 0 %][[ char.pp ]][% endif %]}
[% for w in char.weapons %]
\AddWeapon{[[ w.name ]]}{[[ "{:+d}".format(w.attack_modifier) ]]}{[[ "{}/{}".format(w.damage, w.damage_type) ]]}
@@ -0,0 +1,91 @@
<h1 id="character-companions">Companions</h1>
[% for monster in monsters|sort(attribute='name') %]
<div class="stat-block">
<h2 id="character-companions-[[ monster.name|to_heading_id ]]">[[ monster.name ]]</h2>
[% if monster.description %]
<p class="creature-description">[[ monster.description ]]</p>
[% endif %]
<!-- Basic properties -->
<table class="details">
<tr>
<th>Armor Class</th>
<th>Hit Points</th>
<th>Speed</th>
</tr>
<tr>
<td>[[ monster.armor_class ]]</td>
<td>[[ monster.hp_max ]] ([[ monster.hit_dice ]])</td>
<td>[[ monster.speed ]][% if monster.swim_speed %],
[[ monster.swim_speed ]] swim[% endif %][% if monster.fly_speed %],
[[ monster.fly_speed ]] fly[% endif %][% if monster.burrow_speed %],
[[ monster.burrow_speed ]] burrow[% endif %]</td>
</tr>
</table>
<!-- Attributes -->
<table class="details">
<tr>
<th>STR</th>
<th>DEX</th>
<th>CON</th>
<th>INT</th>
<th>WIS</th>
<th>CHA</th>
</tr>
<tr>
<td>[[ monster.strength.value ]]</td>
<td>[[ monster.dexterity.value ]]</td>
<td>[[ monster.constitution.value ]]</td>
<td>[[ monster.intelligence.value ]]</td>
<td>[[ monster.wisdom.value ]]</td>
<td>[[ monster.charisma.value ]]</td>
</tr>
<tr>
<td>([[ monster.strength.modifier|mod_str ]])</td>
<td>([[ monster.dexterity.modifier|mod_str ]])</td>
<td>([[ monster.constitution.modifier|mod_str ]])</td>
<td>([[ monster.intelligence.modifier|mod_str ]])</td>
<td>([[ monster.wisdom.modifier|mod_str ]])</td>
<td>([[ monster.charisma.modifier|mod_str ]])</td>
</tr>
</table>
<dl class="monster-details details">
[% if monster.skills != "" %]<dt>Skills</dt><dd>[[ monster.skills ]]</dd>[% endif %]
<dt>Senses</dt><dd>[% if monster.senses != "" %][[ monster.senses ]][% else %]--[% endif %]</dd>
<dt>Languages</dt><dd>[% if monster.languages != "" %][[ monster.languages ]][% else %]--[% endif %]</dd>
[% if monster.damage_resistances != "" %]<dt>Damage Resistances</dt><dd>[[ monster.damage_resistances ]]</dd>[% endif %]
[% if monster.damage_immunities != "" %]<dt>Damage Immunities</dt><dd>[[ monster.damage_immunities ]]</dd>[% endif %]
[% if monster.damage_vulnerabilities != "" %]<dt>Damage Vulnerabilities</dt><dd>[[ monster.damage_vulnerabilities ]]</dd>[% endif %]
[% if monster.condition_immunities != "" %]<dt>Condition Immunuties</dt><dd>[[ monster.condition_immunities ]]</dd>[% endif %]
[% if monster.saving_throws != "" %]<dt>Saving Throws</dt><dd>[[ monster.saving_throws ]]</dd>[% endif %]
<dt>Challenge<dd>[[ monster.challenge_rating ]] ([[ monster.challenge_rating | challenge_rating_to_xp ]] XP)</dd>
</dl>
[% if monster.spells | length > 0 %]
<dl class="monster-spell-list">
[% for level, spells in monster.spells | groupby('level') %]
<dt>[% if level == 0 %]Cantrips[% else %]Level [[ level ]][% endif %]</dt>
<dd>
[% for spell in spells %][% if not loop.first %], [% endif %]
<a href="#monster-spells-[[ spell.name | to_heading_id ]]">[[ spell.name ]]</a>[% endfor %]
</dd>
[% endfor %]
</dl>
[% endif %]
[[ monster.__doc__ | rst_to_html(top_heading_level=2) ]]
</div>
[% endfor %]
<h1 id="monster-spells">Monster Spells</h1>
[% from "spellblock.html" import spellblock %]
[% for spell in spell_list | sort(attribute="name") %]
[[ spellblock(spell, id_base="monster-spells") ]]
[% endfor %]
+100
View File
@@ -0,0 +1,100 @@
\pdfbookmark[0]{Companions}{Companions}
\section*{Companions}
[% if use_dnd_decorations %]
[% for monster in monsters|sort(attribute='name') %]
\pdfbookmark[1]{[[ monster.name ]]}{Companions - [[ monster.name ]]}
\begin{DndMonster}{[[ monster.name ]]}
\DndMonsterType{[[ monster.description ]]}
% If you want to use commas in the key values, enclose the values in braces.
\DndMonsterBasics[
armor-class = {[[ monster.armor_class ]]},
hit-points = {[[ monster.hp_max ]] ([[ monster.hit_dice ]])},
speed = {[[ monster.speed ]] ft.[% if monster.swim_speed %], [[ monster.swim_speed ]] ft. swim[% endif %][% if monster.fly_speed %], [[ monster.fly_speed ]] ft. fly[% endif %][% if monster.burrow_speed %], [[ monster.burrow_speed ]] ft. burrow[% endif %]},
]
\DndMonsterAbilityScores[
str = [[ monster.strength.value ]],
dex = [[ monster.dexterity.value ]],
con = [[ monster.constitution.value ]],
int = [[ monster.intelligence.value ]],
wis = [[ monster.wisdom.value ]],
cha = [[ monster.charisma.value ]],
]
\DndMonsterDetails[
saving-throws = {[[ monster.saving_throws ]]},
skills = {[[ monster.skills ]]},
damage-vulnerabilities = {[[ monster.damage_vulnerabilities ]]},
damage-resistances = {[[ monster.damage_resistances ]]},
damage-immunities = {[[ monster.damage_immunities ]]},
condition-immunities = {[[ monster.condition_immunities ]]},
senses = {[[ monster.senses ]]},
languages = {[% if monster.languages %][[ monster.languages ]][% else %] --- [% endif %]},
challenge = {[[ monster.challenge_rating ]] ([[ monster.challenge_rating | challenge_rating_to_xp ]] XP)},
]
%\DndMonsterSection{Actions}
[[ monster.__doc__ | rst_to_latex(top_heading_level=2) ]]
\end{DndMonster}
[% endfor %]
[% else %]
[% for monster in monsters|sort(attribute='name') %]
{
\pdfbookmark[1]{[[ monster.name ]]}{Companions - [[ monster.name ]]}
\section*{[[ monster.name ]]}
[% if monster.description %]
\subsection*{[[ monster.description ]]}
[% endif %]
\begin{tabular}{c c c}
Armor Class & Hit Points & Speed \\
\hline
[[ monster.armor_class ]] &
[[ monster.hp_max ]] ([[ monster.hit_dice ]]) &
[[ monster.speed ]] \\
[% if monster.swim_speed %]
& & [[ monster.swim_speed ]] swim \\
[% endif %]
[% if monster.fly_speed %]
& & [[ monster.fly_speed ]] fly \\
[% endif %]
[% if monster.burrow_speed %]
& & [[ monster.burrow_speed ]] burrow \\
[% endif %]
\end{tabular}
\vspace{0.2cm}
\begin{tabular}{c c c c c c}
STR & DEX & CON & INT & WIS & CHA \\
\hline
[[ monster.strength.value ]] & [[ monster.dexterity.value ]] & [[ monster.constitution.value ]] &
[[ monster.intelligence.value ]] & [[ monster.wisdom.value ]] & [[ monster.charisma.value ]] \\
([[ monster.strength.modifier|mod_str ]]) & ([[ monster.dexterity.modifier|mod_str ]]) &
([[ monster.constitution.modifier|mod_str ]]) & ([[ monster.intelligence.modifier|mod_str ]]) &
([[ monster.wisdom.modifier|mod_str ]]) & ([[ monster.charisma.modifier|mod_str ]]) \\
\end{tabular}
\vspace{0.2cm}
\begin{description}
[% if monster.skills != "" %]\item [Skills:] [[ monster.skills ]][% endif %]
\item [Senses:] [% if monster.senses != "" %][[ monster.senses ]][% else %]--[% endif %]
\item [Languages:] [% if monster.languages != "" %][[ monster.languages ]][% else %]--[% endif %]
[% if monster.damage_resistances != "" %]\item [Damage Resistances:] [[ monster.damage_resistances ]][% endif %]
[% if monster.damage_immunities != "" %]\item [Damage Immunities:] [[ monster.damage_immunities ]][% endif %]
[% if monster.damage_vulnerabilities != "" %]\item [Damage Vulnerabilities:] [[ monster.damage_vulnerabilities ]][% endif %]
[% if monster.condition_immunities != "" %]\item [Condition Immunuties:] [[ monster.condition_immunities ]][% endif %]
[% if monster.saving_throws != "" %]\item [Saving Throws:] [[ monster.saving_throws ]][% endif %]
\item [Challenge:] [[ monster.challenge_rating ]] ([[ monster.challenge_rating | challenge_rating_to_xp ]] XP)
\end{description}
\vspace{0.2cm}
[[ monster.__doc__ | rst_to_latex(top_heading_level=2) ]]
} %\color
[% endfor %]
[% endif %]
@@ -0,0 +1,9 @@
<h1 id="spells">Spells</h1>
<!-- Spell descriptions -->
[% from "spellblock.html" import spellblock %]
[% for spl in spells %]
[[ spellblock(spl, id_base="spells") ]]
[% endfor %]
@@ -0,0 +1,41 @@
\pdfbookmark[0]{Spells}{Spells}
\section*{Spells}
[% for spl in spells %]
\pdfbookmark[1]{[[ spl.name ]]}{Spells - [[ spl.name ]]}
[% if use_dnd_decorations %]
\DndSpellHeader
{[[ spl.name ]]}
{[% if spl.level > 0 %][[ ordinals[spl.level] ]]-level [[ spl.magic_school ]][% else %][[ spl.magic_school ]] Cantrip[% endif %] [% if spl.ritual %](\textit{ritual})[% endif %]}
{[[ spl.casting_time ]]}
{[[ spl.casting_range ]]}
{[[ spl.component_string ]]}
{[[ spl.duration ]]}
[% else %]
\section*{[[ spl.name ]]}
[% if spl.level > 0 %] %
\textit{[[ spl.magic_school ]] Level [[ spl.level ]]} %
[% else %] %
\textit{[[ spl.magic_school ]] Cantrip} %
[% endif %] %
[% if spl.ritual and spl.concentration %]%
(\textit{ritual}, \textit{concentration})%
[% elif spl.ritual %]%
(\textit{ritual})%
[% elif spl.concentration %]%
(\textit{concentration})%
[% endif %]%
%% \noindent
\begin{description}
\setlength{\itemsep}{\zerosep}%
\setlength{\parskip}{0pt}%
\item [Casting Time:] [[ spl.casting_time ]] \\
\item [Duration:] [[ spl.duration ]] \\
\item [Range:] [[ spl.casting_range ]] \\
\item [Components:] [[ spl.component_string ]]
\end{description}
% \vspace{\zerosep}
[% endif %]
[[ spl.__doc__ | rst_to_latex(top_heading_level=1) ]]
[% endfor %]
+25 -4
View File
@@ -86,14 +86,20 @@ def create_monsters_content(
monsters: Sequence[Union[monsters.Monster, str]],
suffix: str,
use_dnd_decorations: bool = False,
base_template: str = "monsters_template"
) -> str:
# Convert strings to Monster objects
template = jinja_env.get_template(f"monsters_template.{suffix}")
template = jinja_env.get_template(base_template+f".{suffix}")
spell_list = [Spell() for monster in monsters for Spell in monster.spells]
return template.render(monsters=monsters,
use_dnd_decorations=use_dnd_decorations, spell_list=spell_list)
def create_gm_spellbook(spell_list, suffix):
template = jinja_env.get_template(f"gm_spellbook_template.{suffix}")
return template.render(spells=spell_list)
def create_party_summary_content(
party: Sequence[Creature],
summary_rst: str,
@@ -282,6 +288,12 @@ def make_gm_sheet(
monsters_, suffix=content_suffix, use_dnd_decorations=fancy_decorations
)
)
# Add the GM Spellbook
spells = [Spell() for monster in monsters_ for Spell in monster.spells]
spells = set(spells)
content.append(create_gm_spellbook(spells, content_suffix))
# Add the random tables
tables = [
find_content(s, valid_classes=[random_tables.RandomTable])
@@ -400,7 +412,7 @@ def make_character_content(
content.append(
create_magic_items_content(character, content_suffix=content_format, use_dnd_decorations=fancy_decorations)
)
if character.is_spellcaster:
if len(getattr(character, 'spells', [])) > 0:
content.append(
create_spellbook_content(character, content_suffix=content_format, use_dnd_decorations=fancy_decorations)
)
@@ -414,6 +426,13 @@ def make_character_content(
content.append(
create_druid_shapes_content(character, content_suffix=content_format, use_dnd_decorations=fancy_decorations)
)
# Create a list of companions
if len(getattr(character, "companions", [])) > 0:
content.append(
create_monsters_content(character.companions, suffix=content_format,
use_dnd_decorations=fancy_decorations, base_template="companions_template")
)
# Postamble, empty for HTML
content.append(
jinja_env.get_template(f"postamble.{content_format}").render(
@@ -479,9 +498,11 @@ def make_character_sheet(
character_props = readers.read_sheet_file(char_file)
character = _char.Character.load(character_props)
# Load image file if present
portrait_file=""
if character.portrait:
portrait_file = character.portrait
if portrait_file is True:
portrait_file=char_file.stem + ".jpeg"
elif portrait_file is False:
portrait_file=""
# Set the fields in the FDF
basename = char_file.stem
char_base = basename + "_char"
+59 -1
View File
@@ -1,9 +1,11 @@
import re
import math
from collections import namedtuple
from math import ceil
import logging
from dungeonsheets.armor import HeavyArmor, NoArmor, NoShield
from dungeonsheets.dice import dice_roll_mean
from dungeonsheets.features import (
AmbushMaster,
Defense,
@@ -28,7 +30,14 @@ from dungeonsheets.features import (
log = logging.getLogger(__name__)
skill_text_locator = re.compile(r"\S+ [+-]\d+")
attack_text_locator = re.compile(r"attack:.*?damage", re.IGNORECASE|re.DOTALL)
attack = re.compile(r"attack:.*?to hit", re.IGNORECASE|re.DOTALL)
damage = re.compile(r"hit:.*?(\d+)d(\d+).*?damage", re.IGNORECASE|re.DOTALL)
damage_avg = re.compile(r"hit:.*?(\d+)", re.IGNORECASE|re.DOTALL)
damage_nodice = re.compile(r"hit:.*?damage", re.IGNORECASE|re.DOTALL)
modifier = re.compile(r"[+-].*?(\d+)", re.IGNORECASE|re.DOTALL)
single_damage = re.compile(r"(\d+)")
def mod_str(modifier):
"""Converts a modifier to a string, eg 2 -> '+2'."""
@@ -325,3 +334,52 @@ class Initiative(NumericalInitiative):
if has_advantage:
ini += "(A)"
return ini
def _add_modifier(att_text, prof):
"""Auxiliary function to add proficiency bonus prof
to att_text."""
_att_bonus_re = modifier.search(att_text)
att_bonus_text = _att_bonus_re.group()
att_bonus = int(att_bonus_text.replace(" ", "").replace("\n", "")) + prof
return re.sub(modifier, "{:+d}".format(att_bonus), att_text)
def skill_modifier(skills_text, prof):
"""Modifies the skill text string adding the proficiency
bonus to its values."""
skills_updated = []
skill_list = re.findall(skill_text_locator, skills_text)
if not skill_list:
return ""
for sk in skill_list:
increased_skill = _add_modifier(sk, prof)
skills_updated.append(increased_skill)
return ", ".join(skills_updated)
def att_dmg_modifier(text, prof):
"""Modify the attack and damage rolls for a strip
of attack text description."""
_att_re = attack.search(text)
if not _att_re:
raise ValueError("No attack info detected.")
att_text = _att_re.group()
new_att_text = _add_modifier(att_text, prof)
text = re.sub(attack, new_att_text, text)
_dmg_re = damage.search(text)
if _dmg_re:
dmg_text = _dmg_re.group()
new_dmg_text = _add_modifier(dmg_text, prof)
dmg_avg_value = dice_roll_mean(new_dmg_text)
_dmg_avg_re = damage_avg.search(new_dmg_text)
dmg_avg_text = _dmg_avg_re.group()
new_dmg_avg_text = re.sub("(\d+)", "{:d}".format(dmg_avg_value),
dmg_avg_text, 1)
new_dmg_text = re.sub(damage_avg, new_dmg_avg_text, new_dmg_text)
text = re.sub(damage, new_dmg_text, text)
else:
_dmg_re = damage_nodice.search(text)
dmg_text = _dmg_re.group()
_sdamage_re = single_damage.search(dmg_text)
sdamage = int(_sdamage_re.group()) + prof
new_dmg_text = re.sub(single_damage, "{:d}".format(sdamage), dmg_text)
text = re.sub(single_damage, new_dmg_text, text)
return text
+1 -1
View File
@@ -385,7 +385,7 @@ class Shortsword(MartialWeapon, MeleeWeapon):
cost = "10 gp"
base_damage = "1d6"
damage_type = "p"
weight = 0
weight = 2
properties = "Finesse, light"
is_finesse = True
ability = "strength"
+218
View File
@@ -0,0 +1,218 @@
"""This file defines some homebrew mechanics that can be imported into
character sheets using ``dungeonsheets.import_homebrew``. See
``homebrew.py`` for an example of how these homebrew mechanics can be
used.
"""
from dungeonsheets import race
from dungeonsheets import features as feats
# from dungeonsheets import mecanics
class WildCompanion(feats.Feature):
"""You gain the ability to summon a spirit that assumes an animal form:
as an action, you can expend a use of your Wild Shape feature
to cast the *find familiar* spell, without material components.
When you cast the spell in this way, the familiar
is a fey instead of a beast, and the familiar disapears after
a number of hours equal to half your druid level.
"""
name = "Wild Companion"
source = "Class (Druid)"
# shifters
class Shifting(feats.Feature):
"""As a bonus action, you can assume a more bestial appearence.
This transformation lasts for 1 minute, until you die, or until
you revert to your normal appearence as a bonus action. When you shift,
you gain temporary hit points equal to your level + your Constitution
modifier (minimum of 1 temporary hit point). You also gain additional
benefits that depend on your shifter subrace. Once you shift,
you can't to so again until you finish a short or long rest.
"""
name = "Beasthide Shifting"
source = "Race (Beasthide Shifter)"
class BeasthideShifting(feats.Feature):
"""Whenever you shift, you gain 1d6 additional temporary hit points.
While shifted, you have a +1 bonus to your Armor Class.
"""
name = "Beasthide Shifting (1x/SR)"
source = "Race (Beasthide Shifter)"
class LongtoothShifting(feats.Feature):
"""While shifted, you can use your elongated fangs to make an unarmed
strike as a bonus action. If you hit with your fangs, you can deal
piercing damage equal to 1d6 + your Strength modifier, instead
of the bludgeoning damage normal for an unarmed attack.
"""
name = "Longtooth Shifting (1x/SR)"
source = "Race (Longtooth Shifter)"
class SwiftstrideShifting(feats.Feature):
"""While shifted, your walking speed increases by 10 feet. Additionally,
you can move up to 10 feet as a reaction when a creature ends its turn
within 5 feet of you. This reactive movement doesn't provoke
opportunity attacks.
"""
name = "Swiftstride Shifting (1x/SR)"
source = "Race (Swiftstride Shifter)"
class WildhuntShifting(feats.Feature):
"""While shifted, you have advantage on Wisdom checks, and no creature
within 30 feet of you can make an attack roll with advantage against you,
unless you are incapacitated.
"""
name = "Wildhunt Shifting (1x/SR)"
source = "Race (Beasthide Shifter)"
class NaturalAthlete(feats.Feature):
"""You have proficiency in the Athletics skill.
"""
name = "Natural Athlete"
source = "Race (Beasthide Shifter)"
class Fierce(feats.Feature):
"""You have proficiency in the Intimidation skill.
"""
name = "Fierce"
source = "Race (Longtooth Shifter)"
class Graceful(feats.Feature):
"""You have proficiency in the Acrobatics skill.
"""
name = "Graceful"
source = "Race (Swiftstride Shifter)"
class NaturalTracker(feats.Feature):
"""You have proficiency in the Survival skill.
"""
name = "Natural Tracker"
source = "Race (Wildhunt Shifter)"
class _Shifter(race.Race):
name = "Shifter"
size = "medium"
speed = 30
languages = ("Common", )
features = (feats.Darkvision, Shifting)
class BeasthideShifter(_Shifter):
name = "Beasthide Shifter"
constitution_bonus = 2
strength_bonus = 1
features = _Shifter.features + (BeasthideShifting, NaturalAthlete)
class LongtoothShifter(_Shifter):
name = "Longtooth Shifter"
constitution_bonus = 2
strength_bonus = 1
features = _Shifter.features + (LongtoothShifting, Fierce)
class SwiftstrideShifter(_Shifter):
name = "Swiftstride Shifter"
dexterity_bonus = 2
charisma_bonus = 1
features = _Shifter.features + (SwiftstrideShifting, Graceful)
class WildhuntShifter(_Shifter):
name = "Wildhunt Shifter"
constitution_bonus = 2
strength_bonus = 1
features = _Shifter.features + (WildhuntShifting, NaturalTracker)
class DualMind(feats.Feature):
"""You have advantage on all Wisdom saving throws.
"""
name = "Dual Mind"
source = "Race (Kalashtar)"
class MentalDiscipline(feats.Feature):
"""You have resistance to psychic damage.
"""
name = "Mental Discipline"
source = "Race (Kalashtar)"
class MindLink(feats.Feature):
"""You can speak telepathically to any creature you can see, provided
the crature is within a number of feet of you equal to 10 times your level.
You don't need to share a language with the creature for it to understand
your telepathic utterances, but the creature must be able to
understand at least one language.
When you are using this trait to speak telepathically to a creature,
you can use your action to give that crature the ability to speak
telepatically with you for 1 hour or until you end this effect as an
action. To use this ability, the creature must be able to see you and must
be within this trait's range. You can give this ability to only
one creature at a time; giving it to another creature takes it away from
another creature who has it."""
name = "Mind Link"
source = "Race (Kalashtar)"
class SeveredFromDreams(feats.Feature):
"""Kalashtar sleep, but they don't connect to the plane of dreams
as other creatures do. Instead, their minds draw from the memoires
of their otherworldly spirit while they sleep. As such,
you are immune to spells that require you to dream, like *dream*,
but not to spells and other magical effects that put you to sleep.
"""
name = "Severed from Dreams"
source = "Race (Kalashtar)"
class Kalashtar(race.Race):
name = "Kalashtar"
size = "medium"
speed = 30
charisma_bonus = 1
wisdom_bonus = 2
languages = ("Common", "Quori", )
features = (DualMind, MentalDiscipline, MindLink, SeveredFromDreams)
class PoisonResiliense(feats.Feature):
"""You have advantage on saving throws you make to avoid or end the
poisoned condition on yourself. You also have resistance to poison damage.
"""
name = "Poison Resilience"
source = "Race (Yuan-Ti)"
class SerpentineSpellcasting(feats.Feature):
"""You know the poison spray cantrip. You can cast animal friendship an
unlimited number of times with this trait, but you can target only
snakes with it. Starting at 3rd level, you can also cast suggestion
with this trait. Once you cast it, you can't do so again until you
finish a long rest. You can also cast it using any spell slots you
have of 2nd level or higher.
Intelligence, Wisdom, or Charisma is your spellcasting ability
for these spells when you cast them with this trait
(choose when you select this race).
"""
class Yuan_Ti(race.Race):
name = "Yuan-Ti"
size = "medium"
speed = 30
languages = ("Common", )
features = (feats.Darkvision, feats.MagicResistance, PoisonResiliense,
SerpentineSpellcasting)
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Tue Feb 22 01:04:23 2022
@author: mauricio
"""
burglars_pack = """backpack, {ball_bearings} ball bearings,
{string} feet of string, bell, {candles} candles, crowbar, hammer,
{pitons} pitons, hooded lantern,
{oil} flasks of oil, {rations} days of rations, tinderbox, waterskin,
{rope} feet of hempen rope."""
diplomats_pack = """chest, {cases} cases for maps and scrolls,
fine clothes, bottle of ink, ink pen, lamp, {oil} flasks of oil,
{paper} paper sheet, vial of perfume, sealing wax, soap."""
dungeoneers_pack = """backpack, crowbar, hammer, {pitons} pitons,
{torches} torches, tinderbox, {rations} days of rations, waterskin,
{rope} feet of hempen rope"""
entertainers_pack = """backpack, bedroll, {costumes} costumes,
{candles} candles, {rations} days of rations, waterskin, disguise kit"""
explorers_pack = """backpack, bedroll, mess kit, tinderbox, {torches} torches,
{rations} days of rations, waterskin, {rope} feet of hempen rope"""
priests_pack = """backpack, blanket, {candles} candles, tinderbox, alms box,
{incense} blocks of incense, censer, vestments, {rations} days of rations,
waterskin"""
scholars_pack = """backpack, book of lore, bottle of ink, ink pen,
{parchment} sheets of parchment, little bag of sand, small knife"""
if __name__ == "__main__":
from dungeonsheets.equipment_reader import equipment_weight_parser
quantities = {"ball_bearings":350, "string": 23, "candles": 4,
"pitons":18, "oil":3, "rations":2, "rope":15,
"cases":3, "paper":5, "torches":7, "costumes":2,
"incense":3, "parchment":17}
for kit in (burglars_pack, diplomats_pack, dungeoneers_pack,
entertainers_pack, explorers_pack, priests_pack,
scholars_pack):
equip = kit.format(**quantities)
print("EQUIPMENT: " + equip)
equip_weight = equipment_weight_parser(equip)
print("WEIGHT: " + str(equip_weight) + " lbs.")
print("="*15)
Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

+182
View File
@@ -0,0 +1,182 @@
"""This file describes the heroic adventurer DooDee.
It's used primarily for saving characters from create-character,
where there will be many missing sections.
Modify this file as you level up and then re-generate the character
sheet by running ``makesheets`` from the command line.
"""
# To add your own content, write a .py file with your definitions.
# Then, import here using the 'import_homebrew' function.
from dungeonsheets import import_homebrew
# from dungeonsheets.equipment_reader import explorers_pack
HB_races = import_homebrew("HB_races.py")
kits = import_homebrew("kits.py")
dungeonsheets_version = '0.17.1'
name = "DooDee"
player_name = "George Martin"
# Be sure to list Primary class first
classes = ['Druid', 'Ranger', 'Sorceror'] # ex: ['Wizard'] or ['Rogue', 'Fighter']
levels = [5, 3, 1] # ex: [10] or [3, 2]
subclasses = ["Circle of the Moon", "Beast Master", None ] # ex: ['Necromacy'] or ['Thief', None]
background = "Hermit"
race = HB_races.WildhuntShifter
alignment = "Lawful Neutral"
xp = 14587
hp_max = 77
# hp_temp = 5
# hp_current = 31
inspiration = 1 # integer inspiration value
# Ability Scores
strength = 10
dexterity = 17
constitution = 14
intelligence = 16
wisdom = 14
charisma = 12
# Select what skills you're proficient with
skill_proficiencies = ('insight', 'perception',
'medicine', 'survival', 'religion')
# Any skills you have "expertise" (Bard/Rogue) in
skill_expertise = ()
# Named features / feats that aren't part of your classes, race, or background.
# Also include Eldritch Invocations and features you make multiple selection of
# (like Maneuvers for Fighter, Metamagic for Sorcerors, Trick Shots for
# Gunslinger, etc.)
# Example:
# features = ('Tavern Brawler',) # take the optional Feat from PHB
features = (HB_races.WildCompanion, "Sharpshooter")
# If selecting among multiple feature options: ex Fighting Style
# Example (Fighting Style):
# feature_choices = ('Archery',)
feature_choices = ("Archery",)
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = ("Cartographer's tools", ) # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """Common, Druidic, Elven, Draconic"""
# Inventory
# Get yourself some money
cp = 0
sp = 95
ep = 12
gp = 140
pp = 0
# Put your equipped weapons and armor here
weapons = ("Longbow", 'Quarterstaff','dagger') # Example: ('shortsword', 'longsword')
magic_items = () # Example: ('ring of protection',)
armor = "Hide Armor" # Eg "leather armor"
shield = "" # Eg "shield"
# The equipment goes here. A total weight will be automatically
# calculated and added.
equipment = kits.explorers_pack.format(rations=9, torches=3,
pitons=10, rope=50) + \
", human skin mask, sacrificial knife, 10 arrows."
# If the weight of an item is undetermined, you can include it
# in the equipment_weight_dict
equipment_weight_dict = {"human skin mask":0.5}
attacks_and_spellcasting = \
"""
Quarterstaff with Shillelagh: +5 to hit, 1d8+3/b
"""
# List of known spells
# Example: spells_prepared = ('magic missile', 'mage armor')
spells_prepared = ("Shillelagh", "Druidcraft", "Cure Wounds", "Faerie Fire",
"Entangle", "Thunderwave", "Fog Cloud", "Barkskin")
# Which spells have not been prepared
__spells_unprepared = ("Speak with animals", "Charm Person",
"Animal Friendship", "Create or Destroy Water",
"Goodberry", "Purify Food and Drink", "Find Familiar")
# all spells known
spells = spells_prepared + __spells_unprepared
# Wild shapes for Druid
wild_shapes = ("Ape", "Wolf", "Mastiff", "Giant Spider", "Tiger",
"Dire Wolf", "Brown Bear","Cat") # Ex: ('ape', 'wolf', 'ankylosaurus')
# List any monsters whose reference can come at hand
# for spells like Find Familiar
companions = ["owl", "poisonous snake", "panther"]
# Rangers Beast for Beast Master
ranger_beast = "Panther"
# Backstory
# Describe your backstory here
personality_traits = """
I am introspective.
"""
ideals = """I search for nature balance."""
bonds = """My friends from my village."""
flaws = """
I lose my temper when I see animal corpses as trophies."""
features_and_traits = """"""
portrait = 'shifter_2.png'
age = 15
height = "1,77m"
weight = "72kg"
eyes = "Black"
skin = "Brown"
hair = "Brown"
# optionally, if you set portrait to false, you can include a text
# in the appearance box using the 'appearece_text' variable:
# appearance_text =
additional_description = \
'''
Find it better to avoid conflict.
'''
backstory = \
'''
Born at Makudan Village, helped many other
shifters to overcome the vampire known as Strahd.
'''
treasure = \
'''
A Dire Wolf tooth
'''
allies = \
'''
His childhood friend Krenak
His elder master Caiubi;
Druids of Rakshak
Druids of Makudan
'''
org_name = \
'''
Druids of Makudan
'''
+46
View File
@@ -8,7 +8,9 @@ from dungeonsheets.character import (
Character,
Wizard,
Druid,
Ranger
)
from dungeonsheets.monsters import Panther
from dungeonsheets.weapons import Weapon, Shortsword, Battleaxe
from dungeonsheets.magic_items import MagicItem
from dungeonsheets.armor import Armor, LeatherArmor, Shield
@@ -239,6 +241,27 @@ class TestCharacter(TestCase):
char.wield_shield(Shield)
self.assertEqual(char.armor_class, 15)
def test_carrying_weight(self):
char = Character(race="lightfoot halfling", strength=12)
# Check carrying capacity
self.assertEqual(char.carrying_capacity, 180)
# Check the armor weight is included
char.wear_armor(LeatherArmor())
self.assertEqual(char.carrying_weight, 10)
# Check the shield weight is included
char = Character()
char.wield_shield("shield")
self.assertEqual(char.carrying_weight, 6)
# Check the weight weapons at hand are included
char = Character()
char.wield_weapon("shortsword")
char.wield_weapon("dagger")
self.assertEqual(char.carrying_weight, 3)
# Check the listed equipment is included
char = Character()
char.equipment = "blanket, crowbar"
self.assertEqual(char.carrying_weight, 8)
def test_speed(self):
# Check that the speed pulls from the character's race
char = Character(race="lightfoot halfling")
@@ -336,3 +359,26 @@ class DruidTestCase(TestCase):
not_beast = monsters.Monster()
not_beast.description = "monster"
self.assertFalse(low_druid.can_assume_shape(not_beast))
class BeastMasterTestCase(TestCase):
def test_ranger_beast(self):
char = Ranger(6, subclasses = ["Beast Master"])
char.ranger_beast = "Panther"
# Test added proficiency to AC and skills
self.assertEqual(char.ranger_beast.armor_class, 15)
_text = char.ranger_beast.skills.lower().replace(" ", "")
self.assertTrue(_text == 'perception+7,stealth+9')
# Check attack and attack damage changed
_text = char.ranger_beast.__doc__
_text = _text.lower().replace("\n", "").replace(" ", "")
self.assertTrue('hit:8(1d6+5)' in _text)
# Test HP changed
self.assertTrue(char.ranger_beast.hp_max == 24)
# Check HP gets the best option
char = Ranger(3, subclasses = ["Beast Master"])
char.ranger_beast = "Panther"
self.assertEqual(char.ranger_beast.hp_max, 13)
+18
View File
@@ -15,6 +15,11 @@ class TestDice(TestCase):
out = dice.read_dice_str("15d10")
self.assertEqual(out.faces, 10)
self.assertEqual(out.num, 15)
# Modifier
out = dice.read_dice_str("2d20 + 5")
self.assertEqual(out.faces, 20)
self.assertEqual(out.num, 2)
self.assertEqual(out.modifier, 5)
# Check a bad value
with self.assertRaises(DiceError):
dice.read_dice_str("Ed15")
@@ -23,6 +28,19 @@ class TestDice(TestCase):
self.assertEqual(dice.combine_dice("1d8 + 6 + 2d8 + 12"), "3d8 + 18")
self.assertEqual(dice.combine_dice("1d8 + 1d5 + 2d8 + 1d5"), "2d5 + 3d8")
def test_dice_mean(self):
dd = dice.read_dice_str("1d10")
dd_mean = dice._dice_mean(dd)
self.assertEqual(dd_mean, 5.5)
dd = dice.read_dice_str("2d20+4")
dd_mean = dice._dice_mean(dd)
self.assertEqual(dd_mean, 25)
def test_dice_roll_mean(self):
dd_mean = dice.dice_roll_mean("1d6")
self.assertEqual(dd_mean, 4)
dd_mean = dice.dice_roll_mean("2d20+2")
self.assertEqual(dd_mean, 23)
def test_simple_rolling(self):
num_tests = 100
+17
View File
@@ -0,0 +1,17 @@
from unittest import TestCase
from dungeonsheets import equipment_reader as equip
class TestEquipmentReader(TestCase):
def test_equipment_weight_parser(self):
content = """backpack, bedroll, mess kit, tinderbox, 10 torches,
9 days of rations, waterskin, 50 feet of hempen rope, Herbalism Kit,
component pouch"""
eq_weight = equip.equipment_weight_parser(content)
self.assertEqual(eq_weight, 62)
# Check additional equipment dict
equipment_weight_dict = {"human skin mask":0.5}
content = content + ", human skin mask"
eq_weight = equip.equipment_weight_parser(content, equipment_weight_dict)
self.assertEqual(eq_weight, 62.5)