Beispiel #1
0
 def attacks(self):
     a = AttackList()
     seen = set()
     for c in self.get_combatants():
         for atk in c.attacks:
             if atk in seen:
                 continue
             seen.add(atk)
             atk_copy = Attack.copy(atk)
             atk_copy.name = f"{atk.name} ({c.name})"
             a.append(atk_copy)
     return a
Beispiel #2
0
 def from_data(cls, d):
     ability_scores = BaseStats.from_dict(d['ability_scores'])
     saves = Saves.from_dict(d['saves'])
     skills = Skills.from_dict(d['skills'])
     display_resists = Resistances.from_dict(d['display_resists'], smart=False)
     traits = [Trait(**t) for t in d['traits']]
     actions = [Trait(**t) for t in d['actions']]
     reactions = [Trait(**t) for t in d['reactions']]
     legactions = [Trait(**t) for t in d['legactions']]
     bonus_actions = [Trait(**t) for t in d.get('bonus_actions', [])]
     mythic_actions = [Trait(**t) for t in d.get('mythic_actions', [])]
     resistances = Resistances.from_dict(d['resistances'])
     attacks = AttackList.from_dict(d['attacks'])
     if d['spellbook'] is not None:
         spellcasting = MonsterSpellbook.from_dict(d['spellbook'])
     else:
         spellcasting = None
     return cls(d['name'], d['size'], d['race'], d['alignment'], d['ac'], d['armortype'], d['hp'], d['hitdice'],
                d['speed'], ability_scores, saves, skills, d['senses'], display_resists, d['condition_immune'],
                d['languages'], d['cr'], d['xp'],
                traits=traits, actions=actions, reactions=reactions, legactions=legactions,
                bonus_actions=bonus_actions, mythic_actions=mythic_actions,
                la_per_round=d['la_per_round'], passiveperc=d['passiveperc'],
                # augmented
                resistances=resistances, attacks=attacks, proper=d['proper'], image_url=d['image_url'],
                spellcasting=spellcasting, token_free_fp=d['token_free'], token_sub_fp=d['token_sub'],
                # sourcing
                source=d['source'], entity_id=d['id'], page=d['page'], url=d['url'], is_free=d['isFree'])
Beispiel #3
0
    def get_attacks(self):
        """Returns an attacklist"""
        if self.character_data is None:
            raise Exception('You must call get_character() first.')
        attacks = AttackList()
        used_names = set()

        def extend(parsed_attacks):
            for atk in parsed_attacks:
                if atk.name in used_names:
                    num = 2
                    while f"{atk.name}{num}" in used_names:
                        num += 1
                    atk.name = f"{atk.name}{num}"
            attacks.extend(parsed_attacks)
            used_names.update(a.name for a in parsed_attacks)

        for src in self.character_data['actions'].values():
            for action in src:
                if action['displayAsAttack']:
                    extend(self.parse_attack(action, "action"))
        for action in self.character_data['customActions']:
            extend(self.parse_attack(action, "customAction"))
        for item in self.character_data['inventory']:
            if item['equipped'] and (item['definition']['filterType']
                                     == "Weapon"
                                     or item.get('displayAsAttack')):
                extend(self.parse_attack(item, "item"))

        if 'Unarmed Strike' not in [a.name for a in attacks]:
            extend(self.parse_attack(None, 'unarmed'))
        return attacks
Beispiel #4
0
    async def attack_import(self, ctx, *, data):
        """
        Imports an attack from JSON exported from the Avrae Dashboard.
        """
        character: Character = await Character.from_ctx(ctx)

        try:
            attack_json = json.loads(data)
        except json.decoder.JSONDecodeError:
            return await ctx.send("This is not a valid attack.")

        if not isinstance(attack_json, list):
            attack_json = [attack_json]

        try:
            attacks = AttackList.from_dict(attack_json)
        except:
            return await ctx.send("This is not a valid attack.")

        conflicts = [a for a in character.overrides.attacks if a.name.lower() in [new.name.lower() for new in attacks]]
        if conflicts:
            if await confirm(ctx, f"This will overwrite {len(conflicts)} attacks with the same name "
                                  f"({', '.join(c.name for c in conflicts)}). Continue?"):
                for conflict in conflicts:
                    character.overrides.attacks.remove(conflict)
            else:
                return await ctx.send("Okay, aborting.")

        character.overrides.attacks.extend(attacks)
        await character.commit(ctx)

        out = f"Imported {len(attacks)} attacks:\n{attacks.build_str(character)}"
        await ctx.send(out)
