示例#1
0
    def test_findattr(self):
        """Check if the function can find attributes."""
        class TestClass():
            my_attr = 47
            YourAttr = 53

        test_class = TestClass()
        # Direct access
        self.assertEqual(stats.findattr(test_class, 'my_attr'),
                         test_class.my_attr)
        self.assertEqual(stats.findattr(test_class, 'YourAttr'),
                         test_class.YourAttr)
        # Swapping spaces for capitalization
        self.assertEqual(stats.findattr(test_class, 'my attr'),
                         test_class.my_attr)
        self.assertEqual(stats.findattr(test_class, 'your attr'),
                         test_class.YourAttr)
示例#2
0
 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 = findattr(background, bg)(owner=self)
         except AttributeError:
             msg = (f'Background "{bg}" not defined. '
                    f'Please add it to ``background.py``')
             self._background = background.Background(owner=self)
             warnings.warn(msg)
示例#3
0
def make_gm_sheet(
    basename: str,
    gm_props: Mapping,
    fancy_decorations: bool = False,
    debug: bool = False,
):
    """Prepare a PDF character sheet from the given character file.

    Parameters
    ----------
    basename
      The basename for saving files.
    gm_props
      Properties for creating the GM notes.
    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.

    """
    tex = [
        jinja_env.get_template("preamble.tex").render(
            use_dnd_decorations=fancy_decorations,
            title=gm_props["session_title"],
        )
    ]
    # Add the monsters
    monsters_ = [findattr(monsters, m)() for m in gm_props.get("monsters", [])]
    if len(monsters_) > 0:
        tex.append(
            create_monsters_tex(monsters_, use_dnd_decorations=fancy_decorations)
        )
    # Add the closing TeX
    tex.append(
        jinja_env.get_template("postamble.tex").render(
            use_dnd_decorations=fancy_decorations
        )
    )
    # Typeset combined LaTeX file
    try:
        if len(tex) > 2:
            latex.create_latex_pdf(
                tex="".join(tex),
                basename=basename,
                keep_temp_files=debug,
                use_dnd_decorations=fancy_decorations,
            )
    except exceptions.LatexNotFoundError:
        log.warning(f"``pdflatex`` not available. Skipping {basename}")
示例#4
0
 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 = findattr(race, newrace)(owner=self)
         except AttributeError:
             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)
示例#5
0
    def wield_shield(self, shield):
        """Accepts a string or Shield class and replaces the current armor.

        If a string is given, then a subclass of
        :py:class:`~dungeonsheets.armor.Shield` is retrived from the
        ``armor.py`` file. Otherwise, an subclass of
        :py:class:`~dungeonsheets.armor.Shield` can be provided
        directly.

        """
        if shield not in ('', 'None', None):
            try:
                NewShield = findattr(armor, shield)
            except AttributeError:
                # Not a string, so just treat it as Armor
                NewShield = shield
            self.shield = NewShield()
示例#6
0
    def wear_armor(self, new_armor):
        """Accepts a string or Armor class and replaces the current armor.

        If a string is given, then a subclass of
        :py:class:`~dungeonsheets.armor.Armor` is retrived from the
        ``armor.py`` file. Otherwise, an subclass of
        :py:class:`~dungeonsheets.armor.Armor` can be provided
        directly.

        """
        if new_armor not in ('', 'None', None):
            if isinstance(new_armor, armor.Armor):
                new_armor = new_armor
            else:
                NewArmor = findattr(armor, new_armor)
                new_armor = NewArmor()
            self.armor = new_armor
示例#7
0
 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 = findattr(monsters, shape)
                 new_shape = NewMonster()
             except AttributeError:
                 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
示例#8
0
    def wield_weapon(self, weapon):
        """Accepts a string and adds it to the list of wielded weapons.

        Parameters
        ----------
        weapon : str
          Case-insensitive string with a name of the weapon.

        """
        # Retrieve the weapon class from the weapons module
        if isinstance(weapon, weapons.Weapon):
            weapon_ = type(weapon)(wielder=self)
        elif isinstance(weapon, str):
            try:
                NewWeapon = findattr(weapons, weapon)
            except AttributeError:
                raise AttributeError(f'Weapon "{weapon}" is not defined')
            weapon_ = NewWeapon(wielder=self)
        elif issubclass(weapon, weapons.Weapon):
            weapon_ = weapon(wielder=self)
        else:
            raise AttributeError(f'Weapon "{weapon}" is not defined')
        # Save it to the array
        self.weapons.append(weapon_)
