Esempio n. 1
0
    def _transform_attack(attack) -> Attack:
        desc = html_to_md(attack['desc'])

        if attack['saveDc'] is not None and attack['saveStat'] is not None:
            stat = constants.STAT_ABBREVIATIONS[attack['saveStat'] - 1]
            for_half = desc and 'half' in desc

            the_attack = automation.Save(stat,
                                         fail=[automation.Damage("{damage}")],
                                         success=[] if not for_half else
                                         [automation.Damage("{damage}/2")],
                                         dc=str(attack['saveDc']))

            # attack, then save
            if attack['toHit'] is not None:
                the_attack = automation.Attack(hit=[the_attack],
                                               attackBonus=str(
                                                   attack['toHit']),
                                               miss=[])

            # target and damage meta
            target = automation.Target('each', [the_attack])
            damage_roll = automation.Roll(attack['damage'] or '0', 'damage')
            effects = [damage_roll, target]
            # description text
            if desc:
                effects.append(automation.Text(desc))

            return Attack(attack['name'], automation.Automation(effects))
        else:
            return Attack.new(attack['name'], attack['toHit'], attack['damage']
                              or '0', desc)
Esempio n. 2
0
def parse_critterdb_traits(data, key):
    traits = []
    attacks = []
    for trait in data['stats'][key]:
        name = trait['name']
        raw = trait['description']

        overrides = list(AVRAE_ATTACK_OVERRIDES_RE.finditer(raw))
        raw_atks = list(ATTACK_RE.finditer(raw))
        raw_damage = list(JUST_DAMAGE_RE.finditer(raw))

        filtered = AVRAE_ATTACK_OVERRIDES_RE.sub('', raw)
        desc = markdownify(filtered).strip()

        if overrides:
            for override in overrides:
                if override.group('simple'):
                    attacks.append(
                        Attack.from_dict({
                            'name': override.group(2) or name,
                            'attackBonus': override.group(3) or None,
                            'damage': override.group(4) or None,
                            'details': desc
                        }))
                elif (freeform_override := override.group('freeform')):
                    try:
                        attack_yaml = yaml.safe_load(freeform_override)
                    except yaml.YAMLError:
                        raise ExternalImportError(
                            f"Monster had an invalid automation YAML ({data['name']}: {name})"
                        )
                    if not isinstance(attack_yaml, list):
                        attack_yaml = [attack_yaml]
                    for atk in attack_yaml:
                        if isinstance(atk, dict):
                            atk['name'] = atk_name = atk.get('name') or name
                            try:
                                attacks.append(Attack.from_dict(atk))
                            except Exception:
                                raise ExternalImportError(
                                    f"An automation YAML contained an invalid attack ({data['name']}: {atk_name})"
                                )
                        else:
                            raise ExternalImportError(
                                f"An automation YAML contained an invalid attack ({data['name']}: {name})"
                            )
                # else: empty override, so skip this attack.
        elif raw_atks:
            for atk in raw_atks:
                attack_bonus = atk.group('attackBonus').lstrip('+')

                # Bonus damage
                bonus = ""
                if (bonus_damage_type := atk.group('damageTypeBonus')) and \
                        (bonus_damage := atk.group('damageBonusInt') or atk.group('damageBonusDice')):
Esempio n. 3
0
    async def attack_add(self, ctx, name, *args):
        """
        Adds an attack to the active character.
        __Arguments__
        -d [damage]: How much damage the attack should do.
        -b [to-hit]: The to-hit bonus of the attack.
        -desc [description]: A description of the attack.
        """
        character: Character = await Character.from_ctx(ctx)
        parsed = argparse(args)

        attack = Attack.new(name,
                            bonus_calc=parsed.join('b', '+'),
                            damage_calc=parsed.join('d', '+'),
                            details=parsed.join('desc', '\n'))

        conflict = next((a for a in character.overrides.attacks
                         if a.name.lower() == attack.name.lower()), None)
        if conflict:
            character.overrides.attacks.remove(conflict)
        character.overrides.attacks.append(attack)
        await character.commit(ctx)

        out = f"Created attack {attack.name}!"
        if conflict:
            out += f" Removed a duplicate attack."
        await ctx.send(out)
