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.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,37 @@ 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])
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 +987,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"):
+28 -7
View File
@@ -5,27 +5,38 @@ from itertools import groupby
from dungeonsheets.exceptions import DiceError
dice_re = re.compile(r"(\d+)d(\d+)", flags=re.I)
dice_re = re.compile(r"(\d+)d(\d+)([+-]\d+)*", flags=re.I)
dice_part_re = re.compile(r"[0-9d]+", flags=re.I)
Dice = namedtuple("Dice", ("num", "faces"))
Dice = namedtuple("Dice", ("num", "faces", "modifier"))
def read_dice_str(dice_str):
"""Interpret a D&D dice string, eg. 3d10.
"""Interpret a D&D dice string, eg. 3d10+2.
Returns
-------
dice : tuple
A named tuple with the scheme (num, faces), so '3d10' return
(num=3, faces=10)
A named tuple with the scheme (num, faces), so '3d10-2' return
(num=3, faces=10, modifier=-2)
"""
dice_str = dice_str.replace(" ", "").replace("\n", "")
match = dice_re.match(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,20 @@ 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))
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]],
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])
@@ -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,8 +498,8 @@ 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"
# Set the fields in the FDF
basename = char_file.stem
+59 -1
View File
@@ -1,9 +1,11 @@
import re
import math
from collections import namedtuple
from math import ceil
import logging
from dungeonsheets.armor import HeavyArmor, NoArmor, NoShield
from dungeonsheets.dice import dice_roll_mean
from dungeonsheets.features import (
AmbushMaster,
Defense,
@@ -28,7 +30,14 @@ from dungeonsheets.features import (
log = logging.getLogger(__name__)
skill_text_locator = re.compile(r"\S+ [+-]\d+")
attack_text_locator = re.compile(r"attack:.*?damage", re.IGNORECASE|re.DOTALL)
attack = re.compile(r"attack:.*?to hit", re.IGNORECASE|re.DOTALL)
damage = re.compile(r"hit:.*?(\d+)d(\d+).*?damage", re.IGNORECASE|re.DOTALL)
damage_avg = re.compile(r"hit:.*?(\d+)", re.IGNORECASE|re.DOTALL)
damage_nodice = re.compile(r"hit:.*?damage", re.IGNORECASE|re.DOTALL)
modifier = re.compile(r"[+-].*?(\d+)", re.IGNORECASE|re.DOTALL)
single_damage = re.compile(r"(\d+)")
def mod_str(modifier):
"""Converts a modifier to a string, eg 2 -> '+2'."""
@@ -325,3 +334,52 @@ class Initiative(NumericalInitiative):
if has_advantage:
ini += "(A)"
return ini
def _add_modifier(att_text, prof):
"""Auxiliary function to add proficiency bonus prof
to att_text."""
_att_bonus_re = modifier.search(att_text)
att_bonus_text = _att_bonus_re.group()
att_bonus = int(att_bonus_text.replace(" ", "").replace("\n", "")) + prof
return re.sub(modifier, "{:+d}".format(att_bonus), att_text)
def skill_modifier(skills_text, prof):
"""Modifies the skill text string adding the proficiency
bonus to its values."""
skills_updated = []
skill_list = re.findall(skill_text_locator, skills_text)
if not skill_list:
return ""
for sk in skill_list:
increased_skill = _add_modifier(sk, prof)
skills_updated.append(increased_skill)
return ", ".join(skills_updated)
def att_dmg_modifier(text, prof):
"""Modify the attack and damage rolls for a strip
of attack text description."""
_att_re = attack.search(text)
if not _att_re:
raise ValueError("No attack info detected.")
att_text = _att_re.group()
new_att_text = _add_modifier(att_text, prof)
text = re.sub(attack, new_att_text, text)
_dmg_re = damage.search(text)
if _dmg_re:
dmg_text = _dmg_re.group()
new_dmg_text = _add_modifier(dmg_text, prof)
dmg_avg_value = dice_roll_mean(new_dmg_text)
_dmg_avg_re = damage_avg.search(new_dmg_text)
dmg_avg_text = _dmg_avg_re.group()
new_dmg_avg_text = re.sub("(\d+)", "{:d}".format(dmg_avg_value),
dmg_avg_text, 1)
new_dmg_text = re.sub(damage_avg, new_dmg_avg_text, new_dmg_text)
text = re.sub(damage, new_dmg_text, text)
else:
_dmg_re = damage_nodice.search(text)
dmg_text = _dmg_re.group()
_sdamage_re = single_damage.search(dmg_text)
sdamage = int(_sdamage_re.group()) + prof
new_dmg_text = re.sub(single_damage, "{:d}".format(sdamage), dmg_text)
text = re.sub(single_damage, new_dmg_text, text)
return text