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.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"):
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
|
||||
@@ -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 ]]}
|
||||
|
||||
\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 %]
|
||||
@@ -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]],
|
||||
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
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
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)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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