mirror of
https://github.com/Threnklyn/dungeon-sheets.git
synced 2026-05-18 20:23:27 +02:00
Merge pull request #129 from bw-mutley/Companions-and-Weight
Companions and weight
This commit is contained in:
@@ -25,6 +25,7 @@ from dungeonsheets.content_registry import find_content
|
|||||||
from dungeonsheets.weapons import Weapon
|
from dungeonsheets.weapons import Weapon
|
||||||
from dungeonsheets.content import Creature
|
from dungeonsheets.content import Creature
|
||||||
from dungeonsheets.dice import combine_dice
|
from dungeonsheets.dice import combine_dice
|
||||||
|
from dungeonsheets.equipment_reader import equipment_weight_parser
|
||||||
|
|
||||||
|
|
||||||
dice_re = re.compile(r"(\d+)d(\d+)")
|
dice_re = re.compile(r"(\d+)d(\d+)")
|
||||||
@@ -86,6 +87,10 @@ class Character(Creature):
|
|||||||
attacks_and_spellcasting = ""
|
attacks_and_spellcasting = ""
|
||||||
class_list = list()
|
class_list = list()
|
||||||
_background = None
|
_background = None
|
||||||
|
_companions = []
|
||||||
|
_carrying_capacity = 0
|
||||||
|
_carrying_weight = 0
|
||||||
|
equipment_weight_dict = {}
|
||||||
|
|
||||||
# Characteristics
|
# Characteristics
|
||||||
personality_traits = (
|
personality_traits = (
|
||||||
@@ -748,16 +753,38 @@ class Character(Creature):
|
|||||||
featS += info_list
|
featS += info_list
|
||||||
return "\n\n".join(featS)
|
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
|
@property
|
||||||
def equipment_text(self):
|
def equipment_text(self):
|
||||||
eq_list = []
|
eq_list = []
|
||||||
if hasattr(self, "magic_items"):
|
if hasattr(self, "magic_items") and len(self.magic_items) > 0:
|
||||||
eq_list += ["**Magic Items**"]
|
eq_list += ["**Magic Items**"]
|
||||||
eq_list += [item.name for item in self.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 += ["**Other Equipment**"]
|
||||||
eq_list += [text.strip() for text in self.equipment.split("\n")
|
eq_list += [text.strip() for text in self.equipment.split("\n")
|
||||||
if not(text.isspace())]
|
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)
|
return "\n\n".join(eq_list)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -961,6 +988,42 @@ class Character(Creature):
|
|||||||
if hasattr(self, "Druid"):
|
if hasattr(self, "Druid"):
|
||||||
self.Druid.wild_shapes = new_shapes
|
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
|
@property
|
||||||
def infusions_text(self):
|
def infusions_text(self):
|
||||||
if hasattr(self, "Artificer"):
|
if hasattr(self, "Artificer"):
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
__all__ = ("Ranger", "RevisedRanger")
|
__all__ = ("Ranger", "RevisedRanger")
|
||||||
|
|
||||||
|
import re
|
||||||
|
import warnings
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from dungeonsheets import features, spells, weapons
|
from dungeonsheets import features, spells, weapons
|
||||||
from dungeonsheets.classes.classes import CharClass, SubClass
|
from dungeonsheets.classes.classes import CharClass, SubClass
|
||||||
|
from dungeonsheets.stats import (
|
||||||
|
attack_text_locator,
|
||||||
|
att_dmg_modifier,
|
||||||
|
skill_modifier,
|
||||||
|
)
|
||||||
|
|
||||||
# PHB
|
# PHB
|
||||||
class Hunter(SubClass):
|
class Hunter(SubClass):
|
||||||
@@ -173,6 +179,7 @@ class MonsterSlayer(SubClass):
|
|||||||
class Ranger(CharClass):
|
class Ranger(CharClass):
|
||||||
name = "Ranger"
|
name = "Ranger"
|
||||||
hit_dice_faces = 10
|
hit_dice_faces = 10
|
||||||
|
_beast = None
|
||||||
saving_throw_proficiencies = ("strength", "dexterity")
|
saving_throw_proficiencies = ("strength", "dexterity")
|
||||||
primary_abilities = ("dexterity", "wisdom")
|
primary_abilities = ("dexterity", "wisdom")
|
||||||
_proficiencies_text = (
|
_proficiencies_text = (
|
||||||
@@ -245,6 +252,44 @@ class Ranger(CharClass):
|
|||||||
20: (0, 4, 3, 3, 3, 2, 0, 0, 0, 0),
|
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
|
# Revised Ranger
|
||||||
class BeastConclave(SubClass):
|
class BeastConclave(SubClass):
|
||||||
|
|||||||
+24
-8
@@ -5,27 +5,38 @@ from itertools import groupby
|
|||||||
|
|
||||||
from dungeonsheets.exceptions import DiceError
|
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_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):
|
def read_dice_str(dice_str):
|
||||||
"""Interpret a D&D dice string, eg. 3d10.
|
"""Interpret a D&D dice string, eg. 3d10+2.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
dice : tuple
|
dice : tuple
|
||||||
A named tuple with the scheme (num, faces), so '3d10' return
|
A named tuple with the scheme (num, faces), so '3d10-2' return
|
||||||
(num=3, faces=10)
|
(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:
|
if match is None:
|
||||||
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)))
|
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
|
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):
|
def combine_dice(dice_str):
|
||||||
"""Condense a dice string into its simplest representation.
|
"""Condense a dice string into its simplest representation.
|
||||||
@@ -56,10 +67,15 @@ def combine_dice(dice_str):
|
|||||||
new_dice_str = " + ".join(new_parts)
|
new_dice_str = " + ".join(new_parts)
|
||||||
return new_dice_str
|
return new_dice_str
|
||||||
|
|
||||||
|
|
||||||
def roll(a, b=None):
|
def roll(a, b=None):
|
||||||
"""roll(20) means roll 1d20, roll(2, 6) means roll 2d6"""
|
"""roll(20) means roll 1d20, roll(2, 6) means roll 2d6"""
|
||||||
if b is None:
|
if b is None:
|
||||||
return random.randint(1, a)
|
return random.randint(1, a)
|
||||||
else:
|
else:
|
||||||
return sum([random.randint(1, b) for _ in range(a)])
|
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))
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
\CharismaProficiency{[[ "charisma" in char.saving_throw_proficiencies ]]}
|
\CharismaProficiency{[[ "charisma" in char.saving_throw_proficiencies ]]}
|
||||||
|
|
||||||
\AcrobaticsProficiency{[[ "acrobatics" in char.skill_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 ]]}
|
\ArcanaProficiency{[[ "arcana" in char.skill_proficiencies ]]}
|
||||||
\AthleticsProficiency{[[ "athletics" in char.skill_proficiencies ]]}
|
\AthleticsProficiency{[[ "athletics" in char.skill_proficiencies ]]}
|
||||||
\DeceptionProficiency{[[ "deception" in char.skill_proficiencies ]]}
|
\DeceptionProficiency{[[ "deception" in char.skill_proficiencies ]]}
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
\PerformanceProficiency{[[ "performance" in char.skill_proficiencies ]]}
|
\PerformanceProficiency{[[ "performance" in char.skill_proficiencies ]]}
|
||||||
\PersuasionProficiency{[[ "persuasion" in char.skill_proficiencies ]]}
|
\PersuasionProficiency{[[ "persuasion" in char.skill_proficiencies ]]}
|
||||||
\ReligionProficiency{[[ "religion" 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 ]]}
|
\StealthProficiency{[[ "stealth" in char.skill_proficiencies ]]}
|
||||||
\SurvivalProficiency{[[ "survival" in char.skill_proficiencies ]]}
|
\SurvivalProficiency{[[ "survival" in char.skill_proficiencies ]]}
|
||||||
|
|
||||||
@@ -101,16 +101,16 @@
|
|||||||
\Initiative{[[ char.initiative ]]}
|
\Initiative{[[ char.initiative ]]}
|
||||||
\Speed{[[ char.speed ]]}
|
\Speed{[[ char.speed ]]}
|
||||||
\MaxHitPoints{[[ char.hp_max ]]}
|
\MaxHitPoints{[[ char.hp_max ]]}
|
||||||
\CurrentHitPoints{[[ char.hp_current ]]}
|
[% if char.hp_current %]\CurrentHitPoints{[[ char.hp_current ]]}[% endif %]
|
||||||
\TemporaryHitPoints{[[ char.hp_temp ]]}
|
[% if char.hp_temp %]\TemporaryHitPoints{[[ char.hp_temp ]]}[% endif %]
|
||||||
\MaxHitDice{[[ char.hit_dice.replace(" ","") ]]}
|
\MaxHitDice{[[ char.hit_dice.replace(" ","") ]]}
|
||||||
\CurrentHitDice{[[ char.hit_dice_current.replace(" ", "") ]]}
|
\CurrentHitDice{[[ char.hit_dice_current.replace(" ", "") ]]}
|
||||||
|
|
||||||
\CP{[[ char.cp ]]}
|
\CP{[% if char.cp > 0 %][[ char.cp ]][% endif %]}
|
||||||
\SP{[[ char.sp ]]}
|
\SP{[% if char.sp > 0 %][[ char.sp ]][% endif %]}
|
||||||
\GP{[[ char.gp ]]}
|
\GP{[% if char.gp > 0 %][[ char.gp ]][% endif %]}
|
||||||
\EP{[[ char.ep ]]}
|
\EP{[% if char.ep > 0 %][[ char.ep ]][% endif %]}
|
||||||
\PP{[[ char.pp ]]}
|
\PP{[% if char.pp > 0 %][[ char.pp ]][% endif %]}
|
||||||
|
|
||||||
[% for w in char.weapons %]
|
[% for w in char.weapons %]
|
||||||
\AddWeapon{[[ w.name ]]}{[[ "{:+d}".format(w.attack_modifier) ]]}{[[ "{}/{}".format(w.damage, w.damage_type) ]]}
|
\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 %]
|
||||||
@@ -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 %]
|
||||||
@@ -86,14 +86,20 @@ def create_monsters_content(
|
|||||||
monsters: Sequence[Union[monsters.Monster, str]],
|
monsters: Sequence[Union[monsters.Monster, str]],
|
||||||
suffix: str,
|
suffix: str,
|
||||||
use_dnd_decorations: bool = False,
|
use_dnd_decorations: bool = False,
|
||||||
|
base_template: str = "monsters_template"
|
||||||
) -> str:
|
) -> str:
|
||||||
# Convert strings to Monster objects
|
# 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]
|
spell_list = [Spell() for monster in monsters for Spell in monster.spells]
|
||||||
return template.render(monsters=monsters,
|
return template.render(monsters=monsters,
|
||||||
use_dnd_decorations=use_dnd_decorations, spell_list=spell_list)
|
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(
|
def create_party_summary_content(
|
||||||
party: Sequence[Creature],
|
party: Sequence[Creature],
|
||||||
summary_rst: str,
|
summary_rst: str,
|
||||||
@@ -282,6 +288,12 @@ def make_gm_sheet(
|
|||||||
monsters_, suffix=content_suffix, use_dnd_decorations=fancy_decorations
|
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
|
# Add the random tables
|
||||||
tables = [
|
tables = [
|
||||||
find_content(s, valid_classes=[random_tables.RandomTable])
|
find_content(s, valid_classes=[random_tables.RandomTable])
|
||||||
@@ -400,7 +412,7 @@ def make_character_content(
|
|||||||
content.append(
|
content.append(
|
||||||
create_magic_items_content(character, content_suffix=content_format, use_dnd_decorations=fancy_decorations)
|
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(
|
content.append(
|
||||||
create_spellbook_content(character, content_suffix=content_format, use_dnd_decorations=fancy_decorations)
|
create_spellbook_content(character, content_suffix=content_format, use_dnd_decorations=fancy_decorations)
|
||||||
)
|
)
|
||||||
@@ -414,6 +426,13 @@ def make_character_content(
|
|||||||
content.append(
|
content.append(
|
||||||
create_druid_shapes_content(character, content_suffix=content_format, use_dnd_decorations=fancy_decorations)
|
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
|
# Postamble, empty for HTML
|
||||||
content.append(
|
content.append(
|
||||||
jinja_env.get_template(f"postamble.{content_format}").render(
|
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_props = readers.read_sheet_file(char_file)
|
||||||
character = _char.Character.load(character_props)
|
character = _char.Character.load(character_props)
|
||||||
# Load image file if present
|
# Load image file if present
|
||||||
portrait_file=""
|
portrait_file = character.portrait
|
||||||
if character.portrait:
|
if portrait_file is True:
|
||||||
portrait_file=char_file.stem + ".jpeg"
|
portrait_file=char_file.stem + ".jpeg"
|
||||||
|
elif portrait_file is False:
|
||||||
|
portrait_file=""
|
||||||
# Set the fields in the FDF
|
# Set the fields in the FDF
|
||||||
basename = char_file.stem
|
basename = char_file.stem
|
||||||
char_base = basename + "_char"
|
char_base = basename + "_char"
|
||||||
|
|||||||
+59
-1
@@ -1,9 +1,11 @@
|
|||||||
|
import re
|
||||||
import math
|
import math
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from math import ceil
|
from math import ceil
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from dungeonsheets.armor import HeavyArmor, NoArmor, NoShield
|
from dungeonsheets.armor import HeavyArmor, NoArmor, NoShield
|
||||||
|
from dungeonsheets.dice import dice_roll_mean
|
||||||
from dungeonsheets.features import (
|
from dungeonsheets.features import (
|
||||||
AmbushMaster,
|
AmbushMaster,
|
||||||
Defense,
|
Defense,
|
||||||
@@ -28,7 +30,14 @@ from dungeonsheets.features import (
|
|||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
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):
|
def mod_str(modifier):
|
||||||
"""Converts a modifier to a string, eg 2 -> '+2'."""
|
"""Converts a modifier to a string, eg 2 -> '+2'."""
|
||||||
@@ -325,3 +334,52 @@ class Initiative(NumericalInitiative):
|
|||||||
if has_advantage:
|
if has_advantage:
|
||||||
ini += "(A)"
|
ini += "(A)"
|
||||||
return ini
|
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
|
||||||
|
|||||||
@@ -385,7 +385,7 @@ class Shortsword(MartialWeapon, MeleeWeapon):
|
|||||||
cost = "10 gp"
|
cost = "10 gp"
|
||||||
base_damage = "1d6"
|
base_damage = "1d6"
|
||||||
damage_type = "p"
|
damage_type = "p"
|
||||||
weight = 0
|
weight = 2
|
||||||
properties = "Finesse, light"
|
properties = "Finesse, light"
|
||||||
is_finesse = True
|
is_finesse = True
|
||||||
ability = "strength"
|
ability = "strength"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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 |
@@ -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
|
||||||
|
|
||||||
|
'''
|
||||||
@@ -8,7 +8,9 @@ from dungeonsheets.character import (
|
|||||||
Character,
|
Character,
|
||||||
Wizard,
|
Wizard,
|
||||||
Druid,
|
Druid,
|
||||||
|
Ranger
|
||||||
)
|
)
|
||||||
|
from dungeonsheets.monsters import Panther
|
||||||
from dungeonsheets.weapons import Weapon, Shortsword, Battleaxe
|
from dungeonsheets.weapons import Weapon, Shortsword, Battleaxe
|
||||||
from dungeonsheets.magic_items import MagicItem
|
from dungeonsheets.magic_items import MagicItem
|
||||||
from dungeonsheets.armor import Armor, LeatherArmor, Shield
|
from dungeonsheets.armor import Armor, LeatherArmor, Shield
|
||||||
@@ -239,6 +241,27 @@ class TestCharacter(TestCase):
|
|||||||
char.wield_shield(Shield)
|
char.wield_shield(Shield)
|
||||||
self.assertEqual(char.armor_class, 15)
|
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):
|
def test_speed(self):
|
||||||
# Check that the speed pulls from the character's race
|
# Check that the speed pulls from the character's race
|
||||||
char = Character(race="lightfoot halfling")
|
char = Character(race="lightfoot halfling")
|
||||||
@@ -336,3 +359,26 @@ class DruidTestCase(TestCase):
|
|||||||
not_beast = monsters.Monster()
|
not_beast = monsters.Monster()
|
||||||
not_beast.description = "monster"
|
not_beast.description = "monster"
|
||||||
self.assertFalse(low_druid.can_assume_shape(not_beast))
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ class TestDice(TestCase):
|
|||||||
out = dice.read_dice_str("15d10")
|
out = dice.read_dice_str("15d10")
|
||||||
self.assertEqual(out.faces, 10)
|
self.assertEqual(out.faces, 10)
|
||||||
self.assertEqual(out.num, 15)
|
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
|
# Check a bad value
|
||||||
with self.assertRaises(DiceError):
|
with self.assertRaises(DiceError):
|
||||||
dice.read_dice_str("Ed15")
|
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 + 6 + 2d8 + 12"), "3d8 + 18")
|
||||||
self.assertEqual(dice.combine_dice("1d8 + 1d5 + 2d8 + 1d5"), "2d5 + 3d8")
|
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):
|
def test_simple_rolling(self):
|
||||||
num_tests = 100
|
num_tests = 100
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
Reference in New Issue
Block a user