Beispiel #5
0
    def __init__(self,
                 name: str,
                 stats: BaseStats = None,
                 levels: Levels = None,
                 attacks: AttackList = None,
                 skills: Skills = None,
                 saves: Saves = None,
                 resistances: Resistances = None,
                 spellbook: Spellbook = None,
                 ac: int = None,
                 max_hp: int = None,
                 hp: int = None,
                 temp_hp: int = 0,
                 creature_type: str = None):
        if stats is None:
            stats = BaseStats.default()
        if levels is None:
            levels = Levels()
        if attacks is None:
            attacks = AttackList()
        if skills is None:
            skills = Skills.default(stats)
        if saves is None:
            saves = Saves.default(stats)
        if resistances is None:
            resistances = Resistances()
        if spellbook is None:
            spellbook = Spellbook()
        if hp is None:
            hp = max_hp

        # ===== static =====
        # all actors have a name
        self._name = name
        # ability scores
        self._stats = stats
        # at least a total level
        self._levels = levels
        # attacks - list of automation actions
        self._attacks = attacks
        # skill profs/saving throws
        self._skills = skills
        self._saves = saves
        # defensive resistances
        self._resistances = resistances
        # assigned by combatant type
        self._creature_type = creature_type

        # ===== dynamic =====
        # hp/ac
        self._ac = ac
        self._max_hp = max_hp
        self._hp = hp
        self._temp_hp = temp_hp

        # spellbook
        self._spellbook = spellbook
Beispiel #6
0
    def get_attacks(self):
        """Returns a list of dicts of all of the character's attacks."""
        if self.character_data is None: raise Exception('You must call get_character() first.')
        character = self.character_data
        attacks = AttackList()
        atk_names = set()
        for attack in character.get('attacks', []):
            if attack.get('enabled') and not attack.get('removed'):
                atk = self.parse_attack(attack)

                # unique naming
                atk_num = 2
                if atk.name in atk_names:
                    while f"{atk.name}{atk_num}" in atk_names:
                        atk_num += 1
                    atk.name = f"{atk.name}{atk_num}"
                atk_names.add(atk.name)

                attacks.append(atk)
        return attacks
Beispiel #7
0
 def from_bestiary(cls, data):
     for key in ('traits', 'actions', 'reactions', 'legactions'):
         data[key] = [Trait(**t) for t in data.pop(key)]
     data['spellcasting'] = Spellbook.from_dict(data.pop('spellbook'))
     data['saves'] = Saves.from_dict(data['saves'])
     data['skills'] = Skills.from_dict(data['skills'])
     data['ability_scores'] = BaseStats.from_dict(data['ability_scores'])
     data['attacks'] = AttackList.from_dict(data['attacks'])
     if 'display_resists' in data:
         data['display_resists'] = Resistances.from_dict(
             data['display_resists'])
     return cls(**data)
Beispiel #8
0
 def get_attacks(self):
     """Returns an attack list."""
     if self.character_data is None: raise Exception('You must call get_character() first.')
     attacks = AttackList()
     for rownum in range(32, 37):  # sht1, R32:R36
         a = self.parse_attack(f"R{rownum}", f"Y{rownum}", f"AC{rownum}")
         if a is not None:
             attacks.append(a)
     if self.additional:
         for rownum in range(3, 14):  # sht2, B3:B13; W3:W13
             additional = self.parse_attack(f"B{rownum}", f"I{rownum}", f"M{rownum}", self.additional)
             other = self.parse_attack(f"W{rownum}", f"AD{rownum}", f"AH{rownum}", self.additional)
             if additional is not None:
                 attacks.append(additional)
             if other is not None:
                 attacks.append(other)
     return attacks