示例#9
0
 def set_attrs(self, **attrs):
     """Bulk setting of attributes. Useful for loading a character from a
     dictionary."""
     for attr, val in attrs.items():
         if attr == 'dungeonsheets_version':
             pass # Maybe we'll verify this later?
         elif attr == 'weapons':
             if isinstance(val, str):
                 val = [val]
             # Treat weapons specially
             for weap in val:
                 self.wield_weapon(weap)
         elif attr == 'magic_items':
             if isinstance(val, str):
                 val = [val]
             for mitem in val:
                 try:
                     self.magic_items.append(findattr(magic_items, mitem)(owner=self))
                 except (AttributeError):
                     msg = (f'Magic Item "{mitem}" not defined. '
                            f'Please add it to ``magic_items.py``')
                     warnings.warn(msg)
         elif attr == 'weapon_proficiencies':
             self.other_weapon_proficiencies = ()
             wps = set([findattr(weapons, w) for w in val])
             wps -= set(self.weapon_proficiencies)
             self.other_weapon_proficiencies = list(wps)
         elif attr == 'armor':
             self.wear_armor(val)
         elif attr == 'shield':
             self.wield_shield(val)
         elif attr == 'circle':
             if hasattr(self, 'Druid'):
                 self.Druid.circle = val
         elif attr == 'features':
             if isinstance(val, str):
                 val = [val]
             _features = []
             for f in val:
                 try:
                     _features.append(findattr(features, f))
                 except AttributeError:
                     msg = (f'Feature "{f}" not defined. '
                            f'Please add it to ``features.py``')
                     # create temporary feature
                     _features.append(features.create_feature(
                         name=f, source='Unknown',
                         __doc__="""Unknown Feature. Add to features.py"""))
                     warnings.warn(msg)
             self.custom_features += tuple(F(owner=self) for F in _features)
         elif (attr == 'spells') or (attr == 'spells_prepared'):
             # Create a list of actual spell objects
             _spells = []
             for spell_name in val:
                 try:
                     _spells.append(findattr(spells, spell_name))
                 except AttributeError:
                     msg = (f'Spell "{spell_name}" not defined. '
                            f'Please add it to ``spells.py``')
                     warnings.warn(msg)
                     # Create temporary spell
                     _spells.append(spells.create_spell(name=spell_name, level=9))
                     # raise AttributeError(msg)
             # Sort by name
             _spells.sort(key=lambda spell: spell.name)
             # Save list of spells to character atribute
             if attr == 'spells':
                 # Instantiate them all for the spells list
                 self._spells = tuple(S() for S in _spells)
             else:
                 # Instantiate them all for the spells list
                 self._spells_prepared = tuple(S() for S in _spells)
         else:
             if not hasattr(self, attr):
                 warnings.warn(f"Setting unknown character attribute {attr}",
                               RuntimeWarning)
             # Lookup general attributes
             setattr(self, attr, val)
示例#10
0
def _resolve_mechanic(mechanic, module, 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::

        >>> from dungeonsheets import spells
        >>> _resolve_mechanic("mage_hand", spells, None)
        >>> class MySpell(spells.Spell): pass
        >>> _resolve_mechanic(MySpell, None, spells.Spell)
        >>> _resolve_mechanic("hocus pocus", spells, None)

    The acceptable entries for *mechanic*, in priority order, are:
      1. A subclass of *SuperClass*
      2. A string with the name of a defined spell in *module*
      3. The name of an unknown spell (creates generic object using *factory*)

    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.
    module : module
      A python module in which to look for the defined string in *name*.
    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
    else:
        try:
            # Retrieve pre-defined mechanic
            Mechanic = findattr(module, mechanic)
        except AttributeError:
            # 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