Esempio n. 4
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
Esempio n. 5
0
    async def attack_add(self, ctx, name, *args):
        """
        Adds an attack to the active character.
        __Valid Arguments__
        -d <damage> - How much damage the attack should do.
        -b <to-hit> - The to-hit bonus of the attack.
        -desc <description> - A description of the attack.
        -verb <verb> - The verb to use for this attack. (e.g. "Padellis <verb> a dagger!")
        proper - This attack's name is a proper noun.
        -criton <#> - This attack crits on a number other than a natural 20.
        -phrase <text> - Some flavor text to add to each attack with this attack.
        -thumb <image url> - The attack's image.
        -c <extra crit damage> - How much extra damage (beyond doubling dice) this attack does on a crit.
        """
        character: Character = await Character.from_ctx(ctx)
        parsed = argparse(args)

        attack = Attack.new(name,
                            bonus_calc=parsed.join('b', '+'),
                            damage_calc=parsed.join('d', '+'),
                            details=parsed.join('desc', '\n'),
                            verb=parsed.last('verb'),
                            proper=parsed.last('proper', False, bool),
                            criton=parsed.last('criton', type_=int),
                            phrase=parsed.join('phrase', '\n'),
                            thumb=parsed.last('thumb'),
                            extra_crit_damage=parsed.last('c'))

        conflict = next((a for a in character.overrides.attacks
                         if a.name.lower() == attack.name.lower()), None)
        if conflict:
            if await confirm(
                    ctx,
                    "This will overwrite an attack with the same name. Continue? (Reply with yes/no)"
            ):
                character.overrides.attacks.remove(conflict)
            else:
                return await ctx.send("Okay, aborting.")
        character.overrides.attacks.append(attack)
        await character.commit(ctx)

        out = f"Created attack {attack.name}!"
        if conflict:
            out += f" Removed a duplicate attack."
        await ctx.send(out)
Esempio n. 6
0
    def parse_attack(self, name_index, bonus_index, damage_index, sheet=None):
        """Calculates and returns a dict."""
        if self.character_data is None:
            raise Exception('You must call get_character() first.')

        wksht = sheet or self.character_data

        name = wksht.value(name_index)
        damage = wksht.value(damage_index)
        bonus = wksht.value(bonus_index)
        details = None

        if not name:
            return None

        if not damage:
            damage = None
        else:
            details = None
            if '|' in damage:
                damage, details = damage.split('|', 1)

            dice, comment = get_roll_comment(damage)
            if details:
                details = details.strip()

            if any(d in comment.lower() for d in DAMAGE_TYPES):
                damage = "{}[{}]".format(dice, comment)
            else:
                damage = dice
                if comment.strip() and not details:
                    damage = comment.strip()

        if bonus:
            try:
                bonus = int(bonus)
            except (TypeError, ValueError):
                bonus = None
        else:
            bonus = None

        attack = Attack.new(name, bonus, damage, details)
        return attack
Esempio n. 7
0
    def parse_attack(self, atk_dict) -> Attack:
        """Calculates and returns a dict."""
        if self.character_data is None:
            raise Exception('You must call get_character() first.')

        log.debug(f"Processing attack {atk_dict.get('name')}")

        # setup temporary local vars
        temp_names = {}
        if atk_dict.get('parent', {}).get('collection') == 'Spells':
            spellParentID = atk_dict.get('parent', {}).get('id')
            try:
                spellObj = next(s
                                for s in self.character_data.get('spells', [])
                                if s.get('_id') == spellParentID)
            except StopIteration:
                pass
            else:
                spellListParentID = spellObj.get('parent', {}).get('id')
                try:
                    spellListObj = next(
                        s for s in self.character_data.get('spellLists', [])
                        if s.get('_id') == spellListParentID)
                except StopIteration:
                    pass
                else:
                    try:
                        temp_names['attackBonus'] = int(
                            self.evaluator.eval(
                                spellListObj.get('attackBonus')))
                        temp_names['DC'] = int(
                            self.evaluator.eval(spellListObj.get('saveDC')))
                    except Exception as e:
                        log.debug(f"Exception parsing spellvars: {e}")

        temp_names['rageDamage'] = self.calculate_stat('rageDamage')
        old_names = self.evaluator.names.copy()
        self.evaluator.names.update(temp_names)
        log.debug(f"evaluator tempnames: {temp_names}")

        # attack bonus
        bonus_calc = atk_dict.get('attackBonus',
                                  '').replace('{', '').replace('}', '')
        if not bonus_calc:
            bonus = None
        else:
            try:
                bonus = int(self.evaluator.eval(bonus_calc))
            except:
                bonus = None

        # damage
        def damage_sub(match):
            out = match.group(1)
            try:
                log.debug(f"damage_sub: evaluating {out}")
                return str(self.evaluator.eval(out))
            except Exception as ex:
                log.debug(f"exception in damage_sub: {ex}")
                return match.group(0)

        damage_calc = atk_dict.get('damage', '')
        damage = re.sub(r'{(.*?)}', damage_sub, damage_calc)
        damage = damage.replace('{', '').replace('}', '')
        if not damage:
            damage = None
        else:
            damage += ' [{}]'.format(atk_dict.get('damageType'))

        # details
        details = atk_dict.get('details', None)
        if details:
            details = re.sub(r'{([^{}]*)}', damage_sub, details)

        # build attack
        name = atk_dict['name']
        attack = Attack.new(name, bonus, damage, details)

        self.evaluator.names = old_names

        return attack