Beispiel #9
0
    def _get_attacks_and_actions(self):
        """
        :rtype: tuple[AttackList, Actions]
        """
        attacks = self._process_attacks(
        )  # this returns all attacks regardless of their status
        actions, action_grantor_ids = self._process_actions(
        )  # this skips customized actions with displayAsAttack

        filtered_attacks = [
            attack for ((id, type_id), attack) in attacks
            if (id, type_id) not in action_grantor_ids
        ]

        return AttackList(filtered_attacks), actions
Beispiel #10
0
 def from_bestiary(cls, data, source):
     for key in ('traits', 'actions', 'reactions', 'legactions'):
         data[key] = [Trait(**t) for t in data.pop(key)]
     data['spellcasting'] = MonsterSpellbook.from_dict(data.pop('spellbook'))
     data['saves'] = Saves.from_dict(data['saves'])
     data['skills'] = Skills.from_dict(data['skills'])
     data['ability_scores'] = BaseStats.from_dict(data['ability_scores'])
     data['attacks'] = AttackList.from_dict(data['attacks'])
     if 'resistances' in data:
         data['resistances'] = Resistances.from_dict(data['resistances'])
     if 'display_resists' in data:
         data['display_resists'] = Resistances.from_dict(data['display_resists'], smart=False)
     else:
         data['display_resists'] = Resistances()
     if 'source' in data:
         del data['source']
     return cls(homebrew=True, source=source, **data)
Beispiel #11
0
    def _get_attacks(self):
        """Returns an attacklist"""
        attacks = AttackList()
        used_names = set()

        def append(atk):
            if atk.name in used_names:
                num = 2
                while f"{atk.name}{num}" in used_names:
                    num += 1
                atk.name = f"{atk.name}{num}"
            attacks.append(atk)
            used_names.add(atk.name)

        for attack in self.character_data['attacks']:
            append(self._transform_attack(attack))

        return attacks
Beispiel #12
0
    async def attack_import(self, ctx, *, data: str):
        """
        Imports an attack from JSON or YAML exported from the Avrae Dashboard.
        """
        # strip any code blocks
        if data.startswith(('```\n', '```json\n', '```yaml\n', '```yml\n',
                            '```py\n')) and data.endswith('```'):
            data = '\n'.join(data.split('\n')[1:]).rstrip('`\n')

        character: Character = await Character.from_ctx(ctx)

        try:
            attack_json = yaml.safe_load(data)
        except yaml.YAMLError:
            return await ctx.send("This is not a valid attack.")

        if not isinstance(attack_json, list):
            attack_json = [attack_json]

        try:
            attacks = AttackList.from_dict(attack_json)
        except Exception:
            return await ctx.send("This is not a valid attack.")

        conflicts = [
            a for a in character.overrides.attacks
            if a.name.lower() in [new.name.lower() for new in attacks]
        ]
        if conflicts:
            if await confirm(
                    ctx,
                    f"This will overwrite {len(conflicts)} attacks with the same name "
                    f"({', '.join(c.name for c in conflicts)}). Continue? (Reply with yes/no)"
            ):
                for conflict in conflicts:
                    character.overrides.attacks.remove(conflict)
            else:
                return await ctx.send("Okay, aborting.")

        character.overrides.attacks.extend(attacks)
        await character.commit(ctx)

        out = f"Imported {len(attacks)} attacks:\n{attacks.build_str(character)}"
        await ctx.send(out)
Beispiel #13
0
 def attacks(self):
     if 'attacks' not in self._cache:
         # attacks granted by effects are cached so that the same object is referenced in initTracker (#950)
         self._cache['attacks'] = self._attacks + AttackList.from_dict(self.active_effects('attack'))
     return self._cache['attacks']
