mirror of
https://github.com/Threnklyn/dungeon-sheets.git
synced 2026-05-18 20:23:27 +02:00
Epub now contains character sheets information.
This commit is contained in:
@@ -248,7 +248,7 @@ class Character(Entity):
|
|||||||
def clear(self):
|
def clear(self):
|
||||||
# reset class-defined items
|
# reset class-defined items
|
||||||
self.class_list = list()
|
self.class_list = list()
|
||||||
self.weapons = list()
|
self._weapons = list()
|
||||||
self.magic_items = list()
|
self.magic_items = list()
|
||||||
self._saving_throw_proficiencies = tuple()
|
self._saving_throw_proficiencies = tuple()
|
||||||
self.other_weapon_proficiencies = tuple()
|
self.other_weapon_proficiencies = tuple()
|
||||||
@@ -850,8 +850,16 @@ class Character(Entity):
|
|||||||
warning_message=msg,
|
warning_message=msg,
|
||||||
)
|
)
|
||||||
# Save it to the array
|
# Save it to the array
|
||||||
self.weapons.append(ThisWeapon(wielder=self))
|
self._weapons.append(ThisWeapon(wielder=self))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def weapons(self):
|
||||||
|
my_weapons = self._weapons.copy()
|
||||||
|
# Account for unarmed strike
|
||||||
|
if len(my_weapons) == 0 or hasattr(self, "Monk"):
|
||||||
|
my_weapons.append(weapons.Unarmed(wielder=self))
|
||||||
|
return my_weapons
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hit_dice(self):
|
def hit_dice(self):
|
||||||
"""What type and how many dice to use for re-gaining hit points.
|
"""What type and how many dice to use for re-gaining hit points.
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class Champion(SubClass):
|
|||||||
name = "Champion"
|
name = "Champion"
|
||||||
features_by_level = defaultdict(list)
|
features_by_level = defaultdict(list)
|
||||||
features_by_level[3] = [features.ImprovedCritical]
|
features_by_level[3] = [features.ImprovedCritical]
|
||||||
features_by_level[7] = [features.RemarkableAthelete]
|
features_by_level[7] = [features.RemarkableAthlete]
|
||||||
features_by_level[10] = [features.AdditionalFightingStyle]
|
features_by_level[10] = [features.AdditionalFightingStyle]
|
||||||
features_by_level[15] = [features.SuperiorCritical]
|
features_by_level[15] = [features.SuperiorCritical]
|
||||||
features_by_level[18] = [features.Survivor]
|
features_by_level[18] = [features.Survivor]
|
||||||
|
|||||||
+24
-1
@@ -70,7 +70,7 @@ class Entity(ABC):
|
|||||||
gp = 0
|
gp = 0
|
||||||
pp = 0
|
pp = 0
|
||||||
equipment = ""
|
equipment = ""
|
||||||
weapons = list()
|
_weapons = list()
|
||||||
magic_items = list()
|
magic_items = list()
|
||||||
armor = None
|
armor = None
|
||||||
shield = None
|
shield = None
|
||||||
@@ -87,3 +87,26 @@ class Entity(ABC):
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def weapons(self):
|
||||||
|
return self._weapons.copy()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def passive_wisdom(self):
|
||||||
|
return self.perception.modifier + 10
|
||||||
|
|
||||||
|
@property
|
||||||
|
def abilities(self):
|
||||||
|
return [self.strength, self.dexterity, self.constitution,
|
||||||
|
self.intelligence, self.wisdom, self.charisma]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def skills(self):
|
||||||
|
return [self.acrobatics, self.animal_handling, self.arcana,
|
||||||
|
self.athletics, self.deception, self.history,
|
||||||
|
self.insight, self.intimidation, self.investigation,
|
||||||
|
self.medicine, self.nature, self.perception,
|
||||||
|
self.performance, self.persuasion, self.religion,
|
||||||
|
self.sleight_of_hand, self.stealth, self.survival,]
|
||||||
|
|
||||||
|
|||||||
@@ -267,6 +267,7 @@ def to_heading_id(inpt: str) -> str:
|
|||||||
return inpt.replace(" ", "-")
|
return inpt.replace(" ", "-")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Prepare the jinja environment
|
# Prepare the jinja environment
|
||||||
jinja_env = jinja_environment()
|
jinja_env = jinja_environment()
|
||||||
jinja_env.filters["rst_to_html"] = rst_to_html
|
jinja_env.filters["rst_to_html"] = rst_to_html
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ class ImprovedCritical(Feature):
|
|||||||
source = "Fighter (Champion)"
|
source = "Fighter (Champion)"
|
||||||
|
|
||||||
|
|
||||||
class RemarkableAthelete(Feature):
|
class RemarkableAthlete(Feature):
|
||||||
"""Starting at 7th level, you can add half your proficiency bonus (round up)
|
"""Starting at 7th level, you can add half your proficiency bonus (round up)
|
||||||
to any Strength, Dexterity, or Constitution check you make that doesn't
|
to any Strength, Dexterity, or Constitution check you make that doesn't
|
||||||
already use your proficiency bonus.
|
already use your proficiency bonus.
|
||||||
@@ -244,7 +244,7 @@ class RemarkableAthelete(Feature):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "Remarkable Athelete"
|
name = "Remarkable Athlete"
|
||||||
source = "Fighter (Champion)"
|
source = "Fighter (Champion)"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ def create_character_pdf_template(character, basename, flatten=False):
|
|||||||
"AC": str(character.armor_class),
|
"AC": str(character.armor_class),
|
||||||
"Initiative": str(character.initiative),
|
"Initiative": str(character.initiative),
|
||||||
"Speed": str(character.speed),
|
"Speed": str(character.speed),
|
||||||
"Passive": 10 + character.perception,
|
"Passive": character.passive_wisdom,
|
||||||
# Saving throws (proficiencies handled later)
|
# Saving throws (proficiencies handled later)
|
||||||
"ST Strength": mod_str(character.strength.saving_throw),
|
"ST Strength": mod_str(character.strength.saving_throw),
|
||||||
"ST Dexterity": mod_str(character.dexterity.saving_throw),
|
"ST Dexterity": mod_str(character.dexterity.saving_throw),
|
||||||
@@ -66,24 +66,24 @@ def create_character_pdf_template(character, basename, flatten=False):
|
|||||||
"ST Wisdom": mod_str(character.wisdom.saving_throw),
|
"ST Wisdom": mod_str(character.wisdom.saving_throw),
|
||||||
"ST Charisma": mod_str(character.charisma.saving_throw),
|
"ST Charisma": mod_str(character.charisma.saving_throw),
|
||||||
# Skills (proficiencies handled below)
|
# Skills (proficiencies handled below)
|
||||||
"Acrobatics": mod_str(character.acrobatics),
|
"Acrobatics": mod_str(character.acrobatics.modifier),
|
||||||
"Animal": mod_str(character.animal_handling),
|
"Animal": mod_str(character.animal_handling.modifier),
|
||||||
"Arcana": mod_str(character.arcana),
|
"Arcana": mod_str(character.arcana.modifier),
|
||||||
"Athletics": mod_str(character.athletics),
|
"Athletics": mod_str(character.athletics.modifier),
|
||||||
"Deception ": mod_str(character.deception),
|
"Deception ": mod_str(character.deception.modifier),
|
||||||
"History ": mod_str(character.history),
|
"History ": mod_str(character.history.modifier),
|
||||||
"Insight": mod_str(character.insight),
|
"Insight": mod_str(character.insight.modifier),
|
||||||
"Intimidation": mod_str(character.intimidation),
|
"Intimidation": mod_str(character.intimidation.modifier),
|
||||||
"Investigation ": mod_str(character.investigation),
|
"Investigation ": mod_str(character.investigation.modifier),
|
||||||
"Medicine": mod_str(character.medicine),
|
"Medicine": mod_str(character.medicine.modifier),
|
||||||
"Nature": mod_str(character.nature),
|
"Nature": mod_str(character.nature.modifier),
|
||||||
"Perception ": mod_str(character.perception),
|
"Perception ": mod_str(character.perception.modifier),
|
||||||
"Performance": mod_str(character.performance),
|
"Performance": mod_str(character.performance.modifier),
|
||||||
"Persuasion": mod_str(character.persuasion),
|
"Persuasion": mod_str(character.persuasion.modifier),
|
||||||
"Religion": mod_str(character.religion),
|
"Religion": mod_str(character.religion.modifier),
|
||||||
"SleightofHand": mod_str(character.sleight_of_hand),
|
"SleightofHand": mod_str(character.sleight_of_hand.modifier),
|
||||||
"Stealth ": mod_str(character.stealth),
|
"Stealth ": mod_str(character.stealth.modifier),
|
||||||
"Survival": mod_str(character.survival),
|
"Survival": mod_str(character.survival.modifier),
|
||||||
# Hit points
|
# Hit points
|
||||||
"HDTotal": character.hit_dice,
|
"HDTotal": character.hit_dice,
|
||||||
"HPMax": str(character.hp_max),
|
"HPMax": str(character.hp_max),
|
||||||
@@ -150,8 +150,6 @@ def create_character_pdf_template(character, basename, flatten=False):
|
|||||||
("Wpn Name 2", "Wpn2 AtkBonus ", "Wpn2 Damage "),
|
("Wpn Name 2", "Wpn2 AtkBonus ", "Wpn2 Damage "),
|
||||||
("Wpn Name 3", "Wpn3 AtkBonus ", "Wpn3 Damage "),
|
("Wpn Name 3", "Wpn3 AtkBonus ", "Wpn3 Damage "),
|
||||||
]
|
]
|
||||||
if len(character.weapons) == 0 or hasattr(character, "Monk"):
|
|
||||||
character.wield_weapon("unarmed")
|
|
||||||
for _fields, weapon in zip(weapon_fields, character.weapons):
|
for _fields, weapon in zip(weapon_fields, character.weapons):
|
||||||
name_field, atk_field, dmg_field = _fields
|
name_field, atk_field, dmg_field = _fields
|
||||||
fields[name_field] = weapon.name
|
fields[name_field] = weapon.name
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import re
|
|||||||
from jinja2 import Environment, PackageLoader
|
from jinja2 import Environment, PackageLoader
|
||||||
|
|
||||||
|
|
||||||
from dungeonsheets.stats import mod_str
|
from dungeonsheets.stats import mod_str, ability_mod_str
|
||||||
|
|
||||||
|
|
||||||
# A dice string, with optional backticks: ``1d6 + 3``
|
# A dice string, with optional backticks: ``1d6 + 3``
|
||||||
@@ -31,4 +31,5 @@ def jinja_environment():
|
|||||||
variable_end_string="]]",
|
variable_end_string="]]",
|
||||||
)
|
)
|
||||||
jinja_env.filters["mod_str"] = mod_str
|
jinja_env.filters["mod_str"] = mod_str
|
||||||
|
jinja_env.filters["ability_mod_str"] = ability_mod_str
|
||||||
return jinja_env
|
return jinja_env
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<h1 class="background">Background</h1>
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<h1 class="character-sheet">Character Sheet</h1>
|
||||||
|
|
||||||
|
<!-- Identity -->
|
||||||
|
<dl class="character-details">
|
||||||
|
<dt>Character Name</dt>
|
||||||
|
<dd>[[ character.name ]]</dd>
|
||||||
|
<dt>Class & Level</dt>
|
||||||
|
<dd>[[ character.classes_and_levels ]]</dd>
|
||||||
|
<dt>Background</dt>
|
||||||
|
<dd>[[ character.background ]]</dd>
|
||||||
|
<dt>Player Name</dt>
|
||||||
|
<dd>[[ character.player_name ]]</dd>
|
||||||
|
<dt>Race</dt>
|
||||||
|
<dd>[[ character.race ]]</dd>
|
||||||
|
<dt>Alignment</dt>
|
||||||
|
<dd>[[ character.alignment ]]</dd>
|
||||||
|
<dt>Experience Points</dt>
|
||||||
|
<dd>[[ character.xp ]]</dd>
|
||||||
|
<dt>Inspiration</dt>
|
||||||
|
<dd>[% if character.inspiration %]✓[% else %]–[% endif %]</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<dl class="combat-stats">
|
||||||
|
<dt>Armor Class</dt>
|
||||||
|
<dd>[[ character.armor_class ]]</dd>
|
||||||
|
<dt>Initiative</dt>
|
||||||
|
<dd>[[ character.initiative ]]</dd>
|
||||||
|
<dt>Speed</dt>
|
||||||
|
<dd>[[ character.speed ]]</dd>
|
||||||
|
<dt>Passive Wisdom (Perception)</dt>
|
||||||
|
<dd>[[ character.passive_wisdom ]]</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<dl class="hit-points">
|
||||||
|
<dt>Hit Point Maximum</dt>
|
||||||
|
<dd>[[ character.hp_max ]]</dd>
|
||||||
|
<dt>Current Hit Points</dt>
|
||||||
|
<dd>[[ character.hp_current ]]</dd>
|
||||||
|
<dt>Temporary Hit Points</dt>
|
||||||
|
<dd>[% if character.hp_temp > 0 %][[ character.hp_temp ]][% endif %]</dd>
|
||||||
|
<dt>Hit Dice Total</dt>
|
||||||
|
<dd>[[ character.hit_dice ]]</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<!-- Character abilities, saving throws and skill modifiers -->
|
||||||
|
<table class="character-abilities">
|
||||||
|
<tr>
|
||||||
|
<th>Ability</th>
|
||||||
|
<th>Mod</th>
|
||||||
|
<th colspan="2">Saving<br />Throw</th>
|
||||||
|
</tr>
|
||||||
|
[% for ability in character.abilities %]
|
||||||
|
<tr>
|
||||||
|
<td>[[ ability.name | capitalize ]]</td>
|
||||||
|
<td>[[ ability.modifier | mod_str ]] ([[ ability.value ]])</td>
|
||||||
|
<td>[% if ability.name in character.saving_throw_proficiencies %]✓[% endif %]</td>
|
||||||
|
<td>[[ character.strength.saving_throw | mod_str ]]</td>
|
||||||
|
</tr>
|
||||||
|
[% endfor %]
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table class"character-skills">
|
||||||
|
<tr>
|
||||||
|
<th>Skill</th>
|
||||||
|
<th>Mod</th>
|
||||||
|
</tr>
|
||||||
|
[% for skill in character.skills %]
|
||||||
|
<tr>
|
||||||
|
<td>[[ skill ]]</td>
|
||||||
|
<td>[[ skill.modifier | mod_str ]]</td>
|
||||||
|
<td>
|
||||||
|
[% if skill.is_expertise == 1 %]✓✓
|
||||||
|
[% elif skill.is_proficient %]✓
|
||||||
|
[% elif skill.is_remarkable_athlete %]◓
|
||||||
|
[% elif skill.is_jack_of_all_trades %]◒
|
||||||
|
[% endif %]
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
[% endfor %]
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table class="attacks-and-spellcasting">
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Atk Bonus</th>
|
||||||
|
<th>Damage/Type</th>
|
||||||
|
</tr>
|
||||||
|
[% for weapon in character.weapons %]
|
||||||
|
<tr>
|
||||||
|
<td>[[ weapon.name ]]</td>
|
||||||
|
<td>[[ weapon.attack_modifier | mod_str ]]</td>
|
||||||
|
<td>[[ weapon.damage ]] / [[ weapon.damage_type ]]</td>
|
||||||
|
</tr>
|
||||||
|
[% endfor %]
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<dl class="proficiences">
|
||||||
|
<dt>Proficiences</dt>
|
||||||
|
<dd>[[ character.proficiencies_text ]]</dd>
|
||||||
|
<dt>Languages</dt>
|
||||||
|
<dd>[[ character.languages ]]</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<h2 id="inventory">Inventory</h2>
|
||||||
|
<ul class="inventory">
|
||||||
|
<li>[[ character.cp ]] CP</li>
|
||||||
|
<li>[[ character.sp ]] SP</li>
|
||||||
|
<li>[[ character.ep ]] EP</li>
|
||||||
|
<li>[[ character.gp ]] GP</li>
|
||||||
|
<li>[[ character.pp ]] PP</li>
|
||||||
|
[% set inventory_items = character.magic_items_text.split(',') %]
|
||||||
|
[% for item in inventory_items %]
|
||||||
|
<li>[[ item ]]</li>
|
||||||
|
[% endfor %]
|
||||||
|
[% set inventory_items = character.equipment.split(',') %]
|
||||||
|
[% for item in inventory_items %]
|
||||||
|
<li>[[ item ]]</li>
|
||||||
|
[% endfor %]
|
||||||
|
</ul>
|
||||||
@@ -3,6 +3,29 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* End fancy decorations */
|
/* End fancy decorations */
|
||||||
|
|
||||||
|
/* Dictionary lists for showing stats, etc */
|
||||||
|
dt {
|
||||||
|
float: left;
|
||||||
|
clear: left;
|
||||||
|
text-align: right;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
dt::after {
|
||||||
|
content: ":";
|
||||||
|
}
|
||||||
|
dd {
|
||||||
|
padding: 0 0 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl.character-details dt {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
dl.character-details dd {
|
||||||
|
width: 200px;
|
||||||
|
margin: 0 0 0 210px;
|
||||||
|
}
|
||||||
|
|
||||||
.known-beast-disabled {
|
.known-beast-disabled {
|
||||||
color: lightgrey;
|
color: lightgrey;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>[[ member.name[:28] ]]</td>
|
<td>[[ member.name[:28] ]]</td>
|
||||||
<td>[[ member.armor_class ]]</td>
|
<td>[[ member.armor_class ]]</td>
|
||||||
<td>[[ member.perception + 10 ]]</td>
|
<td>[[ member.perception.modifier + 10 ]]</td>
|
||||||
<td>[% for class in member.class_list %]
|
<td>[% for class in member.class_list %]
|
||||||
[% if class.spellcasting_ability %] [[ member.spell_save_dc(class) ]], [% endif %]
|
[% if class.spellcasting_ability %] [[ member.spell_save_dc(class) ]], [% endif %]
|
||||||
[% endfor %]
|
[% endfor %]
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
[% for member in party %]
|
[% for member in party %]
|
||||||
[[ member.name[:28] ]]
|
[[ member.name[:28] ]]
|
||||||
& [[ member.armor_class ]]
|
& [[ member.armor_class ]]
|
||||||
& [[ member.perception + 10 ]]
|
& [[ member.perception.modifier + 10 ]]
|
||||||
& [% for class in member.class_list %]
|
& [% for class in member.class_list %]
|
||||||
[% if class.spellcasting_ability %] [[ member.spell_save_dc(class) ]], [% endif %]
|
[% if class.spellcasting_ability %] [[ member.spell_save_dc(class) ]], [% endif %]
|
||||||
[% endfor %]
|
[% endfor %]
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
[% for member in party %]
|
[% for member in party %]
|
||||||
[[ member.name[:28] ]]
|
[[ member.name[:28] ]]
|
||||||
& [[ member.armor_class ]]
|
& [[ member.armor_class ]]
|
||||||
& [[ member.perception + 10 ]]
|
& [[ member.perception.modifier + 10 ]]
|
||||||
& [% for class in member.class_list %]
|
& [% for class in member.class_list %]
|
||||||
[% if class.spellcasting_ability %] [[ member.spell_save_dc(class) ]], [% endif %]
|
[% if class.spellcasting_ability %] [[ member.spell_save_dc(class) ]], [% endif %]
|
||||||
[% endfor %]
|
[% endfor %]
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<h1 class="spell-list">Spell List</h1>
|
||||||
@@ -9,7 +9,7 @@ import re
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from multiprocessing import Pool, cpu_count
|
from multiprocessing import Pool, cpu_count
|
||||||
from itertools import product
|
from itertools import product
|
||||||
from typing import Union, Sequence, Optional
|
from typing import Union, Sequence, Optional, Literal, List
|
||||||
|
|
||||||
from dungeonsheets import (
|
from dungeonsheets import (
|
||||||
character as _char,
|
character as _char,
|
||||||
@@ -70,6 +70,9 @@ class CharacterRenderer():
|
|||||||
return template.render(character=character,
|
return template.render(character=character,
|
||||||
use_dnd_decorations=use_dnd_decorations, ordinals=ORDINALS)
|
use_dnd_decorations=use_dnd_decorations, ordinals=ORDINALS)
|
||||||
|
|
||||||
|
create_character_sheet_content = CharacterRenderer("character_sheet_template.{suffix}")
|
||||||
|
create_spell_list_content = CharacterRenderer("spell_list_template.{suffix}")
|
||||||
|
create_background_content = CharacterRenderer("background_template.{suffix}")
|
||||||
|
|
||||||
create_subclasses_content = CharacterRenderer("subclasses_template.{suffix}")
|
create_subclasses_content = CharacterRenderer("subclasses_template.{suffix}")
|
||||||
create_features_content = CharacterRenderer("features_template.{suffix}")
|
create_features_content = CharacterRenderer("features_template.{suffix}")
|
||||||
@@ -192,7 +195,7 @@ def make_gm_sheet(
|
|||||||
gm_file = Path(gm_file)
|
gm_file = Path(gm_file)
|
||||||
basename = gm_file.stem
|
basename = gm_file.stem
|
||||||
gm_props = readers.read_sheet_file(gm_file)
|
gm_props = readers.read_sheet_file(gm_file)
|
||||||
session_title = gm_props.get("session_title", f"GM Notes: {basename}")
|
session_title = gm_props.pop("session_title", f"GM Notes: {basename}")
|
||||||
# Create the intro tex
|
# Create the intro tex
|
||||||
content_suffix = format_suffixes[output_format]
|
content_suffix = format_suffixes[output_format]
|
||||||
content = [
|
content = [
|
||||||
@@ -295,6 +298,96 @@ def make_gm_sheet(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def make_character_content(
|
||||||
|
character: Character,
|
||||||
|
content_format: Literal["tex", "html"],
|
||||||
|
fancy_decorations: bool = False,) -> List[str]:
|
||||||
|
"""Prepare the inner content for a character sheet.
|
||||||
|
|
||||||
|
This will produce a fully renderable document, suitable for
|
||||||
|
passing to routines in either the ``epub`` or ``latex``
|
||||||
|
modules. If *content_format* is ``"html"``, the returned content
|
||||||
|
is just the portion that would be found inside the
|
||||||
|
``<body></body>`` tag.
|
||||||
|
|
||||||
|
If *content_format* is ``"tex"``, the content returned will not
|
||||||
|
include the character, spell list, or biography sheets, since
|
||||||
|
these are currently processed through fillable PDFs.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
character
|
||||||
|
The character to render content for.
|
||||||
|
content_format
|
||||||
|
Which markup syntax to use.
|
||||||
|
fancy_decorations
|
||||||
|
Use fancy page layout and decorations for extra sheets, namely
|
||||||
|
the dnd style file for *tex*, or extended CSS for *html*.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
content
|
||||||
|
The list of rendered character sheet contents for *character* in
|
||||||
|
markup format *content_format*.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Preamble, empty for HTML
|
||||||
|
content = [
|
||||||
|
jinja_env.get_template(f"preamble.{content_format}").render(
|
||||||
|
use_dnd_decorations=fancy_decorations,
|
||||||
|
title="Features, Magical Items and Spells",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
# Make the character sheet, and background pages if producing HTML
|
||||||
|
if content_format != "tex":
|
||||||
|
content.append(create_character_sheet_content(character,
|
||||||
|
content_suffix=content_format,
|
||||||
|
use_dnd_decorations=fancy_decorations))
|
||||||
|
content.append(create_spell_list_content(character,
|
||||||
|
content_suffix=content_format,
|
||||||
|
use_dnd_decorations=fancy_decorations)
|
||||||
|
)
|
||||||
|
content.append(create_background_content(character,
|
||||||
|
content_suffix=content_format,
|
||||||
|
use_dnd_decorations=fancy_decorations)
|
||||||
|
)
|
||||||
|
# Create a list of subcasses, features, spells, etc
|
||||||
|
if character.subclasses:
|
||||||
|
content.append(create_subclasses_content(character,
|
||||||
|
content_suffix=content_format,
|
||||||
|
use_dnd_decorations=fancy_decorations)
|
||||||
|
)
|
||||||
|
if character.features:
|
||||||
|
content.append(
|
||||||
|
create_features_content(character, content_suffix=content_format, use_dnd_decorations=fancy_decorations)
|
||||||
|
)
|
||||||
|
if character.magic_items:
|
||||||
|
content.append(
|
||||||
|
create_magic_items_content(character, content_suffix=content_format, use_dnd_decorations=fancy_decorations)
|
||||||
|
)
|
||||||
|
if character.is_spellcaster:
|
||||||
|
content.append(
|
||||||
|
create_spellbook_content(character, content_suffix=content_format, use_dnd_decorations=fancy_decorations)
|
||||||
|
)
|
||||||
|
if len(getattr(character, "infusions", [])) > 0:
|
||||||
|
content.append(
|
||||||
|
create_infusions_content(character, content_suffix=content_format, use_dnd_decorations=fancy_decorations)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a list of Druid wild_shapes
|
||||||
|
if len(getattr(character, "all_wild_shapes", [])) > 0:
|
||||||
|
content.append(
|
||||||
|
create_druid_shapes_content(character, content_suffix=content_format, use_dnd_decorations=fancy_decorations)
|
||||||
|
)
|
||||||
|
# Postamble, empty for HTML
|
||||||
|
content.append(
|
||||||
|
jinja_env.get_template(f"postamble.{content_format}").render(
|
||||||
|
use_dnd_decorations=fancy_decorations
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
def make_character_sheet(
|
def make_character_sheet(
|
||||||
char_file: Union[str, Path],
|
char_file: Union[str, Path],
|
||||||
character: Optional[Character] = None,
|
character: Optional[Character] = None,
|
||||||
@@ -336,12 +429,6 @@ def make_character_sheet(
|
|||||||
pages = []
|
pages = []
|
||||||
# Prepare the tex/html content
|
# Prepare the tex/html content
|
||||||
content_suffix = format_suffixes[output_format]
|
content_suffix = format_suffixes[output_format]
|
||||||
content = [
|
|
||||||
jinja_env.get_template(f"preamble.{content_suffix}").render(
|
|
||||||
use_dnd_decorations=fancy_decorations,
|
|
||||||
title="Features, Magical Items and Spells",
|
|
||||||
)
|
|
||||||
]
|
|
||||||
# Start of PDF gen
|
# Start of PDF gen
|
||||||
char_pdf = create_character_pdf_template(
|
char_pdf = create_character_pdf_template(
|
||||||
character=character, basename=char_base, flatten=flatten
|
character=character, basename=char_base, flatten=flatten
|
||||||
@@ -360,43 +447,10 @@ def make_character_sheet(
|
|||||||
sheets.append(spell_base + ".pdf")
|
sheets.append(spell_base + ".pdf")
|
||||||
# end of PDF gen
|
# end of PDF gen
|
||||||
features_base = "{:s}_features".format(basename)
|
features_base = "{:s}_features".format(basename)
|
||||||
# Create a list of subcasses
|
|
||||||
if character.subclasses:
|
|
||||||
content.append( create_subclasses_content(character,
|
|
||||||
content_suffix=content_suffix,
|
|
||||||
use_dnd_decorations=fancy_decorations) )
|
|
||||||
# Create a list of features and magic items
|
# Create a list of features and magic items
|
||||||
if character.features:
|
content = make_character_content(character=character,
|
||||||
content.append(
|
content_format=content_suffix,
|
||||||
create_features_content(character, content_suffix=content_suffix, use_dnd_decorations=fancy_decorations)
|
fancy_decorations=fancy_decorations)
|
||||||
)
|
|
||||||
if character.magic_items:
|
|
||||||
content.append(
|
|
||||||
create_magic_items_content(character, content_suffix=content_suffix, use_dnd_decorations=fancy_decorations)
|
|
||||||
)
|
|
||||||
# Create a list of spells
|
|
||||||
if character.is_spellcaster:
|
|
||||||
content.append(
|
|
||||||
create_spellbook_content(character, content_suffix=content_suffix, use_dnd_decorations=fancy_decorations)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a list of Artificer infusions
|
|
||||||
if getattr(character, "infusions", []):
|
|
||||||
content.append(
|
|
||||||
create_infusions_content(character, content_suffix=content_suffix, use_dnd_decorations=fancy_decorations)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a list of Druid wild_shapes
|
|
||||||
if getattr(character, "wild_shapes", []):
|
|
||||||
content.append(
|
|
||||||
create_druid_shapes_content(character, content_suffix=content_suffix, use_dnd_decorations=fancy_decorations)
|
|
||||||
)
|
|
||||||
|
|
||||||
content.append(
|
|
||||||
jinja_env.get_template(f"postamble.{content_suffix}").render(
|
|
||||||
use_dnd_decorations=fancy_decorations
|
|
||||||
)
|
|
||||||
)
|
|
||||||
# Typeset combined LaTeX file
|
# Typeset combined LaTeX file
|
||||||
if output_format == "pdf":
|
if output_format == "pdf":
|
||||||
try:
|
try:
|
||||||
|
|||||||
+90
-26
@@ -17,7 +17,7 @@ from dungeonsheets.features import (
|
|||||||
NaturalExplorerRevised,
|
NaturalExplorerRevised,
|
||||||
QuickDraw,
|
QuickDraw,
|
||||||
RakishAudacity,
|
RakishAudacity,
|
||||||
RemarkableAthelete,
|
RemarkableAthlete,
|
||||||
SeaSoul,
|
SeaSoul,
|
||||||
SoulOfTheForge,
|
SoulOfTheForge,
|
||||||
SuperiorMobility,
|
SuperiorMobility,
|
||||||
@@ -35,7 +35,11 @@ def mod_str(modifier):
|
|||||||
return "{:+d}".format(modifier)
|
return "{:+d}".format(modifier)
|
||||||
|
|
||||||
|
|
||||||
AbilityScore = namedtuple("AbilityScore", ("value", "modifier", "saving_throw"))
|
def ability_mod_str(character, ability):
|
||||||
|
return mod_str(getattr(character, ability).modifier)
|
||||||
|
|
||||||
|
|
||||||
|
AbilityScore = namedtuple("AbilityScore", ("value", "modifier", "saving_throw", "name"))
|
||||||
|
|
||||||
|
|
||||||
class Ability:
|
class Ability:
|
||||||
@@ -68,7 +72,7 @@ class Ability:
|
|||||||
if is_proficient:
|
if is_proficient:
|
||||||
saving_throw += entity.proficiency_bonus
|
saving_throw += entity.proficiency_bonus
|
||||||
# Create the named tuple
|
# Create the named tuple
|
||||||
value = AbilityScore(modifier=modifier, value=score, saving_throw=saving_throw)
|
value = AbilityScore(modifier=modifier, value=score, saving_throw=saving_throw, name=self.ability_name)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def __set__(self, entity, val):
|
def __set__(self, entity, val):
|
||||||
@@ -78,38 +82,98 @@ class Ability:
|
|||||||
|
|
||||||
|
|
||||||
class Skill:
|
class Skill:
|
||||||
"""An ability-based skill, such as athletics."""
|
"""An ability-based skill, such as athletics.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
ability_name:
|
||||||
|
The name of the ability, as a python-compatible string.
|
||||||
|
skill_name:
|
||||||
|
The name of the base ability that determines the skill
|
||||||
|
modifier.
|
||||||
|
is_proficient
|
||||||
|
Bool that describes if the owner is proficient in this skill.
|
||||||
|
is_expertise
|
||||||
|
Bool that describes if the owner has expertise in this skill.
|
||||||
|
is_jack_of_all_trades
|
||||||
|
Bool that describes if this skill benefits from Jack of All
|
||||||
|
Trades feature (False if already proficient).
|
||||||
|
is_remarkable_athlete
|
||||||
|
Bool that describes if this skill benefits from Remarkable
|
||||||
|
Athlete feature (False if already proficient).
|
||||||
|
modifier
|
||||||
|
The base ability modifier, after relevant proficiency bonuses
|
||||||
|
have been applied.
|
||||||
|
proficiency_modifier
|
||||||
|
The bonus that is applied to the base ability. Usually the same
|
||||||
|
as the owner's proficiency bonus if ``is_proficient`` is True,
|
||||||
|
but can be different based on class features.,
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
ability_name = ""
|
||||||
|
skill_name = ""
|
||||||
|
entity = None
|
||||||
|
|
||||||
def __init__(self, ability):
|
def __init__(self, ability):
|
||||||
self.ability_name = ability
|
self.ability_name = ability
|
||||||
|
|
||||||
def __set_name__(self, entity, name):
|
def __set_name__(self, owner, name):
|
||||||
self.skill_name = name.lower().replace("_", " ")
|
self.skill_name = name.lower().replace("_", " ")
|
||||||
self.character = entity
|
|
||||||
|
|
||||||
def __get__(self, entity, owner):
|
def __get__(self, entity, owner):
|
||||||
log.debug("Getting skill '%s' for '%s'", self.skill_name, entity.name)
|
self.entity = entity
|
||||||
ability = getattr(entity, self.ability_name)
|
return self
|
||||||
modifier = ability.modifier
|
|
||||||
# Check for proficiency
|
|
||||||
proficiencies = [p.replace("_", " ") for p in entity.skill_proficiencies]
|
|
||||||
is_proficient = self.skill_name in proficiencies
|
|
||||||
log.debug(
|
|
||||||
"%s is proficient in %s: %s", entity.name, self.skill_name, is_proficient
|
|
||||||
)
|
|
||||||
if is_proficient:
|
|
||||||
modifier += entity.proficiency_bonus
|
|
||||||
elif entity.has_feature(JackOfAllTrades):
|
|
||||||
modifier += entity.proficiency_bonus // 2
|
|
||||||
elif entity.has_feature(RemarkableAthelete):
|
|
||||||
if self.ability_name.lower() in ("strength", "dexterity", "constitution"):
|
|
||||||
modifier += ceil(entity.proficiency_bonus / 2.0)
|
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.skill_name.title()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_remarkable_athlete(self):
|
||||||
|
already_proficient = (self.is_proficient or self.is_expertise)
|
||||||
|
if self.entity.has_feature(RemarkableAthlete) and not already_proficient:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_jack_of_all_trades(self):
|
||||||
|
already_proficient = (self.is_proficient or self.is_expertise or self.is_remarkable_athlete)
|
||||||
|
if self.entity.has_feature(JackOfAllTrades) and not already_proficient:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_proficient(self):
|
||||||
|
# Check for proficiency
|
||||||
|
proficiencies = [p.replace("_", " ") for p in self.entity.skill_proficiencies]
|
||||||
|
is_proficient = self.skill_name in proficiencies
|
||||||
|
return is_proficient
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_expertise(self):
|
||||||
|
return self.skill_name in self.entity.skill_expertise
|
||||||
|
|
||||||
|
@property
|
||||||
|
def proficiency_modifier(self):
|
||||||
|
modifier = 0
|
||||||
|
if self.is_proficient:
|
||||||
|
modifier += self.entity.proficiency_bonus
|
||||||
|
if self.is_remarkable_athlete:
|
||||||
|
modifier += ceil(self.entity.proficiency_bonus / 2.0)
|
||||||
|
if self.is_jack_of_all_trades:
|
||||||
|
modifier += self.entity.proficiency_bonus // 2
|
||||||
# Check for expertise
|
# Check for expertise
|
||||||
is_expert = self.skill_name in entity.skill_expertise
|
if self.is_expertise:
|
||||||
if is_expert:
|
modifier += self.entity.proficiency_bonus
|
||||||
modifier += entity.proficiency_bonus
|
return modifier
|
||||||
log.info("'%s' modifier for '%s': %d", self.skill_name, entity.name, modifier)
|
|
||||||
|
@property
|
||||||
|
def modifier(self):
|
||||||
|
ability = getattr(self.entity, self.ability_name)
|
||||||
|
modifier = ability.modifier + self.proficiency_modifier
|
||||||
|
log.info("%s modifier for '%s': %d", self, self.entity.name, modifier)
|
||||||
return modifier
|
return modifier
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -56,11 +56,40 @@ class EpubOutputTestCase(unittest.TestCase):
|
|||||||
gm_epub = Path(f"{GMFILE.stem}.epub").resolve()
|
gm_epub = Path(f"{GMFILE.stem}.epub").resolve()
|
||||||
char_epub = Path(f"{CHARFILE.stem}.epub").resolve()
|
char_epub = Path(f"{CHARFILE.stem}.epub").resolve()
|
||||||
|
|
||||||
|
def new_character(self):
|
||||||
|
char = character.Character(
|
||||||
|
name="Dr. Who",
|
||||||
|
classes=["Monk", "Druid", "Artificer"],
|
||||||
|
levels=[1, 1, 1],
|
||||||
|
subclasses=["way of the open hand", None, None],
|
||||||
|
magic_items=["cloak of protection"],
|
||||||
|
spells=["invisibility"],
|
||||||
|
wild_shapes=["crocodile"],
|
||||||
|
infusions=["boots of the winding path"]
|
||||||
|
)
|
||||||
|
return char
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
for f in [self.gm_epub, self.char_epub]:
|
for f in [self.gm_epub, self.char_epub]:
|
||||||
if f.exists():
|
if f.exists():
|
||||||
f.unlink()
|
f.unlink()
|
||||||
|
|
||||||
|
def test_character_html_content(self):
|
||||||
|
my_char = self.new_character()
|
||||||
|
html = make_sheets.make_character_content(character=my_char,
|
||||||
|
content_format="html")
|
||||||
|
html = "".join(html)
|
||||||
|
# Make sure the various sections get rendered
|
||||||
|
self.assertIn("Subclasses</h1>", html)
|
||||||
|
self.assertIn("Features</h1>", html)
|
||||||
|
self.assertIn("Magic Items</h1>", html)
|
||||||
|
self.assertIn("Spells</h1>", html)
|
||||||
|
self.assertIn("Infusions</h1>", html)
|
||||||
|
self.assertIn("Known Beasts</h1>", html)
|
||||||
|
# Check the character sheet
|
||||||
|
self.assertIn("Character Sheet</h1>", html)
|
||||||
|
self.assertIn("Dr. Who", html)
|
||||||
|
|
||||||
def test_gm_file_created(self):
|
def test_gm_file_created(self):
|
||||||
# Check that a file is created once the function is run
|
# Check that a file is created once the function is run
|
||||||
make_sheets.make_gm_sheet(gm_file=GMFILE, output_format="epub")
|
make_sheets.make_gm_sheet(gm_file=GMFILE, output_format="epub")
|
||||||
|
|||||||
+7
-3
@@ -77,10 +77,14 @@ class TestStats(TestCase):
|
|||||||
proficiency_bonus = 2
|
proficiency_bonus = 2
|
||||||
|
|
||||||
my_class = MyClass()
|
my_class = MyClass()
|
||||||
self.assertEqual(my_class.acrobatics, 2)
|
self.assertEqual(str(my_class.acrobatics), "Acrobatics")
|
||||||
|
self.assertEqual(my_class.acrobatics.modifier, 2)
|
||||||
|
self.assertEqual(str(my_class.sleight_of_hand), "Sleight Of Hand")
|
||||||
# Check for a proficiency
|
# Check for a proficiency
|
||||||
my_class.skill_proficiencies = ["acrobatics"]
|
my_class.skill_proficiencies = ["acrobatics"]
|
||||||
self.assertEqual(my_class.acrobatics, 4)
|
self.assertTrue(my_class.acrobatics.is_proficient)
|
||||||
|
self.assertEqual(my_class.acrobatics.proficiency_modifier, 2)
|
||||||
|
self.assertEqual(my_class.acrobatics.modifier, 4)
|
||||||
# Check for a proficiency with spaces in the name
|
# Check for a proficiency with spaces in the name
|
||||||
my_class.skill_proficiencies = ["sleight_of_hand"]
|
my_class.skill_proficiencies = ["sleight_of_hand"]
|
||||||
self.assertEqual(my_class.sleight_of_hand, 4)
|
self.assertEqual(my_class.sleight_of_hand.modifier, 4)
|
||||||
|
|||||||
Reference in New Issue
Block a user