Esempio n. 8
0
    def parse_attack(self, atkIn, atkType):
        """Calculates and returns a list of dicts."""
        if self.character_data is None:
            raise Exception('You must call get_character() first.')
        prof = self.get_stats().prof_bonus
        out = []

        def monk_scale():
            monk_level = self.get_levels().get('Monk')
            if not monk_level:
                monk_dice_size = 0
            elif monk_level < 5:
                monk_dice_size = 4
            elif monk_level < 11:
                monk_dice_size = 6
            elif monk_level < 17:
                monk_dice_size = 8
            else:
                monk_dice_size = 10
            return monk_dice_size

        if atkType == 'action':
            if atkIn['dice'] is None:
                return []  # thanks DDB
            isProf = atkIn['isProficient']
            atk_bonus = None
            dmgBonus = None

            dice_size = max(monk_scale(), atkIn['dice']['diceValue'])
            base_dice = f"{atkIn['dice']['diceCount']}d{dice_size}"

            if atkIn["abilityModifierStatId"]:
                atk_bonus = self.stat_from_id(atkIn['abilityModifierStatId'])
                dmgBonus = self.stat_from_id(atkIn['abilityModifierStatId'])

            if atkIn["isMartialArts"] and self.get_levels().get("Monk"):
                atk_bonus = max(atk_bonus,
                                self.stat_from_id(2))  # allow using dex
                dmgBonus = max(dmgBonus, self.stat_from_id(2))

            if isProf and atk_bonus is not None:
                atk_bonus += prof

            if dmgBonus:
                damage = f"{base_dice}+{dmgBonus}[{parse_dmg_type(atkIn)}]"
            else:
                damage = f"{base_dice}[{parse_dmg_type(atkIn)}]"
            attack = Attack.new(atkIn['name'], atk_bonus, damage,
                                atkIn['snippet'])
            out.append(attack)
        elif atkType == 'customAction':
            isProf = atkIn['isProficient']
            dmgBonus = (atkIn['fixedValue'] or 0) + (atkIn['damageBonus'] or 0)
            atk_bonus = None
            if atkIn['statId']:
                atk_bonus = self.stat_from_id(atkIn['statId']) + (
                    prof if isProf else 0) + (atkIn['toHitBonus'] or 0)
                dmgBonus = (atkIn['fixedValue'] or 0) + self.stat_from_id(
                    atkIn['statId']) + (atkIn['damageBonus'] or 0)

            if atkIn['attackSubtype'] == 3:  # natural weapons
                if atk_bonus is not None:
                    atk_bonus += self.get_stat('natural-attacks')
                dmgBonus += self.get_stat('natural-attacks-damage')

            damage = f"{atkIn['diceCount']}d{atkIn['diceType']}+{dmgBonus}[{parse_dmg_type(atkIn)}]"
            attack = Attack.new(atkIn['name'], atk_bonus, damage,
                                atkIn['snippet'])
            out.append(attack)
        elif atkType == 'item':
            itemdef = atkIn['definition']
            character_item_bonuses = self.get_specific_item_bonuses(
                atkIn['id'])
            item_specific_bonuses = self._item_modifiers(itemdef)

            item_properties = itemdef['properties'] + [
                collections.defaultdict(lambda: None, name=n)
                for n in item_specific_bonuses['extraProperties']
            ]

            isProf = self.get_prof(
                itemdef['type']) or character_item_bonuses['isPact']

            mod_bonus = self.get_relevant_atkmod(itemdef, item_properties)
            if character_item_bonuses['isHex']:
                mod_bonus = max(mod_bonus, self.stat_from_id(6))
            if itemdef['magic'] and self.get_levels().get(
                    'Artificer') and "Battle Ready" in self._all_features:
                mod_bonus = max(mod_bonus, self.stat_from_id(4))

            magic_bonus = item_specific_bonuses['magicBonus']
            item_dmg_bonus = self.get_stat(f"{itemdef['type'].lower()}-damage")

            dmgBonus = mod_bonus + magic_bonus + character_item_bonuses[
                'damage'] + item_dmg_bonus
            toHitBonus = (
                prof if isProf else
                0) + magic_bonus + character_item_bonuses['attackBonus']

            is_melee = not 'Range' in [p['name'] for p in item_properties]
            is_one_handed = not 'Two-Handed' in [
                p['name'] for p in item_properties
            ]
            is_weapon = itemdef['filterType'] == 'Weapon'
            has_gwf = "Great Weapon Fighting" in self._all_features

            if is_melee and is_one_handed:
                dmgBonus += self.get_stat('one-handed-melee-attacks-damage')

            if not is_melee and is_weapon:
                toHitBonus += self.get_stat('ranged-weapon-attacks')

            if character_item_bonuses[
                    'isPact'] and self._improved_pact_weapon_applies(itemdef):
                dmgBonus += 1
                toHitBonus += 1

            base_dice = None
            if itemdef['fixedDamage']:
                base_dice = itemdef['fixedDamage']
            elif itemdef['damage']:
                if not itemdef['isMonkWeapon']:
                    base_dice = f"{itemdef['damage']['diceCount']}d{itemdef['damage']['diceValue']}"
                else:
                    dice_size = max(monk_scale(),
                                    itemdef['damage']['diceValue'])
                    base_dice = f"{itemdef['damage']['diceCount']}d{dice_size}"

            damage_type = (item_specific_bonuses['replaceDamageType']
                           or itemdef['damageType'] or 'unknown').lower()

            if base_dice and is_melee and has_gwf and not is_one_handed:
                base_dice += "ro<3"

            if base_dice:
                damage = f"{base_dice}+{dmgBonus}[{damage_type}" \
                         f"{'^' if itemdef['magic'] or character_item_bonuses['isPact'] else ''}]"
            else:
                damage = None

            atk_bonus = character_item_bonuses[
                'attackBonusOverride'] or mod_bonus + toHitBonus
            details = character_item_bonuses['note'] or html2text.html2text(
                itemdef['description'], bodywidth=0).strip()
            name = character_item_bonuses['name'] or itemdef['name']
            attack = Attack.new(name, atk_bonus, damage, details)
            out.append(attack)

            if 'Versatile' in [p['name'] for p in item_properties]:
                versDmg = next(p['notes'] for p in item_properties
                               if p['name'] == 'Versatile')
                if has_gwf:
                    versDmg += "ro<3"
                damage = f"{versDmg}+{dmgBonus}[{damage_type}" \
                         f"{'^' if itemdef['magic'] or character_item_bonuses['isPact'] else ''}]"
                attack = Attack.new(f"2-Handed {name}", atk_bonus, damage,
                                    details)
                out.append(attack)
        elif atkType == 'unarmed':
            dice_size = monk_scale()
            ability_mod = self.stat_from_id(
                1) if not self.get_levels().get('Monk') else max(
                    self.stat_from_id(1), self.stat_from_id(2))
            character_item_bonuses = self.get_specific_item_bonuses(
                1)  # magic number: Unarmed Strike ID
            if dice_size:
                dmg = f"1d{dice_size}+{ability_mod}"
            else:
                dmg = 1 + ability_mod

            atk_bonus = character_item_bonuses['attackBonusOverride'] or \
                        (prof + self.get_stat('natural-attacks') + character_item_bonuses['attackBonus'])
            dmg_bonus = self.get_stat(
                'natural-attacks-damage') + character_item_bonuses['damage']
            if dmg_bonus:
                dmg = f"{dmg}+{dmg_bonus}"

            details = character_item_bonuses['note'] or None
            name = character_item_bonuses['name'] or "Unarmed Strike"

            attack = Attack.new(name, ability_mod + atk_bonus,
                                f"{dmg}[bludgeoning]", details)
            out.append(attack)
        return out