Beispiel #14
0
def _monster_factory(data, bestiary_name):
    ability_scores = BaseStats(data['stats']['proficiencyBonus'] or 0,
                               data['stats']['abilityScores']['strength'] or 10,
                               data['stats']['abilityScores']['dexterity'] or 10,
                               data['stats']['abilityScores']['constitution'] or 10,
                               data['stats']['abilityScores']['intelligence'] or 10,
                               data['stats']['abilityScores']['wisdom'] or 10,
                               data['stats']['abilityScores']['charisma'] or 10)
    cr = {0.125: '1/8', 0.25: '1/4', 0.5: '1/2'}.get(data['stats']['challengeRating'],
                                                     str(data['stats']['challengeRating']))
    num_hit_die = data['stats']['numHitDie']
    hit_die_size = data['stats']['hitDieSize']
    con_by_level = num_hit_die * ability_scores.get_mod('con')
    hp = floor(((hit_die_size + 1) / 2) * num_hit_die) + con_by_level
    hitdice = f"{num_hit_die}d{hit_die_size} + {con_by_level}"

    proficiency = data['stats']['proficiencyBonus']
    if proficiency is None:
        raise ExternalImportError(f"Monster's proficiency bonus is nonexistent ({data['name']}).")

    skills = Skills.default(ability_scores)
    skill_updates = {}
    for skill in data['stats']['skills']:
        name = spaced_to_camel(skill['name'])
        if skill['proficient']:
            mod = skills[name].value + proficiency
        else:
            mod = skill.get('value')
        if mod is not None:
            skill_updates[name] = mod
    skills.update(skill_updates)

    saves = Saves.default(ability_scores)
    save_updates = {}
    for save in data['stats']['savingThrows']:
        name = save['ability'].lower() + 'Save'
        if save['proficient']:
            mod = saves.get(name).value + proficiency
        else:
            mod = save.get('value')
        if mod is not None:
            save_updates[name] = mod
    saves.update(save_updates)

    attacks = []
    traits, atks = parse_critterdb_traits(data, 'additionalAbilities')
    attacks.extend(atks)
    actions, atks = parse_critterdb_traits(data, 'actions')
    attacks.extend(atks)
    reactions, atks = parse_critterdb_traits(data, 'reactions')
    attacks.extend(atks)
    legactions, atks = parse_critterdb_traits(data, 'legendaryActions')
    attacks.extend(atks)

    attacks = AttackList.from_dict(attacks)
    spellcasting = parse_critterdb_spellcasting(traits, ability_scores)

    resistances = Resistances.from_dict(dict(vuln=data['stats']['damageVulnerabilities'],
                                             resist=data['stats']['damageResistances'],
                                             immune=data['stats']['damageImmunities']))

    return Monster(name=data['name'], size=data['stats']['size'], race=data['stats']['race'],
                   alignment=data['stats']['alignment'],
                   ac=data['stats']['armorClass'], armortype=data['stats']['armorType'], hp=hp, hitdice=hitdice,
                   speed=data['stats']['speed'], ability_scores=ability_scores, saves=saves, skills=skills,
                   senses=', '.join(data['stats']['senses']), resistances=resistances, display_resists=resistances,
                   condition_immune=data['stats']['conditionImmunities'], languages=data['stats']['languages'], cr=cr,
                   xp=data['stats']['experiencePoints'], traits=traits, actions=actions, reactions=reactions,
                   legactions=legactions, la_per_round=data['stats']['legendaryActionsPerRound'],
                   attacks=attacks, proper=data['flavor']['nameIsProper'], image_url=data['flavor']['imageUrl'],
                   spellcasting=spellcasting, homebrew=True, source=bestiary_name)
