mirror of
https://github.com/Threnklyn/dungeon-sheets.git
synced 2026-06-07 13:15:53 +02:00
Included companions and weight management
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,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
@@ -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))
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user