Esempio n. 9
0
                attack_bonus = atk.group('attackBonus').lstrip('+')

                # Bonus damage
                bonus = ""
                if (bonus_damage_type := atk.group('damageTypeBonus')) and \
                        (bonus_damage := atk.group('damageBonusInt') or atk.group('damageBonusDice')):
                    bonus = f" + {bonus_damage} [{bonus_damage_type}]"

                # Versatile Attacks
                if (vers_damage_type := atk.group('damageTypeVers')) and \
                        (verse_damage := atk.group('damageIntVers') or atk.group('damageDiceVers')):
                    damage = f"{verse_damage} [{vers_damage_type}]" + bonus
                    attacks.append(
                        Attack.from_dict({
                            'name': f"2 Handed {name}",
                            'attackBonus': attack_bonus,
                            'damage': damage,
                            'details': desc
                        }))

                # Ranged Attacks
                if (ranged_damage_type := atk.group('damageTypeRanged')) and \
                        (ranged_damage := atk.group('damageRangedInt') or atk.group('damageRangedDice')):  # ranged
                    damage = f"{ranged_damage}[{ranged_damage_type}]" + bonus
                    attacks.append(
                        Attack.from_dict({
                            'name': f"Ranged {name}",
                            'attackBonus': attack_bonus,
                            'damage': damage,
                            'details': desc
                        }))