Beispiel #15
0
    def __init__(self,
                 name: str,
                 size: str,
                 race: str,
                 alignment: str,
                 ac: int,
                 armortype: str,
                 hp: int,
                 hitdice: str,
                 speed: str,
                 ability_scores: BaseStats,
                 cr: str,
                 xp: int,
                 passiveperc: int = None,
                 senses: str = '',
                 vuln: list = None,
                 resist: list = None,
                 immune: list = None,
                 condition_immune: list = None,
                 saves: Saves = None,
                 skills: Skills = None,
                 languages: list = None,
                 traits: list = None,
                 actions: list = None,
                 reactions: list = None,
                 legactions: list = None,
                 la_per_round=3,
                 srd=True,
                 source='homebrew',
                 attacks: AttackList = None,
                 proper: bool = False,
                 image_url: str = None,
                 spellcasting=None,
                 page=None,
                 display_resists: Resistances = None,
                 **_):
        if vuln is None:
            vuln = []
        if resist is None:
            resist = []
        if immune is None:
            immune = []
        if condition_immune is None:
            condition_immune = []
        if saves is None:
            saves = Saves.default(ability_scores)
        if skills is None:
            skills = Skills.default(ability_scores)
        if languages is None:
            languages = []
        if traits is None:
            traits = []
        if actions is None:
            actions = []
        if reactions is None:
            reactions = []
        if legactions is None:
            legactions = []
        if attacks is None:
            attacks = AttackList()
        if spellcasting is None:
            spellcasting = Spellbook({}, {}, [])
        if passiveperc is None:
            passiveperc = 10 + skills.perception.value

        try:
            levels = Levels({"Monster": spellcasting.caster_level or int(cr)})
        except ValueError:
            levels = None

        resistances = Resistances(vuln=vuln, resist=resist, immune=immune)

        super(Monster, self).__init__(name=name,
                                      stats=ability_scores,
                                      attacks=attacks,
                                      skills=skills,
                                      saves=saves,
                                      resistances=resistances,
                                      spellbook=spellcasting,
                                      ac=ac,
                                      max_hp=hp,
                                      levels=levels)
        self.size = size
        self.race = race
        self.alignment = alignment
        self.armortype = armortype
        self.hitdice = hitdice
        self.speed = speed
        self.cr = cr
        self.xp = xp
        self.passive = passiveperc
        self.senses = senses
        self.condition_immune = condition_immune
        self.languages = languages
        self.traits = traits
        self.actions = actions
        self.reactions = reactions
        self.legactions = legactions
        self.la_per_round = la_per_round
        self.srd = srd
        self.source = source
        self.proper = proper
        self.image_url = image_url
        self.page = page  # this should really be by source, but oh well
        # resistances including notes, e.g. "Bludgeoning from nonmagical weapons"
        self._displayed_resistances = display_resists or resistances
Beispiel #16
0
    def __init__(
            self,
            name: str,
            size: str,
            race: str,
            alignment: str,
            ac: int,
            armortype: str,
            hp: int,
            hitdice: str,
            speed: str,
            ability_scores: BaseStats,
            saves: Saves,
            skills: Skills,
            senses: str,
            display_resists: Resistances,
            condition_immune: list,
            languages: list,
            cr: str,
            xp: int,
            # optional
            traits: list = None,
            actions: list = None,
            reactions: list = None,
            legactions: list = None,
            la_per_round=3,
            passiveperc: int = None,
            # augmented
            resistances: Resistances = None,
            attacks: AttackList = None,
            proper: bool = False,
            image_url: str = None,
            spellcasting=None,
            # sourcing
            homebrew=False,
            **kwargs):
        if traits is None:
            traits = []
        if actions is None:
            actions = []
        if reactions is None:
            reactions = []
        if legactions is None:
            legactions = []
        if attacks is None:
            attacks = AttackList()
        if spellcasting is None:
            spellcasting = MonsterSpellbook()
        if passiveperc is None:
            passiveperc = 10 + skills.perception.value

        # old/new resist handling
        if resistances is None:
            # fall back to old-style resistances (deprecated)
            vuln = kwargs.get('vuln', [])
            resist = kwargs.get('resist', [])
            immune = kwargs.get('immune', [])
            resistances = Resistances.from_dict(
                dict(vuln=vuln, resist=resist, immune=immune))

        try:
            levels = Levels({"Monster": spellcasting.caster_level or int(cr)})
        except ValueError:
            levels = None

        Sourced.__init__(self,
                         'monster',
                         homebrew,
                         source=kwargs['source'],
                         entity_id=kwargs.get('entity_id'),
                         page=kwargs.get('page'),
                         url=kwargs.get('url'),
                         is_free=kwargs.get('is_free'))
        StatBlock.__init__(self,
                           name=name,
                           stats=ability_scores,
                           attacks=attacks,
                           skills=skills,
                           saves=saves,
                           resistances=resistances,
                           spellbook=spellcasting,
                           ac=ac,
                           max_hp=hp,
                           levels=levels)
        self.size = size
        self.race = race
        self.alignment = alignment
        self.armortype = armortype
        self.hitdice = hitdice
        self.speed = speed
        self.cr = cr
        self.xp = xp
        self.passive = passiveperc
        self.senses = senses
        self.condition_immune = condition_immune
        self.languages = languages
        self.traits = traits
        self.actions = actions
        self.reactions = reactions
        self.legactions = legactions
        self.la_per_round = la_per_round
        self.proper = proper
        self.image_url = image_url
        # resistances including notes, e.g. "Bludgeoning from nonmagical weapons"
        self._displayed_resistances = display_resists or resistances
