import math import warnings from collections import defaultdict from dungeonsheets import exceptions, features, monsters, weapons from dungeonsheets.classes.classes import CharClass, SubClass from dungeonsheets.content_registry import find_content # PHB class LandCircle(SubClass): """The Circle of the Land is made up of mystics and sages who safeguard ancient knowledge and rites through a vast oral tradition. These druids meet within sacred circles of trees or standing stones to whisper primal secrets in Druidic. The circle's wisest members preside as the chief priests of communities that hold to the Old Faith and serve as advisors to the rulers of those folk. As a member of this circle, your magic is influenced by the land where you were initiated into the circle's mysterious rites """ name = "Circle of the Land" circle = "land" features_by_level = defaultdict(list) features_by_level[2] = [features.BonusCantrip, features.NaturalRecovery] features_by_level[3] = [features.CircleSpells] features_by_level[6] = [features.LandsStride] features_by_level[10] = [features.NaturesWard] features_by_level[14] = [features.NaturesSanctuary] class MoonCircle(SubClass): """Druids of the Circle of the Moon are fierce guardians of the wilds. Their order gathers under the full moon to share news and trade warnings. They haunt the deepest parts of the wilderness, where they might go for weeks on end before crossing paths with another humanoid creature, let alone another druid. Changeable as the moon, a druid of this circle might prowl as a great cat one night, soar over the treetops as an eagle the next day, and crash through the undergrowth in bear form to drive off a trespassing monster. The wild is in the druid's blood. """ name = "Circle of the Moon" circle = "moon" features_by_level = defaultdict(list) features_by_level[2] = [features.CombatWildShape, features.CircleForms] features_by_level[6] = [features.PrimalStrike] features_by_level[10] = [features.ElementalWildShape] features_by_level[14] = [features.ThousandForms] # XGTE class DreamsCircle(SubClass): """Druids who are members of the Circle of Dreams hail from regions that have strong ties to the Feywild and its dreamlike realms. The druids' guardianship of the natural world makes for a natural alliance between them and good-aligned fey. These druids seek to fill the world with dreamy wonder. Their magic mends wounds and brings joy to downcast hearts, and the realms they protect are gleaming, fruitful places, where dream and reality blur together and where the weary can find rest. """ name = "Circle of Dreams" circle = "dreams" features_by_level = defaultdict(list) features_by_level[2] = [features.BalmOfTheSummerCourt] features_by_level[6] = [features.HearthOfMoonlightAndShadow] features_by_level[10] = [features.HiddenPaths] features_by_level[14] = [features.WalkerInDreams] class ShepherdCircle(SubClass): """Druids of the Circle of the Shepherd commune with the spirits of nature, especially the spirits of beasts and the fey, and call to those spirits for aid. These druids recognize that all living things play a role in the natural world, yet they focus on protecting animals and fey creatures that have difficulty defending themselves. Shepherds, as they are known, see such creatures as their charges. They ward off monsters that threaten them, rebuke hunters who kill more prey than necessary, and prevent civilization from encroaching on rare animal habitats and on sites sacred to the fey. Many of these druids are happiest far from cities and towns, content to spend their days in the company of animals and the fey creatures of the wilds. Members of this circle become adventurers to oppose forces that threaten their charges or to seek knowledge and power that will help them safeguard their charges better. Wherever these druids go, the spirits of the wilderness are with them """ name = "Circle of the Shepherd" circle = "shepherd" languages = ("Sylvan",) features_by_level = defaultdict(list) features_by_level[2] = [features.SpeechOfTheWoods, features.SpiritTotem] features_by_level[6] = [features.MightySummoner] features_by_level[10] = [features.GuardianSpirit] features_by_level[14] = [features.FaithfulSummons] # GGTR class SporesCircle(SubClass): """Druids of the Circle of Spores find beauty in decay. They see within mold and other fungi the ability to transform lifeless material into abundant, ableit somewhat strange, life. These druids believe that life and death are parts of a grand cycle, with one leading to the other and then back again. Death isn't the end of life, but instead a change of state that sees life shift into a new form. Druids of this circle have a complex relationship with the undead. Unlike most other druids, they see nothing inherently wrong with undeath, which they consider to be a companion to life and death. But these druids believe that the natural cycle is heathiest when each segment of it is vibrant and changing. Undead that seek to replace all life with undeath, or that try to avoid passing to a final rest, violate the cycle and must be thwarted. """ name = "Circle of Spores" circle = "spores" features_by_level = defaultdict(list) features_by_level[2] = [ features.CircleSpells, features.HaloOfSpores, features.SymbioticEntity, ] features_by_level[6] = [features.FungalInfestation] features_by_level[10] = [features.SpreadingSpores] features_by_level[14] = [features.FungalBody] class Druid(CharClass): name = "Druid" _wild_shapes = () _circle = "" hit_dice_faces = 8 subclass_select_level = 2 saving_throw_proficiencies = ("intelligence", "wisdom") primary_abilities = ("wisdom",) languages = "Druidic" _proficiencies_text = ( "Light armor", "medium armor", "shields (druids will not wear armor or use shields made of metal)", "clubs", "daggers", "darts", "javelins", "maces", "quarterstaffs", "scimitars", "sickles", "slings", "spears", ) weapon_proficiencies = ( weapons.Club, weapons.Dagger, weapons.Dart, weapons.Javelin, weapons.Mace, weapons.Quarterstaff, weapons.Scimitar, weapons.Sickle, weapons.Sling, weapons.Spear, ) multiclass_weapon_proficiencies = () _multiclass_proficiencies_text = ( "Light armor", "medium armor", "shields (druids will not wear armor or use shields made of metal)", ) class_skill_choices = ( "Arcana", "Animal Handling", "Insight", "Medicine", "Nature", "Perception", "Religion", "Survival", ) features_by_level = defaultdict(list) features_by_level[2] = [features.WildShape] features_by_level[18] = [features.TimelessBody, features.BeastSpells] features_by_level[20] = [features.Archdruid] subclasses_available = ( LandCircle, MoonCircle, DreamsCircle, ShepherdCircle, SporesCircle, ) spellcasting_ability = "wisdom" spell_slots_by_level = { 1: (2, 2, 0, 0, 0, 0, 0, 0, 0, 0), 2: (2, 3, 0, 0, 0, 0, 0, 0, 0, 0), 3: (2, 4, 2, 0, 0, 0, 0, 0, 0, 0), 4: (3, 4, 3, 0, 0, 0, 0, 0, 0, 0), 5: (3, 4, 3, 2, 0, 0, 0, 0, 0, 0), 6: (3, 4, 3, 3, 0, 0, 0, 0, 0, 0), 7: (3, 4, 3, 3, 1, 0, 0, 0, 0, 0), 8: (3, 4, 3, 3, 2, 0, 0, 0, 0, 0), 9: (3, 4, 3, 3, 3, 1, 0, 0, 0, 0), 10: (4, 4, 3, 3, 3, 2, 0, 0, 0, 0), 11: (4, 4, 3, 3, 3, 2, 1, 0, 0, 0), 12: (4, 4, 3, 3, 3, 2, 1, 0, 0, 0), 13: (4, 4, 3, 3, 3, 2, 1, 1, 0, 0), 14: (4, 4, 3, 3, 3, 2, 1, 1, 0, 0), 15: (4, 4, 3, 3, 3, 2, 1, 1, 1, 0), 16: (4, 4, 3, 3, 3, 2, 1, 1, 1, 0), 17: (4, 4, 3, 3, 3, 2, 1, 1, 1, 1), 18: (4, 4, 3, 3, 3, 3, 1, 1, 1, 1), 19: (4, 4, 3, 3, 3, 3, 2, 1, 1, 1), 20: (4, 4, 3, 3, 3, 3, 2, 2, 1, 1), } def select_subclass(self, subclass_str): if subclass_str in ["", "None", "none", None]: return None for sc in self.subclasses_available: if (subclass_str.lower() == sc.circle.lower()) or ( subclass_str.lower() in sc.name.lower() ): return sc(owner=self.owner) return None @property def circle(self): if isinstance(self.subclass, SubClass): return self.subclass.circle.lower() else: return self._circle @circle.setter def circle(self, circle_str): if isinstance(self.subclass, SubClass): self.subclass = self.select_subclass(circle_str) else: self._circle = circle_str @property def all_wild_shapes(self): """Return all wild shapes, regardless of validity.""" return self._wild_shapes @property def wild_shapes(self): """Return a list of valid wild shapes for this Druid.""" valid_shapes = [] for shape in self._wild_shapes: # Check if shape can be transformed into if self.can_assume_shape(shape): valid_shapes.append(shape) return valid_shapes @wild_shapes.setter 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 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 def can_assume_shape(self, shape: monsters.Monster) -> bool: """Determine if a given shape meets the requirements for transforming. See Pg 66 of player's handbook. Parameters ========== shape A monster that the Druid wishes to transform into. Returns ======= can_assume True if the monster meets the C/R, swim and flying speed restrictions. """ # Determine acceptable states based on druid level if self.level < 2: max_cr = -1 max_swim = 0 max_fly = 0 elif self.level < 4: max_cr = 1 / 4 max_swim = 0 max_fly = 0 elif self.level < 8: max_cr = 1 / 2 max_swim = None max_fly = 0 else: max_cr = 1 max_swim = None max_fly = None # Make adjustments for moon circle druids if self.circle == "moon": if 2 <= self.level < 6: max_cr = 1 elif self.level >= 6: max_cr = math.floor(self.level / 3) # Check if the beast shape can be assumed valid_cr = max_cr is None or shape.challenge_rating <= max_cr valid_swim = max_swim is None or shape.swim_speed <= max_swim valid_fly = max_fly is None or shape.fly_speed <= max_fly can_assume = shape.is_beast and valid_cr and valid_swim and valid_fly return can_assume @property def spells(self): return tuple(S() for S in self.spells_prepared) @spells.setter def spells(self, val): if len(val) > 0: warnings.warn( "Druids cannot learn spells, use ``spells_prepared`` instead.", RuntimeWarning, )