Esempio n. 10
0
    def parse_attack(self, atkIn, atkType):
        """Calculates and returns a list of dicts."""
        if self.character_data is None:
            raise Exception('You must call get_character() first.')
        prof = self.get_stats().prof_bonus
        out = []

        def monk_scale():
            monk_level = self.get_levels().get('Monk')
            if not monk_level:
                monk_dice_size = 0
            elif monk_level < 5:
                monk_dice_size = 4
            elif monk_level < 11:
                monk_dice_size = 6
            elif monk_level < 17:
                monk_dice_size = 8
            else:
                monk_dice_size = 10
            return monk_dice_size

        if atkType == 'action':
            if atkIn['dice'] is None:
                return []  # thanks DDB
            isProf = atkIn['isProficient']
            atkBonus = None
            dmgBonus = None

            dice_size = max(monk_scale(), atkIn['dice']['diceValue'])
            base_dice = f"{atkIn['dice']['diceCount']}d{dice_size}"

            if atkIn["abilityModifierStatId"]:
                atkBonus = self.stat_from_id(atkIn['abilityModifierStatId'])
                dmgBonus = self.stat_from_id(atkIn['abilityModifierStatId'])

            if atkIn["isMartialArts"] and self.get_levels().get("Monk"):
                atkBonus = max(atkBonus,
                               self.stat_from_id(2))  # allow using dex
                dmgBonus = max(dmgBonus, self.stat_from_id(2))

            if isProf:
                atkBonus += prof

            if dmgBonus:
                damage = f"{base_dice}+{dmgBonus}[{parse_dmg_type(atkIn)}]"
            else:
                damage = f"{base_dice}[{parse_dmg_type(atkIn)}]"
            attack = Attack.new(atkIn['name'], atkBonus, damage,
                                atkIn['snippet'])
            out.append(attack)
        elif atkType == 'customAction':
            isProf = atkIn['isProficient']
            dmgBonus = (atkIn['fixedValue'] or 0) + (atkIn['damageBonus'] or 0)
            atkBonus = None
            if atkIn['statId']:
                atkBonus = self.stat_from_id(atkIn['statId']) + (
                    prof if isProf else 0) + (atkIn['toHitBonus'] or 0)
                dmgBonus = (atkIn['fixedValue'] or 0) + self.stat_from_id(
                    atkIn['statId']) + (atkIn['damageBonus'] or 0)

            if atkIn['attackSubtype'] == 3:  # natural weapons
                if atkBonus is not None:
                    atkBonus += self.get_stat('natural-attacks')
                dmgBonus += self.get_stat('natural-attacks-damage')

            damage = f"{atkIn['diceCount']}d{atkIn['diceType']}+{dmgBonus}[{parse_dmg_type(atkIn)}]"
            attack = Attack.new(atkIn['name'], atkBonus, damage,
                                atkIn['snippet'])
            out.append(attack)
        elif atkType == 'item':
            itemdef = atkIn['definition']
            weirdBonuses = self.get_specific_item_bonuses(atkIn['id'])
            isProf = self.get_prof(itemdef['type']) or weirdBonuses['isPact']
            magicBonus = self._item_magic_bonus(itemdef)
            modBonus = self.get_relevant_atkmod(
                itemdef) if not weirdBonuses['isHex'] else self.stat_from_id(6)
            item_dmg_bonus = self.get_stat(f"{itemdef['type'].lower()}-damage")

            dmgBonus = modBonus + magicBonus + weirdBonuses[
                'damage'] + item_dmg_bonus
            toHitBonus = (prof if isProf else
                          0) + magicBonus + weirdBonuses['attackBonus']

            is_melee = not 'Range' in [
                p['name'] for p in itemdef['properties']
            ]
            is_one_handed = not 'Two-Handed' in [
                p['name'] for p in itemdef['properties']
            ]
            is_weapon = itemdef['filterType'] == 'Weapon'

            if is_melee and is_one_handed:
                dmgBonus += self.get_stat('one-handed-melee-attacks-damage')

            if not is_melee and is_weapon:
                toHitBonus += self.get_stat('ranged-weapon-attacks')

            if weirdBonuses['isPact'] and self._improved_pact_weapon_applies(
                    itemdef):
                dmgBonus += 1
                toHitBonus += 1

            base_dice = None
            if itemdef['fixedDamage']:
                base_dice = itemdef['fixedDamage']
            elif itemdef['damage']:
                if not itemdef['isMonkWeapon']:
                    base_dice = f"{itemdef['damage']['diceCount']}d{itemdef['damage']['diceValue']}"
                else:
                    dice_size = max(monk_scale(),
                                    itemdef['damage']['diceValue'])
                    base_dice = f"{itemdef['damage']['diceCount']}d{dice_size}"

            if base_dice:
                damage = f"{base_dice}+{dmgBonus}" \
                         f"[{itemdef['damageType'].lower()}" \
                         f"{'^' if itemdef['magic'] or weirdBonuses['isPact'] else ''}]"
            else:
                damage = None

            atkBonus = weirdBonuses[
                'attackBonusOverride'] or modBonus + toHitBonus
            details = html2text.html2text(itemdef['description'],
                                          bodywidth=0).strip()
            attack = Attack.new(itemdef['name'], atkBonus, damage, details)
            out.append(attack)

            if 'Versatile' in [p['name'] for p in itemdef['properties']]:
                versDmg = next(p['notes'] for p in itemdef['properties']
                               if p['name'] == 'Versatile')
                damage = f"{versDmg}+{dmgBonus}[{itemdef['damageType'].lower()}" \
                         f"{'^' if itemdef['magic'] or weirdBonuses['isPact'] else ''}]"
                attack = Attack.new(f"2-Handed {itemdef['name']}", atkBonus,
                                    damage, details)
                out.append(attack)
        elif atkType == 'unarmed':
            dice_size = monk_scale()
            ability_mod = self.stat_from_id(
                1) if not self.get_levels().get('Monk') else max(
                    self.stat_from_id(1), self.stat_from_id(2))
            atkBonus = prof
            if dice_size:
                dmg = f"1d{dice_size}+{ability_mod}"
            else:
                dmg = 1 + ability_mod

            atkBonus += self.get_stat('natural-attacks')
            natural_bonus = self.get_stat('natural-attacks-damage')
            if natural_bonus:
                dmg = f"{dmg}+{natural_bonus}"

            attack = Attack.new("Unarmed Strike", ability_mod + atkBonus,
                                f"{dmg}[bludgeoning]")
            out.append(attack)
        return out