Included companions and weight management

This commit is contained in:
bw-mutley
2022-03-19 12:31:03 -03:00
committed by GitHub
parent de1506c13f
commit f2949521d7
5 changed files with 361 additions and 13 deletions
+64 -2
View File
@@ -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,37 @@ 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])
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 +987,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"):
+28 -7
View File
@@ -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)
""" """
dice_str = dice_str.replace(" ", "").replace("\n", "")
match = dice_re.match(dice_str) match = dice_re.match(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,20 @@ 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))
if __name__ == "__main__":
ds = "10d12+10"
v = read_dice_str(ds)
print(v)
print(_dice_mean(v))
+188
View File
@@ -0,0 +1,188 @@
#!/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,
"vial of acid":1,
"flask of alchemist's fire":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,
"book":5,
"glass bottle":2,
"bucket":2,
"caltrops":2/20,
"candle":0,
"crosbow bolt case":1,
"scroll case":1,
"map case":1,
"feet of chain":1,
"feet chain":1,
"chalk":0,
"chest":25,
"climber's kit":12,
"common clothes":3,
"costume":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,
"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,
"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,
"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 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,
"waterskin":5,
"wheatstone":1}
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})
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
+22 -3
View File
@@ -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])
@@ -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,8 +498,8 @@ 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"
# Set the fields in the FDF # Set the fields in the FDF
basename = char_file.stem basename = char_file.stem
+59 -1
View File
@@ -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