def background(self, bg): if isinstance(bg, background.Background): self._background = bg self._background.owner = self elif isinstance(bg, type) and issubclass(bg, background.Background): self._background = bg(owner=self) elif isinstance(bg, str): try: self._background = find_content( bg, valid_classes=[background.Background])(owner=self) except exceptions.ContentNotFound: msg = ( f'Background "{bg}" not defined. Please add it to ``background.py``' ) self._background = background.Background(owner=self) warnings.warn(msg)
def race(self, newrace): if isinstance(newrace, race.Race): self._race = newrace self._race.owner = self elif isinstance(newrace, type) and issubclass(newrace, race.Race): self._race = newrace(owner=self) elif isinstance(newrace, str): try: self._race = find_content(newrace, valid_classes=[race.Race])( owner=self ) except exceptions.ContentNotFound: msg = f'Race "{newrace}" not defined. Please add it to ``race.py``' self._race = race.Race(owner=self) warnings.warn(msg) elif newrace is None: self._race = race.Race(owner=self)
def wild_shapes(self, new_shapes): actual_shapes = [] # Retrieve the actual monster classes if possible for shape in new_shapes: if isinstance(shape, monsters.Monster): # Already a monster shape so just add it as is new_shape = shape else: # Not already a monster so see if we can find one try: NewMonster = find_content(shape, valid_classes=[monsters.Monster]) new_shape = NewMonster() except exceptions.ContentNotFound: msg = ( f"Wild shape '{shape}' not found. Please add it to" " ``monsters.py``" ) raise exceptions.MonsterError(msg) actual_shapes.append(new_shape) # Save the updated list for later self._wild_shapes = actual_shapes
def _resolve_mechanic(mechanic, SuperClass, warning_message=None): """Take a raw entry in a character sheet and turn it into a usable object. Eg: spells can be defined in many ways. This function accepts all of those options and returns an actual *Spell* class that can be used by a character:: >>> _resolve_mechanic("mage_hand", SuperClass=spells.Spell) >>> _resolve_mechanic("mage_hand", SuperClass=None) >>> from dungeonsheets import spells >>> class MySpell(spells.Spell): pass >>> _resolve_mechanic(MySpell, SuperClass=spells.Spell) >>> _resolve_mechanic("hocus pocus", SuperClass=spells.Spell) The acceptable entries for *mechanic*, in priority order, are: 1. A subclass of *SuperClass* 2. A string with the name of defined content 3. The name of an unknown spell (creates generic object using *factory*) *SuperClass* can be ``None`` to match any class, however this will raise an exception if more than one content type has this name. For example, "shield" can refer to both the armor or the spell, so ``_resolve_mechanic("shield")`` will raise an exception. Parameters ========== mechanic : str, type The thing to be resolved, either a string with the name of the mechanic, or a subclass of *ParentClass* describing the mechanic. SuperClass : type Class to determine whether *mechanic* should just be allowed through as is. error_message : str, optional A string whose ``str.format()`` method (receiving one positional argument *mechanic*) will be used for displaying a warning when an unknown mechanic is resolved. If omitted, no warning will be displayed. Returns ======= Mechanic A class representing the resolved game mechanic. This will likely be a subclass of *SuperClass* if the other parameters are well behaved, but this is not enforced. """ is_already_resolved = isinstance(mechanic, type) and issubclass( mechanic, SuperClass ) if is_already_resolved: Mechanic = mechanic elif SuperClass is not None and isinstance(mechanic, SuperClass): # Has been instantiated for some reason Mechanic = type(mechanic) else: try: # Retrieve pre-defined mechanic valid_classes = [SuperClass] if SuperClass is not None else [] Mechanic = find_content(mechanic, valid_classes=valid_classes) except ValueError: # No pre-defined mechanic available if warning_message is not None: # Emit the warning msg = warning_message.format(mechanic) warnings.warn(msg) else: # Create a generic message so we can make a docstring later. msg = f'Mechanic "{mechanic}" not defined. Please add it.' # Create generic mechanic from the factory class_name = "".join([s.title() for s in mechanic.split("_")]) mechanic_name = mechanic.replace("_", " ").title() attrs = {"name": mechanic_name, "__doc__": msg, "source": "Unknown"} Mechanic = type(class_name, (SuperClass,), attrs) return Mechanic
def make_gm_sheet( gm_file: Union[str, Path], output_format: str = "pdf", fancy_decorations: bool = False, debug: bool = False, ): """Prepare a PDF character sheet from the given character file. Parameters ---------- gm_file The file with the gm_sheet definitions. output_format Either "pdf" or "epub" to generate a PDF file or an EPUB file. fancy_decorations Use fancy page layout and decorations for extra sheets, namely the dnd style file: https://github.com/rpgtex/DND-5e-LaTeX-Template. debug Provide extra info and preserve temporary files. """ # Parse the GM file and filename gm_file = Path(gm_file) basename = gm_file.stem gm_props = readers.read_sheet_file(gm_file) session_title = gm_props.pop("session_title", f"GM Notes: {basename}") # Create the intro tex content_suffix = format_suffixes[output_format] content = [ jinja_env.get_template(f"preamble.{content_suffix}").render( use_dnd_decorations=fancy_decorations, title=session_title, ) ] # Add the party stats table and session summary party = [] for char_file in gm_props.pop("party", []): # Check if it's already resolved if isinstance(char_file, Creature): member = char_file elif isinstance(char_file, type) and issubclass(char_file, Creature): # Needs to be instantiated member = char_file() else: # Resolve the file path char_file = Path(char_file) if not char_file.is_absolute(): char_file = gm_file.parent / char_file char_file = char_file.resolve() # Load the character file log.debug(f"Loading party member: {char_file}") character_props = readers.read_sheet_file(char_file) member = _char.Character.load(character_props) party.append(member) summary = gm_props.pop("summary", "") content.append( create_party_summary_content( party, summary_rst=summary, suffix=content_suffix, use_dnd_decorations=fancy_decorations, )) # Parse any extra homebrew sections, etc. content.append( create_extra_gm_content(sections=gm_props.pop("extra_content", []), suffix=content_suffix, use_dnd_decorations=fancy_decorations)) # Add the monsters monsters_ = [] for monster in gm_props.pop("monsters", []): if isinstance(monster, monsters.Monster): # It's already a monster, so just add it new_monster = monster else: try: MyMonster = find_content(monster, valid_classes=[monsters.Monster]) except exceptions.ContentNotFound: msg = f"Monster '{monster}' not found. Please add it to ``monsters.py``" warnings.warn(msg) continue else: new_monster = MyMonster() monsters_.append(new_monster) if len(monsters_) > 0: content.append( create_monsters_content(monsters_, suffix=content_suffix, use_dnd_decorations=fancy_decorations)) # Add the random tables tables = [ find_content(s, valid_classes=[random_tables.RandomTable]) for s in gm_props.pop("random_tables", []) ] content.append( create_random_tables_content( tables=tables, suffix=content_suffix, use_dnd_decorations=fancy_decorations, )) # Add the closing TeX content.append( jinja_env.get_template(f"postamble.{format_suffixes[output_format]}"). render(use_dnd_decorations=fancy_decorations)) # Warn about any unhandled sheet properties gm_props.pop("dungeonsheets_version") gm_props.pop("sheet_type") if len(gm_props.keys()) > 0: msg = f"Unhandled attributes in '{str(gm_file)}': {','.join(gm_props.keys())}" log.warning(msg) warnings.warn(msg) # Produce the combined output depending on the format requested if output_format == "pdf": # Typeset combined LaTeX file try: if len(content) > 2: latex.create_latex_pdf( tex="".join(content), basename=basename, keep_temp_files=debug, use_dnd_decorations=fancy_decorations, ) except exceptions.LatexNotFoundError: log.warning(f"``pdflatex`` not available. Skipping {basename}") elif output_format == "epub": chapters = {session_title: "".join(content)} # Make sheets in the epub for each party member for char in party: char_html = make_character_content( char, "html", fancy_decorations=fancy_decorations) chapters[char.name] = "".join(char_html) # Create the combined HTML file epub.create_epub( chapters=chapters, basename=basename, title=session_title, use_dnd_decorations=fancy_decorations, ) else: raise exceptions.UnknownOutputFormat( f"Unknown output format requested: {output_format}. Valid options are:" " 'pdf', 'epub'")