Beispiel #17
0
    def from_data(cls, data):
        # print(f"Parsing {data['name']}")
        _type = parse_type(data['type'])
        alignment = parse_alignment(data['alignment'])
        speed = parse_speed(data['speed'])
        ac = data['ac']['ac']
        armortype = data['ac'].get('armortype') or None
        if not 'special' in data['hp']:
            hp = data['hp']['average']
            hitdice = data['hp']['formula']
        else:
            hp = 0
            hitdice = data['hp']['special']
        scores = BaseStats(0, data['str'] or 10, data['dex'] or 10, data['con']
                           or 10, data['int'] or 10, data['wis'] or 10,
                           data['cha'] or 10)
        if isinstance(data['cr'], dict):
            cr = data['cr']['cr']
        else:
            cr = data['cr']

        # resistances
        vuln = parse_resists(data['vulnerable'],
                             notated=False) if 'vulnerable' in data else None
        resist = parse_resists(data['resist'],
                               notated=False) if 'resist' in data else None
        immune = parse_resists(data['immune'],
                               notated=False) if 'immune' in data else None

        display_resists = Resistances(*[
            parse_resists(data.get(r))
            for r in ('resist', 'immune', 'vulnerable')
        ])

        condition_immune = data.get('conditionImmune',
                                    []) if 'conditionImmune' in data else None

        languages = data.get('languages',
                             '').split(', ') if 'languages' in data else None

        traits = [Trait(t['name'], t['text']) for t in data.get('trait', [])]
        actions = [Trait(t['name'], t['text']) for t in data.get('action', [])]
        legactions = [
            Trait(t['name'], t['text']) for t in data.get('legendary', [])
        ]
        reactions = [
            Trait(t['name'], t['text']) for t in data.get('reaction', [])
        ]

        skills = Skills.default(scores)
        skills.update(data['skill'])

        saves = Saves.default(scores)
        saves.update(data['save'])

        scores.prof_bonus = _calc_prof(scores, saves, skills)

        source = data['source']
        proper = bool(data.get('isNamedCreature') or data.get('isNPC'))

        attacks = AttackList.from_dict(data.get('attacks', []))
        spellcasting = data.get('spellcasting', {})
        spells = [SpellbookSpell(s) for s in spellcasting.get('spells', [])]
        spellbook = Spellbook({}, {}, spells, spellcasting.get('dc'),
                              spellcasting.get('attackBonus'),
                              spellcasting.get('casterLevel', 1))

        return cls(data['name'],
                   parsesize(data['size']),
                   _type,
                   alignment,
                   ac,
                   armortype,
                   hp,
                   hitdice,
                   speed,
                   scores,
                   cr,
                   xp_by_cr(cr),
                   data['passive'],
                   data.get('senses', ''),
                   vuln,
                   resist,
                   immune,
                   condition_immune,
                   saves,
                   skills,
                   languages,
                   traits,
                   actions,
                   reactions,
                   legactions,
                   3,
                   data.get('srd', False),
                   source,
                   attacks,
                   spellcasting=spellbook,
                   page=data.get('page'),
                   proper=proper,
                   display_resists=display_resists)